The same gateway, settling real USDC on Base. Read docs/configuration.md first. It explains what is refused and why, and this file assumes it.
Two things are structurally different from the local and testnet examples:
- No
signerPrivateKey, and no way to have one.facilitator.mode: localis refused on mainnet: it signs with a key this process holds, which is a hot wallet inside the resource server. A remote facilitator broadcasts and pays the gas. - Almost nothing is defaulted. Only the port, the receipt store path and
the RPC URL have fallbacks. Every other
${VAR}has none, so a missing one fails config loading rather than resolving to something plausible.
MERCHANT_WALLET |
your wallet. Address only: the gateway never takes a merchant key. See Paying into a contract. |
ALLOW_X402_MAINNET=true |
the explicit opt-in. Never a default. |
| CDP credentials, or a bearer token | cdp uses your CDP API key and the configured CDP URL. For bearer, set the facilitator's HTTPS url. Neither needs an extra peer. |
ADMIN_TOKEN |
required here: it guards the receipt routes, which carry your ledger. |
| A Base RPC | health checks only. Use a dedicated endpoint: the public one's outages become your readiness failures. |
To run without a facilitator credential, see base-mainnet-payai,
which sets allowUnauthenticatedFacilitator.
MERCHANT_WALLET sets the x402 payTo address, which can be a contract such
as a revenue splitter on Base. USDC's EIP-3009 transferWithAuthorization
transfers tokens without invoking a recipient callback, so the contract needs
no receive handler.
Before using a contract:
- Confirm it can send USDC onward, for example through its
distributefunction. Otherwise, the funds stay at that address. - Verify its deployment on each offer's network. A contract exists only on the
chain where it was deployed, while an EOA's key controls its address on every
EVM chain. Do not reuse the address on another network unless the intended
contract is also deployed there.
docs/configuration.mdreuses${MERCHANT_WALLET}for Base and Polygon offers. Setting it to a Base contract would send Polygon payments to an address nobody controls there.
ALLOW_X402_MAINNET=true \
MERCHANT_WALLET=0xYourWallet \
GATEWAY_PUBLIC_BASE_URL=https://your.gateway \
ADMIN_TOKEN=... \
MERCHANT_API_BASE_URL=http://localhost:3000 \
CDP_API_KEY_ID=... CDP_API_KEY_SECRET=... \
npm run agent-commerce -- validate --config examples/base-mainnet/config.yamlDrop any one of those and it fails, naming the path:
FAIL CONFIG_INVALID: Unresolved environment variable "${ALLOW_X402_MAINNET}"
referenced at config path "$.payments.x402.allowMainnet"With every value set, doctor reports the deployment as
LIVE MAINNET MODE - REAL FUNDS.
export ALLOW_X402_MAINNET=true
export X402_SMOKE_TARGETS=base
export X402_MAINNET_BUYER_PRIVATE_KEY=0x... # funded with USDC on Base
export X402_MAINNET_MERCHANT_ADDRESS=0x...
export X402_FACILITATOR_URL=https://...
export CDP_API_KEY_ID=... CDP_API_KEY_SECRET=... # optional; or X402_FACILITATOR_TOKEN
npm run test:mainnetEach successful x402 smoke run spends X402_SMOKE_AMOUNT (default
0.01) of real USDC. The x402 suite skips itself, naming missing settings,
unless X402_SMOKE_TARGETS names base; a named target then fails without the
opt-in, buyer key, merchant address or facilitator URL.
The facilitator credential is optional. Setting ALLOW_MPP_MAINNET=true also
enables the separate, paid MPP smoke suite.
It proves, in order: the guard refuses a config that has not opted in · the
deployment reports itself as live mainnet · a payment settles on Base · the
receipt carries the settlement reference and a 2xx backend status · the
resource is delivered exactly once · the same authorization presented again is
refused, and the backend is not called again · no credential appears in
anything logged. The USDC Transfer is read back from the transaction receipt.
See smoke tests.
It never runs in CI; see the main README for why.