Skip to main content
This example demonstrates how to build a real-time DLOB (Decentralized Limit Order Book) and serve it via a web API. It includes both L2 (aggregated) and L3 (order-level) endpoints with a web UI.

What It Does

The DLOB builder:
  • Syncs all user accounts with open orders
  • Maintains a live order book via gRPC subscriptions
  • Serves L2 (price-level) order book data
  • Serves L3 (individual order) data
  • Includes a web UI for visualization

Architecture

┌─────────────┐
│ gRPC Stream │ → User Account Updates
└─────────────┘

┌─────────────┐
│ DLOB Builder│ → Maintains Order Book
└─────────────┘

┌─────────────┐
│  Web Server │ → REST API + UI
└─────────────┘

Complete Source Code

use std::sync::Arc;
use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::{Html, Json},
    routing::get,
    Router,
};
use drift_rs::{
    dlob::{builder::DLOBBuilder, DLOB},
    types::{MarketId, MarketType},
    Context, DriftClient, GrpcSubscribeOpts, RpcClient,
};
use serde::{Deserialize, Serialize};
use solana_commitment_config::CommitmentLevel;
use solana_keypair::Keypair;
use tower::ServiceBuilder;
use tower_http::cors::CorsLayer;

#[derive(Serialize, Deserialize)]
struct OrderbookLevel {
    price: u64,
    size: u64,
}

#[derive(Serialize, Deserialize)]
struct L2Response {
    slot: u64,
    oracle_price: u64,
    asks: Vec<OrderbookLevel>,
    bids: Vec<OrderbookLevel>,
    market_index: u16,
}

#[derive(Deserialize)]
struct L2Query {
    market_index: u16,
}

#[derive(Serialize, Deserialize)]
struct L3OrderResponse {
    price: u64,
    size: u64,
    max_ts: u64,
    order_id: u32,
    reduce_only: bool,
    kind: String,
    user: String,
}

#[derive(Serialize, Deserialize)]
struct L3Response {
    slot: u64,
    oracle_price: u64,
    bids: Vec<L3OrderResponse>,
    asks: Vec<L3OrderResponse>,
    market_index: u16,
}

#[derive(Deserialize)]
struct L3Query {
    market_index: u16,
    #[serde(default)]
    max_orders: Option<usize>,
}

async fn get_l2_orderbook(
    Query(params): Query<L2Query>,
    State(state): State<Arc<AppState>>,
) -> Result<Json<L2Response>, StatusCode> {
    let l2_book = state
        .dlob
        .get_l2_snapshot(params.market_index, MarketType::Perp);

    let asks: Vec<OrderbookLevel> = l2_book
        .asks
        .iter()
        .map(|(price, size)| OrderbookLevel {
            price: *price,
            size: *size,
        })
        .collect();

    let bids: Vec<OrderbookLevel> = l2_book
        .bids
        .iter()
        .map(|(price, size)| OrderbookLevel {
            price: *price,
            size: *size,
        })
        .collect();

    Ok(Json(L2Response {
        slot: l2_book.slot,
        oracle_price: l2_book.oracle_price,
        asks,
        bids,
        market_index: params.market_index,
    }))
}

async fn get_l3_orderbook(
    Query(params): Query<L3Query>,
    State(state): State<Arc<AppState>>,
) -> Result<Json<L3Response>, StatusCode> {
    let oracle_price = state
        .drift
        .try_get_oracle_price_data_and_slot(MarketId::perp(params.market_index))
        .unwrap()
        .data
        .price as u64;

    let l3_book = state
        .dlob
        .get_l3_snapshot(params.market_index, MarketType::Perp);

    let convert_order = |order: &drift_rs::dlob::L3Order| L3OrderResponse {
        price: order.price,
        size: order.size,
        max_ts: order.max_ts,
        order_id: order.order_id,
        reduce_only: order.is_reduce_only(),
        kind: format!("{:?}", order.kind),
        user: order.user.to_string(),
    };

    let perp_market = state
        .drift
        .try_get_perp_market_account(params.market_index)
        .unwrap();
    let unix_now = std::time::SystemTime::now()
        .duration_since(std::time::SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    let trigger_price = perp_market
        .get_trigger_price(oracle_price as i64, unix_now, true)
        .expect("got trigger price");
    
    let max_orders = params.max_orders.unwrap_or(usize::MAX);
    let bids: Vec<L3OrderResponse> = l3_book
        .top_bids(
            max_orders,
            Some(oracle_price),
            Some(&perp_market),
            Some(trigger_price),
        )
        .map(convert_order)
        .collect();
    let asks: Vec<L3OrderResponse> = l3_book
        .top_asks(
            max_orders,
            Some(oracle_price),
            Some(&perp_market),
            Some(trigger_price),
        )
        .map(convert_order)
        .collect();

    Ok(Json(L3Response {
        slot: l3_book.slot,
        oracle_price,
        bids,
        asks,
        market_index: params.market_index,
    }))
}

async fn clob_ui() -> Html<&'static str> {
    Html(include_str!("clob.html"))
}

struct AppState {
    drift: DriftClient,
    dlob: &'static DLOB,
}

#[tokio::main]
async fn main() {
    let _ = dotenv::dotenv();
    env_logger::init();

    let rpc_url = std::env::var("RPC_URL")
        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
    let drift = DriftClient::new(
        Context::MainNet,
        RpcClient::new(rpc_url),
        Keypair::new().into(),
    )
    .await
    .expect("initialized client");

    let account_map = drift.backend().account_map();
    println!("syncing initial User accounts/orders");
    account_map
        .sync_user_accounts(vec![
            drift_rs::memcmp::get_user_with_order_filter()
        ])
        .await
        .expect("synced user accounts");

    let dlob_builder = DLOBBuilder::new(account_map);

    println!("starting gRPC subscription to live order changes");
    let grpc_url = std::env::var("GRPC_URL").expect("GRPC_URL set");
    let grpc_x_token = std::env::var("GRPC_X_TOKEN").expect("GRPC_X_TOKEN set");

    let perp_markets = vec![
        MarketId::perp(0),
        MarketId::perp(1),
        MarketId::perp(2),
        MarketId::perp(59),
        MarketId::perp(79),
    ];

    let res = drift
        .grpc_subscribe(
            grpc_url,
            grpc_x_token,
            GrpcSubscribeOpts::default()
                .commitment(CommitmentLevel::Confirmed)
                .usermap_on()
                .on_user_account(dlob_builder.account_update_handler(account_map))
                .on_slot(dlob_builder.slot_update_handler(
                    drift.clone(), 
                    perp_markets
                )),
            true,
        )
        .await;

    if let Err(err) = res {
        eprintln!("{err}");
        std::process::exit(1);
    }

    let dlob = dlob_builder.dlob();
    dlob.enable_l2_snapshot(); // disabled by default
    let state = Arc::new(AppState { dlob, drift });

    // Build the web server
    let app = Router::new()
        .route("/", get(clob_ui))
        .route("/l2", get(get_l2_orderbook))
        .route("/l3", get(get_l3_orderbook))
        .layer(ServiceBuilder::new().layer(CorsLayer::permissive()))
        .with_state(state);

    println!("Starting web server on http://localhost:8080");
    println!("CLOB UI: http://localhost:8080/");
    println!("L2 API: curl 'http://localhost:8080/l2?market_index=0'");
    println!("L3 API: curl 'http://localhost:8080/l3?market_index=0'");

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

API Endpoints

L2 Order Book (Aggregated)

Endpoint: GET /l2?market_index={market_index} Response:
{
  "slot": 123456789,
  "oracle_price": 10000000000,
  "market_index": 0,
  "asks": [
    {"price": 10100000000, "size": 5000000000},
    {"price": 10200000000, "size": 3000000000}
  ],
  "bids": [
    {"price": 9900000000, "size": 4000000000},
    {"price": 9800000000, "size": 2000000000}
  ]
}

L3 Order Book (Individual Orders)

Endpoint: GET /l3?market_index={market_index}&max_orders={limit} Response:
{
  "slot": 123456789,
  "oracle_price": 10000000000,
  "market_index": 0,
  "asks": [
    {
      "price": 10100000000,
      "size": 1000000000,
      "max_ts": 1234567890,
      "order_id": 42,
      "reduce_only": false,
      "kind": "Limit",
      "user": "ABC123..."
    }
  ],
  "bids": [...]
}

Key Concepts

L2 vs L3 Data

L2 (Aggregated):
  • Orders grouped by price level
  • Smaller payload size
  • Good for displaying traditional order books
  • Must enable with dlob.enable_l2_snapshot()
L3 (Individual Orders):
  • Every individual order
  • Includes user, order ID, timestamps
  • Useful for matching engines and order routing
  • Enabled by default

DLOB Builder Handlers

The DLOB builder provides handlers for gRPC subscriptions:
// Handle user account updates (orders)
let account_handler = dlob_builder.account_update_handler(account_map);

// Handle slot updates (refresh oracle prices)
let slot_handler = dlob_builder.slot_update_handler(drift.clone(), markets);

GrpcSubscribeOpts::default()
    .on_user_account(account_handler)
    .on_slot(slot_handler)

Running the Example

# Set environment variables
export RPC_URL="https://api.mainnet-beta.solana.com"
export GRPC_URL="your-grpc-endpoint"
export GRPC_X_TOKEN="your-token"

# Run
cargo run --example dlob-builder

# Access the API
curl 'http://localhost:8080/l2?market_index=0'
curl 'http://localhost:8080/l3?market_index=0&max_orders=10'

# Open web UI
open http://localhost:8080

Use Cases

  • Trading Interfaces: Display live order books in your UI
  • Market Making: Find best prices and available liquidity
  • Analytics: Analyze order flow and market depth
  • Order Routing: Route orders to best available liquidity

Performance Tips

  1. Enable L2 only when needed - It adds computational overhead
  2. Filter markets - Subscribe only to markets you need
  3. Use confirmed commitment - Balance between speed and reliability
  4. Implement caching - Cache responses for high-frequency requests

Next Steps

  • Add WebSocket support for streaming updates
  • Implement order book snapshots at intervals
  • Add market depth calculations
  • Build a matching engine on top of the DLOB

Build docs developers (and LLMs) love