name: orchestration description: Three-phase build system for Scaffold-ETH 2 dApps. Localhost to testnet to production. When to use Scaffold hooks vs raw wagmi, auto-generated types, and common agent mistakes. metadata: author: Andre Ginja version: 1.0.0
Orchestration: Three-Phase Build System
You are building a dApp with Scaffold-ETH 2. There are exactly three phases. You do not skip phases. You do not deploy to mainnet from localhost. You follow the pipeline.
What You Probably Got Wrong
You used raw wagmi hooks.
useWriteContractfrom wagmi resolves its promise when the transaction is SUBMITTED, not when it is CONFIRMED. Your UI says "Success!" while the tx is still pending. Scaffold-ETH 2 hooks (useScaffoldWriteContract,useScaffoldReadContract) wait for confirmation and handle errors. Use them.You deployed contracts before the UI worked locally. Now you are debugging contract logic AND network issues AND gas problems simultaneously. Phase 1 exists to isolate contract+UI bugs from deployment bugs.
You hardcoded contract addresses. Scaffold-ETH 2 auto-generates deployment files with addresses and ABIs. If you are manually pasting addresses into your frontend, you are fighting the framework.
You committed your deployer private key. It is now on GitHub forever. You need to rotate that key immediately and use environment variables going forward.
You ran
yarn chaininstead ofyarn fork. Without a fork, your local chain has no USDC, no Uniswap, no Aave, no real token balances. If your dApp interacts with ANY existing protocol, you need a fork.
Phase 1: Localhost (Contracts + UI on Anvil)
Goal: Get everything working locally. Zero gas costs. Instant feedback. Fix all logic bugs here.
Start the Local Chain
For a standalone dApp with no external protocol dependencies:
yarn chain
For any dApp that interacts with existing protocols (Uniswap, Aave, Chainlink, any ERC-20):
yarn fork --network mainnet
# or
yarn fork --network base
# or
yarn fork --network optimism
This starts Anvil with a fork of the target chain. All protocol state is available locally. You get 10 pre-funded accounts with 10,000 ETH each.
Deploy Contracts Locally
yarn deploy
This runs your deploy scripts in packages/hardhat/deploy/ (or packages/foundry/script/). It generates:
deployedContracts.ts— auto-generated TypeScript with addresses and ABIs- This file is imported by Scaffold hooks automatically
Never manually edit deployedContracts.ts. It is regenerated on every deploy.
Start the Frontend
yarn start
Frontend runs at http://localhost:3000. Hot reload is enabled. Changes to contracts require re-deploy (yarn deploy) but the frontend picks up new ABIs automatically.
Use Scaffold Hooks, Not Raw Wagmi
This is the single most important rule in this entire skill.
WRONG — raw wagmi:
import { useWriteContract } from "wagmi";
const { writeContract } = useWriteContract();
const handleClick = async () => {
// THIS RESOLVES WHEN TX IS SUBMITTED, NOT CONFIRMED
// Your UI will show success while the tx might still revert
await writeContract({
address: "0x...", // hardcoded — breaks on redeploy
abi: [...], // manually pasted — goes stale
functionName: "mint",
args: [amount],
});
setSuccess(true); // WRONG — tx might not be confirmed yet
};
RIGHT — Scaffold hooks:
import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth";
const { writeContractAsync, isMining } = useScaffoldWriteContract("MyToken");
const handleClick = async () => {
// THIS RESOLVES WHEN TX IS CONFIRMED
// Address and ABI come from auto-generated deployedContracts
await writeContractAsync({
functionName: "mint",
args: [amount],
});
// Safe to update UI — tx is confirmed
setSuccess(true);
};
Reading contract data:
// WRONG
const { data } = useReadContract({
address: "0x...",
abi: [...],
functionName: "balanceOf",
args: [userAddress],
});
// RIGHT
const { data: balance } = useScaffoldReadContract({
contractName: "MyToken",
functionName: "balanceOf",
args: [userAddress],
});
Writing with value (sending ETH):
const { writeContractAsync } = useScaffoldWriteContract("MyContract");
await writeContractAsync({
functionName: "deposit",
value: parseEther("0.1"),
});
Watching events:
import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth";
const { data: events } = useScaffoldEventHistory({
contractName: "MyToken",
eventName: "Transfer",
fromBlock: 0n,
filters: { to: userAddress },
});
Auto-Generated Types
When you deploy, Scaffold-ETH 2 generates full TypeScript types for your contracts. This means:
functionNameis autocompleted — typos are caught at compile timeargsare type-checked — wrong argument types are caught at compile timecontractNamemust match a deployed contract — non-existent contracts are caught at compile time
If you bypass Scaffold hooks and use raw wagmi, you lose ALL of this type safety.
Phase 1 Checklist
- Local chain running (forked if needed)
- All contracts deployed locally
- Every contract function callable from the UI
- Every read function displays data correctly
- Error states handled (reverts show user-friendly messages)
- No hardcoded addresses in frontend code
- No raw wagmi hooks — only Scaffold hooks
- Hot reload works — change contract, redeploy, UI updates
Phase 2: Live Testnet / Mainnet Contracts, Local UI
Goal: Deploy contracts to a real chain. Test with real gas. Verify on block explorer. Frontend still runs locally for fast iteration.
Configure the Target Chain
In packages/hardhat/hardhat.config.ts:
const config: HardhatUserConfig = {
networks: {
base: {
url: process.env.BASE_RPC_URL || "https://mainnet.base.org",
accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
},
},
};
CRITICAL: Use environment variables for private keys and RPC URLs.
# .env (NEVER commit this file)
DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
Verify .gitignore includes:
.env
.env.local
.env.production
Deploy to Target Chain
yarn deploy --network base
This updates deployedContracts.ts with the new addresses on the target chain. The frontend automatically switches to using these addresses when you connect to the correct network.
Verify Contracts on Block Explorer
yarn verify --network base
This submits your source code to Basescan (or Etherscan, etc.). Verified contracts show the "Contract" tab with readable source code. Users can interact directly through the explorer.
If automatic verification fails:
npx hardhat verify --network base CONTRACT_ADDRESS "constructor_arg_1" "constructor_arg_2"
Test with Real Gas
Connect your wallet to the target chain. Execute every flow:
- Connect wallet
- Switch to correct network (if needed)
- Approve tokens (if needed)
- Execute transaction
- Wait for confirmation
- Verify state changed
Pay attention to:
- Gas estimation: Does the UI show reasonable gas costs?
- Transaction speed: How long does confirmation take?
- Error messages: Do reverts show useful information?
- Token approvals: Does the approval flow work correctly?
Phase 2 Checklist
- Contracts deployed to target chain
- Contracts verified on block explorer
-
deployedContracts.tsupdated with live addresses - Every flow tested with real gas
- Approval flows work correctly
- Error messages are user-friendly
- No secrets in committed code
- Gas costs are reasonable
Phase 3: Production Deployment
Goal: Ship the frontend. Real users can access your dApp.
Option A: IPFS Deployment (Censorship-Resistant)
See the frontend-playbook skill for detailed IPFS deployment instructions. The critical points:
# Clean build is MANDATORY
rm -rf packages/nextjs/.next packages/nextjs/out
# Build for static export
yarn build
# Deploy to IPFS via BuidlGuidl
yarn ipfs
CRITICAL: Set trailingSlash: true in next.config.js or every route except / returns 404 on IPFS.
Option B: Vercel Deployment
# From the monorepo root
vercel --prod
Configure in Vercel dashboard:
- Root Directory:
packages/nextjs - Framework Preset: Next.js
- Environment Variables: Add all required env vars
Option C: Custom Server
yarn build
yarn start
Run behind nginx or similar reverse proxy with SSL.
Post-Deploy Verification
After deploying the frontend:
- Open in incognito — no cached state, no connected wallet
- Connect a fresh wallet — test the first-time user experience
- Execute every flow — don't assume it works because it worked locally
- Check mobile — responsive design, wallet connect on mobile
- Monitor transactions — watch for failed txs in the first hour
Phase 3 Checklist
- Frontend deployed and accessible
- All routes load correctly (not just
/) - Wallet connect works
- Network switching works
- All transactions execute successfully
- Mobile responsive
- No console errors in production
- Default Scaffold-ETH branding removed
- Monitoring in place (at minimum, watch the contract on a block explorer)
Common Mistakes Agents Make
Mistake 1: Skipping Phase 1
Agent deploys directly to testnet. Every contract change costs gas and takes 15+ seconds to confirm. Debugging is 10x slower.
Fix: Always start with yarn chain or yarn fork. Get everything working locally first.
Mistake 2: Using useWriteContract Instead of useScaffoldWriteContract
The raw wagmi hook resolves on tx submission. The Scaffold hook resolves on tx confirmation. This difference causes:
- "Success" toasts before the tx is actually mined
- UI state updates that get rolled back on revert
- Missing error handling for reverted transactions
Fix: Search your codebase for useWriteContract, useReadContract, useContractRead, useContractWrite. Replace ALL of them with Scaffold equivalents.
Mistake 3: Hardcoding ABIs
Agent copies the ABI from Etherscan and pastes it into the frontend. When the contract is redeployed, the ABI goes stale.
Fix: The ABI lives in deployedContracts.ts. Scaffold hooks read it automatically. You never need to manually handle ABIs.
Mistake 4: Committing Secrets
# Check if secrets are in git history
git log --all --oneline -S "PRIVATE_KEY" -- "*.ts" "*.tsx" "*.js" "*.env"
If this returns results, the key is compromised. Rotate it immediately. Even if you delete the file, the key lives in git history forever.
Fix: Use .env files. Add them to .gitignore BEFORE creating them. Use process.env.VARIABLE_NAME in config files.
Mistake 5: Not Verifying Contracts
Unverified contracts are black boxes on block explorers. Users cannot read the code. This destroys trust.
Fix: Run yarn verify --network <chain> immediately after deployment. If it fails, debug until it passes.
Mistake 6: Deploying Frontend Before Testing on Target Chain
Agent deploys contracts to mainnet, deploys frontend to Vercel, and THEN discovers the approval flow is broken with real tokens.
Fix: Phase 2 exists specifically to catch these issues. Test every flow with real gas before deploying the frontend.
Quick Reference: Scaffold-ETH 2 Hooks
| Task | Hook | Key Prop |
|---|---|---|
| Read contract | useScaffoldReadContract |
contractName, functionName, args |
| Write contract | useScaffoldWriteContract |
Returns writeContractAsync, isMining |
| Send ETH with call | useScaffoldWriteContract |
Add value: parseEther("0.1") |
| Watch events | useScaffoldEventHistory |
eventName, fromBlock, filters |
| Contract address | useDeployedContractInfo |
Returns data.address, data.abi |
| Network info | useTargetNetwork |
Returns targetNetwork with chain config |
Directory Structure Reference
packages/
├── hardhat/ # or foundry/
│ ├── contracts/ # Your Solidity contracts
│ ├── deploy/ # Deploy scripts (numbered: 00_deploy_X.ts)
│ └── hardhat.config.ts # Network configuration
├── nextjs/
│ ├── app/ # Next.js app router pages
│ ├── components/ # React components
│ ├── hooks/ # Custom hooks (Scaffold hooks are here)
│ ├── contracts/
│ │ └── deployedContracts.ts # AUTO-GENERATED — do not edit
│ └── scaffold.config.ts # Chain selection, polling intervals
└── package.json # Monorepo root
The three phases are not optional. They are the difference between a dApp that works and one that fails in production. Follow the pipeline.