智能体支付避坑:业务幂等与整数金额
开源 MCP 服务 PurseString 的作者分享智能体支付中的常见错误及修复。已展示的措施包括为购买意图生成确定性幂等键、将幂等保护传递至 PayPal,以及用最小货币单位的整数表示金额。
AI 深度解读
AI 自动付款最容易出事的地方,可能只是一次重试、一个小数,或一条重复到达的回调。PurseString 的作者把这些支付老问题放进 Agent 场景,给出的结论是:模型可以提出付款请求,但金额计算、预算校验和审批必须交给确定的程序规则。
这篇值得看的,是四类故障怎样落到具体实现上。不过先记住边界:PurseString 还是 v0.1 alpha,只在 Lithic 和 PayPal 沙箱完成端到端测试,尚未经过真钱环境检验。
重试不能变成第二笔订单
项目的起因很直接:结账页面卡住几秒,Agent 判断付款失败,于是重试,结果两次扣款都成功了。作者没有给模型补一句「不要重复付款」,而是给购买意图建立确定的身份:
SHA256(agent_id, task_id, vendor, amount)
同一个 Agent、同一个任务,向同一商家支付同一金额,30 分钟内返回相同的购买意图和卡片,不再创建新的。
这里的幂等,指的是重复请求仍然得到同一次业务操作的结果。要去重的是一次购买,而不只是某次 API 调用。
PayPal payout 还会把幂等键继续传给 PayPal。否则,PurseString 即使在本地做了去重,也可能在「支付平台已受理、本地尚未记下结果」时崩溃,重启后再次付款。
不过,作者没解释 30 分钟为什么合适,也没说明两次同金额购买如何区分。如果一个任务确实需要向同一商家买两次,这组字段是否足够,需要结合业务判断。
预算算不准,可能是金额和状态都记错了
第一类错误来自浮点数。作者用下面的例子说明,为什么 $0.30 的预算可能拒绝先花 $0.10、再花 $0.20:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 <= 0.30 // false 😬
要看的是第二行:比较运算没有出错,参与比较的金额已经偏离了预期。
作者起初用微小容差和 toFixed(2) 修补,后来改成以货币最小单位做整数运算。USD 用 cents,JPY 用 yen,KWD 用 fils。于是 $0.10 加 $0.20 变成 10 + 20 = 30,恰好落在 30 cents 的预算内。
外部接口仍然允许在 policies.yaml 和工具调用里写 amount: 15.00,内部负责转换。像 $15.001 这样超过货币允许精度的金额,直接拒绝,不静默舍入。
第二类错误更隐蔽:刷卡获批,不等于已经扣款。
酒店可能预授权 $200,最终只扣 $150;加油站可能先冻结 $100,最后扣 $43。如果把每次授权都记成支出,预算就会被没有实际花掉的钱占满,商家重新授权时也可能被误拒。
PurseString 因此把预授权冻结(hold)与实际扣款(capture)分开。Lithic 实时询问是否批准交易,随后通过事件告知结果:扣款成功就记录实际金额、结束冻结;撤销、作废或过期就释放冻结,归还预算。
订阅卡月度限额计算的是「已扣款金额+仍在冻结的金额」,退款上限则依据实际扣款。每个事件只应用一次,支付服务重复发送 webhook,也不会重复记账。
日志要能验真,支付权限要落在模型之外
出事之后,财务需要知道 Agent 为什么买、比较过什么、触发了哪些规则、由谁批准。PurseString 会记录这些信息,但普通 JSON 文件可以被改写,有 shell 权限的 Agent 也可能接触它。
作者给日志加了哈希链:每条包含序号 seq、前一条哈希 prev 和自身 hash。编辑、删除、插入或调换中间记录,会破坏链条。
验证日志可以运行 pursestring verify-log。原文展示的报错是 BROKEN at line 2 (seq 2): hash mismatch: entry was edited。
普通 SHA-256 只能揭示不一致;有文件写权限的人,仍可能修改记录后重算整条链。设置 PURSESTRING_AUDIT_KEY,并把密钥保存在别处后,记录改用 HMAC 签名,没有密钥就无法重建合法链条。
边界也很明确:删掉日志末尾,剩余部分仍然可能完整。因此 verify-log 会输出链头哈希,需要另存到工单或 Slack 消息等位置,供日后核对。
支付权限同样不能只靠模型自律。作者增加了 PayPal、Venmo payout 通道,收款人从规则里的名单读取,Agent 不能自由指定。邮件里一句「改付到这个地址」,就没有可直接修改的收款字段。
PayPal 自身的 MCP 工具也经过 PurseString:小额退款可自动执行,大额等待人工,超过上限拒绝;接受争议或取消订阅始终需要人工。
客户邮件若写着「立刻退给我 $900」,拿到 create_refund 权限的 Agent 就可能尝试执行。支付平台并不知道这句话是不是合法指令,系统必须在工具执行边界检查规则。
我的判断
我认同作者把控制放在模型之外。对于预算、收款人和审批门槛,可验证的程序判断比提示词更适合作为最后一道关口。四类修复也有共同点:即使 Agent 判断失误,系统仍应限制后果并留下证据。
但这还是实现经验,不能当成生产可靠性的证明。作者报告了重复扣款经历和修复方案,也说明完成了沙箱测试;没有给出修复前后故障率、并发压力或长期运行的对照数据。
几个地方还需要继续追问。购买去重需要明确业务身份;webhook 只处理一次之外,事件乱序如何处理,原文没说清;HMAC 能否有效,取决于 Agent 是否真的拿不到密钥。
对普通团队,也未必需要引入整套 MCP 支付服务器。如果现有 Node 服务已经统一执行预算、收款名单和审批规则,复用现有边界同样合理。PurseString 提供的是一种集中约束 Agent 支付工具的做法,原文没有证明它优于所有现有支付架构。
能拿来做什么
- 给 AI 编程流程接入支付工具前,先在 Node 服务里落实购买去重、固定收款名单和金额上限,并检查幂等键是否传到了支付服务。
- 审查金额转换入口,让内部预算累计和比较使用货币最小单位整数,明确拒绝超精度输入;前端显示两位小数不能代替账务校验。
- 在沙箱里覆盖超时重试、重复 webhook、部分扣款和授权撤销,检查预算是否重复占用、能否正确释放。
- 想试用项目,可以按原文的 10 分钟指南
docs/try-it.md,用下面的命令接入 Claude Code。它默认启动在 mock rail;添加沙箱密钥后,再验证对应支付通道。
claude mcp add pursestring -- npx -y github:suseendar1414/pursestring
注意命令中的 --:后面的内容是启动 MCP server 的命令。项目尚未经过真钱验证,试用应使用沙箱密钥。