Featured image of post Agent 的钱包不设防:x402 时代没人做的记账活,以及 60 行 Go 代码怎么补上

Agent 的钱包不设防:x402 时代没人做的记账活,以及 60 行 Go 代码怎么补上

Bankrbot 被一条摩尔斯电码推文转走约 17 万美元,没丢私钥也没爆漏洞——预算写在 prompt 里等于没写。x402 让 Agent 一次 HTTP 往返就扣真钱,本文从 Go SDK 内置 spend controls 出发,补齐单笔上限之外的滚动窗口账本、per-payee 限额和 drain 告警,附可抄代码骨架与五层控制放置自查表。

2026 年 5 月,攻击者在 X 上发了一条藏摩尔斯电码的推文。Grok 把它公开解码了,然后 @bankrbot——这个持有真实资金的 DeFi agent 把解码结果当成了主人的授权指令,转走了约 30 亿 DRB 代币,Ledger 估算价值 约 17.4 万美元(各家报道在 15 万到 20 万之间,后追回约八成)。没有私钥被盗,没有合约漏洞。它的"预算"活在模型的指令里,而那条指令是攻击者写的。

现在 x402 协议正在让这种事变成日常:一次 HTTP 往返,402 状态码回个报价,agent 签个名,钱就出去了。没有账单页,没有消费短信,没有人审批。这篇写给刚给 agent 接上钱包(或者正准备接)的后端同学:你的支出控制该放在哪一层,以及 Go SDK 给了你一半答案、剩下那一半怎么自己补。

一次 402 往返,钱就没了——整条人类风控链被砍掉了

x402 是 Coinbase 在 2025 年 5 月发布的开放标准(Apache-2.0,2026 年 4 月起由 Linux Foundation 接管),做的事一句话:复活那个从来没用上的 HTTP 402 状态码,让 API 按次收费像下载图片一样自然。

看它的握手流程就能明白风险在哪:

x402 四步握手时序:402 报价 → 签名重试 → facilitator 链上结算 → 200 返回资源,全程没有人工审批节点

x402 请求-支付-重试时序,依据官方仓库 README 的规范步骤绘制

客户端请求资源,服务端回 402 加一个 PAYMENT-REQUIRED 头(里面是价格、代币、收款地址、链),客户端挑一个报价、用 EIP-3009 签一笔 USDC 授权,带着 PAYMENT-SIGNATURE 头重试,facilitator 验证并上链结算,约一秒后返回 200 和 PAYMENT-RESPONSE 回执。全程不需要账号、不需要 API key、不需要人工确认——这三个"不需要"正是它卖给你的东西,也是它从你的风控流程里拆走的东西。

传统 API 计费链路里,每一环都有 checkpoint:申请 key 要实名,余额不足有邮件,异常调用有网关熔断,月底有账单对账。x402 把这些全压进了一次签名。Coinbase 自己的文档称已处理超过 1 亿笔 x402 支付;第三方计数器 agenteconomy 给的口径是 1.88 亿笔、4180 万美元流水(截至 2026-09-27),Keyrock 报告则统计 2025-05 到 2026-04 共 7300 万美元、平均单笔只有 0.31 美元。三个数字互相打架——这本身就是个问题:连总量都没有统一账本,agent 自己的账本更没人管。引用任何一个数都得带上"谁测的、什么窗口",这是读 agent 支付数据的第一原则。

一次签名 0.31 美元的微支付太便宜,便宜到没人觉得需要给它记账。但乘以一万个自主循环的 agent,就是 Bankrbot 那 17 万。

写在 prompt 里的预算,攻击者说了算

复盘 Bankrbot 事件的控制失效点,会发现它不是"模型犯错",而是控制放错了层。社区安全清单(AFG 的 agent 支付安全 checklist 引用的 Ledger 分析)把 agent 的资金控制点分成五层,从上到下:

控制层 防什么 Bankrbot 案例
Prompt / 模型指令 什么都不防,只是"建议" ❌ 攻击者的推文就是在这里注入的
SDK 客户端配置 单笔金额、资产白名单 ❌ 当时没有 per-payment 硬上限
API 网关 / spend policy 速率、累计额度、对手方 ❌ 无中间层,签名直达链
TEE / HSM 密钥隔离 私钥本身不被进程读取 ✅ 密钥没泄露——但没用
链上 module / 多签 / 冷静期 最后一道资金隔离 ⚠️ 事后追回约 80% 靠的就是后续处置

规律很清楚:越靠近模型的控制,越容易被模型自己"说服"掉。提示词里写"你每天最多花 10 美元",对一段被注入的指令来说形同虚设——因为执行这句话的不是数据库,是一个概率模型,而它的输入恰好是攻击者可写的。密钥隔离做得再好,只要"花不花、花多少"这个决策在模型脑子里完成,防线就是纸糊的。

这就是本篇的核心判断:记账和安全边界必须落在签名之前、运行在模型之外。任何"能被 prompt 影响到的控制"都不是控制,是待攻破的攻击面。

Go SDK 给了你一半答案:单笔上限有了,累计账本得你自己建

好消息是 x402 官方 Go SDK 已经内置了一层模型外的硬控制。go get github.com/x402-foundation/x402/go/v2(当前 v2.27.0,2026-09-22 发布),Newx402Client() 默认开启 spend controls:只允许默认识别的 USD 锚定资产,单笔上限 1 美元。想调就调:

1
2
3
4
5
6
7
8
client := x402.Newx402Client().
    SetSpendControls(x402.SpendControls{
        MaxAmountPerPayment: "$5", // 单笔 USD 上限,留空默认 $1
        AllowedAssets: []x402.SpendControlAsset{
            {Network: "eip155:8453", Asset: "PYUSD", MaxAmountPerPayment: "500000"},
        },
    })
// DisableSpendControls() 会把所有 caps 关掉——生产环境慎用

注意两个坑。其一,SDK 文档明确说这套 controls 只管金额和资产,不管网络偏好——链的范围靠"只注册你要付的 network"来控制,未注册的链永远不会被选中。其二,也是最大的缺口:这是单笔上限,不是日预算。$1 一笔听起来安全,agent 一小时循环 60 次就是 $60。滚动窗口的累计上限只在 CDP 的商业 TypeScript SDK 里有,开源 Go SDK 需要你自建——这正是下面这 60 行要补的活。

利用 SDK 的 OnBeforePaymentCreation hook(官方文档给的示例就是"价格超用户上限就拒付"),把每笔支付先过一遍本地账本。注意一个细节:SDK 文档里的示例写的是 ctx.Requirements,而 v2.27.0 源码里这个字段实际叫 SelectedRequirements——所以下面的骨架按源码写,照抄文档示例编译会报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type SpendLedger struct {
	mu     sync.Mutex
	daily  map[string]float64 // date -> 当日累计 USD
	payees map[string]int     // 收款地址 -> 历史支付次数
	dayCap float64            // 日累计上限
}

client.OnBeforePaymentCreation(func(ctx x402.PaymentCreationContext) (*x402.BeforePaymentCreationHookResult, error) {
	req := ctx.SelectedRequirements           // 实现 GetAmount()/GetPayTo() 等 view 接口
	usd := estimateUSD(req.GetAsset(), req.GetAmount()) // 按 amount + 代币 decimals 折算
	today := time.Now().Format("2006-01-02")

	l.mu.Lock()
	defer l.mu.Unlock()

	// 规则一:日累计超限,拒付
	if l.daily[today]+usd > l.dayCap {
		return &x402.BeforePaymentCreationHookResult{
			Abort:  true,
			Reason: fmt.Sprintf("daily budget exhausted: %.2f + %.2f > %.2f",
				l.daily[today], usd, l.dayCap),
		}, nil
	}
	// 规则二:从未付过的对手方,先告警再放行(或挂起等确认)
	if l.payees[req.GetPayTo()] == 0 {
		alert.NewPayee(req.GetPayTo(), usd)
	}
	l.daily[today] += usd
	l.payees[req.GetPayTo()]++
	return nil, nil
})

三条配套纪律,比代码本身更重要:

  1. cap 触发必须打日志。spend control 静默拒付合法的高价调用,你的 agent 会表现为"莫名其妙拿不到数据"——这是无声故障,排查起来比报错难十倍。SDK 有现成的 OnPaymentCreationFailure hook,把拒付原因全部落盘。
  2. 账本只能事后看见损失,事前防线只有资金隔离。钱包里只放本周预算(float),花完即止。Drain 检测的形状特征就用 Agentic Finance Graph 那套逻辑:比平时大很多 + 从未付过的地址,两条都命中基本就是事故现场。
  3. 持久化别用内存 map。上面骨架为了可读省了存储层,生产直接换 SQLite 或 Redis,重启丢账本等于重置预算。

单笔上限防的是"一次手滑",累计账本防的是"一千次手滑",资金隔离防的是"前两层都被绕过"。三层各防一种事故,一层都省不掉。

如果你是纯 web2 团队、不碰链上支付,这套框架照样平移——Stripe 的 MPP、Google 的 AP2、甚至普通信用卡通道,失控问题一模一样:签名(刷卡)之前的控制在哪个层、能不能被模型输入影响。变的只是代码,不变的是"哪层防什么"这张地图。

已经开始有人卖"agent 的账单"了

判断一个基础设施缺口的成熟度,看有没有人把它做成生意。2026 年 9 月 26 日,Agentic Finance Graph 上线了 Desk——一个专门监测链上真实付费 agent 的产品:免费盯 3 个 agent,Operator 档 29 USDC/30 天盯 25 个,Team 档 149 USDC/30 天盯 250 个;它自己的证据包也走 x402 按次售卖,1–5 美分一份。也就是说,这个产品本身就在演示"agent 花钱买数据"这个场景,而它卖的是这个场景的可观测性。

Agentic Finance Graph Desk 定价页:Watch 免费 3 个 agent / Operator 29 USDC / Team 149 USDC,2026-09-26 报价

来源:agenticfinancegraph.com/pricing,截图于 2026-09-28

同赛道还有 BlockRunAI 的 ClawRouter——一个 agent 原生的 LLM 路由器,USDC/x402 计费,GitHub 上 6,613 stars(2026 年 2 月创建,MIT 协议)。它做的事是让 agent 自主挑最便宜的模型并逐次自付——费用行为彻底脱离人类仪表盘。一边是 agent 付钱的自动化,一边是"替你看住 agent 付钱"的 SaaS 化,中间那段空白期恰恰是你现在自建账本的窗口期:第三方监测服务盯的是别人家 agent 的公开链上行为,你自己钱包的内部预算、per-payee 限额、业务语义的异常判断,它替不了。

反过来,如果你是卖方——API 按 x402 计价对外提供服务——同样的账本逻辑可以拿来识别买家:真付费 agent 和刷量钱包在支付形状上完全不同(AFG 的分级思路是从 L0 到 L9 的行为阶梯:支付频率、对手方多样性、金额分布、是否复用固定 payee)。你的服务端完全可以用同一套 drain 检测反向跑在流量指纹上。

先把控制层问对,再动手写代码

今晚就能做的三件小事:把你的 agent 项目 grep 一遍 DisableSpendControls、MaxAmountPerPayment、budget、limit、allowlist 这五个词——如果"预算"只出现在 prompt 文件里而不是代码里,恭喜,你和 Bankrbot 同款架构;然后在任意一次工具调用处打个断点,看看这笔支付在签名前经过了几个能"说不"的检查点,答案是零的话,上面的双层限额 hook 六十行就能补上;最后翻一下你的 agent 钱包余额对比上月账单,算算现在的烧钱速度够撑几天——这个数字大概率比你以为的小。

x402 把支付变成了 HTTP 的一个状态码,但"谁能花钱、花多少、花完了谁知道吗"这三个问题,协议一个都没替你回答。记账这件事,在 agent 时代不会自动变简单,只会变得更紧急。