Going Live on Mainnet
The engine runs the same code, the same flows and the same checks on testnet and mainnet. Nothing is minted that isn't backed, no transaction is sent that its wallet can't pay for, and USDT only ever moves out of a funded treasury — on both networks. Going live is a configuration change plus funding real wallets.
This page is the checklist, in order.
1. What changes between testnet and mainnet
| Testnet | Mainnet | |
|---|---|---|
| Switch | ENGINE_NETWORK=testnet | ENGINE_NETWORK=mainnet |
| Variables read | *_TESTNET_* (e.g. CELO_TESTNET_RELAYER_PRIVATE_KEY) | plain (e.g. CELO_RELAYER_PRIVATE_KEY) |
| USDT / USDC | mock contracts | Tether / Circle — can't be minted; treasuries must be funded with real tokens |
| Airtime gateway | always simulated (no real airtime) | always live (real airtime is sent) |
| IMC backing check | the simulated gateway counts as backing | the live float must cover all IMC in circulation |
| Data | tagged testnet | tagged mainnet — the two never mix in the dashboard |
The dashboard's network switch lets admins view the other network read-only; everything shown on mainnet is mainnet data.
2. Engine (comet-engine/.env)
ENGINE_NETWORK=mainnet
# Celo mainnet
CELO_RPC_URL=https://forno.celo.org
CELO_CHAIN_ID=42220
CELO_RELAYER_PRIVATE_KEY=<engine relayer — keep secret>
CELO_BLOCK_EXPLORER=https://celoscan.io
CELO_USDT_CONTRACT=0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e
CELO_USDC_CONTRACT=0xcebA9300f2b948710d2653dD7B07f33A8B32118C
CELO_IMC_CONTRACT=0x766AA4F469A295330b10D150f842d71977D56dD6
CELO_IMC_TYPE=airtime
CELO_IMC_BACKING=airtime
CELO_UNISWAP_FACTORY=0x7322d1E588fd4df5570c1c852Ce47a91675F851f
CELO_UNISWAP_ROUTER=0x093a7a9dC23a7a4c1088824D1A39f6A72f48C252
# Stellar (optional) — mainnet seeds; horizon/passphrase default to public
STELLAR_RELAYER_SEED=
STELLAR_ISSUER_SEED=
# Airtime gateway — live
IMPALAPAY_BASE_URL=https://airtime-api.impalapay.com/api/app
IMPALAPAY_MERCHANT_ID=<reseller merchant ID>
IMPALAPAY_USERNAME=<reseller username>
IMPALAPAY_PASSWORD=<reseller password> # also used to read the float for the IMC proof of reserve
AIRTIME_BONUS_PCT=6
# Security
SERVICE_TOKENS=<long random token(s)>
WALLET_ENCRYPTION_KEY=<never change once wallets exist>
SUPER_ADMIN_EMAIL=<seed super admin>
SUPER_ADMIN_PASSWORD=<strong>
CORS_ALLOWED_ORIGINS=https://<your dashboard host>
# CORS_ALLOW_DEV_ORIGINS must be unset in productionThe seed super admin
SUPER_ADMIN_EMAIL / SUPER_ADMIN_PASSWORD create the seed super admin on first boot — the platform's own account, with every permission and the engine's relayers as its relayers. It can't be created from the dashboard; add named staff as Admin users (Organisation → Admin users) instead, so each person has their own login and scoped permissions.
3. On-chain prerequisites
- Let the engine mint IMC. The engine relayer must be a minter on the IMC contract. On the IMC Diamond, the admin (the Celo Safe
0x3110…da28) callssetMinter(<engine relayer address>, true). Until then every IMC mint fails withLibAccessControl: Not authorized. The engine relayer's address is shown under Wallets → Relayers (Owner: Engine (.env)). - Gas. Fund the engine relayer with CELO. The engine refuses any transaction a wallet can't pay gas for, and tops up user wallets' gas from the relayer.
- Airtime float. Top up the ImpalaPay organization wallet. Every IMC mint requires
float_kes ≥ (IMC in circulation + new IMC) × CBK USD/KES. - Treasuries (per tenant — for the Comet Wallet, the
comettenant). In the dashboard, act as the tenant (top bar) and open Treasury → IMC & Airtime:- IMC: book the airtime top-up you made at the gateway — the engine mints its IMC (face value incl. bonus ÷ CBK USD/KES) into the treasury.
- USDT: send real USDT to the Treasury USDT address shown there. Every KES → USDT swap in the app pays out of it, and USDT → KES swaps return to it.
- IMM vault (for IMM cycles and trades): fund it with real USDT under Markets → Pricing & trades → Fund vault.
- AMM pools (optional, for the on-chain AMM swap): add liquidity under Markets → AMM pools.
4. Tenants
Each tenant must add its own relayer (Wallets → Relayers → Add a relayer) and fund it with CELO and the tokens it trades — the engine never signs or pays for a tenant with the platform's relayer. IMC is the exception: only the engine mints it, and never on a tenant's behalf.
5. Comet Wallet backend (app-core-backend/.env)
COMET_ENGINE_BASE_URL=https://<engine host> # no trailing /api/v1
COMET_ENGINE_TOKEN=<one of the engine's SERVICE_TOKENS>
COMET_ENGINE_TENANT=cometThe backend doesn't need ImpalaPay credentials: airtime withdrawals redeem through the engine, which holds the reseller account.
The backend syncs CBK rates from the engine every 15 minutes, redeems airtime through the engine (lock → send → burn), and pays USDT from the treasury — identically on both networks.
6. Flutter app (FusionFy)
The production flavor already talks to https://cometapi.mamlakapsp.com/api/v1 (lib/flavors.dart) — point that host at the mainnet app-core-backend. Build:
flutter build appbundle --flavor production -t lib/main_production.dart # Android
flutter build ipa --flavor production -t lib/main_production.dart # iOSNo app change is needed between networks: the app only ever talks to app-core-backend.
7. Before announcing
GET /api/v1/networks→"current": "mainnet".- Dashboard Settings → Network Mainnet, Airtime gateway Live, CBK rate present.
- Wallets → Relayers → the engine relayer has CELO; no tenant relayer you rely on shows 0 gas.
- Book a small IMC top-up; confirm it appears in the journal (mainnet).
- From a staff Comet Wallet account: buy a little airtime, withdraw 10 KES of airtime to an Airtel number, and swap a small KES → USDT → KES round trip.
8. History recorded before network tagging
Journal entries and admin actions written before the engine recorded its network have no network tag and count as the active network. Run the one-off script to tag them (checked: all historic on-chain entries are Celo Sepolia transactions):
mongosh "$MONGO_URI/$MONGO_DB" comet-engine/scripts/tag_legacy_network.mongosh.js