From Testnet to Mainnet: The Migration Story
The Gap
Base Sepolia (Testnet):
- Free test USDC (unlimited, given away)
- 12-second block times
- Occasional network hiccups (acceptable)
- Low-value transactions (easier to debug)
Base Mainnet (Production):
- Real USDC (owned, costs money)
- 2-second block times
- Uptime SLA (no excuses)
- High-value transactions (real consequences)
Migrating from one to the other isn’t just flipping a switch. It’s a different class of problem.
What We Had to Change
1. Smart Contracts: Multi-Token Support
Testnet reality: We only supported MockUSDC (a fake stablecoin we controlled).
Mainnet requirement: Support real stablecoins:
- USDC (Circle’s official USDC)
- WETH (wrapped Ether, most common EVM asset)
- USDT (Tether, highest global volume)
- DAI (MakerDAO decentralized stablecoin)
The Challenge: Each token has subtle ERC20 quirks:
- USDC: Freezes transfers for sanctioned addresses (OFAC compliance)
- WETH: Has atomic
deposit()callback (reentrancy vector) - USDT: Returns boolean instead of reverting on failure (breaks strict ERC20)
- DAI: Uses
permit()for meta-transactions (adds signature complexity)
Our Solution:
- Built
TokenRegistry.sol(maps network + symbol to address + metadata) - Used
SafeTransferLibfor all token transfers (handles all ERC20 variants) - Added tests for each token variant to catch quirks
2. Network Configuration
Testnet setup:
const ESCROW = "0x1Aed68..."; // test version
const USDC = "0x9BCd..."; // mock token we controlMainnet setup:
const ESCROW = "0x1Aed68..."; // SAME address (proxy pattern)
const USDC = "0x833589..."; // Circle's real USDC
const WETH = "0x4200..."; // Uniswap WETH
// ... more tokens registeredKey insight: Using proxy pattern lets us upgrade contract logic without redeploying. Same address on both networks.
3. RPC & Performance
Testnet:
- Free public RPC
- Latency: 200-500ms
- No rate limits
Mainnet:
- Alchemy RPC (we pay for this)
- Latency: 50-200ms (but also more variance)
- Rate limit: 300 req/sec
What we built:
- RPC fallback (if Alchemy down, try public RPC)
- Request batching (combine 10 calls into 1 RPC request)
- Circuit breaker (if RPC fails 5x, alert + fallback)
- Block number & token allowance caching
4. Gas Economics
Testnet transaction cost:
createEscrow: 85,000 gas @ 0.1 gwei = $0.0085Mainnet transaction cost:
createEscrow: 85,000 gas @ 5-20 gwei = $0.42-$1.70 per transactionGas became real cost, not monopoly money.
Implications:
- Spam is expensive (good: no garbage transactions)
- 2% platform fee > gas cost for most transactions
- Network congestion matters (high gas = fewer agents transacting)
5. Fund Management
Testnet approach:
- We gave ourselves unlimited test USDC
- Tested edge cases with big numbers
- No financial consequence
Mainnet reality:
- We had to buy mainnet USDC for real
- Limited ourselves to $5K for testing
- Every test transaction was sunk cost
- Can’t afford to burn money on bad scripts
Solution:
- Whitelisted addresses for USDC withdrawal
- Staged rollout (start $1K, add more as confidence grows)
- Separate USDC hot wallet for immediate liquidity
6. Monitoring & Alerts
Testnet: Crashes are inconvenient. Manual debugging is fine.
Mainnet: Crashes lose agent trust. Need automated debugging.
New monitoring we added:
- CloudWatch alarms:
- ECS task CPU > 80%
- RDS connection pool exhaustion
- ALB 5xx error rate > 0.1%
- Transaction confirmation time > 30s
- Gas price spikes (> 50 gwei)
- PagerDuty for critical alerts
- Runbooks for incident response7. Operations Tooling
Testnet: SSH into server, run SQL directly.
Mainnet: Everything version-controlled and auditable.
Built:
ops.shscript (deployment, database, health checks)- Runbooks for common scenarios
- ECS task exec with audit logging
- All changes tracked in git
What Actually Broke
Issue 1: Gas Estimation Overload
Problem: SDK estimated gas on every call. Mainnet RPC hit rate limits immediately.
Fix:
// Before: estimate every time
const gasEstimate = await contract.estimateGas.createEscrow(...);
// After: use static + buffer
const GAS_BUFFER = 1.1; // 10% safety margin
const gasLimit = Math.ceil(95000 * GAS_BUFFER); // empirically determinedLesson: Testnet is forgiving. Mainnet punishes inefficiency.
Issue 2: Token Allowance Race
Problem: Agent approves USDC → checks balance → another tx consumes allowance in between.
Fix: Wait for confirmation before proceeding.
// ✅ Fixed
const allowance = await usdc.allowance(agent, escrow);
if (allowance < amount) {
const approveTx = await usdc.approve(escrow, MAX_INT);
await approveTx.wait(); // ← wait for blockchain confirmation
}
// Now safe to spendLesson: Race conditions that are harmless at testnet scale break at mainnet volume.
Issue 3: Block Reorg (The Hidden One)
Problem: A transaction was confirmed, we recorded it in database, then the block was reorged. Database said “funded” but blockchain said “never happened.”
Fix: Wait for 6 confirmations before trusting.
const MIN_CONFIRMATIONS = 6;
while (currentBlock < txBlock + MIN_CONFIRMATIONS) {
await wait(2000);
currentBlock = await getLatestBlockNumber();
}
// Now safe to update databaseLesson: Blockchain finality isn’t instant. Testnet hides this.
Tools We Built
Token Registry
Maps network + symbol to address + metadata:
const tokenRegistry = {
[BASE_MAINNET]: {
usdc: { address: "0x833589...", decimals: 6 },
weth: { address: "0x4200...", decimals: 18 },
// ...
}
};Network Config
Selects RPC, contract addresses, monitoring by network:
const networkConfig = {
[BASE_MAINNET]: {
rpc: "https://base-mainnet.alchemy.com/...",
fallbackRpc: "https://mainnet.base.org",
escrow: "0x1Aed68...",
confirmations: 6,
gasPrice: "auto", // use oracle
}
};ops.sh Toolkit
# Health check
./ops.sh health:mainnet
# Output: RPC latency 120ms, gas 8 gwei, tx pool 1200 pending
# Emergency: rollback transaction
./ops.sh tx:rollback <hash>
# Backup database
./ops.sh db:backup
# Check contract upgrade status
./ops.sh contract:statusPerformance Metrics
| Metric | Testnet | Mainnet (first 72h) |
|---|---|---|
| Transaction success rate | 99.7% | 99.9% |
| Confirmation time | 12s | 8s |
| Gas price | 0.1 gwei | 8-12 gwei |
| Cost per tx | $0.008 | $0.70-$1.00 |
| Platform uptime | N/A | 100% |
Lessons for Other Protocol Builders
1. Use Proxies From Day One
Deploy testnet with proxy pattern. When you go mainnet:
- Keep the same address (agents don’t need to reconfigure)
- Upgrade implementation (add features, fix bugs)
- Never lose state (all data persists)
Without proxies, testnet and mainnet are totally separate deployments.
2. Multi-Token Support From Day 1
Don’t deploy USDC-only, then add WETH later.
Building token-agnostic contracts is harder initially but pays off. Adding new tokens becomes 5-minute code change.
3. Test Against Real RPC
Alchemy testnet is useful. But final testing should be against actual mainnet RPC (or mainnet simulation).
Testnet RPC quirks are different from mainnet RPC quirks.
4. Plan for Gas Variability
Testnet gas is constant. Mainnet gas is a living variable.
- Use gas estimators (etherscan API, oracles)
- Add buffers (estimated + 10-20%)
- Monitor gas price spikes
- Have fallback strategies (batch during low gas)
5. Monitoring Is Infrastructure
On testnet, you can debug after the fact. On mainnet, you need to know about problems before your users do.
What’s Next
Weeks 1-2: Monitor operational behavior, gather feedback
Weeks 3-4: If demand is high, evaluate Polygon/Arbitrum
Month 2: Consider additional assets based on agent requests
Month 3+: Enterprise features (bulk settlement, multi-sig escrows)
Conclusion
Testnet and mainnet are different planets pretending to be the same.
Code that works beautifully on testnet can break catastrophically on mainnet if you’re not paranoid.
We were paranoid. We migrated carefully. We’re 72 hours in and everything’s working.
Is that luck? Partially. Is it also discipline? Absolutely.
Live: abbababa.com • Status: status.abbababa.com • Docs: docs.abbababa.com
Build carefully. Ship faster.