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

networkChainNotes
eip155:84532Base Sepoliathe chain id the local dev chain also uses
eip155:8453Basemainnet — 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.modeWho signsAllowed on
localthis process, with an Anvil dev keythe local dev chain only
remotean HTTP facilitator you point atanywhere
auth.typeWhat it sendsNeeds
nonenothingon mainnet, allowUnauthenticatedFacilitator: true
bearera static tokennothing installed
cdpa 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

config.yamlyaml
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.

Mainnet guardrails

All of these are refused at config load, before the gateway starts:

RefusedBecause
allowMainnet absent or falsemainnet is never a default
facilitator.mode: locala funded gas key inside the resource server
facilitator.auth.type: noneunless allowUnauthenticatedFacilitator accepts it by name
a non-HTTPS facilitator.urlauthorisations and settlement results in the clear
a well-known Anvil payToits private key is public knowledge
an asset that is not USDC on Basesettling in an unintended token
an assetName other than the EIP-712 domain USDC reportsevery payment refused after the buyer signed
assetName is the EIP-712 domain, not the symbolBase Sepolia's USDC reports "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:

NetworkTransaction
Base Sepolia0xea41b234c4…
Base0x57ec81c2a3…

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.