Go SDK 参考#
Go SDK 参考(适用于 exact、exact + permit2、upto、aggr_deferred)#
模块 / 包#
所有导入路径都以 github.com/okx/payments/go/x402/... 为前缀。下表按 Go module / package 组织。
| 包路径 | 描述 |
|---|---|
github.com/okx/payments/go/x402 | 核心:资源服务端 X402ResourceServer、facilitator 客户端接口、hooks、错误类型、Network / Price 等 |
github.com/okx/payments/go/x402/types | 线格式类型(v1/v2):PaymentRequirements、PaymentPayload、PaymentRequired、SupportedKind 等 |
github.com/okx/payments/go/x402/http | HTTP 资源服务端 HTTPServer、路由配置 RoutesConfig、HTTPFacilitatorClient / OKXFacilitatorClient |
github.com/okx/payments/go/x402/http/nethttp | net/http 中间件 |
github.com/okx/payments/go/x402/http/gin | Gin 中间件 |
github.com/okx/payments/go/x402/http/echo | Echo 中间件 |
github.com/okx/payments/go/x402/mechanisms/evm | EVM 共享原语:payload 类型、Permit2 / upto 常量、AssetInfo / NetworkConfig |
github.com/okx/payments/go/x402/mechanisms/evm/exact/server | exact(EIP-3009 / Permit2)卖方 scheme |
github.com/okx/payments/go/x402/mechanisms/evm/upto/server | upto(cap + override)卖方 scheme |
github.com/okx/payments/go/x402/mechanisms/evm/deferred/server | aggr_deferred(TEE 聚合)卖方 scheme |
github.com/okx/payments/go/x402/adapters | 多协议入口适配器 X402Adapter(与 MPP 等统一调度时使用) |
Go SDK 当前主要提供服务端(卖方)和 facilitator 客户端能力。
mechanisms/evm/exact/client、upto/client、deferred/client等买方包存在,但本参考聚焦卖方侧;买方支付签名能力以这些 client 包为准,本文不展开。
核心类型#
Network / Price / AssetAmount#
Network 是具名字符串类型并带通配符匹配方法,Price 是空接口(可传 string、数字或 AssetAmount)。
// Network 是 CAIP-2 格式的链标识,例如 "eip155:196"。
type Network string
func ParseNetwork(s string) Network
func (n Network) Match(pattern Network) bool // "eip155:1" 匹配 "eip155:*"
func (n Network) Parse() (namespace, reference string, err error)
// Price 可以是 "$0.01" / "0.01" / 数字 / AssetAmount。
type Price interface{}
type AssetAmount struct {
Asset string `json:"asset"` // 代币合约地址
Amount string `json:"amount"` // 最小单位金额
Extra map[string]interface{} `json:"extra,omitempty"`
}
包级网络辅助函数:
func IsWildcardNetwork(network Network) bool
func MatchesNetwork(pattern Network, network Network) bool
ResourceInfo#
type ResourceInfo struct {
URL string `json:"url"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
PaymentRequirements#
type PaymentRequirements struct {
Scheme string `json:"scheme"` // "exact" | "aggr_deferred" | "upto"
Network string `json:"network"` // CAIP-2
Asset string `json:"asset"` // 代币合约地址
Amount string `json:"amount"` // 价格(upto 时为 cap),最小单位
PayTo string `json:"payTo"` // 收款地址
MaxTimeoutSeconds int `json:"maxTimeoutSeconds"`
Extra map[string]interface{} `json:"extra,omitempty"` // scheme 专属数据
}
Extra 中常见的字段:
| key | scheme | 含义 |
|---|---|---|
assetTransferMethod | exact / upto | "eip3009"(默认)或 "permit2";upto server 始终写 "permit2" |
facilitatorAddress | upto | upto proxy 强制 witness.facilitator == msg.sender,由 UptoEvmScheme.EnhancePaymentRequirements 从 supportedKind.Extra 自动注入 |
name / version | exact(EIP-3009 路径) | EIP-712 domain,供客户端签名 |
在
mechanisms/evm/upto/server包里这两个 key 还有具名常量:AssetTransferMethodKey = "assetTransferMethod"、ExtraFacilitatorAddressKey。
PaymentRequired#
402 响应体(v2)。
type PaymentRequired struct {
X402Version int `json:"x402Version"`
Error string `json:"error,omitempty"`
Resource *ResourceInfo `json:"resource,omitempty"`
Accepts []PaymentRequirements `json:"accepts"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
PaymentPayload#
客户端的签名支付(v2)。Payload 内容随 scheme 不同而不同(EIP-3009 / Permit2 / upto Permit2)。
type PaymentPayload struct {
X402Version int `json:"x402Version"`
Payload map[string]interface{} `json:"payload"` // 详见下方 EVM Payload 一节
Accepted PaymentRequirements `json:"accepted"`
Resource *ResourceInfo `json:"resource,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
func (p PaymentPayload) GetVersion() int
func (p PaymentPayload) GetScheme() string
func (p PaymentPayload) GetNetwork() string
func (p PaymentPayload) GetPayload() map[string]interface{}
Go SDK 显式保留了 v1 类型(
PaymentRequirementsV1/PaymentPayloadV1/PaymentRequiredV1/SupportedKindV1),并通过PaymentRequirementsView/PaymentPayloadView两个接口让 hooks 做到版本无关。新接入默认用 v2。
Facilitator 类型#
Go SDK 的 facilitator 接口在网络边界传 []byte(由 SDK 内部按版本路由),verify/settle 的入参不是 typed 的 VerifyRequest / SettleRequest,而是 payload + requirements 的原始字节。响应类型仍然是 typed 的。
VerifyResponse / SettleResponse#
type VerifyResponse struct {
IsValid bool `json:"isValid"`
InvalidReason string `json:"invalidReason,omitempty"`
InvalidMessage string `json:"invalidMessage,omitempty"`
Payer string `json:"payer,omitempty"`
}
type SettleResponse struct {
Success bool `json:"success"`
ErrorReason string `json:"errorReason,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
Payer string `json:"payer,omitempty"`
Transaction string `json:"transaction"` // 交易哈希(aggr_deferred 为空)
Network Network `json:"network"`
Status string `json:"status,omitempty"` // OKX 扩展: "pending" | "success" | "timeout"
// upto 等场景下实际结算金额可能严格小于签名 cap。
Amount string `json:"amount,omitempty"`
}
sync/async 结算开关放在 facilitator 客户端配置 上(
OKXFacilitatorConfig.SyncSettle,见下文),而不是每次 settle 调用或每条路由上。
SupportedKind / SupportedResponse#
type SupportedKind struct {
X402Version int `json:"x402Version"`
Scheme string `json:"scheme"`
Network string `json:"network"`
// upto: facilitator 地址通过 extra.facilitatorAddress 暴露;卖方 scheme
// 会在 EnhancePaymentRequirements 时把它注入 challenge 的 extra。
Extra map[string]interface{} `json:"extra,omitempty"`
}
type SupportedResponse struct {
Kinds []SupportedKind `json:"kinds"`
Extensions []string `json:"extensions"`
Signers map[string][]string `json:"signers"` // CAIP family → signer 地址
}
SettleStatusResponse#
type SettleStatusResponse struct {
Success bool `json:"success"`
Status string `json:"status,omitempty"` // "pending" | "success" | "failed"
ErrorReason string `json:"errorReason,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
Payer string `json:"payer,omitempty"`
Transaction string `json:"transaction,omitempty"`
Network Network `json:"network,omitempty"`
}
接口#
SchemeNetworkServer#
服务端 scheme 实现。exact / aggr_deferred / upto 都实现该接口。
type SchemeNetworkServer interface {
Scheme() string
ParsePrice(price Price, network Network) (AssetAmount, error)
EnhancePaymentRequirements(
ctx context.Context,
requirements types.PaymentRequirements,
supportedKind types.SupportedKind,
extensions []string,
) (types.PaymentRequirements, error)
}
FacilitatorClient#
与远程 facilitator 通信的网络边界。注意入参是字节,版本由 SDK 内部检测。
type FacilitatorClient interface {
Verify(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*VerifyResponse, error)
Settle(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*SettleResponse, error)
GetSupported(ctx context.Context) (SupportedResponse, error)
}
// 可选接口:支持按交易哈希查询结算状态(用于 timeout 恢复轮询)。
type SettleStatusChecker interface {
GetSettleStatus(ctx context.Context, txHash string) (*SettleStatusResponse, error)
}
ResourceServerExtension / FacilitatorExtension#
Go 的扩展接口精简如下:
// 在 types 包。
type ResourceServerExtension interface {
Key() string
EnrichDeclaration(declaration interface{}, transportContext interface{}) interface{}
}
// 在 x402 包,facilitator 侧扩展。
type FacilitatorExtension interface {
Key() string
}
func NewFacilitatorExtension(key string) FacilitatorExtension
verify/settle extension 的富化通过一组 hooks(
BeforeVerifyHook/AfterVerifyHook/OnVerifyFailureHook/BeforeSettleHook/AfterSettleHook/OnSettleFailureHook)实现,通过WithBeforeVerifyHook(...)等ResourceServerOption注册(见下)。
服务端 API(X402ResourceServer)#
构造与注册#
构造用函数式选项(opts ...ResourceServerOption),scheme 通过链式 Register(network, scheme) 直接注册——同一 network 上多个 scheme 可共存,路由侧按 scheme 名挑选。
import (
"github.com/okx/payments/go/x402"
exact "github.com/okx/payments/go/x402/mechanisms/evm/exact/server"
deferred "github.com/okx/payments/go/x402/mechanisms/evm/deferred/server"
uptoserver "github.com/okx/payments/go/x402/mechanisms/evm/upto/server"
)
server := x402.Newx402ResourceServer(
x402.WithFacilitatorClient(fac),
).
Register("eip155:196", exact.NewExactEvmScheme()). // exact (EIP-3009 / Permit2)
Register("eip155:196", deferred.NewAggrDeferredEvmScheme()). // aggr_deferred
Register("eip155:196", uptoserver.NewUptoEvmScheme()) // upto (cap + override)
构造选项:
type ResourceServerOption func(*x402ResourceServer)
func WithFacilitatorClient(client FacilitatorClient) ResourceServerOption
func WithSchemeServer(network Network, schemeServer SchemeNetworkServer) ResourceServerOption // 等价于链式 Register
func WithCacheTTL(ttl time.Duration) ResourceServerOption
func WithBeforeVerifyHook(hook BeforeVerifyHook) ResourceServerOption
func WithAfterVerifyHook(hook AfterVerifyHook) ResourceServerOption
func WithOnVerifyFailureHook(hook OnVerifyFailureHook) ResourceServerOption
func WithBeforeSettleHook(hook BeforeSettleHook) ResourceServerOption
func WithAfterSettleHook(hook AfterSettleHook) ResourceServerOption
func WithOnSettleFailureHook(hook OnSettleFailureHook) ResourceServerOption
方法#
func Newx402ResourceServer(opts ...ResourceServerOption) *x402ResourceServer
func (s *x402ResourceServer) Register(network Network, schemeServer SchemeNetworkServer) *x402ResourceServer
func (s *x402ResourceServer) RegisterExtension(extension types.ResourceServerExtension) *x402ResourceServer
// 拉取 facilitator 的 supported kinds 并缓存。initialize 不是可选的:
// HTTP 层会在中间件挂载/首次请求时驱动它(见下)。
func (s *x402ResourceServer) Initialize(ctx context.Context) error
func (s *x402ResourceServer) HasRegisteredScheme(network Network, scheme string) bool
func (s *x402ResourceServer) HasFacilitatorSupport(network Network, scheme string) bool
func (s *x402ResourceServer) GetFacilitatorClient(network Network, scheme string) FacilitatorClient
// 用一份 ResourceConfig + supportedKind 生成 challenge(内部会调度对应 scheme 的
// ParsePrice + EnhancePaymentRequirements)。
func (s *x402ResourceServer) BuildPaymentRequirements(
ctx context.Context,
config ResourceConfig,
supportedKind types.SupportedKind,
extensions []string,
) (types.PaymentRequirements, error)
func (s *x402ResourceServer) VerifyPayment(
ctx context.Context,
payload types.PaymentPayload,
requirements types.PaymentRequirements,
) (*VerifyResponse, error)
// overrides: upto scheme 用——业务 handler 决定本次实际扣款金额(≤ cap),
// 通过响应头透传给中间件,中间件解析为 *SettlementOverrides 后调本接口。
func (s *x402ResourceServer) SettlePayment(
ctx context.Context,
payload types.PaymentPayload,
requirements types.PaymentRequirements,
overrides *SettlementOverrides,
) (*SettleResponse, error)
func (s *x402ResourceServer) FindMatchingRequirements(
available []types.PaymentRequirements,
payload types.PaymentPayload,
) *types.PaymentRequirements
轮询结算状态没有独立的核心方法;逻辑收进 HTTP 层(
HTTPServer.SetPollDeadline+OnSettlementTimeouthook + facilitator 的GetSettleStatus,见下)。PollResult类型本身存在:
type PollResult string
const (
PollResultSuccess PollResult = "success"
PollResultFailed PollResult = "failed"
PollResultTimeout PollResult = "timeout"
)
ResourceConfig / SettlementOverrides#
type ResourceConfig struct {
Scheme string `json:"scheme"`
PayTo string `json:"payTo"`
Price Price `json:"price"`
Network Network `json:"network"`
MaxTimeoutSeconds int `json:"maxTimeoutSeconds,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
}
// 由业务 handler 通过响应头设置;中间件读出后在 settle 时应用。
type SettlementOverrides struct {
// 实际结算金额(原子单位),必须 <= 授权 cap。
Amount string `json:"amount,omitempty"`
}
Amount字段被记为原子单位整数字符串(must be<= authorized max)。
OKX Facilitator 客户端(OKXFacilitatorClient)#
import x402http "github.com/okx/payments/go/x402/http"
syncSettle := true
fac, err := x402http.NewOKXFacilitatorClient(&x402http.OKXFacilitatorConfig{
Auth: x402http.OKXAuthConfig{
APIKey: apiKey,
SecretKey: secretKey,
Passphrase: passphrase,
// BaseURL / BasePath 可选
},
BaseURL: "https://web3.okx.com", // 默认值;sandbox/staging 可覆盖
SyncSettle: &syncSettle, // 默认 true:等链上确认后返回(exact)
})
type OKXAuthConfig struct {
APIKey string
SecretKey string
Passphrase string
BaseURL string // 默认 "https://web3.okx.com"
BasePath string // 例如 "/api/v6/x402"
}
type OKXFacilitatorConfig struct {
Auth OKXAuthConfig
BaseURL string // 默认 "https://web3.okx.com"
SyncSettle *bool // 默认 true
HTTPClient *http.Client
Timeout time.Duration
}
func NewOKXFacilitatorClient(config *OKXFacilitatorConfig) (*OKXFacilitatorClient, error)
func (c *OKXFacilitatorClient) GetSupported(ctx context.Context) (x402.SupportedResponse, error)
func (c *OKXFacilitatorClient) Verify(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error)
func (c *OKXFacilitatorClient) Settle(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error)
func (c *OKXFacilitatorClient) GetSettleStatus(ctx context.Context, txHash string) (*x402.SettleStatusResponse, error)
NewOKXFacilitatorClient 在缺少 APIKey / SecretKey / Passphrase 时返回错误。客户端自动给请求加 HMAC-SHA256 OKX 认证头,并自动解包 {"code":0,"data":{...},"msg":""} 信封。
sync vs async:sync/async 由
OKXFacilitatorConfig.SyncSettle(per-client,默认true)控制。HTTP 的RouteConfig没有SyncSettle字段。
通用 HTTP facilitator 客户端#
非 OKX 的 facilitator 用 HTTPFacilitatorClient:
type FacilitatorConfig struct {
URL string
HTTPClient *http.Client
AuthProvider AuthProvider
Timeout time.Duration // 默认 30s
Identifier string
}
func NewHTTPFacilitatorClient(config *FacilitatorConfig) *HTTPFacilitatorClient
func NewFacilitatorClient(config *FacilitatorConfig) *HTTPFacilitatorClient // 别名
const DefaultFacilitatorURL = "https://x402.org/facilitator"
HMAC 认证#
Go 把认证封装进了 OKXFacilitatorClient 内部,对外暴露的是一个低层签名函数和认证抽象接口:
// 计算 OKX 签名: Base64(HMAC-SHA256(secret, timestamp + METHOD + path + body))。
func ComputeSignature(secretKey, timestamp, method, path, body string) string
type AuthProvider interface {
GetAuthHeaders(ctx context.Context) (AuthHeaders, error)
}
type AuthHeaders struct {
// 各端点的认证头集合
}
没有"给任意请求一键加全套
OK-ACCESS-*头"的公开函数;具体头部由OKXFacilitatorClient内部基于ComputeSignature组装。
HTTP 工具#
请求头编/解码#
Go SDK 当前没有把 header 编解码函数作为公开 API 暴露——PAYMENT-SIGNATURE / PAYMENT-REQUIRED / PAYMENT-RESPONSE 的 base64 编解码由 HTTPServer.ProcessHTTPRequest / 中间件内部完成。这几个头名在源码里是字符串字面量,不是导出常量。
常量#
// http 包
const DefaultPollInterval = 1 * time.Second
const DefaultPollDeadline = 5 * time.Second
const SettlementOverridesHeader = "settlement-overrides"
const (
ResultNoPaymentRequired = "no-payment-required"
ResultPaymentVerified = "payment-verified"
ResultPaymentError = "payment-error"
)
Go 只把
SettlementOverridesHeader提升为导出常量,PAYMENT-SIGNATURE/PAYMENT-REQUIRED/PAYMENT-RESPONSE三个头名在内部使用。
路由配置#
RoutesConfig 是 map[string]RouteConfig,key 是 "GET /path" 形式。每个 accept 是 PaymentOption。
type RoutesConfig map[string]RouteConfig
type RouteConfig struct {
Accepts PaymentOptions `json:"accepts"`
Resource string `json:"resource,omitempty"` // 手动钉死 ResourceInfo.url;为空时按请求自动拼
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
CustomPaywallHTML string `json:"customPaywallHtml,omitempty"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
AcceptedDomains []string `json:"acceptedDomains,omitempty"` // payload.resource.url host 白名单(容忍反代/CDN 改写 Host)
UnpaidResponseBody UnpaidResponseBodyFunc `json:"-"` // 自定义未付费响应体
}
type PaymentOptions = []PaymentOption
type PaymentOption struct {
Scheme string `json:"scheme"` // "exact" | "aggr_deferred" | "upto"
PayTo interface{} `json:"payTo"` // string 或 DynamicPayToFunc
Price interface{} `json:"price"` // x402.Price 或 DynamicPriceFunc
Network x402.Network `json:"network"`
MaxTimeoutSeconds int `json:"maxTimeoutSeconds,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
}
// PayTo / Price 支持运行时动态求值。
type DynamicPayToFunc func(context.Context, HTTPRequestContext) (string, error)
type DynamicPriceFunc func(context.Context, HTTPRequestContext) (x402.Price, error)
两点说明:(1) sync/async 由 facilitator 客户端
OKXFacilitatorConfig.SyncSettle控制,不在RouteConfig上;(2)PayTo/Price是interface{},既能传静态值,也能传Dynamic*Func动态解析回调。
示例(多 scheme 共存)#
routes := x402http.RoutesConfig{
"GET /api/data": {
Accepts: x402http.PaymentOptions{
// 1) 默认 exact + EIP-3009(X Layer 上的 USD₮0)
{
Scheme: "exact",
Price: "$0.01",
Network: "eip155:196",
PayTo: "0xSeller",
},
// 2) exact + Permit2(通用 ERC-20)
{
Scheme: "exact",
Price: "$0.01",
Network: "eip155:196",
PayTo: "0xSeller",
Extra: map[string]any{"assetTransferMethod": "permit2"},
},
// 3) aggr_deferred(TEE 聚合)
{
Scheme: "aggr_deferred",
Price: "$0.001",
Network: "eip155:196",
PayTo: "0xSeller",
},
},
Description: "Premium data",
MimeType: "application/json",
},
}
exact + permit2就是exactscheme + 路由Extra: {"assetTransferMethod":"permit2"}。upto路由通常不需要手填Extra.facilitatorAddress——UptoEvmScheme.EnhancePaymentRequirements会从 facilitator/supported流自动注入。
中间件#
为 net/http / Gin / Echo 各提供一个中间件包。三者 API 形状一致:都有 X402Payment(Config)、PaymentMiddleware(...)、PaymentMiddlewareFromConfig(...)、PaymentMiddlewareFromHTTPServer(...)、SimpleX402Payment(...)。
net/http#
import (
nethttpmw "github.com/okx/payments/go/x402/http/nethttp"
evm "github.com/okx/payments/go/x402/mechanisms/evm/exact/server"
)
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handler)
handler := nethttpmw.X402Payment(nethttpmw.Config{
Routes: routes,
Facilitator: fac,
Schemes: []nethttpmw.SchemeConfig{
{Network: "eip155:*", Server: evm.NewExactEvmScheme()},
},
SyncFacilitatorOnStart: true,
Timeout: 30 * time.Second,
})(mux)
func X402Payment(config Config) func(http.Handler) http.Handler
type Config struct {
Routes x402http.RoutesConfig
Facilitator x402.FacilitatorClient // 用 Facilitator 或 Facilitators,二选一
Facilitators []x402.FacilitatorClient
Schemes []SchemeConfig
PaywallConfig *x402http.PaywallConfig
SyncFacilitatorOnStart bool // 默认 true:启动时拉 supported kinds
Timeout time.Duration // 默认 30s
ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
SettlementHandler func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse)
}
type SchemeConfig struct {
Network x402.Network
Server x402.SchemeNetworkServer
}
// 用预构造好的 server / HTTPServer(便于先挂 hook):
func PaymentMiddleware(routes x402http.RoutesConfig, server *x402.X402ResourceServer, opts ...MiddlewareOption) func(http.Handler) http.Handler
func PaymentMiddlewareFromHTTPServer(httpServer *x402http.HTTPServer, opts ...MiddlewareOption) func(http.Handler) http.Handler
func PaymentMiddlewareFromConfig(routes x402http.RoutesConfig, opts ...MiddlewareOption) func(http.Handler) http.Handler
// 最小配置版(单路由 + 单 facilitator URL):
func SimpleX402Payment(payTo string, price string, network x402.Network, facilitatorURL string) func(http.Handler) http.Handler
// 从 handler 内部取已验证的 payload / requirements:
func PayloadFromContext(ctx context.Context) (*types.PaymentPayload, bool)
func RequirementsFromContext(ctx context.Context) (*types.PaymentRequirements, bool)
MiddlewareOption 形式(与 Config 等价):WithFacilitatorClient / WithScheme(network, server) / WithPaywallConfig / WithSyncFacilitatorOnStart / WithTimeout / WithErrorHandler / WithSettlementHandler。
Gin#
import ginmw "github.com/okx/payments/go/x402/http/gin"
r.Use(ginmw.X402Payment(ginmw.Config{
Routes: routes,
Facilitator: fac,
Schemes: []ginmw.SchemeConfig{
{Network: "eip155:*", Server: evm.NewExactEvmScheme()},
},
SyncFacilitatorOnStart: true,
Timeout: 30 * time.Second,
}))
三个框架都提供 upto 局部结算辅助 SetSettlementOverrides——业务 handler 通过它把本次实际扣款金额回传给中间件(中间件在 settle 前读出,并把响应头从客户端响应里剥掉):
// 各框架同名函数,签名随框架的 request/response 句柄变化:
func ginmw.SetSettlementOverrides(c *gin.Context, overrides *x402.SettlementOverrides)
func echomw.SetSettlementOverrides(c echo.Context, overrides *x402.SettlementOverrides)
func nethttpmw.SetSettlementOverrides(w http.ResponseWriter, overrides *x402.SettlementOverrides)
// handler 内(upto)——以 Gin 为例:
ginmw.SetSettlementOverrides(c, &x402.SettlementOverrides{Amount: "1234000"})
Echo#
echo 包与上面同构:X402Payment(Config) echo.MiddlewareFunc、PaymentMiddleware*、SimpleX402Payment,Config / SchemeConfig / MiddlewareOption 字段一致(回调签名换成 func(echo.Context, ...))。
中间件流程#
- 匹配请求路由 cfg;没命中 → 透传给 inner handler
- 无
PAYMENT-SIGNATURE请求头 → 返回 402 +PAYMENT-REQUIRED(浏览器请求则渲染 paywall HTML) - 解码并验证支付 payload,与路由
accepts匹配 - 经 facilitator 验证(
Verify) - 调内部 handler 并缓冲响应
- 若 handler 设置了 settlement override(upto,用
SetSettlementOverrides),中间件解析为*SettlementOverrides - 经 facilitator 结算(
Settle) - 异步(
status:"pending"/"timeout")→ 在pollDeadline内轮询GetSettleStatus - 仍超时 → 调用
OnSettlementTimeouthook(若配置) - 给响应加
PAYMENT-RESPONSE头
HTTPServer 上可挂的 hook / 设置#
预构造 HTTPServer 后用 PaymentMiddlewareFromHTTPServer 挂载,可链式注册:
httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer).
OnProtectedRequest(requestHook).
SetPollDeadline(8 * time.Second).
OnSettlementTimeout(func(ctx context.Context, txHash, network string) (confirmed bool, err error) {
// 超时观测:链上二次确认 / 日志 / 上报指标
return false, nil
})
func (s *HTTPServer) OnProtectedRequest(hook ProtectedRequestHook) *HTTPServer
func (s *HTTPServer) SetPollDeadline(deadline time.Duration) *HTTPServer
func (s *HTTPServer) OnSettlementTimeout(hook OnSettlementTimeoutHook) *HTTPServer
func (s *HTTPServer) RegisterPaywallProvider(provider PaywallProvider) *HTTPServer
func (s *HTTPServer) AddRoutes(routes RoutesConfig) *HTTPServer
func (s *HTTPServer) Initialize(ctx context.Context) error // 拉 supported kinds + 校验路由配置
// 超时恢复 hook:参数是 (ctx, txHash, network),返回 confirmed 决定是否交付资源。
type OnSettlementTimeoutHook func(ctx context.Context, txHash string, network string) (confirmed bool, err error)
OnSettlementTimeoutHook的参数顺序是(ctx, txHash, network),返回(confirmed bool, err error)。
EVM 机制(mechanisms/evm + scheme server 子包)#
ExactEvmScheme#
import exact "github.com/okx/payments/go/x402/mechanisms/evm/exact/server"
scheme := exact.NewExactEvmScheme()
scheme.Scheme() // "exact"
负责:价格解析("$0.01" / "0.01" / AssetAmount);按代币 decimals 换算原子单位;按 network 查默认资产;向 Extra 注入 EIP-712 domain(name / version)供 EIP-3009 签名;买家在 Extra.assetTransferMethod="permit2" 时走 Permit2 流程。
func NewExactEvmScheme() *ExactEvmScheme
func (s *ExactEvmScheme) Scheme() string
func (s *ExactEvmScheme) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error)
func (s *ExactEvmScheme) EnhancePaymentRequirements(ctx context.Context, requirements types.PaymentRequirements, supportedKind types.SupportedKind, extensionKeys []string) (types.PaymentRequirements, error)
func (s *ExactEvmScheme) ConvertToTokenAmount(decimalAmount string, network string) (string, error)
func (s *ExactEvmScheme) ConvertFromTokenAmount(tokenAmount string, network string) (string, error)
func (s *ExactEvmScheme) GetDisplayAmount(amount string, network string, asset string) (string, error)
func (s *ExactEvmScheme) GetSupportedNetworks() []string
func (s *ExactEvmScheme) ValidatePaymentRequirements(requirements x402.PaymentRequirements) error
func (s *ExactEvmScheme) RegisterMoneyParser(parser x402.MoneyParser) *ExactEvmScheme // 自定义价格→资产换算链
AggrDeferredEvmScheme#
import deferred "github.com/okx/payments/go/x402/mechanisms/evm/deferred/server"
scheme := deferred.NewAggrDeferredEvmScheme()
scheme.Scheme() // "aggr_deferred"
价格 / 需求逻辑全部委托给 ExactEvmScheme;卖家配置与 exact 完全相同,链上结算由 facilitator TEE 聚合(Facilitator 将 session-key 签名转换为 EOA 签名并批量上链)。
func NewAggrDeferredEvmScheme() *AggrDeferredEvmScheme
func (s *AggrDeferredEvmScheme) Scheme() string // "aggr_deferred"
func (s *AggrDeferredEvmScheme) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error)
func (s *AggrDeferredEvmScheme) EnhancePaymentRequirements(ctx context.Context, requirements types.PaymentRequirements, supportedKind types.SupportedKind, extensions []string) (types.PaymentRequirements, error)
UptoEvmScheme#
import uptoserver "github.com/okx/payments/go/x402/mechanisms/evm/upto/server"
scheme := uptoserver.NewUptoEvmScheme()
scheme.Scheme() // "upto"
upto 是 Permit2-only 的 cap-and-override 模式:
PaymentRequirements.Amount是上限(cap),不是实际扣款EnhancePaymentRequirements始终写Extra.assetTransferMethod = "permit2"- 自动从
supportedKind.Extra.facilitatorAddress注入到 challenge,让买家把 facilitator 地址钉进witness.facilitator(合约层强制msg.sender == witness.facilitator) - 业务 handler 通过
SetSettlementOverrides(net/http / Gin / Echo 都有)决定实际扣款,中间件读出后用 overrides 调SettlePayment,余额按实际用量扣
func NewUptoEvmScheme() *UptoEvmScheme
func (s *UptoEvmScheme) Scheme() string // "upto"
func (s *UptoEvmScheme) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error)
func (s *UptoEvmScheme) EnhancePaymentRequirements(ctx context.Context, requirements types.PaymentRequirements, supportedKind types.SupportedKind, extensionKeys []string) (types.PaymentRequirements, error)
func (s *UptoEvmScheme) RegisterMoneyParser(parser x402.MoneyParser) *UptoEvmScheme
// ... ConvertToTokenAmount / ConvertFromTokenAmount / GetDisplayAmount / GetSupportedNetworks / ValidatePaymentRequirements 同 exact
// 服务端结构 / 跨字段校验(在 payload 转发给 facilitator 前运行):
func ValidateUptoPayload(payload types.PaymentPayload, requirements types.PaymentRequirements) error
// Extra key 常量
const AssetTransferMethodKey = "assetTransferMethod"
const ExtraFacilitatorAddressKey = /* re-export from upto/client */
ValidateUptoPayload 依次校验:scheme=="upto"、network 一致、payload 结构是 upto Permit2(以 witness.facilitator 为判别)、spender 是 X402UptoPermit2ProxyAddress、witness to==PayTo、witness facilitator==Extra.facilitatorAddress、token==Asset、permitted.amount==Amount(签名 cap)、deadline 足够、签名能 recover 到 from。这是 facilitator-free 的结构校验;链上模拟 + 签名验证仍在 facilitator 侧。
自托管 facilitator scheme(exact/facilitator、upto/facilitator)#
若不用 OKX 托管 facilitator(OKXFacilitatorClient),而是自己跑 facilitator(持签名器、自行 verify + 上链),用这两个包。它们实现 x402.SchemeNetworkFacilitator,构造时注入 evm.FacilitatorEvmSigner(提供签名地址 + RPC 原语)+ 可选 config。
import (
exactfac "github.com/okx/payments/go/x402/mechanisms/evm/exact/facilitator"
uptofac "github.com/okx/payments/go/x402/mechanisms/evm/upto/facilitator"
)
// exact:SimulateInSettle 是普通 bool(零值 false,不重跑)。
type exactfac.ExactEvmSchemeConfig struct {
DeployERC4337WithEIP6492 bool // 自动部署 ERC-4337 智能钱包(EIP-6492)
SimulateInSettle bool // settle 时重跑 eth_call 预演(verify 始终预演)
}
func exactfac.NewExactEvmScheme(signer evm.FacilitatorEvmSigner, config *ExactEvmSchemeConfig) *ExactEvmScheme
// upto:SimulateInSettle 是 *bool —— nil ⇒ 默认 true。
type uptofac.UptoEvmSchemeConfig struct {
SimulateInSettle *bool // settle 时重跑 eth_call 预演;nil ⇒ true,传 *false 关闭
}
func uptofac.NewUptoEvmScheme(signer evm.FacilitatorEvmSigner, config *UptoEvmSchemeConfig) *UptoEvmScheme
// config = nil 时全用默认(SimulateInSettle 默认 true)。
// 底层 Permit2 结算层 config(被 UptoEvmScheme 透传):
type uptofac.UptoPermit2FacilitatorConfig struct {
SimulateInSettle *bool // 同上,nil ⇒ true
}
两个 facilitator 的
SimulateInSettle语义不同:exact是普通bool(零值 false,不重跑);upto是*bool(nil ⇒ 默认 true)——因为 upto 实际扣款可能严格 < cap,settle 前重跑模拟更稳妥。
EVM Payload 类型(mechanisms/evm)#
type AssetTransferMethod string
const (
AssetTransferMethodEIP3009 AssetTransferMethod = "eip3009"
AssetTransferMethodPermit2 AssetTransferMethod = "permit2"
)
// ---- EIP-3009 (default) ----
type ExactEIP3009Authorization struct {
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
ValidAfter string `json:"validAfter"`
ValidBefore string `json:"validBefore"`
Nonce string `json:"nonce"`
}
type ExactEIP3009Payload struct {
Signature string `json:"signature,omitempty"`
Authorization ExactEIP3009Authorization `json:"authorization"`
}
func PayloadFromMap(data map[string]interface{}) (*ExactEIP3009Payload, error)
func (p *ExactEIP3009Payload) ToMap() map[string]interface{}
// v1/v2 在 Go 里是同一结构的别名:
type ExactEvmPayloadV1 = ExactEIP3009Payload
type ExactEvmPayloadV2 = ExactEIP3009Payload
// ---- Exact + Permit2 ----
type Permit2TokenPermissions struct {
Token string `json:"token"`
Amount string `json:"amount"`
}
type Permit2Witness struct {
To string `json:"to"`
ValidAfter string `json:"validAfter"`
}
func (w Permit2Witness) WitnessTypeString() string
type Permit2Authorization struct {
From string `json:"from"`
Permitted Permit2TokenPermissions `json:"permitted"`
Spender string `json:"spender"`
Nonce string `json:"nonce"`
Deadline string `json:"deadline"`
Witness Permit2Witness `json:"witness"`
}
func (a Permit2Authorization) WitnessTypeString() string
type ExactPermit2Payload struct {
Signature string `json:"signature"`
Permit2Authorization Permit2Authorization `json:"permit2Authorization"`
}
func Permit2PayloadFromMap(data map[string]interface{}) (*ExactPermit2Payload, error)
func (p *ExactPermit2Payload) ToMap() map[string]interface{}
// ---- Upto + Permit2 (cap mode) ----
// upto witness 多了 facilitator,让 upto proxy 链上强制 msg.sender == witness.facilitator。
type UptoPermit2Witness struct {
To string `json:"to"`
Facilitator string `json:"facilitator"`
ValidAfter string `json:"validAfter"`
}
func (w UptoPermit2Witness) WitnessTypeString() string
type UptoPermit2Authorization struct {
From string `json:"from"`
Permitted Permit2TokenPermissions `json:"permitted"` // permitted.amount 是 cap
Spender string `json:"spender"`
Nonce string `json:"nonce"`
Deadline string `json:"deadline"`
Witness UptoPermit2Witness `json:"witness"`
}
func (a UptoPermit2Authorization) WitnessTypeString() string
type UptoPermit2Payload struct {
Signature string `json:"signature"`
Permit2Authorization UptoPermit2Authorization `json:"permit2Authorization"`
}
func UptoPermit2PayloadFromMap(data map[string]interface{}) (*UptoPermit2Payload, error)
func (p *UptoPermit2Payload) ToMap() map[string]interface{}
// payload 形状判别:
func IsEIP3009Payload(data map[string]interface{}) bool
func IsPermit2Payload(data map[string]interface{}) bool
func IsUptoPermit2Payload(data map[string]interface{}) bool
payload 用
map[string]interface{}表示,通过IsEIP3009Payload/IsPermit2Payload/IsUptoPermit2Payload三个判别函数区分 EIP3009 / Permit2 / upto Permit2。
Permit2 / Upto 常量(mechanisms/evm)#
// Permit2 是 CREATE2-vanity 部署,每条 EVM 链地址相同。
const PERMIT2Address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
const MULTICALL3Address = "0xcA11bde05977b3631167028862bE2a173976CA11"
// x402 部署的 Permit2 proxy 合约(vanity 地址)。
const X402ExactPermit2ProxyAddress = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001"
const X402UptoPermit2ProxyAddress = "0x4020e7393B728A3939659E5732F87fdd8e680002"
// Permit2 witness typehash 字符串——字段顺序 ABI-significant。
const Permit2ExactWitnessTypeString = "Witness(address to,uint256 validAfter)"
const Permit2UptoWitnessTypeString = "Witness(address to,address facilitator,uint256 validAfter)"
// scheme 名 / 默认参数
const SchemeExact = "exact"
const SchemeUpto = "upto"
const SchemeAggrDeferred = "aggr_deferred"
const DefaultDecimals = 6
const DefaultValidityPeriod = 3600 // 秒
资产 / 链配置(mechanisms/evm)#
资产信息用 AssetInfo + GetAssetInfo,链信息在 NetworkConfig + GetNetworkConfig,并预置一个 NetworkConfigs map。
type AssetInfo struct {
Address string
Name string // EIP-712 domain name(USD₮0 用 U+20AE)
Version string
Decimals int
AssetTransferMethod AssetTransferMethod // 强制 "permit2"
SupportsEip2612 bool
}
func GetAssetInfo(network string, assetSymbolOrAddress string) (*AssetInfo, error)
type NetworkConfig struct {
ChainID *big.Int
DefaultAsset AssetInfo
}
func GetNetworkConfig(network string) (*NetworkConfig, error)
func GetEvmChainId(network string) (*big.Int, error)
// 预置 chain ID(含 X Layer 主网 eip155:196 与测试网 eip155:1952):
var ChainIDXLayer = big.NewInt(196)
var ChainIDXLayerTestnet = big.NewInt(1952)
// 以及 ChainIDBase / ChainIDBaseSepolia / ChainIDStable / ChainIDMonad / ... 见 NetworkConfigs
错误类型#
x402 包的错误是一组具体错误类型 + 字符串错误码常量。
// 通用支付错误
type PaymentError struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
func NewPaymentError(code, message string, details map[string]interface{}) *PaymentError
func (e *PaymentError) Error() string
// 验证失败
type VerifyError struct {
InvalidReason string
Payer string
InvalidMessage string
}
func NewVerifyError(reason, payer, message string) *VerifyError
// 结算失败
type SettleError struct {
ErrorReason string
Payer string
Network Network
Transaction string
ErrorMessage string
}
func NewSettleError(reason, payer string, network Network, transaction, message string) *SettleError
// facilitator 返回了畸形成功体
type FacilitatorResponseError struct { /* 未导出字段 */ } // 在 http 包
func (e *FacilitatorResponseError) Error() string
func (e *FacilitatorResponseError) Unwrap() error
// 路由配置校验错误(HTTPServer.Initialize 返回)
type RouteConfigurationError struct { Errors []RouteValidationError }
type RouteValidationError struct {
RoutePattern string
Scheme string
Network x402.Network
Reason string // "missing_scheme" | "missing_facilitator"
Message string
}
错误码常量(x402 包):
const (
ErrCodeInvalidPayment = "invalid_payment"
ErrCodePaymentRequired = "payment_required"
ErrCodeInsufficientFunds = "insufficient_funds"
ErrCodeNetworkMismatch = "network_mismatch"
ErrCodeSchemeMismatch = "scheme_mismatch"
ErrCodeSignatureInvalid = "signature_invalid"
ErrCodePaymentExpired = "payment_expired"
ErrCodeSettlementFailed = "settlement_failed"
ErrCodeUnsupportedScheme = "unsupported_scheme"
ErrCodeUnsupportedNetwork = "unsupported_network"
)
工具函数(x402 包)#
x402 包暴露的工具函数较少(部分 find 辅助是未导出的泛型函数):
func DeepEqual(a, b interface{}) bool
func IsWildcardNetwork(network Network) bool
func MatchesNetwork(pattern Network, network Network) bool
// payload.resource.url 与请求 URL 的匹配(可选 host 白名单容忍反代改写)。
func ResourceMatches(payloadURL, requestURL string, acceptedDomains []string) bool
没有导出的 base64 编解码函数(base64 编解码是 HTTP 层内部细节)。
findSchemesByNetwork/findByNetworkAndScheme是未导出的泛型辅助,不属于公开 API。
Schema 验证(x402 包)#
x402 包提供包级校验函数 ValidatePaymentRequirements / ValidatePaymentPayload,且 payload 校验是版本感知的(同时处理 v1/v2);没有针对 PaymentRequired 的导出校验函数。
func ValidatePaymentRequirements(r PaymentRequirements) error
func ValidatePaymentPayload(p PaymentPayload) error // 版本感知:v1/v2 通吃
Go SDK 参考(适用于 charge、session)#
本节覆盖 MPP(
charge/session)的卖方实现。几个关键设计点:
- module path:按 module path 引用,下面用 Go module / package 表 组织。
- challenge 生成 + 校验收敛进高层协调器
server.Mpp(Charge/SessionChallenge/VerifyCredential/VerifySession),配合 framework middleware。- builder 风格:charge 用链式
WithXxx,session 用一个 config struct(evm.EVMSessionMethodConfig)。- 泛型 store:
store.Store[T]/store.FileStore[T]/store.MemoryStore[T]是 Go 泛型,channel 状态用store.ChannelState实例化。ResourceURL新字段(本分支新增):per-endpoint 营收聚合标签,仅 Charge 模式,透传到challenge.request交给 SA。
Go module / package#
| Go module | package(import path) | 描述 |
|---|---|---|
github.com/okx/payments/go/mpp | .../mpp/server | 高层协调器 server.Mpp:Charge / SessionChallenge / VerifyCredential / VerifySession、EVMConfig、ChargeRouteConfig / SessionRouteConfig、ParseDollarAmount |
.../mpp/evm | EVM charge / session method:EVMChargeMethod(builder)、EVMSessionMethod + EVMSessionMethodConfig、EIP-712 签名、Signer / PrivateKeySigner、Split / SessionSplit、EVMMethodDetails(含 ResourceURL)、常量 | |
.../mpp/protocol | 协议层:PaymentChallenge / PaymentCredential / Receipt、ChargeVerifier / SessionVerifier interface、challenge/credential codec、HMAC ComputeChallengeID、VerificationError | |
.../mpp/saclient | SA-API client:SAClient interface、默认实现 OKXSAClient、测试用 MockSAClient、请求/响应类型、SAErrorCode | |
.../mpp/store | 泛型 store:Store[T]、MemoryStore[T]、FileStore[T]、ChannelState、DeductFromChannel | |
.../mpp/errors | 稳定错误码:MppError、MppErrorCode(含新增 InvalidSplit)、RFC 9457 PaymentErrorDetails | |
.../mpp/http/nethttp .../mpp/http/gin | drop-in middleware:ChargeMiddleware / SessionMiddleware / GetReceipt | |
.../mpp/adapters | 双协议路由用的 MPP adapter:MppAdapter + MppRouteConfig | |
github.com/okx/payments/go/paymentrouter | .../paymentrouter | 双协议(MPP + x402)路由核心:ProtocolAdapter interface、RouteConfig、Config |
.../paymentrouter/nethttp | net/http drop-in PaymentGate:New(...).For(cfg)(handler) | |
github.com/okx/payments/go/x402 | .../x402/adapters | x402 协议 adapter:X402Adapter(包进 paymentrouter) |
各 package 直接按上面的 import path 引用。
常量#
// X Layer mainnet chain ID。
const evm.XLayerChainID uint64 = 196
// X Layer mainnet escrow 合约地址(不传 EscrowContract 时的兜底)。
const evm.DefaultEscrowContract = "0x5E550002e64FaF79B41D89fE8439eEb1be66CE3b"
// EIP-712 voucher 签名 domain(不显式覆盖时的默认值)。
const evm.DefaultDomainName = "EVM Payment Channel"
const evm.DefaultDomainVersion = "1"
// challenge 默认有效期(分钟)。
const evm.DefaultExpiresMinutes = 5
// method / intent 名。
const evm.MethodNameEVM = "evm"
// session action 常量。
const evm.ActionOpen, evm.ActionTopUp, evm.ActionVoucher, evm.ActionClose, evm.ActionSettle = "open", "topUp", "voucher", "close", "settle"
// session receipt status 常量。
const evm.StatusOpen, evm.StatusClosed = "open", "closed"
// 最大分账路数。
const evm.MaxSplits = 10
核心类型(mpp/evm、mpp/saclient)#
SAResponse#
SA-API 统一响应包装;client 自动解包 data。
type saclient.SAResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
SAResponse用json.RawMessage延迟解码,由各端点方法解到具体类型。
EVMMethodDetails / Split#
Charge challenge 的 methodDetails(base64url 编进 request)。
type evm.EVMMethodDetails struct {
ChainID *uint64 `json:"chainId,omitempty"`
FeePayer *bool `json:"feePayer,omitempty"` // server pays gas(transaction 模式)
Memo *string `json:"memo,omitempty"`
Splits []Split `json:"splits,omitempty"`
// ResourceURL —— 本 charge 保护的 endpoint URL(如 "https://api.shop.com/photo")。
// 原样透传到 challenge.request 交给 SA,让商户按 URL 聚合交易量 / 营收。
// 商户持有;SDK 不自动填。仅 Charge 模式;Session 模式不支持(一个 session
// 可能横跨多个 URL)。
ResourceURL *string `json:"resourceUrl,omitempty"`
}
func (d *EVMMethodDetails) IsFeePayer() bool
func evm.ParseEVMMethodDetails(methodDetails json.RawMessage) (*EVMMethodDetails, error)
// Constraints: sum(splits[].amount) < totalAmount;primary recipient 必须留正余额;splits ≤ MaxSplits。
type evm.Split struct {
Amount string `json:"amount"` // base-units integer string
Memo *string `json:"memo,omitempty"`
Recipient string `json:"recipient"` // 40-hex 地址
}
charge 的分账类型是
evm.Split;ResourceURL是本分支新增字段。
EVMSessionMethodDetails / SessionSplit#
type evm.EVMSessionMethodDetails struct {
EscrowContract string `json:"escrowContract"`
ChannelID *string `json:"channelId,omitempty"`
MinVoucherDelta *string `json:"minVoucherDelta,omitempty"`
ChainID *uint64 `json:"chainId,omitempty"`
FeePayer *bool `json:"feePayer,omitempty"`
Splits []SessionSplit `json:"splits,omitempty"`
}
func (d *EVMSessionMethodDetails) IsFeePayer() bool
func evm.ParseEVMSessionMethodDetails(methodDetails json.RawMessage) (*EVMSessionMethodDetails, error)
// Constraints: bps in [1, 9999];sum(splits[].bps) < 10000。
type evm.SessionSplit struct {
Recipient string `json:"recipient"`
Bps uint32 `json:"bps"`
Memo *string `json:"memo,omitempty"`
}
Eip3009Authorization#
Charge payload.authorization 形状(client 签 EIP-3009 后填进去)。分账时 Splits 各路独立一份 EIP-3009。
type saclient.Eip3009Authorization struct {
Type string `json:"type"` // always "eip-3009"
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
ValidAfter string `json:"validAfter"`
ValidBefore string `json:"validBefore"`
Nonce string `json:"nonce"`
Signature string `json:"signature,omitempty"`
Splits []Eip3009Authorization `json:"splits,omitempty"`
}
分账直接用
Eip3009Authorization自嵌套Splits,每路独立一份 EIP-3009 授权。
ChargeReceipt / SessionReceipt / SessionStatus#
type saclient.ChargeReceipt struct {
Method string `json:"method"`
Reference string `json:"reference"` // on-chain tx hash
Status string `json:"status"`
Timestamp string `json:"timestamp"`
ChainID uint64 `json:"chainId"`
ChallengeID string `json:"challengeId"`
ExternalID string `json:"externalId"`
}
type saclient.SessionReceipt struct {
Method string `json:"method"`
Intent string `json:"intent"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
ChannelID string `json:"channelId"`
ChainID uint64 `json:"chainId"`
Reference string `json:"reference"`
Deposit string `json:"deposit"` // 当前 on-chain known deposit
}
// GET /session/status 的响应。
type saclient.SessionStatus struct {
ChannelID string `json:"channelId"`
Payer string `json:"payer"`
Payee string `json:"payee"`
Token string `json:"token"`
Deposit string `json:"deposit"`
CumulativeAmount string `json:"cumulativeAmount"`
SettledOnChain string `json:"settledOnChain"`
RemainingBalance string `json:"remainingBalance"`
SessionStatus string `json:"sessionStatus"` // OPEN, CLOSING, CLOSED
}
evm package 另有一份面向 receipt header 的
evm.SessionReceipt(ChannelID/CumulativeAmount/EscrowContract/Status/Reference+ToBaseReceipt),用于把 settlement 编进protocol.Receipt;上面的saclient.SessionReceipt才是 SA-API 端点返回体。
Session 请求 payload(SA-API)#
SDK 主动调 /session/settle / /session/close 的 body(扁平,不带 challenge wrapper);统一包在泛型信封 saclient.CredentialRequest[P] 里。
type saclient.CredentialRequest[P any] struct {
Challenge *protocol.ChallengeEcho `json:"challenge,omitempty"`
Payload P `json:"payload"`
Source string `json:"source,omitempty"`
}
type saclient.SessionSettlePayload struct {
Action string `json:"action,omitempty"` // "settle"
ChannelID string `json:"channelId"`
CumulativeAmount string `json:"cumulativeAmount"`
VoucherSignature string `json:"voucherSignature"` // payer 65-byte r‖s‖v hex
PayeeSignature string `json:"payeeSignature"` // payee 65-byte r‖s‖v hex
Nonce string `json:"nonce"`
Deadline string `json:"deadline"`
}
type saclient.SessionClosePayload struct {
Action string `json:"action,omitempty"` // "close"
ChannelID string `json:"channelId"`
CumulativeAmount string `json:"cumulativeAmount"`
VoucherSignature string `json:"voucherSignature"` // waiver 分支为 ""
PayeeSignature string `json:"payeeSignature"`
Nonce string `json:"nonce"`
Deadline string `json:"deadline"`
}
// 类型别名:各端点用具体 payload 实例化信封。
type saclient.SessionSettleRequest = CredentialRequest[SessionSettlePayload]
type saclient.SessionCloseRequest = CredentialRequest[SessionClosePayload]
type saclient.SessionOpenRequest = CredentialRequest[SessionOpenPayload]
type saclient.SessionTopUpRequest = CredentialRequest[SessionTopUpPayload]
type saclient.ChargeSettleRequest = CredentialRequest[ChargeTransactionPayload]
type saclient.ChargeVerifyHashRequest = CredentialRequest[ChargeHashPayload]
settle / close 的 body 分别是
SessionSettlePayload/SessionClosePayload,再用泛型CredentialRequest[P]包成 request。
SAClient interface#
可插拔 SA-API client 接口,默认实现 OKXSAClient,测试用 MockSAClient。
type saclient.SAClient interface {
// Charge(client-facing —— 透传 credential)
Settle(ctx context.Context, req *ChargeSettleRequest) (*ChargeReceipt, error)
VerifyHash(ctx context.Context, req *ChargeVerifyHashRequest) (*ChargeReceipt, error)
// Session(client-facing —— 透传 credential)
SessionOpen(ctx context.Context, req *SessionOpenRequest) (*SessionReceipt, error)
SessionTopUp(ctx context.Context, req *SessionTopUpRequest) (*SessionReceipt, error)
// Session(merchant-facing —— server 构造 request)
SessionSettle(ctx context.Context, req *SessionSettleRequest) (*SessionReceipt, error)
SessionClose(ctx context.Context, req *SessionCloseRequest) (*SessionReceipt, error)
// Session(read-only)
SessionStatus(ctx context.Context, channelID string) (*SessionStatus, error)
}
charge 的 settle / verify-hash 对应
Settle/VerifyHash。Go 没有/session/voucher端点 —— voucher 在 SDK 本地处理(EVMSessionMethod.VerifySession的 voucher 分支)。
OKX SA-API 客户端(saclient.OKXSAClient)#
type saclient.OKXSAClient struct { /* private */ }
func saclient.NewOKXSAClient(baseURL, apiKey, secretKey, passphrase string, opts ...Option) *OKXSAClient
// functional option。
type saclient.Option func(*OKXSAClient)
func saclient.WithHTTPClient(c *http.Client) Option // 替换默认 http.Client
实现 SAClient;每个请求自动加 OKX API key + HMAC-SHA256 签名 + passphrase 认证头。
构造统一走单个
NewOKXSAClient(baseURL, ...):baseURL显式必传(生产传https://web3.okx.com,sandbox/staging 传对应 URL)。
端点#
SAClient 方法 | OKX 路径 |
|---|---|
Settle() | POST /api/v6/pay/mpp/charge/settle |
VerifyHash() | POST /api/v6/pay/mpp/charge/verifyHash |
SessionOpen() | POST /api/v6/pay/mpp/session/open |
SessionTopUp() | POST /api/v6/pay/mpp/session/topUp |
SessionSettle() | POST /api/v6/pay/mpp/session/settle |
SessionClose() | POST /api/v6/pay/mpp/session/close |
SessionStatus(channelID) | GET /api/v6/pay/mpp/session/status?channelId=... |
OKX 响应包在 {"code": 0, "data": {...}, "msg": ""},client 自动解包。
测试用 MockSAClient:接受所有 credential、不做链上校验、返合成 receipt。
func saclient.NewMockSAClient(chainID uint64) *MockSAClient
Charge — evm.EVMChargeMethod#
实现 protocol.ChargeVerifier,把 credential 透传给 SA-API。用链式 builder 配置。
type evm.EVMChargeMethod struct { /* private */ }
h
func evm.NewEVMChargeMethod() *EVMChargeMethod
func (m *EVMChargeMethod) WithChainID(chainID uint64) *EVMChargeMethod
func (m *EVMChargeMethod) WithRecipient(recipient string) *EVMChargeMethod
func (m *EVMChargeMethod) WithSAClient(c saclient.SAClient) *EVMChargeMethod // nil → local-only 模式
func (m *EVMChargeMethod) WithFeePayer(feePayer bool) *EVMChargeMethod // true → transaction 模式
// protocol.ChargeVerifier 实现:
func (m *EVMChargeMethod) Method() string
func (m *EVMChargeMethod) ChallengeMethodDetails() *EVMMethodDetails
func (m *EVMChargeMethod) PrepareRequest(request protocol.ChargeRequest, _ *protocol.PaymentCredential) protocol.ChargeRequest
func (m *EVMChargeMethod) Verify(ctx context.Context, cred *protocol.PaymentCredential, request *protocol.ChargeRequest) (*protocol.Receipt, error)
payload.type 路由(在 Verify 内部):
"transaction"→SAClient.Settle(SA-API 链上 broadcasttransferWithAuthorization)"hash"→SAClient.VerifyHash(client 已自行 broadcast,SA-API 验证 tx hash)
Splits 透传 payload.authorization.splits[],SA-API 拥有 split 校验权。
EVMChargeMethod只管「校验」(ChargeVerifier),challenge 生成交给高层server.Mpp.Charge(...),配置走server.EVMConfig+server.ChargeRouteConfig(见下)。
构造 EVMSessionMethod#
type evm.EVMSessionMethod struct { /* private */ }
func evm.NewEVMSessionMethod(cfg EVMSessionMethodConfig) (*EVMSessionMethod, error)
type evm.EVMSessionMethodConfig struct {
// 必填。
Recipient string // payee 钱包地址
SAClient saclient.SAClient // SA-API client
// 可选(零值 = 默认)。
ChainID uint64 // 默认 196 (X Layer)
EscrowContract string // 默认 DefaultEscrowContract
Signer Signer // payee 签名器;nil → settle/close 关闭
Store store.Store[store.ChannelState] // 默认 in-memory store
PerRequestCost *big.Int // 每次请求扣费额
MinVoucherDelta *big.Int // voucher 最小递增量
NonceProvider NonceProvider // 默认 UuidNonceProvider
Deadline *big.Int // 默认 U256 MAX(永不过期)
DomainName string // 默认 "EVM Payment Channel"
DomainVersion string // 默认 "1"
FeePayer bool // payee 是否代付 gas
}
配置收敛成单个
EVMSessionMethodConfigstruct,由NewEVMSessionMethod一次性校验必填字段并返回error。
Session — evm.EVMSessionMethod#
实现 protocol.SessionVerifier。维护本地 channel state、voucher 本地验签 + 累计扣费、商户主动 settle/close。
protocol.SessionVerifier 实现#
func (m *EVMSessionMethod) Method() string
func (m *EVMSessionMethod) ChallengeMethodDetails() json.RawMessage // 无 escrow 配置时返 nil
func (m *EVMSessionMethod) VerifySession(ctx context.Context, cred *protocol.PaymentCredential, request *protocol.SessionRequest) (*protocol.Receipt, error)
func (m *EVMSessionMethod) Respond(cred *protocol.PaymentCredential, receipt *protocol.Receipt) any
Respond 对 open/topUp/close 返管理响应、对 voucher(serve-resource)返 nil。
业务方法#
// 底层 channel store。
func (m *EVMSessionMethod) ChannelStore() store.Store[store.ChannelState]
// 原子扣费:available = highestVoucher - spent;不足返 insufficient-balance 错误。
func (m *EVMSessionMethod) DeductFromSession(ctx context.Context, channelID string, amount *big.Int) (*store.ChannelState, error)
// 取本地 highest voucher → 签 SettleAuthorization → 调 /session/settle。
func (m *EVMSessionMethod) SettleWithAuthorization(ctx context.Context, channelID string) (*saclient.SessionReceipt, error)
// 签 CloseAuthorization → 调 /session/close → 成功后从 store 移除。
func (m *EVMSessionMethod) CloseWithAuthorization(ctx context.Context, channelID string) (*saclient.SessionReceipt, error)
voucher 验签内联进
VerifySession的 voucher 分支,仅暴露DeductFromSession;CloseWithAuthorization只收channelID(waiver 与否由 store 里有无 voucher 决定)。channel 状态查询走saclient.SAClient.SessionStatus,不在EVMSessionMethod上单列。
Session action 路由(VerifySession 内部,按 payload.action)#
action | 行为 |
|---|---|
"open" | payee 校验 → SA SessionOpen → 写本地 store |
"voucher" | 本地验签 + 升 highest voucher → 扣费(DeductFromSession) |
"topUp" | SA SessionTopUp → 累加本地 deposit |
"close" | 取 payer 提供的 voucher → 本地 close 流程 |
session payload 类型(credential 内部,按 action 分):evm.OpenPayload / evm.VoucherPayload / evm.TopUpPayload / evm.ClosePayload,各有 Validate() error。
Session — store.Store[T] interface#
Go 的 store 是泛型 key-value 接口。channel 状态用 store.ChannelState 实例化。
type store.Store[T any] interface {
Get(ctx context.Context, key string) (*T, error) // 不存在返 (nil, nil)
Put(ctx context.Context, key string, value *T) error
Delete(ctx context.Context, key string) error
}
// 原子扣费 helper:
// 读 channel → 检查约束 → 更新 Spent → 写回。
func store.DeductFromChannel(ctx context.Context, s Store[ChannelState], channelID string, amount *big.Int) (*ChannelState, error)
ChannelState#
type store.ChannelState struct {
ChannelID string `json:"channelId"`
ChainID uint64 `json:"chainId"`
EscrowContract string `json:"escrowContract"`
Payer string `json:"payer"`
Payee string `json:"payee"`
Token string `json:"token"`
AuthorizedSigner string `json:"authorizedSigner"` // open 时 address(0) → payer
Deposit *big.Int `json:"deposit"`
HighestVoucherAmount *big.Int `json:"highestVoucherAmount"`
HighestVoucherSignature []byte `json:"highestVoucherSignature,omitempty"`
MinVoucherDelta *big.Int `json:"minVoucherDelta,omitempty"` // nil 关闭节流
Spent *big.Int `json:"spent"` // invariant: spent ≤ highestVoucherAmount
Units uint64 `json:"units"` // deduct 次数
Finalized bool `json:"finalized"`
CloseRequestedAt uint64 `json:"closeRequestedAt"`
CreatedAt string `json:"createdAt"`
}
// 链上视图(SA 拉回的 on-chain channel)。
type store.OnChainChannel struct {
Payer string `json:"payer"`
Payee string `json:"payee"`
Token string `json:"token"`
AuthorizedSigner string `json:"authorizedSigner"`
Deposit *big.Int `json:"deposit"`
Settled *big.Int `json:"settled"`
CloseRequestedAt uint64 `json:"closeRequestedAt"`
Finalized bool `json:"finalized"`
}
channel 状态用
store.ChannelState表示;金额字段用*big.Int,签名等可选字节字段用[]byte(nil 表示缺省)。
实现 — MemoryStore[T] / FileStore[T]#
// 默认:进程内 map,值经 JSON round-trip 深拷贝防止 caller 篡改。
type store.MemoryStore[T any] struct { /* private */ }
func store.NewMemoryStore[T any]() *MemoryStore[T]
// 文件持久化:每个 key 一个 JSON 文件,per-key mutex 保证同 key 并发安全。
type store.FileStore[T any] struct { /* private */ }
func store.NewFileStore[T any](dir string) (*FileStore[T], error) // 自动创建目录
channel 场景固定用 store.ChannelState 实例化,例如:
chStore, err := store.NewFileStore[store.ChannelState]("/var/lib/mpp/channels")
MemoryStore 的两个 caveat:
- 重启即丢:进程重启 / crash 丢失所有 channel state。长期 channel / 多实例 HA / 热重载场景请换
FileStore,或自实现持久化 store(SQLite / Redis / Postgres / ...)实现Store[ChannelState]注入EVMSessionMethodConfig.Store。 - abandoned channel 累积:payer 不调 close 时记录会一直留 —— session lifecycle 通用问题,商户应有 cleanup / TTL 策略。
NonceProvider interface#
type evm.NonceProvider interface {
Allocate(payee common.Address, channelID [32]byte) (*big.Int, error)
}
// 默认实现:UUID v4 → big.Int(128-bit random,stateless,跨多实例 / 重启安全)。
type evm.UuidNonceProvider struct{}
func evm.NewUuidNonceProvider() *UuidNonceProvider
func (p *UuidNonceProvider) Allocate(_ common.Address, _ [32]byte) (*big.Int, error)
合约层 nonce 已用集 key = (payee, channelId, nonce),重复使用以 NonceAlreadyUsed revert。SDK 只负责分配「大概率没用过」的 nonce,不追踪已用集。
EIP-712 签名(mpp/evm)#
Signer interface 与默认实现#
type evm.Signer interface {
Sign(hash []byte) ([]byte, error)
SignTypedData(typedData apitypes.TypedData) ([]byte, error)
Address() common.Address
}
type evm.PrivateKeySigner struct { /* private */ }
func evm.NewPrivateKeySigner(key *ecdsa.PrivateKey) *PrivateKeySigner
func evm.NewPrivateKeySignerFromHex(hexKey string) (*PrivateKeySigner, error) // 带或不带 "0x"
Go 定义自己的
evm.Signerinterface,默认PrivateKeySigner;远程 / KMS 签名器实现这三个方法即可注入。
Voucher 签名 / 验签#
// EIP-712 voucher 结构 1:1 对应合约的 Voucher{ bytes32 channelId; uint128 cumulativeAmount }。
func evm.SignVoucher(
signer Signer,
channelID [32]byte,
cumulativeAmount *big.Int,
escrowContract common.Address,
chainID uint64,
domainName, domainVersion string, // 空 → DefaultDomainName / DefaultDomainVersion
) ([]byte, error) // 65-byte 签名,v 编为 27/28
// 1) len == 65 2) low-s precheck 3) EIP-712 digest 4) ecrecover + 严格地址比较。
func evm.VerifyVoucher(
escrowContract common.Address,
chainID uint64,
channelID [32]byte,
cumulativeAmount *big.Int,
sig []byte,
expectedSigner common.Address,
domainName, domainVersion string,
) bool // v 接受 27/28 或 0/1
func evm.ValidateVoucherSignature(sig []byte) error // 65 字节 + low-s 检查
VerifyVoucher返bool,预校验单独走ValidateVoucherSignature(sig) error。
SettleAuthorization / CloseAuthorization 签名#
type evm.SignedAuthorization struct {
ChannelID [32]byte
CumulativeAmount *big.Int
Nonce *big.Int
Deadline *big.Int
Signature []byte // 65-byte r||s||v
}
// primaryType 选 "SettleAuthorization" 或 "CloseAuthorization"。
func evm.SignAuthorization(
signer Signer,
primaryType string,
channelID [32]byte,
cumulativeAmount *big.Int,
nonce *big.Int,
deadline *big.Int,
escrowContract common.Address,
chainID uint64,
domainName string,
domainVersion string,
) (*SignedAuthorization, error)
// 确定性 channelId = keccak256(abi.encode(payer, payee, token, salt, authorizedSigner, escrowContract, chainID))。
func evm.ComputeChannelID(
payer, payee, token common.Address,
salt [32]byte,
authorizedSigner, escrowContract common.Address,
chainID uint64,
) [32]byte
settle / close 授权签名合并为单个
SignAuthorization,靠primaryType选 settle / close。
解码 challenge.request#
在 protocol package 用一组顶层 codec 函数,从 PaymentChallenge.Request(base64url JSON)解出 typed request。
// 从 challenge 解 request。
func protocol.RequestFromChallenge(c *PaymentChallenge) (json.RawMessage, error)
func protocol.RequestFromChallengeTyped(c *PaymentChallenge, v interface{}) error
// 直接对 base64url 串解码。
func protocol.DeserializeRequest(encoded string) (json.RawMessage, error)
func protocol.DeserializeRequestTyped(encoded string, v interface{}) error
// 反向:把 typed request 编成 base64url 串。
func protocol.SerializeRequest(v interface{}) (string, error)
// 用法:
var req protocol.SessionRequest
if err := protocol.RequestFromChallengeTyped(ch, &req); err != nil { /* ... */ }
protocol.ChargeRequest / protocol.SessionRequest 还带 WithBaseUnits()(decimal → base units)、ValidateMaxAmount(max) 等 helper。
Drop-in middleware(mpp/http/nethttp、mpp/http/gin)#
Go 提供 net/http 与 gin 两套 framework middleware,直接包业务 handler;payment 逻辑收敛在 server.Mpp。
import mpphttp "github.com/okx/payments/go/mpp/http/nethttp"
// net/http:
func nethttp.ChargeMiddleware(m *server.Mpp, cfg server.ChargeRouteConfig) func(http.Handler) http.Handler
func nethttp.SessionMiddleware(m *server.Mpp, cfg server.SessionRouteConfig) func(http.Handler) http.Handler
func nethttp.GetReceipt(r *http.Request) *protocol.Receipt // 校验成功后从 ctx 取 receipt
// gin(同名函数,签名换成 gin):
import mppgin "github.com/okx/payments/go/mpp/http/gin"
func gin.ChargeMiddleware(m *server.Mpp, cfg server.ChargeRouteConfig) gin.HandlerFunc
func gin.SessionMiddleware(m *server.Mpp, cfg server.SessionRouteConfig) gin.HandlerFunc
func gin.GetReceipt(c *gin.Context) *protocol.Receipt
校验失败按 protocol.VerificationError.HTTPStatus() 自动映射 HTTP 状态码(400 payload/format、410 channel-not-found/closed、其余默认 402)。
高层协调器 server.Mpp#
charge 与 session 的 challenge 生成统一到 server.Mpp:注入一份 EVMConfig + charge/session verifier,对外暴露「生成 challenge」与「校验 credential」两组方法。
type server.EVMConfig struct {
ChainID uint64 // 期望链 ID(196 = X Layer)
Recipient string // 期望收款地址(带/不带 0x)
SecretKey string // challenge ID 的 HMAC 密钥;空 = 空 key(确定但不认证)
Realm string // WWW-Authenticate realm;空默认 "mpp"
}
type server.Mpp struct { /* private */ }
func server.NewMpp(cfg EVMConfig, charge protocol.ChargeVerifier, session protocol.SessionVerifier) *Mpp
// 两个 verifier 任一可为 nil(只用一种 intent 时)。
// 生成 challenge(返 WWW-Authenticate header 值):
func (m *Mpp) Charge(ctx context.Context, cfg ChargeRouteConfig) (string, error)
func (m *Mpp) SessionChallenge(ctx context.Context, cfg SessionRouteConfig) (string, error)
// 校验 credential:
func (m *Mpp) VerifyCredential(ctx context.Context, challengeHeader, authHeader string) (*protocol.Receipt, error)
func (m *Mpp) VerifySession(ctx context.Context, challengeHeader, authHeader string) (*protocol.SessionVerifyResult, error)
// 低层变体(自带 request / options):
func (m *Mpp) ChargeWithOptions(ctx context.Context, req protocol.ChargeRequest, opts ChargeOptions) (string, error)
func (m *Mpp) SessionChallengeWithDetails(ctx context.Context, req protocol.SessionRequest, opts SessionChallengeOptions) (string, error)
Route config#
per-route 参数。注意 ResourceURL 是本分支新增(仅 Charge)。
type server.ChargeRouteConfig struct {
Amount string // human-readable decimal,如 "0.01"
Currency string // ERC-20 合约地址
Decimals uint32 // 代币精度(USDC = 6)
Description string
ExternalID string // caller 自定义 reference
Splits []evm.Split // 次级收款方,主收款方拿 total - sum(splits);≤ 10
ResourceURL string // 【新增】本 charge 保护的 endpoint URL,透传给 SA 按 URL 聚合营收;空则不上报。仅 Charge 模式。
}
type server.SessionRouteConfig struct {
Amount string // human-readable decimal,如 "0.001"
Currency string // ERC-20 合约地址
Decimals uint32
Description string
ExternalID string
UnitType string // 计费单位:"request" / "second" / "byte" ...
SuggestedDeposit string // 建议初始 deposit(base units)
}
server package 另有 ParseDollarAmount(amount string, decimals uint32) (string, error):human-readable decimal → 整数 base-units(如 ParseDollarAmount("1.50", 6) → "1500000")。
错误类型#
Go 用三层错误:protocol.VerificationError(verifier 层)、saclient.SAErrorCode(SA-API 业务码)、errors.MppError / MppErrorCode(稳定的字符串码 + RFC 9457 problem details;OKXSAClient 把 SAErrorCode 映射进它)。
type protocol.VerificationError struct {
Message string `json:"message"`
Code ErrorCode `json:"code,omitempty"`
Retryable bool `json:"retryable"`
}
func (e *VerificationError) Error() string
func (e *VerificationError) HTTPStatus() int // 400 / 410 / 402(默认)
func (e *VerificationError) WithRetryable() *VerificationError
// 机器可读错误码(字符串),含 .SpecCode() / .String():
type protocol.ErrorCode string
const (
ErrorCodeExpired, ErrorCodeInvalidAmount, ErrorCodeInvalidRecipient,
ErrorCodeTransactionFailed, ErrorCodeNotFound, ErrorCodeInvalidCredential,
ErrorCodeNetworkError, ErrorCodeChainIdMismatch, ErrorCodeCredentialMismatch,
ErrorCodeChannelNotFound, ErrorCodeChannelClosed, ErrorCodeInsufficientBalance,
ErrorCodeInvalidPayload, ErrorCodeInvalidSignature, ErrorCodeAmountExceedsDeposit,
ErrorCodeDeltaTooSmall ErrorCode = /* ... */
)
// 配套构造器:protocol.ErrSig / ErrAmount / ErrChannelNotFound / ErrInsufficientBalance / ...
// 以及 VerificationErrorXxx(msg) 一组。
SA-API 业务错误码映射(saclient.SAErrorCode)#
type saclient.SAErrorCode int
const (
SACodeSuccess = 0
SACodeInvalidParams = 70000 // 缺必填字段或格式错误
SACodeUnsupportedChain = 70001 // 链不在支持列表
SACodePayerBlocked = 70002 // 付款方在黑名单
SACodeInvalidCredential = 70003 // source 缺失 / feePayer=true 不支持 hash 模式 / txHash 已用
SACodeInvalidSignature = 70004 // 签名验证失败
SACodeSplitSumExceedsTotal = 70005 // 分账总额 ≥ 主金额
SACodeSplitCountExceeded = 70006 // 分账数量 > 10
SACodeTxNotConfirmed = 70007 // 交易未在链上确认
SACodeChannelClosed = 70008 // 链上 channel 已关闭
SACodeChallengeInvalid = 70009 // challenge 不存在或已过期
SACodeChannelNotFound = 70010 // channelId 不存在
SACodeGracePeriodTooShort = 70011 // escrow grace period < 10 分钟,拒绝开通
SACodeAmountExceedsDeposit = 70012 // cumulativeAmount 超 deposit 余额
SACodeVoucherDeltaTooSmall = 70013 // voucher 递增量低于 minVoucherDelta
SACodeChannelClosing = 70014 // channel CLOSING 状态,不收新 voucher
SACodeInternalError = 8000 // API 服务内部错误
)
本地账户余额不足扣费(
available < amount)由 verifier 层的protocol.ErrorCodeInsufficientBalance/ErrInsufficientBalance(...)表达,不在SAErrorCode常量集里(本地扣费由DeductFromSession/store.DeductFromChannel检查,不是 SA-API 业务码)。
errors package —— MppError / MppErrorCode#
saclient.OKXSAClient 把 SA-API 业务码(SAErrorCode)映射成稳定的字符串错误码 errors.MppErrorCode,封进 *errors.MppError 返回。
type errors.MppErrorCode string
type errors.MppError struct {
Code MppErrorCode `json:"code"`
Message string `json:"message"`
Reason string `json:"reason,omitempty"`
}
func (e *MppError) Error() string
func (e *MppError) ToProblemDetails(challengeID string) *PaymentErrorDetails // RFC 9457
MppErrorCode 取值(节选,含本分支新增 InvalidSplit):
| Code | 含义 |
|---|---|
MalformedCredential | credential 畸形 |
InvalidChallenge | challenge 非法 |
InvalidSignature | 签名验证失败 |
InvalidSplit | 【新增】分账非法(总额超主金额 / 路数超 10) |
InsufficientBalance | 余额不足 |
AmountExceedsDeposit | 金额超 deposit |
DeltaTooSmall | voucher 递增量过小 |
ChannelNotFound | channel 不存在 |
ChannelClosed | channel 已关闭 |
SignerMismatch | 签名者不匹配 |
BadRequest | 请求参数错误 |
Internal | 内部错误 |
完整集还含
AmountExceedsMax/InvalidAmount/InvalidConfig/Http/ChainIdMismatch/Json/HexDecode/Base64Decode/UnsupportedPaymentMethod/MissingHeader/InvalidBase64Url/VerificationFailed/PaymentExpired/PaymentRequired/InvalidPayload/Io/InvalidUtf8/SystemTime。
SA-API 业务码 → MppErrorCode 映射(OKXSAClient.mapSAError)#
| SA code | 含义 | 映射 MppErrorCode |
|---|---|---|
| 70000 | 缺必填字段 / 格式错误 | BadRequest |
| 70001 | 链不在支持列表 | Internal |
| 70002 | 付款方在黑名单 | MalformedCredential |
| 70003 | source 缺失 / feePayer+hash 不兼容 / txHash 已用 | MalformedCredential |
| 70004 | 签名验证失败 | InvalidSignature |
| 70005 | 分账总额 ≥ 主金额 | InvalidSplit |
| 70006 | 分账数量 > 10 | InvalidSplit |
| 70007 | 交易未链上确认 | Internal |
| 70008 | channel 已关闭 | ChannelClosed |
| 70009 | challenge 不存在 / 已过期 | InvalidChallenge |
| 70010 | channelId 不存在 | ChannelNotFound |
| 70011 | escrow grace period 不满足 | Internal |
| 70012 | cumulativeAmount 超 deposit 余额 | AmountExceedsDeposit |
| 70013 | voucher 递增量 < minVoucherDelta | DeltaTooSmall |
| 70014 | channel CLOSING 状态 | ChannelClosed |
| 8000 | API 内部错误 | Internal |
默认分支(未列出的码)→
Internal。
双协议路由(paymentrouter + mpp/adapters + x402/adapters)#
让一个 net/http app 同时接 MPP + x402,业务 handler 协议无关。Go 用 adapter pattern + PaymentGate middleware。
Adapter interface#
type paymentrouter.ProtocolAdapter interface {
Name() string // "mpp" | "x402" | 自定义
Priority() int // 越小越先 Detect(MPP < x402)
Detect(r *http.Request) bool // 看请求头是否属于本协议
GetChallenge(ctx context.Context, r *http.Request, cfg any) (http.Header, error) // 生成本协议 402 challenge header
Handle(w http.ResponseWriter, r *http.Request, cfg any) error // 校验 + 写 receipt / 错误
}
GetChallenge直接传cfg any,adapter 内部对自己的 config struct 做 type-assert。中间件用闭包包装,无需额外的注册 hook。
内置 adapter#
import (
mppadapters "github.com/okx/payments/go/mpp/adapters"
x402adapters "github.com/okx/payments/go/x402/adapters"
)
// MppAdapter 接 *server.Mpp(用 server.NewMpp(...) 拼出)。
func mppadapters.NewMppAdapter(mpp *server.Mpp) *MppAdapter
func (a *MppAdapter) Name() string
func (a *MppAdapter) Priority() int
func (a *MppAdapter) Detect(r *http.Request) bool
func (a *MppAdapter) GetChallenge(ctx context.Context, r *http.Request, cfg any) (http.Header, error)
func (a *MppAdapter) Handle(w http.ResponseWriter, r *http.Request, cfg any) error
// X402Adapter 接 *x402http.HTTPServer(routes 可传 nil,由路由 config 懒注册)。
func x402adapters.NewX402Adapter(server *x402http.HTTPServer) *X402Adapter
func (a *X402Adapter) Name() string
func (a *X402Adapter) Priority() int
func (a *X402Adapter) Detect(r *http.Request) bool
func (a *X402Adapter) GetChallenge(ctx context.Context, r *http.Request, cfg any) (http.Header, error)
func (a *X402Adapter) Handle(w http.ResponseWriter, r *http.Request, cfg any) error
NewX402Adapter一步到位,poll deadline / settlement hook 等行为配在传入的*x402http.HTTPServer/ facilitator 客户端上。MppAdapter的 priority 内置固定,不可调。
每 adapter 的类型化路由配置#
// MPP per-route config(MppAdapter 内部 type-assert 到这个类型)。
type mppadapters.MppRouteConfig struct {
Intent string `json:"intent"` // "charge" 或 "session"(空 → "charge")
Amount string `json:"amount"` // base-units integer string
Currency string `json:"currency"`
Decimals uint32 `json:"decimals"`
Description string `json:"description,omitempty"`
ExternalID string `json:"externalId,omitempty"` // charge only
Realm string `json:"realm,omitempty"`
UnitType string `json:"unitType,omitempty"` // session only
SuggestedDeposit string `json:"suggestedDeposit,omitempty"` // session only
}
// x402 per-route config(X402Adapter 内部 type-assert 到这个类型)。
type x402http.RouteConfig struct {
Accepts x402http.PaymentOptions `json:"accepts"`
Resource string `json:"resource,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
CustomPaywallHTML string `json:"customPaywallHtml,omitempty"`
AcceptedDomains []string `json:"acceptedDomains,omitempty"`
// ... 见 x402 参考
}
MppRouteConfig带Decimals/Realm字段;x402 侧直接复用 x402 SDK 的x402http.RouteConfig。
Router 配置#
import (
pr "github.com/okx/payments/go/paymentrouter"
prhttp "github.com/okx/payments/go/paymentrouter/nethttp"
)
// RouteConfig 是 map[adapterName]config —— 没列的 adapter 在本 route 上不启用。
type pr.RouteConfig map[string]any
// 核心 config(用于底层 CompiledRouter / Detect / MergeChallenges)。
type pr.Config struct {
Routes []RouteEntry
Protocols []ProtocolAdapter
OnError func(err error, phase, protocol string)
}
type pr.RouteEntry struct {
Pattern string // "GET /path" 或 "/path"
Config RouteConfig
}
const (
pr.PhaseDetect = "detect"
pr.PhaseChallenge = "challenge"
pr.PhaseHandle = "handle"
)
// 启动期校验:route 引用了未注册的 adapter Name → panic(尽早暴露配置错误)。
func pr.ValidateRouteKeys(routes []RouteEntry, protocols []ProtocolAdapter)
// net/http drop-in gate:
type prhttp.PaymentGate struct { /* private */ }
func prhttp.New(protocols []pr.ProtocolAdapter, opts ...Option) *PaymentGate
func (g *PaymentGate) For(cfg pr.RouteConfig) func(http.Handler) http.Handler // 单 route 的 middleware
type prhttp.Option func(*PaymentGate)
func prhttp.WithOnError(fn func(err error, phase, protocol string)) Option
用
prhttp.New(protocols, ...)建一个PaymentGate,再对每条 route 调.For(cfg)(handler)显式挂到 mux —— 路由匹配交给net/http的ServeMux,不在 router 内部维护 pattern 列表。错误回调签名是func(err error, phase, protocol string),phase取pr.PhaseDetect/PhaseChallenge/PhaseHandle。
端到端拼装示例#
package main
import (
"net/http"
mppadapters "github.com/okx/payments/go/mpp/adapters"
"github.com/okx/payments/go/mpp/server"
pr "github.com/okx/payments/go/paymentrouter"
prhttp "github.com/okx/payments/go/paymentrouter/nethttp"
x402adapters "github.com/okx/payments/go/x402/adapters"
x402http "github.com/okx/payments/go/x402/http"
)
func main() {
// mpp *server.Mpp 与 x402Server *x402http.HTTPServer 按各自参考构造(略)。
var mpp *server.Mpp
var x402Server *x402http.HTTPServer
gate := prhttp.New(
[]pr.ProtocolAdapter{
mppadapters.NewMppAdapter(mpp),
x402adapters.NewX402Adapter(x402Server),
},
prhttp.WithOnError(func(err error, phase, protocol string) {
// 记录 detect / challenge / handle 各阶段的协议错误
}),
)
routeCfg := pr.RouteConfig{
"mpp": mppadapters.MppRouteConfig{
Intent: "charge",
Amount: "100",
Currency: "0x...",
Decimals: 6,
Description: "photo",
},
"x402": x402http.RouteConfig{
Description: "photo",
MimeType: "image/png",
// Accepts: ... 见 x402 参考
},
}
mux := http.NewServeMux()
mux.Handle("GET /photo", gate.For(routeCfg)(photoHandler()))
http.ListenAndServe(":8080", mux)
}
func photoHandler() http.Handler { /* 协议无关的业务 handler */ return nil }
- Go SDK 参考(适用于 exact、exact + permit2、upto、aggr_deferred)模块 / 包核心类型Network / Price / AssetAmountResourceInfoPaymentRequirementsPaymentRequiredPaymentPayloadFacilitator 类型VerifyResponse / SettleResponseSupportedKind / SupportedResponseSettleStatusResponse接口SchemeNetworkServerFacilitatorClientResourceServerExtension / FacilitatorExtension服务端 API(X402ResourceServer)构造与注册方法ResourceConfig / SettlementOverridesOKX Facilitator 客户端(OKXFacilitatorClient)通用 HTTP facilitator 客户端HMAC 认证HTTP 工具请求头编/解码常量路由配置示例(多 scheme 共存)中间件net/httpGinEcho中间件流程HTTPServer 上可挂的 hook / 设置EVM 机制(mechanisms/evm + scheme server 子包)ExactEvmSchemeAggrDeferredEvmSchemeUptoEvmScheme自托管 facilitator scheme(exact/facilitator、upto/facilitator)EVM Payload 类型(mechanisms/evm)Permit2 / Upto 常量(mechanisms/evm)资产 / 链配置(mechanisms/evm)错误类型工具函数(x402 包)Schema 验证(x402 包)Go SDK 参考(适用于 charge、session)Go module / package常量核心类型(mpp/evm、mpp/saclient)SAResponseEVMMethodDetails / SplitEVMSessionMethodDetails / SessionSplitEip3009AuthorizationChargeReceipt / SessionReceipt / SessionStatusSession 请求 payload(SA-API)SAClient interfaceOKX SA-API 客户端(saclient.OKXSAClient)端点Charge — evm.EVMChargeMethod构造 EVMSessionMethodSession — evm.EVMSessionMethodprotocol.SessionVerifier 实现业务方法Session action 路由(VerifySession 内部,按 payload.action)Session — store.Store[T] interfaceChannelState实现 — MemoryStore[T] / FileStore[T]NonceProvider interfaceEIP-712 签名(mpp/evm)Signer interface 与默认实现Voucher 签名 / 验签SettleAuthorization / CloseAuthorization 签名解码 challenge.requestDrop-in middleware(mpp/http/nethttp、mpp/http/gin)高层协调器 server.MppRoute config错误类型SA-API 业务错误码映射(saclient.SAErrorCode)errors package —— MppError / MppErrorCodeSA-API 业务码 → MppErrorCode 映射(OKXSAClient.mapSAError)双协议路由(paymentrouter + mpp/adapters + x402/adapters)Adapter interface内置 adapter每 adapter 的类型化路由配置Router 配置端到端拼装示例
