演示模式:面向 eSIM 运营商的参考店铺。价格为实时价格,支付已关闭。 运行你自己的运营商 →
Travel eSIM · demo
中文

SDK 参考

目录

现有 eSIM 运营商把自己的系统接入 AIMarket 轨道所需的一切:软件包、供应商接口、适用于任意语言系统的 HTTP 供应商、卖家节点、webhooks、esim.*@v1 契约和一致性检查。

其他文档:概览 · 接入指南 · 集成 · 运营者指南 · 智能体指南

1. 软件包

软件包内容安装
@aimarket/esim-railsSDK:卖家节点、供应商、webhooks、schema、esim-rails check发布后用 npm i @aimarket/esim-rails;在此之前从本仓库的克隆中 npm i ./rails
aimarket-hub(PyPI)aimarket node:把你收录到各枢纽的联邦节点pip install aimarket-hub==3.15.26,或套件的 node/ 镜像
JSON Schema面向任意语言的契约rails/spec/esim-v1/*.schema.json
Docker 镜像店铺、卖家节点、联邦节点由 docker compose up --build 构建

Node.js 20 或更高版本。SDK 为 ESM。

import {
  createNode, ed25519Signer, buildPaywall, fileStore,          // the seller node
  httpSupplier, esimAccessSupplier, airaloSupplier, esimGoSupplier, createEsimAccessClient,   // suppliers
  createWebhooks, verifyWebhook,                                // your systems
  validate, checkSeller,                                        // the contract
} from "@aimarket/esim-rails";

2. 供应商接口

供应商就是 eSIM 的来源。店铺和卖家节点只通过五个方法与它交互。

方法返回规则
catalog()Plan[]你销售的套餐,按你的成本
order({ transactionId, planId, costUsd }){ orderRef }按 transactionId 幂等;当 costUsd 已不是你的成本时拒绝
profiles({ orderRef }) 或 profiles({ profileId })Profile[]配置文件生成期间返回 []
cancel({ profileId })true仅限未使用的配置文件
balance(){ usd } 或 null无需监控余额时返回 null
Plan     { id, name, countries: ["FR", …], kind?: "country" | "region" | "global", data_bytes, days,
           speed?, networks?, top_up?, unused_valid_days?, cost_usd }
Profile  { profileId, orderRef, activationCode, iccid?, qrCodeUrl?, installUrl?, apn?, expires?,
           state?, installed?, dataUsedBytes?, dataTotalBytes?, activated? }

抛出 SupplierError(code, message, { retryable }):retryable: true 表示“暂时不行”(故障、频率限制); 否则表示“不行”(未知套餐、价格变动、余额不足)。“不行”永远不会向买家扣款。

在你自己代码中的供应商:

import { SupplierError } from "@aimarket/esim-rails";

export const mySupplier = {
  async catalog() {
    return (await db.plans.active()).map((p) => ({
      id: p.sku, name: p.title, countries: p.countries, kind: p.region ? "region" : "country",
      data_bytes: p.gb * 1024 ** 3, days: p.days, cost_usd: p.cost }));
  },
  async order({ transactionId, planId, costUsd }) {
    const plan = await db.plans.get(planId);
    if (!plan || plan.cost !== costUsd) throw new SupplierError("price_changed", "the plan changed");
    const o = await db.orders.upsertByTransaction(transactionId, planId);   // the same id, the same order
    return { orderRef: o.id };
  },
  async profiles({ orderRef, profileId }) {
    const rows = orderRef ? await smdp.profilesOfOrder(orderRef) : [await smdp.profile(profileId)].filter(Boolean);
    return rows.map((r) => ({ profileId: r.id, orderRef: r.order, activationCode: r.lpa, iccid: r.iccid, state: r.state }));
  },
  async cancel({ profileId }) { await smdp.release(profileId); return true; },
  async balance() { return null; },                                       // your own stock: nothing to watch
};

内置:esimAccessSupplier(createEsimAccessClient({ accessCode, secretKey }))、airaloSupplier({ clientId, clientSecret })、 esimGoSupplier({ apiKey }) 和 httpSupplier({ url, secret })——配置与注意事项见集成。

3. HTTP 供应商:你的系统,任意语言

用 SUPPLIER=http、SUPPLIER_URL 和 SUPPLIER_SECRET_FILE 运行套件,并在你的系统中实现五个端点。 完整契约见 rails/spec/supplier-http.md。

方法与路径请求体响应
GET /catalog—{"plans": [Plan]}
POST /orders{"transaction_id", "plan_id", "cost_usd"}{"order_ref"}
GET /profiles?order_ref=… 或 ?profile_id=…—{"profiles": [Profile]}(snake_case)
POST /profiles/{profile_id}/cancel{}{"cancelled": true}
GET /balance—{"usd": 250.0},或 404

每个请求都带有 X-Kit-Timestamp 和 X-Kit-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + METHOD + " " + path + "." + body)>。拒绝签名错误以及超过五分钟的请求。

sequenceDiagram
  participant K as 套件
  participant Y as 你的系统
  K->>Y: GET /catalog(已签名)
  Y-->>K: 带 cost_usd 的套餐
  K->>Y: POST /orders {transaction_id, plan_id, cost_usd}
  Y-->>K: {order_ref}   (相同 transaction_id → 相同 order_ref)
  loop 直到就绪
    K->>Y: GET /profiles?order_ref=…
    Y-->>K: [] … 然后 [带 activation_code 的配置文件]
  end
  K->>Y: POST /profiles/{id}/cancel   (仅在付款结算失败时)

一个最小的 Python 接收端(Flask),包含最重要的两个端点:

import hashlib, hmac, json, time
from flask import Flask, request, jsonify, abort

SECRET = open("/run/secrets/kit_supplier").read().strip().encode()
app = Flask(__name__)

@app.before_request
def signed():
    ts, sig = request.headers.get("X-Kit-Timestamp", "0"), request.headers.get("X-Kit-Signature", "")
    path = request.full_path.rstrip("?")
    msg = f"{ts}.{request.method} {path}.".encode() + request.get_data()
    want = "v1=" + hmac.new(SECRET, msg, hashlib.sha256).hexdigest()
    if abs(time.time() * 1000 - int(ts)) > 300_000 or not hmac.compare_digest(want, sig):
        abort(401)

@app.get("/catalog")
def catalog():
    return jsonify(plans=[{"id": p.sku, "name": p.title, "countries": p.countries, "kind": "country",
                           "data_bytes": p.bytes, "days": p.days, "cost_usd": float(p.cost)} for p in active_plans()])

@app.post("/orders")
def orders():
    body = request.get_json()
    plan = find_plan(body["plan_id"])
    if plan is None or float(plan.cost) != body["cost_usd"]:
        return jsonify(error="price_changed"), 409
    order = upsert_order(body["transaction_id"], plan)          # idempotent by transaction_id
    return jsonify(order_ref=order.ref)

包含全部五个端点的 JavaScript 参考实现:rails/examples/operator-backend.mjs。

4. 卖家节点

const node = createNode({
  supplier,                         // a supplier (section 2) or an eSIM Access client
  store: fileStore("/data/orders.json", fs),     // or your own {get, put}: order records, never codes
  secret: statusSecret,             // 16+ characters: the key of the buyers' status tokens
  signer: ed25519Signer(pem),       // your node's identity: signs every free answer
  publicUrl: "https://esim.example.com/agents",
  paywall,                          // buildPaywall(...) below, or null to quote without selling
  events,                           // createWebhooks(...) or { emit() {} }
  pricing: { marginPct: 100, minMarginUsd: 1 },
  sellerName: "My eSIM", productId: "esim-mine", demo: false,
});
const paywall = await buildPaywall({
  cdpKeyFile: "/run/secrets/cdp_api_key.json", payTo: "0xYourBaseAddress", publicUrl: "https://esim.example.com/agents",
  priceOf: (offerId) => node.priceOf(offerId), onSettleFailure: (p) => node.settleFailed(p),
});
node.app.listen(9490);

路由:POST /quote 和 POST /status(另有供枢纽使用的 /quote/invoke、/status/invoke)、POST /order (x402)、GET /listing.json、GET /healthz。免费路由允许任意网页调用(CORS);下单路由不允许。

5. Webhooks

设置 WEBHOOK_URL 和 WEBHOOK_SECRET_FILE(16 个字符以上),或传入 createWebhooks({ url, secret })。

事件何时
order.created买家在店铺发起订单
order.delivered已付款并交付
order.refund_due已付款但供应商拒绝:请退款给付款方
order.underpaid收到低于价格的付款
order.expired24 小时内未收到付款
order.awaiting_payment签名付款结算失败;订单重新开放
agent_order.delivered智能体的订单已交付
agent_order.cancelled智能体的付款结算失败;eSIM 已取消
POST /hooks/esim HTTP/1.1
Content-Type: application/json
X-Kit-Event: order.delivered
X-Kit-Timestamp: 1765432100000
X-Kit-Signature: v1=5d41402abc4b2a76b9719d911017c592…

{"id": "3f1c…", "type": "order.delivered", "created_at": "2026-10-14T10:46:24.000Z",
 "data": {"order_id": "Qh3…", "status": "delivered", "offer_id": "JP_5_30", "offer_name": "Japan 5GB 30Days",
          "price_usd": "6.40", "cost_usd": 3.2, "method": "tron",
          "payment": {"ref": "tron:…", "payer": "T…", "units": "6400042"}, "esim_tran_no": "26101410460022",
          "error": null, "created_at": "2026-10-14T10:45:58.000Z"}}

签名覆盖 timestamp + "." + body。投递至少一次(约半分钟内尝试三次):按 id 去重。事件中永远不包含激活码、 二维码链接或订单令牌。

import { verifyWebhook } from "@aimarket/esim-rails";
app.post("/hooks/esim", express.text({ type: "*/*" }), (req, res) => {
  const ok = verifyWebhook({ secret, timestamp: req.get("x-kit-timestamp"), signature: req.get("x-kit-signature"), body: req.body });
  if (!ok) return res.sendStatus(401);
  crm.record(JSON.parse(req.body));
  res.sendStatus(204);
});
def kit_event_ok(secret: bytes, timestamp: str, signature: str, body: bytes) -> bool:
    want = "v1=" + hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return abs(time.time() * 1000 - int(timestamp)) < 300_000 and hmac.compare_digest(want, signature)

6. 契约:esim.*@v1

能力输入 schema结果 schema
esim.quote@v1quote-inputquote-result(含 offer)
esim.order@v1{offer_id} + x402delivery
esim.status@v1status-inputstatus-result

Schema:rails/spec/esim-v1。免费响应为 {"success": true, "result": …}, 带 X-Provider-Signature:对 {capability_id, product_id, input_sha256, result} 的规范 JSON 所做的 Ed25519 签名,base64 编码(按键排序、无空格、非 ASCII 原样保留;input_sha256 是规范输入的十六进制 SHA-256)。下单以 402 回应,并带 x402 v2 PAYMENT-REQUIRED 头:方案 exact、网络 eip155:8453、 USDC、6 位小数的套餐价格、你的地址。

在你自己的测试中验证:import { validate } from "@aimarket/esim-rails"; validate("offer", o) 返回 [] 或问题列表。

7. 一致性检查

把它指向任意卖家节点,无论是套件的还是你自己编写的:

npx esim-rails check https://esim.example.com/agents --country JP

它对演示节点的输出:

✓ listing.json — esim.quote@v1, esim.status@v1; order at https://modelmarket.dev/providers/esim/order
✓ esim.quote@v1 answer — 5 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.

它只调用免费路由,并向下单路由询问价格;它从不付款。

8. 环境变量

变量服务
SUPPLIERstore、railsesimaccess(默认)、airalo、esimgo 或 http
AIRALO_CLIENT_ID_FILE、AIRALO_CLIENT_SECRET_FILE、AIRALO_BASE_URLstore、railsAiralo Partner API
ESIMGO_API_KEY_FILE、ESIMGO_BASE_URLstore、railseSIM Go
ESIMACCESS_ACCESS_CODE_FILE、ESIMACCESS_SECRET_KEY_FILEstore、railseSIM Access 密钥
SUPPLIER_URL、SUPPLIER_SECRET_FILEstore、rails你的系统,用于 SUPPLIER=http
WEBHOOK_URL、WEBHOOK_SECRET_FILEstore、rails发往你系统的事件;未设置时关闭
MARGIN_PCT、MIN_MARGIN_USDstore、rails价格 = 成本 + 加价
PAY_TO_EVM、CDP_KEY_FILErails智能体付款地址,以及 facilitator 密钥
PAY_TO_EVM、PAY_TO_TON、PAY_TO_TRON、PAY_TO_SOLANA、EVM_CHAINSstore用户付款地址
DEMO_MODEstore、rails1:提供报价和页面,不销售
PUBLIC_URL、PRODUCT_ID、SELLER_NAME / STORE_NAME两者你的身份

9. 版本

esim.*@v1 只增不改:可能出现新的可选字段,已有字段保持含义不变。会破坏客户端的变更将成为 esim.*@v2, 与 v1 并列发布。SDK 遵循 semver;HTTP 供应商契约为第 1 版。

GitHub 上的套件 →