x402 Go/Gin 高级服务端实战:动态定价、收款路由、生命周期钩子与 API 可发现性
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
本文是 x402 支付协议(HTTP 之上构建的互联网支付协议)Go 语言服务端的高级实践指南,以 examples/go/servers/advanced/README.md 为骨架,结合仓库内 7 个可独立运行的 Gin 示例与 Go SDK 源码,系统讲解动态定价、按请求上下文路由收款地址、支付生命周期钩子、Bazaar API 可发现性、多网络支持与自定义代币等进阶模式。读完本文,你将能够基于 Gin 搭建一个具备分层定价、多收款方、支付事件埋点与代币自定义能力的生产级 x402 资源服务器,并理解
PAYMENT-REQUIRED/PAYMENT-RESPONSE协议的完整交互过程。
目录
- 前置条件与快速开始
- 示例总览:六种进阶模式一览
- 多网络支持:同时接收 EVM 与 SVM 支付
- Bazaar 扩展:让 API 可被客户端与 Agent 发现
- 动态定价:按请求上下文实时计算价格
- 动态收款路由:把支付路由到不同收款方
- 生命周期钩子:在验证与结算前后挂载业务逻辑
- 自定义货币解析:接受 USDC 之外的代币
- 协议响应格式:402 与 200 的完整报文
- 源码级原理:动态函数如何被解析与执行
前置条件与快速开始
本示例面向Go 1.21 及以上环境,需要满足三个条件:
- Go 1.21 或更高版本;
- 一个有效的EVM 收款地址(
EVM_PAYEE_ADDRESS),用于接收付款; - 一个支持目标支付网络的 Facilitator 端点 URL(
FACILITATOR_URL)。Facilitator 负责撮合、验证与结算,可参考项目生态中的 Facilitator 列表进行选择。
以examples/go/servers/advanced目录为工作目录,按以下三步启动:
第一步:配置环境变量
cp .env-example .env(注:仓库中该目录未附带.env-example文件,可自行创建.env并填入下述变量;示例代码使用godotenv.Load()读取。)
需要填写两个必需变量:
| 变量 | 作用 |
|---|---|
FACILITATOR_URL | Facilitator 端点 URL,如https://x402.org/facilitator |
EVM_PAYEE_ADDRESS | 接收付款的 Ethereum 地址 |
所有示例都会在启动时校验这两个变量,缺失即打印错误并退出(见各示例文件开头)。all-networks示例额外支持可选变量SVM_PAYEE_ADDRESS(Solana 收款地址)。
第二步:安装依赖
go mod download第三步:运行示例
每个示例都是独立程序,直接运行即可,默认监听:4021端口:
go run hooks.go示例总览:六种进阶模式一览
| 示例 | 启动命令 | 演示内容 |
|---|---|---|
all-networks | go run all_networks.go | 支持所有已配置网络(EVM/SVM),网络可选按环境变量配置 |
bazaar | go run bazaar.go | 通过 Bazaar 扩展实现 API 可发现性 |
hooks | go run hooks.go | 支付生命周期钩子 |
dynamic-price | go run dynamic-price.go | 基于请求上下文的动态定价 |
dynamic-pay-to | go run dynamic-pay-to.go | 将支付路由到不同收款方 |
custom-money-definition | go run custom-money-definition.go | 接受替代代币 |
目录中还包含一个 README 表格未列出的eip2612-gas-sponsoring.go,演示 EIP-2612 燃料赞助场景,可一并参考。它们共享同一套基础设施:ginfw.Default()创建 Gin 引擎 →ginmw.X402Payment(...)挂载支付中间件 → 注册受保护路由处理器。所有示例均以GET /weather作为演示资源,返回 JSON 天气数据。
启动后可用任一 x402 客户端实测。Go 客户端示例位于 examples/go/clients/custom,在examples/go/servers/advanced下运行时可用cd ../../clients/custom进入(该客户端同样需要先配置好.env),然后执行go run main.go发起带支付的请求。
多网络支持:同时接收 EVM 与 SVM 支付
all-networks示例演示了如何在一个服务里同时支持EVM(如 Base Sepolia)与SVM(Solana Devnet)两条链上的 exact 支付。核心思路是按环境变量动态组装PaymentOptions与Schemes:
evmNetwork := x402.Network("eip155:84532") // Base Sepolia svmNetwork := x402.Network("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") // Solana Devnet paymentOptions := x402http.PaymentOptions{} if evmAddress != "" { paymentOptions = append(paymentOptions, x402http.PaymentOption{ Scheme: "exact", Price: "$0.001", Network: evmNetwork, PayTo: evmAddress, }) } if svmAddress != "" { paymentOptions = append(paymentOptions, x402http.PaymentOption{ Scheme: "exact", Price: "$0.001", Network: svmNetwork, PayTo: svmAddress, }) } schemes := []ginmw.SchemeConfig{} if evmAddress != "" { schemes = append(schemes, ginmw.SchemeConfig{Network: evmNetwork, Server: evm.NewExactEvmScheme()}) } if svmAddress != "" { schemes = append(schemes, ginmw.SchemeConfig{Network: svmNetwork, Server: svm.NewExactSvmScheme()}) }关键设计点:
- 最少配置原则:两个地址至少提供一个即可启动;未配置的网络不会被声明,客户端拿到的
accepts数组只包含实际可用的支付选项。 - Scheme 与网络一一绑定:
ginmw.SchemeConfig把Network(CAIP-2 标识)与具体的方案服务器(evm.NewExactEvmScheme()/svm.NewExactSvmScheme())配对,中间件据此选择验证与结算逻辑。 - 健康检查免支付:
GET /health不经过支付中间件,便于负载均衡器探活。该文件注释要求新增链时按网络前缀字母序追加("eip155"在"solana"之前),便于维护。
Bazaar 扩展:让 API 可被客户端与 Agent 发现
Bazaar 是 x402 的发现扩展(见 go/extensions/bazaar)。bazaar示例演示了如何在路由上声明机器可读的 API 文档——包括 HTTP 方法、查询参数、请求/响应 JSON Schema 与示例输出,使客户端和 AI Agent 无需人工文档即可发现并调用你的付费服务。
discoveryExtension, err := bazaar.DeclareDiscoveryExtension( bazaar.MethodGET, map[string]interface{}{"city": "San Francisco"}, // 示例查询参数 types.JSONSchema{ "properties": map[string]interface{}{ "city": map[string]interface{}{ "type": "string", "description": "City name to get weather for", }, }, "required": []string{"city"}, }, "", // GET 请求无 body &types.OutputConfig{ Example: map[string]interface{}{ "city": "San Francisco", "weather": "foggy", "temperature": 60, }, Schema: types.JSONSchema{ "properties": map[string]interface{}{ "city": map[string]interface{}{"type": "string"}, "weather": map[string]interface{}{"type": "string"}, "temperature": map[string]interface{}{"type": "number"}, }, "required": []string{"city", "weather", "temperature"}, }, }, ) routes := x402http.RoutesConfig{ "GET /weather": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", PayTo: evmPayeeAddress, Price: "$0.001", Network: evmNetwork, }, }, Description: "Weather data", MimeType: "application/json", Extensions: map[string]interface{}{ types.BAZAAR: discoveryExtension, }, }, }DeclareDiscoveryExtension的参数含义:
| 参数 | 说明 |
|---|---|
MethodGET | 声明该 API 使用 GET 方法 |
map[string]interface{}{"city": ...} | 示例查询参数,告知调用方应传什么参数 |
types.JSONSchema{...} | 请求参数的 JSON Schema 约束(type、description、required) |
"" | GET 请求无请求体;POST 等场景可在此声明 body 的 JSON Schema |
&types.OutputConfig{...} | 响应示例与响应 JSON Schema |
适用场景:客户端和 AI Agent 能自动发现你的服务。需要注意一个实现细节:若路由模式使用通配符*(如GET /weather/*)且同时挂载 Bazaar 扩展,go/http/server.go 的validateRouteConfiguration会打印警告——通配符路由会自动生成var1、var2这类参数名,建议改用命名参数(如/weather/:city)以获得更准确的发现元数据。
动态定价:按请求上下文实时计算价格
静态Price只能写死一个价格。dynamic-price示例把价格声明替换为函数,让价格在每次请求时根据上下文计算,可用于分层定价、按用户定价、按内容定价等场景:
dynamicPrice := func(ctx context.Context, reqCtx x402http.HTTPRequestContext) (x402.Price, error) { tier := "standard" // 实际可从 reqCtx.Adapter 的 query/header 中提取 if tier == "premium" { return "$0.005", nil // Premium 档:0.5 美分 } return "$0.001", nil // Standard 档:0.1 美分 } routes := x402http.RoutesConfig{ "GET /weather": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", PayTo: evmPayeeAddress, Price: x402http.DynamicPriceFunc(dynamicPrice), Network: evmNetwork, }, }, }, }把普通函数签名func(ctx, reqCtx) (x402.Price, error)用x402http.DynamicPriceFunc(...)包装后赋给Price字段即可。Price字段类型为interface{},SDK 会在处理请求时做类型断言(见下文「源码级原理」)。
完整示例中,dynamicPrice内部以tier变量区分档位,并在 handler 里通过c.DefaultQuery("tier", "standard")消费查询参数:?tier=premium返回更详细的天气数据(湿度、风速、降水),?tier=standard只返回基础数据,从而实现价格与内容同步分层。
适用场景:分层定价、基于用户的定价、基于内容的定价,以及任何"价格随请求变化"的业务。
动态收款路由:把支付路由到不同收款方
dynamic-pay-to示例解决的是"钱付给谁"的问题——在 Marketplace 中,不同卖家的资源应把支付路由到对应卖家的地址。做法同样是把PayTo替换为函数:
addressLookup := map[string]string{ "US": "0x...", "UK": "0x...", // ... 每个国家/卖家一个地址 } dynamicPayTo := func(ctx context.Context, reqCtx x402http.HTTPRequestContext) (string, error) { country := "US" // 实际可从 reqCtx.Adapter 的 query/header 中提取 address, ok := addressLookup[country] if !ok { address = defaultAddress // 未命中时回退到默认地址 } return address, nil } routes := x402http.RoutesConfig{ "GET /weather": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", PayTo: x402http.DynamicPayToFunc(dynamicPayTo), Price: "$0.001", Network: evmNetwork, }, }, }, }完整示例用国家代码(US/UK/CA/AU/NZ/IE/FR)作为键做地址查找表,未命中时回退到EVM_PAYEE_ADDRESS默认地址。函数签名func(ctx context.Context, reqCtx x402http.HTTPRequestContext) (string, error)通过x402http.DynamicPayToFunc包装后赋给PayTo字段。
适用场景:Marketplace 应用,根据被访问的资源把支付路由给不同的卖家、内容创作者或服务提供商。生产环境中地址查找表通常替换为数据库查询。
生命周期钩子:在验证与结算前后挂载业务逻辑
hooks示例把支付流程拆成验证(verify)与结算(settle)两个阶段,并在每个阶段的前、后、失败路径上各暴露一个钩子。SDK 层面所有钩子类型定义在 go/server_hooks.go,核心代码如下:
facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ URL: facilitatorURL, }) server := x402.Newx402ResourceServer( x402.WithFacilitatorClient(facilitatorClient), ). Register(evmNetwork, evm.NewExactEvmScheme()). OnBeforeVerify(func(ctx x402.VerifyContext) (*x402.BeforeHookResult, error) { fmt.Println("Before verify hook", ctx) // 返回 &x402.BeforeHookResult{Abort: true, Reason: "..."} 可中止验证 return nil, nil }). OnAfterSettle(func(ctx x402.SettleResultContext) error { // 支付成功入账:写库、发通知等 db.RecordTransaction(ctx.Result.Transaction, ctx.Result.Payer) return nil }). OnSettleFailure(func(ctx x402.SettleFailureContext) (*x402.SettleFailureHookResult, error) { // 返回 &x402.SettleFailureHookResult{Recovered: true, Result: &x402.SettleResponse{...}} 可恢复失败 return nil, nil }) r := gin.Default() r.Use(ginmw.PaymentMiddleware(routes, server))注意这里使用的是x402.Newx402ResourceServer+Register+ 链式钩子注册,再交给ginmw.PaymentMiddleware;而其他示例使用的是ginmw.X402Payment(ginmw.Config{...})一体化配置。两种方式等价,前者更便于精细编排钩子。
可用钩子全景(每个钩子的完整签名与语义见 go/server_hooks.go):
| 钩子 | 触发时机 | 能力 |
|---|---|---|
OnBeforeVerify | 支付验证之前 | 可中止(返回BeforeHookResult{Abort: true, Reason: ...}) |
OnAfterVerify | 验证成功之后 | 副作用处理;返回错误只记日志,不影响验证结果 |
OnVerifyFailure | 验证失败时 | 可恢复(返回VerifyFailureHookResult{Recovered: true, Result: ...}) |
OnBeforeSettle | 结算之前 | 可中止 |
OnAfterSettle | 结算成功之后 | 副作用处理;错误只记日志 |
OnSettleFailure | 结算失败时 | 可恢复(返回SettleFailureHookResult{Recovered: true, Result: ...}) |
钩子上下文的语义:
VerifyContext/SettleContext通过视图接口(PaymentPayloadView、PaymentRequirementsView)提供版本无关的访问方式,同时附带PayloadBytes/RequirementsBytes原始字节,为 Bazaar 等扩展提供逃生舱。BeforeHookResult含Abort/Reason/Message三个字段;失败恢复钩子的Recovered=true会让 SDK 直接用你提供的Result替代错误返回(相关类型定义见 go/server_hooks.go)。- 除链式
.OnXxx()外,还可用选项式x402.WithBeforeVerifyHook(...)等ResourceServerOption注册钩子(见 go/server_hooks.go),便于与函数式配置风格统一。
hooks.go完整示例会打印六个钩子的执行日志(🔵 前置钩子 / 🟢 成功钩子 / 🔴 失败钩子),运行后即可在控制台观察完整的支付生命周期。
适用场景:
- 把支付事件写入数据库或监控系统;
- 在处理支付前做自定义校验(如风控、黑白名单);
- 对失败支付实现重试或恢复逻辑;
- 支付成功后触发副作用(通知、数据库更新、发放权益)。
自定义货币解析:接受 USDC 之外的代币
默认情况下 exact 方案的货币解析器把美元价格映射为 USDC。custom-money-definition示例通过RegisterMoneyParser注册自定义解析器,按网络或金额条件选择代币:
evmScheme := evm.NewExactEvmScheme().RegisterMoneyParser( func(amount float64, network x402.Network) (*x402.AssetAmount, error) { // 在 Gnosis Chain(eip155:100)上使用 Wrapped XDAI if string(network) == "eip155:100" { return &x402.AssetAmount{ Amount: fmt.Sprintf("%.0f", amount*1e18), // WXDAI 18 位小数 Asset: "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", // Gnosis 上 WXDAI 地址 Extra: map[string]interface{}{"token": "Wrapped XDAI"}, }, nil } // 大额支付(>100 美元)改用 DAI if amount > 100 { return &x402.AssetAmount{ Amount: fmt.Sprintf("%.0f", math.Round(amount*1e18)), // DAI 18 位小数 Asset: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", // Base Sepolia 上 DAI 地址 Extra: map[string]interface{}{"token": "DAI", "tier": "large"}, }, nil } return nil, nil // 返回 nil 走默认 USDC 解析器 }, ) r.Use(ginmw.X402Payment(ginmw.Config{ Routes: routes, Facilitator: facilitatorClient, Schemes: []ginmw.SchemeConfig{ {Network: evmNetwork, Server: evmScheme}, // 使用自定义 scheme }, }))解析器语义要点:
- 输入是美元金额(float)与目标网络,输出
*x402.AssetAmount——包含原子单位Amount(按代币小数位换算,如amount*1e18)、合约地址Asset与Extra附加信息; - 返回
nil, nil表示回退到该网络默认解析器(USDC),实现"默认 + 特例"的叠加策略; - 方案服务器(
evmScheme)被整体注册进ginmw.SchemeConfig,中间件验证时会使用自定义解析结果。
示例中针对 Gnosis 使用 WXDAI、针对大额使用 DAI 两条分支仅为演示——注释明确提示 WXDAI 并不符合 EIP-3009 合规要求,生产使用需确认代币与结算机制的兼容性。
适用场景:接受 USDC 之外的代币;或按条件切换代币(如大额用 DAI、特定网络用自定义代币)。
协议响应格式:402 与 200 的完整报文
理解响应格式是调试与二次开发的基础。以下报文来自 README 的完整记录,与 SDK 实现一致。
未支付请求:HTTP 402 Payment Required
HTTP/1.1 402 Payment Required Content-Type: application/json; charset=utf-8 PAYMENT-REQUIRED: <base64-encoded JSON> {}PAYMENT-REQUIRED头携带 base64 编码的 JSON 支付需求。注意amount是原子单位(如1000= 0.001 USDC,因为 USDC 为 6 位小数):
{ "x402Version": 2, "error": "Payment required", "resource": { "url": "http://localhost:4021/weather", "description": "Weather data", "mimeType": "application/json" }, "accepts": [ { "scheme": "exact", "network": "eip155:84532", "amount": "1000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x...", "maxTimeoutSeconds": 300, "extra": { "name": "USDC", "version": "2", "resourceUrl": "http://localhost:4021/weather" } } ] }支付成功:HTTP 200 OK
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 PAYMENT-RESPONSE: <base64-encoded JSON> {"report":{"weather":"sunny","temperature":70}}PAYMENT-RESPONSE头携带 base64 编码的结算详情:
{ "success": true, "transaction": "0x...", "network": "eip155:84532", "payer": "0x...", "requirements": { "scheme": "exact", "network": "eip155:84532", "amount": "1000", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "payTo": "0x...", "maxTimeoutSeconds": 300, "extra": { "name": "USDC", "version": "2", "resourceUrl": "http://localhost:4021/weather" } } }从源码看(go/http/server.go),服务端对浏览器请求(Accept含text/html且User-Agent含Mozilla)返回 HTML paywall 页面而非 JSON;对 API 客户端返回带PAYMENT-REQUIRED头的 402。HTML 生成遵循"路由自定义 HTML > 注册的PaywallProvider> 内置 EVM/SVM 模板"的降级链(go/http/paywall.go),内置模板把paymentRequired序列化注入window.x402全局对象供前端钱包逻辑使用。
源码级原理:动态函数如何被解析与执行
这一节把前文的动态能力与协议流程落到源码,方便你排查问题或扩展自定义能力。
RoutesConfig 与 PaymentOption 的数据结构
路由与支付选项的定义在 go/http/server.go:
PaymentOption:单个支付选项,含Scheme、PayTo(interface{},可为字符串或DynamicPayToFunc)、Price(interface{},可为x402.Price或DynamicPriceFunc)、Network、可选MaxTimeoutSeconds与Extra;RouteConfig:路由级配置,含Accepts(支付选项数组)、Resource、Description、MimeType、CustomPaywallHTML、Extensions与可选的UnpaidResponseBody回调(可为未支付请求生成自定义响应体);RoutesConfig:map[路由模式]RouteConfig,模式形如"GET /weather",也支持*通配符。
动态函数的运行时解析
核心逻辑在BuildPaymentRequirementsFromOptions(go/http/server.go):遍历每个PaymentOption,用类型断言判断字段是否为函数——
- 若
option.PayTo断言为DynamicPayToFunc,则调用payToFunc(ctx, reqCtx)得到收款地址;否则按字符串处理; - 若
option.Price断言为DynamicPriceFunc,则调用priceFunc(ctx, reqCtx)得到价格;否则按静态值处理; - 解析结果组装成
x402.ResourceConfig后交给BuildPaymentRequirementsFromConfig生成支付需求(含金额换算与货币解析)。
这意味着动态函数每次请求都会执行,且能拿到完整的HTTPRequestContext(Adapter、Path、Method、PaymentHeader、RoutePattern,见 go/http/server.go),因此价格、收款方可以基于 query、header、用户会话等任意请求信息决定。
请求处理主流程
ProcessHTTPRequest(go/http/server.go)的完整链路:
- 用编译好的路由正则匹配
Path+Method,未命中 →no-payment-required; - 依次执行
protectedRequestHooks(可GrantAccess免支付放行,或Abort返回 403); - 解析
PAYMENT-SIGNATURE头得到 V2 支付负载(无头 → 返回 402 +PAYMENT-REQUIRED); - 构建所有支付需求(触发动态函数解析)→
FindMatchingRequirements找到匹配项 →VerifyPayment验证签名/授权; - 验证通过返回
payment-verified,由 Gin 中间件放行到业务 handler,随后ProcessSettlement(go/http/server.go)执行结算并生成PAYMENT-RESPONSE头;结算失败时回写 402。
Gin 中间件配置项
ginmw.X402Payment(ginmw.Config{...})的完整配置(go/http/gin/middleware.go):
| 配置项 | 说明 |
|---|---|
Routes | x402http.RoutesConfig路由支付配置 |
Facilitator/FacilitatorClients | Facilitator 客户端(可用WithFacilitatorClient添加多个) |
Schemes | []ginmw.SchemeConfig{Network, Server},声明各网络的方案服务器 |
PaywallConfig | 浏览器 paywall 的AppName、AppLogo、CurrentURL、Testnet配置 |
SyncFacilitatorOnStart | 启动时同步 Facilitator 支持信息(动态价格/支付示例均设为true) |
Timeout | 支付操作的上下文超时(示例为30 * time.Second) |
ErrorHandler/SettlementHandler | 自定义错误与结算处理回调 |
启动后建议观察各示例控制台输出:hooks示例打印六个钩子的执行日志;dynamic-price打印档位与价格;dynamic-pay-to打印按国家路由的地址;all-networks打印已启用的网络与监听地址。结合PAYMENT-REQUIRED/PAYMENT-RESPONSE头(可用任意 HTTP 调试工具查看 base64 解码后的 JSON),即可端到端验证本文所述的全部行为。
【免费下载链接】x402A payments protocol for the internet. Built on HTTP.项目地址: https://gitcode.com/GitHub_Trending/x4/x402
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考