Rust SDK 参考#

Rust SDK 参考(适用于 exactexact + permit2uptoaggr_deferred#

Crate#

目录 / Lib aliasPublished name (crates.io)描述
x402-coreokxweb3-app-x402-core核心:服务端、facilitator 客户端、类型、HTTP 工具、HMAC 认证
x402-axumokxweb3-app-x402-axumAxum 中间件(Tower Layer/Service)
x402-evmokxweb3-app-x402-evmEVM 机制:exact(EIP-3009 / Permit2)、uptoaggr_deferred

Cargo.toml deps 用 Published nameokxweb3-app-*),源码 useLib alias(短名)—— crate 通过 [lib] name = "..." 显式重命名,编译期对得上。

Rust SDK 目前提供服务端(卖方)和 facilitator 客户端功能。买方侧的支付签名功能正在计划中。


核心类型#

Network / Money / Price#

rust
pub type Network = String;
// CAIP-2 format, e.g., "eip155:196"

pub type Money = String;
// User-friendly amount, e.g., "$0.01", "0.01"

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Price {
    Money(Money),
    Asset(AssetAmount),
}

AssetAmount#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetAmount {
    pub asset: String,      // Token contract address
    pub amount: String,     // Amount in token's smallest unit
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extra: Option<HashMap<String, serde_json::Value>>,
}

ResourceInfo#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceInfo {
    pub url: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
}

PaymentRequirements#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequirements {
    pub scheme: String,              // "exact" | "aggr_deferred" | "upto"
    pub network: Network,            // CAIP-2 identifier
    pub asset: String,               // Token contract address
    pub amount: String,              // Price (or cap, for "upto") in token's smallest unit
    pub pay_to: String,              // Recipient wallet address
    pub max_timeout_seconds: u64,    // Authorization validity window
    #[serde(default)]
    pub extra: HashMap<String, serde_json::Value>,  // Scheme-specific data
}

extra 中常见的字段:

keyscheme含义
assetTransferMethodexact / upto"eip3009"(默认)或 "permit2"
facilitatorAddressuptoupto proxy 强制 witness.facilitator == msg.sender,由 enhance_payment_requirements 自动从 getSupported 注入
name / versionexact(EIP-3009 路径)EIP-712 domain,用于客户端签名

PaymentRequired#

402 响应体。

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequired {
    #[serde(rename = "x402Version")]
    pub x402_version: u32,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub resource: ResourceInfo,
    pub accepts: Vec<PaymentRequirements>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extensions: Option<HashMap<String, serde_json::Value>>,
}

PaymentPayload#

客户端的签名支付。payload 内容随 scheme 不同而不同(EIP-3009 / Permit2 / upto Permit2)。

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentPayload {
    #[serde(rename = "x402Version")]
    pub x402_version: u32,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource: Option<ResourceInfo>,
    pub accepted: PaymentRequirements,
    pub payload: HashMap<String, serde_json::Value>,  // 详见下方 EVM Payload 一节
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extensions: Option<HashMap<String, serde_json::Value>>,
}

Facilitator 类型#

VerifyRequest / VerifyResponse#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyRequest {
    #[serde(rename = "x402Version")]
    pub x402_version: u32,
    pub payment_payload: PaymentPayload,
    pub payment_requirements: PaymentRequirements,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyResponse {
    pub is_valid: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub invalid_reason: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub invalid_message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub payer: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extensions: Option<HashMap<String, serde_json::Value>>,
}

SettleRequest / SettleResponse#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleRequest {
    #[serde(rename = "x402Version")]
    pub x402_version: u32,
    pub payment_payload: PaymentPayload,
    pub payment_requirements: PaymentRequirements,
    /// OKX extension: if true, wait for on-chain confirmation (exact scheme only).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sync_settle: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleResponse {
    pub success: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error_reason: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error_message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub payer: Option<String>,
    pub transaction: String,          // Tx hash (empty for aggr_deferred)
    pub network: Network,
    /// Actual amount settled in atomic units. Present for schemes like
    /// `upto` where settlement amount may differ from the cap.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub amount: Option<String>,
    /// OKX extension: "pending" | "success" | "timeout".
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extensions: Option<HashMap<String, serde_json::Value>>,
}

SupportedKind / SupportedResponse#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupportedKind {
    #[serde(rename = "x402Version")]
    pub x402_version: u32,
    pub scheme: String,
    pub network: Network,
    /// upto: facilitator 地址通过 `extra.facilitatorAddress` 暴露;卖家
    /// SDK 会在 `enhance_payment_requirements` 时把它注入到 challenge 的
    /// extra 里。
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extra: Option<HashMap<String, serde_json::Value>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportedResponse {
    pub kinds: Vec<SupportedKind>,
    pub extensions: Vec<String>,
    /// CAIP family pattern → signer addresses.
    pub signers: HashMap<String, Vec<String>>,
}

SettleStatusResponse#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleStatusResponse {
    pub success: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error_reason: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error_message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub payer: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub transaction: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub network: Option<Network>,
    /// "pending" | "success" | "failed"
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

Traits#

SchemeNetworkServer#

服务端 scheme 实现。exact / aggr_deferred / upto 都实现该 trait。

rust
#[async_trait]
pub trait SchemeNetworkServer: Send + Sync {
    fn scheme(&self) -> &str;

    async fn parse_price(
        &self,
        price: &Price,
        network: &Network,
    ) -> Result<AssetAmount, X402Error>;

    async fn enhance_payment_requirements(
        &self,
        payment_requirements: PaymentRequirements,
        supported_kind: &SupportedKind,
        facilitator_extensions: &[String],
    ) -> Result<PaymentRequirements, X402Error>;
}

FacilitatorClient#

与远程 facilitator 通信的网络边界。

rust
#[async_trait]
pub trait FacilitatorClient: Send + Sync {
    async fn get_supported(&self) -> Result<SupportedResponse, X402Error>;
    async fn verify(&self, request: &VerifyRequest) -> Result<VerifyResponse, X402Error>;
    async fn settle(&self, request: &SettleRequest) -> Result<SettleResponse, X402Error>;
    async fn get_settle_status(&self, tx_hash: &str) -> Result<SettleStatusResponse, X402Error>;
}

ResourceServerExtension#

rust
#[async_trait]
pub trait ResourceServerExtension: Send + Sync {
    fn key(&self) -> &str;

    async fn enrich_payment_required(
        &self,
        payment_required: PaymentRequired,
        context: &PaymentRequiredContext,
    ) -> PaymentRequired { payment_required }

    async fn enrich_verify_extensions(
        &self,
        extensions: HashMap<String, serde_json::Value>,
        payment_payload: &PaymentPayload,
        payment_requirements: &PaymentRequirements,
    ) -> HashMap<String, serde_json::Value> { extensions }

    async fn enrich_settle_extensions(
        &self,
        extensions: HashMap<String, serde_json::Value>,
        payment_payload: &PaymentPayload,
        payment_requirements: &PaymentRequirements,
    ) -> HashMap<String, serde_json::Value> { extensions }
}

pub struct PaymentRequiredContext {
    pub url: String,
    pub method: String,
}

pub struct SettleResultContext {
    pub url: String,
    pub method: String,
    pub payment_payload: PaymentPayload,
    pub payment_requirements: PaymentRequirements,
    pub settle_response: SettleResponse,
}

FacilitatorExtension#

rust
#[async_trait]
pub trait FacilitatorExtension: Send + Sync {
    fn key(&self) -> &str;
    fn supported_networks(&self) -> Vec<Network>;
}

服务端 API (X402ResourceServer)#

构造与注册#

rust
use x402_core::server::X402ResourceServer;
use x402_evm::{ExactEvmScheme, AggrDeferredEvmScheme, UptoEvmScheme};

// register() uses builder pattern (consumes self, returns Self).
// 同一 network 上多个 scheme 共存,路由配置侧按 scheme 名挑。
let mut server = X402ResourceServer::new(facilitator)
    .register("eip155:196", ExactEvmScheme::new())          // exact (EIP-3009 / Permit2)
    .register("eip155:196", AggrDeferredEvmScheme::new())   // aggr_deferred
    .register("eip155:196", UptoEvmScheme::new());          // upto (cap + override)

方法#

rust
use std::collections::HashMap;
use std::time::Duration;

use x402_core::error::X402Error;
use x402_core::facilitator::FacilitatorClient;
use x402_core::http::{PollResult, SettlementOverrides};
use x402_core::server::X402ResourceServer;
use x402_core::types::{
    PaymentPayload, PaymentRequirements, ResourceInfo, SchemeNetworkServer,
    SettleResponse, SupportedResponse, VerifyResponse,
};

impl X402ResourceServer {
    pub fn new(facilitator: impl FacilitatorClient + 'static) -> Self;

    pub fn register(
        self,
        network: &str,
        scheme: impl SchemeNetworkServer + 'static,
    ) -> Self;

    pub async fn initialize(&mut self) -> Result<(), X402Error>;

    pub fn supported(&self) -> Option<&SupportedResponse>;
    pub fn facilitator(&self) -> &dyn FacilitatorClient;

    /// `resource` 当前未被消费,但 API 形状保留以便后续扩展(请求级
    /// `ResourceInfo` 覆盖)。
    pub async fn build_payment_requirements(
        &self,
        scheme: &str,        // "exact" | "aggr_deferred" | "upto"
        network: &str,       // "eip155:196"
        price: &str,         // "$0.01"
        pay_to: &str,        // "0xSeller"
        max_timeout_seconds: u64,
        resource: &ResourceInfo,
        config_extra: Option<&HashMap<String, serde_json::Value>>,
    ) -> Result<PaymentRequirements, X402Error>;

    pub async fn verify_payment(
        &self,
        payment_payload: &PaymentPayload,
        payment_requirements: &PaymentRequirements,
    ) -> Result<VerifyResponse, X402Error>;

    /// `settlement_overrides`: upto scheme 用 — 业务 handler 决定本次实际
    /// 扣款金额(≤ cap),通过响应头 `settlement-overrides` 透传给中间件,
    /// 中间件解析后调用本接口。
    pub async fn settle_payment(
        &self,
        payment_payload: &PaymentPayload,
        payment_requirements: &PaymentRequirements,
        sync_settle: Option<bool>,
        settlement_overrides: Option<&SettlementOverrides>,
    ) -> Result<SettleResponse, X402Error>;

    pub async fn poll_settle_status(
        &self,
        tx_hash: &str,
        poll_interval: Duration,  // DEFAULT_POLL_INTERVAL = 1s
        poll_deadline: Duration,  // DEFAULT_POLL_DEADLINE = 5s
    ) -> PollResult;
}

PollResult / SettlementOverrides#

rust
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PollResult {
    Success,   // Transaction confirmed on-chain
    Failed,    // Transaction failed on-chain
    Timeout,   // Polling deadline exceeded
}

/// 由业务 handler 通过响应头 `settlement-overrides` 设置(推荐用
/// `set_settlement_overrides()` helper);中间件读出并在 settle 时应用。
/// `amount` 支持三种格式:
///   - 原子单位整数:  "1234000"
///   - 占 cap 百分比:  "50%"
///   - 美元字符串:    "$0.05" (与 `price` 同语法)
/// **⚠️ handler 不写响应头 → 按 cap 全额扣款(同 exact 行为)**
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettlementOverrides {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub amount: Option<String>,
}

OKX Facilitator 客户端 (OkxHttpFacilitatorClient)#

rust
use x402_core::http::OkxHttpFacilitatorClient;

// Default URL (https://web3.okx.com).
let client = OkxHttpFacilitatorClient::new(
    "your-api-key",
    "your-secret-key",
    "your-passphrase",
)?;

// 或自定义 base URL(sandbox / staging):
let client = OkxHttpFacilitatorClient::with_url(
    "https://sandbox.okx.com",
    "your-api-key",
    "your-secret-key",
    "your-passphrase",
)?;

new / with_url 均返回 Result<Self, X402Error>。客户端实现 FacilitatorClient trait,所有请求自动带 HMAC-SHA256 认证头。

调用的端点#

方法OKX 路径
get_supported()GET /api/v6/pay/x402/supported
verify(request)POST /api/v6/pay/x402/verify
settle(request)POST /api/v6/pay/x402/settle
get_settle_status(tx_hash)GET /api/v6/pay/x402/settle/status?txHash=...

OKX 响应被包装在 {"code": 0, "data": {...}, "msg": ""} 中,客户端会自动解包。


HMAC 认证#

rust
use x402_core::http::hmac::build_auth_headers;

// 在自定义 HTTP 客户端里给请求加 OKX 认证头。
let headers = build_auth_headers(
    api_key,
    secret_key,
    passphrase,
    "GET",                       // 大写 HTTP method
    "/api/v6/pay/x402/supported",
    "",                          // GET 一般 body 为空
)?;
// Returns Result<HeaderMap, X402Error>
// Headers: OK-ACCESS-KEY, OK-ACCESS-SIGN, OK-ACCESS-TIMESTAMP, OK-ACCESS-PASSPHRASE

签名规则:Base64(HMAC-SHA256(secret_key, timestamp + METHOD + request_path + body))sign_request 是 crate-internal 实现细节,未公开。


HTTP 工具#

请求头编/解码#

rust
use x402_core::http::{
    encode_payment_signature_header,
    decode_payment_signature_header,
    encode_payment_required_header,
    decode_payment_required_header,
    encode_payment_response_header,
    decode_payment_response_header,
};

let encoded = encode_payment_required_header(&payment_required)?;  // → base64 string
let decoded = decode_payment_required_header(&header_value)?;       // → PaymentRequired

常量#

rust
pub const DEFAULT_POLL_INTERVAL:       Duration = Duration::from_secs(1);
pub const DEFAULT_POLL_DEADLINE:       Duration = Duration::from_secs(5);
pub const PAYMENT_SIGNATURE_HEADER:    &str = "PAYMENT-SIGNATURE";
pub const PAYMENT_REQUIRED_HEADER:     &str = "PAYMENT-REQUIRED";
pub const PAYMENT_RESPONSE_HEADER:     &str = "PAYMENT-RESPONSE";
pub const SETTLEMENT_OVERRIDES_HEADER: &str = "settlement-overrides";

路由配置#

rust
use x402_core::http::{RoutesConfig, RoutePaymentConfig, AcceptConfig};
use std::collections::HashMap;

pub type RoutesConfig = HashMap<String, RoutePaymentConfig>;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoutePaymentConfig {
    pub accepts: Vec<AcceptConfig>,
    pub description: String,
    pub mime_type: String,
    /// `true` 让 settle 等链上确认后再返回(exact only);
    /// `false` / `None` → 异步结算 (`status="pending"`),中间件按
    /// `DEFAULT_POLL_DEADLINE` 轮询。
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sync_settle: Option<bool>,
    /// 商户手动钉 `ResourceInfo.url`;`None` 时中间件按
    /// `X-Forwarded-Proto` + `Host` + path + query 自动拼。
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcceptConfig {
    pub scheme: String,    // "exact" | "aggr_deferred" | "upto"
    pub price: String,     // "$0.01" / "0.01" / JSON AssetAmount
    pub network: String,   // "eip155:196"
    pub pay_to: String,
    /// 默认 300s (5 min)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_timeout_seconds: Option<u64>,
    /// Scheme-specific metadata. 常见用法:
    ///   - exact: `{"assetTransferMethod":"permit2"}` 切到 Permit2 流程
    ///   - upto: 通常留空 — `UptoEvmScheme::enhance_payment_requirements`
    ///           会自动注入 `assetTransferMethod` + `facilitatorAddress`
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extra: Option<HashMap<String, serde_json::Value>>,
}

示例(多 scheme 共存)#

rust
use std::collections::HashMap;
use serde_json::json;
use x402_axum::{AcceptConfig, RoutePaymentConfig};

let mut routes = HashMap::new();
routes.insert("GET /api/data".to_string(), RoutePaymentConfig {
    accepts: vec![
        // 1) 默认 exact + EIP-3009 (USD₮0 on X Layer)
        AcceptConfig {
            scheme: "exact".to_string(),
            price: "$0.01".to_string(),
            network: "eip155:196".to_string(),
            pay_to: "0xSeller".to_string(),
            max_timeout_seconds: None,
            extra: None,
        },
        // 2) exact + Permit2 (USD-pegged stablecoins)
        AcceptConfig {
            scheme: "exact".to_string(),
            price: "$0.01".to_string(),
            network: "eip155:196".to_string(),
            pay_to: "0xSeller".to_string(),
            max_timeout_seconds: None,
            extra: Some(HashMap::from([(
                "assetTransferMethod".to_string(),
                json!("permit2"),
            )])),
        },
        // 3) aggr_deferred (TEE 聚合)
        AcceptConfig {
            scheme: "aggr_deferred".to_string(),
            price: "$0.001".to_string(),
            network: "eip155:196".to_string(),
            pay_to: "0xSeller".to_string(),
            max_timeout_seconds: None,
            extra: None,
        },
    ],
    description: "Premium data".to_string(),
    mime_type: "application/json".to_string(),
    sync_settle: Some(true),
    resource: None,
});

Axum 中间件 (x402-axum)#

基本用法#

rust
use axum::{Router, routing::get};
use x402_axum::{payment_middleware, RoutesConfig};
use x402_core::server::X402ResourceServer;

let app = Router::new()
    .route("/api/data", get(handler))
    .layer(payment_middleware(routes, server));

构造函数#

rust
use std::time::Duration;
use x402_axum::{
    OnSettlementTimeoutHook, PaymentLayer, PaymentResolverFn, RoutesConfig,
};
use x402_core::server::X402ResourceServer;

// Basic middleware
pub fn payment_middleware(
    routes: RoutesConfig,
    server: X402ResourceServer,
) -> PaymentLayer;

// 自定义 settle/status 轮询截止时间
pub fn payment_middleware_with_poll_deadline(
    routes: RoutesConfig,
    server: X402ResourceServer,
    poll_deadline: Duration,
) -> PaymentLayer;

// settle 超时回调(手动二次确认链上交易)
pub fn payment_middleware_with_timeout_hook(
    routes: RoutesConfig,
    server: X402ResourceServer,
    timeout_hook: OnSettlementTimeoutHook,
) -> PaymentLayer;

pub fn payment_middleware_with_timeout_hook_and_deadline(
    routes: RoutesConfig,
    server: X402ResourceServer,
    timeout_hook: OnSettlementTimeoutHook,
    poll_deadline: Duration,
) -> PaymentLayer;

// 自定义 payment resolver(动态决定路由配置)
pub fn payment_middleware_with_resolver(
    routes: RoutesConfig,
    server: X402ResourceServer,
    resolver: PaymentResolverFn,
) -> PaymentLayer;

OnSettlementTimeoutHook#

rust
use std::pin::Pin;
use std::future::Future;
use x402_axum::{OnSettlementTimeoutHook, SettlementTimeoutResult};

pub struct SettlementTimeoutResult {
    pub confirmed: bool,
}

/// 参数顺序: (tx_hash, network),**不是** (route, tx_hash)。
pub type OnSettlementTimeoutHook = Box<
    dyn Fn(String, String)
            -> Pin<Box<dyn Future<Output = SettlementTimeoutResult> + Send>>
        + Send + Sync,
>;

let hook: OnSettlementTimeoutHook = Box::new(|tx_hash, network| {
    Box::pin(async move {
        // 在这里做超时观测:链上二次确认 / 日志 / 上报指标 / 等等
        SettlementTimeoutResult { confirmed: false }
    })
});

中间件流程#

  1. 取本次请求的路由 cfg:优先看 req.extensions::<PreMatchedRoute>()(外层 router 已经匹配好的),没有就退回 find_route_config(state.routes, method, path) —— 都没命中 → 透传给 inner handler
  2. 没有 payment-signature 请求头 → 返回 402 + PAYMENT-REQUIRED 请求头
  3. 解码并验证支付 payload
  4. 将 payload 与路由 accepts 匹配
  5. 通过 facilitator 验证 (POST /verify)
  6. 调用内部处理器并缓冲响应
  7. 如果业务 handler 写了 settlement-overrides 响应头(upto),中间件解析后用 SettlementOverrides 调 settle
  8. 通过 facilitator 结算 (POST /settle)
  9. 异步 (status: "pending") → 在 poll_deadline 内轮询
  10. 超时 → 调用 OnSettlementTimeoutHook(如果配置了)
  11. 向响应添加 PAYMENT-RESPONSE 请求头

重新导出#

rust
pub use x402_core::http::{
    AcceptConfig,
    BeforeHookResult,
    OnAfterSettleHook, OnAfterVerifyHook,
    OnBeforeSettleHook, OnBeforeVerifyHook,
    OnProtectedRequestHook,
    OnSettleFailureHook, OnSettlementTimeoutHook,
    OnVerifyFailureHook,
    PaymentResolverFn,
    PollResult,
    ProtectedRequestResult,
    RequestContext, ResolvedAccept,
    RoutePaymentConfig, RoutesConfig,
    SettleContext, SettleRecoveryResult, SettleResultContext,
    SettlementOverrides, SettlementTimeoutResult,
    VerifyContext, VerifyRecoveryResult, VerifyResultContext,
    DEFAULT_POLL_DEADLINE, DEFAULT_POLL_INTERVAL,
    SETTLEMENT_OVERRIDES_HEADER,
};
pub use x402_core::server::X402ResourceServer;

x402-axum 自身额外暴露的类型:

rust
/// 外层 router 把已匹配的路由 cfg 钉进 request extensions,
/// middleware 看到就直接用、跳过自己的 path 二次匹配。
pub struct PreMatchedRoute(pub RoutePaymentConfig);

EVM 机制 (x402-evm)#

ExactEvmScheme#

rust
use x402_evm::ExactEvmScheme;

let scheme = ExactEvmScheme::new();
scheme.scheme();  // "exact"

负责:

  • 价格解析:"$0.01" / "0.01" / JSON AssetAmount
  • 用代币 decimals 把价格换算成原子单位
  • 按 network 查 [get_default_asset] 选默认资产
  • 注入 EIP-712 domain(name / version)到 extra,供客户端 EIP-3009 签名
  • 同时支持 Permit2:买家在 extra.assetTransferMethod = "permit2" 时签 Permit2 凭证,落地 ExactPermit2Payload

AggrDeferredEvmScheme#

rust
use x402_evm::AggrDeferredEvmScheme;

let scheme = AggrDeferredEvmScheme::new();
scheme.scheme();  // "aggr_deferred"

价格 / 需求逻辑全部委托给 ExactEvmScheme;卖家配置上与 exact 完全相同,链上结算由 facilitator TEE 聚合。

UptoEvmScheme#

rust
use x402_evm::UptoEvmScheme;

let scheme = UptoEvmScheme::new();
scheme.scheme();  // "upto"

uptoPermit2-only 的 cap-and-override 模式:

  • PaymentRequirements.amount上限(cap),不是实际扣款
  • enhance_payment_requirements 强制 extra.assetTransferMethod = "permit2"
  • 自动从 getSupportedextra.facilitatorAddress 注入到 challenge,让买家把 facilitator 地址钉进 witness.facilitator(合约层强制 msg.sender == witness.facilitator
  • 业务 handler 在响应头写 settlement-overrides: {"amount":"..."} 决定实际扣款,中间件读出后调 settle_payment(..., overrides),余额自动不扣

EVM Payload 类型#

rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AssetTransferMethod {
    #[serde(rename = "eip3009")]
    Eip3009,
    Permit2,
}

// ---- EIP-3009 (default) ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EIP3009Authorization {
    pub from: String,
    pub to: String,
    pub value: String,
    pub valid_after: String,
    pub valid_before: String,
    pub nonce: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExactEIP3009Payload {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
    pub authorization: EIP3009Authorization,
}

// ---- Exact + Permit2 ----
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permit2Witness {
    pub to: String,
    pub valid_after: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Permit2Permitted {
    pub token: String,
    pub amount: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permit2Authorization {
    pub from: String,
    pub permitted: Permit2Permitted,
    pub spender: String,
    pub nonce: String,
    pub deadline: String,
    pub witness: Permit2Witness,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExactPermit2Payload {
    pub signature: String,
    pub permit2_authorization: Permit2Authorization,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExactEvmPayloadV2 {
    EIP3009(ExactEIP3009Payload),
    Permit2(ExactPermit2Payload),
}

impl ExactEvmPayloadV2 {
    pub fn is_permit2(&self) -> bool;
    pub fn is_eip3009(&self) -> bool;
}

// ---- Upto + Permit2 (cap mode) ----
/// Upto witness — adds `facilitator` so the upto proxy can enforce
/// `msg.sender == witness.facilitator` on chain.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Witness {
    pub to: String,
    pub facilitator: String,
    pub valid_after: String,
}

/// `permitted.amount` is the cap; facilitator may settle ≤ this.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Authorization {
    pub from: String,
    pub permitted: Permit2Permitted,
    pub spender: String,
    pub nonce: String,
    pub deadline: String,
    pub witness: UptoPermit2Witness,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Payload {
    pub signature: String,
    pub permit2_authorization: UptoPermit2Authorization,
}

/// 看 `PaymentPayload.payload` JSON 是否是 upto witness 形状
/// (`permit2Authorization.witness.facilitator` 存在)。
pub fn is_upto_permit2_payload(payload: &serde_json::Value) -> bool;

Permit2 / Upto 常量#

买家前置(一次性):使用 exact + permit2upto 路径前,买家钱包需 approve(PERMIT2_ADDRESS, MAX)。OKX Agentic Wallet 用户:Agent 检测 allowance 不足时引导授权(MAX / 自定义 / 撤销 3 选 1),用户确认后自动上链;普通 EOA 钱包需自己发 approve 交易。

rust
// Permit2 是 CREATE2-vanity 部署,每条 EVM 链地址相同。
pub const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3";

pub const PERMIT2_EIP712_DOMAIN_NAME: &str = "Permit2";

// x402 部署的 Permit2 proxy 合约 — 接收 Permit2 签名后转发到 ERC-20。
pub const X402_EXACT_PERMIT2_PROXY_ADDRESS: &str =
    "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
pub const X402_UPTO_PERMIT2_PROXY_ADDRESS: &str =
    "0x4020e7393B728A3939659E5732F87fdd8e680002";

// Permit2 witness typehash 字面值 — 字段顺序 ABI-significant,任何
// 重排都会导致链上验签失败。
pub const PERMIT2_EXACT_WITNESS_TYPE_STRING: &str = /* see source */;
pub const PERMIT2_UPTO_WITNESS_TYPE_STRING:  &str = /* see source */;

资产配置#

rust
#[derive(Debug, Clone)]
pub struct DefaultAssetInfo {
    pub address: &'static str,
    pub name: &'static str,       // EIP-712 domain name (USD₮0 用 U+20AE)
    pub version: &'static str,
    pub decimals: u8,
    pub asset_transfer_method: Option<&'static str>,  // 强制 "permit2"
    pub supports_eip2612: bool,                       // EIP-2612 permit() 支持
}

#[derive(Debug, Clone)]
pub struct ChainConfig {
    pub network: &'static str,
    pub chain_id: u64,
}

// Pre-registered:
//   XLAYER_MAINNET (eip155:196)  + XLAYER_MAINNET_USDT  (0x779ded..., USD₮0, 6 decimals)
//   XLAYER_TESTNET (eip155:1952) + XLAYER_TESTNET_USDT  (0x9e29b3..., USD₮0, 6 decimals)

pub fn get_default_asset(network: &str) -> Option<DefaultAssetInfo>;

错误类型#

rust
#[derive(Debug, thiserror::Error)]
pub enum X402Error {
    #[error(transparent)]
    Verify(#[from] VerifyError),
    #[error(transparent)]
    Settle(#[from] SettleError),
    #[error(transparent)]
    FacilitatorResponse(#[from] FacilitatorResponseError),

    #[error("configuration error: {0}")]
    Config(String),
    #[error("route configuration error: {0}")]
    RouteConfig(String),
    #[error("unsupported scheme: {0}")]
    UnsupportedScheme(String),
    #[error("unsupported network: {0}")]
    UnsupportedNetwork(String),
    #[error("price parse error: {0}")]
    PriceParse(String),

    #[error("http error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
    #[error("base64 decode error: {0}")]
    Base64Decode(#[from] base64::DecodeError),

    #[error("not initialized: {0}")]
    NotInitialized(String),
    #[error("{0}")]
    Other(String),
}

#[derive(Debug, Clone, thiserror::Error)]
pub struct VerifyError {
    pub status_code: u16,
    pub invalid_reason: Option<String>,
    pub invalid_message: Option<String>,
    pub payer: Option<String>,
}

#[derive(Debug, Clone, thiserror::Error)]
pub struct SettleError {
    pub status_code: u16,
    pub error_reason: Option<String>,
    pub error_message: Option<String>,
    pub payer: Option<String>,
    pub transaction: String,
    pub network: Network,
}

#[derive(Debug, Clone, thiserror::Error)]
pub struct FacilitatorResponseError(pub String);

工具函数 (x402_core::utils)#

rust
pub fn safe_base64_encode(data: &str) -> String;
pub fn safe_base64_decode(data: &str) -> Result<String, X402Error>;

/// Network 通配符匹配: "eip155:*" 匹配 "eip155:196"
pub fn network_matches_pattern(network: &str, pattern: &str) -> bool;

pub fn find_schemes_by_network<'a, T>(
    map: &'a HashMap<String, HashMap<String, T>>,
    network: &str,
) -> Option<&'a HashMap<String, T>>;

pub fn find_by_network_and_scheme<'a, T>(
    map: &'a HashMap<String, HashMap<String, T>>,
    scheme: &str,
    network: &str,
) -> Option<&'a T>;

pub fn deep_equal(obj1: &serde_json::Value, obj2: &serde_json::Value) -> bool;

Schema 验证 (x402_core::schemas)#

rust
pub fn validate_payment_requirements(req: &PaymentRequirements) -> Result<(), X402Error>;
pub fn validate_payment_payload(payload: &PaymentPayload) -> Result<(), X402Error>;
pub fn validate_payment_required(required: &PaymentRequired) -> Result<(), X402Error>;

验证所有必填字段均不为空。



Rust SDK 参考(适用于 chargesession#

Crate#

目录 / Lib aliasPublished name (crates.io)描述
mpp-evmokxweb3-app-mppOKX MPP EVM Seller SDK:EvmChargeMethod / EvmSessionMethod / EvmChargeChallenger、SA-API client、本地 store、EIP-712 签名、(feature handlers)Axum drop-in handlers
mpp(上游 crates.io)上游 MPP 协议层:PaymentCredential / PaymentChallenge / ChargeMethod / SessionMethod traits、Axum extractor MppCharge<C> / WithReceipt<T>、challenge codec、HMAC、PaymentErrorDetails
payment-router-axumokxweb3-app-payment-router-axum双协议(MPP + x402)路由 Tower Layer,adapter pattern 让一个 axum app 同时接两种协议

mpp-evm 通过 pub use ::mpp; 把上游 crate 也透传出来,业务侧只引一个 okxweb3-app-mpp 就够;想用上游 proxy 等模块时走 mpp_evm::mpp::proxy::... 即可。


常量#

rust
/// X Layer mainnet chain ID.
pub const DEFAULT_CHAIN_ID: u64 = 196;

/// X Layer mainnet escrow 合约地址(不传 `with_escrow` 时的兜底)。
pub const DEFAULT_ESCROW_CONTRACT: &str = "0x5E550002e64FaF79B41D89fE8439eEb1be66CE3b";

核心类型 (mpp-evm)#

SaApiResponse#

SA-API 统一响应包装,客户端自动解包 data

rust
#[derive(Debug, Clone, Deserialize)]
pub struct SaApiResponse<T> {
    pub code: i64,
    pub data: Option<T>,
    #[serde(default)]
    pub msg: String,
}

ChargeMethodDetails / ChargeSplit#

Charge challenge 的 methodDetails(base64url-encoded 进 request)。

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeMethodDetails {
    pub chain_id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fee_payer: Option<bool>,                // server pays gas (transaction mode)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub permit2_address: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memo: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub splits: Option<Vec<ChargeSplit>>,
    /// Endpoint URL this charge protects; SA 按 URL 聚合营收。
    /// SDK 不自动填,需要时构造 challenge 时手动塞。
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resource_url: Option<String>,
}

/// Constraints: sum(splits[].amount) < request.amount; primary recipient
/// must retain a non-zero remainder.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChargeSplit {
    pub amount: String,                         // base-units integer string
    pub recipient: String,                      // 40-hex EIP-55 address
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memo: Option<String>,
}

SessionMethodDetails / SessionSplit#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionMethodDetails {
    pub chain_id: u64,
    pub escrow_contract: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub channel_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub min_voucher_delta: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fee_payer: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub splits: Option<Vec<SessionSplit>>,
}

/// Constraints: `bps` in `[1, 9999]`; `sum(splits[].bps) < 10000`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSplit {
    pub recipient: String,
    pub bps: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub memo: Option<String>,
}

Eip3009Authorization / Eip3009Split#

Charge payload.authorization 形状(client 签 EIP-3009 后填进去)。

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Authorization {
    #[serde(rename = "type")]
    pub auth_type: String,                      // always "eip-3009"
    pub from: String,
    pub to: String,
    pub value: String,
    pub valid_after: String,
    pub valid_before: String,
    pub nonce: String,
    pub signature: String,
    /// 分账时每路 split 独立签一份 EIP-3009 (主 + 每 split 1)。
    #[serde(skip_serializing_if = "Option::is_none")]
    pub splits: Option<Vec<Eip3009Split>>,
}

impl Eip3009Authorization {
    pub const TYPE: &'static str = "eip-3009";
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Split {
    pub from: String,
    pub to: String,
    pub value: String,
    pub valid_after: String,
    pub valid_before: String,
    pub nonce: String,
    pub signature: String,
}

ChargeReceipt / SessionReceipt / ChannelStatus#

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeReceipt {
    pub method: String,                         // "evm"
    pub reference: String,                      // on-chain tx hash
    pub status: String,
    pub timestamp: String,
    pub chain_id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub confirmations: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub challenge_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionReceipt {
    pub method: String,
    pub intent: String,
    pub status: String,
    pub timestamp: String,
    pub chain_id: u64,
    pub channel_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    /// 当前 on-chain known deposit。
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deposit: Option<String>,
    // 以下字段新协议已废弃,仅为反序列化兼容保留 Option:
    #[serde(skip_serializing_if = "Option::is_none")]
    pub challenge_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub accepted_cumulative: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub spent: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub confirmations: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub units: Option<u64>,
}

/// Response from GET /session/status.
///
/// 注:`cumulative_amount` 在新版协议中已被剔除(只有 settle 才会更新),
/// 字段仍为 `Option` 仅为 backwards-compat。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelStatus {
    pub channel_id: String,
    pub payer: String,
    pub payee: String,
    pub token: String,
    pub deposit: String,
    pub settled_on_chain: String,
    pub session_status: String,
    pub remaining_balance: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cumulative_amount: Option<String>,
}

SettleRequestPayload / CloseRequestPayload#

SDK 主动调 /session/settle / /session/close 的请求 body(扁平,不带 challenge wrapper)。

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleRequestPayload {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub action: Option<String>,                 // "settle"
    pub channel_id: String,
    pub cumulative_amount: String,              // uint128 decimal string
    pub voucher_signature: String,              // 65-byte r‖s‖v hex (payer)
    pub payee_signature: String,                // 65-byte r‖s‖v hex (payee)
    pub nonce: String,                          // uint256 decimal string
    pub deadline: String,                       // uint256 decimal string
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseRequestPayload {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub action: Option<String>,                 // "close"
    pub channel_id: String,
    pub cumulative_amount: String,
    /// Normal branch: 65-byte r‖s‖v hex.
    /// Waiver branch (cumulative ≤ settledOnChain or no local voucher): "".
    pub voucher_signature: String,
    pub payee_signature: String,
    pub nonce: String,
    pub deadline: String,
}

ServerAccountingState#

rust
/// Server-side per-session accounting.
/// Invariants:
///   accepted_cumulative monotonically non-decreasing
///   spent monotonically non-decreasing
///   available = accepted_cumulative - spent
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerAccountingState {
    pub accepted_cumulative: u128,
    pub spent: u128,
    pub settled_on_chain: u128,
}

SaApiClient trait#

可插拔 SA-API client 接口,默认实现 [OkxSaApiClient]。

rust
#[async_trait]
pub trait SaApiClient: Send + Sync {
    // Charge
    async fn charge_settle(
        &self,
        credential: &serde_json::Value,
    ) -> Result<ChargeReceipt, SaApiError>;
    async fn charge_verify_hash(
        &self,
        credential: &serde_json::Value,
    ) -> Result<ChargeReceipt, SaApiError>;

    // Session
    // 注: 没有 /session/voucher endpoint;voucher 在 SDK 本地处理
    //     (`EvmSessionMethod::submit_voucher`)。
    async fn session_open(
        &self,
        credential: &serde_json::Value,
    ) -> Result<SessionReceipt, SaApiError>;
    async fn session_top_up(
        &self,
        credential: &serde_json::Value,
    ) -> Result<SessionReceipt, SaApiError>;
    async fn session_settle(
        &self,
        payload: &SettleRequestPayload,
    ) -> Result<SessionReceipt, SaApiError>;
    async fn session_close(
        &self,
        payload: &CloseRequestPayload,
    ) -> Result<SessionReceipt, SaApiError>;
    async fn session_status(
        &self,
        channel_id: &str,
    ) -> Result<ChannelStatus, SaApiError>;
}

OKX SA-API 客户端 (OkxSaApiClient)#

rust
#[derive(Debug, Clone)]
pub struct OkxSaApiClient { /* private */ }

impl OkxSaApiClient {
    /// Default production URL (https://web3.okx.com).
    pub fn new(api_key: String, secret_key: String, passphrase: String) -> Self;

    /// Custom base URL (sandbox / staging).
    pub fn with_base_url(
        base_url: String,
        api_key: String,
        secret_key: String,
        passphrase: String,
    ) -> Self;
}

实现 SaApiClient trait;每个请求自动加 HMAC-SHA256 认证头。HTTP 超时 30 秒。

端点#

trait 方法OKX 路径
charge_settle()POST /api/v6/pay/mpp/charge/settle
charge_verify_hash()POST /api/v6/pay/mpp/charge/verifyHash
session_open()POST /api/v6/pay/mpp/session/open
session_top_up()POST /api/v6/pay/mpp/session/topUp
session_settle()POST /api/v6/pay/mpp/session/settle
session_close()POST /api/v6/pay/mpp/session/close
session_status(channel_id)GET /api/v6/pay/mpp/session/status?channelId=...

OKX 响应包装在 {"code": 0, "data": {...}, "msg": ""},客户端自动解包。


Charge — EvmChargeMethod#

实现 mpp::protocol::traits::ChargeMethod,把 credential 透传给 SA-API。

rust
#[derive(Clone)]
pub struct EvmChargeMethod { /* private */ }

impl EvmChargeMethod {
    pub fn new(sa_client: Arc<dyn SaApiClient>) -> Self;
}

payload.type 路由:

  • "transaction"charge_settle(SA-API 链上 broadcast transferWithAuthorization
  • "hash"charge_verify_hash(client 已自行 broadcast,SA-API 验证 tx hash)

Splits 透传 payload.authorization.splits[],SA-API 拥有 split 校验权。


Charge — EvmChargeChallenger#

实现 upstream mpp::server::axum::ChargeChallenger,可挂到 MppCharge<C> extractor。

Config#

rust
pub struct EvmChargeChallengerConfig {
    pub charge_method: EvmChargeMethod,
    pub currency: String,                       // ERC-20 contract, 40-hex
    pub recipient: String,                      // primary payee address
    pub chain_id: u64,                          // 196 = X Layer
    pub fee_payer: Option<bool>,                // Some(true) = transaction mode
    pub realm: String,                          // for WWW-Authenticate header
    pub secret_key: String,                     // HMAC for challenge signing
    pub splits: Option<Vec<ChargeSplit>>,
    /// 每路 charger 一个 URL,SA 按 URL 聚合营收。`None` 关闭上报。
    pub resource_url: Option<String>,
}

构造#

rust
#[derive(Clone)]
pub struct EvmChargeChallenger { /* private */ }

impl EvmChargeChallenger {
    pub fn new(cfg: EvmChargeChallengerConfig) -> Self;

    pub fn builder(
        charge_method: EvmChargeMethod,
        realm: impl Into<String>,
        secret_key: impl Into<String>,
    ) -> EvmChargeChallengerBuilder;
}

pub struct EvmChargeChallengerBuilder { /* private */ }

impl EvmChargeChallengerBuilder {
    pub fn currency(self, v: impl Into<String>) -> Self;
    pub fn recipient(self, v: impl Into<String>) -> Self;
    pub fn chain_id(self, v: u64) -> Self;
    pub fn fee_payer(self, v: bool) -> Self;
    pub fn splits(self, v: Vec<ChargeSplit>) -> Self;
    pub fn resource_url(self, v: impl Into<String>) -> Self;
    pub fn build(self) -> EvmChargeChallenger;
}

EvmChargeChallenger 实现 mpp::server::axum::ChargeChallenger,提供 challenge(amount, options) + verify_payment(authorization_header) —— 跟 upstream MppCharge<C> extractor / WithReceipt<T> 一起组成最简 charge handler。


Session — EvmSessionMethod#

实现 mpp::protocol::traits::SessionMethod。维护本地 channel state、支持 voucher 本地验签 + 累计扣费、商户主动 settle/close。

构造与配置#

rust
#[derive(Clone)]
pub struct EvmSessionMethod { /* private */ }

impl EvmSessionMethod {
    /// Default in-memory store.
    pub fn new(sa_client: Arc<dyn SaApiClient>) -> Self;

    /// Inject a custom [`SessionStore`].
    pub fn with_store(
        sa_client: Arc<dyn SaApiClient>,
        store: Arc<dyn SessionStore>,
    ) -> Self;

    /// Inject the payee signer. 接受任意
    /// `alloy::signers::Signer + Send + Sync + 'static`
    /// (PrivateKeySigner / AwsSigner / LedgerSigner / 自定义远程签名器)。
    pub fn with_signer<S: Signer + Send + Sync + 'static>(mut self, signer: S) -> Self;

    /// Startup fast-fail check: `signer.address() == expected`.
    pub fn verify_payee(self, expected: Address) -> Result<Self, SaApiError>;

    /// Custom nonce 分配器(默认 [`UuidNonceProvider`])。
    pub fn with_nonce_provider(mut self, p: Arc<dyn NonceProvider>) -> Self;

    /// Custom EIP-712 domain `name` / `version`(默认 OKX canonical 值)。
    pub fn with_domain_meta(
        mut self,
        name: impl Into<Cow<'static, str>>,
        version: impl Into<Cow<'static, str>>,
    ) -> Self;

    /// Custom signature deadline(默认 `U256::MAX`,永不过期)。
    pub fn with_deadline(mut self, d: U256) -> Self;

    /// challenge methodDetails (raw JSON)。
    pub fn with_method_details(mut self, details: serde_json::Value) -> Self;

    /// challenge methodDetails (typed)。
    pub fn with_typed_method_details(
        mut self,
        details: SessionMethodDetails,
    ) -> Result<Self, serde_json::Error>;

    /// 最小 builder:仅设 escrow;`chain_id` 默认 X Layer (196)。
    /// 不显式调用时 escrow 自动使用 [`DEFAULT_ESCROW_CONTRACT`]。
    pub fn with_escrow(self, escrow_contract: impl Into<String>) -> Self;

    /// Startup 检查本地 EIP-712 domain 与合约的 `domainSeparator()` 是否
    /// 一致;mismatch → 8000。强烈推荐在 startup 调一次 —— 否则后续每张
    /// voucher / settle / close 签名都会被链上合约拒绝。
    pub fn assert_domain_matches(&self, on_chain: B256) -> Result<(), SaApiError>;
}

业务方法#

rust
impl EvmSessionMethod {
    /// Local store handle.
    pub fn store(&self) -> Arc<dyn SessionStore>;

    /// On-chain channel status (passthrough to SA-API).
    pub async fn status(&self, channel_id: &str) -> Result<ChannelStatus, SaApiError>;

    /// Local voucher: guard + EIP-712 verify + bump `highest_voucher`.
    /// Byte-level idempotent (same cum + same sig): verify 和 highest
    /// 更新被跳过,但 deduct 仍然运行。
    pub async fn submit_voucher(
        &self,
        channel_id: &str,
        cumulative_amount: u128,
        signature: Bytes,
    ) -> Result<(), SaApiError>;

    /// Atomic deduct: `available = highest_voucher_amount - spent`;
    /// insufficient → 70015.
    pub async fn deduct_from_channel(
        &self,
        channel_id: &str,
        amount: u128,
    ) -> Result<ChannelRecord, SaApiError>;

    /// Take local highest voucher → sign SettleAuth → call `/session/settle`.
    pub async fn settle_with_authorization(
        &self,
        channel_id: &str,
    ) -> Result<SessionReceipt, SaApiError>;

    /// Sign CloseAuth → call `/session/close` → remove from store on success.
    /// 如果 `cumulative_amount` / `provided_voucher_sig` 都是 `None`,
    /// 走 waiver 分支(空字符串)。
    pub async fn close_with_authorization(
        &self,
        channel_id: &str,
        cumulative_amount: Option<u128>,
        provided_voucher_sig: Option<Bytes>,
    ) -> Result<SessionReceipt, SaApiError>;
}

Session action 路由(SessionMethod::verify_session 内部)#

payload.action行为
"open"payee 校验 → SA session/open → 写本地 store
"voucher"submit_voucher(本地验签 + 升 highest)→ deduct_from_channel(扣费)
"topUp"SA session/topUp → 累加本地 deposit
"close"取 payer 提供的 voucher → 本地 close 流程

Session — SessionStore trait#

rust
/// Closure-based atomic update. `Err` aborts the whole update; the store
/// keeps the old value (transaction semantics).
pub type ChannelUpdater =
    Box<dyn FnOnce(&mut ChannelRecord) -> Result<(), SaApiError> + Send>;

#[async_trait]
pub trait SessionStore: Send + Sync {
    async fn get(&self, channel_id: &str) -> Option<ChannelRecord>;
    async fn put(&self, record: ChannelRecord);
    async fn remove(&self, channel_id: &str);

    /// Atomic read-modify-write. Channel absent → 70010 channel_not_found;
    /// updater 返回 Err → 不写、错误向上传。
    async fn update(
        &self,
        channel_id: &str,
        updater: ChannelUpdater,
    ) -> Result<ChannelRecord, SaApiError>;
}

ChannelRecord#

rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelRecord {
    pub channel_id: String,
    pub chain_id: u64,
    pub escrow_contract: Address,
    pub payer: Address,
    pub payee: Address,
    /// `authorized_signer` 在 open 时已解析为 payer (address(0) → payer),
    /// 存储层始终看到非零地址。
    pub authorized_signer: Address,
    pub deposit: u128,
    pub highest_voucher_amount: u128,
    pub highest_voucher_signature: Option<Bytes>,
    /// 节流: voucher 最小递增量;`None` 关闭节流。
    pub min_voucher_delta: Option<u128>,
    /// 已扣费 (invariant: spent ≤ highest_voucher_amount)。
    #[serde(default)]
    pub spent: u128,
    /// deduct 调用次数。
    #[serde(default)]
    pub units: u64,
}

impl ChannelRecord {
    pub fn voucher_signer(&self) -> Address;    // 返回 authorized_signer
}

默认实现 — InMemorySessionStore#

rust
#[derive(Debug, Default, Clone)]
pub struct InMemorySessionStore { /* private */ }

impl InMemorySessionStore {
    pub fn new() -> Self;
}

进程内 HashMap,适合大多数 single-process 部署(操作短、锁竞争小)。两个 caveat:

  • 重启即丢:进程重启 / crash 丢失所有 channel state。如果业务不能接受这个 loss(长期 channel、多实例 HA、热重载),自实现持久化 store(SQLite / Redis / Postgres / DynamoDB / ...)接 with_store(...) 注入。
  • abandoned channel 累积:payer 不调 close 时记录会一直留 —— 这是 session lifecycle 通用问题,不是 in-memory 特有。商户应有 cleanup 策略,或按业务 TTL 清理。

NonceProvider trait#

rust
#[async_trait]
pub trait NonceProvider: Send + Sync {
    async fn allocate(
        &self,
        payee: Address,
        channel_id: B256,
    ) -> Result<U256, SaApiError>;
}

/// 默认实现: UUID v4 → U256 (128-bit random, stateless, safe across
/// multi-instance / restart)。
#[derive(Debug, Default, Clone)]
pub struct UuidNonceProvider;

合约层 nonce 已用集 key = (payee, channelId, nonce),重复使用以 NonceAlreadyUsed revert。SDK 只负责分配「大概率没用过」的 nonce,不追踪已用集。


EIP-712 签名 (mpp_evm::eip712)#

Domain#

rust
pub const VOUCHER_DOMAIN_NAME:    &str = "EVM Payment Channel";
pub const VOUCHER_DOMAIN_VERSION: &str = "1";

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomainMeta {
    pub name: Cow<'static, str>,
    pub version: Cow<'static, str>,
}

impl DomainMeta {
    pub fn new(
        name: impl Into<Cow<'static, str>>,
        version: impl Into<Cow<'static, str>>,
    ) -> Self;
}

impl Default for DomainMeta { /* uses VOUCHER_DOMAIN_* constants */ }

pub fn build_domain(
    meta: &DomainMeta,
    chain_id: u64,
    escrow_contract: Address,
) -> alloy_sol_types::Eip712Domain;

Voucher 验签#

rust
sol! {
    /// EIP-712 typed struct; 1:1 with the contract's `Voucher`.
    struct Voucher {
        bytes32 channelId;
        uint128 cumulativeAmount;
    }
}

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum VerifyError {
    #[error("signature must be 65 bytes, got {0}")]
    BadLength(usize),
    #[error("non-canonical signature: s exceeds secp256k1 half-order (high-s)")]
    HighS,
    #[error("signature parse failed")]
    SignatureParse,
    #[error("ecrecover failed")]
    Recover,
    #[error("signer mismatch: recovered {recovered}, expected {expected}")]
    AddressMismatch { recovered: Address, expected: Address },
}

/// 1) signature.len() == 65  2) low-s precheck  3) EIP-712 digest
/// 4) ecrecover + strict address comparison
pub fn verify_voucher(
    meta: &DomainMeta,
    escrow_contract: Address,
    chain_id: u64,
    channel_id: B256,
    cumulative_amount: u128,
    signature: &[u8],
    expected_signer: Address,
) -> Result<(), VerifyError>;

SettleAuthorization / CloseAuthorization 签名#

rust
sol! {
    struct SettleAuthorization {
        bytes32 channelId;
        uint128 cumulativeAmount;
        uint256 nonce;
        uint256 deadline;
    }
    struct CloseAuthorization {
        bytes32 channelId;
        uint128 cumulativeAmount;
        uint256 nonce;
        uint256 deadline;
    }
}

#[derive(Debug, Clone)]
pub struct SignedAuthorization {
    pub channel_id: B256,
    pub cumulative_amount: u128,
    pub nonce: U256,
    pub deadline: U256,
    pub signature: Bytes,                       // 65-byte (r, s, v)
}

pub async fn sign_settle_authorization(
    meta: &DomainMeta,
    signer: &(impl Signer + ?Sized),
    escrow_contract: Address,
    chain_id: u64,
    channel_id: B256,
    cumulative_amount: u128,
    nonce: U256,
    deadline: U256,
) -> Result<SignedAuthorization, SaApiError>;

pub async fn sign_close_authorization(
    meta: &DomainMeta,
    signer: &(impl Signer + ?Sized),
    escrow_contract: Address,
    chain_id: u64,
    channel_id: B256,
    cumulative_amount: u128,
    nonce: U256,
    deadline: U256,
) -> Result<SignedAuthorization, SaApiError>;

Challenge 构造器 (mpp_evm::charge::challenge)#

rust
pub const METHOD_NAME:           &str = "evm";
pub const INTENT_CHARGE:         &str = "charge";
pub const INTENT_SESSION:        &str = "session";
pub const DEFAULT_EXPIRES_MINUTES: i64 = 5;

/// Build a `method="evm"` charge challenge with HMAC-protected `id`.
pub fn build_charge_challenge(
    secret_key: &str,
    realm: &str,
    request: &mpp::protocol::intents::ChargeRequest,
    expires: Option<&str>,
    description: Option<&str>,
) -> Result<mpp::protocol::core::PaymentChallenge, String>;

pub fn build_session_challenge(
    secret_key: &str,
    realm: &str,
    request: &mpp::protocol::intents::SessionRequest,
    expires: Option<&str>,
    description: Option<&str>,
) -> Result<mpp::protocol::core::PaymentChallenge, String>;

/// 用 base-units amount + typed method details 拼一个 request body。
pub fn charge_request_with(
    amount_base_units: impl Into<String>,
    currency: impl Into<String>,
    recipient: impl Into<String>,
    details: ChargeMethodDetails,
) -> Result<mpp::protocol::intents::ChargeRequest, String>;

pub fn session_request_with(
    amount_per_unit_base: impl Into<String>,
    currency: impl Into<String>,
    recipient: impl Into<String>,
    details: SessionMethodDetails,
) -> Result<mpp::protocol::intents::SessionRequest, String>;

CredentialExt — 解码 challenge.request#

PaymentCredential.challenge.requestBase64UrlJson<T>.decode() 返通用 error。本扩展把它归一到 SaApiError,可与 SDK 其他调用一起 ?

rust
pub trait CredentialExt {
    fn decode_request<R: DeserializeOwned>(&self) -> Result<R, SaApiError>;
}

impl CredentialExt for mpp::protocol::core::PaymentCredential { /* ... */ }

// Usage:
use mpp_evm::CredentialExt;
use mpp::protocol::intents::SessionRequest;

let request: SessionRequest = credential.decode_request()?;

Axum drop-in handlers (mpp_evm::axum, feature = "handlers")#

需要在 Cargo.toml 里启用 handlers feature:

toml
okxweb3-app-mpp = { version = "0.2", features = ["handlers"] }
rust
use mpp_evm::axum as mpp_axum;

#[derive(Debug, Clone, Deserialize)]
pub struct SettleBody {
    #[serde(rename = "channelId")]
    pub channel_id: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct StatusQuery {
    #[serde(rename = "channelId")]
    pub channel_id: String,
}

/// POST /session/settle — body { "channelId": "0x..." }.
pub async fn session_settle(
    State(method): State<Arc<EvmSessionMethod>>,
    Json(body): Json<SettleBody>,
) -> Response;

/// GET /session/status?channelId=0x...
pub async fn session_status(
    State(method): State<Arc<EvmSessionMethod>>,
    Query(q): Query<StatusQuery>,
) -> Response;

错误自动按 SaApiError::to_problem_details(...) 映射到正确的 HTTP 状态码。

注:模块名是 mpp_evm::axum,跟外部 axum crate 同名;在同一 .rs 文件里同时引用两者时建议给 mpp 那个起别名(如 use mpp_evm::axum as mpp_axum;)。


错误类型#

rust
#[derive(Debug, Clone, thiserror::Error)]
#[error("SA API error {code}: {msg}")]
pub struct SaApiError {
    pub code: u32,
    pub msg: String,
}

impl SaApiError {
    pub fn new(code: u32, msg: impl Into<String>) -> Self;

    /// Map to mpp-rs `PaymentErrorDetails` (RFC 9457 ProblemDetails).
    pub fn to_problem_details(
        &self,
        challenge_id: Option<&str>,
    ) -> mpp::PaymentErrorDetails;
}

错误码映射#

code含义
8000API 服务内部错误
70000缺少必填字段或格式错误
70001链不在支持列表
70002付款方在黑名单
70003source 缺失、feePayer=true 不支持 hash 模式、或 txHash 已被使用
70004签名验证失败
70005分账总额 ≥ 主金额
70006分账数量 > 10
70007交易未在链上确认
70008链上合约 channel 状态已关闭
70009challenge 不存在或已过期
70010channelId 不存在
70011Escrow 合约 grace period < 10 分钟,拒绝开通 channel
70012cumulativeAmount 超过 channel 存款余额
70013voucher 递增量低于 minVoucherDelta
70014channel 处于 CLOSING 状态,不接受新 Voucher
70015本地账户余额不足扣费(available < amount

双协议路由 (payment-router-axum)#

让一个 axum app 同时接 MPP + x402,业务 handler 协议无关。

Adapter trait#

rust
pub trait ProtocolAdapter: Send + Sync + 'static {
    fn name(&self) -> &str;                                 // "mpp" | "x402" | 自定义
    fn priority(&self) -> u32;                              // 10 = MPP, 20 = x402, 100+ = 自定义
    fn detect(&self, parts: &http::request::Parts) -> bool; // 看请求头是否属于本协议(不读 body)

    /// 生成本协议在该 route 的 402 challenge 行。`route_cfg` 是类型擦除
    /// 的 `AdapterConfig`,每个 adapter 用 `downcast_ref::<MyRouteConfig>()`
    /// 拿回具体类型。
    fn get_challenge<'a>(
        &'a self,
        parts: &'a http::request::Parts,
        route_cfg: &'a AdapterConfig,
    ) -> ChallengeFuture<'a>;

    fn make_service(&self, inner: InnerService) -> InnerService;

    /// 路由匹配后、forward 前调用。把匹配好的 cfg 注入 extensions,
    /// 供 inner middleware 直传使用。默认空实现。
    fn enrich_request_extensions(
        &self,
        _extensions: &mut http::Extensions,
        _route_cfg: &UnifiedRouteConfig,
    ) {
    }

    /// 启动期一次性调用,让 adapter 把路由注册到内部状态。默认空实现。
    fn prepare(&self, _routes: &[(String, UnifiedRouteConfig)]) -> Result<(), String> {
        Ok(())
    }
}

内置 adapter#

rust
use std::sync::Arc;
use payment_router_axum::adapters::{MppAdapter, MppRouteConfig, X402Adapter, X402RouteConfig};

// MppAdapter::new 接收 `Arc<EvmMpp>`(通过 EvmMpp::builder(...).with_charge(...).build() 拼出)。
let mpp_adapter: Arc<dyn ProtocolAdapter> = Arc::new(MppAdapter::new(mpp));

// X402Adapter::new 接 owned `X402ResourceServer`,返回 builder;用 `.build()` 收尾。
// 路由配置不在这里传,而是放进 UnifiedRouteConfig.adapter_configs["x402"]。
let x402_adapter: Arc<dyn ProtocolAdapter> = Arc::new(X402Adapter::new(x402_server).build());

Adapter 完整构造方法#

rust
impl MppAdapter {
    pub fn new(mpp: Arc<EvmMpp>) -> Self;
    /// 覆盖默认优先级 10(用户自定义 adapter 应从 100 起)。
    pub fn with_priority(self, priority: u32) -> Self;
}

impl X402Adapter {
    /// 一参 + builder 收尾。
    pub fn new(server: X402ResourceServer) -> X402AdapterBuilder;
}

impl X402AdapterBuilder {
    pub fn priority(self, priority: u32) -> Self;
    pub fn poll_deadline(self, d: Duration) -> Self;
    pub fn resolver(self, resolver: PaymentResolverFn) -> Self;

    // 与 x402-axum payment_middleware_with_* 同名 hook,原样转发到内部
    // PaymentLayer。
    pub fn on_protected_request(self, hook: OnProtectedRequestHook) -> Self;
    pub fn on_before_verify(self, hook: OnBeforeVerifyHook) -> Self;
    pub fn on_after_verify(self, hook: OnAfterVerifyHook) -> Self;
    pub fn on_verify_failure(self, hook: OnVerifyFailureHook) -> Self;
    pub fn on_before_settle(self, hook: OnBeforeSettleHook) -> Self;
    pub fn on_after_settle(self, hook: OnAfterSettleHook) -> Self;
    pub fn on_settle_failure(self, hook: OnSettleFailureHook) -> Self;
    pub fn on_settlement_timeout(self, hook: OnSettlementTimeoutHook) -> Self;

    pub fn build(self) -> X402Adapter;
}

MppAdapter 内部用 mpp::server::axum::ChargeChallenger 完整流程(HMAC 校验 + EIP-3009 验签 + SA-API 结算);X402Adapter 内部走 x402-axum 原生 PaymentMiddleware

每 adapter 的类型化路由配置#

rust
/// MPP per-route config.
#[derive(Debug, Clone, Default)]
pub struct MppRouteConfig {
    /// `"charge"` or `"session"`(空 → `"charge"`)
    pub intent: String,
    pub amount: String,                         // base-units integer string
    pub currency: String,
    pub description: Option<String>,
    /// 商户业务方自己的 reference id (charge only)
    pub external_id: Option<String>,
    /// session only: 计费单位 ("request" / "byte" 等)
    pub unit_type: Option<String>,
    /// session only: suggested initial deposit (base units, stringified)
    pub suggested_deposit: Option<String>,
}

/// x402 per-route config(编译期映射到 `x402_axum::RoutePaymentConfig`)。
#[derive(Debug, Clone, Default)]
pub struct X402RouteConfig {
    pub accepts: Vec<x402_axum::AcceptConfig>,
    pub description: String,
    pub mime_type: String,
    pub sync_settle: Option<bool>,
    pub resource: Option<String>,
}

Router 配置#

rust
use std::collections::HashMap;
use std::sync::Arc;
use payment_router_axum::{
    AdapterConfig, BuildError, PaymentRouterConfig, PaymentRouterLayer,
    ProtocolAdapter, UnifiedRouteConfig,
};

#[derive(Clone)]
pub struct AdapterConfig(Arc<dyn Any + Send + Sync>);

impl AdapterConfig {
    pub fn new<T: Any + Send + Sync + 'static>(value: T) -> Self;
    pub fn downcast_ref<T: Any>(&self) -> Option<&T>;
}

#[derive(Debug, Clone, Default)]
pub struct UnifiedRouteConfig {
    pub description: Option<String>,
    /// adapter.name() → 该 adapter 在本 route 上的 type-erased 配置。
    /// 没列的 adapter 在本 route 上不启用。
    pub adapter_configs: HashMap<String, AdapterConfig>,
}

impl UnifiedRouteConfig {
    pub fn builder() -> UnifiedRouteConfigBuilder;
}

impl UnifiedRouteConfigBuilder {
    pub fn description(self, desc: impl Into<String>) -> Self;
    /// 类型化注入;T 必须与对应 adapter 期望的类型一致。
    pub fn adapter<T: Any + Send + Sync + 'static>(
        self,
        name: impl Into<String>,
        config: T,
    ) -> Self;
    pub fn build(self) -> UnifiedRouteConfig;
}

pub struct PaymentRouterConfig {
    /// `Vec<(pattern, route_cfg)>`。Vec 而非 HashMap —— 声明顺序敏感
    /// (spec §9 first-match-wins)。pattern 形如 "GET /path" 或 "/path"。
    pub routes: Vec<(String, UnifiedRouteConfig)>,
    /// 协议 adapter 列表 (MPP / x402 / 自定义)。
    pub protocols: Vec<Arc<dyn ProtocolAdapter>>,
    pub on_error: Option<Arc<ErrorHandler>>,
}

pub type ErrorHandler =
    dyn Fn(&(dyn std::error::Error + Send + Sync), ErrorContext) + Send + Sync + 'static;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorPhase {
    Detect,        // adapter.detect()
    Challenge,     // adapter.get_challenge()
    Handle,        // adapter-wrapped service call
}

impl ErrorPhase {
    pub fn as_str(self) -> &'static str;  // "detect" / "challenge" / "handle"
}

#[derive(Debug, Clone)]
pub struct ErrorContext {
    pub phase: ErrorPhase,
    pub protocol: String,
    pub route: Option<String>,
}

pub struct PaymentRouterLayer { /* tower::Layer */ }

impl PaymentRouterLayer {
    /// 失败返回 `payment_router_axum::BuildError`:unified routes 引用了
    /// 未注册的 `adapter.name()`,或某 adapter 的 `prepare()` hook 报错
    /// (例如类型化配置 downcast 失败)。
    pub fn new(cfg: PaymentRouterConfig) -> Result<Self, BuildError>;
}

端到端拼装示例#

rust
let route = UnifiedRouteConfig::builder()
    .description("photo")
    .adapter("mpp", MppRouteConfig {
        intent: "charge".into(),
        amount: "100".into(),
        currency: "0x...".into(),
        description: Some("photo".into()),
        external_id: None,
        unit_type: None,
        suggested_deposit: None,
    })
    .adapter("x402", X402RouteConfig {
        accepts: vec![/* AcceptConfig {...} */],
        description: "photo".into(),
        mime_type: "image/png".into(),
        sync_settle: None,
        resource: None,
    })
    .build();

let layer = PaymentRouterLayer::new(PaymentRouterConfig {
    routes: vec![("GET /photo".into(), route)],
    protocols: vec![mpp_adapter, x402_adapter],
    on_error: None,
})?;
目录