Build Your Own Facilitator
Create a custom x402 facilitator using Rust
This guide explains how to build a custom x402 facilitator implementation using the x402-rs ecosystem.
Overview
A facilitator helps the seller to avoid bothering with on-chain intricacies:
- Verifies payment payloads signed by clients
- Settles payments on-chain
- Manages blockchain connections and signers
The x402-rs ecosystem provides building blocks to create custom facilitators tailored to your needs.
Why Build a Custom Facilitator?
You might want to build a custom facilitator for several reasons:
-
Support for custom blockchains — You need to support a blockchain that is not yet supported by the official x402-rs crates. This involves implementing a custom chain provider and adapting the payment schemes to work with it.
-
Custom chain provider behavior — You want to customize how the facilitator interacts with a supported chain. For example, you might want to implement a custom transaction settlement logic for your chain: to add custom transaction signing logic, gas pricing strategies, or nonce management.
-
Chain-specific deployment — You want to run a facilitator that only supports specific chains or schemes, reducing the binary size and attack surface. This can be achieved through feature flags in the
facilitatorcrate or by creating a minimal custom facilitator. -
Custom middleware or authentication — You need to add custom HTTP middleware, authentication, or logging that is specific to your infrastructure.
-
Integration with existing infrastructure — You want to integrate the x402 facilitator into an existing application or service, rather than running it as a standalone binary.
Architecture
If you are curious, and expect to dig deeper, here is a high-level architecture:
flowchart TB
Server["Your HTTP Server<br/>(Axum, Actix, Rocket, etc.)"]
Server --> Facilitator["x402-facilitator-local<br/>(Verification & Settlement Logic)"]
Facilitator --> ChainReg[Chain Registry]
Facilitator --> SchemeReg[Scheme Registry]
ChainReg -.-> EIP155["EIP-155 Provider"]
ChainReg -.-> Solana["Solana Provider"]
ChainReg -.-> Aptos["Aptos Provider"]
EIP155 ~~~ Solana ~~~ Aptos
SchemeReg -.-> V1E155["V1Eip155Exact"]
SchemeReg -.-> V2E155["V2Eip155Exact"]
SchemeReg -.-> V1Sol["V1SolanaExact"]
SchemeReg -.-> V2Sol["V2SolanaExact"]
SchemeReg -.-> V2Apt["V2AptosExact"]
V1E155 ~~~ V2E155 ~~~ V1Sol ~~~ V2Sol ~~~ V2Apt
Getting Started
1. Add Dependencies
Note: The versions shown below are indicative. Please check the latest versions on crates.io or the source repository if the packages are not published on crates.io.
[dependencies]
x402-types = { version = "1.0", features = ["cli"] }
x402-facilitator-local = { version = "1.0" }
x402-chain-eip155 = { version = "1.0", features = ["facilitator"] }
x402-chain-solana = { version = "1.0", features = ["facilitator"] }
dotenvy = "0.15"
serde_json = "1.0"
tokio = { version = "1.35", features = ["full"] }
async-trait = "0.1"
axum = "0.8"
tower-http = "0.9"
rustls = { version = "0.23", features = ["ring"] }
2. Initialize the Facilitator
use x402_facilitator_local::{FacilitatorLocal, handlers};
use x402_types::chain::{ChainRegistry, FromConfig};
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;
use tower_http::cors;
use axum::http::Method;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize rustls crypto provider
rustls::crypto::CryptoProvider::install_default(
rustls::crypto::ring::default_provider()
).expect("Failed to initialize rustls crypto provider");
// Load .env variables
dotenvy::dotenv().ok();
// Load configuration
let config = Config::load()?;
// Initialize chain registry from config
let chain_registry = ChainRegistry::from_config(config.chains()).await?;
// Register supported schemes
let scheme_blueprints = {
let mut blueprints = SchemeBlueprints::new();
blueprints.register(V1Eip155Exact);
blueprints.register(V2Eip155Exact);
blueprints.register(V1SolanaExact);
blueprints.register(V2SolanaExact);
blueprints
};
// Build scheme registry
let scheme_registry =
SchemeRegistry::build(chain_registry, scheme_blueprints, config.schemes());
// Create facilitator
let facilitator = FacilitatorLocal::new(scheme_registry);
let state = Arc::new(facilitator);
// Create HTTP routes with CORS
let app = Router::new()
.merge(handlers::routes().with_state(state))
.layer(
cors::CorsLayer::new()
.allow_origin(cors::Any)
.allow_methods([Method::GET, Method::POST])
.allow_headers(cors::Any),
);
// Run server
let addr = SocketAddr::new(config.host(), config.port());
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
3. Configuration
Create a config.json file:
{
"port": 8080,
"host": "0.0.0.0",
"chains": {
"eip155:8453": {
"eip1559": true,
"flashblocks": true,
"signers": ["$BASE_PRIVATE_KEY"],
"rpc": [
{
"http": "https://mainnet.base.org",
"rate_limit": 100
}
]
},
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": {
"signer": "$SOLANA_PRIVATE_KEY",
"rpc": "https://api.mainnet-beta.solana.com",
"pubsub": "wss://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:*"
}
]
}
Notice how config.json references secrets via environment variable syntax (e.g., $BASE_PRIVATE_KEY). These are resolved at runtime — values prefixed with $ or wrapped in ${...} are read from the corresponding environment variable.
Create a .env file alongside your config.json to store these secrets:
BASE_PRIVATE_KEY=0x...
SOLANA_PRIVATE_KEY=base58key...
The facilitator uses dotenvy to automatically load .env from the current working directory at startup (as shown in Step 2 with dotenvy::dotenv().ok()). Make sure you run the binary from the directory containing your .env file, or export the variables manually before launching.
Never commit your .env file to version control. Add .env to your .gitignore.
4. Build and Run
Once you have your config.json, .env, and your code from the previous steps, you can build and run the facilitator.
Run locally via cargo:
# Run with the default config path (config.json in the current directory)
cargo run
# Run with a custom config path using the --config (-c) CLI flag
cargo run -- --config /path/to/config.json
# Alternatively, specify the config path via the CONFIG env variable
CONFIG=/path/to/config.json cargo run
Build and run a release binary:
# Build a release binary
cargo build --release
# Run with the --config flag
./target/release/your-facilitator --config /path/to/config.json
# Or via the CONFIG env variable
CONFIG=/path/to/config.json ./target/release/your-facilitator
The config file path is resolved in the following order of priority:
--config(-c) CLI flagCONFIGenvironment variableconfig.jsonin the current working directory (default)
If the facilitator starts successfully, you will see the server listening on the configured host:port (default 0.0.0.0:8080).
5. Verify It Works
With the facilitator running, you can test the full payment flow by running a seller (resource server) pointed at your facilitator, and a buyer (client) that pays for a protected resource.
See these guides to set up both sides:
- Quickstart — Accept your first x402 payment in 5 minutes (TypeScript or Rust)
- Making Payments — Build a client that pays for x402-protected resources
Point the seller’s facilitatorUrl to your running facilitator (e.g., http://localhost:8080) instead of the hosted FareSide endpoint. When the buyer makes a payment, you should see your facilitator verify and settle the transaction on-chain.
Advanced Customization
Custom Scheme Implementation
To implement a custom payment scheme:
- Implement the
X402SchemeFacilitatortrait fromx402-types - Implement the
X402SchemeFacilitatorBuildertrait - Implement the
X402SchemeIdtrait - Register it with the
SchemeBlueprints
See the How to Write a Scheme guide for detailed instructions.
Custom Chain Support
To add support for a new blockchain that is not yet supported by x402-rs:
-
Implement the
ChainProviderOpstrait for your provider type. This trait provides basic operations like getting signer addresses and chain ID. -
Implement the
FromConfigtrait to construct your provider from configuration. This allows your provider to be initialized from the JSON configuration file. -
Create scheme implementations for your chain. For each scheme you want to support (e.g.,
exact), implement theX402SchemeFacilitatortrait. Your scheme will use your custom chain provider to interact with the blockchain. -
Register with the
ChainRegistry. Add your chain provider to the registry so it can be discovered by the scheme registry. -
Add the scheme to your facilitator’s
schemes.rs. Similar to how thefacilitatorcrate has aschemes.rsfile that implementsX402SchemeFacilitatorBuilderfor each scheme, you’ll need to add your scheme there to bridge the genericChainProviderenum to your chain-specific provider type.
Custom Chain Provider for Supported Chains
Even for supported chains like EIP-155 (EVM), you might want to customize the chain provider behavior. The EIP-155 schemes use the Eip155MetaTransactionProvider trait (see x402-types) to send transactions. You can implement this trait to customize:
- Transaction signing logic — Add custom signature validation or multi-sig support
- Gas pricing strategies — Implement dynamic gas pricing based on network conditions
- Nonce management — Customize how nonces are tracked and reset
- Transaction submission — Add retry logic, batching, or fallback to different RPC endpoints
To do this:
- Create a new type that wraps or replaces
Eip155ChainProvider - Implement
Eip155MetaTransactionProviderfor your type - Implement
ChainProviderOpsandFromConfigfor your type - Use your custom provider when building the
ChainRegistry
Chain-Specific Facilitator Deployment
If you want to run a facilitator that only supports specific chains (e.g., only Solana, only EVM chains), you have two options:
Option 1: Use the facilitator crate with feature flags
The facilitator crate supports feature flags to enable only specific chains. Since this crate is not published on crates.io, use a git dependency:
[dependencies]
x402-facilitator = { git = "https://github.com/x402-rs/x402-rs", default-features = false, features = ["chain-solana"] }
Available features:
chain-eip155— Enable EIP-155 (EVM) chain supportchain-solana— Enable Solana chain supportchain-aptos— Enable Aptos chain supporttelemetry— Enable OpenTelemetry tracing
Then in your main.rs, simply call the run function:
#[tokio::main]
async fn main() {
let result = x402_facilitator::run().await;
if let Err(e) = result {
eprintln!("{e}");
std::process::exit(1)
}
}
Option 2: Create a minimal custom facilitator
Follow the “Getting Started” section above, but only register the schemes you need:
// Only register Solana schemes
let scheme_blueprints = {
let mut blueprints = SchemeBlueprints::new();
blueprints.register(V1SolanaExact);
blueprints.register(V2SolanaExact);
blueprints
};
This approach gives you full control over the binary size and dependencies.
Middleware Integration
Integrate with your existing HTTP framework:
use axum::{middleware, Router};
let app = Router::new()
.route("/verify", post(verify_handler))
.route("/settle", post(settle_handler))
.layer(middleware::from_fn(your_auth_middleware));
Adding Pre/Post Processing Logic
You can wrap FacilitatorLocal to add custom logic before or after payment verification and settlement. This is useful for:
- Logging and auditing — Log all payment attempts for compliance
- Rate limiting — Enforce limits on verification/settlement calls
- Custom validation — Add business-specific validation rules
- Metrics collection — Track payment success rates, latency, etc.
To do this, create a wrapper struct and implement the Facilitator trait:
use x402_facilitator_local::FacilitatorLocal;
use x402_types::facilitator::Facilitator;
use x402_types::proto;
use std::sync::Arc;
/// A wrapper around FacilitatorLocal that adds custom pre/post processing.
pub struct FancyFacilitator<A> {
inner: FacilitatorLocal<A>,
}
impl<A> FancyFacilitator<A> {
pub fn new(inner: FacilitatorLocal<A>) -> Self {
Self { inner }
}
}
impl<A: Clone + Send + Sync + 'static> Facilitator for FancyFacilitator<A>
where
FacilitatorLocal<A>: Facilitator,
{
type Error = <FacilitatorLocal<A> as Facilitator>::Error;
async fn verify(
&self,
request: &proto::VerifyRequest,
) -> Result<proto::VerifyResponse, Self::Error> {
// Pre-processing: custom validation, logging, rate limiting, etc.
println!("Verifying payment for scheme: {:?}", request.scheme);
// Delegate to inner facilitator
let response = self.inner.verify(request).await?;
// Post-processing: audit logging, metrics, etc.
println!("Payment verified: payer={}", response.payer);
Ok(response)
}
async fn settle(
&self,
request: &proto::SettleRequest,
) -> Result<proto::SettleResponse, Self::Error> {
// Pre-processing
println!("Settling payment...");
// Delegate to inner facilitator
let response = self.inner.settle(request).await?;
// Post-processing
println!("Payment settled: tx={}", response.transaction);
Ok(response)
}
async fn supported(&self) -> Result<proto::SupportedResponse, Self::Error> {
self.inner.supported().await
}
}
// Usage:
let facilitator = FacilitatorLocal::new(scheme_registry);
let fancy_facilitator = FancyFacilitator::new(facilitator);
let state = Arc::new(fancy_facilitator);
Deployment
Docker
Create a Dockerfile:
FROM rust:trixie as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:trixie-slim
COPY --from=builder /app/target/release/your-facilitator /usr/local/bin/
ENTRYPOINT ["your-facilitator"]
Environment Variables
HOST- Server bind address (default:0.0.0.0)PORT- Server port (default:8080)CONFIG- Path to configuration file (default:config.json). You can also pass the config path via the--config(-c) CLI flag, which takes priority over this environment variable.RUST_LOG- Log level (default:info)OTEL_*- OpenTelemetry configuration (whentelemetryfeature enabled)
Observability
Enable OpenTelemetry tracing:
use x402_facilitator_local::util::Telemetry;
let telemetry = Telemetry::new()
.with_name("my-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);
Example
- x402-facilitator - Useful for understanding the structure
Support
For questions or issues:
- Open an issue on GitHub
- Check the x402 protocol documentation
- Review individual crate documentation on docs.rs