Go live with x402 + Coinbase CDP
Settle real USDC over HTTP-402 with a Coinbase CDP facilitator — from Base Sepolia testnet to Base mainnet, the EIP-712 domain trap, the txHash-is-authoritative rule, funding, and the live cutover checklist.
The checkout core is zero-dependency — it never imports a chain library. To settle real
USDC it does the cheap, structural part itself (verify the EIP-3009 envelope matches the
quote) and hands the on-chain tail to an injected Facilitator that owns the viem /
Coinbase CDP code. This guide takes you from a testnet settle to a live mainnet one, with the
traps that bite everyone called out explicitly.
On the hosted control plane the facilitator is wired for
you: a veto_live_ key (mint it in Dashboard → Developers at
https://merchants.veto-ai.com) settles real USDC on Base mainnet; a veto_test_ key settles
on a deterministic mock (see test mode). Production runs the rail in
url-mode against a self-hosted x402-rs facilitator (FACILITATOR_MODE=url +
FACILITATOR_URL) — no Coinbase CDP dependency. If you just want the hosted, copy-paste
path, see Set up Veto end-to-end (hosted). This guide is what
runs under the hosted plane — and exactly what you implement when you self-host, where the CDP
facilitator below is one supported backend.
How settlement splits
agent ──signed EIP-3009 transferWithAuthorization──▶ POST /agent/checkout/{id}/settle
│
SDK x402 rail (zero-dep) │ structural verify:
├─ to === payTo │ value === amountBaseUnits
└─ validBefore in the future │ signature present
▼
injected Facilitator (your viem/CDP code)
├─ ecrecover signer === authorization.from
└─ broadcast → CONFIRMED on-chain txHash ◀── authoritativeThe split is deliberate: the SDK stays installable with nothing, and all the secret-bearing, network-touching crypto lives in your facilitator where the dependencies already are.
1. Configure the x402 rail
import { createCheckout, x402Rail, BALANCED } from '@veto-protocol/checkout';
const checkout = createCheckout({
merchant: { id: 'acme', name: 'Acme', domain: 'shop.acme.example' },
catalog: [/* … */],
receiving: { x402: { chain: 'base-sepolia', address: '0xYourReceivingAddr…', asset: 'USDC' } },
policy: BALANCED(),
rails: [x402Rail({ facilitator: myFacilitator })], // inject the on-chain settler
});Start with chain: 'base-sepolia' (testnet). You'll flip it to base in the
cutover.
2. The facilitator contract
A Facilitator receives the already-structurally-validated envelope plus the quote it must
match, does the real verify + settle, and returns the outcome. It must never throw for an
expected on-chain failure — it returns ok: false with reason codes so the gate can surface
them as a clean 402 the agent can act on.
import type { Facilitator } from '@veto-protocol/checkout';
const myFacilitator: Facilitator = {
async settle({ payment, requirement, session }) {
// 1. resolve the network from requirement.chain (else UNSUPPORTED_CHAIN — never guess)
// 2. ecrecover the EIP-3009 signer; assert it === authorization.from (else SIGNER_MISMATCH)
// 3. broadcast transferWithAuthorization via CDP, idempotencyKey = the EIP-3009 nonce
// 4. return the CONFIRMED txHash as the AUTHORITATIVE reference
return { ok: true, txHash: '0x…', reasonCodes: ['OK'] };
},
};Coinbase ships the heavy lifting. The reference CDP facilitator wraps @coinbase/x402 +
@x402/core, mints a per-request CDP JWT, and broadcasts transferWithAuthorization. You
provide CDP_API_KEY_ID and CDP_API_KEY_SECRET; everything below is about getting the
details right.
3. The traps (these are the ones that actually bite)
These are ported verbatim from a proven end-to-end harness. Each one fails silently if you get it wrong — a transfer that looks signed but never authorizes, or a "failure" that already moved money. Treat them as load-bearing.
Trap 1 — the EIP-712 domain name: "USD Coin" vs "USDC"
USDC reports a different EIP-712 domain name per chain:
| Network | chainId | USDC contract | domain name |
|---|---|---|---|
| Base mainnet | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | "USD Coin" |
| Base Sepolia | 84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | "USDC" |
If you sign (or verify) transferWithAuthorization with the wrong domain name, the
signature recovers to the wrong address and the transfer silently fails to authorize.
Never hardcode one name for both chains.
The authoritative source is the live 402's extra.{name,version} — the buyer's signature
is bound to whatever the 402 advertised. Read name/version off the wire, and fall back to a
per-chain table only when the 402 didn't carry them. Verify and settle against the name the
signature was actually bound to, never a guess.
Trap 2 — the txHash is authoritative; never blind-retry
The facilitator can report success: false while the transaction still confirms on-chain,
or throw an error that nonetheless carries a txHash (broadcast happened, confirmation timed
out). The rule:
- If a
txHashexists, value moved (or is moving) — return it regardless of the success flag, and flag the outcome as "verify on-chain." - Never auto-retry when a hash exists. A fresh broadcast risks a double-pay.
// On an ambiguous facilitator response:
if (txHash) {
return { ok: false, txHash, reasonCodes: ['SETTLEMENT_FAILED'] /* verify the chain; do NOT retry */ };
}Trap 3 — idempotency = the EIP-3009 nonce
Use the EIP-3009 nonce as the settle idempotency key. The USDC contract tracks
authorizationState[from][nonce], so a given signed authorization settles at most once —
a replay is a chain no-op, not a double-pay. This is also why a re-submitted settle is safe.
Trap 4 — integer base units, never floats
USDC has 6 decimals: $0.01 == 10000 base units. Do all amount math in integer/string
base units. The quote carries amountBaseUnits as an exact string — settle against that,
never a re-derived float.
4. Settle on testnet first
Stay on Base Sepolia until a real testnet USDC transfer confirms end to end.
Get CDP credentials
Create an API key in the Coinbase Developer Platform and set both halves:
CDP_API_KEY_ID=…
CDP_API_KEY_SECRET=…
X402_NETWORK=base-sepolia # default; the live rail reads this when receiving pins no chainFund the buyer wallet with testnet USDC
x402 is paid up front by the buyer — your receiving wallet doesn't need a balance to receive. For an end-to-end test the payer (the agent's wallet) needs:
- a little Base Sepolia ETH for gas (a Base Sepolia faucet), and
- Base Sepolia USDC at
0x036CbD…CF7e(a USDC testnet faucet).
Your merchant receiving address needs nothing to accept a transfer.
Run a settle and read the txHash
Drive a checkout to settle. On success the facilitator returns a real Sepolia txHash; open
it on the explorer to confirm value landed at your receiving address:
https://sepolia.basescan.org/tx/<txHash>If you get SIGNER_MISMATCH, you almost certainly hit
trap 1 — the domain name didn't match
what the signature was bound to.
5. The mainnet cutover
Once a Sepolia settle confirms cleanly, flip to Base mainnet:
Point receiving at mainnet
receiving: { x402: { chain: 'base', address: '0xYourMainnetReceivingAddr…', asset: 'USDC' } }Mainnet USDC is 0x833589fCD6…02913 with domain name "USD Coin" — the network table and the
live 402 handle this; you only change chain.
Use a live key / live facilitator
On the hosted plane, switch from a veto_test_ to a veto_live_ key (mint it under
Dashboard → Developers) — a live key is the only thing that reaches the real facilitator
that moves USDC. In production that's the url-mode x402-rs facilitator
(FACILITATOR_MODE=url + FACILITATOR_URL), so no Coinbase CDP credentials are needed. A test
key can never touch a real chain; it's rejected with
TEST_MODE_RAIL_FORBIDDEN. Self-hosting, this is just your live
facilitator — x402-rs in url-mode, or the reference CDP facilitator with production
credentials.
Refuse to settle live on a fake rail
A live merchant must never silently fall through to a mock. If CDP credentials are missing, settlement must fail loud rather than mint a fake txHash. The hosted plane enforces this; self-hosting, guard it yourself — never default a live key to the mock facilitator.
Verify a real mainnet txHash
Run one real, small purchase and confirm the txHash on https://basescan.org/tx/<txHash>.
That on-chain hash is the settlement reference embedded in the order — the authoritative proof
value moved.
Facilitator failure modes
When settlement fails, the facilitator surfaces a specific reason code. These appear only with a real injected facilitator — the default mock does structural validation and never emits them:
| Reason | Meaning | What to do |
|---|---|---|
SIGNER_MISMATCH | Recovered signer ≠ authorization.from. | Usually trap 1; verify the domain name. |
UNSUPPORTED_CHAIN | Chain/network not in the supported table. | Pin a supported chain (base, base-sepolia); never guess a domain. |
INSUFFICIENT_FUNDS | Payer USDC balance too low. | The buyer (payer) funds the transfer, not you. |
NONCE_ALREADY_USED | EIP-3009 nonce already settled. | The auth was already spent — a replay, not a new sale. |
SETTLEMENT_FAILED | Broadcast failed. | If a txHash is present, verify on-chain and do not retry (trap 2). |
The agent never needs this page at runtime. The payment_required your checkout returns is
self-describing — it carries description (what to pay and how) and next (the literal
call to make). Docs are for you, the merchant; the wire is for the agent.
Go-live checklist
A Sepolia settle confirmed end to end
A real testnet USDC transfer landed at your receiving address, with a verifiable txHash.
Domain handling reads the wire
Name/version come from the live 402's extra, falling back to the per-chain table — never a
single hardcoded name.
Ambiguous outcomes verify, never retry
A txHash on a "failure" is treated as authoritative; no code path blind-retries a broadcast.
Live credentials present and required
CDP keys are set, and a live settle refuses to run on the mock rail.
One real mainnet purchase verified
A small live order confirmed on BaseScan before you opened the doors.
Then verify the receipt it produced and you're live.
Test mode & sandbox
Build and exercise a full checkout with zero real money — the offline mock rail, hosted test keys and mode isolation, the deterministic mock facilitator, and a sandbox loop that mirrors production exactly.
Policies & trust
Set the rules that decide which agent spends you accept — dollar caps, velocity limits, mandate requirements, reputation floors, and review routing — then publish them safely as immutable versions.