
x402 payment-identifier 扩展实战用支付 ID 实现 HTTP 402 支付的幂等性【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402本文是 x402 开源支付协议仓库中examples/typescript/clients/payment-identifier示例的完整实战指南讲解如何借助payment-identifier扩展为基于 HTTP 402 的链上支付流程引入幂等性idempotency客户端为每个逻辑请求生成唯一支付 ID服务端按 ID 缓存响应从而让网络故障重试、客户端崩溃恢复等场景不再产生重复扣款。读完本文你将掌握generatePaymentId()、appendPaymentIdentifierToExtensions()等扩展 API 的用法、底层校验约束与格式规范并能在本地完整跑通首次请求扣款、同 ID 重试直接命中缓存的端到端验证。为什么支付需要幂等性x402 的支付链路涉及 EVM 链上签名、交易广播与 HTTP 请求属于典型的跨系统不确定状态场景一次支付请求在网络上超时后客户端无法确定服务端是否已经完成了扣款与放行。如果简单重试可能造成同一笔逻辑交易被重复结算如果不重试又可能因为网络抖动白白丢失有效请求。payment-identifier扩展正是为解决该问题而生它允许客户端在PaymentPayload中携带一个全局唯一的支付 ID服务端以此为键缓存已处理请求的响应。当同一个 ID 再次出现时服务端直接返回缓存结果、跳过支付处理从而把至少一次的不可靠重试收敛为恰好一次的幂等语义。从源码结构看该扩展在 TypeScript 包中位于 typescript/packages/extensions/src/payment-identifier由类型定义、工具函数、客户端辅助函数、服务端声明函数与校验/提取逻辑五个模块组成并配套了 payment-identifier.test.ts 共 669 行的完整测试。核心工作流程关联文档 examples/typescript/clients/payment-identifier/README.md 将幂等流程概括为四个步骤客户端调用generatePaymentId()生成一个唯一的支付 ID客户端在构造PaymentPayload之前通过appendPaymentIdentifierToExtensions()把支付 ID 写入扩展字段服务端以支付 ID 为键缓存已处理的响应携带相同支付 ID 的重试请求直接命中缓存返回不再重新处理支付。对应的 TypeScript 骨架代码如下出自 index.ts 的完整版见下一节import { x402Client, wrapFetchWithPayment } from x402/fetch; import { appendPaymentIdentifierToExtensions, generatePaymentId } from x402/extensions/payment-identifier; const client new x402Client(); // ... register schemes ... // Generate a unique payment ID for this logical request const paymentId generatePaymentId(); // Hook into payment flow to add the payment ID before payload creation client.onBeforePaymentCreation(async ({ paymentRequired }) { if (!paymentRequired.extensions) { paymentRequired.extensions {}; } appendPaymentIdentifierToExtensions(paymentRequired.extensions, paymentId); }); const fetchWithPayment wrapFetchWithPayment(fetch, client); // First request - payment is processed const response1 await fetchWithPayment(url); // Retry with same payment ID - cached response returned (no payment) const response2 await fetchWithPayment(url);需要注意第 2 步发生在onBeforePaymentCreation钩子中即在协议握手返回PaymentRequired402 响应之后、客户端最终构造PaymentPayload之前这样支付 ID 才能随支付载荷一并提交给服务端。客户端示例的完整实现仓库中的可运行示例位于 examples/typescript/clients/payment-identifier/index.ts相比 README 中的骨架它补充了方案注册、环境变量读取、耗时统计与结算验证等完整逻辑。初始化与方案注册import { config } from dotenv; import { x402Client, wrapFetchWithPayment, x402HTTPClient } from x402/fetch; import { ExactEvmScheme } from x402/evm/exact/client; import { UptoEvmScheme } from x402/evm/upto/client; import { privateKeyToAccount } from viem/accounts; config(); const privateKey process.env.PRIVATE_KEY as 0x${string}; if (!privateKey) { console.error(❌ PRIVATE_KEY environment variable is required); process.exit(1); } const baseURL process.env.RESOURCE_SERVER_URL || http://localhost:4022; const endpointPath process.env.ENDPOINT_PATH || /weather; const url ${baseURL}${endpointPath};这里注册了两个 EVM 支付方案ExactEvmScheme精确金额与UptoEvmScheme上限金额均以privateKeyToAccount构造的签名者完成签名网络匹配模式为eip155:*。三个可配置环境变量的默认值分别为http://localhost:4022、/weather以及必填的PRIVATE_KEY。支付 ID 的生成与注入const paymentId generatePaymentId(); console.log(\n Generated Payment ID: ${paymentId}); client.onBeforePaymentCreation(async ({ paymentRequired }) { if (paymentRequired.extensions) { appendPaymentIdentifierToExtensions(paymentRequired.extensions, paymentId); } });注意示例中paymentRequired.extensions在服务端声明了该扩展时才会存在appendPaymentIdentifierToExtensions内部同样会做防御性检查详见下文客户端注入的源码实现只在服务端确实声明了payment-identifier时才写入 ID。首次请求与重试请求的对比验证const fetchWithPayment wrapFetchWithPayment(fetch, client); const startTime1 Date.now(); const response1 await fetchWithPayment(url, { method: GET }); const duration1 Date.now() - startTime1; const body1 await response1.json(); console.log(Response (${duration1}ms):, JSON.stringify(body1, null, 2)); const paymentResponse1 new x402HTTPClient(client).getPaymentSettleResponse(name response1.headers.get(name), ); if (paymentResponse1) { console.log(\n Payment settled on ${paymentResponse1.network}); }x402HTTPClient(client).getPaymentSettleResponse()从响应头中解析结算凭证settle response据此确认首次请求确实发生了链上结算。第二次请求复用同一个paymentId发起示例预期服务端直接返回缓存cached: true此时getPaymentSettleResponse返回空值控制台打印✅ No payment processed - response served from cache!。最后程序还会计算两次请求的耗时差并输出缓存加速百分比if (duration2 duration1) { console.log( ⚡ Cached response was ${Math.round((1 - duration2 / duration1) * 100)}% faster!, ); }扩展 API 与格式约束源码级解析常量与格式规范在 types.ts 中定义了扩展的格式契约常量值说明PAYMENT_IDENTIFIERpayment-identifier扩展在extensions对象中的键名PAYMENT_ID_MIN_LENGTH16支付 ID 最小长度PAYMENT_ID_MAX_LENGTH128支付 ID 最大长度PAYMENT_ID_PATTERN/^[a-zA-Z0-9_-]$/仅允许字母、数字、连字符与下划线扩展结构为{ info: { required, id? }, schema }required表示服务端是否强制要求客户端提供 IDtrue时缺失将得到 400 Bad Requestid为客户端提供的幂等键schema是对应 JSON Schema基于 JSON Schema 2020-12 草案用于对info结构做程序化校验。生成函数generatePaymentIdutils.ts 中的实现非常简洁export function generatePaymentId(prefix: string pay_): string { const uuid crypto.randomUUID().replace(/-/g, ); return ${prefix}${uuid}; }默认前缀pay_可传入自定义前缀如txn_传空字符串则无前缀基于crypto.randomUUID()生成 UUID v4 并去掉连字符得到 32 位十六进制字符默认生成的pay_ 32 位十六进制 36 字符落在 16128 的合法区间内。同文件提供的isValidPaymentId()会先检查类型与长度边界16 ≤ length ≤ 128再用PAYMENT_ID_PATTERN校验字符集。测试 payment-identifier.test.ts 覆盖了默认/自定义/无前缀三种生成方式、100 次生成的唯一性以及过短15 字符、过长129 字符、非法字符!#$%^*()、空格、点号等拒绝路径。客户端注入appendPaymentIdentifierToExtensionsclient.ts 中该函数的关键行为是仅在服务端声明了扩展时才写入 IDexport function appendPaymentIdentifierToExtensions( extensions: Recordstring, unknown, id?: string, ): Recordstring, unknown { const extension extensions[PAYMENT_IDENTIFIER]; // Only append if the server declared this extension with valid structure if (!isPaymentIdentifierExtension(extension)) { return extensions; } const paymentId id ?? generatePaymentId(); if (!isValidPaymentId(paymentId)) { throw new Error( Invalid payment ID: ${paymentId}. ID must be 16-128 characters and contain only alphanumeric characters, hyphens, and underscores., ); } extension.info.id paymentId; return extensions; }未传id时自动调用generatePaymentId()传入自定义 ID 时会先经isValidPaymentId校验非法则抛出明确错误若服务端未声明该扩展isPaymentIdentifierExtension返回 false函数原样返回不会污染扩展对象。服务端声明declarePaymentIdentifierExtension服务端侧通过 resourceServer.ts 中的declarePaymentIdentifierExtension(required false)在PaymentRequired.extensions中声明能力export function declarePaymentIdentifierExtension( required: boolean false, ): PaymentIdentifierExtension { return { info: { required }, schema: paymentIdentifierSchema, }; }同时导出一个paymentIdentifierResourceServerExtension常量key: PAYMENT_IDENTIFIER供资源服务器通过registerExtension接入统一扩展注册体系。服务端校验与提取validation.ts 提供了完整的服务端侧工具集extractPaymentIdentifier(paymentPayload, validate true)从PaymentPayload.extensions中提取 ID缺失或非法返回nullextractAndValidatePaymentIdentifier()一次性返回{ id, validation }便于在结算处理器中同时拿到 ID 与校验结果validatePaymentIdentifier(extension)结合 JSON SchemaAjv与格式约束做双重校验validatePaymentIdentifierRequirement(paymentPayload, serverRequired)当服务端要求必填而客户端未提供时返回 400 级别错误信息isPaymentIdentifierRequired()/hasPaymentIdentifier()用于快速判断必填性、扩展是否存在。端到端运行指南前置条件Node.js v20推荐通过 nvm 安装pnpm v10推荐通过 pnpm 官方安装脚本安装一个运行中的 payment-identifier 服务端参见 payment-identifier server example用于支付的有效 EVM 私钥示例环境为 Base Sepolia 网络上的 USDC。安装与构建在 TypeScript 示例根目录examples/typescript统一安装并构建所有包cd ../../ pnpm install pnpm build cd clients/payment-identifier配置环境变量复制本地环境变量模板并填入私钥cp .env-local .env必填环境变量PRIVATE_KEY—— 用于 EVM 支付的以太坊私钥。可选环境变量由 index.ts 读取RESOURCE_SERVER_URL默认http://localhost:4022与ENDPOINT_PATH默认/weather。启动服务端并运行客户端先在另一个终端启动 payment-identifier 服务端cd ../../servers/payment-identifier pnpm dev再运行客户端pnpm dev预期输出 Generated Payment ID: pay_7d5d747be160e280504c099d984bcfe0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ First Request (with payment ID: pay_7d5d747be160e280...) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Making request to: http://localhost:4022/weather Response (1523ms): { report: { weather: sunny, temperature: 70, cached: false } } Payment settled on eip155:84532 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Second Request (SAME payment ID: pay_7d5d747be160e280...) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Making request to: http://localhost:4022/weather Expected: Server returns cached response without payment processing Response (45ms): { report: { weather: sunny, temperature: 70, cached: true } } ✅ No payment processed - response served from cache! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Payment ID: pay_7d5d747be160e280504c099d984bcfe0 First request: 1523ms (payment processed) Second request: 45ms (cached) ⚡ Cached response was 97% faster!注意输出中的耗时与加速百分比是运行时动态计算的随网络与链上确认速度变化并非固定指标关键在于第二次请求的响应体出现cached: true且没有新的结算凭证。服务端侧幂等实现与行为矩阵为了让客户端示例跑通服务端必须在 examples/typescript/servers/payment-identifier 中实现缓存逻辑。其核心模式为在PaymentRequired响应中通过declarePaymentIdentifierExtension(false)声明支持可选在onAfterSettle钩子中调用extractPaymentIdentifier(paymentPayload)取出 ID将响应写入内存缓存示例 TTL 为 1 小时在onProtectedRequest钩子中解码paymentHeader、提取 ID 并查询缓存命中则直接返回{ grantAccess: true }跳过支付。服务端的幂等行为矩阵出自服务端示例 README场景服务端响应新的支付 ID正常处理支付并缓存响应相同支付 IDTTL 内返回缓存响应跳过支付相同支付 IDTTL 已过期正常处理支付并更新缓存无支付 ID正常处理支付不缓存必填与可选配置// Payment ID is optional (clients can omit it) declarePaymentIdentifierExtension(false) // Payment ID is required (clients must provide it) declarePaymentIdentifierExtension(true)缓存 TTL 调优短 TTL515 分钟适用于对时效性敏感的资源长 TTL124 小时适用于静态或低频变化的资源。生产环境注意事项分布式场景应使用 Redis 等共享缓存替代内存Map缓存故障要优雅降级——缓存不可用时照常处理支付可引入载荷哈希相同 ID 但不同载荷时返回 409 Conflict进一步防止误用监控缓存命中率用于调优 TTL 与识别滥用行为。适用场景网络故障请求失败后安全重试避免重复扣款客户端崩溃持久化支付 ID重启后恢复进行中的请求负载均衡同一请求命中共享缓存的多个服务端节点仍能去重测试/回放开发期间以相同 ID 反复回放请求不消耗资金。最佳实践在逻辑请求层级生成支付 ID而不是每次重试都生成新 ID——否则重试将失去幂等意义持久化支付 ID对长时间运行的操作保存 ID使其在进程重启后依然有效使用描述性前缀如order_、sub_区分支付类型便于日志审计不要跨逻辑请求复用支付 ID不同业务请求必须使用不同 ID避免误命中缓存。小结通过payment-identifier扩展x402 将 HTTP 402 支付流程中的重试不确定性收敛为可验证的幂等语义客户端一侧只需在onBeforePaymentCreation钩子中注入generatePaymentId()生成的 ID服务端一侧以extractPaymentIdentifier 缓存实现去重。扩展包内置了严格的格式校验16128 字符、字母数字与-/_、JSON Schema 校验和必填控制并有完整的单元测试与端到端示例支撑可直接作为生产支付网关幂等层的实现参考。如需继续深入可阅读 扩展实现源码、扩展测试 以及 服务端示例。【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考