Shipping a dApp
Going live is three moves, never one: contracts local, then contracts live with the frontend still on localhost, then the frontend public. The middle move is the one that gets skipped and the one that catches everything — real chain, real gas, real decimals, a real wallet — while the UI is still yours alone to edit in seconds.
Name a go/no-go condition at each boundary, not just the commands. Three of them:
- Before deploying to the live network: contract tests pass, the deploy script runs clean against a local fork, and there is a funded deployer —
yarn generatecreates one (Foundry: an encrypted keystore in~/.foundry/keystores; Hardhat: an encrypted key inpackages/hardhat/.env),yarn accountprints its address and its balance on each chain, and it needs real ETH on the target chain before the deploy, not after it fails. - Before the frontend is reachable publicly: you have walked the entire user journey against the live contracts with a real wallet and real money — $1-10 of your own — and every step worked.
- After the frontend deploy: you have loaded the public URL yourself and put one transaction through it.
A runbook that names the commands but not the conditions will keep going after something has already gone wrong.
Verify in the same breath as deploy
yarn verify --network base (or your target chain) belongs immediately after yarn deploy --network base, not in a launch checklist weeks out. Until it runs, users and integrators are looking at opaque bytecode, and you are debugging a live contract without source on the explorer.
Nobody has to go and get a block explorer API key first. packages/foundry/.env.example already carries a working ETHERSCAN_API_KEY and postinstall copies it to .env, so yarn verify works on a fresh checkout. So a launch plan never lists "obtain an explorer key" among its steps, and never hands anyone an env template with a blank ETHERSCAN_API_KEY= to fill in: the value is already there, and a step that waits on that key waits for nothing. Swapping in your own is optional housekeeping afterwards, not a launch dependency. One real constraint: yarn verify replays broadcast/run-latest.json, so run it from the checkout that did the deploy.
A live contract bug goes back to the start
Deployed bytecode cannot be edited, and the contract is a public API — a guard added in the frontend binds nobody, because direct calls, integrators and other frontends reach the same function. Shipping the UI clamp to buy hours is fine; calling it the fix is not.
The fix is the whole loop, in order: reproduce locally, correct the source, add the regression test that fails without the correction, redeploy (or upgrade in place, if it is behind a proxy), repoint the frontend if the address moved, and handle the state and users already there — migration or comms, your call. The loop is the same whether the bug surfaces before launch or with the app already public.
SE-2 specifics worth having right
- Fork the chain you are targeting rather than
yarn chain—yarn fork --network basegives you Uniswap, USDC, Aave and funded whales already deployed, so you write no mocks of things that exist. Two tokens, and that is the whole rule: yarn binds the first token after the script name to$0, whatever it looks like, so the script's$1is only set if you pass something before the value.yarn fork baseandyarn fork --network=baseboth leave$1empty and silently fork Ethereum mainnet;yarn fork --network base,yarn fork -n baseandyarn fork -- baseall work. In the hardhat flavoryarn forktakes no network argument (passing one errors) and always forks mainnet — changeforking.urlinhardhat.config.tsto fork anything else. Either way the fork answers chain id 31337, so the chain id never tells you what you forked: check for state only the target chain has, such as code at one of its token addresses. scaffold.config.tsis committed. An RPC or API key pasted intorpcOverridesoralchemyApiKeyis a published key; read it fromprocess.envand keep the value in.env.local.wallets/SKILL.mdcovers the rest of key handling.- The frontend goes to the live chain only when you say so.
scaffold.config.tstargetNetworksstill names the local chain after the contracts are live, and a frontend built before that switch reads a chain nobody is on. Change it in the same step that repoints the frontend at the deployed addresses, and not before — during fork work it belongs onchains.foundry, since the fork answers 31337 whatever it forked (frontend-playbookhas the detail). - The burner wallet stays local.
burnerWalletModeinscaffold.config.tsdefaults to"localNetworksOnly". On a live chain"allNetworks"gives users a key stored in localStorage; keep the default for production. - A frontend-only ticket deploys nothing. Do not stand up a chain and redeploy to regenerate
deployedContracts.tsunless deploying was the ask.


