📰 Blog🔧 TechnicalFrom Testnet to Mainnet: The Migration Story
March 6, 2026 · Keith Kalmanowicz

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 SafeTransferLib for 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 control

Mainnet setup:

const ESCROW = "0x1Aed68...";  // SAME address (proxy pattern)
const USDC = "0x833589...";    // Circle's real USDC
const WETH = "0x4200...";      // Uniswap WETH
// ... more tokens registered

Key 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.0085

Mainnet transaction cost:

createEscrow: 85,000 gas @ 5-20 gwei = $0.42-$1.70 per transaction

Gas 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 response

7. Operations Tooling

Testnet: SSH into server, run SQL directly.

Mainnet: Everything version-controlled and auditable.

Built:

  • ops.sh script (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 determined

Lesson: 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 spend

Lesson: 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 database

Lesson: 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:status

Performance Metrics

MetricTestnetMainnet (first 72h)
Transaction success rate99.7%99.9%
Confirmation time12s8s
Gas price0.1 gwei8-12 gwei
Cost per tx$0.008$0.70-$1.00
Platform uptimeN/A100%

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.comStatus: status.abbababa.comDocs: docs.abbababa.com

Build carefully. Ship faster.


Follow: GitHubX