Last updated 21 August 2026 · engineering facts quoted from the implementation
eSIM Tabby sells travel eSIM data plans to AI agents over x402, in production, on Base. The agent creates an order, receives an HTTP 402 quoting the price in USDC, signs an EIP-3009 transfer authorisation with its wallet, and a facilitator settles it on-chain. No browser, no card, no custody in between — the USDC moves from the buyer's wallet straight to ours.
Most writing about x402 explains the protocol and then reaches for the same hypotheticals: paying per LLM inference call, agents tipping agents. x402.org reports 75.4 million transactions and $24.2 million in volume over 30 days (checked 21 August 2026), yet names no product you could point at. This article is the other kind of writeup: what one production seller sends and receives, byte for byte, which design decisions the spec left to us, and what has broken since going live. I run eSIM Tabby and wrote the payment code, so every shape below is quoted from the implementation, not paraphrased from the spec.
One honesty note before the details: we publish no volume figures here because we don't have audited ones to share. The claims that follow are about mechanism, and each carries the date it was checked.
eSIM Tabby is an MCP server — an agent connects to
https://esimtabby.com/mcp and gets eleven tools covering search, purchase, activation
and refunds. Payment rail is chosen at order creation with one parameter. The tool
description the agent reads says it plainly:
'card' returns a hosted checkout link for a human to open in a browser. 'x402' returns an x402_url for a wallet tool to settle in USDC, with no browser step — only choose it if you have a wallet.
Calling create_checkout(plan_id, pay_with="x402") returns:
{
"order_id": "aB3xK9mQ2LpZ",
"price": "14.00 USD",
"reused": false,
"x402_url": "https://esimtabby.com/pay/aB3xK9mQ2LpZ/x402",
"how_to_pay": "Make an x402 request to x402_url with your wallet tool — Coinbase's is named make_http_request_with_x402 (method POST). Only call pay_order('aB3xK9mQ2LpZ') instead if this MCP client itself signs x402 payments — a wallet that is a separate tool cannot pay that way."
}
Two details are load-bearing. The price comes from the live plan catalogue, never from
a tool argument — an agent cannot quote itself a discount. And how_to_pay
names Coinbase's wallet tool and the HTTP method explicitly, because we learned that
"use your wallet tool" is not enough of a pointer for a model to match against its own
tool list. More on that under what breaks.
Calling the same tool again for the same plan within 30 minutes returns the same order
with "reused": true instead of minting a duplicate — agents retry, and
each retry must not be a second bill.
The agent POSTs to the x402_url with no payment attached and gets the 402
that gives the protocol its name. Per the
x402
v2 specification, the quote travels base64-encoded in a
PAYMENT-REQUIRED header. Decoded, ours looks like this for a $14.00
plan:
{
"x402Version": 2,
"error": "Payment required for this order",
"resource": {
"url": "https://esimtabby.com/pay/aB3xK9mQ2LpZ/x402",
"description": "Japan 5GB 30 days",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "14000000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x…our payout address…",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2" }
}
]
}
Reading it field by field: network is Base mainnet in
CAIP-2 form. asset is
Circle's
USDC contract on Base. amount is "14000000" because USDC
has six decimals — $14.00, and a wrong scale here over- or undercharges by 100×, which
is why a test pins that exact string. extra carries the
EIP-712 domain of the USDC
contract, and it is per-chain for a reason that cost us a morning — see below.
The wallet signs an EIP-3009 transferWithAuthorization over those terms
and retries the POST with the signed payload base64-encoded in a
PAYMENT-SIGNATURE header:
{
"x402Version": 2,
"resource": { "…": "as quoted" },
"accepted": { "…": "the offer being satisfied" },
"payload": {
"signature": "0x…",
"authorization": {
"from": "0xBuyer…", "to": "0x…payTo…", "value": "14000000",
"validAfter": "…", "validBefore": "…", "nonce": "0x…"
}
}
}
The buyer signs; nobody transacts yet. Our server does no cryptography at all — it
forwards the payload to a facilitator (Coinbase's CDP facilitator in production,
https://x402.org/facilitator on testnet) which answers two questions in
order: POST /verify — is this signature valid and sufficient? — and only
then POST /settle, which broadcasts the transfer on-chain.
Verify-then-settle is deliberate: settlement puts a transaction on a chain, and a
payload that was never going to be valid should not get that far.
On success the agent gets its receipt, with the settlement object mirrored in a
PAYMENT-RESPONSE header:
{
"order_id": "aB3xK9mQ2LpZ",
"status": "paid",
"transaction": "0x…on-chain tx hash…",
"next_step": "Payment settled. The eSIM is being issued — poll this order to collect the activation QR code."
}
From there it is ordinary commerce: the provider order is placed, and
get_esim_activation returns the QR code and the manual SM-DP+ details. We
verified the whole chain end to end on 5 August 2026 — a Japan 100MB plan quoted at
$0.90, settled on Base Sepolia and fulfilled with a live ICCID.
There is a second door to the same settlement path: a natively x402-aware MCP client
can skip HTTP and call the pay_order tool, carrying the signed payment in
the JSON-RPC request's _meta["x402/payment"] key. One tool, three result
shapes — the 402 quote when called bare, the settled order with a transaction hash, or
the unchanged order plus a result note when it no longer needed paying. In
practice almost every wallet today is a separate tool that pays URLs, which is why the
URL leads everywhere and the tool is barely advertised.
An order keeps the payment rail it was first given. Ask for USDC on an order that already has a live Stripe checkout link and the server refuses, both when quoting and when settling:
order aB3xK9mQ2LpZ is already awaiting a card payment at its checkout link. Cancel it and create a new order to pay in USDC.
The reasoning, from the code comment enforcing it: a live checkout link plus a USDC quote is two ways to pay for one eSIM. The human opens the link the agent handed over earlier while the wallet settles, and whichever payment loses buys nothing — and if the loser is the USDC transfer, it cannot be refunded on-chain. The check lives at the quoting layer, not just where the rail is chosen, because a checkout URL handed out in an earlier conversation outlives the call that produced it.
The reverse switch is free, deliberately. An order awaiting USDC has no Stripe session at all — one is only opened when a card is requested — so asking to pay by card later simply opens the session, which is itself what closes the wallet rail. Not opening sessions eagerly also kills a whole bug class: an unused Stripe session expires after an hour, and a stray expiry event must never land on an order that was already paid in USDC.
Three dated incidents, each of which shaped the code.
The most common wallet couldn't read a spec-correct 402 (12 August
2026). Coinbase's payments-mcp wallet (v2.12.1) routes requests through its
own proxy, which drops the v2 PAYMENT-REQUIRED response header — and its
fallback reads only the v1 JSON body. A v2-only server is unpayable by that wallet even
when its 402 is spec-perfect; we verified this on the wire against a live order. The
fix: our 402 now answers in both generations at once — v2 in the header, the same quote
in v1 shape in the body — and accepts either PAYMENT-SIGNATURE (v2) or
X-PAYMENT (v1) coming back. The signature inside is the same EIP-3009
authorisation either way; only the JSON around it differs, so v1 support is a
translation at the door and the facilitator only ever sees v2.
The EIP-712 domain is per-chain, and we advertised one name everywhere
(13 August 2026). Circle names the USDC contract "USDC" on Base Sepolia and
"USD Coin" on Base mainnet. The wallet signs over whatever domain the seller
advertises, so one hardcoded name meant every mainnet signature was refused at
/verify — and the refusal reads like a wallet bug, not a server bug. The
domains are now per-chain, proven against each contract's own
DOMAIN_SEPARATOR(). If a new chain misbehaves, check the domain first.
A buyer got stranded between the tool and the wallet (12 August
2026). An agent held Coinbase's make_http_request_with_x402 tool,
met our pay_order tool, and retried pay_order against the 402
three times without ever engaging the wallet — because the wallet is a separate tool
that pays URLs, and our responses steered it at the MCP transport only a
natively-signing client can complete. Even after the steering flipped, "use your wallet
tool" wasn't enough; the model only connected the dots once the hint named the tool and
the HTTP method. Agents are literal readers: error strings are user interface.
A fourth lesson, cheaper but worth having: refusals must be logged server-side. Early
on, a /verify rejection travelled back inside the buyer's wallet tool and
died there — four refused payments in one morning and no record on our side of why.
The dangerous window is not the status flip after settlement — it is the facilitator
round-trip before it. Two callers can both pass the "is this order still pending?"
check, both reach /settle, and the chain has no opinion about the second
transfer being pointless. The scenario is not exotic: a client that times out, re-signs
and resends while the first request is still in flight is exactly this, and the second
USDC transfer cannot be reversed.
So settlement holds a database row lock across the entire facilitator call, taken with
nowait — the loser hears "another payment for this order is being settled
right now; nothing was charged for this one" immediately, rather than blocking a minute
to be told the order is paid. A threaded test pins the property that the second payer
never reaches the facilitator at all.
A replayed payment after settlement isn't treated as an error either. The order is in
a perfectly good state, and telling the agent which state — "Nothing was charged
— this order is no longer awaiting payment" alongside the current status — is
more use than a failure it has to interpret. Underpayment is structurally impossible
rather than checked after the fact: the requirements sent to the facilitator are
rebuilt from the order server-side on every settlement, never taken from the client's
copy, so a wallet that edits its accepted block to quote itself $0.000001
still gets verified against the full price.
Both rails end at the same fulfilment code; everything upstream differs.
| What you're comparing | Card (Stripe Checkout) | x402 (USDC) | Caveat |
|---|---|---|---|
| Who can pay | A human with a browser | An agent holding a wallet | A human with a card cannot pay an x402 order |
| Payment step | Opens a hosted page | Signs an authorisation inline | x402 needs a funded wallet already set up |
| Confirmation | Out of band, via webhook | In the response, with a tx hash | Card orders are polled; x402 answers synchronously |
| Refunds | Automatic, to the card | Manual — no on-chain rail | USDC refunds are returned by hand by support |
| Merchant of record | Stripe (handles VAT) | Us — the VAT position is ours | Not x402's problem to solve, but it lands on the seller |
| Money lands | Stripe balance, scheduled payout | On-chain, at our address, instantly | Off-ramping USDC is a separate problem |
That refunds row deserves a sentence, because it is the part protocol explainers skip
entirely. An on-chain transfer has no reverse. Sending money back would mean custodying
a signing key with USDC behind it, so a cancelled USDC order goes to a
refund_pending state: the eSIM is taken back from the provider first, and
a person returns the payment to the wallet that paid. Cancellation itself works the
same on both rails — refundable until the profile
is installed — only the return trip differs.
The v2 spec defines message shapes and the facilitator API. It deliberately does not define a seller's order model, and that is where most of our code lives:
pay/<order_id>/x402 for a
pending order. A settled order is never even quoted a price — quoting one invites
someone to pay it, and settlement would refuse them only after they had signed.Returning a spec-correct 402 took an afternoon. Production x402 was everything around it: locking an order to one rail, holding a lock across settlement, speaking two protocol generations because the popular wallet needs the old one, getting a per-chain signing domain right, writing error strings an agent can act on, and deciding what a refund means when the money moved on a chain with no reverse gear. None of that is in the spec, and all of it is where the incidents happened.
If you're building a seller: start from the order model, not the protocol handshake, and assume the error strings you write are the only documentation the buying agent will ever read.
Want to watch an agent do this?
Connect eSIM Tabby to Claude — claude mcp add --transport http esimtabby
https://esimtabby.com/mcp — and ask for a plan with pay_with="x402". The
machine-readable server details, including the x402 configuration, are in the
server card. Setup for Claude desktop
and ChatGPT is on the homepage.
What is a production example of x402?
eSIM Tabby (esimtabby.com) sells travel eSIM data plans over x402: an AI agent orders a plan, receives an HTTP 402 quoting USDC on Base, signs an EIP-3009 authorisation, and Coinbase's facilitator settles it on-chain. The eSIM activation QR code is returned in the same conversation. Live as of August 2026.
How does an AI agent pay for something with USDC without a browser?
Over x402, the seller's HTTP 402 response carries payment terms — amount, asset, recipient, network. The agent's wallet signs an EIP-3009 transfer authorisation over exactly those terms and resends the request with the signature attached. A facilitator verifies it and broadcasts the transfer; no checkout page is involved.
Which network and asset does eSIM Tabby accept over x402?
USDC on Base (CAIP-2 eip155:8453), using the exact scheme from the x402 v2 spec, with Coinbase's CDP facilitator handling verify and settle. Development runs against Base Sepolia and the free x402.org facilitator. Amounts are quoted in USDC's six decimals — $14.00 travels as "14000000".
Can a human pay an x402 order with a card?
No. An order keeps the rail it was created on: an x402 order can only be settled by a wallet, and asking for a card later simply opens a Stripe session on the same order — which is what closes the wallet rail. A card order can't be paid in USDC without cancelling and reordering.
Are x402 payments refundable?
The purchase is refundable until the eSIM profile is installed, same as a card order. The return trip differs: card refunds are automatic through Stripe, while USDC has no on-chain refund rail, so support returns the payment to the paying wallet manually and the order sits in a refund_pending state until then.
Does x402 v1 or v2 matter for compatibility?
In practice, yes. eSIM Tabby answers each 402 in both shapes — v2 in the PAYMENT-REQUIRED header, v1 in the body — because a widely used wallet's proxy drops the v2 header (verified 12 August 2026). The signed authorisation inside is identical; only the JSON envelope differs.