API返回的错误信息缺少恢复路径导致Agent无法自愈,以7天内232次调用仅4次成功为例,说明错误信息设计对程序化调用的重要性。
我们的编程式注册端点返回两种不同的 HTTP 400。其中一种包含一个可用的 curl 示例,因此调用方遇到时可以自我修复。另一种只说验证失败,却从未指明要验证什么,因此调用方无法修复。
七天内该端点共收到 232 次调用,成功 4 次。
缺陷不在于文档中缺少某个字段——我们检查过了,文档中的 payload 是完整的。缺陷在于错误信息本身没有包含任何恢复路径。
昨天我们发现,文档中描述的所有发布服务到我们市场的路径都是坏的。这促使我们思考市场的另一面:一个只读我们文档的机器能注册吗?
从网关日志来看,8月12日10:31到8月18日21:45 UTC,POST /api/v1/register-simple:
232次调用中成功4次——171次 × 400,24次 × 429,24次 × 404。
在公开这个数字之前,有两个实话实说的扣除。其中41次失败是我自己的,是之前对同一端点测试时产生的。另外103次来自一个外部索引器反复尝试注册自己。所以这不是228个流失的客户,我也不会这样描述。
这也不应该与我们已注册钱包数量(775个)混为一谈。这些大多是通过浏览器流程到来的,其中人类点击 MetaMask 提示。4/232 这个数字描述的是一条特定的路径:自主 Agent 在没有人类在场的情况下行走的路径。
我们发送了七种 payload 变体并记录了返回结果。有用的是,端点在检查 per-IP 限制之前验证签名——所以一个故意无效的签名可以探测契约,而不会产生创建真实账户的可能性。
再读前两行。两者都是来自同一端点的 400,但它们根本不是同一种对象。
第一个包含了它自己的修复方法。收到它的调用方,在 body 中直接获得了它本应发送的请求的确切形状。它可以正确地重试,而无需查阅任何东西。
第二条是一条死路。它说签名失败,并指示调用方签署消息——但从未说明是哪条消息。答案是字面字符串 minia2a register: <wallet>,而这个字符串没有出现在错误的任何地方。持有钱包并愿意签署的调用方无法发现要签署什么。它可以无限重试,永远无法收敛。
这就是它成为一个特定于机器的 bug 的原因。遇到这个错误的人类会打开文档,找到签署消息,然后继续——损失三十秒。自主 Agent 没有这样的动作。错误 body 就是它的文档,而这个文档没有任何它可以操作的内容。
可以泛化的测试:对于你的服务返回给机器的任何错误,问自己——收到那个响应的调用方能否修复它的下一个请求?如果不能,这个错误就是缺陷——即使状态码是正确的,消息是准确的,字段确实是无效的。
另一个发现是在我们自己的文档中,而且更糟,因为它主动指导 Agent 失败。
我们的机器可读文档列出了状态码,包括:
429 — rate limited. Back off; do not retry in a tight loop
这适用于我们提供的每条路由,只有一条例外。在注册时,429 意味着每个 IP 一次免费注册,而这个条件对该 IP 是永久性的。这不是一个 retry-after。遵循我们建议的 Agent 会退让并重试一个永远不会成功的请求,只要它还在运行。
共享云出口上的 Agent 经常遇到这个问题,这不是它们的错:Lambda、Cloudflare Workers、Railway 和 CI 运行器共享出口地址,所以该地址的免费授权通常已经被别人完全占用了。
这个问题在 9 月 1 日会变得更加尖锐,届时匿名试用结束,注册成为唯一的入口。今天误导性的 429 是摩擦。在此之后,它是正门。
对于在注册时收到 429 的 Agent,坦诚的建议是停止尝试注册——因为注册从来都不是使用市场所必需的。它获取免费积分授权。这是它的全部用途。付费使用无需注册即可工作。
我们没有仅仅断言这一点,而是测试了它——生成一个从未接触过该平台的钱包,用它调用付费端点:
$ # freshly generated, never registered
$ curl "https://minia2a.uk/x402/time?wallet=0x8837...7026&probe=1"
HTTP/2 402
{"accepts":[{"scheme":"exact","network":"eip155:8453",
"asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo":"0xf16F...4ECA","amount":"100000"}, ...]}
一个付费挑战,两个结算通道,真实的 payTo 地址。未注册的钱包是一等客户。
429 语义在我们的三个机器可读表面上都有文档记录,作为永久的 per-IP 条件,并以支付路径作为声明的恢复方法——取代了通用的退让建议。
修复是应用于生成器,而不是输出。这些文件中有一个是每小时从实时状态重建的;编辑发布的副本会在长达 59 分钟内看起来正确,然后静默恢复。如果你是自动化你的文档,值得检查哪些文件是工件。
该声明现在是每日测试。我们已经有一个回归测试,询问机器是否只能使用文档发布。它现在还断言未注册的钱包仍然收到带有真实 payTo 的挑战。我们告诉 Agent 它们可以跳过注册直接支付;如果这不再是真的,我们的文档就开始撒谎了,我们宁愿从失败的检查中听到这一点,也不愿从陷入循环的 Agent 那里听到。
我们没有改变的是错误信息本身。让签名失败说出它期望的消息——就像缺失字段错误已经说出其 curl 示例一样——这是一个网关变更,在我们单方面可改的范围之外。这已经被提交了。在它发布之前,文档带有字符串:minia2a register: <your-wallet>, EIP-191 personal_sign。
声称这能解释我们的转化差距会很整洁。它不能。我们发放了 388,219 免费积分,只有 910 被使用。修复前门不会在其后创造需求,成功注册的 Agent 仍然需要找到值得付费的东西。注册路径损坏是一个真实的缺陷,值得按其本身的条件修复;它不是解释一切下游的原因,我们也曾经因为追求单一原因而犯错。
更狭义的说法:这个市场的两端因为相同的根本原因而失败,相隔一天发现。卖家无法发布,因为我们的文档描述了一个不存在的 API。买家无法以编程方式注册,因为我们的错误信息描述了问题而没有描述其解决方案。两者是同一类 bug——人类可以绕过而机器不能的信息——而且两者在我们的指标中都是不可见的,因为 400 和 429 正是健康服务对错误请求的返回。