Agent 付费调用超时后,如何避免重复扣款
文章分析支付已完成但响应丢失时,Agent 自动重试造成重复收费的风险,并介绍 MCP 网关的四层防护。摘录具体说明 EIP-3009 授权中的 nonce 如何阻止同一签名重复转账,同时区分重新签名产生的新扣款。
AI 深度解读
AI agent 调用付费 API,钱已经付了,响应却没收到。它按失败重试,可能就为同一件事再付一次钱。问题出在「请求没返回」与「付款没成功」之间的判断空隙。
作者团队运营一个按次收取 USDC 的 MCP 网关,用四层机制处理这个空隙:付款凭证只能使用一次、重复请求返回已有结果、结算超时后查链确认、失败调用不收费。值得借鉴的不是某个支付协议,而是如何让自动重试有明确的边界。
付款只能用一次,为什么还需要幂等键
他们的网关通过 Base 上的 x402,或 Tempo 上的 MPP 收款。
使用 x402 时,agent 并不直接转账,而是签署一份 USDC 转账授权,采用 EIP-3009,再由服务端提交。授权带有 nonce,也就是用来区分授权的一次性标识。同一个付款人的同一个 nonce,代币合约只接受一次。
因此,即使服务端丢了记录,重发完全相同的签名也不能再次完成转账。新的收费需要 agent 给出新的签名。MPP 也有一次性凭证机制,重复使用会返回 403 credential_replayed,调用不会再次收费。
但这只解决了钱的问题。agent 仍可能处于「已经付费,却拿不到结果」的状态。
为此,作者在通过网关向商店购买商品的订单支付中,支持 x402 的 payment-identifier 扩展。agent 可以附带一个长度为 16 到 128 个字符的 payment id,作为幂等键(idempotency key),标识同一次支付。
服务端按三种情况处理:
- 相同 id、相同付款,返回首次请求保存的答案,不再执行第二次 verify 或 settle,答案保留 24 小时。
- 相同 id,但首次请求仍在执行,返回
409 payment_in_flight。 - 相同 id,却换了报价、金额、收款人或付款人,返回
409 payment_identifier_conflict。
最后一种处理很关键。假如客户端换了购买内容,却误用了旧 id,直接返回旧结果会把客户端错误藏起来。明确冲突,才能让调用方知道自己的请求出了问题。
超时之后,先承认自己不知道
最难处理的不是明确成功或明确失败,而是结算已广播,负责结算的 facilitator 调用却超时了,或者交易仍在等待确认。
没有收到结算结果,不足以判定付款失败。
作者把这种情况记为 unknown,交给后台任务查链。检查内容包括付款人和 nonce 对应的授权状态,以及 USDC 转账本身。确认成功后,订单变为已支付,再交付结果;如果经过 24 小时仍未确认,订单才被最终标记为失败,并释放预留库存。
他们不会为了确认结果,再提交一次相同授权的结算。
这里要区分两件事:相同授权本身已有一次性保护;应用层仍需正确追踪未知状态,避免把超时推进成新的付款流程。前者限制资金动作,后者决定订单能不能恢复到正确状态。
对于真正失败的工具调用,他们另设了一条收费规则:只有交付响应才收费。上游拒绝输入或发生故障,都不计费。作者给出的运营数据是,上周有 1,905 次调用被供应商拒绝,全部没有收费。
这条规则不能消除重试,但能让失败后的再次调用有合理的收费基础。
24 小时后,钱和结果有不同的保障
结果缓存不是永久保存的。
如果 agent 在 25 小时后带着相同 payment id 回来,它已经拿不到保存的答案。旧签名仍然不能再次完成付款,但如果它确实需要重新调用,就得决定是否为一次新调用重新付费。
因此,「不会重复扣款」需要说清范围:同一份付款授权不能重复使用,不等于同一项业务工作永远只会收费一次。如果 agent 自动生成新签名、新支付请求,仍可能为相同目的再次花钱。
作者接受的取舍是,缓存覆盖现实中的重试与恢复周期,而支付的一次性保证独立存在,不依赖缓存记录。对接方至少需要知道这两个期限:请求结果能恢复多久,未知结算状态会等待多久。原文中两者都是 24 小时,但承担的职责不同。
我的判断
我认同这套分层:支付凭证管重复付款,幂等键管重复请求,状态恢复管不确定性,收费规则管失败成本。单靠一个「自动重试」开关,解决不了这些不同的问题。
不过,原文展示的是机制和一项运营统计,没有重复收费率、恢复成功率,也没有与其他实现的对照数据。1,905 次失败调用未收费,能说明这条规则实际覆盖了业务流量,不能证明整套系统已经消除了所有重复收费路径。
它还有两个需要追问的地方。第一,payment id 的结果回放明确用于订单支付,不能据此认定所有工具调用都有同样能力。第二,「只为交付响应收费」里的交付没有被精确定义:是上游产出结果、网关发出响应,还是客户端确认收到?文章开头的问题,恰好可能发生在这些环节之间。
与仅在数据库里记录幂等键相比,一次性付款凭证多了一道独立约束;但一般团队若采用其他支付方式,仍需确认对应支付渠道究竟提供什么保证,不能直接照搬链上 nonce 的结论。
能拿来做什么
- 给 Node 付费调用封装加入稳定的 payment id,同一次业务重试沿用原 id,并检查报价、金额和交易双方是否保持一致。
- 为 agent 明确区分超时、处理中和请求冲突。收到
409 payment_in_flight时进入等待或查询流程,收到409 payment_identifier_conflict时修正请求,避免盲目重试。 - 把结算状态设计成包含 unknown 的状态机,通过后台核对恢复订单;不要把一次网络异常直接映射成付款失败。
- 在接入付费工具前确认结果保存期限与收费时点,并限制 agent 在未知状态下自动创建新付款,避免一次性凭证之外出现重复购买。