How to Write a Scheme
Implement custom payment logic for x402-rs
This guide explains how to create a custom payment scheme for the x402-rs facilitator.
What is a Scheme?
A scheme defines how a payment is verified and settled on a specific blockchain. It encapsulates:
- Payload format — The structure of payment data (signatures, transactions, authorizations)
- Verification logic — How to validate a payment is correct before execution
- Settlement logic — How to execute the payment on-chain
- Supported chains — Which blockchain networks the scheme works with
For example, the exact scheme implements ERC-3009 transferWithAuthorization for EVM chains and SPL token transfers for Solana. You might create a new scheme for subscription payments, escrow flows, or alternative token standards.
Schemes and Blueprints
The x402 protocol defines schemes at the specification level — conceptual descriptions of how a particular payment flow works (e.g., “exact” means the buyer authorizes a transfer of the exact amount to the seller).
A blueprint is the concrete Rust implementation of that scheme for the x402-rs facilitator. It is a struct (e.g., V2SolanaExact) that carries the scheme’s unique identifier and knows how to create a handler for a given chain provider. In code, a blueprint is any type that implements both X402SchemeId (providing the identifier like v2-solana-exact) and X402SchemeFacilitatorBuilder (providing the factory method to instantiate the handler). These two traits are combined into the X402SchemeBlueprint marker trait.
At startup, you register blueprints into a SchemeBlueprints registry. The system then uses your configuration to build concrete handlers (X402SchemeFacilitator) from these blueprints — one handler per blueprint per matching chain provider. The handlers are stored in the SchemeRegistry and process verify, settle, and supported requests at runtime.
Overview
Not every part of the scheme system is meant to be extended. The table below clarifies which concepts are open for customization and which are fixed by the protocol or implementation:
| Concept | Open/Closed | Description |
|---|---|---|
| Schemes | Open | Widely extensible. Anyone can create custom schemes for new payment flows. |
| Protocol Versions | Closed | Fixed set: v1 and v2. Defined by the x402 specification. (v1 is legacy, v2 will probably live for the next few years) |
| Chain Providers | Closed | Predefined set for the implementation due to chain-specific complexity. |
Architecture
flowchart TB
subgraph Registration
SB[SchemeBlueprints] -->|by id| BP[X402SchemeBlueprint]
BP -->|build with ChainProvider| H[Box dyn X402SchemeFacilitator]
end
subgraph Runtime
SR[SchemeRegistry] -->|by_slug| H
H -->|verify| VR[VerifyResponse]
H -->|settle| SR2[SettleResponse]
H -->|supported| SPR[SupportedResponse]
end
Crate Structure
Schemes are organized in chain-specific crates under crates/chains/:
crates/
├── x402-types/ # Core types and traits
│ └── src/scheme/ # X402SchemeId, X402SchemeFacilitator, etc.
├── x402-chain-solana/ # Solana-specific implementations
│ └── src/
│ ├── v1_solana_exact/
│ └── v2_solana_exact/
├── x402-chain-eip155/ # EVM-specific implementations
│ └── src/
│ ├── v1_eip155_exact/
│ └── v2_eip155_exact/
└── x402-chain-aptos/ # Aptos-specific implementations
└── src/
└── v2_aptos_exact/
Each scheme directory contains:
mod.rs- Module exports and scheme ID implementationfacilitator.rs- Facilitator implementation (server-side)client.rs- Client implementation (optional)server.rs- Server types (optional)types.rs- Scheme-specific types
Naming Convention
Scheme IDs follow the pattern: v{version}-{namespace}-{scheme}
| ID | Struct Name | Directory |
|---|---|---|
v2-solana-exact | V2SolanaExact | v2_solana_exact/ |
v1-eip155-exact | V1Eip155Exact | v1_eip155_exact/ |
v2-solana-myscheme | V2SolanaMyscheme | v2_solana_myscheme/ |
This makes it easy to map between IDs, chain namespaces, scheme names, and code.
Core Traits and Structs
X402SchemeId
Provides identification for a scheme. This trait defines the scheme’s version, namespace, and name:
pub trait X402SchemeId {
/// The x402 protocol version (1 or 2). Defaults to 2.
fn x402_version(&self) -> u8 {
2
}
/// The chain namespace (e.g., "eip155", "solana")
fn namespace(&self) -> &str;
/// The scheme name (e.g., "exact", "myscheme")
fn scheme(&self) -> &str;
/// Computed ID: "v{version}-{namespace}-{scheme}"
fn id(&self) -> String {
format!(
"v{}-{}-{}",
self.x402_version(),
self.namespace(),
self.scheme()
)
}
}
X402SchemeFacilitatorBuilder
Factory for creating scheme facilitators:
pub trait X402SchemeFacilitatorBuilder<P> {
/// Creates a new scheme handler for the given chain provider.
///
/// # Arguments
///
/// * `provider` - The chain provider to use for on-chain operations
/// * `config` - Optional scheme-specific configuration
fn build(
&self,
provider: P,
config: Option<serde_json::Value>,
) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>>;
}
- The type parameter
Prepresents the chain provider type (e.g.,&ChainProvider,Arc<SolanaChainProvider>) - The
buildmethod receives a chain provider. Implementations typically use a chain-specific trait likeSolanaChainProviderLiketo access provider methods - The optional
configallows scheme-specific configuration (parse however you wish, see “Configure in JSON” section)
X402SchemeBlueprint
A combined trait that requires both X402SchemeId and X402SchemeFacilitatorBuilder. This is automatically implemented for any type that implements both traits:
pub trait X402SchemeBlueprint<P>:
X402SchemeId + for<'a> X402SchemeFacilitatorBuilder<&'a P>
{
}
impl<T, P> X402SchemeBlueprint<P> for T where
T: X402SchemeId + for<'a> X402SchemeFacilitatorBuilder<&'a P>
{
}
The type parameter P represents the chain provider type that the blueprint can work with.
X402SchemeFacilitator
Three core operations every scheme facilitator must implement:
#[async_trait::async_trait]
pub trait X402SchemeFacilitator: Send + Sync {
async fn verify(&self, request: &proto::VerifyRequest)
-> Result<proto::VerifyResponse, X402SchemeFacilitatorError>;
async fn settle(&self, request: &proto::SettleRequest)
-> Result<proto::SettleResponse, X402SchemeFacilitatorError>;
async fn supported(&self)
-> Result<proto::SupportedResponse, X402SchemeFacilitatorError>;
}
| Method | Purpose |
|---|---|
verify | Validate a payment without executing it. |
settle | Execute the payment on-chain. |
supported | Advertise what payment kinds this scheme supports. |
SchemeHandlerSlug
At runtime, handlers are identified by a slug combining chain ID, version, and scheme name:
pub struct SchemeHandlerSlug {
pub chain_id: ChainId,
pub x402_version: u8,
pub name: String,
}
This allows the same scheme to be applied to different chains.
Step-by-Step Guide
Step 1: Define Types
Use proto generics. For v2 schemes:
// In crates/chains/x402-chain-solana/src/v2_solana_myscheme/types.rs
use x402_types::proto::v2;
pub type PaymentRequirements = v2::PaymentRequirements<MyScheme, MyAmountType, MyAddressType, MyExtra>;
pub type PaymentPayload = v2::PaymentPayload<PaymentRequirements, MyPayload>;
pub type VerifyRequest = v2::VerifyRequest<PaymentPayload, PaymentRequirements>;
pub type SettleRequest = VerifyRequest;
Step 2: Implement X402SchemeId
// In crates/chains/x402-chain-solana/src/v2_solana_myscheme/mod.rs
use x402_types::scheme::X402SchemeId;
pub struct V2SolanaMyscheme;
impl X402SchemeId for V2SolanaMyscheme {
// x402_version() defaults to 2, no need to override
fn namespace(&self) -> &str {
"solana"
}
fn scheme(&self) -> &str {
"myscheme"
}
}
Step 3: Implement X402SchemeFacilitatorBuilder
In the chain-specific crate, implement the builder for the chain-specific provider type:
// In crates/chains/x402-chain-solana/src/v2_solana_myscheme/facilitator.rs
use crate::chain::provider::SolanaChainProviderLike;
use x402_types::chain::ChainProviderOps;
use x402_types::scheme::X402SchemeFacilitator;
impl<P> X402SchemeFacilitatorBuilder<P> for V2SolanaMyscheme
where
P: SolanaChainProviderLike + ChainProviderOps + Send + Sync + 'static,
{
fn build(
&self,
provider: P,
config: Option<serde_json::Value>,
) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn Error>>
{
// Optionally parse config here
let config = config
.map(serde_json::from_value::<V2SolanaMyschemeFacilitatorConfig>)
.transpose()?
.unwrap_or_default();
Ok(Box::new(V2SolanaMyschemeFacilitator::new(provider, config)))
}
}
Then, in the facilitator crate, implement the adapter for the generic ChainProvider enum:
// In facilitator/src/schemes.rs
#[cfg(feature = "chain-solana")]
use x402_chain_solana::V2SolanaMyscheme;
#[cfg(feature = "chain-solana")]
impl X402SchemeFacilitatorBuilder<&ChainProvider> for V2SolanaMyscheme {
fn build(
&self,
provider: &ChainProvider,
config: Option<serde_json::Value>,
) -> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>> {
let solana_provider = if let ChainProvider::Solana(provider) = provider {
Arc::clone(provider)
} else {
return Err("V2SolanaMyscheme::build: provider must be a SolanaChainProvider".into());
};
self.build(solana_provider, config)
}
}
Step 4: Implement Facilitator
// In crates/chains/x402-chain-solana/src/v2_solana_myscheme/facilitator.rs
use crate::chain::provider::SolanaChainProviderLike;
use x402_types::chain::ChainProviderOps;
use x402_types::proto;
use x402_types::proto::v2;
use x402_types::scheme::{
X402SchemeFacilitator, X402SchemeFacilitatorError,
};
/// Configuration for V2 Solana Myscheme facilitator
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct V2SolanaMyschemeFacilitatorConfig {
// Add your scheme-specific configuration fields here
}
impl Default for V2SolanaMyschemeFacilitatorConfig {
fn default() -> Self {
Self {
// Set default values for your configuration
}
}
}
pub struct V2SolanaMyschemeFacilitator<P> {
provider: P,
config: V2SolanaMyschemeFacilitatorConfig,
}
impl<P> V2SolanaMyschemeFacilitator<P> {
pub fn new(provider: P, config: V2SolanaMyschemeFacilitatorConfig) -> Self {
Self { provider, config }
}
}
#[async_trait::async_trait]
impl<P> X402SchemeFacilitator for V2SolanaMyschemeFacilitator<P>
where
P: SolanaChainProviderLike + ChainProviderOps + Send + Sync,
{
async fn verify(&self, request: &proto::VerifyRequest)
-> Result<proto::VerifyResponse, X402SchemeFacilitatorError>
{
let request = types::VerifyRequest::from_proto(request.clone())?;
// Your verification logic...
Ok(proto::v2::VerifyResponse::valid(payer.to_string()).into())
}
async fn settle(&self, request: &proto::SettleRequest)
-> Result<proto::SettleResponse, X402SchemeFacilitatorError>
{
// Your settlement logic...
Ok(proto::v2::SettleResponse::Success { payer, transaction, network }.into())
}
async fn supported(&self) -> Result<proto::SupportedResponse, X402SchemeFacilitatorError> {
let chain_id = self.provider.chain_id();
let kinds = vec![proto::SupportedPaymentKind {
x402_version: proto::v2::X402Version2.into(),
scheme: "myscheme".to_string(),
network: chain_id.to_string(),
extra: None,
}];
let signers = {
let mut signers = HashMap::with_capacity(1);
signers.insert(chain_id, self.provider.signer_addresses());
signers
};
Ok(proto::SupportedResponse {
kinds,
extensions: Vec::new(),
signers,
})
}
}
Step 5: Register the Scheme
For custom facilitators, register dynamically in the facilitator crate:
// In facilitator/src/schemes.rs
#[cfg(feature = "chain-solana")]
use x402_chain_solana::V2SolanaMyscheme;
// Then in your initialization code:
let blueprints = SchemeBlueprints::new().and_register(V2SolanaMyscheme);
Step 6: Configure in JSON
{
"schemes": [
{
"enabled": true,
"id": "v2-solana-myscheme",
"chains": "solana:*",
"config": { "yourOption": "value" }
}
]
}
id: The scheme blueprint ID (matchesX402SchemeId::id())chains: Pattern matching (*for all,{a,b}for specific chain references)config: Passed to yourbuild()method
Per-Chain Custom Handlers
A powerful feature of the scheme system is the ability to have different handlers for the same scheme on different chains. This is useful when:
- A specific chain requires custom logic (e.g., different gas handling, chain-specific optimizations)
- You want to override the default behavior for a particular chain
- You need chain-specific configuration
How It Works
- Create a custom scheme blueprint that extends or modifies the base scheme behavior
- Register it with a unique ID (e.g.,
v1-eip155-exact-custom) - Enable it for specific chains in your config
Example: Custom Handler for a Specific Chain
Suppose you want eip155:3 to use custom logic while all other EVM chains use the standard v1-eip155-exact:
Step 1: Create the custom scheme
// In crates/chains/x402-chain-eip155/src/v1_eip155_exact_custom/mod.rs
use x402_types::scheme::X402SchemeId;
pub struct V1Eip155ExactCustom;
impl X402SchemeId for V1Eip155ExactCustom {
fn x402_version(&self) -> u8 {
1
}
fn namespace(&self) -> &str {
"eip155"
}
fn scheme(&self) -> &str {
"exact" // Same scheme name - will handle "exact" payments
}
// Override the default ID to distinguish from the standard scheme
fn id(&self) -> String {
"v1-eip155-exact-custom".to_string()
}
}
// In crates/chains/x402-chain-eip155/src/v1_eip155_exact_custom/facilitator.rs
use crate::chain::provider::Eip155ChainProviderLike;
use x402_types::chain::ChainProviderOps;
use x402_types::scheme::X402SchemeFacilitator;
impl<P> X402SchemeFacilitatorBuilder<P> for V1Eip155ExactCustom
where
P: Eip155ChainProviderLike + ChainProviderOps + Send + Sync + 'static,
{
fn build(&self, provider: P, config: Option<serde_json::Value>)
-> Result<Box<dyn X402SchemeFacilitator>, Box<dyn Error>>
{
// Your custom facilitator with chain-specific logic
Ok(Box::new(V1Eip155ExactCustomFacilitator::new(provider, config)))
}
}
// In facilitator/src/schemes.rs
#[cfg(feature = "chain-eip155")]
use x402_chain_eip155::V1Eip155ExactCustom;
#[cfg(feature = "chain-eip155")]
impl X402SchemeFacilitatorBuilder<&ChainProvider> for V1Eip155ExactCustom {
fn build(&self, provider: &ChainProvider, config: Option<serde_json::Value>)
-> Result<Box<dyn X402SchemeFacilitator>, Box<dyn std::error::Error>>
{
let eip155_provider = if let ChainProvider::Eip155(provider) = provider {
Arc::clone(provider)
} else {
return Err("V1Eip155ExactCustom::build: provider must be an Eip155ChainProvider".into());
};
self.build(eip155_provider, config)
}
}
Step 2: Register both schemes
// In facilitator/src/schemes.rs
#[cfg(feature = "chain-eip155")]
use x402_chain_eip155::{V1Eip155Exact, V1Eip155ExactCustom};
// Then in your initialization code:
let blueprints = SchemeBlueprints::new()
.and_register(V1Eip155Exact) // Standard handler
.and_register(V1Eip155ExactCustom); // Custom handler
Step 3: Configure in JSON
{
"chains": {
"eip155:1": { ... },
"eip155:3": { ... },
"eip155:8453": { ... }
},
"schemes": [
{
"id": "v1-eip155-exact",
"chains": "eip155:*"
},
{
"id": "v1-eip155-exact-custom",
"chains": "eip155:3",
"config": { "customOption": "value" }
}
]
}
Key Points
- The scheme name (returned by
scheme()) determines which payment requests the handler processes - The ID (returned by
id()) is used to match config entries to blueprints - Multiple blueprints can have the same
scheme()but differentid()values - The
chainspattern in config determines which chain(s) each blueprint instance handles - Each config entry creates a separate handler instance for matching chains
Chain Pattern Matching
The chains field supports several patterns:
| Pattern | Matches |
|---|---|
eip155:84532 | Exact chain ID |
eip155:* | All EVM chains |
solana:* | All Solana chains |
eip155:{1,8453} | Specific chain references |