Merchant setup
Accept payments
Configure x402 on EVM networks, Solana or NEAR, or experimental MPP; funds go straight to your wallet.
Payments use x402 v2, scheme exact, on EVM, Solana and NEAR: the buyer signs a transfer that names your payTo as recipient, and a facilitator broadcasts it. On EVM that is an EIP-3009 authorization or, per offer, a Permit2 signature. Funds move buyer → merchant directly on chain. The gateway holds no funds or buyer or merchant key.
npm install @devlab.group/[email protected] @x402/[email protected] @x402/[email protected] [email protected]
npm install @x402/[email protected] @solana/[email protected] # only for a Solana offer
npm install @x402/[email protected] # only for a NEAR offerThe x402 entry needs @x402/core, @x402/evm and viem even when every offer is on Solana or NEAR. The Solana and NEAR peers load only when such an offer is configured.
Configure the rail
A private Anvil chain with a mock USDC — deterministic, free, and what the demo and CI use.
npm run chain:start # Anvil, chain id 31337; stays in the foreground
# in a second terminal
npm run chain:deploy # MockUSDC, funded buyer, writes .deploy/local.jsonpayments:
x402:
enabled: true
network: ${X402_NETWORK:-eip155:31337} # CAIP-2; the local chain
rpcUrl: ${X402_RPC_URL:-http://127.0.0.1:8545}
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
paymentFlow: authorization # default; or upfront
facilitator:
mode: local # eip155:31337 only
signerPrivateKey: ${X402_FACILITATOR_PRIVATE_KEY}The local chain is eip155:31337, Anvil's default chain id, and it has no default token: set all four asset fields.
validate and doctor read .deploy/local.json and fill the X402_ASSET*, MERCHANT_WALLET and X402_FACILITATOR_PRIVATE_KEY placeholders for you, but not X402_NETWORK or X402_RPC_URL. The gateway process does not: export any placeholder without a fallback, such as X402_ASSET, before you start it.eip155:31337, and its health check requires the RPC to answer as Anvil; every public network needs a remote facilitator.Real USDC on a public testnet through the public x402.org facilitator. No key anywhere in this file.
payments:
x402:
enabled: true
network: eip155:84532
rpcUrl: https://base-sepolia-rpc.publicnode.com # health checks and reconcile
payTo: ${MERCHANT_WALLET} # no asset fields: the network's USDC
maxTimeoutSeconds: 300
facilitator:
mode: remote
url: https://x402.org/facilitator
auth: { type: none }MERCHANT_WALLET=0xYourMerchantAddress \
npx @devlab.group/agent-commerce validate --config config.yamlGet test USDC for the buyer at faucet.circle.com. The buyer needs no ETH: EIP-3009 is signed offline and the facilitator pays the gas.
Omit asset fields to use the profile's USDC address, EIP-712 domain and decimals; overrides must match. For another EVM token with EIP-3009, set all four asset fields.
Real funds. Nothing is defaulted, and every line below is checked at load.
payments:
x402:
enabled: true
network: eip155:8453
rpcUrl: ${X402_RPC_URL:-https://base.drpc.org} # health checks and reconcile
payTo: ${MERCHANT_WALLET} # no asset fields: USDC on Base
maxTimeoutSeconds: 600
allowMainnet: ${ALLOW_X402_MAINNET} # must resolve to true
facilitator:
mode: remote
url: ${X402_FACILITATOR_URL} # https only
auth:
type: bearer # or: cdp, with apiKeyId and apiKeySecret (needs @coinbase/x402 and CDP's url)
token: ${X402_FACILITATOR_TOKEN}| Required | Why |
|---|---|
allowMainnet: true | mainnet is never a default |
facilitator.mode: remote | no gas key inside the gateway |
| an HTTPS facilitator.url | no authorizations in the clear |
| facilitator auth, or allowUnauthenticatedFacilitator | an explicit counterparty decision |
| a non-development payTo | Anvil keys are public knowledge |
USDC asset fields, if set, match Base's (assetName: "USD Coin") | the buyer signs that EIP-712 domain |
The same rules apply to every built-in mainnet: Ethereum, Polygon, Arbitrum One, Avalanche C-Chain, Solana and NEAR. doctor then reports LIVE MAINNET MODE - REAL FUNDS and names each network.
Other networks
network is a CAIP-2 id, built in or declared. Unknown ids are CONFIG_INVALID at load, never a guess. Every public network needs a remote facilitator whose /supported lists it, or health fails.
| Network | network | Status |
|---|---|---|
| Base Sepolia, Base | eip155:84532, eip155:8453 | supported; settled with EIP-3009 and Permit2 |
| Ethereum, Polygon, Arbitrum One, Arbitrum Sepolia, Avalanche C-Chain | eip155:1, eip155:137, eip155:42161, eip155:421614, eip155:43114 | supported; USDC domain, Permit2 and the x402 proxy checked on chain, no payment run yet |
| Solana, Solana Devnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 | supported; settled on devnet and mainnet |
| NEAR, NEAR Testnet | near:mainnet, near:testnet | partial; settled only through the SDK facilitator the test suite runs |
| your own EVM chain | declared under networks with kind | partial; you supply the chain and all four asset fields |
payments:
x402:
networks:
eip155:10: { kind: mainnet, displayName: OP Mainnet } # kind selects the mainnet guardrailsA declared network has no default asset. Built-in networks cannot be redeclared; their entry under networks may set only rpcUrl. Ready-made configurations: Arbitrum Sepolia, Solana devnet, NEAR testnet.
Permit2
For an ERC-20 without transferWithAuthorization, set assetTransferMethod: permit2 on the offer (EVM only; the default is eip3009). Permit2 signs under its own EIP-712 domain, so the token needs only asset and assetDecimals.
payments:
x402:
network: eip155:31337
payTo: ${MERCHANT_WALLET}
asset: ${TOKEN_ADDRESS}
assetDecimals: 18
assetTransferMethod: permit2Before paying, the buyer approves the Permit2 contract 0x000000000022D473030F116dDEE9F6B43aC78BA3 for the token once, which costs gas. The gateway sends no approval and accepts no gas-sponsoring extension; a buyer without the allowance gets 412 with permit2_allowance_required.
Solana
Leave out the asset fields to settle the network's USDC through a remote facilitator, which pays the fee. payTo is the merchant's wallet.
payments:
x402:
network: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 # Solana Devnet
rpcUrl: ${SOLANA_DEVNET_RPC_URL}
payTo: ${SOLANA_MERCHANT_WALLET}
facilitator:
mode: remote
url: ${X402_FACILITATOR_URL}- The wallet's USDC token account must already exist; send it any amount once. Health fails without it.
- Each offer names the facilitator's fee payer from its
/supportedresponse. Without one, readiness fails and no challenge is built. - A signed transaction expires with its blockhash, about 60 to 90 seconds after signing. Use
paymentFlow: upfrontfor a handler that can take longer.
NEAR
The buyer signs a NEP-366 delegate action holding one ft_transfer of the network's USDC; the facilitator's relayer submits it and pays gas. payTo is a NEAR account id, named or implicit.
payments:
x402:
network: near:testnet
rpcUrl: ${NEAR_TESTNET_RPC_URL}
payTo: ${NEAR_MERCHANT_ACCOUNT}
facilitator:
mode: remote
url: ${X402_FACILITATOR_URL}payTomust hold a storage deposit on the token (NEP-145storage_deposit), or every transfer to it fails. Health fails until it does.- The buyer signs with a full-access key:
ft_transferattaches one yoctoNEAR, which a function-call key cannot. - The delegate has no chain id: use separate buyer keys for mainnet and testnet. Use
upfrontif a handler can outlastmaxTimeoutSeconds; an expired delegate is refused at settlement.
Offer several networks
List offers under options to accept one resource on several networks or in several tokens. The challenge lists every offer, and the buyer pays one. All offers settle through the one facilitator under payments.x402, which must list each network.
payments:
x402:
networks:
eip155:8453: { rpcUrl: "${BASE_RPC_URL}" } # quote a placeholder inside { }
eip155:137: { rpcUrl: "${POLYGON_RPC_URL}" }
options:
- network: eip155:8453
payTo: ${MERCHANT_WALLET}
- network: eip155:137
payTo: ${MERCHANT_WALLET}
# facilitator, allowMainnet and maxTimeoutSeconds as before- Each network needs its own RPC endpoint for health checks:
networks.<id>.rpcUrl, else the sharedrpcUrl. RPC URLs must behttp://orhttps://. - Config load refuses mixing the direct form with
options, an empty list, a repeated (network, asset,payTo), and offers that mix local, testnet and mainnet. - Health checks every offer; one failing offer fails the provider, and discovery publishes each offer's status.
A paid resource can replace the rail's offers with its own list under x402.accepts. The rail's offers still serve every other resource.
resources:
market_report:
# backend, pricing and expose as before
payments: [x402]
x402:
accepts:
- network: eip155:137
payTo: ${POLYGON_MERCHANT_WALLET}A complete template with Base, Polygon, Arbitrum One and Solana is in Multi-network mainnet.
Put a price on a resource
pricing:
type: fixed
amount: "0.01" # decimal string in display units — never a float
currency: USDC
payments: [x402]Amounts are converted to base units inside the provider (6 decimals for USDC). More precision than the asset supports is a configuration error, not a rounding opportunity; with several offers, the least precise one sets the limit.
A resource paid in a network's default USDC must be priced in USDC or USD. USD takes USDC at face value, with no exchange-rate lookup, and the payment records USDC with the USD price kept as metadata.quote. Payments in any other token keep the resource's label.
payments is an ordered list. A resource may name both rails, such as [x402, mpp]: the first enabled one serves it, and nothing selects or falls back between rails per request.
Choose when payment settles
Set paymentFlow under payments.x402 or payments.mpp to choose when that rail settles. A fixed-price resource can override the selected rail's setting.
| Flow | Order | Backend fails | Settlement refused |
|---|---|---|---|
authorization (x402 default) | verify, reserve, call backend, settle | nothing settles; the attempt is recorded as rejected | the backend's response is withheld; 402 |
upfront (MPP default) | verify, reserve, settle, call backend | the payment stays settled; the receipt records a failed delivery | the backend is never called |
With authorization, backend time counts toward the payment window. If settlement is refused or cannot be confirmed, the gateway withholds the response although the backend has run. Choose upfront when backend side effects must follow payment, or when the handler can outlast a Solana blockhash or a NEAR delegate.
After a rejected x402 attempt, the buyer can retry the unspent proof. The gateway reclaims the attempt and archives its previous row in payment_attempt_history; on-chain replay checks prevent double settlement. MPP credentials are single-use per challenge, so MPP buyers need a fresh challenge.
resources:
create_order:
# …
pricing: { type: fixed, amount: "5.00", currency: USDC }
payments: [x402]
paymentFlow: upfront # this resource settles before the backend callMPP (experimental)
MPP is a second payment rail, off by default: the charge intent, evm method and EIP-3009 authorization credential, with USDC on Base Sepolia or Base, or a token on the local chain eip155:31337. The default network is Base Sepolia, so a local config names eip155:31337. The buyer still signs an EIP-3009 transfer to your address, and settlement still goes through an x402 facilitator, so the money path is the same.
MPP settles before the backend by default (upfront): its charge intent counts a backend call before the payment is verified as partial access. paymentFlow: authorization restores the x402 order, at the cost that a refused or unconfirmed settlement leaves the backend's work unpaid, and backend time counts toward the challenge lifetime, challengeTtlSeconds.
npm install @devlab.group/[email protected] [email protected] [email protected] @x402/[email protected] @x402/[email protected]payments:
mpp:
enabled: true
network: eip155:84532 # the default; eip155:31337 locally, eip155:8453 on Base
rpcUrl: ${MPP_RPC_URL}
asset: ${MPP_ASSET} # token contract on the selected RPC
assetName: USDC # EIP-712 domain name; "USD Coin" on Base
assetVersion: '2'
recipient: ${MERCHANT_WALLET} # merchant-controlled; not gateway-owned
realm: api.example.com # the challenge realm, single line
challengeSecret: ${MPP_CHALLENGE_SECRET} # minimum length 32
challengeTtlSeconds: 300 # optional
paymentFlow: upfront # default; or authorization
facilitator:
mode: remote
url: ${MPP_FACILITATOR_URL}The challenge is HMAC-bound to the resource and its terms with challengeSecret, and the buyer's EIP-3009 nonce must equal the challenge hash. Over HTTP the challenge arrives in WWW-Authenticate: Payment …, the proof goes in Authorization: Payment …, and the result comes back in Payment-Receipt. For a request with a body, the challenge also binds a sha-256 digest of the body bytes. The paid retry must use the same bytes, even if both bodies represent the same JSON. Serve MPP behind TLS: the core draft forbids issuing challenges over plain HTTP.
draft-httpauth-payment-01, draft-payment-intent-charge-00 and draft-evm-charge-00, and pins [email protected]. For the EIP-3009 nonce it follows mppx, not the EVM draft, so credentials that use the draft's nonce, including those from mppx 0.12 or earlier, fail with wrong_nonce. Splits, subscriptions, sessions, Permit2 and every method other than evm are not implemented.Facilitator authentication
| auth | Config | Installs |
|---|---|---|
none | auth: { type: none } | nothing |
bearer | auth: { type: bearer, token: "${X402_FACILITATOR_TOKEN}" } | nothing |
cdp | auth: { type: cdp, apiKeyId: "${CDP_API_KEY_ID}", apiKeySecret: "${CDP_API_KEY_SECRET}" } | @coinbase/x402 |
cdp signs each request for https://api.cdp.coinbase.com/platform/v2/x402, so config load refuses it with any other facilitator.url.
What an agent receives
An unpaid call to a paid resource answers 402 (HTTP) or an isError tool result (MCP) carrying this envelope:
{
"status": "payment-required",
"code": "PAYMENT_REQUIRED",
"requestId": "…",
"resourceId": "market_report",
"message": "Payment of 0.01 USDC is required for resource \"market_report\". …",
"payment": {
"provider": "x402",
"version": "2",
"amount": "0.01",
"currency": "USDC",
// destination, network and asset are deprecated, and absent when
// the challenge lists several offers: read each one in accepts
"destination": "0x…",
"network": "eip155:31337",
"asset": "0x…",
"expiresAt": "…",
"accepts": [ /* x402 v2 PaymentRequirements, verbatim */ ],
"envelope": { /* x402 v2 PaymentRequired, verbatim */ }
}
}With several offers, accepts lists each one and the envelope leaves out destination, network and asset. The agent side of that exchange is in Handle a payment.
Where the money went
agent-commerce start and repository scripts print each enabled rail's full settlement destination for operator review. Custom createGateway scripts print what they log.
x402 settlement
network eip155:31337 via http://127.0.0.1:8545
asset 0x5fbdb2315678afecb367f032d93f642f64180aa3 (MockUSDC v2)
pays to 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 <- merchant-controlled, not the gateway
facilitator local
MPP settlement (charge, evm, authorization)
network eip155:31337 via http://127.0.0.1:8545
asset 0x5fbdb2315678afecb367f032d93f642f64180aa3 (MockUSDC v2)
pays to 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (merchant-controlled; not gateway-owned)
facilitator localWith several offers, each prints its own network, asset and payee, and a resource with its own offers lists them under its id. RPC URLs show only their origin, since providers put API keys in the path. The same terms are published at GET /.well-known/agent-commerce, with a health status per offer when there are several, and doctor compares them with your config.