02 — Delegate Payment 与 Payment Handlers(支付核心)
这是整个协议工程含量最高、安全立场最鲜明的一支。难点从来不是「调用某个支付 API」,而是「怎么让 agent 既能替用户付款,又不真正持有/滥用用户的卡」。源头:
rfcs/rfc.delegate_payment.md与rfcs/rfc.payment_handlers.md。
2.1 核心问题:卡号不能裸奔
如果 agent 直接拿到用户卡号转交商家,会同时引爆三颗雷:
- 安全/合规:agent 成了持卡环节,要扛 PCI DSS;卡号在多方间流动,泄露面巨大。
- 滥用:agent 拿到一张「可无限次、任意金额」的卡,等于一把万能钥匙。
- 审计:谁在什么时候为哪一单用了这张卡?没有清晰边界。
ACP 的答案是 delegate payment(委托支付):把卡换成一个受约束的一次性 token。
2.2 思路:用「额度护栏」把 token 关进笼子
delegate_payment 只有一个端点(MUST-implement),把支付凭证 vault 成一个 token,但这个 token 只能在显式的 Allowance(额度)约束内被用(rfcs/rfc.delegate_payment.md:7)。
这个 token 在 payment handlers 框架里有个名字:SPT(Shared Payment Token),前缀 vt_。它是「通用包装器」——可以包住底层任意 token 类型,对外提供一致的额度约束(rfcs/rfc.payment_handlers.md:486-493)。
端点:POST /agentic_commerce/delegate_payment,成功回 201 带 token id + created(rfcs/rfc.delegate_payment.md:82-83、:58-62)。
2.3 原理演示:Allowance 就是那道笼子
下面用伪实现把「额度护栏」的核心想法演出来:
# 示意,非源码:delegate_payment 在 PSP 侧大致做的事
def delegate_payment(payment_method, allowance, risk_signals):
# 1) 把真实卡 vault 成内部凭证(agent 永远拿不到原卡)
vaulted = psp.vault(payment_method)
# 2) 发一个 token,把 allowance 死死焊在它身上
token = mint_token(
vaulted,
reason=allowance["reason"], # 必须是 "one_time"
max_amount=allowance["max_amount"], # 整数分,封顶
currency=allowance["currency"], # 锁币种
checkout_session_id=allowance["checkout_session_id"], # 锁这一单
merchant_id=allowance["merchant_id"], # 锁这个商家
expires_at=allowance["expires_at"], # 过期即废
)
return {"id": token.id, "created": now()} # 形如 vt_01J8Z3...
对照真实契约:Allowance 的字段 reason(MUST 是 one_time)、max_amount、currency、checkout_session_id、merchant_id、expires_at 全部定义在 rfcs/rfc.delegate_payment.md:121-128;「token 必须在 allowance.expires_at 时或之后失效」是硬规则(:65-66)。
重点看 reason: one_time——这就是为什么 token 是「一次性」的:它从设计上拒绝多用途(rfcs/rfc.delegate_payment.md:123,非目标里明确「不做超出 allowance 的多用途 token」:17)。
2.4 请求体的另外两块:卡详情与风险信号
delegate_payment 请求体顶层有五块(rfcs/rfc.delegate_payment.md:90-96):payment_method(必填,目前只支持 card)、allowance(必填)、billing_address(可选)、risk_signals(必填,≥1)、metadata(必填)。
- PaymentMethodCard:
type必须card;card_number_type ∈ fpan | network_token;一堆display_*字段供展示;number/cvc等敏感字段(:98-114)。注意它区分fpan(真实卡号)与network_token(网络令牌),后者更安全。 - RiskSignal:
type必须card_testing、score(整数)、action ∈ blocked | manual_review | authorized(:130-134)。这是把「这次请求的风控判断」标准化地随请求一起传,让 PSP 有一致的风险数据格式。
安全要求很硬:日志不得含完整 PAN 或 CVC,必须 TLS 1.3(rfcs/rfc.delegate_payment.md:277-278)。这个端点也有完整的幂等规则(§5),与 checkout 那套一致(:187-268)。
2.5 Payment Handlers:把「支付方式」做成自描述对象
光有 delegate_payment 还不够——agent 得先知道「这家店支持哪些支付方式、每种怎么用、用哪个 PSP」。早期协议只有一个 payment_methods: ["card", ...] 字符串数组,表达力太弱(rfcs/rfc.payment_handlers.md:56-69)。
Payment Handlers 框架把每种支付方式升级成一个富的、自描述的对象,在会话响应的 capabilities.payment.handlers[] 里返回。
handler 的双重身份(精华): 每个 handler 既是规范(specification)又是实例(instance)(rfcs/rfc.payment_handlers.md:91-101):
- 规范:一个 reverse-DNS 名(如
dev.acp.tokenized.card)+ 版本 + schema URL,定义「这类支付方式的协议」。agent 实现一次,就能对接任 何支持该规范的商家。 - 实例:某商家的具体配置(它的
merchant_id、收哪些卡品牌、环境是 sandbox 还是 production)。
handler 声明的关键字段(rfcs/rfc.payment_handlers.md:169-181):
| 字段 | 含义 |
|---|---|
id | 会话内 handler 实例的唯一 id(instrument 用它回指) |
name | reverse-DNS 规范名,如 dev.acp.tokenized.card |
version / spec | 规范版本(YYYY-MM-DD)+ 人读文档 URL |
requires_delegate_payment | 是否必须走 delegate_payment(推荐 true) |
requires_pci_compliance | 是否碰 PCI 敏感数据(推荐 false) |
psp | 商家用哪个 PSP(stripe/adyen…),告诉 agent 该向谁 vault |
config_schema / instrument_schemas | 校验配置 / 支付凭据结构的 JSON Schema URL |
config | handler 专属配置(含 merchant_id,委托时必需) |
为什么默认 requires_delegate_payment: true? 因为委托能带来四样东西:显式额度边界、标准化风险信号、凭证作用域绑定、清晰审计链(rfcs/rfc.payment_handlers.md:307-315)。换句话说,「安全是默认值」。
2.6 完整支付链路:从 handler 到收款
把两块拼起来,看一条端到端的真实数据流(rfcs/rfc.payment_handlers.md:209-289):
① 建会话 → 商家在响应里给出 handlers[]
其中一个: { id: "card_tokenized", psp: "stripe",
requires_delegate_payment: true,
config: { merchant_id: "acct_123", ... } }
│
② agent 从 config 抓出 merchant_id │
▼
③ agent → POST /delegate_payment(向 stripe,带 merchant_id 进 allowance)
◀── 回 SPT: { id: "vt_01J8...", type:"spt", allowance:{...} }
│
④ agent 把 SPT 塞进 payment instrument │
▼
⑤ agent → POST /complete
payment_data.instrument.credential = { type:"spt", token:"vt_01J8..." }
│
⑥ 商家拿 SPT,走它自己的 stripe 解包并扣款、建单
关键衔接点(易错): handler 的 config.merchant_id 是必需的,因为 agent 要把它带进 delegate_payment 的 allowance.merchant_id,PSP 才能把 vault token 正确地 scope 到这个商家账户(rfcs/rfc.payment_handlers.md:400-405、:584-590)。psp 字段则告诉 agent「该向哪个 PSP 发 vault 请求」,消除歧义(:407-432)。
2.7 Payment Instrument 与 Credential 的层次
agent 在 complete 时提交的 instrument 有一个基础 schema(rfcs/rfc.payment_handlers.md:183-207):id(agent 分配)、handler_id(回指哪个 handler)、type、credential。credential 的结构由 handler 的 credential schema 决定,框架内置两种基础凭据:
- SPT credential(
rfcs/rfc.payment_handlers.md:731-776):{ type:"spt", token:"vt_..." },token 必须匹配^vt_[a-zA-Z0-9]+$。 - Agentic Token credential(
:778-824):卡网络(visa/mastercard)专为 agentic commerce 发的 token。
additionalProperties: true 让 handler 能在基础 instrument 上扩展字段(比如需要 AVS 校验的卡加 billing_address)(rfcs/rfc.payment_handlers.md:723-727)。
2.8 一个聪明的衍生模式:Seller-Backed Handler
有些支付方式根本没有「可转交的凭证」——比如用户存在商家那里的卡、商家发的礼品卡、积分、店内余额。这些怎么纳入同一套审计/可观测模型?
dev.acp.seller_backed 模式(rfcs/rfc.seller_backed_payment_handler.md)的巧思:即使凭证完全在商家侧解析,也仍然走 delegate_payment。
requires_delegate_paymentMUSTtrue、requires_pci_complianceMUSTfalse、pspSHOULD"seller_managed"(rfcs/rfc.seller_backed_payment_handler.md:70-73)。- 商家为每个可选项声明一个 handler 实例(每张存卡、每张礼品卡各一个
id);agent 把用户选的id+ 一个 token 回传,商家用 id 解析到原始支付方式并扣款(:52-59)。
为什么不直接传 id 就好? RFC 直 接列了反例:裸传 id 会丢掉审计链、丢掉 agent 对退款/争议的可见性、丢掉额度约束(rfcs/rfc.seller_backed_payment_handler.md:38-49)。所以哪怕「没凭证可委托」,也强制走委托端点来保住协议的可观测性。这是「为了统一不变量,宁可多一次空转」的典型设计。
2.9 本章要带走的三句话
- 卡 → SPT 是核心:
delegate_payment用 Allowance(限额/币种/单/时)+reason:one_time把 token 关进笼子。 - handler 是规范 + 实例的二象:agent 按规范实现一次,通吃所有支持它的商家;
merchant_id/psp是 vault 时的路由钥匙。 - 「安全是默认值」:
requires_delegate_payment默认 true;连无凭证的 seller-backed 也强制走委托,只为保住审计链。