
X402
- 12 installs
- Updated May 1, 2026
- melonask/x402-skills
Helps with ai & agent building tasks.
About
x402 is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- x402
- AI & Agent Building
- AI-coding skill
X402 by the numbers
- 12 all-time installs (skills.sh)
- Ranked #11,618 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/melonask/x402-skills --skill x402Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| Last updated | May 1, 2026 |
| Repository | melonask/x402-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
x402 Protocol & x402-rs Development Guide
The x402 protocol activates the long-dormant HTTP 402 Payment Required status code to enable blockchain payments directly through HTTP. The client never touches the blockchain directly — it produces a cryptographic signature, and a facilitator verifies and settles the payment on-chain.
Three Actors
Client Resource Server Facilitator
| | |
| 1. GET /paid-content | |
|---------------------------->| |
| 2. 402 Payment Required | |
| Payment-Required: <b64> | |
|<----------------------------| |
| 3. Sign payment (off-chain) | |
| 4. GET /paid-content | |
| Payment-Signature: <b64> | |
|---------------------------->| |
| | 5. POST /verify |
| |------------------------->|
| | 6. Valid |
| |<-------------------------|
| | 7. Execute handler |
| | 8. POST /settle |
| |------------------------->|
| | 9. On-chain tx |
| |<-------------------------|
| 11. 200 OK + content | |
|<----------------------------| |Crate Architecture
| Crate | Purpose |
|---|---|
x402-types | Core protocol types, facilitator traits, CAIP-2 chain IDs, config, wire format (V1/V2) |
x402-axum | Axum middleware — protect routes with x402 payments (server-side) |
x402-reqwest | Reqwest middleware — transparent x402 payment handling (client-side) |
x402-facilitator-local | Local facilitator — verify and settle payments |
x402-chain-eip155 | EVM chain support (Ethereum, Base, Polygon, Avalanche, Sei, Celo, etc.) |
x402-chain-solana | Solana chain support |
x402-chain-aptos | Aptos chain support (git-only dependency, not on crates.io) |
x402-facilitator | Production facilitator binary (not on crates.io) |
Quick Start: Which Guide Do I Need?
The user's task determines which guide to read next:
- Creating a paid API/protecting routes -> Read
references/server-guide.md - Building an agent that pays for data -> Read
references/client-guide.md - Running a facilitator / verifying payments -> Read
references/facilitator-guide.md - Chain-specific config, tokens, networks -> Read
references/chain-config.md - V1 vs V2 differences, schemes, gasless internals -> Read
references/protocol-details.md - Building custom payment schemes (e.g., EIP-7702) -> Read
references/custom-scheme-guide.md
Minimal Working Examples
Server: Protect a Route (Axum)
# Cargo.toml
[dependencies]
x402-axum = "1.0"
x402-chain-eip155 = { version = "1.0", features = ["server"] }
x402-types = "1.0"
alloy-primitives = "1.4"
axum = "0.8"
tokio = { version = "1", features = ["full"] }use alloy_primitives::address;
use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use x402_axum::X402Middleware;
use x402_chain_eip155::{V2Eip155Exact, KnownNetworkEip155};
use x402_types::networks::USDC;
let x402 = X402Middleware::new("https://facilitator.x402.rs");
let app = Router::new().route(
"/paid-content",
get(handler).layer(
x402.with_price_tag(V2Eip155Exact::price_tag(
address!("0xYourWalletAddress"),
USDC::base_sepolia().amount(10u64), // 10 USDC (6 decimals)
))
),
);
async fn handler() -> impl IntoResponse {
(StatusCode::OK, "Paid content here!")
}Client: Auto-Pay for Data (Reqwest)
# Cargo.toml
[dependencies]
x402-reqwest = { version = "1.0", features = ["json"] }
x402-chain-eip155 = { version = "1.0", features = ["client"] }
alloy-signer-local = "1.4"
reqwest = { version = "0.13", features = ["json"] }
tokio = { version = "1", features = ["full"] }use x402_reqwest::{ReqwestWithPayments, ReqwestWithPaymentsBuild, X402Client};
use x402_chain_eip155::V2Eip155ExactClient;
use alloy_signer_local::PrivateKeySigner;
use std::sync::Arc;
use reqwest::Client;
let signer: Arc<PrivateKeySigner> = Arc::new("0x...private_key...".parse()?);
let x402_client = X402Client::new()
.register(V2Eip155ExactClient::new(signer));
let client = Client::new()
.with_payments(x402_client)
.build();
// Payments are handled transparently
let res = client.get("https://api.example.com/protected").send().await?;Multi-Chain Client (EVM + Solana)
use x402_reqwest::{X402Client, ReqwestWithPayments, ReqwestWithPaymentsBuild};
use x402_chain_eip155::{V1Eip155ExactClient, V2Eip155ExactClient};
use x402_chain_solana::{V1SolanaExactClient, V2SolanaExactClient};
use alloy_signer_local::PrivateKeySigner;
use solana_keypair::Keypair;
use solana_client::nonblocking::rpc_client::RpcClient;
use std::sync::Arc;
let evm_signer: Arc<PrivateKeySigner> = Arc::new("0x...".parse()?);
let sol_kp = Arc::new(Keypair::from_base58_string("..."));
let sol_rpc = Arc::new(RpcClient::new("https://api.mainnet-beta.solana.com"));
let x402_client = X402Client::new()
.register(V1Eip155ExactClient::new(evm_signer.clone()))
.register(V2Eip155ExactClient::new(evm_signer))
.register(V1SolanaExactClient::new(sol_kp.clone(), sol_rpc.clone()))
.register(V2SolanaExactClient::new(sol_kp, sol_rpc));
let client = Client::new().with_payments(x402_client).build();Key Concepts at a Glance
Any Token, Any Chain
x402 supports any ERC-20 token (not just USDC). The price_tag method accepts any token contract address:
use x402_chain_eip155::KnownNetworkEip155;
use x402_types::networks::USDC;
// USDC (convenience helper)
USDC::base_sepolia().amount(10u64)
// Any ERC-20 token directly
V2Eip155Exact::price_tag(
address!("0xTokenContractAddress"),
"1000000".to_string(), // amount in smallest token unit
)Gasless Payments
The client never pays gas. Three mechanisms exist for EVM chains:
1. EIP-3009 (transferWithAuthorization) — preferred when token supports it (e.g., USDC). Client signs one message, facilitator calls the transfer function. 2. Permit2 (Uniswap universal proxy) — fallback for any ERC-20. Requires one-time approve(Permit2) setup by the user (gasless options exist). 3. EIP-2612 gas sponsoring — facilitator can sponsor the Permit2 approval itself if the token supports permit().
V1 vs V2 Protocol
| Aspect | V1 | V2 |
|---|---|---|
| Network IDs | Network names ("base-sepolia") | CAIP-2 chain IDs ("eip155:84532") |
| Payment header | X-PAYMENT | Payment-Signature |
| Requirements header | JSON body | Payment-Required header (base64) |
| Token support | USDC-focused | Any ERC-20/SPL/Aptos token |
Both are supported simultaneously by x402-rs. Prefer V2 for new projects.
Price Tag Types
| Type | Chain | Protocol | Scheme |
|---|---|---|---|
V1Eip155Exact::price_tag() | EVM | V1 | exact |
V2Eip155Exact::price_tag() | EVM | V2 | exact |
V1SolanaExact::price_tag() | Solana | V1 | exact |
V2SolanaExact::price_tag() | Solana | V2 | exact |
V2AptosExact::price_tag() | Aptos | V2 | exact |
V2Eip155Upto::price_tag() | EVM | V2 | upto |
Custom Schemes & Extensibility
If built-in schemes like exact or upto don't fit, x402-rs allows you to implement custom schemes via the X402SchemeId and X402SchemeFacilitator traits. This is highly useful for implementing bleeding-edge blockchain patterns—like EIP-7702 delegation—where the scheme dictates unique signature payloads and Type 4 transaction settlements, while keeping the HTTP middleware layers unchanged.
Installation Patterns
Server (accept payments)
[dependencies]
x402-axum = "1.0"
x402-chain-eip155 = { version = "1.0", features = ["server"] }
x402-chain-solana = { version = "1.0", features = ["server"] }
x402-types = "1.0"Client (make payments)
[dependencies]
x402-reqwest = { version = "1.0", features = ["json"] }
x402-chain-eip155 = { version = "1.0", features = ["client"] }
x402-chain-solana = { version = "1.0", features = ["client"] }Facilitator (verify/settle)
[dependencies]
x402-facilitator-local = "1.0"
x402-chain-eip155 = { version = "1.0", features = ["facilitator"] }
x402-chain-solana = { version = "1.0", features = ["facilitator"] }Full custom facilitator server
[dependencies]
x402-facilitator-local = { version = "1.0", features = ["telemetry"] }
x402-chain-eip155 = { version = "1.0", features = ["facilitator"] }
x402-chain-solana = { version = "1.0", features = ["facilitator"] }
x402-types = { version = "1.0", features = ["cli"] }Common Patterns
Dynamic Pricing
Compute price per-request based on headers, query params, or auth:
x402.with_dynamic_price(|headers, uri, _base_url| {
let has_discount = uri.query().map(|q| q.contains("discount")).unwrap_or(false);
let amount = if has_discount { 50u64 } else { 100u64 };
async move {
vec![V2Eip155Exact::price_tag(
address!("0x..."),
USDC::base_sepolia().amount(amount),
)]
}
})Return vec![] from dynamic pricing to bypass payment entirely (conditional free access).
Multi-Chain Payment Acceptance (Server)
Accept payments on multiple chains simultaneously:
x402
.with_price_tag(V2Eip155Exact::price_tag(
address!("0x..."),
USDC::base_sepolia().amount(10u64),
))
.with_price_tag(V2SolanaExact::price_tag(
"EGBQqKn968sVv5cQh5Cr72pSTHfxsuzq7o7asqYB5uEV".to_string(),
USDC::solana().amount(10u64),
))Settlement Timing
// Default: settle after handler (content served even if settlement fails mid-flight)
x402.settle_after_execution();
// Settle before handler (guarantees payment, but slight latency)
x402.settle_before_execution();Custom Facilitator Endpoints
x402.with_base_url(Url::parse("https://api.example.com").unwrap())
.with_resource(Url::parse("https://api.example.com/premium").unwrap())
.with_description("Premium API access")
.with_mime_type("application/json")
.with_supported_cache_ttl(Duration::from_secs(300))Running a Facilitator
Docker (simplest)
docker run -v $(pwd)/config.json:/app/config.json -p 8080:8080 \
ghcr.io/x402-rs/x402-facilitatorFrom Source
# All chains
cargo run --package x402-facilitator --features full
# Specific chains only
cargo run --package x402-facilitator --features chain-eip155,chain-solanaSee references/facilitator-guide.md for full facilitator configuration.
Reference Files
For detailed information on any topic, read the appropriate reference file:
references/server-guide.md— Complete server-side guide: static/dynamic pricing, multi-chain, custom schemes, error handling, thePaygateProtocoltraitreferences/client-guide.md— Complete client-side guide: scheme registration, payment selection, multi-chain clients, custom selectors, V1/V2 handlingreferences/facilitator-guide.md— Facilitator setup, configuration, custom facilitator implementation, scheme registry, OpenTelemetry, graceful shutdownreferences/chain-config.md— Per-chain config (EVM, Solana, Aptos), all known networks, USDC addresses, RPC setup, feature flags, environment variables,LiteralOrEnvreferences/protocol-details.md— V1 vs V2 wire format, EIP-3009 flow, Permit2 flow, EIP-2612 gas sponsoring, smart wallet support (EIP-1271/EIP-6492),uptoscheme, custom scheme implementationreferences/custom-scheme-guide.md— Creating custom schemes, extending the protocol, and full-stack EIP-7702 implementation.
Known Issues with x402-rs SKILL.md (April 2026)
This file documents real-world compilation and runtime issues discovered while testing every code example from the x402-rs skill documentation against the latest published crates (1.4.6). All issues were reproduced and fixed in a production workspace using real dependencies (not mocked).
Environment: rustc 1.95.0, cargo 1.95.0, Linux x86_64. Crates tested: x402-axum 1.4.6, x402-reqwest 1.4.6, x402-chain-eip155 1.4.6, x402-types 1.4.6, x402-facilitator-local 1.4.6 (published 2026-04-14).
---
Issue 1: Server example missing Router type annotation
Location: SKILL.md lines 75–97, references/server-guide.md lines 24–44 Severity: Compilation error Status: Fixed with explicit type in real project
The Bug
The skill shows:
let app = Router::new().route(
"/paid-content",
get(handler).layer(
x402.with_price_tag(...)
),
);This fails because Router::new() returns Router<S> where S is the state type, and the chained .route(...).layer(...) doesn't allow inference. Compiler error:
error[E0283]: type annotations needed for `Router<_>`The Real Fix
Add an explicit Router type annotation:
let app: Router = Router::new().route(
"/paid-content",
get(handler).layer(
x402.with_price_tag(V2Eip155Exact::price_tag(
address!("0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07"),
USDC::base_sepolia().amount(10u64),
))
),
);Verified: cargo run succeeds for x402-production-server.
---
Issue 2: Missing KnownNetworkEip155 import for USDC::base_sepolia()
Location: references/chain-config.md lines 45–65 (and many server/client snippets) Severity: Compilation error Status: Fixed by adding the trait import
The Bug
Calling USDC::base_sepolia() or any USDC::* method fails unless the trait KnownNetworkEip155 is in scope:
error[E0599]: no function or associated item named `base_sepolia` found for struct `USDC`The Real Fix
Always import:
use x402_chain_eip155::KnownNetworkEip155;
use x402_types::networks::USDC;Verified: Required in x402-production-server and x402-production-e2e.
---
Issue 3: X402Middleware config methods live on X402LayerBuilder, not X402Middleware
Location: SKILL.md lines 295–300 (settle timing is correct, .with_resource/.with_mime_type/.with_description are not) Severity: Compilation error Status: Fixed – use X402LayerBuilder methods
The Bug
The skill shows:
let x402 = X402Middleware::new("https://facilitator.x402.rs")
.with_base_url(Url::parse("https://api.example.com").unwrap()) // correct
.with_resource(Url::parse("https://api.example.com/premium").unwrap()) // NOT on X402Middleware
.with_description("Premium API access") // NOT on X402Middleware
.with_mime_type("application/json"); // NOT on X402MiddlewareThese methods do not exist on X402Middleware; they exist only on X402LayerBuilder, which is returned by with_price_tag(...) or with_dynamic_price(...).
The Real Fix
Chain resource/description/mime type on the LayerBuilder:
let app: Router = Router::new().route(
"/custom-config",
get(handler).layer(
x402.with_price_tag(V2Eip155Exact::price_tag(
address!("..."),
USDC::base_sepolia().amount(10u64),
))
.with_description("Premium API access".to_string())
.with_mime_type("application/json".to_string())
),
);Verified: compiles successfully in x402-production-server.
---
Issue 4: price_tag does NOT accept a plain String for amount
Location: SKILL.md lines 168–173, references/server-guide.md lines 71–76 Severity: Compilation error Status: price_tag requires DeployedTokenAmount
The Bug
The skill claims:
V2Eip155Exact::price_tag(
address!("0xTokenContractAddress"),
"1000000".to_string(), // amount in smallest token unit
)This produces:
error[E0308]: arguments to this function are incorrect
expected `DeployedTokenAmount<Uint<256, 4>, Eip155TokenDeployment>`, found `String`The second argument must be a DeployedTokenAmount, obtained only from:
USDC::base_sepolia().amount(1000000u64)USDC::base_sepolia().parse("0.01").unwrap()
The Real Fix
Use amount() or parse() on the Eip155TokenDeployment type from USDC:
V2Eip155Exact::price_tag(
address!("0x..."),
USDC::base_sepolia().amount(1000000u64),
)Verified: correct signature confirmed by inspecting x402-chain-eip155/src/v2_eip155_exact/server.rs:44.
---
Issue 5: PaymentSelector trait has different signatures and PaymentCandidate fields are public
Location: references/client-guide.md lines 147–173 Severity: Compilation error Status: Fixed in production client
The Bugs
1. select takes a lifetime: fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> – the skill omits lifetimes. 2. amount and chain_id are public fields, not methods. Calling c.amount() or c.network() fails:
error[E0599]: no method named `amount` found for reference `&&PaymentCandidate`
error[E0599]: no method named `network` found for reference `&&PaymentCandidate`The Real Fix
struct CheapestSelector;
impl PaymentSelector for CheapestSelector {
fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
candidates.iter().min_by_key(|c| c.amount) // field, not method
}
}
struct ChainPreferenceSelector { preferred_chain: String }
impl PaymentSelector for ChainPreferenceSelector {
fn select<'a>(&self, candidates: &'a [PaymentCandidate]) -> Option<&'a PaymentCandidate> {
candidates.iter()
.find(|c| c.chain_id.to_string().contains(&self.preferred_chain))
.or_else(|| candidates.first())
}
}Verified: all client tests pass in x402-production-client.
---
Issue 6: SchemeRegistry::build signature differs from skill example
Location: references/facilitator-guide.md lines 199–246 Severity: Compilation error Status: Fixed in production facilitator
The Bugs
1. SchemeRegistry::build is not fallible (returns Self, not Result<Self, ...>); 2. It needs a ChainRegistry<P> where P: ChainProviderOps — using () will not satisfy this bound unless you provide a real provider like Eip155ChainProvider. 3. SchemeConfig::chains is ChainIdPattern, parsed with "eip155:*".parse()?. 4. SchemeRegistry has no ::new(); use Default::default() for an empty registry, or SchemeRegistry::build(...) for a populated one.
The Real Fix (minimal compilation test)
use x402_types::chain::ChainRegistry;
use x402_types::scheme::SchemeRegistry;
use x402_chain_eip155::Eip155ChainProvider;
let chain_registry: ChainRegistry<Eip155ChainProvider> = ChainRegistry::new(HashMap::new());
let blueprints = SchemeBlueprints::new()
.and_register(V1Eip155Exact)
.and_register(V2Eip155Exact);
let config: Vec<SchemeConfig> = vec![];
let scheme_registry = SchemeRegistry::build(chain_registry, blueprints, &config);Verified: x402-production-facilitator compiles.
---
Issue 7: Solana dependency conflict breaks compilation
Location: SKILL.md lines 131–153 (multi-chain with Solana) Severity: Compilation error Status: Confirmed upstream issue; workaround documented
The Bug
Adding x402-chain-solana pulls spl-token-2022 v10.0.0, which has an incompatible API with solana-native-token v0.1.0:
error[E0308]: expected `&OptionalNonZeroPubkey`, found `&MaybeNull<Pubkey>`
--> spl-token-2022-10.0.0/src/extension/token_group/processor.rsThe Real Fix
The ecosystem conflict exists on crates.io as of April 2026. Workarounds:
- Use the upstream GitHub repo directly (
git = "https://github.com/x402-rs/x402-rs") and its pinnedCargo.lock. - In standalone crates.io projects, omit Solana support and document the limitation.
Verified: EVM-only multi-chain client works perfectly.
---
Issue 8: Skill lists crate version "1.0" when actual latest is 1.4.6
Location: All Cargo.toml snippets across the skill Severity: Non-breaking (semver compatible) Status: Noted for accuracy
The skill uses "1.0" which resolves to 1.4.6 because of semver compatibility. However, for documentation accuracy, the latest published versions as of April 2026 are:
| Crate | Latest |
|---|---|
x402-axum | 1.4.6 |
x402-reqwest | 1.4.6 |
x402-chain-eip155 | 1.4.6 |
x402-chain-solana | 1.4.6 |
x402-chain-aptos | 1.4.6 (git-only) |
x402-types | 1.4.6 |
x402-facilitator-local | 1.4.6 |
x402-facilitator | not on crates.io |
---
Verified Working Patterns (tested in production workspace)
| Pattern | Crate / Feature | Status |
|---|---|---|
| Static pricing server | x402-axum + x402-chain-eip155/server | Works |
Dynamic pricing (with_dynamic_price) | x402-axum + x402-chain-eip155/server | Works |
| Conditional free access (empty vec) | x402-axum | Works |
settle_before_execution() | x402-axum | Works |
settle_after_execution() | x402-axum | Works (default) |
with_supported_cache_ttl on middleware | x402-axum | Works |
with_description, with_mime_type on LayerBuilder | x402-axum | Works |
USDC::base_sepolia().amount(u64) | x402-chain-eip155 + KnownNetworkEip155 | Works |
USDC::base_sepolia().parse("0.01") | x402-chain-eip155 + KnownNetworkEip155 | Works |
USDC::base(), polygon(), avalanche(), sei(), xdc() etc. | x402-chain-eip155 + KnownNetworkEip155 | Works |
| Single-chain EVM client | x402-reqwest + x402-chain-eip155/client | Works |
| Multi-chain EVM client (V1 + V2) | x402-reqwest + x402-chain-eip155/client | Works |
Custom PaymentSelector implementation | x402-reqwest / x402-types | Works (with lifetime annotation + fields) |
| FacilitatorLocal creation | x402-facilitator-local | Works |
handlers::routes() | x402-facilitator-local | Works |
| Docker facilitator runtime | ghcr.io/x402-rs/x402-facilitator:latest | Works – tested live |
E2E: server returns 402 + Payment-Required header | Real facilitator over internet | Verified |
| E2E: paying client signs and retries | Real facilitator over internet | Verified (rejected due to 0 USDC balance, which is expected) |
---
All tests were executed in a real workspace with actual crate dependencies from crates.io, running real binaries that talk to the live public facilitator at `https://facilitator.x402.rs`.
x402-skills
A comprehensive skill for the LLM that enables building solutions with the x402 protocol and the x402-rs Rust library. The x402 protocol activates the long-dormant HTTP 402 Payment Required status code to enable blockchain payments directly through HTTP — the client never touches the blockchain, and the facilitator handles all on-chain operations.
Overview
With this skill, the LLM developer can:
- Create paid API services — Protect Axum routes behind blockchain micropayments using
x402-axummiddleware. Accept any ERC-20 token, SPL token, or Aptos coin across EVM, Solana, and Aptos blockchains. - Build paying agents — Create
reqwestclients that transparently handle x402 payments when accessing paid data endpoints, with automatic signature generation and retry logic. - Run and customize facilitators — Deploy production facilitators via Docker or from source, or build fully custom facilitators with programmatic verify/settle access.
- Implement gasless payments — Leverage the full gasless stack: EIP-3009 (preferred), Permit2 (universal fallback), and EIP-2612 gas sponsoring.
- Support smart wallets — Handle EOA, EIP-1271 (deployed smart wallets), and EIP-6492 (counterfactual wallets) seamlessly.
- Work with any token — Not limited to USDC. Any ERC-20 token, SPL token, or native coin can be used as payment.
Installation
npx skills add melonask/x402-skillsFile Structure
x402/
├── SKILL.md # Main entry point (336 lines)
│ ├── Protocol overview & 3-actor diagram
│ ├── Crate architecture table
│ ├── Quick-start routing guide
│ ├── Minimal working examples (server, client, multi-chain)
│ ├── Key concepts (any token, gasless, V1 vs V2, price tag types)
│ ├── Installation patterns (server, client, facilitator)
│ ├── Common patterns (dynamic pricing, multi-chain, settlement timing)
│ └── Reference file routing
│
└── references/
├── server-guide.md # Axum middleware — protecting routes (294 lines)
├── client-guide.md # Reqwest middleware — auto-paying agents (253 lines)
├── facilitator-guide.md # Facilitator setup & custom builds (358 lines)
├── chain-config.md # Networks, tokens, RPC, feature flags (250 lines)
├── protocol-details.md # V1/V2, gasless stack, smart wallets (365 lines)
└── custom-scheme-guide.md # Custom schemes & EIP-7702 implementation (150 lines)The skill uses progressive disclosure: the SKILL.md main file stays concise (under 500 lines) while detailed guides are organized into domain-specific reference files that are loaded on demand.
Supported Blockchains
| Chain | Networks | Protocol Versions | Payment Methods |
|---|---|---|---|
| EVM (EIP-155) | Base, Polygon, Avalanche, Sei, Celo, XDC, XRPL EVM, Peaq, IoTeX, and more | V1 + V2 | EIP-3009, Permit2 |
| Solana | Mainnet, Devnet | V1 + V2 | SPL Token pre-signed transfer |
| Aptos | Mainnet, Testnet | V2 | Fungible asset transfer (sponsored) |
Quick Examples
Protect a Route (Server)
use x402_axum::X402Middleware;
use x402_chain_eip155::{V2Eip155Exact, KnownNetworkEip155};
use x402_types::networks::USDC;
let x402 = X402Middleware::new("https://facilitator.x402.rs");
let app = Router::new().route(
"/paid-content",
get(handler).layer(
x402.with_price_tag(V2Eip155Exact::price_tag(
address!("0xYourWallet"),
USDC::base_sepolia().amount(10u64),
))
),
);Auto-Pay for Data (Client)
use x402_reqwest::{X402Client, ReqwestWithPayments, ReqwestWithPaymentsBuild};
use x402_chain_eip155::V2Eip155ExactClient;
let client = Client::new()
.with_payments(
X402Client::new()
.register(V2Eip155ExactClient::new(signer))
)
.build();
// Payments handled transparently
let res = client.get("https://api.example.com/protected").send().await?;Run a Facilitator (Docker)
docker run -v $(pwd)/config.json:/app/config.json -p 8080:8080 \
ghcr.io/x402-rs/x402-facilitatorx402-rs Crate Overview
| Crate | Published | Purpose |
|---|---|---|
x402-types | crates.io | Core types, facilitator traits, CAIP-2 chain IDs, wire format (V1/V2) |
x402-axum | crates.io | Axum middleware for protecting routes with payments |
x402-reqwest | crates.io | Reqwest middleware for transparent payment handling |
x402-facilitator-local | crates.io | Local facilitator implementation |
x402-chain-eip155 | crates.io | EVM/EIP-155 chain support |
x402-chain-solana | crates.io | Solana chain support |
x402-chain-aptos | git-only | Aptos chain support |
x402-facilitator | git-only | Production facilitator binary |
Protocol Highlights
- Gasless for clients — The client only produces an off-chain cryptographic signature. The facilitator pays gas.
- Trust-minimized — The facilitator cannot modify the payment amount or destination (locked by the client's signature).
- Any token — Works with any ERC-20 token, SPL token, or fungible asset, not just USDC.
- Zero protocol fees — Only nominal payment network fees.
- V1 and V2 — Full backward compatibility; V2 adds CAIP-2 chain IDs, multi-chain, and extensible schemes.
References
- x402 Protocol Specification
- x402-rs GitHub Repository
- Coinbase x402 Documentation
- CAIP-2 Chain IDs
- EIP-3009: transferWithAuthorization
- Permit2 by Uniswap
License
This skill documentation is provided for use with the x402 protocol ecosystem. The x402-rs library itself is licensed under Apache-2.0.
Chain Configuration: Networks, Tokens, and RPC Setup
This reference covers all supported blockchain networks, token addresses, RPC configuration, feature flags, and environment variables for each chain.
Supported Networks
EVM Chains (EIP-155)
| Network | Chain ID | CAIP-2 ID | Name in x402-rs | USDC Address |
|---|---|---|---|---|
| Base | 8453 | eip155:8453 | USDC::base() | Built-in |
| Base Sepolia | 84532 | eip155:84532 | USDC::base_sepolia() | Built-in |
| Polygon | 137 | eip155:137 | USDC::polygon() | Built-in |
| Polygon Amoy | 80002 | eip155:80002 | USDC::polygon_amoy() | Built-in |
| Avalanche C-Chain | 43114 | eip155:43114 | USDC::avalanche() | Built-in |
| Avalanche Fuji | 43113 | eip155:43113 | USDC::avalanche_fuji() | Built-in |
| Sei | 1329 | eip155:1329 | USDC::sei() | Built-in |
| Sei Testnet | — | eip155:* | USDC::sei_testnet() | Built-in |
| XDC Network | — | eip155:* | USDC::xdc() | Built-in |
| XRPL EVM | — | eip155:* | USDC::xrpl_evm() | Built-in |
| Peaq | — | eip155:* | USDC::peaq() | Built-in |
| IoTeX | — | eip155:* | USDC::iotex() | Built-in |
| Celo | — | eip155:* | USDC::celo() | Built-in |
| Celo Sepolia | — | eip155:* | USDC::celo_sepolia() | Built-in |
Solana
| Network | CAIP-2 ID | Name in x402-rs |
|---|---|---|
| Solana Mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | USDC::solana() |
| Solana Devnet | solana:EtWTRABZaYq6iM94YHpEqi5Swv8aLxwG3TpvNsw7qjjp | USDC::solana_devnet() |
Aptos
| Network | CAIP-2 ID | Name in x402-rs |
|---|---|---|
| Aptos Mainnet | aptos:1 | USDC::aptos() |
| Aptos Testnet | aptos:2 | USDC::aptos_testnet() |
Using the USDC Convenience Struct
The USDC struct (from x402_types::networks) provides pre-configured token deployments:
use x402_types::networks::USDC;
use x402_chain_eip155::{V2Eip155Exact, KnownNetworkEip155};
// Get USDC deployment info for a specific network
let usdc_base = USDC::base(); // Returns Eip155TokenDeployment for Base Mainnet
let usdc_sepolia = USDC::base_sepolia(); // Returns Eip155TokenDeployment for Base Sepolia
// Get the amount in smallest units
let amount = usdc_base.amount(1_000_000u64); // 1.0 USDC (6 decimals)
// Parse from decimal string
let amount = usdc_sepolia.parse("0.01").unwrap(); // 10000 smallest units
// Use in a price tag
let price_tag = V2Eip155Exact::price_tag(
address!("0xYourWallet"),
usdc_base.amount(100),
);Solana USDC
use x402_types::networks::USDC;
use x402_chain_solana::V2SolanaExact;
let usdc_sol = USDC::solana();
let price_tag = V2SolanaExact::price_tag(
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM".to_string(), // pay-to address
usdc_sol.amount(1_000_000), // 1.0 USDC
);Using Any Token (Not Just USDC)
The protocol supports any ERC-20 token. Pass the token contract address directly:
use x402_chain_eip155::{V2Eip155Exact, KnownNetworkEip155};
use alloy_primitives::address;
// Accept DAI payments on Base Sepolia
let price_tag = V2Eip155Exact::price_tag(
address!("0xDA898..."), // Your wallet address (pay_to)
"1000000000000000000".to_string(), // 1 DAI (18 decimals)
);For this to work, the server must include the token contract address in the payment requirements. On the client side, the facilitator will handle the specific transfer mechanism based on what the token supports (EIP-3009 or Permit2).
Chain-Specific Config for Facilitator
EVM Chain Config
{
"eip155:8453": {
"eip1559": true,
"flashblocks": false,
"receipt_timeout_secs": 30,
"signers": ["$FACILITATOR_KEY"],
"rpc": [
{
"http": "https://mainnet.base.org",
"rate_limit": 100
},
{
"http": "$BACKUP_RPC",
"rate_limit": 50
}
]
}
}Parameters:
eip1559— Use EIP-1559 gas model (default:true)flashblocks— Enable flashblocks for faster confirmation (default:false)receipt_timeout_secs— Max wait for transaction receipt (default:30)signers— Array of hex-encoded private keys. Multiple signers enable round-robin load distribution.rpc— Array of RPC configs withhttpURL and optionalrate_limit(requests per second)
Solana Chain Config
{
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": {
"signers": ["$SOLANA_PRIVATE_KEY"],
"rpc": [{ "http": "https://api.mainnet-beta.solana.com" }],
"pubsub": "wss://api.mainnet-beta.solana.com"
}
}Parameters:
signers— Array of base58-encoded 64-byte Solana private keysrpc— Array of HTTP RPC endpointspubsub— Optional WebSocket endpoint for faster transaction confirmations via signature subscriptions
Aptos Chain Config
{
"aptos:1": {
"sponsor_gas": true,
"signer": "$APTOS_FACILITATOR_KEY",
"rpc": "https://fullnode.mainnet.aptoslabs.com/v1",
"api_key": "$APTOS_API_KEY"
}
}Parameters:
sponsor_gas— Whether facilitator pays gas (default:false)signer— Hex-encoded Ed25519 private keyrpc— Aptos REST API endpointapi_key— Optional API key for rate-limited endpoints
Environment Variable Resolution
Config values support transparent environment variable references:
{
"http": "https://mainnet.base.org", // Literal string
"http": "$RPC_URL", // Simple env var reference
"http": "${RPC_URL}", // Braced env var reference
"signers": ["$FACILITATOR_PRIVATE_KEY"] // Resolved from environment
}The LiteralOrEnv<T> wrapper:
- Stores the original variable name alongside the resolved value
Displayreconstructs$VAR_NAMEsyntax (prevents leaking secrets in logs)- Supports config round-tripping (save and reload without exposing values)
Top-Level Environment Variables
| Variable | Description | Default |
|---|---|---|
HOST | Server bind address | 0.0.0.0 |
PORT | Server port | 8080 |
CONFIG | Path to config file | config.json |
OTEL_EXPORTER_OTLP_ENDPOINT | OpenTelemetry collector | — |
OTEL_SERVICE_NAME | Service name | — |
Feature Flags by Crate
x402-chain-eip155
| Feature | Description |
|---|---|
server | Server-side price tag generation |
client | Client-side payment signing |
facilitator | Facilitator-side verify/settle |
telemetry | OpenTelemetry tracing |
x402-chain-solana
| Feature | Description |
|---|---|
server | Server-side price tag generation |
client | Client-side payment signing |
facilitator | Facilitator-side verify/settle |
telemetry | OpenTelemetry tracing |
x402-chain-aptos
| Feature | Description |
|---|---|
facilitator | Facilitator-side verify/settle |
telemetry | OpenTelemetry tracing |
Note: Aptos requires git-only dependencies and must include patches in Cargo.toml.
x402-types
| Feature | Description |
|---|---|
cli | CLI argument parsing via clap |
telemetry | Tracing instrumentation |
Workspace Dependencies
The root Cargo.toml defines shared versions (workspace v1.4.6, Rust edition 2024, MSRV 1.88.0):
[workspace.dependencies]
x402-types = { version = "1.0", path = "crates/x402-types" }
x402-axum = { version = "1.0", path = "crates/x402-axum" }
x402-reqwest = { version = "1.0", path = "crates/x402-reqwest" }
x402-facilitator-local = { version = "1.0", path = "crates/x402-facilitator-local" }
x402-chain-eip155 = { version = "1.0", path = "crates/chains/x402-chain-eip155" }
x402-chain-solana = { version = "1.0", path = "crates/chains/x402-chain-solana" }
x402-chain-aptos = { version = "1.0", path = "crates/chains/x402-chain-aptos" }Key dependency versions:
alloy-primitives1.4.1 — EVM types and token amountsaxum0.8 — HTTP frameworkreqwest0.13 — HTTP clienttokio1.35 — Async runtimeserde1.0 — Serializationtower0.5 — Service trait and layers
Client-Side Guide: Building Agents That Pay for Data
This guide covers using x402-reqwest to build Rust clients that automatically handle x402 payments. The middleware detects 402 Payment Required responses, signs payments, and retries — all transparently.
Installation
[dependencies]
x402-reqwest = { version = "1.0", features = ["json"] }
x402-chain-eip155 = { version = "1.0", features = ["client"] }
x402-chain-solana = { version = "1.0", features = ["client"] }
alloy-signer-local = "1.4"
reqwest = { version = "0.13", features = ["json"] }
tokio = { version = "1", features = ["full"] }Basic Usage: Single-Chain Client
use x402_reqwest::{ReqwestWithPayments, ReqwestWithPaymentsBuild, X402Client};
use x402_chain_eip155::V2Eip155ExactClient;
use alloy_signer_local::PrivateKeySigner;
use std::sync::Arc;
use reqwest::Client;
let signer: Arc<PrivateKeySigner> = Arc::new("0xYOUR_PRIVATE_KEY".parse()?);
let x402_client = X402Client::new()
.register(V2Eip155ExactClient::new(signer));
let client = Client::new()
.with_payments(x402_client)
.build();
// Transparent payment handling
let response = client
.get("https://api.example.com/protected")
.send()
.await?;When the server returns 402 Payment Required, the middleware:
1. Parses the payment requirements from the response 2. Finds a registered scheme client that can handle the payment 3. Signs the payment payload using the scheme client 4. Retries the request with the Payment-Signature header
Scheme Client Registration
Register scheme clients for each blockchain protocol version you want to support:
EVM Clients
use x402_chain_eip155::{V1Eip155ExactClient, V2Eip155ExactClient};
let x402_client = X402Client::new()
.register(V1Eip155ExactClient::new(evm_signer.clone()))
.register(V2Eip155ExactClient::new(evm_signer));V1Eip155ExactClient— Handles V1 protocol payments on EVM chainsV2Eip155ExactClient— Handles V2 protocol payments on EVM chains
Both require an Arc<PrivateKeySigner> (from alloy_signer_local).
Solana Clients
use x402_chain_solana::{V1SolanaExactClient, V2SolanaExactClient};
use solana_keypair::Keypair;
use solana_client::nonblocking::rpc_client::RpcClient;
let keypair = Arc::new(Keypair::from_base58_string("YOUR_SOLANA_PRIVATE_KEY"));
let rpc_client = Arc::new(RpcClient::new("https://api.mainnet-beta.solana.com"));
let x402_client = X402Client::new()
.register(V1SolanaExactClient::new(keypair.clone(), rpc_client.clone()))
.register(V2SolanaExactClient::new(keypair, rpc_client));Both require an Arc<Keypair> and an Arc<RpcClient>.
Multi-Chain Client (EVM + Solana)
Build an agent that can pay on any supported chain:
use x402_reqwest::{X402Client, ReqwestWithPayments, ReqwestWithPaymentsBuild};
use x402_chain_eip155::{V1Eip155ExactClient, V2Eip155ExactClient};
use x402_chain_solana::{V1SolanaExactClient, V2SolanaExactClient};
use alloy_signer_local::PrivateKeySigner;
use solana_keypair::Keypair;
use solana_client::nonblocking::rpc_client::RpcClient;
use std::sync::Arc;
use reqwest::Client;
// EVM setup
let evm_signer: Arc<PrivateKeySigner> = Arc::new("0x...".parse()?);
// Solana setup
let sol_kp = Arc::new(Keypair::from_base58_string("..."));
let sol_rpc = Arc::new(RpcClient::new("https://api.mainnet-beta.solana.com"));
// Register all scheme clients
let x402_client = X402Client::new()
.register(V1Eip155ExactClient::new(evm_signer.clone()))
.register(V2Eip155ExactClient::new(evm_signer))
.register(V1SolanaExactClient::new(sol_kp.clone(), sol_rpc.clone()))
.register(V2SolanaExactClient::new(sol_kp, sol_rpc));
// Build the client
let client = Client::new()
.with_payments(x402_client)
.build();
// Use normally — x402 is handled transparently
let data: serde_json::Value = client
.get("https://premium-api.example.com/data")
.send()
.await?
.json()
.await?;Payment Selection
When the server offers multiple payment options (e.g., different chains or tokens), X402Client uses a PaymentSelector to choose which to pay.
Default: FirstMatch
The default selector picks the first matching registered scheme:
let x402_client = X402Client::new()
.register(V2Eip155ExactClient::new(evm_signer))
.register(V2SolanaExactClient::new(sol_kp, sol_rpc));
// Will try EVM first, then SolanaCustom Payment Selector
Implement the PaymentSelector trait for custom logic:
use x402_reqwest::X402Client;
use x402_types::scheme::client::{PaymentSelector, PaymentCandidate};
struct CheapestSelector;
impl PaymentSelector for CheapestSelector {
fn select(&self, candidates: &[PaymentCandidate]) -> Option<&PaymentCandidate> {
// Pick the payment option with the lowest amount
candidates.iter().min_by_key(|c| c.amount())
}
}
struct ChainPreferenceSelector {
preferred_chain: String,
}
impl PaymentSelector for ChainPreferenceSelector {
fn select(&self, candidates: &[PaymentCandidate]) -> Option<&PaymentCandidate> {
// Prefer a specific chain
candidates.iter().find(|c| c.network().contains(&self.preferred_chain))
.or_else(|| candidates.first())
}
}
let x402_client = X402Client::new()
.register(V2Eip155ExactClient::new(evm_signer))
.with_selector(CheapestSelector);How It Works: The Payment Flow
1. The client makes a normal HTTP request to a protected endpoint 2. If the response is 402 Payment Required:
- The middleware parses the
Payment-Requiredheader (V2) or response body (V1) - It extracts the list of
PaymentRequirements - It iterates through registered scheme clients to find ones that can handle the payment
- The
PaymentSelectorpicks the best option - The selected scheme client signs the payment payload
3. The middleware retries the original request with the Payment-Signature header 4. If the server verifies the payment, it returns 200 OK with the content
V2 Payment Header
Payment-Signature: <base64-encoded PaymentPayload>V1 Payment Header
X-PAYMENT: <base64-encoded PaymentPayload>The middleware automatically detects which protocol version the server uses and responds accordingly.
Building an AI Agent That Pays for Data
Here's a practical example of an agent that fetches paid data:
use x402_reqwest::{X402Client, ReqwestWithPayments, ReqwestWithPaymentsBuild};
use x402_chain_eip155::V2Eip155ExactClient;
use alloy_signer_local::PrivateKeySigner;
use std::sync::Arc;
use reqwest::Client;
async fn fetch_paid_market_data() -> Result<String, Box<dyn std::error::Error>> {
let signer: Arc<PrivateKeySigner> = Arc::new("0x...".parse()?);
let client = Client::new()
.with_payments(
X402Client::new()
.register(V2Eip155ExactClient::new(signer))
)
.build();
// This endpoint requires x402 payment — handled transparently
let response = client
.get("https://premium-api.example.com/market-data?symbol=BTC")
.send()
.await?;
let data: serde_json::Value = response.json().await?;
Ok(serde_json::to_string_pretty(&data)?)
}Telemetry
Enable the telemetry feature for structured tracing:
x402-reqwest = { version = "1.0", features = ["telemetry", "json"] }Tracing spans emitted:
x402.reqwest.handle— Entire middleware handling (402 detection + retry)x402.reqwest.next— Underlying HTTP request (initial and retry)x402.reqwest.make_payment_headers— Payment signingx402.reqwest.parse_payment_required— 402 response parsing
Important Notes
- The client never pays gas. All on-chain operations are handled by the facilitator.
- The client's private key is only used for signing payment authorizations off-chain.
- EVM payments use EIP-712 typed data signing (for EIP-3009) or Permit2 signatures.
- Solana payments sign a serialized
VersionedTransaction. - Payments are one-time use — each nonce can only be used once, preventing replay attacks.
Custom Scheme Guide: Building Extensions (e.g., EIP-7702)
The x402 protocol is heavily extensible. If the built-in exact or upto schemes do not fit your use case, you can define Custom Schemes. A scheme dictates the lifecycle of a payment: how the server declares the price, how the client authorizes it, and how the facilitator verifies and settles it on-chain.
This guide walks through building a complete custom scheme: `v2-eip155-eip7702`. This scheme leverages EIP-7702 to allow a user's Externally Owned Account (EOA) to temporarily act as a smart contract (delegate) and execute a gasless transaction sponsored by the Facilitator. Smart contract Delegate.sol the exact same address (0xD064939e706dC03699dB7Fe58bB0553afDF39fDd) deployed across Ethereum Mainnet, L2s (Base, Optimism, Arbitrum), and alternative L1s (Polygon, BNB, Avalanche).
1. Scheme Definition & Types
First, define the scheme identifier and the wire-format types for the Requirements (what the server asks for) and the Payload (what the client sends back).
use alloy_primitives::{Address, Bytes, U256};
use alloy_eips::eip7702::SignedAuthorization;
use serde::{Deserialize, Serialize};
use x402_types::scheme::X402SchemeId;
pub struct V2Eip155Eip7702;
impl X402SchemeId for V2Eip155Eip7702 {
fn x402_version(&self) -> u8 { 2 }
fn namespace(&self) -> &str { "eip155" }
fn scheme(&self) -> &str { "eip7702" }
}
/// Server-side: Declared in the 402 Payment Required header
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip7702Requirements {
pub token: Address,
pub amount: U256,
pub payee: Address,
pub required_delegate: Address, // The EIP-7702 contract to delegate to
}
/// Client-side: Attached to the retried request
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip7702Payload {
pub sender: Address,
pub authorization: SignedAuthorization,
pub calldata: Bytes, // Execution intent (e.g., transfer(payee, amount))
}2. Server-Side: Generating the Price Tag
To protect Axum routes with your new scheme, implement a helper that generates a v2::PriceTag. The server embeds your custom Eip7702Requirements into the JSON wire format.
use x402_types::proto::v2::{PriceTag, PaymentRequirements};
impl V2Eip155Eip7702 {
pub fn price_tag(
network: String, // e.g., "eip155:84532"
token: Address,
amount: U256,
payee: Address,
required_delegate: Address,
) -> PriceTag {
let reqs = Eip7702Requirements { token, amount, payee, required_delegate };
PriceTag {
requirements: PaymentRequirements {
scheme: "eip7702".to_string(),
network: network.parse().unwrap(),
amount: amount.to_string(),
asset: token.to_string(),
pay_to: payee.to_string(),
max_timeout_seconds: 300,
// Embed custom scheme constraints
extra: Some(serde_json::to_value(reqs).unwrap()),
},
enricher: None,
}
}
}3. Facilitator-Side: Verify and Settle
The facilitator acts as the trustless verifier and gas sponsor. Implement X402SchemeFacilitator to handle the off-chain verification and the Type 4 (EIP-7702) on-chain settlement.
use x402_types::facilitator::{FacilitatorError, X402SchemeFacilitator};
use alloy_rpc_types::TransactionRequest;
use alloy_provider::Provider;
use async_trait::async_trait;
pub struct Eip7702Facilitator<P> {
pub provider: P,
}
#[async_trait]
impl<P: Provider + Send + Sync> X402SchemeFacilitator for Eip7702Facilitator<P> {
type Requirements = Eip7702Requirements;
type Payload = Eip7702Payload;
async fn verify(&self, reqs: &Self::Requirements, payload: &Self::Payload) -> Result<(), FacilitatorError> {
// 1. Verify EIP-7702 delegation target matches the merchant's requirement
if payload.authorization.address() != &reqs.required_delegate {
return Err(FacilitatorError::VerificationFailed("Invalid delegate contract".into()));
}
// 2. Recover the signature to ensure the sender authorized the delegation
let recovered = payload.authorization.recover_authority()
.map_err(|_| FacilitatorError::VerificationFailed("Invalid signature".into()))?;
if recovered != payload.sender {
return Err(FacilitatorError::VerificationFailed("Signer mismatch".into()));
}
Ok(())
}
async fn settle(&self, _reqs: &Self::Requirements, payload: &Self::Payload) -> Result<String, FacilitatorError> {
// Construct the EIP-7702 Type 4 transaction
// The facilitator pays gas, executing the calldata against the delegated EOA
let tx = TransactionRequest::default()
.with_to(payload.sender)
.with_call_data(payload.calldata.clone())
.with_authorization_list(vec![payload.authorization.clone()]);
let pending = self.provider.send_transaction(tx).await
.map_err(|e| FacilitatorError::SettlementFailed(e.to_string()))?;
let receipt = pending.get_receipt().await
.map_err(|e| FacilitatorError::SettlementFailed(e.to_string()))?;
Ok(receipt.transaction_hash.to_string())
}
}_Note: You must also implement X402SchemeFacilitatorBuilder to register this with the SchemeRegistry (see references/facilitator-guide.md)._
4. Client-Side: Automatically Paying (Reqwest)
To make your Rust AI agents capable of paying via your custom scheme, implement the PaymentClient trait. The client intercepts the requirements, uses Alloy to sign the EIP-7702 authorization tuple, and builds the payload.
use x402_types::scheme::client::{PaymentClient, PaymentCandidate};
use x402_types::proto::v2::{PaymentRequired, PaymentPayload, PayloadData};
use alloy_signer::Signer;
use alloy_network::Ethereum;
pub struct V2Eip155Eip7702Client<S> {
signer: S,
}
impl<S: Signer + Send + Sync> PaymentClient for V2Eip155Eip7702Client<S> {
fn accept(&self, required: &PaymentRequired) -> Vec<PaymentCandidate> {
let mut candidates = Vec::new();
for req in &required.payment_requirements {
if req.scheme == "eip7702" && req.network.namespace() == "eip155" {
// Parse the extra EIP-7702 requirements
let custom_reqs: Eip7702Requirements = serde_json::from_value(req.extra.clone().unwrap()).unwrap();
// Sign the EIP-7702 Authorization tuple (mock async context)
// In a real client, this logic runs inside the async payload builder
let auth = self.signer.sign_auth_tuple(custom_reqs.required_delegate).await.unwrap();
// Build execution calldata (e.g., triggering the transfer)
let calldata = build_calldata(&custom_reqs);
let payload = Eip7702Payload {
sender: self.signer.address(),
authorization: auth,
calldata,
};
let payment_payload = PaymentPayload {
x402_version: 2,
resource: required.resource.clone(),
accepted: req.clone(),
payload: PayloadData::Custom(serde_json::to_value(payload).unwrap()),
};
candidates.push(PaymentCandidate::new(req.clone(), payment_payload));
}
}
candidates
}
}Summary
By breaking out the logic into these four components (Types, PriceTag, Facilitator, Client), you can drop absolutely any blockchain interaction logic into the x402-rs ecosystem. The Axum and Reqwest middlewares will automatically handle the HTTP lifecycle, allowing you to focus entirely on the cryptographic scheme.
Facilitator Guide: Verification, Settlement, and Custom Facilitators
The facilitator is the middleware between HTTP and the blockchain. It verifies that a client's payment signature is valid, checks balances, and settles the payment on-chain. This guide covers running a production facilitator and building custom ones.
What a Facilitator Does
1. Verify (POST /verify) — Validates the payment signature, checks balances, confirms parameters match requirements 2. Settle (POST /settle) — Broadcasts the transaction on-chain and waits for confirmation 3. Supported (GET /supported) — Lists supported (scheme, chain) pairs
The facilitator cannot modify the payment amount or destination — these are locked by the client's signature.
HTTP Endpoints
| Endpoint | Method | Description |
|---|---|---|
/ | GET | Server greeting |
/verify | GET | Schema information for verify endpoint |
/verify | POST | Verify a payment payload |
/settle | GET | Schema information for settle endpoint |
/settle | POST | Settle a verified payment on-chain |
/supported | GET | List supported payment schemes and networks |
/health | GET | Health check (delegates to /supported) |
Running a Production Facilitator
Option 1: Docker (Recommended)
Prebuilt images are at GitHub Container Registry:
docker run -v $(pwd)/config.json:/app/config.json -p 8080:8080 \
ghcr.io/x402-rs/x402-facilitatorOption 2: Build from Source
# Clone the repo
git clone https://github.com/x402-rs/x402-rs.git
cd x402-rs
# Run with all chains + telemetry
cargo run --package x402-facilitator --features full
# Run with specific chains only
cargo run --package x402-facilitator --features chain-eip155,chain-solana
# With custom config
cargo run --package x402-facilitator -- --config /path/to/config.jsonFeature Flags
| Feature | Description |
|---|---|
telemetry | OpenTelemetry tracing and metrics |
chain-eip155 | EVM/EIP-155 chain support |
chain-solana | Solana chain support |
chain-aptos | Aptos support (requires patches) |
full | All features enabled |
Facilitator Configuration
Complete config.json Example
{
"port": 8080,
"host": "0.0.0.0",
"chains": {
"eip155:84532": {
"_comment": "Base Sepolia",
"eip1559": true,
"flashblocks": true,
"receipt_timeout_secs": 30,
"signers": ["0xFACILITATOR_PRIVATE_KEY"],
"rpc": [
{
"http": "https://sepolia.base.org",
"rate_limit": 50
},
{
"http": "https://base-sepolia.g.alchemy.com/v2/YOUR_KEY",
"rate_limit": 100
}
]
},
"eip155:8453": {
"_comment": "Base Mainnet",
"eip1559": true,
"flashblocks": false,
"signers": ["$FACILITATOR_PRIVATE_KEY"],
"rpc": [
{
"http": "https://mainnet.base.org",
"rate_limit": 100
}
]
},
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": {
"_comment": "Solana Mainnet",
"signers": ["$SOLANA_PRIVATE_KEY"],
"rpc": [
{
"http": "https://api.mainnet-beta.solana.com"
}
]
}
},
"schemes": [
{
"id": "v1-eip155-exact",
"chains": "eip155:*"
},
{
"id": "v2-eip155-exact",
"chains": "eip155:*"
},
{
"id": "v1-solana-exact",
"chains": "solana:*"
},
{
"id": "v2-solana-exact",
"chains": "solana:*"
}
]
}Config Parameters
Top-level:
port— Server port (default:8080, env:PORT)host— Bind address (default:0.0.0.0, env:HOST)config— Path to config file (env:CONFIG, default:config.json)
*Per-chain (EVM `eip155:`):**
eip1559— Use EIP-1559 gas model (default:true)flashblocks— Enable flashblocks for faster confirmation (default:false)receipt_timeout_secs— Max seconds to wait for tx receipt (default:30)signers— Array of hex-encoded EVM private keys (first one is primary)rpc— Array of RPC endpoints withhttpURL and optionalrate_limit
*Per-chain (Solana `solana:`):**
signers— Array of base58-encoded Solana private keysrpc— Array of RPC endpointspubsub— Optional WebSocket endpoint for faster confirmations
Environment Variable Resolution
Config values support $VAR_NAME and ${VAR_NAME} syntax:
{
"http": "$RPC_URL",
"signers": ["$FACILITATOR_PRIVATE_KEY"]
}Values are resolved at load time. The LiteralOrEnv<T> type stores the original env var name and only resolves it when needed, preventing sensitive values from appearing in logs.
Multiple RPC Endpoints with Failover
The facilitator supports multiple RPC endpoints per chain with rate limiting:
{
"eip155:8453": {
"rpc": [
{ "http": "https://mainnet.base.org", "rate_limit": 100 },
{ "http": "$BACKUP_RPC_URL", "rate_limit": 50 }
]
}
}Multiple signers enable round-robin load distribution for transaction signing.
Building a Custom Facilitator
For advanced use cases, build your own facilitator using x402-facilitator-local:
[dependencies]
x402-facilitator-local = { version = "1.0", features = ["telemetry"] }
x402-chain-eip155 = { version = "1.0", features = ["facilitator"] }
x402-chain-solana = { version = "1.0", features = ["facilitator"] }
x402-types = { version = "1.0", features = ["cli"] }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde_json = "1"Complete Custom Facilitator Example
use x402_facilitator_local::{FacilitatorLocal, handlers};
use x402_types::chain::ChainRegistry;
use x402_types::scheme::{SchemeBlueprints, SchemeRegistry};
use x402_chain_eip155::{V1Eip155Exact, V2Eip155Exact};
use x402_chain_solana::{V1SolanaExact, V2SolanaExact};
use std::sync::Arc;
use axum::Router;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load chain config (from file or env)
let chains_config = /* ... */;
// Initialize chain registry with blockchain providers
let chain_registry = ChainRegistry::from_config(&chains_config).await?;
// Register supported payment schemes
let scheme_blueprints = SchemeBlueprints::new()
.and_register(V1Eip155Exact)
.and_register(V2Eip155Exact)
.and_register(V1SolanaExact)
.and_register(V2SolanaExact);
// Build the scheme registry from config
let schemes_config = /* ... */;
let scheme_registry = SchemeRegistry::build(
chain_registry,
scheme_blueprints,
&schemes_config,
);
// Create the facilitator
let facilitator = FacilitatorLocal::new(scheme_registry);
let state = Arc::new(facilitator);
// Create HTTP routes
let app = Router::new()
.merge(handlers::routes().with_state(state));
// Run the server
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}Facilitator Architecture
FacilitatorLocal
|
SchemeRegistry (routes requests to the right handler)
|
┌────┴────┐
V1Eip155 V2Eip155 V1Solana V2Solana
Exact Exact Exact Exact
(handler) (handler) (handler) (handler)
| | | |
Eip155 Eip155 Solana Solana
Provider Provider Provider Provider1. ChainRegistry manages blockchain providers and connections 2. SchemeBlueprints defines available payment schemes 3. SchemeRegistry combines chains and schemes into executable handlers 4. FacilitatorLocal routes /verify and /settle to the correct handler
Using the Facilitator Trait Directly
For programmatic use (not HTTP):
use x402_chain_eip155::{V1Eip155Exact, Eip155ChainProvider};
use x402_types::scheme::X402SchemeFacilitatorBuilder;
use x402_types::proto::v2::{VerifyRequest, SettleRequest};
// Create chain provider from config
let provider = Eip155ChainProvider::from_config(&config).await?;
// Build facilitator for a specific scheme
let facilitator = V1Eip155Exact.build(provider, None)?;
// Verify a payment
let verify_response = facilitator.verify(&verify_request).await?;
// Settle a verified payment
let settle_response = facilitator.settle(&settle_request).await?;Graceful Shutdown
use x402_facilitator_local::util::SigDown;
let sig_down = SigDown::try_new()?;
let cancellation_token = sig_down.cancellation_token();
axum::serve(listener, app)
.with_graceful_shutdown(async move {
cancellation_token.cancelled().await;
})
.await?;Handles SIGTERM and SIGINT for clean shutdown.
OpenTelemetry
use x402_facilitator_local::util::Telemetry;
let telemetry = Telemetry::new()
.with_name("x402-facilitator")
.with_version("1.0.0")
.register();
let tracing_layer = telemetry.http_tracing();
let app = Router::new()
.merge(handlers::routes().with_state(state))
.layer(tracing_layer);Set environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT— Collector endpointOTEL_SERVICE_NAME— Service name for tracesOTEL_SERVICE_VERSION— Service version
Aptos Support
Aptos requires git-only dependencies and patches:
[dependencies]
x402-chain-aptos = { version = "1.0", features = ["facilitator"] }
[patch.crates-io]
merlin = { git = "https://github.com/aptos-labs/merlin" }
[patch."https://github.com/aptos-labs/aptos-core"]
aptos-runtimes = { path = "patches/aptos-runtimes" }Aptos facilitator config:
{
"aptos:1": {
"sponsor_gas": true,
"signer": "$APTOS_FACILITATOR_KEY",
"rpc": "https://fullnode.mainnet.aptoslabs.com/v1",
"api_key": "$APTOS_API_KEY"
}
}Protocol Details: V1 vs V2, Schemes, and Gasless Payments
This reference covers the internal workings of the x402 protocol, the differences between V1 and V2, the gasless payment stack (EIP-3009, Permit2, EIP-2612), smart wallet support, and custom scheme implementation.
V1 vs V2 Protocol
V1 Protocol
Uses network names (e.g., "base-sepolia") and JSON-based communication:
- 402 response: JSON body with
acceptsarray andx402Version: "1" - Payment header:
X-PAYMENT: <base64> - Payment response: JSON body
- Network identification: String names like
"base","polygon" - Scope: Single-call, exact payments, USDC-focused
V2 Protocol
Uses CAIP-2 chain IDs and HTTP headers:
- 402 response:
Payment-Required: <base64>header - Payment header:
Payment-Signature: <base64> - Payment response:
Payment-Response: <base64>header - Network identification: CAIP-2 format like
"eip155:84532","solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" - Scope: Multi-chain, extensible schemes, any token, dynamic pricing
V2 PaymentRequired Structure
{
"x402Version": "2",
"resource": {
"url": "https://api.example.com/premium",
"description": "Premium API access",
"mimeType": "application/json"
},
"paymentRequirements": [
{
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds": 60,
"extra": {
"assetTransferMethod": "eip3009",
"name": "USDC",
"version": "2"
}
}
]
}V2 PaymentPayload Structure
{
"x402Version": 2,
"resource": {
"url": "https://api.example.com/premium-data",
"description": "Access to premium market data",
"mimeType": "application/json"
},
"accepted": {
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"maxTimeoutSeconds": 60,
"extra": {
"assetTransferMethod": "eip3009"
}
},
"payload": {
"signature": "0x2d6a7588...148b571c",
"authorization": {
"from": "0x857b06519E91e3A54538791bDbb0E22373e36b66",
"to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"value": "10000",
"validAfter": "1740672089",
"validBefore": "1740672154",
"nonce": "0xf3746613..."
}
}
}Both V1 and V2 are supported simultaneously by x402-rs. The client automatically detects which protocol the server uses.
Payment Schemes
exact Scheme
The primary scheme. Transfers a specific fixed amount. "Pay exactly $0.01 to read this article."
Built-in implementations:
| Implementation | Protocol | Chain | Transfer Method |
|---|---|---|---|
V1Eip155Exact | V1 | EVM | EIP-3009 transferWithAuthorization |
V2Eip155Exact | V2 | EVM | EIP-3009 or Permit2 |
V1SolanaExact | V1 | Solana | SPL Token pre-signed transfer |
V2SolanaExact | V2 | Solana | SPL Token pre-signed transfer |
V2AptosExact | V2 | Aptos | Fungible asset transfer |
upto Scheme (Planned)
Transfers up to a specified amount based on resources consumed. "Pay up to $0.05 for this LLM response."
Currently available for EIP-155 chains only:
V2Eip155Upto— V2 EVM upto payment (experimental)
deferred Scheme (Planned)
Supports deferred settlement and payment scheduling.
Gasless Payment Stack (EVM)
The x402 protocol provides a layered approach to gasless payments on EVM chains. The client never pays gas — the facilitator does.
Layer 1: EIP-3009 (Preferred)
Use when the token supports it (e.g., USDC on Base, Ethereum, Polygon).
EIP-3009 defines transferWithAuthorization, allowing a token holder to authorize a transfer with an off-chain signature:
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) externalFlow:
1. Client signs an EIP-712 typed TransferWithAuthorization message off-chain 2. Sends signature in Payment-Signature header 3. Facilitator calls token.transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature) on-chain 4. One-time use — each nonce can only be used once
Why preferred: Simplest flow, truly gasless for the client, single on-chain call.
Layer 2: Permit2 (Universal Fallback)
Use for any ERC-20 token that doesn't support EIP-3009.
Uses Uniswap's Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3) with the X402ExactPermit2Proxy (0x4020615294c913F045dc10f0a5cdEbd86c280001).
Phase 1: One-Time Setup
The user must approve Permit2 to spend their tokens. Three options:
- Option A: User submits
approve(Permit2)on-chain themselves (pays own gas) - Option B:
erc20ApprovalGasSponsoring— Facilitator sponsors the approval transaction - Option C:
eip2612GasSponsoring— If token supports EIP-2612permit(), user signs a permit, facilitator callsx402ExactPermit2Proxy.settleWithPermit()
Phase 2: Payment
1. Client signs a permitWitnessTransferFrom EIP-712 message with the payTo address as a "witness" 2. The witness prevents the spender (Permit2 proxy) from routing funds elsewhere 3. Facilitator verifies the signature (supports EIP-6492 and EIP-1271) 4. Facilitator validates allowance, balance, and constraints 5. Facilitator settles via X402ExactPermit2Proxy
Layer 3: EIP-2612 (Gas Sponsorship Helper)
Not a standalone payment method. Used as an extension to make the Permit2 approval step gasless:
- Token implements
permit(owner, spender, value, deadline, v, r, s)(EIP-2612) - User signs a permit message off-chain
- Facilitator submits the permit + approval in one call
- User never pays gas
In x402-rs: The Permit2PaymentPayloadExt trait provides eip2612_gas_sponsoring() for unified EIP-2612 handling.
Priority Order
When assetTransferMethod is not specified in the payment requirements:
1. EIP-3009 — if the token supports it (simplest, truly gasless) 2. Permit2 — universal fallback for any ERC-20
When assetTransferMethod is specified:
"eip3009"— Force EIP-3009 path"permit2"— Force Permit2 path
Smart Wallet Support
x402-rs supports smart contract wallets (not just EOAs):
EIP-1271 (Deployed Smart Wallets)
Smart wallets that implement isValidSignature(address, bytes32, bytes) can produce signatures that the facilitator verifies via contract call.
EIP-6492 (Counterfactual Smart Wallets)
For smart wallets that are not yet deployed on-chain. Uses a magic byte suffix:
<signature><32-byte-magic:0x6492649264926492649264926492649264926492649264926492649264926492><validator_address>The facilitator detects this format and:
1. Tries to call isValidSignature on the wallet (if deployed) 2. If reverted with EIP6492 error, deploys the wallet first, then verifies 3. Uses the Validator6492 contract (0x82c6D79a13E6F42A8C7A3Ea24A4628B1eD9c5E34) for validation
Signature Detection in x402-rs
The facilitator intelligently dispatches based on signature format:
- EOA signatures (64-65 bytes): Parsed as
(r, s, v), dispatched to standard EIP-3009 function - EIP-1271 signatures: Passed as full bytes for contract wallet verification
- EIP-6492 signatures: Detected by 32-byte magic suffix, validated via Validator6492
CAIP-2 Chain IDs (V2)
The V2 protocol uses CAIP-2 (Chain Agnostic Improvement Proposal 2) identifiers:
{namespace}:{reference}Examples:
eip155:8453— Base Mainneteip155:84532— Base Sepoliaeip155:137— Polygonsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp— Solana Mainnetaptos:1— Aptos Mainnet
Parsing:
use x402_types::chain::ChainId;
let chain_id: ChainId = "eip155:8453".parse().unwrap();
assert_eq!(chain_id.namespace(), "eip155");
assert_eq!(chain_id.reference(), "8453");
// V1 compatibility
let chain_id = ChainId::from_network_name("base").unwrap();V2 is designed to work with any CAIP-2 chain ID without requiring a predefined registry.
Solana Payment Flow
Solana payments use pre-signed VersionedTransaction:
1. Client receives PaymentRequired response with price tags 2. Client selects Solana payment option 3. Client creates a VersionedTransaction containing:
- SPL Token transfer instruction
- Compute budget instructions (compute unit limit + price)
4. Client signs with their keypair 5. Client serializes and base64-encodes the transaction 6. Facilitator deserializes, validates, simulates, checks balance 7. Facilitator adds its signature as fee payer and submits on-chain 8. Waits for confirmation
Aptos Payment Flow
Aptos payments use sponsored transactions with BCS-encoded payloads:
1. Client creates a transaction calling 0x1::primary_fungible_store::transfer 2. Client signs with their Ed25519 key 3. Client BCS-encodes and base64-encodes the transaction 4. Facilitator deserializes, validates, simulates, checks balance 5. If sponsor_gas is enabled, facilitator adds fee payer signature 6. Facilitator submits the dual-signed transaction on-chain
Custom Scheme Implementation
Create a custom payment scheme by implementing the X402SchemeId trait and either the client or facilitator trait:
Scheme ID
use x402_types::scheme::X402SchemeId;
use x402_types::proto::{v1, v2};
pub struct MyCustomScheme;
impl X402SchemeId for MyCustomScheme {
fn x402_version(&self) -> u8 { 2 }
fn namespace(&self) -> &str { "eip155" }
fn scheme(&self) -> &str { "my-custom" }
}Facilitator Builder
use x402_types::scheme::X402SchemeFacilitatorBuilder;
impl<P> X402SchemeFacilitatorBuilder<P> for MyCustomScheme {
fn build(&self, provider: P, config: Option<&serde_json::Value>)
-> Result<Box<dyn x402_types::facilitator::Facilitator>, Box<dyn std::error::Error>>
{
// Return your facilitator implementation
todo!()
}
}Client-Side
Implement accept to sign payments and return PaymentPayload:
impl PaymentClient for MyCustomClient {
fn accept(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate> {
// Check if this client can handle the payment requirements
// Return candidates with signed payloads
todo!()
}
}For the complete guide on writing schemes, see the x402-rs docs at docs/how-to-write-a-scheme.md.
Wire Format Types
x402-types::proto::v1
V1 protocol types using network names.
x402-types::proto::v2
V2 protocol types using CAIP-2 chain IDs:
PaymentRequired— 402 response structurePaymentRequirements— Individual payment optionPaymentPayload— Client's signed paymentPriceTag— Server-side payment configurationVerifyRequest/VerifyResponse— Facilitator verifySettleRequest/SettleResponse— Facilitator settleSupportedResponse— Facilitator capabilities
Key Facilitator Traits
// Core facilitator trait
#[async_trait]
pub trait Facilitator: Send + Sync {
async fn verify(&self, request: VerifyRequest) -> Result<VerifyResponse, FacilitatorError>;
async fn settle(&self, request: SettleRequest) -> Result<SettleResponse, FacilitatorError>;
}
// Scheme ID trait
pub trait X402SchemeId: Send + Sync {
fn x402_version(&self) -> u8;
fn namespace(&self) -> &str;
fn scheme(&self) -> &str;
}
// Scheme facilitator builder
pub trait X402SchemeFacilitatorBuilder<P>: X402SchemeId {
fn build(&self, provider: P, config: Option<&Value>)
-> Result<Box<dyn Facilitator>, Box<dyn Error>>;
}Server-Side Guide: Protecting Routes with x402 Payments
This guide covers using x402-axum to gate Axum routes behind blockchain micropayments. The middleware is a tower::Layer that intercepts requests, validates payment headers via a facilitator, and settles payments.
Installation
[dependencies]
x402-axum = "1.0"
x402-chain-eip155 = { version = "1.0", features = ["server"] }
x402-chain-solana = { version = "1.0", features = ["server"] }
x402-types = "1.0"
alloy-primitives = "1.4"
axum = "0.8"
tokio = { version = "1", features = ["full"] }Enable the telemetry feature for OpenTelemetry tracing:
x402-axum = { version = "1.0", features = ["telemetry"] }Basic Usage: Static Pricing
The simplest way to protect a route is with a static price tag:
use alloy_primitives::address;
use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use x402_axum::X402Middleware;
use x402_chain_eip155::{V2Eip155Exact, KnownNetworkEip155};
use x402_types::networks::USDC;
let x402 = X402Middleware::new("https://facilitator.x402.rs");
let app = Router::new().route(
"/paid-content",
get(handler).layer(
x402.with_price_tag(V2Eip155Exact::price_tag(
address!("0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07"),
USDC::base_sepolia().parse("0.01").unwrap(),
))
),
);
async fn handler() -> impl IntoResponse {
(StatusCode::OK, "This is VIP content!")
}When a request arrives without valid payment, the middleware returns 402 Payment Required with the Payment-Required header (base64-encoded JSON describing accepted payment).
Understanding price_tag Parameters
The price_tag method takes two arguments:
1. `pay_to` — The wallet address that receives the payment (as a string or Address type) 2. `amount` — The payment amount in the token's smallest unit
For USDC (6 decimals), "0.01" USDC = 10000 smallest units. Use the USDC convenience struct for known networks:
USDC::base_sepolia().amount(10000u64) // 0.01 USDC on Base Sepolia
USDC::base().amount(1000000u64) // 1.0 USDC on Base Mainnet
USDC::solana().amount(1000000u64) // 1.0 USDC on SolanaOr use any ERC-20 token address directly:
V2Eip155Exact::price_tag(
address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC on Ethereum
"1000000".to_string(), // 1.0 USDC (6 decimals)
)Dynamic Pricing
Use with_dynamic_price to compute prices per-request:
x402.with_dynamic_price(|headers, uri, _base_url| {
let has_discount = uri.query()
.map(|q| q.contains("discount"))
.unwrap_or(false);
let amount = if has_discount { 50u64 } else { 100u64 };
async move {
vec![V2Eip155Exact::price_tag(
address!("0x..."),
USDC::base_sepolia().amount(amount),
)]
}
})The callback receives:
headers— the request headers (&HeaderMap)uri— the request URI (&Uri)base_url— the configured base URL (Option<&Url>)
It returns Vec<PriceTag> — a list of accepted payment options. Return vec![] to bypass payment entirely (conditional free access).
Conditional Free Access Example
x402.with_dynamic_price(|headers, uri, _base_url| {
let is_free = uri.query()
.map(|q| q.contains("free"))
.unwrap_or(false);
async move {
if is_free {
vec![] // No payment required
} else {
vec![V2Eip155Exact::price_tag(
address!("0x..."),
USDC::base_sepolia().amount(100u64),
)]
}
}
})With this: GET /api/data returns 402, but GET /api/data?free serves content directly.
Multi-Chain Payment Acceptance
Accept payments from multiple blockchains on the same endpoint:
use x402_chain_eip155::V2Eip155Exact;
use x402_chain_solana::V2SolanaExact;
use x402_chain_aptos::V2AptosExact;
use solana_pubkey::pubkey;
x402
// Accept USDC on Base Sepolia
.with_price_tag(V2Eip155Exact::price_tag(
address!("0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07"),
USDC::base_sepolia().parse("0.01").unwrap(),
))
// Accept USDC on Solana
.with_price_tag(V2SolanaExact::price_tag(
pubkey!("EGBQqKn968sVv5cQh5Cr72pSTHfxsuzq7o7asqYB5uEV").to_string(),
USDC::solana().amount(100),
))The client picks whichever chain they prefer. The facilitator handles verification and settlement for the chosen chain.
Multi-Token Acceptance
Accept different tokens on the same chain:
x402
.with_price_tag(V2Eip155Exact::price_tag(
address!("0xYourWallet"),
"1000000".to_string(), // 1 USDC (6 decimals)
))Each price_tag can specify a different token contract address and amount. The client selects which to pay.
Settlement Timing
Control when on-chain settlement happens:
// Default: settle after handler execution
let x402 = X402Middleware::new("https://facilitator.x402.rs")
.settle_after_execution();
// Alternative: settle before handler execution
let x402 = X402Middleware::new("https://facilitator.x402.rs")
.settle_before_execution();- `settle_after_execution` (default): Content is served first, then settlement happens. If settlement fails, content was already served.
- `settle_before_execution`: Settlement completes before the handler runs. Guarantees payment, but adds latency.
Configuration Options
let x402 = X402Middleware::new("https://facilitator.x402.rs")
.with_base_url(Url::parse("https://api.example.com").unwrap())
.with_resource(Url::parse("https://api.example.com/premium").unwrap())
.with_description("Premium API access")
.with_mime_type("application/json")
.with_supported_cache_ttl(Duration::from_secs(300));- `with_base_url`: Base URL for computing resource URLs dynamically. Defaults to
http://localhost/. - `with_resource`: Explicit full URI of the protected resource (recommended in production).
- `with_description`: Human-readable description of what is being paid for.
- `with_mime_type`: MIME type of the protected resource (default:
application/json). - `with_supported_cache_ttl`: Cache TTL for facilitator capability queries.
Duration::from_secs(0)disables caching.
Error Handling
The middleware returns appropriate 402 responses with error details:
VerificationError::PaymentHeaderRequired— MissingPayment-SignatureheaderVerificationError::InvalidPaymentHeader— Malformed payment headerVerificationError::NoPaymentMatching— Payment doesn't match any accepted requirementsVerificationError::VerificationFailed— Facilitator rejected the paymentPaygateError::Settlement— On-chain settlement failed
Custom Schemes
Implement the PaygateProtocol trait to support custom payment methods:
use x402_axum::paygate::PaygateProtocol;
use x402_types::proto::v2;
pub struct MyCustomScheme;
impl MyCustomScheme {
pub fn price_tag(
pay_to: String,
asset: String,
amount: u64,
) -> v2::PriceTag {
v2::PriceTag {
requirements: v2::PaymentRequirements {
scheme: "my-custom-scheme".to_string(),
pay_to,
asset,
network: "eip155:8453".parse().unwrap(),
amount: amount.to_string(),
max_timeout_seconds: 300,
extra: None,
},
enricher: None,
}
}
}See references/protocol-details.md for the full custom scheme implementation guide.
Telemetry
Enable the telemetry feature for structured tracing spans:
x402.handle_requestx402.verify_paymentx402.settle_payment
Connect to OpenTelemetry exporters (Jaeger, Tempo, etc.) via standard tracing subscribers.
HTTP Behavior
V2 Protocol (preferred)
When no valid payment is provided:
HTTP/1.1 402 Payment Required
Payment-Required: <base64-encoded PaymentRequired>The Payment-Required header contains base64-encoded JSON:
{
"x402Version": "2",
"resource": { "url": "...", "description": "...", "mimeType": "..." },
"paymentRequirements": [
{
"scheme": "exact",
"network": "eip155:84532",
"amount": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x...",
"maxTimeoutSeconds": 60,
"extra": {
"assetTransferMethod": "eip3009",
"name": "USDC",
"version": "2"
}
}
]
}V1 Protocol
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": "X-PAYMENT header is required",
"accepts": [...],
"x402Version": "1"
}