Reference
Networks & facilitators
Local, testnet and mainnet: how the deployment mode is derived, and the guardrails around real funds.
Same gateway, same pipeline — a different network and a facilitator that is not this process. There is no code change and no “live mode” to switch on.
Supported networks
| network | Chain | Notes |
|---|---|---|
eip155:84532 | Base Sepolia | the chain id the local dev chain also uses |
eip155:8453 | Base | mainnet — real funds |
Anything else is CONFIG_INVALID at load. The chain id is signed into the buyer's EIP-712 domain, so an unrecognised network is never guessed at.
Deployment mode
local, testnet and mainnet are derived from the network and the facilitator together, because chain id 84532 belongs to both the local dev chain and Base Sepolia. The mode is reported by doctor, by the payment provider's health check, and at /.well-known/agent-commerce.
The facilitator
A facilitator verifies the buyer's authorisation and broadcasts the transfer. It is the only component that needs gas, and it is never this gateway on a public network.
| facilitator.mode | Who signs | Allowed on |
|---|---|---|
local | this process, with an Anvil dev key | the local dev chain only |
remote | an HTTP facilitator you point at | anywhere |
| auth.type | What it sends | Needs |
|---|---|---|
none | nothing | on mainnet, allowUnauthenticatedFacilitator: true |
bearer | a static token | nothing installed |
cdp | a fresh Coinbase Developer Platform JWT per request | @coinbase/x402 |
A facilitator cannot redirect your money — the authorisation names its recipient, amount and chain. What it can do is see every authorisation you handle, and stop answering.
Configuration by network
payments:
x402:
enabled: true
network: ${X402_NETWORK} # CAIP-2; eip155:84532 locally
rpcUrl: ${X402_RPC_URL}
asset: ${X402_ASSET} # ERC-20 with EIP-3009
assetName: ${X402_ASSET_NAME} # EIP-712 domain name
assetVersion: ${X402_ASSET_VERSION}
assetDecimals: ${X402_ASSET_DECIMALS}
payTo: ${MERCHANT_WALLET} # merchant-controlled. NEVER the gateway's.
maxTimeoutSeconds: 120
facilitator:
mode: local # local dev chain only
signerPrivateKey: ${X402_FACILITATOR_PRIVATE_KEY}signerPrivateKey must be an Anvil well-known key; startup refuses it against anything that is not a local or private RPC.
payments:
x402:
enabled: true
network: eip155:84532
rpcUrl: https://base-sepolia-rpc.publicnode.com # health checks only
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" # Circle USDC
assetName: USDC
assetVersion: "2"
assetDecimals: 6
payTo: ${MERCHANT_WALLET}
maxTimeoutSeconds: 300
facilitator:
mode: remote
url: https://x402.org/facilitator
auth: { type: none }Test USDC from faucet.circle.com; the buyer needs no ETH. doctor reports TESTNET on Base Sepolia.
payments:
x402:
enabled: true
network: eip155:8453
rpcUrl: ${X402_RPC_URL:-https://base.drpc.org} # health checks only
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
assetName: "USD Coin" # the EIP-712 domain, not "USDC"
assetVersion: "2"
assetDecimals: 6
payTo: ${MERCHANT_WALLET}
maxTimeoutSeconds: 600
allowMainnet: ${ALLOW_X402_MAINNET} # must resolve to true
facilitator:
mode: remote
url: ${X402_FACILITATOR_URL} # https only
auth:
type: bearer # or: cdp (needs @coinbase/x402)
token: ${X402_FACILITATOR_TOKEN}doctor prints LIVE MAINNET MODE - REAL FUNDS.
Mainnet guardrails
All of these are refused at config load, before the gateway starts:
| Refused | Because |
|---|---|
allowMainnet absent or false | mainnet is never a default |
facilitator.mode: local | a funded gas key inside the resource server |
facilitator.auth.type: none | unless allowUnauthenticatedFacilitator accepts it by name |
| a non-HTTPS facilitator.url | authorisations and settlement results in the clear |
| a well-known Anvil payTo | its private key is public knowledge |
| an asset that is not USDC on Base | settling in an unintended token |
| an assetName other than the EIP-712 domain USDC reports | every payment refused after the buyer signed |
"USDC"; Base mainnet's reports "USD Coin". The buyer signs that string, so the wrong one gets every payment refused with invalid_exact_evm_token_name_mismatch after they signed. Both values are pinned and checked at load.It has actually settled
Both of these moved 0.01 USDC from a buyer to a merchant through a remote facilitator:
| Network | Transaction |
|---|---|
| Base Sepolia | 0xea41b234c4… |
| Base | 0x57ec81c2a3… |
Reproduce with npm run test:testnet and npm run test:mainnet. Both spend real funds, skip themselves without credentials, and never run in CI — a workflow would mean a funded key in repository secrets.
Worked configurations: Base Sepolia, Base mainnet and Base mainnet (PayAI).
Accept paymentsThe integration walkthrough for each network.