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
- Enable only what you need: L2 snapshots use less memory than L3
- Use appropriate depth: Limit order book depth for better performance
- Handle missing orderbooks: Use
get_l3_snapshot_safe()if markets might not exist - 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.