eSIM Tabby

What happens when an AI agent buys an eSIM with USDC

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.

How does an agent start a purchase?

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.

How does the agent pay without a browser?

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.

Why can't an order switch from card to USDC?

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.

What breaks in production?

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.

What stops a double payment?

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.

Hosted checkout or x402 — which rail when?

Both rails end at the same fulfilment code; everything upstream differs.

What you're comparingCard (Stripe Checkout)x402 (USDC)Caveat
Who can payA human with a browserAn agent holding a walletA human with a card cannot pay an x402 order
Payment stepOpens a hosted pageSigns an authorisation inlinex402 needs a funded wallet already set up
ConfirmationOut of band, via webhookIn the response, with a tx hashCard orders are polled; x402 answers synchronously
RefundsAutomatic, to the cardManual — no on-chain railUSDC refunds are returned by hand by support
Merchant of recordStripe (handles VAT)Us — the VAT position is oursNot x402's problem to solve, but it lands on the seller
Money landsStripe balance, scheduled payoutOn-chain, at our address, instantlyOff-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.

What did the spec leave for us to decide?

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:

The 402 is the easy part; the order is the work

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.

Frequently asked questions

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.