Demo mode: a reference storefront for eSIM operators. Prices are live, payments are off. Run your own operator →
Travel eSIM · demo
English

Agent guide

Contents

This guide is for developers of AI agents that book trips, plan travel or run errands. Your agent can find a travel data eSIM for a country, buy it with one x402 payment in USDC on Base, and hand the traveller a QR code. No account, no API key, no checkout page.

Every seller running the eSIM operator kit answers the same three calls at https://<seller domain>/agents/. To try them now, use the demo node https://modelmarket.dev/providers/esim: it quotes live wholesale-backed prices and refuses orders with demo_mode.

Other documents: overview · operator guide

The three capabilities

CapabilityRoutePriceDoes
esim.quote@v1POST /agents/quotefreeOffers covering a country, cheapest first. Regional and global plans that include the country count too.
esim.order@v1POST /agents/orderthe offer's price, x402Buys one eSIM. Answers with the activation code, QR link, install link, ICCID and a status token.
esim.status@v1POST /agents/statusfreeState, data used, expiry, and the activation code again, for the holder of the status token.

GET /agents/listing.json describes all three, with the seller's public key.

1. Find a seller

2. Get offers

curl -s -X POST https://modelmarket.dev/providers/esim/quote \
  -H 'content-type: application/json' \
  -d '{"country":"FR","min_gb":3,"min_days":7,"limit":3}'
InputType
countrystring, requiredISO 3166-1 alpha-2 code of the destination: FR, JP, US.
min_gbnumberSmallest data allowance.
min_daysintegerShortest validity.
max_usdnumberHighest price.
limitinteger, 1 to 50How many offers.

Always send min_gb and min_days from the trip. Without them, the cheapest offer is often a 0.5 GB one-day plan.

The answer (one offer, lists shortened):

{
  "success": true,
  "result": {
    "seller": { "name": "modelmarket.dev demo seller", "product_id": "esim-modelmarket", "demo": true },
    "offers": [
      {
        "offer_id": "EU-33_3_15",
        "name": "Europe (33 areas) 3GB 15Days",
        "coverage": { "kind": "region", "countries": ["NO", "DE", "BE", "FI", "…"] },
        "data_bytes": 3221225472,
        "data_gb": 3,
        "days": 15,
        "speed": "3G/4G/5G",
        "networks": [{ "country": "Austria", "operators": [{ "name": "A1.net", "type": "5G" }] }],
        "top_up": true,
        "unused_valid_days": 180,
        "price": { "usd": "2.03", "currency": "USDC" },
        "usd_per_gb": 0.68
      }
    ],
    "order": { "url": "https://modelmarket.dev/providers/esim/order", "method": "POST", "payment": "x402, USDC on Base" },
    "catalogue_at": "2026-10-14T10:32:44.619Z"
  }
}

3. Check the seller's signature (optional)

Free answers carry X-Provider-Signature: an Ed25519 signature, base64, over canonical JSON of {capability_id, product_id, input_sha256, result}. The canonical form has sorted keys, no spaces, and non-ASCII characters as they are. input_sha256 is the hex SHA-256 of the canonical input. The public key and product_id are in /agents/listing.json. Hubs check this signature themselves. Check it yourself when you call a seller directly:

import { createHash, createPublicKey, verify } from "node:crypto";

const canonical = (v) => Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
  : v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
  : JSON.stringify(v ?? null);

export function signedBySeller({ pubkey, capabilityId, productId, input, result, signature }) {
  const input_sha256 = createHash("sha256").update(canonical(input)).digest("hex");
  const bytes = Buffer.from(canonical({ capability_id: capabilityId, product_id: productId, input_sha256, result }));
  const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(pubkey, "base64")]);
  return verify(null, bytes, createPublicKey({ key: der, format: "der", type: "spki" }), Buffer.from(signature, "base64"));
}

4. Buy

POST <order.url> with {"offer_id": "<from the quote>"}. The first answer is 402 Payment Required with x402 v2 terms: scheme exact, network eip155:8453 (Base), USDC, the offer's price, paid to the seller. Any x402 v2 client pays it. Your wallet signs an EIP-3009 authorization and pays no gas. In TypeScript with the x402 packages:

npm i @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);   // a wallet with USDC on Base

// Keep the payment header the wrapper sends: a retry must carry the same payment, not a new one.
let paidHeaders = null;
const keepPayment = (input, init) => {
  const req = new Request(input, init);
  if (req.headers.has("PAYMENT-SIGNATURE")) paidHeaders = new Headers(req.headers);
  return fetch(req);
};
const payingFetch = wrapFetchWithPaymentFromConfig(keepPayment, {
  schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
});

export async function buyEsim(orderUrl, offerId) {
  const body = JSON.stringify({ offer_id: offerId });
  let res = await payingFetch(orderUrl, { method: "POST", headers: { "content-type": "application/json" }, body });
  // 503 allocation_pending: the eSIM is ordered but not ready yet. Same payment, a little later.
  for (let i = 0; i < 6 && res.status === 503 && paidHeaders; i++) {
    await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") || 20)));
    res = await fetch(orderUrl, { method: "POST", headers: paidHeaders, body });
  }
  const answer = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${answer.error}: ${answer.message}`);
  return answer.result;
}

What you are charged for. The seller verifies the payment, buys the eSIM, and settles the payment only after the eSIM exists. A refused order costs nothing. An order still being prepared (503 allocation_pending) is not charged yet; the same payment sent again collects the same eSIM, never a second one. A payment that fails to settle cancels the eSIM.

The answer:

{
  "success": true,
  "result": {
    "order_no": "B26101410460022",
    "esim_tran_no": "26101410460022",
    "iccid": "8985…",
    "activation_code": "LPA:1$…",
    "qr_code_url": "https://…",
    "install_url": "https://…",
    "apn": "internet",
    "expires": "2027-04-12T10:46:24+0000",
    "status_token": "…",
    "offer": { "offer_id": "TR_0.5_1", "name": "Turkey 500MB 1Day", "…": "…" }
  }
}

The activation code is a secret. Anyone who has it can install the eSIM. Give it, the QR link and the install link to the traveller only. Do not log them, and do not put them in a shared chat, a ticket or a calendar event. Keep esim_tran_no and status_token to check the eSIM later.

5. Errors

StatuserrorMeaningDo
400unknown_offerThe offer_id is not in the catalogue (anymore).Quote again.
402(x402 terms)Payment required, or the payment was refused: wrong amount, not enough USDC, expired.Pay, or fix the wallet.
403demo_modeA demo node. It sells nothing.Use a seller that sells.
409supplier_<code>The wholesaler refused the order: plan withdrawn, price changed. You were not charged.Quote again.
503allocation_pendingOrdered, not ready yet. Not charged yet.Same payment after Retry-After seconds (example above).
503catalogue_unavailable, payments_offThe seller cannot sell right now.Another seller, or later.
500internal_errorThe order failed. You were not charged.Later, or another seller.

6. Help the traveller install it

7. Check usage

curl -s -X POST https://<seller>/agents/status -H 'content-type: application/json' \
  -d '{"esim_tran_no":"26101410460022","status_token":"…"}'

The answer has state (the wholesaler's own value: GOT_RESOURCE means not installed yet; others include IN_USE, USED_UP and CANCEL), installed, data_used_bytes, data_total_bytes, activated, expires, and the delivery fields again. A wrong token gets 404 not_found, the same as an unknown eSIM.

8. Good manners

The kit on GitHub →