跳到主要内容

01 — 协议数据结构与四步流程

本章讲清 x402 最底层的"语言":三个核心 JSON 结构,和它们在四步循环里如何流转。读完你能照着手写一对合法的请求/响应。

1.1 先认四步循环

x402 规范把整个协议浓缩成一句话的循环(specs/x402-specification-v2.md §2):

  1. Client Request — 客户端请求资源。
  2. Payment Required — 没带有效付款,服务器回"需要付款"信号 + 报价。
  3. Payment Authorization — 客户端在下一次请求里带上一份签名的付款授权
  4. Settlement — 服务器验证授权、发起链上结算。

注意第 4 步在规范里其实拆成两个动作:verify(验证)settle(结算),对应 facilitator 的两个端点。为什么要拆?因为服务器想"先确认钱能扣,把活干完,再真正扣"——见第 4 章的时序细节。

1.2 三个核心结构(与传输、方案无关)

x402 最聪明的设计起点是:核心数据结构既不绑传输层、也不绑付款方案。规范 §5 原话——"All transports and schemes use these exact data structures, differing only in how they represent them (transport layer) and what validation/settlement logic they apply (scheme layer)"。

这三个结构在 TS 参考实现里就是几个朴素的 typetypescript/packages/core/src/types/payments.ts):

PaymentRequired — 服务器的"报价单"

服务器对未付费请求回这个。核心是 accepts 数组:"你可以用下面任意一种方式付款"

// typescript/packages/core/src/types/payments.ts:22-28 — PaymentRequired
export type PaymentRequired = {
x402Version: number;
error?: string; // 为什么要付款的人类可读说明
resource: ResourceInfo; // 被保护资源的描述(url/描述/图标/标签)
accepts: PaymentRequirements[]; // ★ 一组可接受的付款方式
extensions?: Record<string, unknown>;
};

每个 accepts 元素是一个 PaymentRequirements——一种具体报价:

// typescript/packages/core/src/types/payments.ts:12-20 — PaymentRequirements
export type PaymentRequirements = {
scheme: string; // 付款方案,如 "exact"
network: Network; // CAIP-2 网络标识,如 "eip155:8453"(Base 主网)
asset: string; // 代币合约地址 或 法币 ISO 4217 码
amount: string; // 原子单位金额(字符串避免大数精度问题)
payTo: string; // 收款地址
maxTimeoutSeconds: number; // 付款授权的最长有效期
extra: Record<string, unknown>; // 方案特定字段(如 EVM 的 EIP-712 域 name/version)
};

关于几个字段的直觉:

  • amount 是字符串、是原子单位。 "10000" 在 USDC(6 位小数)里是 $0.01。用字符串而非 number 是为了不丢大整数精度。
  • network 用 CAIP-2 格式 namespace:reference(规范 §11.1)。eip155:8453 = Base 主网,solana:5eykt4Us... = Solana 主网。链无关的统一寻址。
  • extra 是方案逃生舱。 exact-EVM 会在这里塞 EIP-712 签名域的 name / version(见第 3 章),别的方案塞别的。

PaymentPayload — 客户端的"签好的单"

客户端选定一种 accepts、签名后回传这个:

// typescript/packages/core/src/types/payments.ts:30-36 — PaymentPayload
export type PaymentPayload = {
x402Version: number;
resource?: ResourceInfo;
accepted: PaymentRequirements; // ★ 客户端选中的那一种报价(回显)
payload: Record<string, unknown>; // ★ 方案特定的付款数据(如 EVM 的签名+授权)
extensions?: Record<string, unknown>;
};

关键对应关系:服务器发的是 accepts(复数,候选),客户端回的是 accepted(单数,已选)。payload 字段才是真正的"签名货物"——它的内容完全由 scheme 决定。比如 exact-EVM 的 payload 长这样(规范 §5.2):

// payload 字段(exact-EVM 方案),来自 specs/x402-specification-v2.md §5.2.1
{
"signature": "0x2d6a75...",
"authorization": {
"from": "0x857b06...", "to": "0x209693...", "value": "10000",
"validAfter": "1740672089", "validBefore": "1740672154",
"nonce": "0xf37466..."
}
}

而 Solana 方案的 payload 会是一笔序列化的、待广播的交易——同一个字段名,装的东西天差地别。这正是"Types 与 Logic 解耦"的体现(第 2 章)。

SettleResponse — 结算回执

facilitator 上链后返回,服务器再转给客户端:

// 形态见 specs/x402-specification-v2.md §5.3;TS 类型在 core/src/types/facilitator.ts
{
"success": true,
"transaction": "0x1234...def", // 链上交易哈希
"network": "eip155:84532",
"payer": "0x857b06..."
}

失败时 success:false + errorReason(标准错误码,规范 §9,如 insufficient_funds)。

还有一个 VerifyResponseisValid + invalidReason + payer),是 verify 阶段的回执,结构类似但不上链。

1.3 把三个结构串成一次往返

下面用伪代码把"两次 HTTP 往返"演一遍,帮你看清三个结构怎么接力。这是高层时序,不是源码:

// 示意,非源码 —— 展示三个结构如何接力

// ── 往返 1:撞墙 ──
client.GET("/weather") // ① 无付款
server → 402, header["PAYMENT-REQUIRED"] = base64(PaymentRequired{
accepts: [ {scheme:"exact", network:"eip155:8453", amount:"1000", payTo:"0x..", ...} ]
}) // ② 报价

// ── 客户端本地:挑一种 accepts,钱包签名(不广播)──
const chosen = select(paymentRequired.accepts) // 选中一种
const payload = scheme.createPaymentPayload(chosen) // 产出 {accepted, payload:{signature,...}}

// ── 往返 2:带着签名重试 ──
client.GET("/weather", header["PAYMENT-SIGNATURE"] = base64(payload)) // ③
server.verify(payload, chosen) // ④ 调 facilitator 验签 → isValid:true
const data = runHandler() // 跑真实业务
server.settle(payload, chosen) // ⑤ 调 facilitator 上链 → txHash
server → 200, body=data, header["PAYMENT-RESPONSE"] = base64(SettleResponse{success,transaction}) // ⑥

重点看 chosen 这个变量:它从服务器的 accepts[] 里被选出来,作为 payload.accepted 回传,又作为 verify/settle 的第二参数。服务器收到后会做一件要紧事——核对客户端回的 accepted 确实是自己报过的价(防止客户端篡改金额/收款地址)。这个核对逻辑在 x402ResourceServer.findMatchingRequirements / paymentRequirementsMatchAcceptedcore/src/server/x402ResourceServer.ts:1318:1681),用的是"核心字段必须完全相等、extra 允许客户端追加但不能改"的子集匹配规则。

1.4 版本:v1 与 v2 的差别

仓库同时支持两个协议版本,编排器靠 x402Version 字段路由。x402Version 当前值为 2(core/src/index.ts 的常量 x402Version)。最该知道的两点差异:

  • 报价的承载位置不同。 v2 把 PaymentRequired 放进 响应 headerPAYMENT-REQUIRED,base64),v1 放在 响应 body。见 x402HTTPResourceServer.createHTTPPaymentRequiredResponsecore/src/http/x402HTTPResourceServer.ts:1103)。
  • 网络标识不同。 v2 用 CAIP-2(eip155:8453),v1 用裸名字(base-sepolia)。x402Client.registerV1 接收 v1 风格的网络名(core/src/client/x402Client.ts:249)。

下一章看这三个结构是怎么被三个编排器 + scheme 接口"生产"和"消费"的。