Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building a Server with the SDK

When using tiders-x402-server as a library, you construct and start the server from your own code. This gives you full control over database setup, pricing logic, and server lifecycle.

Both Rust and Python examples are shown side by side. They follow the same steps and produce identical servers. For the full range of configuration options see the Configuration Reference.

Prefer zero-code setup? Use the CLI instead — define everything in a YAML config file and run

tiders-x402-server start

1. Connect to a Facilitator

The facilitator handles blockchain-side payment operations (verification and settlement). Point it at a public facilitator or a running facilitator instance.

Rust:

#![allow(unused)]
fn main() {
use tiders_x402_server::FacilitatorClient;

let facilitator = FacilitatorClient::try_from("https://facilitator.x402.rs")
    .expect("Failed to create facilitator client");
}

Python:

import tiders_x402_server

facilitator = tiders_x402_server.FacilitatorClient("https://facilitator.x402.rs")

2. Create a Database

Create a database backend.

The examples in the repo ship with a sample CSV file (uniswap_v3_pool_swap.csv) containing Uniswap V3 swap data. For DuckDB, you can load it directly. For PostgreSQL and ClickHouse, seed scripts are provided.

DuckDB

Rust:

#![allow(unused)]
fn main() {
let conn = duckdb::Connection::open_in_memory().expect("Failed to open DuckDB");
conn.execute_batch(
    "CREATE TABLE uniswap_v3_pool_swap AS \
     SELECT * FROM read_csv_auto('../uniswap_v3_pool_swap.csv');"
).expect("Failed to load sample data");
let db = tiders_x402_server::database::db_duckdb::DuckDbDatabase::new(conn);
}

Python:

import duckdb
db_path = "data/duckdb.db"
db = tiders_x402_server.DuckDbDatabase(db_path)

See examples/rust/src/main.rs and examples/seed_postgresql.sql / examples/seed_clickhouse.sql for the PostgreSQL and ClickHouse construction patterns.

3. Define Price Tags

A PriceTag describes a single pricing tier: who gets paid, how much, in which token, and under which pricing model. Tables can have multiple price tags for tiered pricing — clients receive all applicable options in the 402 response.

Three pricing models are supported: per-row, fixed, and metadata price. Amounts accept human-readable decimal strings (e.g., "0.002").

Per Row

Price scales linearly with the number of rows returned. Supports tiered pricing via min_items / max_items.

Rust:

#![allow(unused)]
fn main() {
use std::str::FromStr;
use x402_chain_eip155::chain::ChecksummedAddress;
use x402_types::networks::USDC;
use tiders_x402_server::{PriceTag, PricingModel};
use tiders_x402_server::payment::price::TokenAmount;

let usdc = USDC::base_sepolia();

// Default tier: $0.002 per row
let default_tag = PriceTag {
    pay_to: ChecksummedAddress::from_str("0x[your_address]").unwrap(),
    pricing: PricingModel::PerRow {
        amount_per_item: TokenAmount(usdc.parse("0.002").unwrap().amount),
        min_total_amount: None,
        min_items: None,
        max_items: None,
    },
    token: usdc.clone(),
    description: None,
    is_default: true,
};

// Bulk tier: $0.001 per row for 100+ rows
let bulk_tag = PriceTag {
    pay_to: ChecksummedAddress::from_str("0x[your_address]").unwrap(),
    pricing: PricingModel::PerRow {
        amount_per_item: TokenAmount(usdc.parse("0.001").unwrap().amount),
        min_total_amount: None,
        min_items: Some(100),
        max_items: None,
    },
    token: usdc.clone(),
    description: None,
    is_default: false,
};
}

Python:

usdc = tiders_x402_server.USDC("base_sepolia")

default_tag = tiders_x402_server.PriceTag(
    pay_to="0x[your_address]",
    amount_per_item="0.002",
    token=usdc,
    is_default=True,
)

bulk_tag = tiders_x402_server.PriceTag(
    pay_to="0x[your_address]",
    amount_per_item="0.001",
    token=usdc,
    min_items=100,
)

Fixed

A flat fee regardless of how many rows are returned.

Rust:

#![allow(unused)]
fn main() {
let fixed_tag = PriceTag {
    pay_to: ChecksummedAddress::from_str("0x[your_address]").unwrap(),
    pricing: PricingModel::Fixed {
        amount: TokenAmount(usdc.parse("1.00").unwrap().amount),
    },
    token: usdc.clone(),
    description: Some("Fixed price query".to_string()),
    is_default: false,
};
}

Python:

fixed_tag = tiders_x402_server.PriceTag.fixed(
    pay_to="0x[your_address]",
    fixed_amount="1.00",
    token=usdc,
    description="Fixed price query",
)

Metadata Price

A flat fee for accessing table metadata (schema and payment offers) via GET /api/table/{name}. Without this tag, metadata is returned freely. Charging for metadata is useful to deter scraping or fund schema-discovery costs.

Rust:

#![allow(unused)]
fn main() {
let metadata_tag = PriceTag {
    pay_to: ChecksummedAddress::from_str("0x[your_address]").unwrap(),
    pricing: PricingModel::MetadataPrice {
        amount: TokenAmount(usdc.parse("1.00").unwrap().amount),
    },
    token: usdc.clone(),
    description: Some("Metadata access fee".to_string()),
    is_default: false,
};
}

Python:

metadata_tag = tiders_x402_server.PriceTag.metadata_price(
    pay_to="0x[your_address]",
    amount="1.00",
    token=usdc,
    description="Metadata access fee",
)

4. Build Table Payment Offers

A TablePaymentOffers groups the price tags for a single table along with its description and schema. The schema is optional but recommended — it is shown to clients in the discovery document.

Rust:

#![allow(unused)]
fn main() {
use tiders_x402_server::{TablePaymentOffers, Database};

let schema = db.get_table_schema("uniswap_v3_pool_swap")
    .await
    .expect("Failed to get table schema");

let offers_table = TablePaymentOffers::new(
    "uniswap_v3_pool_swap".to_string(),
    vec![default_tag, bulk_tag, fixed_tag],
    Some(schema),
)
.with_description("Uniswap V3 swaps".to_string());
}

Python:

schema = db.get_table_schema("uniswap_v3_pool_swap")

offers_table = tiders_x402_server.TablePaymentOffers(
    "uniswap_v3_pool_swap",
    [default_tag, bulk_tag, fixed_tag],
    schema=schema,
    description="Uniswap V3 swaps",
)

For free tables that require no payment, use TablePaymentOffers::new_free_table (Rust) or TablePaymentOffers.new_free_table (Python). See the Configuration Reference for all available methods.

5. Build the Payment Configuration

The global payment configuration holds the facilitator client and all table offers.

Rust:

#![allow(unused)]
fn main() {
use tiders_x402_server::GlobalPaymentConfig;

let mut global_payment_config = GlobalPaymentConfig::default(facilitator);
global_payment_config.add_offers_table(offers_table);
}

Python:

global_payment_config = tiders_x402_server.GlobalPaymentConfig(facilitator)
global_payment_config.add_offers_table(offers_table)

6. Create State and Start the Server

Wrap the database, payment configuration, and dashboards state into the application state, then start the server.

AppState::new takes a DashboardsState even when you don’t want any dashboards — pass an empty one. The CLI’s auto-built AppState does the same when dashboards: is missing from the YAML.

Rust:

#![allow(unused)]
fn main() {
use std::path::PathBuf;
use url::Url;
use tiders_x402_server::{AppState, start_server};
use tiders_x402_server::dashboard::DashboardsState;

let server_base_url = Url::parse("http://localhost:4021").expect("Failed to parse base URL");
let server_bind_address = "0.0.0.0:4021".to_string();

let dashboards_state = DashboardsState {
    root: PathBuf::new(),
    dashboards: vec![],
};

let state = AppState::new(
    db,
    global_payment_config,
    server_base_url,
    server_bind_address,
    dashboards_state,
);

start_server(state).await;
}

Python:

state = tiders_x402_server.AppState(
    db,
    global_payment_config,
    "http://localhost:4021",
    "0.0.0.0:4021",
)

tiders_x402_server.start_server(state)

The server blocks until it receives a shutdown signal (Ctrl+C or SIGTERM).

Verifying the Server

Once running, hit the discovery endpoint:

curl http://localhost:4021/api/

This returns a JSON document listing every table, its pricing tiers, and the available endpoints. Use one of the client scripts to actually run a paid query end-to-end.

Environment Variables

The server loads .env files via dotenvy. OpenTelemetry tracing is opt-in — set these variables to export traces:

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_SERVICE_NAME=tiders-x402-server