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.

Optional peers for x402bash
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 offer

The 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.

bash
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.json
config.yamlyaml
payments:
  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.
The local facilitator key is an Anvil well-known key — local development only, never fund it. The in-process facilitator runs only on eip155:31337, and its health check requires the RPC to answer as Anvil; every public network needs a remote facilitator.

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.

NetworknetworkStatus
Base Sepolia, Baseeip155:84532, eip155:8453supported; settled with EIP-3009 and Permit2
Ethereum, Polygon, Arbitrum One, Arbitrum Sepolia, Avalanche C-Chaineip155:1, eip155:137, eip155:42161, eip155:421614, eip155:43114supported; USDC domain, Permit2 and the x402 proxy checked on chain, no payment run yet
Solana, Solana Devnetsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1supported; settled on devnet and mainnet
NEAR, NEAR Testnetnear:mainnet, near:testnetpartial; settled only through the SDK facilitator the test suite runs
your own EVM chaindeclared under networks with kindpartial; you supply the chain and all four asset fields
yaml
payments:
  x402:
    networks:
      eip155:10: { kind: mainnet, displayName: OP Mainnet }   # kind selects the mainnet guardrails

A 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.

yaml
payments:
  x402:
    network: eip155:31337
    payTo: ${MERCHANT_WALLET}
    asset: ${TOKEN_ADDRESS}
    assetDecimals: 18
    assetTransferMethod: permit2

Before 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.

yaml
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 /supported response. 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: upfront for 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.

yaml
payments:
  x402:
    network: near:testnet
    rpcUrl: ${NEAR_TESTNET_RPC_URL}
    payTo: ${NEAR_MERCHANT_ACCOUNT}
    facilitator:
      mode: remote
      url: ${X402_FACILITATOR_URL}
  • payTo must hold a storage deposit on the token (NEP-145 storage_deposit), or every transfer to it fails. Health fails until it does.
  • The buyer signs with a full-access key: ft_transfer attaches one yoctoNEAR, which a function-call key cannot.
  • The delegate has no chain id: use separate buyer keys for mainnet and testnet. Use upfront if a handler can outlast maxTimeoutSeconds; an expired delegate is refused at settlement.
NEAR support is partialNEAR has settled on a local sandbox, testnet and mainnet, all through the SDK facilitator with the test suite's own relayer, but not through a hosted facilitator.

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.

yaml
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 shared rpcUrl. RPC URLs must be http:// or https://.
  • 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.

yaml
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

yaml
    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.

FlowOrderBackend failsSettlement refused
authorization (x402 default)verify, reserve, call backend, settlenothing settles; the attempt is recorded as rejectedthe backend's response is withheld; 402
upfront (MPP default)verify, reserve, settle, call backendthe payment stays settled; the receipt records a failed deliverythe 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.

yaml
resources:
  create_order:
    # …
    pricing: { type: fixed, amount: "5.00", currency: USDC }
    payments: [x402]
    paymentFlow: upfront   # this resource settles before the backend call

MPP (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.

Optional peers for MPPbash
npm install @devlab.group/[email protected] [email protected] [email protected] @x402/[email protected] @x402/[email protected]
config.yamlyaml
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.

Pinned to pre-1.0 draftsThe gateway targets 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

authConfigInstalls
noneauth: { type: none }nothing
bearerauth: { type: bearer, token: "${X402_FACILITATOR_TOKEN}" }nothing
cdpauth: { 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:

json
{
  "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.

text
  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  local

With 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.