Advanced 15 min read

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:

  1. 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.

  2. 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.

  3. 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 facilitator crate or by creating a minimal custom facilitator.

  4. Custom middleware or authentication — You need to add custom HTTP middleware, authentication, or logging that is specific to your infrastructure.

  5. 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.

Security

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:

  1. --config (-c) CLI flag
  2. CONFIG environment variable
  3. config.json in 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:

  1. Implement the X402SchemeFacilitator trait from x402-types
  2. Implement the X402SchemeFacilitatorBuilder trait
  3. Implement the X402SchemeId trait
  4. 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:

  1. Implement the ChainProviderOps trait for your provider type. This trait provides basic operations like getting signer addresses and chain ID.

  2. Implement the FromConfig trait to construct your provider from configuration. This allows your provider to be initialized from the JSON configuration file.

  3. Create scheme implementations for your chain. For each scheme you want to support (e.g., exact), implement the X402SchemeFacilitator trait. Your scheme will use your custom chain provider to interact with the blockchain.

  4. Register with the ChainRegistry. Add your chain provider to the registry so it can be discovered by the scheme registry.

  5. Add the scheme to your facilitator’s schemes.rs. Similar to how the facilitator crate has a schemes.rs file that implements X402SchemeFacilitatorBuilder for each scheme, you’ll need to add your scheme there to bridge the generic ChainProvider enum 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:

  1. Create a new type that wraps or replaces Eip155ChainProvider
  2. Implement Eip155MetaTransactionProvider for your type
  3. Implement ChainProviderOps and FromConfig for your type
  4. 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 support
  • chain-solana — Enable Solana chain support
  • chain-aptos — Enable Aptos chain support
  • telemetry — 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 (when telemetry feature 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

Support

For questions or issues: