If you resell travel eSIMs, you probably have a wholesale account with eSIM Access, eSIM Go or Airalo Partners, and a checkout built for people: pick a country, pay by card, scan a QR code. An AI agent that books a trip cannot use that checkout. It needs an endpoint that answers "3 GB in Japan" with priced offers, takes a payment without an account, and returns the activation code in the same response.
The eSIM operator kit (MIT) is that endpoint plus a storefront for people, built around your existing supplier account. Until this week, though, it asked for a lot before showing anything. The first step wanted a server, a domain with an A record, free ports 80 and 443, and your supplier's API keys. We cloned our own repository as a stranger would, ran ./onboard.sh on a laptop, and it stopped at .env is missing and asked for a domain. A reseller who only wanted to see what agents would actually call had to register with a supplier first.
So the first step is now a command that needs none of that.
One command, no accounts
You need git and Docker. We ran it with Docker Engine 29.2.1 and Compose 5.1.0; the overlay for this mode uses Compose's !reset and !override merge tags, which old Compose v2 releases do not understand.
git clone https://github.com/alexar76/esim-operator-kit.git
cd esim-operator-kit
./onboard.sh --try
It builds two containers and binds them to 127.0.0.1 only: the storefront on port 9495 and the seller node, the part agents talk to, on port 9490. The supplier behind them is SUPPLIER=sample, a catalogue of 49 made-up plans across 26 countries with invented costs. The kit refuses to start that supplier unless DEMO_MODE=1 is set, and the storefront banner says the plans and prices are made up. Nothing is written to .env, secrets/ or data/: the containers keep their state on tmpfs.
On an Apple M4 with 32 GB this took 32 seconds with a cold Docker build and 6 seconds once the images were cached. At the end the script runs the same conformance check that any seller node has to pass before it joins the federation. This is its output from that run, unedited:
✓ listing.json — esim.quote@v1; order at http://localhost:9490/order
✓ esim.quote@v1 answer — 3 offers for JP, all valid
✓ cheapest first
✓ signature — Ed25519 over the request-bound canonical, key from listing.json
✓ bad input is a 400
✓ unknown eSIM is a 404 not_found
✓ unknown offer is a 400 before any price
⚠ x402 terms — a demo node: it sells nothing (demo_mode)
✓ CORS on the free routes
Conforms to esim.*@v1.
The warning is expected. A node in demo mode has no payment terms to check, because it never sells.
./onboard.sh --try --down removes the containers and the images it built. Docker's build cache is all that is left.
The same code on both sides. --try swaps the supplier for a sample catalogue and leaves out TLS, payments and the hub announcement.
What an agent sends
The quote is a plain POST, free and unauthenticated:
curl -s localhost:9490/quote -H content-type:application/json \
-d '{"country":"JP","min_gb":3}'
This is the response from the sample catalogue, shortened. The second offer and some fields are cut:
{
"success": true,
"result": {
"seller": { "name": "Sample eSIM store", "product_id": "esim-sample", "demo": true },
"offers": [
{
"offer_id": "SAMPLE-JP-3-15",
"name": "Sample · Japan 3 GB / 15 days",
"coverage": { "kind": "country", "countries": ["JP"] },
"data_gb": 3,
"days": 15,
"price": { "usd": "6.00", "currency": "USDC" },
"usd_per_gb": 2
}
],
"order": { "url": "http://localhost:9490/order", "method": "POST", "payment": "x402, USDC on Base" }
}
}
Offers come cheapest first, and the node signs each answer with its Ed25519 key, which it publishes in its listing.json. An agent that found the seller through a hub can check that the quote came from the node the hub listed.
Then the agent orders:
curl -si localhost:9490/order -X POST -H content-type:application/json \
-d '{"offer_id":"SAMPLE-JP-3-15"}'
In --try it ends here, on purpose:
HTTP/1.1 403 Forbidden
{"success":false,"error":"demo_mode","message":"demo mode: this node quotes made-up sample prices and sells nothing; connect a wholesale account to sell"}
Until 13 November that message said "quotes live prices", which was true on our public demo node and false on a laptop running sample plans. Writing this article is how we noticed. The node now says which of the two it is.
The same order on a node that sells
With a real supplier and demo mode off, that first POST /order returns 402 Payment Required with x402 terms: the amount, your wallet address, USDC on Base. The agent signs one EIP-3009 transfer authorization for exactly that amount and sends the order again with a PAYMENT-SIGNATURE header. It needs no account with you, no API key, and no ETH for gas.
An agent's purchase from a selling node, as rails/src/node.js handles it. The money moves last.
Three things in that path matter to an operator more than to the agent:
- The money moves after the eSIM exists. The node asks the facilitator to verify the signature, orders the profile from your supplier, answers with the activation code, and only then settles. If your supplier refuses the order, the agent gets
409 "the supplier refused this order; you were not charged", and an error answer is never settled. - A retry cannot buy twice. The supplier's transaction id is derived from the payment itself. An agent that times out and sends the same signed payment again gets the same eSIM, not a second one billed to your wholesale balance.
- The activation code goes to the payer only. It is in the order response and nowhere else: not in webhooks, logs or receipts. The status token lets the buyer check installation or ask for a refund of an unused eSIM later.
This path has run with real money. In October our own agent bought a Turkey eSIM through it for 0.31 USDC and got the money back for the unused profile. Both transfers are on Base, and that article has the transaction links.
From the laptop to a real account
On a server, the same script without --try asks for the domain, the store name and your supplier keys. It writes .env, starts in demo mode, and announces the node to a hub, so you can check everything before anything is sold.
| Supplier | SUPPLIER= |
Status |
|---|---|---|
| eSIM Access | esimaccess |
live-tested: real orders, cancellations and refunds |
| Airalo Partner API | airalo |
built to Airalo's published API reference; not yet run on a live Airalo account |
| eSIM Go API v2.5 | esimgo |
built to eSIM Go's published reference; not yet run on a live account |
| Your own platform | http |
five signed HTTP endpoints: catalog, orders, profiles, cancel, balance |
If you already run Airalo or eSIM Go, you would be the first to run those adapters against a live account, and we would rather hear what breaks than guess.
By default the price is your supplier's cost plus 100%, and never less than cost plus $1.00. A price list (PRICES_FILE, CSV or JSON) overrides that per offer, country, region or plan size. You remain the merchant of record: VAT, refunds policy and support are yours, as they are on your current storefront. Agents pay in USDC on Base. The storefront for people takes crypto on ten networks, and card payments can be added with your own Stripe keys.
Where this stands
We run one demo seller node on modelmarket.dev. Its catalogue comes live from eSIM Access, 1,823 plans in 205 countries when we checked on 13 November, and it refuses every order. No eSIM operator outside our team has connected a node to the federation yet, and the only agent purchases so far are our own tests.
That is also why the cheap first step matters. Before this week, finding out whether the agent side fits your business meant opening a supplier account and renting a server. Now it costs you 32 seconds on a laptop.
The kit, the guides in five languages and the conformance check are in the repository. The landing at esim.modelmarket.dev has the same three commands at the top.
We build the eSIM operator kit and run the modelmarket.dev hub. AI assistance was used for the code changes, the measurements, the figures and this article. The commands and outputs above come from runs on 13 November 2026 and can be repeated from the repository.





