Skip to content
首页 价格 教程 常见问题 专栏

eSIM 的 MCP server —— 一份开发者指南

我们如何做出第一套面向 AI agent 的 eSIM API:token 的完整生命周期、五层消费防线、HMAC 支付验签、三个应急 kill switch,以及为什么加密货币轨道天然适合自主运行的智能体。

Roamzy 的 MCP server 是一层很薄的协议桥——大约 400 行 TypeScript,打包成一个 100KB 的 tarball——它让 AI agent 能够真的买到 eSIM,而所有与安全相关的判断都留在后端完成。Agent 的每一笔花费,都由 五道相互独立的消费限额、三个 kill switch 以及 HMAC-SHA-512 的 webhook 验签共同看守;结算走 USDT,因此我们完全不需要存储任何银行卡信息。下面把每一层拆开讲清楚:它具体怎么工作,以及我们为什么把职责边界画在这个位置。

今天绝大多数线上服务是为浏览器造的。用户坐在一个靠 session cookie 认证的页面前,点按钮、填表单,后端从头到尾都假设这套流程一定成立。AI agent 完全不吃这一套:它没有浏览器会话,没有 DOM 可点,没有摄像头去扫二维码,也没有耐心陪你走完一整套 OAuth 跳转。为 agent 做产品,意味着把认证、支付、交付这三层全部按另一套前提重新搭一遍。

这篇文章讲的是 Roamzy MCP server 背后的架构取舍。读者对象有两类:一类是正在开发、要与真实交易系统打交道的 AI agent 的开发者;另一类是其他服务的工程师——他们想知道一个所谓“agent-ready”的 API,在生产环境里到底长什么样、要付出哪些代价。

MCP server 只是一层薄壳

MCP server 本身很小——大约 400 行 TypeScript,用 esbuild 打成一个 100KB 的 tarball。它存在的唯一目的,是在两种协议之间做翻译:一边是来自 Claude Desktop / Cursor / Continue 的、基于 stdio 的 MCP 请求,另一边是发往 https://roamzy.io/api/v1/* 的 HTTPS REST 调用。仅此一件事。

MCP server 里没有任何业务逻辑。消费限额、token 校验、支付验签——统统放在后端:这样它们可以被独立测试,可以集中审计,改动的时候也不必逼着每一个 agent 用户去升级自己本地装的那份 MCP server。MCP server 就是一座协议桥,别的什么都不是。

这个边界画得对。把业务逻辑塞进 MCP server,等于把它分散到 N+1 个地方(服务端,再加上每个客户端各自装的那份 tarball),随之而来的是升级滞后,还把攻击面从机房搬到了用户的笔记本上。让 MCP server 保持为纯粹的 HTTP 客户端胶水,我们换来的是一层足够薄、足够容易审计的壳——一个有安全意识的用户大约 30 分钟就能把它从头读到尾——而我们要更新时,只需在同一个 URL 上重新发布 tarball。

Token 用 SHA-256 哈希存储,明文只显示一次

Token 的格式是 rk_live_<32-char-base64url>,Stripe 风格。前缀写死不变,这样 GitHub Secret Scanning 之类的扫描工具才认得出这是一把密钥、才能在它泄漏时报警。主体是 24 字节随机数的 base64url 编码,熵值 192 位。

创建 token 时,明文只在一个黄色警示框里向用户展示一次,关掉就再也拿不回来。服务端只保存 SHA-256 哈希值。另外我们保留 12 个字符的前缀提示(形如 rk_live_abc1)用于界面展示,方便用户在同时启用多个 token 的时候分辨谁是谁。

比对用的是 Node 的 timingSafeEqual,用来堵住计时侧信道。吊销做成软删除(写入 revoked_at),审计链路不会因为一次吊销就断掉。吊销是即时生效的:下一次拿着已吊销 token 发起的 API 调用,会在毫秒级返回 401。

五层消费防线

大多数基于 token 的系统只有一道、最多两道消费限额。我们做了五道,每一道对应的是不同的失效场景:

  1. 单 token 日限额(默认 $50 USDT,可调范围 $1-$1000)。滚动 24 小时之内的消费上限。
  2. 单 token 月限额(默认 $500,可调范围 $1-$10000)。滚动 30 天之内的消费上限。
  3. 冷静期——token 创建后的头 7 天,无论日限额被设成多少,累计消费一律封顶 $50 USDT。这条规则写死在代码里,用户自己调不高。万一 token 刚建好就泄漏出去(比如开发者手一滑,把它连同别的代码一起贴进了公开的 gist),这道线负责把爆炸半径限定住。
  4. 大额交易阈值(默认 $200)——超过这个金额的购买,必须由真人用户在 dashboard 里手动确认一次。API 会返回 big_txn_needs_confirmation,而 agent 有义务把这件事明确抛给用户,不能自己吞掉。
  5. 购买权限默认关闭——创建 token 的时候,“允许购买”这个勾选框默认是不勾上的,也就是说这个 token 只能调用只读接口。用户必须自己主动打开它。如果一个 agent 的任务只是查一查目录和资费,那么保持这个默认值就是最安全的配置。

计数器存在 api_tokens 表上,窗口采用惰性轮转:某个请求读取计数器时,如果存储的窗口标记(日限额是 YYYY-MM-DD,月限额是 YYYY-MM)和当前时间对不上,这个计数就直接按零处理。这样做既省掉了一个每日定时任务,也能容忍机器时钟的漂移。

校验会执行两次。下单的时候,服务端先做一次软校验:本次金额加上今日已花,会不会超过日限额?如果会,就用一个稳定的错误码把请求驳回,并在响应里附上还剩多少额度的提示。等到支付 webhook 真正到达,计数器才被硬性累加——也就是说,只有真金白银的 USDT 确实到账了,这笔钱才算被花掉。

支付完整性——HMAC 与唯一约束

最重要的一道反欺诈防线,是对 NowPayments 的 webhook 做 HMAC-SHA-512 验签。共享密钥(IPN_SECRET)只存在于服务端的环境变量里。每一个 webhook 报文都会带上 x-nowpayments-sig 头;服务端拿原始请求体重新算一次 HMAC,再和这个头做比对。拿不到密钥,攻击者就伪造不出一条“已支付”的报文——哪怕他知道订单 ID 和 intent ID 也没用(他多半是知道的,因为下单响应里就把这两个值返回给他了)。

第二层是数据库层面的唯一约束:ledger 表上 (ref_type, ref_id) 这个组合必须唯一。每一笔入账都被写成 ref_type="nowpayments"ref_id=<payment_id>。如果同一个 payment_id 的 webhook 被触发了两次(可能是网络重试,也可能是重放攻击),第二次插入会撞上唯一约束,应用层随即以 alreadyApplied=true 短路返回。重复入账在结构上就不可能发生。

第三层是架构层面的:只有 services/billing.ts 里的那几个函数能改动 eSIM 的余额。系统里没有任何一个管理端接口可以随手给账户加钱;连“人工激活 eSIM”这种管理工具,走的也是同一条入账路径。这把攻击面压到了最小——余额上升有且只有一条代码路径,而所有的安全检查恰恰都长在这条路径上。

应急响应用的三个 kill switch

事故一定会发生。Token 会泄漏。用户的 agent 会发疯,一口气刷出一堆订单。支付通道会宕机。所以我们做了三个相互独立的 kill switch,好让处置力度和事故规模相称,而不是一出事就把整个平台停掉:

  • 单 token 吊销——用户在 dashboard 里点一下 Revoke,只影响这一个 token。适用于某个 agent 行为异常,或者某把密钥确认已经泄漏。
  • 按用户封禁 agent——管理员带上封禁原因调用 POST /api/admin/users/:id/agent-block,该用户名下的所有 token 一起失效。适用于个别账号被滥用或者被盗的情况。
  • 全局 agent 暂停——管理员调用 POST /api/admin/agents/pause,全平台、所有用户的 Bearer token 一律返回 503。适用于基础设施事故,或者正在进行中的安全调查。

运行状态对所有 agent 公开可查:GET /api/v1/status(免鉴权,CORS *)。守规矩的 agent 会在发起购买之前先轮询这个接口,看到 purchases_paused 或者 agents_paused 为真就自动退避。对于无视状态接口、照样硬冲的 agent,Roamzy 保留限流的权利。

另外还有一个粒度更细的暂停开关:agents.purchases.paused 只拦截 POST /orders,读接口照常可用。在排查支付侧问题、又不想把信息类 API 一起黑掉的时候,这个开关特别好用。

主激活方式是二维码,不是 LPA

大多数 agent 流程,跑在与 eSIM 最终落地设备不同的机器上。agent 可能是 Mac 上的 Claude Desktop,而 eSIM 要装到用户的 iPhone 上。在这种跨设备的场景下,二维码才是正确的交付面:API 返回 qr_image_url(一个由 qrserver.com 渲染出来的 PNG 地址),agent 把它直接内嵌进对话里,用户拿手机摄像头一扫就完成了——和平时扫码付款的动作没有任何区别。

lpa_url 字段我们也会一并返回,但它只有在 agent 和目标设备是同一台机器时才有用——比如 agent 本身就跑在用户的 iPhone 上。在 iOS 17.4+ 和 Android 14+ 上,点击一个 lpa: 链接会直接唤起系统自带的 eSIM 安装程序。但对多数 agent 的使用场景来说,二维码仍然更可靠。

这是个很小的设计决定,但它相当关键:不少 MCP server 是照着“浏览器标签页”的心智模型写出来的,默认用户就坐在 agent 旁边,随手点一个链接就完事了。而消费级 eSIM 的真实使用场景是跨设备的,这个默认假设一开始就不成立。

为什么用 USDT,而不是法币

加密货币支付天然适合 agent,理由有三个,而且没有一个是一眼就能看出来的:

  1. 不必承担存卡的合规负担。存储银行卡信息需要 PCI DSS Level 1 合规,此外还要遵守卡组织关于持卡人如何为每一笔交易完成身份验证的规则。这些规则里的绝大多数,都默认键盘前面坐着一个活人——国内的支付宝、微信支付同样以“人在场”为前提来设计。加密货币把整套体系一次性绕开了。
  2. 没有拒付风险。一笔 USDT 交易只要在链上确认,就是终局,不存在“持卡人半年之后发起争议”这条路径。这一点对 agent 交易的意义,比对普通浏览器交易更大:因为实时审阅并批准了这笔支出的,未必是那个真人用户本人。
  3. 结算口径干净。agent 的预算以 USDT 计价,用户钱包里装的是 USDT,商户结算拿到的还是 USDT。中间没有法币兑换,没有汇率敞口,也没有银行工作时间带来的延迟。整条链路从头到尾都是 API 形状的。

这确实收窄了可触达的人群——大多数不碰加密货币的旅行者,不会为了买一张 eSIM 去弄 USDT。但对确实会用的那批人来说(加密原生的旅行者、由 agent 驱动的流程、把出行整个自动化掉的开发者),它一次性抹掉了一整摞摩擦。

我们没有做什么(以及为什么)

OAuth 式的细粒度 scope token。我们只有一个权限位(能买 / 不能买),没有做一长串 scope 列表。理由很简单:2026 年的 agent 还用不到“只读余额”或者“只读 eSIM”这种区分。等它们真的需要了,我们再加也不迟。过早设计出来的权限模型,制造出的 bug 往往比它防住的还多。

匿名的 agent 优先注册。目前 agent 还不能凭空创建一个 Roamzy 用户;真人必须先在浏览器里登录一次,才能签发出 token。这确实是摩擦。我们把它记在了 backlog 里,名字叫 agent-first flow(匿名建号 + magic-link 认领),但真正动手之前,我们想先看到流量数据。如果 50% 的潜在用户卡在“注册账号”这一步流失掉了,那是一个值得为之优化的信号;如果只有 5%,那就不是。

面向 agent 事件的 webhook 推送。目前 agent 是靠轮询 GET /orders/:id 来获取状态更新的。换成推送模式在规模上确实更高效,但复杂度也随之上来(agent 得自己托管一个端点,还要做验签和重试策略)。而在 USDT 大约 10 分钟的确认窗口里,按 5 秒一次轮询总共也就 120 个请求——便宜得很。

自己上手试试

如果你正在做 AI agent,想接入 eSIM 购买,最快的路径就是 MCP server。在 Claude Desktop 里 60 秒装完——见这篇教程。非 MCP 客户端可以直接看完整的 OpenAPI 3.0 规范 /api/v1/openapi.json,交互式 Swagger UI 在 /api/v1/docs,写给 agent 的长文说明在 /llms-full.txt

更深入的问题,可以找 Telegram 支持机器人 @roamzy_support_bot