Skip to main content
The DLOB (Decentralized Limit Order Book) aggregates all open orders across Drift markets, providing an efficient way to find matching orders, discover liquidity, and build market-making strategies.

Understanding the DLOB

The DLOB is:
  • Event-driven: Incrementally built from user account updates
  • Real-time: Maintains live order state through subscriptions
  • Multi-market: Aggregates orders across all perp and spot markets
  • L2 and L3 views: Provides both aggregated (L2) and order-level (L3) snapshots

Setting Up the DLOB

Initialize the DLOB Builder

use drift_rs::{
    DriftClient, Context, RpcClient,
    dlob::builder::DLOBBuilder,
};
use solana_keypair::Keypair;

// Initialize Drift client
let drift = DriftClient::new(
    Context::MainNet,
    RpcClient::new("https://api.mainnet-beta.solana.com".to_string()),
    Keypair::new().into(),
)
.await?;

// Get account map for tracking user accounts
let account_map = drift.backend().account_map();

// Sync initial user accounts with open orders
println!("Syncing initial user accounts...");
account_map
    .sync_user_accounts(vec![
        drift_rs::memcmp::get_user_with_order_filter()
    ])
    .await?;

// Create DLOB builder
let dlob_builder = DLOBBuilder::new(account_map);

Subscribe to Live Updates

Use gRPC to receive real-time order updates:
use drift_rs::{GrpcSubscribeOpts, MarketId};
use solana_commitment_config::CommitmentLevel;

let grpc_url = std::env::var("GRPC_URL")?;
let grpc_token = std::env::var("GRPC_X_TOKEN")?;

// Markets to track
let markets = vec![
    MarketId::perp(0),  // SOL-PERP
    MarketId::perp(1),  // BTC-PERP
    MarketId::perp(2),  // ETH-PERP
];

// Subscribe with DLOB update handlers
drift.grpc_subscribe(
    grpc_url,
    grpc_token,
    GrpcSubscribeOpts::default()
        .commitment(CommitmentLevel::Confirmed)
        .usermap_on()  // Track all user accounts
        .on_user_account(dlob_builder.account_update_handler(account_map))
        .on_slot(dlob_builder.slot_update_handler(drift.clone(), markets)),
    true,  // sync on startup
)
.await?;

Get the DLOB Instance

let dlob = dlob_builder.dlob();

// Enable L2 snapshots (disabled by default)
dlob.enable_l2_snapshot();

// L3 snapshots are enabled by default
// To disable: dlob.disable_l3_snapshot();
L2 snapshots aggregate orders by price level and are more memory efficient. L3 snapshots provide full order details including user addresses.

Working with L3 Order Books

Get L3 Snapshot

use drift_rs::types::MarketType;

let market_index = 0;  // SOL-PERP
let l3_book = dlob.get_l3_snapshot(market_index, MarketType::Perp);

println!("Order book slot: {}", l3_book.slot);
println!("Oracle price used: {}", l3_book.oracle_price);

Iterate Through Bids and Asks

use drift_rs::MarketId;

// Get current oracle price
let oracle_price = drift
    .try_get_oracle_price_data_and_slot(MarketId::perp(market_index))
    .unwrap()
    .data
    .price as u64;

// Get perp market for VAMM price calculations
let perp_market = drift
    .try_get_perp_market_account(market_index)
    .unwrap();

// Iterate through top 10 bids
for bid in l3_book.top_bids(10, Some(oracle_price), Some(&perp_market), None) {
    println!(
        "Bid: {:.4} @ ${:.2} (user: {})",
        bid.size as f64 / 1_000_000_000.0,
        bid.price as f64 / 1_000_000.0,
        bid.user
    );
}

// Iterate through top 10 asks
for ask in l3_book.top_asks(10, Some(oracle_price), Some(&perp_market), None) {
    println!(
        "Ask: {:.4} @ ${:.2} (user: {})",
        ask.size as f64 / 1_000_000_000.0,
        ask.price as f64 / 1_000_000.0,
        ask.user
    );
}

Find Crossing Orders

Identify orders that would match:
use drift_rs::dlob::TakerOrder;
use drift_rs::types::PositionDirection;

// Define a taker order
let taker_order = TakerOrder {
    market_index: 0,
    market_type: MarketType::Perp,
    direction: PositionDirection::Long,
    price: 125_000_000,  // $125
    size: 5_000_000_000,  // 5 SOL
};

// Find matching maker orders
let slot = drift.get_slot().await.unwrap();
let crosses = dlob.find_crosses_for_taker_order(
    slot,
    oracle_price,
    taker_order,
    Some(&perp_market),
    Some(20),  // depth
);

println!("Found {} matching orders", crosses.orders.len());
for (maker_order, fill_size) in crosses.orders {
    println!(
        "  Match: {:.4} @ ${:.2} with {}",
        fill_size as f64 / 1_000_000_000.0,
        maker_order.price as f64 / 1_000_000.0,
        maker_order.user
    );
}

if crosses.is_partial {
    println!("Taker order would be partially filled");
}

if crosses.has_vamm_cross {
    println!("Order would also match with vAMM");
}

Find Auction Crosses

Find orders in their auction phase that cross resting liquidity:
let slot = drift.get_slot().await.unwrap();
let oracle_price = drift.oracle_price(MarketId::perp(market_index)).await?;

// Get trigger price for trigger orders
let unix_now = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)
    .unwrap()
    .as_secs() as i64;
let trigger_price = perp_market
    .get_trigger_price(oracle_price, unix_now, true)?;

let crosses_and_makers = dlob.find_crosses_for_auctions(
    market_index,
    MarketType::Perp,
    slot,
    oracle_price as u64,
    Some(&perp_market),
    trigger_price,
    Some(64),  // depth
);

println!("Auction crosses found: {}", crosses_and_makers.crosses.len());

for (taker, maker_matches) in crosses_and_makers.crosses {
    println!(
        "Auction order {} can match {} makers",
        taker.order_id,
        maker_matches.orders.len()
    );
}

// Check for crossing resting limit orders
if let Some((taker, maker)) = crosses_and_makers.limit_crosses {
    println!(
        "Found crossing limit orders: {} (taker) x {} (maker)",
        taker.order_id, maker.order_id
    );
}

Working with L2 Order Books

Get L2 Snapshot

let l2_book = dlob.get_l2_snapshot(market_index, MarketType::Perp);

println!("\nL2 Order Book (Aggregated by price):");
println!("Slot: {}", l2_book.slot);
println!("Oracle: ${:.2}", l2_book.oracle_price as f64 / 1_000_000.0);

println!("\nBids:");
for (price, size) in l2_book.bids.iter().take(10) {
    println!("  {:.4} @ ${:.2}",
        *size as f64 / 1_000_000_000.0,
        *price as f64 / 1_000_000.0
    );
}

println!("\nAsks:");
for (price, size) in l2_book.asks.iter().take(10) {
    println!("  {:.4} @ ${:.2}",
        *size as f64 / 1_000_000_000.0,
        *price as f64 / 1_000_000.0
    );
}

Place and Take Orders

Use the DLOB to find maker orders and execute place-and-take:
use drift_rs::{
    TransactionBuilder,
    types::{OrderParams, OrderType, PositionDirection, MarketType},
};

// Get top makers from DLOB
async fn get_top_makers_from_dlob(
    dlob: &drift_rs::dlob::DLOB,
    drift: &DriftClient,
    market_index: u16,
    side: &str,  // "bid" or "ask"
    limit: usize,
) -> Vec<(solana_sdk::pubkey::Pubkey, drift_rs::types::accounts::User)> {
    let l3_book = dlob.get_l3_snapshot(market_index, MarketType::Perp);
    
    let oracle_price = drift
        .oracle_price(MarketId::perp(market_index))
        .await
        .unwrap() as u64;
    
    let perp_market = drift
        .try_get_perp_market_account(market_index)
        .unwrap();
    
    let orders = if side == "bid" {
        l3_book.top_bids(limit, Some(oracle_price), Some(&perp_market), None)
            .collect::<Vec<_>>()
    } else {
        l3_book.top_asks(limit, Some(oracle_price), Some(&perp_market), None)
            .collect::<Vec<_>>()
    };
    
    // Fetch user accounts for makers
    let mut makers = Vec::new();
    for order in orders {
        if let Ok(user_account) = drift.get_user_account(&order.user).await {
            makers.push((order.user, user_account));
        }
    }
    
    makers
}

// Execute place-and-take
let market_index = 0;
let makers = get_top_makers_from_dlob(
    &dlob,
    &drift,
    market_index,
    "ask",  // We're buying (taking asks)
    4,
)
.await;

let order = OrderParams {
    market_index,
    market_type: MarketType::Perp,
    base_asset_amount: 10_000_000_000,  // 10 SOL
    order_type: OrderType::Market,
    direction: PositionDirection::Long,
    ..Default::default()
};

let subaccount = wallet.default_sub_account();
let subaccount_data = drift.get_user_account(&subaccount).await?;

let tx = TransactionBuilder::new(
    drift.program_data(),
    subaccount,
    std::borrow::Cow::Borrowed(&subaccount_data),
    false,
)
.with_priority_fee(1_000, Some(200_000))
.place_and_take(order, &makers, None, None, None)
.build();

let sig = drift.sign_and_send(tx).await?;
println!("Place-and-take executed: {}", sig);
Place-and-take allows you to match with specific maker orders in a single transaction, providing better execution and lower slippage.

Complete Example: DLOB Web Server

use drift_rs::{
    DriftClient, Context, RpcClient, GrpcSubscribeOpts, MarketId,
    dlob::builder::DLOBBuilder,
    types::MarketType,
};
use axum::{routing::get, Router, Json};
use serde::{Serialize, Deserialize};
use std::sync::Arc;

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

async fn get_orderbook(
    drift: Arc<DriftClient>,
    dlob: &'static drift_rs::dlob::DLOB,
) -> Json<serde_json::Value> {
    let market_index = 0;
    let oracle_price = drift
        .oracle_price(MarketId::perp(market_index))
        .await
        .unwrap() as u64;
    
    let l3_book = dlob.get_l3_snapshot(market_index, MarketType::Perp);
    let perp_market = drift.try_get_perp_market_account(market_index).unwrap();
    
    let bids: Vec<L3OrderResponse> = l3_book
        .top_bids(10, Some(oracle_price), Some(&perp_market), None)
        .map(|o| L3OrderResponse {
            price: o.price,
            size: o.size,
            user: o.user.to_string(),
            order_id: o.order_id,
        })
        .collect();
    
    let asks: Vec<L3OrderResponse> = l3_book
        .top_asks(10, Some(oracle_price), Some(&perp_market), None)
        .map(|o| L3OrderResponse {
            price: o.price,
            size: o.size,
            user: o.user.to_string(),
            order_id: o.order_id,
        })
        .collect();
    
    Json(serde_json::json!({
        "slot": l3_book.slot,
        "oracle_price": oracle_price,
        "bids": bids,
        "asks": asks,
    }))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize and subscribe (as shown above)
    // ...
    
    let app = Router::new()
        .route("/orderbook", get(|| async {
            get_orderbook(Arc::new(drift.clone()), dlob).await
        }));
    
    println!("Starting server on http://localhost:8080");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
    axum::serve(listener, app).await?;
    
    Ok(())
}

Best Practices

  1. Enable only what you need: L2 snapshots use less memory than L3
  2. Use appropriate depth: Limit order book depth for better performance
  3. Handle missing orderbooks: Use get_l3_snapshot_safe() if markets might not exist
  4. Update regularly: The DLOB is only as fresh as your subscriptions
The DLOB requires active gRPC subscriptions to stay current. Without subscriptions, the order book will become stale.

Next Steps

Build docs developers (and LLMs) love