Skip to main content

Pay with x402 (exact)

Your agent signs an EIP-3009 transferWithAuthorization over EIP-712 typed data and sends the signature. The API recovers the signer, then asks a facilitator — Coinbase CDP — to verify it and broadcast the transfer. The signature is the payment: there is no deposit address and no card. Coinbase’s x402 overview and quickstart for buyers cover the client side, including the @x402/fetch and @x402/axios wrappers and the Go and Python clients that handle the 402 retry for you. You need a token whose role grants Invoice:PayMember, Billing, or Admin — an EOA whose key your agent controls, and that EOA funded with USDC on the network the challenge names.

Create the experiment

Confirm the quote as usual. The response carries payment.pay_url — the invoice route you settle at.

Read the challenge

Call /pay with the method header and no credential:
The challenge is in the PAYMENT-REQUIRED response header, base64-encoded, not in the body. Decode it:
payTo is the merchant EOA, not a deposit address. asset is the raw ERC-20 contract address, and network its CAIP-2 chain id — read both from the challenge rather than hardcoding them. extra carries the token’s EIP-712 domain, name and version.

Sign the authorization

Build the EIP-712 typed-data message for the token’s transferWithAuthorization:
  • from — your EOA.
  • topayTo from the challenge.
  • value — the challenge’s amount verbatim. It is already in the token’s base units, so do not rescale it; the signed value must equal it exactly.
  • nonce — a fresh 32-byte random value you generate. It is your own on-chain replay token; the challenge does not issue one.
  • validAfter, validBefore — a window you choose. validAfter must be at or before now, and validBefore no later than maxTimeoutSeconds (300 seconds) from now.
The EIP-712 domain must match the token contract exactly: name and version from the challenge’s extra, the chain id, and the challenge’s asset as the verifying contract. The API recovers the signer over the same hash, so a domain that differs in any field recovers a different address and the payment is refused.
Read name and version from the challenge rather than assuming them. USDC’s EIP-712 domain is not the same string on every chain, and a wrong domain fails as an authorization error with nothing pointing at the cause.

Retry with the signature

Base64-encode the signed payload as JSON and send it in the PAYMENT-SIGNATURE header:
The API recovers the signer, checks the recipient, amount, and deadline, then asks the facilitator to verify. The facilitator is authoritative on balance and on whether the nonce is already spent. On success it broadcasts the transfer, and the transaction hash comes back on the 200:
A not_yet_settled verdict re-emits the same 402. That is not a rejection — retry with the same signature. Re-signing burns the nonce for nothing. Every other 402 after you send a signature is final: a recovered signer that does not match, a wrong amount or recipient, an expired validBefore, or a spent nonce. Resending the same signature will not clear any of them — fix the cause and sign a corrected authorization, with a fresh nonce. The challenge does not change, so reading it again returns the same terms.
This rail has no settlement recovery. If a settling call dies before you read its response, check the invoice status and your EOA on Base before signing anything else — a transfer that reached the chain but was never recorded looks exactly like a rejection, and signing again with a fresh nonce pays twice.