← All posts

How we process x402 eSIM orders in production: the full architecture

Engineering tour of a real production x402 service — facilitator selection, Polygon + TON multi-chain settlement, payment-state machine, fulfilment, and what we got wrong.

2026-05-13·5 min read·eSIMx402 Team·x402 / production / polygon / ton / architecture

Most x402 content online is either marketing (Coinbase, AWS) or hackathon tutorials. This post is neither — it documents the architecture behind a production x402 service that has been running real eSIM orders since early 2026.

We chose eSIM as the first vertical because the unit economics fit x402 perfectly: an order is $0.55 to $50, settlement is permanent (the eSIM is provisioned once the wholesale provider issues an ICCID), and there's a real demand from AI travel agents and IoT devices that none of the legacy reseller APIs can serve.

Stack at a glance

The full request lifecycle, from POST /api/v1/x402/order to QR code delivery, touches four sub-systems:

  • API gateway (FastAPI behind Cloudflare) — parses the request, holds the order in PENDING_PAYMENT state, returns the 402 with payment details
  • Facilitator — Coinbase x402 facilitator on Polygon for USDC/USDT, and a TON-native facilitator we wrote in-house for USDT-on-TON
  • Provisioning worker — listens for payment confirmations, makes the wholesale call to eSIM Access, stores the ICCID + QR data
  • Polling endpointGET /api/v1/x402/order/{id} returns the current state with strong cache headers so polling clients don't hammer the DB

All four are stateless. State lives in Postgres (orders) and Redis (idempotency keys + payment confirmation cache).

Facilitator selection — why we run both Coinbase and a custom TON one

Coinbase's x402 facilitator service is excellent for Polygon, Base, Arbitrum, and World. It's free under 1,000 transactions per month and handles the heavy lifting: payment proof verification, replay protection, chain re-org guarding.

But TON isn't in Coinbase's facilitator coverage as of May 2026. We wrote a small TON facilitator ourselves because half of our prepaid-stablecoin users hold USDT on TON (it's the dominant USDT chain among travelers).

The custom TON facilitator is ~200 lines of Python: subscribe to TON's LiteServer for incoming USDT transfers to our deposit addresses, verify the destination matches an active order, and emit a Redis event the provisioning worker listens for.

We didn't bridge TON to Polygon because:

  • Bridging adds 60-300s latency, killing the agent-perceived response time
  • Bridge fees would eat the margin on $1-5 orders entirely
  • TON's native USDT is liquid enough that holders aren't asking us to "just bridge"

If you're building a similar service, picking facilitators per chain (rather than wrapping everything in a single bridge) is the right call for cost and latency.

The payment-state machine

An order moves through six states:

NEW              → just created, no payment yet
PENDING_PAYMENT  → 402 returned, payment window open
PAID             → on-chain payment confirmed by facilitator
PROVISIONING    → wholesale eSIM provider working
DELIVERED        → ICCID + QR code stored, order complete
EXPIRED / REFUNDED → terminal failure states

The interesting transitions:

  • NEW → PENDING_PAYMENT: idempotent. If the same client retries within 60s with the same body, we return the same order ID + same payment details. Critical for agents that auto-retry on flaky networks.
  • PENDING_PAYMENT → PAID: triggered by facilitator webhook OR by our background polling loop (whichever fires first). We re-verify the payment proof server-side even when the facilitator already confirmed — defense in depth.
  • PAID → PROVISIONING → DELIVERED: synchronous from PAID. If the wholesaler is slow, the order sits in PROVISIONING; the client just keeps polling. Median PAID→DELIVERED is 2.4 seconds.
  • PROVISIONING → REFUNDED: if the wholesaler fails 3 retries (~30s of internal back-off), we kick off an on-chain refund to the payer's address. Refund target time is 5 minutes.

Idempotency is enforced at the DB level via a unique constraint on (client_ip, plan_id, created_at_minute). Coarser than "exact body match" but eliminates the most common double-spend scenario (agent retries with same parameters).

What we got wrong (initially)

Three things broke in real traffic that the tutorials don't warn about:

1. Chain re-orgs on Polygon. First version trusted Coinbase facilitator confirmation immediately. When Polygon had a 6-block reorg in March 2026, three orders briefly appeared paid then unpaid. Now we wait for 6 confirmations on Polygon (~12 seconds extra latency) before marking PAID.

2. Wallet rounds to integer USDC. Several agent wallets we tested round payment amounts to integer USDC. Order quoted at $6.21 → wallet sends $6 → we'd reject as underpayment. Now we accept payments within ±$0.50 of the quoted amount, with the difference auto-refunded on PAID transition. The margin loss is negligible vs the conversion loss from rejected orders.

3. ICCID assignment is non-refundable. Once the wholesale provider issues an ICCID, we owe them payment — even if the user never activates the eSIM. So we delay the wholesaler call until the payment is fully confirmed. Earlier versions called the wholesaler optimistically during PENDING_PAYMENT to shave latency; that pattern cost us ~$200 in unrecoverable ICCIDs in week 1 before we caught it.

What's next

The current bottleneck isn't infrastructure — it's distribution. Most agents that would buy eSIM data don't know x402 services exist. We're testing two paths: tighter integrations with AWS Bedrock AgentCore Payments (which launched May 7) and World AgentKit. Both treat x402 as the underlying transport, which means we don't need agent-side SDK work — just clean documentation of our endpoints.

If you're building an agent that needs cellular data, the API is documented at /docs. The reference Python and Node clients we use internally will go open-source in the next two weeks. Questions: dev@esimx402.com.

RELATED