跳到主要内容

02 — Delegate Payment 与 Payment Handlers(支付核心)

这是整个协议工程含量最高、安全立场最鲜明的一支。难点从来不是「调用某个支付 API」,而是「怎么让 agent 既能替用户付款,又不真正持有/滥用用户的卡」。源头:rfcs/rfc.delegate_payment.mdrfcs/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_amountcurrencycheckout_session_idmerchant_idexpires_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_testingscore(整数)、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 用它回指)
namereverse-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
confighandler 专属配置(含 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_paymentallowance.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)、typecredentialcredential 的结构由 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_payment MUST truerequires_pci_compliance MUST falsepsp SHOULD "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_paymentAllowance(限额/币种/单/时)+ reason:one_time 把 token 关进笼子。
  • handler 是规范 + 实例的二象:agent 按规范实现一次,通吃所有支持它的商家;merchant_id/psp 是 vault 时的路由钥匙。
  • 安全是默认值」:requires_delegate_payment 默认 true;连无凭证的 seller-backed 也强制走委托,只为保住审计链。