Демо-режим: референсная витрина для eSIM-операторов. Цены живые, оплата отключена. Запустить своего оператора →
Travel eSIM · demo
Русский

Гайд для агентов

Содержание

Этот гайд для разработчиков ИИ-агентов, которые бронируют поездки, планируют путешествия или выполняют поручения. Ваш агент может найти eSIM для поездки в нужную страну, купить её одним платежом x402 в USDC в сети Base и отдать путешественнику QR-код. Без аккаунта, без API-ключа, без страницы оплаты.

Каждый продавец на ките eSIM-оператора отвечает на одни и те же три вызова по адресу https://<домен продавца>/agents/. Чтобы попробовать прямо сейчас, используйте демо-узел https://modelmarket.dev/providers/esim: он отдаёт живые цены, основанные на оптовых, и отклоняет заказы с ошибкой demo_mode.

Другие документы: обзор · гайд оператора · онбординг · интеграции · SDK

Три возможности

ВозможностьМаршрутЦенаЧто делает
esim.quote@v1POST /agents/quoteбесплатноПредложения, покрывающие страну, от дешёвых к дорогим. Региональные и глобальные тарифы, в которые входит страна, тоже считаются.
esim.order@v1POST /agents/orderцена предложения, x402Покупает одну eSIM. В ответе код активации, ссылка на QR, ссылка установки, ICCID и статус-токен.
esim.status@v1POST /agents/statusбесплатноСостояние, расход трафика, срок действия и снова код активации — для владельца статус-токена.

GET /agents/listing.json описывает все три, вместе с публичным ключом продавца.

1. Найдите продавца

2. Получите предложения

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}'
ВходТип
countryстрока, обязательноКод страны назначения по ISO 3166-1 alpha-2: FR, JP, US.
min_gbчислоМинимальный объём трафика.
min_daysцелоеМинимальный срок действия.
max_usdчислоМаксимальная цена.
limitцелое, от 1 до 50Сколько предложений вернуть.

Всегда передавайте min_gb и min_days из параметров поездки. Без них самым дешёвым часто оказывается тариф на 0.5 ГБ на один день.

Ответ (одно предложение, списки сокращены):

{
  "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. Проверьте подпись продавца (необязательно)

Бесплатные ответы несут X-Provider-Signature: подпись Ed25519 в base64 над каноническим JSON {capability_id, product_id, input_sha256, result}. В канонической форме ключи отсортированы, пробелов нет, символы не из ASCII записаны как есть. input_sha256 — SHA-256 в hex от канонического входа. Публичный ключ и product_id есть в /agents/listing.json. Хабы проверяют эту подпись сами. Когда обращаетесь к продавцу напрямую, проверяйте её сами:

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. Купите

POST <order.url> с {"offer_id": "<из предложения>"}. Первый ответ — 402 Payment Required с условиями x402 v2: схема exact, сеть eip155:8453 (Base), USDC, цена предложения, получатель — продавец. Заплатить может любой клиент x402 v2. Ваш кошелёк подписывает авторизацию EIP-3009 и не платит за газ. На TypeScript с пакетами x402:

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);   // кошелёк с USDC в Base

// Запоминаем заголовок платежа, который отправляет обёртка: повтор должен нести тот же платёж, а не новый.
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: eSIM заказана, но ещё не готова. Тот же платёж, чуть позже.
  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 });
  }
  if (res.status === 402) {   // a refused payment says why in the x402 header, not in the body
    const terms = res.headers.get("payment-required");
    throw new Error(`402 ${terms ? JSON.parse(Buffer.from(terms, "base64").toString("utf8")).error : "payment required"}`);
  }
  const answer = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${answer.error}: ${answer.message}`);
  return answer.result;
}

За что списываются деньги. Продавец проверяет платёж, покупает eSIM и проводит платёж только после того, как eSIM создана. Отклонённый заказ ничего не стоит. Заказ, который ещё готовится (503 allocation_pending), пока не оплачен; тот же платёж, отправленный снова, забирает ту же eSIM и никогда вторую. Если платёж не удалось провести, eSIM отменяется.

Ответ:

{
  "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", "…": "…" }
  }
}

Код активации — секрет. Любой, у кого он есть, может установить eSIM. Отдавайте его, ссылку на QR и ссылку установки только путешественнику. Не пишите их в логи и не кладите в общий чат, тикет или событие календаря. Сохраните esim_tran_no и status_token, чтобы проверять eSIM позже.

5. Ошибки

СтатусerrorЗначениеЧто делать
400unknown_offerТакого offer_id нет в каталоге (уже нет).Запросите предложения снова.
402(условия x402)Нужна оплата, или платёж отклонён: не та сумма, не хватает USDC, истёк срок.Заплатите или исправьте кошелёк.
403demo_modeДемо-узел. Он ничего не продаёт.Используйте продавца, который продаёт.
409supplier_<code>Оптовик отклонил заказ: тариф снят, цена изменилась. Деньги не списаны.Запросите предложения снова.
503allocation_pendingЗаказано, ещё не готово. Пока не оплачено.Тот же платёж через Retry-After секунд (пример выше).
503catalogue_unavailable, payments_offПродавец сейчас не может продавать.Другой продавец или позже.
500internal_errorЗаказ не удался. Деньги не списаны.Позже или другой продавец.

6. Помогите путешественнику установить eSIM

7. Проверьте расход

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

В ответе state (значение оптовика: GOT_RESOURCE значит ещё не установлена; среди других — IN_USE, USED_UP и CANCEL), installed, data_used_bytes, data_total_bytes, activated, expires и снова поля выдачи. Неверный токен получает 404 not_found, так же как неизвестная eSIM.

8. Хорошие манеры

Кит на GitHub →