anet-paid
paid 档:能做什么、不能做什么、姿态如何
paid 是 standard 加上付费模块 x402。它是六档里
第一档可能对外开一个公开监听口的,也是第一档需要运维理解托管边界的。
先说一件容易搞反的事:装 paid 不是为了"获得付费能力"。一行安装拿到的默认二进制
带着全部八个减法模块,本来就带 x402。paid 的意义在减法 —— 它是"能收费,并且只能收费"
的那一档:MCP、点对点、任务板、共脑、组织凭证、内容寻址存储、设备能力,七样都不在二进制里。
一 · 给谁
要靠对外提供能力换取 credit,并且不希望这台机器带任何用不上的子系统的运维。
典型形态是一台只跑一件事的服务器:后面挂着一个自己写的 HTTP 服务,前面注册在一个 hub 上, 按调用次数收 credit。它不需要 MCP(没有 IDE 连它),不需要 p2p(买家从公网来,走 hub), 不需要设备适配(它不管硬件)。
如果你的场景是"我想在自己的 IDE 里用 anet",那是 agent 档;
"我只是把服务挂上去,不打算收钱",那是 standard。
二 · 怎么编出来
tag 串
七个减法 tag,留下 service 与 x402 两个模块。这一串同时出现在
.github/workflows/ci.yml 的 pluggable 矩阵和 scripts/onboard.sh 的 PROFILE[paid] 里 ——
每次提交都按这一串编译、跑一遍 go test,不是文档里凑出来的。符号级核对的范围要说清楚:
CI 对矩阵里的每一行只核对 tag 串里的第一个 tag(这一行是 no_mcp),
其余六个模块各有一行单 tag 的矩阵行做同样的核对。
或者用带 commit 戳的构建脚本(它会把 commit、构建时间和 tag 串一起写进二进制):
编完立刻核一遍:
上面是输出的形状,不是某一次构建的转录 —— 尖括号里两处随构建而变。
第二行读的是模块注册表(module.Compiled(),init() 跑过才在里面),不是构建时的 tag 字符串,
所以它不会说谎。只有 service,x402 两个名字才是 paid。
第一行的 commit 和时间来自 -ldflags -X。直接 go build 不带这些 flag 时它们是 unknown,
scripts/build.sh 会把它们戳进去 —— 版本常量只能回答"这是哪个 release",
回答不了"生产上跑的是不是我刚编的那个"。
scripts/build.sh 用 -ldflags "-s -w",-s 会剥掉符号表,go tool nm 因此答不出东西。
要在这个二进制上自己数符号(go tool nm ./anet-paid | grep -c module/x402 应大于 0、
grep -c internal/mcpserv 应等于 0),编的时候就别加 -s。
详见自己核一遍。
一行安装对应什么
没有对应写法。 发布件里那份安装脚本(ANet/deploy/release/install.sh)的全部 flag 是
--system --user --prefix DIR --base URL --hub URL --name NAME --token INVITE
--shell --help —— 其中只有 --shell 会换二进制(换成 anet-shell-<平台>),
没有任何 flag 能选发行版档位。--token 是 hub 开了准入之后注册要用的邀请码,与档位无关。
线上那个 URL 实际服务的是哪一份副本,以 curl -fsSL https://agentnetwork.org.cn/install.sh | sh -s -- --help
的输出为准。
这条命令装到的是 anet-<os>-<arch>,即不带任何减法 tag 的 full 档(含 x402,不含 shell)。
它能收费,但它同时带着 MCP、p2p、任务板、共脑、组织、CAS、anetlink。
min / standard / paid / agent / p2p 五档目前没有发布件,只能自己编。
v0.1.7 的 deploy/release/build-release.sh 每平台只出两个件:anet-<plat>(full)与
anet-shell-<plat>(full+shell)。这是总览页
第八节记录的已知缺口之一。
三 · 配置:两个块
paid 只认两个模块配置块,都写在 ~/.anet/config.json(或 ANET_DATA_DIR 指向的目录)的
modules 下面。
modules.service —— 卖什么,多少钱
price的单位是 hub 账本的 credit,省略或写 0 表示免费,不是"价格为零"。 免费的能力根本不产生 402,调用方不必解析一个付款要求才知道不用付钱。id与url缺一不可,id不能重复,否则启动时报错(module/service/service.go的工厂函数)。timeout_ms默认 2 分钟。一个挂住的能力会一直占着一次委派,而委派方在 hub 另一头等着。
modules.x402 —— 要不要开公开兑付口
大多数 paid 节点这个块可以完全不写。付费模块在没有配置块时照样加载(它的工厂函数
对空配置返回一个可用的模块而不是 nil),因为一个已经注册在 hub 上的节点本来就能收付款,
不需要额外配置。
只有要开凭证兑付口才写:
voucher_addr / voucher_url 见第七节,witness_hub / witness_every_seconds 见第四节第 7 条。
这里先记一条:voucher_addr 与 voucher_url 必须同时给,只给一个会在启动时被拒绝,
错误信息是 x402: voucher_addr and voucher_url must be set together。
四 · 能做什么
standard 的全部(注册、被找到、按能力 id 对外提供服务、签发收据、上证据链),加上下面这些。
1. 收费:标价的能力答报价而不是拒绝
配好 price 之后不需要别的动作。买方第一次投递时,paid 节点回一个
PAYMENT_REQUIRED —— 这是一个完整答案:签了名、有收据、上了链,不是一个错误。
买方带上授权再投一次,paid 节点拿授权去 hub 结算,先结算后干活,然后执行、回结果。
顺序是有意的:后结算意味着干完才发现收不到钱;先结算意味着活失败了钱已经付了。
选后者是因为后者证据模型说得清楚 —— 效果和付款都在两条链上。
2. 花钱:买别人标价的能力
不带 --pay 时你拿到的就是那张报价,自己决定付不付。要一次做完:
--pay 是两次委派,不是一次的续期:第一次以报价结束(那是真答案,双方链上都有),
第二次带着授权重新投。授权里写死了金额、收款方、账本和这一次交互的 id,
所以它不能被重新瞄准到别的活、更大的数或另一个 hub。
3. 看账
回的是 hub 账本上的余额加近期流水条目。余额是 hub 的数,这一点第十节会展开。 本节点自己链上的对应事件用:
两者不一致是能看见的,这正是分开存两份的理由。
4. 对账与审计
把本节点签过的授权、验过的结算、做过的兑付,与 hub 公布的这个账户的流水做比对。
报告里 missing_from_hub(本节点记了、hub 的流水解释不了的付款)是严重项;
unexplained_hub_entries(hub 有、本节点没有对应事件的条目)不一定有问题 ——
运营发放本来就没有 agent 侧事件,所以它是列出来而不是判为造假。
拉取 hub 的发放链,验签、验前向哈希链接,并与本节点此前记录过的链头比对。 它阻止不了 hub 增发 —— hub 就是发行方 —— 它确立的是 hub 无法追溯地或无声地做这件事。
5. 兑付:把 credit 拿出来
签一张"付给 hub"的授权。额度真的离开流通,hub 签字说明取走了多少、对着哪个外部凭据。
--ref 对这套代码是一串不透明字符 —— 发票号、打款流水、你和运营方约好的任何东西;
它不碰外部支付,也不假装碰。收据回来后写进本节点的链,并标 verified 表示 hub 的签名验过没有。
6. 给 x402 网关签一笔付款
只在 stdout 打印 PAYMENT-SIGNATURE 头的值,可以直接管进 curl。它不结算任何东西 ——
签名授权的是一笔别人可以去 facilitator 兑现的付款,在兑现之前什么都没动。
可选 --network hub:<hub-aid> 与 --interaction <id>。
7. 给 hub 的发放链签见证(可选)
modules.x402.witness_hub: true 打开后,节点会按 witness_every_seconds(默认 3600 秒)
定期取 hub 的发放链头,记在自己的链上并回签一份声明给 hub。默认关 —— 精简节点不该被要求
替网络干活;打开的节点收益最直接:它拿到了关于自己余额所在账本的独立证据。
五 · 不能做什么,以及它是怎么表现的
这一节比上一节重要。"不在二进制里"和"关掉了"是两种性质,而表现形式决定了你会不会被它坑到。
MCP:命令存在,但明确拒绝
(error: 前缀由 cmd/anet/main.go 统一加在子命令的错误上。)
cmd/anet/mcp_stub.go 在 no_mcp 下提供这个桩,而不是让子命令消失。理由写在代码注释里:
一个在 MCP 客户端里配好了 anet mcp 的运维,应该拿到一句话说明装错了档,而不是 "unknown command"。
点对点:没有传输,但命令不拦你
paid 里没有 p2p 模块,所以没有直连投递,委派全部经 hub 中继。这是预期行为。
但要注意一个不一致:
位置:
internal/daemon/hub_client.go的AdvertisePeerAddress,经internal/daemon/control_api.go的hP2PAdvertise暴露。 行为:anet p2p-advertise <addr>在paid构建上会正常执行 —— 它只是签名并向 hubPOST /agents/{aid}/p2p发布一个地址,路径全在内核里,不检查 p2p 模块是否编进来。 影响:节点会在 hub 的地址目录里公布一个自己根本不监听的地址,对端拿去拨号只会失败。anet help --all里那句 "needs the p2p module" 是提示,不是拦截。 发现方式:读p2p-advertise的调用链,它没有经过任何模块。
要点对点见 p2p 档。
设备、共脑、组织、CAS、任务板:配了就拒绝启动,并点名 tag
这五个模块不在二进制里。如果 config.json 里留着它们的配置块,daemon 起不来,
错误信息点名缺哪个 tag:
来自 module/module.go 的 Build。这是有意的:一个读到 "blackboard": {...} 却默默忽略的 daemon,
会让运维对着"为什么共脑没同步"查上半天。
注意 providers.anetlink.socket 这个旧写法也算配置了 anetlink
(internal/daemon/modules.go 的 moduleConfig 会把它折成 anetlink 的模块块),
所以从 full 切到 paid 时这一项同样要删。
加法 tag 的错误提示措辞不同。shell 默认不在,所以它的提示是
it needs -tags shell, which the default build does not use,而不是让人去找一个
根本不存在的 no_shell。见 module/module.go 的 optInNames。
对方没有付费模块时:你会拿到 UNAVAILABLE
这是 paid 最常遇到的"不能做"。你标好了价、也能付钱,但对面是 min 或 standard:
- 你向一个无
x402的提供方要一件标价的能力 → 它答UNAVAILABLE,消息里带built with -tags no_x402, or the x402 module is not configured。它不会免费替你干。 - 反过来,一个无
x402的买方向你要一件标价的能力 → 它连授权都签不出来, 它自己的/x402-authorize/balance/redeem/reconcile/audit-hub五个入口 全部回同一句话。
scripts/onboard.sh 第 6 节核对第一种情况,第 7 节核对第二种(它实际跑 /balance
/redeem /reconcile /audit-hub 四个),第 8 节核对 paid 自己这一侧。
一个反直觉但重要的细节:standard 档也会在自己的卡片里公布价格。
internal/daemon/card.go 的 priceList 只问 provider 实现没实现 provider.Priced,
而 service 模块无论有没有 x402 都实现了它。所以一个 standard 节点可以"标了价、
公布了价、然后拒绝执行"。这不是缺陷 —— 它拒绝比白干正确 —— 但配 price 之前要确认
自己这一档带 x402。用 anet version 看第二行。
诚实效果状态:五个值不合并
| 状态 | 在 paid 上什么时候出现 |
|---|---|
OK | 活干完了,证据随结果返回 |
PAYMENT_REQUIRED | 你要钱,并且告诉了对方多少。不是错误 |
UNAVAILABLE | 做不了。例如后端服务连不上,或对方无付费模块 |
FAILED | 试了,没成。例如后端返回 5xx,或回复不是 JSON 对象 |
UNVERIFIED | 做了,但拿不到可验证的效果信号。service 模块不产生这一个 |
UNAVAILABLE(没试)与 FAILED(试了没成)是两种状态,不合并。
见证据与效果。
service 模块只会答 OK / FAILED / UNAVAILABLE(module/service/service.go 的
Invoke),PAYMENT_REQUIRED 由内核在调用之前给出。UNVERIFIED 来自能对自己的效果
说得更细的 provider(例如 ANetLink 的适配器),而那些模块不在 paid 里。
六 · 姿态
监听什么
| 面 | 地址 | 默认 |
|---|---|---|
| 回环控制面 | 127.0.0.1:39811(DefaultConfig) | 每一档都有,不属于任何模块 |
| 凭证兑付口 | modules.x402.voucher_addr | 默认不开。不写这个字段就没有 net.Listen |
控制面只有 bearer token 保护,走明文 HTTP,所以它必须留在 127.0.0.1。
这不是 paid 特有的,是所有档共有的。
x402 是 anet 进程里唯一会 net.Listen 的模块,位置是
module/x402/voucher.go 的 startRedeemFace,只在 voucher_addr 非空时执行。
不开这个口的 paid 节点照样收费,只是买家走 hub 中继而不是直连兑付。
出站连什么
- 它注册的那个 hub:中继轮询、结算、拉余额与流水、拉发放链(
/x402/supported的清算账本表缓存 5 分钟)。 modules.service里写明的每一个 HTTP URL(通常是127.0.0.1上的本机服务)。- 打开
witness_hub后,按小时给 hub 的发放链头签见证。
没有别的。
那七个减法 tag 各减掉了什么
no_mcp 减掉的是 CLI 的 MCP northbound(cmd/anet/mcp.go),它不是挂在 module.Host
上的模块;它由 cmd/anet/module_mcp_declare.go(同一个 //go:build !no_mcp)声明进
anet version 的模块行,所以 paid 这一行没有 mcp,正说明这个 tag 生效了。其余六个是模块。
| 减法 tag | 少了什么 |
|---|---|
no_p2p | 这台机器不再需要跑 anetpeer,因此不接受入站对等连接 |
no_mcp | 少 34 个依赖包(MCP Go SDK 及其 jsonschema 依赖树) |
no_anetlink | 不连 ANetLink 运行时的 C1 unix socket |
no_taskboard | 不会去 hub 的共享任务板领活 |
no_cas / no_blackboard / no_org | 这三个包里没有任何 net/http 或 net.Dial 调用,拔掉减的是代码量不是网络面 |
shell 在 paid 里也没有 —— 它是加法 tag,默认构建从来没有它,full 也没有。
要带就是另一个二进制:-tags shell,no_mcp,no_taskboard,…,见 +shell。
七 · 那个公开口
只有 paid(和 full)才有这一节。
它是干什么的
hub 可以替一个 agent 卖能力:买家在 hub 上付钱,hub 结算并交给买家一张凭证, 而不是结果。买家拿凭证直接去 agent 兑付。
redeem_at 就是这台机器上 voucher_url 的值。hub 只卖门票,不代理内容 ——
请求和结果都不经过它,它搬不动自己读不懂的字节。
hub 在这条路上不能做三件事
- 不能定价。价钱读自 agent 自己签的卡片(
anet.pricing扩展),hub 拿不出那个签名, 最多拒卖。 - 不能指路。兑付地址同样在卡片的签名里(endpoint 协议名
x402-redeem), 否则 hub 就能把买家指到自己的机器上,那就又变成代理了。 - 不能替你查重。一张凭证只用一次,而这道关卡在 agent 这一侧。
一次性怎么保证
module/x402/voucher.go 的 spent set 从本节点自己的证据链上重放
(事件类型 anet.voucher.redeemed,启动时最多回读 10000 条)。
只放在内存里会让进程一重启就忘光,持票人就能同一份钱换两次活。
记录写在干活之前:崩在中途的买家有真实的申诉和证据,反过来则让崩溃可以被刷成免费劳动力。
拒绝也记(anet.voucher.refused)。不记的话,一个付了钱被拒的买家手里只有这台机器的沉默。
开这个口之前要想清楚的
voucher_addr 与 voucher_url 分开,是因为它们是两件事。
监听地址常写成 0.0.0.0:8402 或容器内端口;把这种地址签进节点卡片等于对外广告一个
谁也到不了的门牌。世界看到的是什么,只有运维知道 —— 所以要你自己写。
只给一个字段会在启动时被拒,而不是安静地卖出收不到的货。
- NAT 后面没有入口的机器不要配这个口。 hub 会因为卡片里没有兑付地址而拒绝出售
(
409,并提示走中继委派),而不是卖一张兑不掉的票。这种情况用普通的付费委派就行。 - 这个口是
public的,和回环控制面姿态相反,所以它的 HTTP 处理是另写一份的 (module/x402/http.go),请求体上限 8 MB。共用工具函数迟早意味着共用默认值, 而适合一扇私门的默认值对一个开着的端口是错的。 - 兑付一次的超时是 10 分钟。买家已经付过钱了,为了让 socket 好看而截断工作等于收钱卖超时。
八 · 实测数字
数字来自总览页第五节的重测(2026-09-03,go 1.26.7,
linux/amd64,CGO_ENABLED=0,-trimpath -ldflags "-s -w")。
| 档 | 体积 | 依赖包数 | anet version 报的模块 |
|---|---|---|---|
min | 12,100 K | 265 | (none) |
standard | 12,124 K | 266 | service |
paid | 12,240 K | 267 | service,x402 |
paid+shell | 12,292 K | 268 | service,shell,x402 |
full | 14,320 K | 308 | 八个减法模块全在 |
三条读法:
standard→paid只多 116 K 和 1 个依赖。 这两档的区别是"不可能付款"与 "能收费并可能开公开口",不是 116 K。分档的理由从来不是体积。min→paid只多 140 K 和 2 个依赖。paid比full小 2,080 K,其中绝大部分(约 1,900 K)是no_mcp省下来的。 剩下六个 tag 加起来不到 200 K。
ANet/docs/DISTRIBUTIONS-zh.md 的同一行记的是 12,264 K / 267 个依赖,与上表差 24 K
(min 与 standard 两行也各差二三十 K,full 的依赖数差 1)。上表标注了测量日期与工具链,
那份文档的实测表没有标注日期,所以连两次测量隔了多远都说不出来,差异原因未核实。
要以自己能复现的数字为准,按自己核一遍重跑一次。
九 · 升级与切换
身份和数据不受影响
身份私钥、config.json、交互记录和证据链都在数据目录里
(默认 ~/.anet,可用 ANET_DATA_DIR 改),不在二进制里。换二进制不动这些。
具体做法:
停掉节点。 anet stop(或 anet stop --all)。它走控制面优雅关闭,不需要找 PID。
换二进制。 把新编的 anet-paid 放到原来那个路径上(--system 装的一般是
/usr/local/bin/anet)。
先清配置,再启动。 新档里没有的模块,配置块必须先删,否则 daemon 拒绝启动并点名 tag (见第五节)。
启动并核对。 anet up,然后 anet version 看模块行、anet status 看 hub 注册,
anet logs 看启动那行 anet: modules compiled in: …。
从哪一档切过来要改什么
| 从 | 要做的事 |
|---|---|
standard → paid | 配置不用动。给要收费的能力加 price 即可 |
full → paid | 删掉 modules 下的 anetlink / blackboard / org / cas / taskboard / p2p 块,以及旧写法 providers.anetlink;如果 IDE 里配了 anet mcp,那条也要撤 |
paid → standard | 删掉 modules.x402 块。留着会拒绝启动;删了之后标价的能力开始答 UNAVAILABLE |
降档不会撤回已经公布出去的东西。 从 paid 切到 standard 之后:卡片里的价格还在
(service 模块自己实现 provider.Priced,与 x402 无关),买家看得到价、付不了钱;
如果之前配过 voucher_url,hub 上那份卡片仍然记着一个兑付地址,而新构建根本不开那个口。
重新 anet hub-register <hub-url> 一次让卡片重签,或先把 price 去掉。
账户里的 credit 呢
余额在 hub 的账本上,跟着 AID 走,与二进制无关。但降到无 x402 的档之后你读不到它,
也取不出来 —— /x402-authorize /balance /redeem /reconcile /audit-hub
五个付费入口都会回 this build has no payment support。
要把 credit 取出来,先在还带 x402 的构建上 anet redeem。
十 · paid 特有的注意事项
一、余额在 hub 手里,这一点无法回避
credit 记在 hub 的库里。hub 可以增发,可以在你不知情时改一个数字,可以收了钱不给你兑付。 协议阻止不了这些,代码也阻止不了。
它能做到的是另一件事:hub 改不掉双方手里已经签好的记录。所以一笔账对不上时, 这是一场双方都拿得出记录的争执,而不是一方的说法。你注册到哪个 hub,就是选择信任谁。
节点这一侧可以自己核的两件事:
以及 hub 公开的供应量等式(任何人都能重算,不需要是这个 hub 的用户):
二、credit 怎么进来
只有两条路:
-
注册赠额:首次注册到一个 hub 时自动发 100 credit(
ANetHub的RegistrationGrant)。只发一次,按 agent 行去重,重新注册不会再发。 -
运营发放:hub 运维在持有账本的那台机器上执行
这是一个命令行 flag,不是 HTTP 端点。谁可以创造 credit 是一个策略问题, 这一轮没有回答,而一个端点会顺手替它回答 —— 回答得还很糟,因为它需要的那种鉴权还不存在。 要求对持有账本的机器有 shell 访问,是一个可以辩护的过渡方案:那和数据库文件是同一条信任边界。
这条命令要求 hub 停着跑(flag 的帮助文本这么写)。发放会记进 hub 自己那一行的供给计数, 所以它照常出现在
/x402/supply里 —— 账外造币会毁掉唯一那条让托管可核验的算术。
100 credit 很小是有意的:够试出这东西有没有用,不够到值得批量注册身份来薅 —— 而身份是免费铸的,所以赠额永远不能超过铸一个身份的成本。
三、跨 hub 付款:一次付款只造一份 credit
买方在 hub A、卖方在 hub B 时,402 会同时列出 A 愿意清算的账本
(自己的在前,后面跟 /x402/supported 报出的对端账本),买方挑自己有余额的那条。
付款方 hub 在 payee 不在本地时销毁 credit 并在 hub_due 记下欠款;
收款方 hub 凭收据创造 credit。两侧各自把这次变动写进自己的发放链。
这条不是理论上的整齐 —— 曾经两侧各记一次贷,一次付款在两个账本上各造一份,
联邦总量按付款额增长,而任何一侧的单边供给检查都看不到它。
运维面在 hub 上。列出对外负债:
签一份清偿声明并投给对端(-payee 取自 -due 的输出,指明清的是哪一笔):
减记发生在投递之后 —— 对端拒收的清偿仍然欠着,先减会让两个 hub 对同一笔债务的看法不一致。
引入的信任要说明白:B 给自己用户入账,是因为 A 说它扣了自己用户的钱。 收据让这个说法可追责,但不能让它变真。所以 peer 是白名单,欠款是记着而不是抹平的。
四、出错时读什么
| 状态 / 字段 | 含义 |
|---|---|
PAYMENT_REQUIRED | 不是错误。对方要钱,并告诉了你多少 |
insufficient_funds | 余额不够。活本身没问题 |
network_mismatch | 授权签的是另一个 hub 的账本 |
anet.payment.settled 的 verified: false | 记了结算,但没能验证 hub 的签名。"不知道"和"知道没问题"是两种状态。结果里的 paid 对象只有 transaction / amount / network / receipt,这个字段在链上那条记录里 |
凭证 already been redeemed | 这张票用过了。一次性由提供方把关 |
x402: voucher_addr and voucher_url must be set together | 启动被拒。两个字段要么都给,要么都不给 |
this build has no payment support | 这不是 paid 档。anet version 看第二行 |
十一 · 这一档的行为承诺是怎么验的
scripts/onboard.sh 会构建 min / standard / paid 三档并逐条核对本页的说法:
paid 起得来、注册得上、读得到余额(注册赠额)、向一个无付费模块的提供方要价时
拿到明确答复而不是白干(脚本接受 UNAVAILABLE 或 PAYMENT_REQUIRED,不接受第三种)。
真实网络上的完整闭环在 scripts/prodtest.sh:两个 hub、三个节点,第 6 节走
"报价 → 只问价不扣钱 → 付款 → 活真的干了 → 收据回来了 → 双方余额各动 25 →
付款方链上有 authorized 与 settled、收款方链上有 settled";第 7 节走网关路径 —— 在一个 hub 上付钱、拿凭证、
到另一台机器的 voucher_url 上兑付。
onboard.sh 只覆盖 min / standard / paid 三档。agent / p2p / full
只有 CI 的编译与单元测试覆盖,没有端到端的新用户路径核对。这是总览页
记录的已知缺口之一。