Skip to main content
This example demonstrates how to build a market making bot that continuously quotes orders on both sides of the market. It supports both WebSocket and gRPC-based subscriptions for real-time data.

What It Does

The market maker example:
  • Subscribes to market and account updates
  • Places continuous bid and ask orders
  • Cancels and replaces orders on a fixed interval
  • Monitors fills, cancellations, and position changes
  • Supports both fixed-price and floating limit orders

Key Features

WebSocket Subscription Mode

  • Uses traditional Solana WebSocket subscriptions
  • Lower latency for simple use cases
  • Suitable for single-market makers

gRPC Subscription Mode

  • Uses Geyser gRPC for enhanced performance
  • Supports subscribing to multiple accounts efficiently
  • Better for multi-market strategies

Source Code

Main Entry Point

use drift_rs::{Context, Wallet};

mod grpc_marker;
mod ws_maker;

#[derive(argh::FromArgs)]
struct Args {
    /// run gRPC example
    #[argh(switch)]
    grpc: bool,
}

#[tokio::main]
async fn main() {
    let _ = dotenv::dotenv();
    let args: Args = argh::from_env();

    let wallet: Wallet = (drift_rs::utils::load_keypair_multi_format(
        &std::env::var("PRIVATE_KEY").expect("base58 PRIVATE_KEY set"),
    )
    .unwrap())
    .into();

    let context = if std::env::var("MAINNET").is_ok() {
        Context::MainNet
    } else {
        Context::DevNet
    };

    if args.grpc {
        println!("running gRPC maker example");
        grpc_marker::grpc_marker(context, wallet).await;
    } else {
        println!("running Ws maker example");
        ws_maker::ws_maker(context, wallet).await;
    }
}

WebSocket Market Maker

use std::time::Duration;
use drift_rs::{
    event_subscriber::{DriftEvent, EventSubscriber},
    math::constants::{BASE_PRECISION_U64, PRICE_PRECISION_U64},
    types::{
        accounts::{PerpMarket, User},
        MarketPrecision, MarketType, OrderParams, OrderType, 
        PositionDirection, PostOnlyParam,
    },
    Context, DriftClient, Pubkey, RpcClient, TransactionBuilder, Wallet,
};
use futures_util::StreamExt;

pub async fn ws_maker(context: Context, wallet: Wallet) {
    let rpc_url = std::env::var("RPC_URL")
        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
    let drift = DriftClient::new(context, RpcClient::new(rpc_url), wallet)
        .await
        .expect("initialized client");
    
    // Subscribe to blockhashes for fast transaction building
    let _ = drift.subscribe_blockhashes().await;

    let market_id = drift.market_lookup("sol-perp").unwrap();
    let market_info = drift
        .try_get_perp_market_account(market_id.index())
        .unwrap();

    let sub_account_address = drift.wallet().sub_account(0);
    let _ = drift
        .subscribe_account(&sub_account_address)
        .await
        .expect("subscribed account");
    let _ = drift
        .subscribe_oracles(&[market_id])
        .await
        .expect("subscribed oracle");

    let mut account_events = EventSubscriber::subscribe(
        drift.ws(), 
        sub_account_address
    ).await.unwrap();

    loop {
        let mut requote_interval = tokio::time::interval(Duration::from_millis(400));
        tokio::select! {
            biased;
            _ = requote_interval.tick() => {
                let sub_account_data: User = drift
                    .try_get_account(&sub_account_address)
                    .expect("has account");
                let oracle_account = drift
                    .try_get_oracle_price_data_and_slot(market_id)
                    .expect("has oracle");

                if let Ok(position) = sub_account_data
                    .get_perp_position(market_info.market_index) 
                {
                    let upnl = position
                        .get_unrealized_pnl(oracle_account.data.price)
                        .unwrap();
                    println!(
                        "current position value: ${}, upnl: ${upnl}", 
                        position.quote_asset_amount
                    );
                }

                let quote_price = 123 * PRICE_PRECISION_U64;
                let quote_size = 5_u64 * BASE_PRECISION_U64;

                place_orders(
                    &drift,
                    &market_info,
                    sub_account_address,
                    &sub_account_data,
                    vec![
                        // Fixed price limit order
                        OrderParams {
                            order_type: OrderType::Limit,
                            price: standardize_amount(
                                quote_price, 
                                market_info.price_tick()
                            ),
                            base_asset_amount: standardize_amount(
                                quote_size, 
                                market_info.quantity_tick()
                            ).max(market_info.min_order_size()),
                            direction: PositionDirection::Long,
                            market_type: MarketType::Perp,
                            market_index: market_info.market_index,
                            post_only: PostOnlyParam::MustPostOnly,
                            user_order_id: 1,
                            ..Default::default()
                        },
                        // Floating limit order (oracle offset)
                        OrderParams {
                            order_type: OrderType::Limit,
                            oracle_price_offset: Some(
                                (1 * PRICE_PRECISION_U64) as i32
                            ),
                            base_asset_amount: standardize_amount(
                                quote_size, 
                                market_info.quantity_tick()
                            ).max(market_info.min_order_size()),
                            direction: PositionDirection::Short,
                            market_type: MarketType::Perp,
                            market_index: market_info.market_index,
                            post_only: PostOnlyParam::MustPostOnly,
                            ..Default::default()
                        }
                    ],
                ).await;
            },
            event = account_events.next() => {
                match event.unwrap() {
                    DriftEvent::OrderFill { maker_order_id, .. } => {
                        println!("order filled. id:{maker_order_id}");
                    }
                    DriftEvent::OrderCancel { maker_order_id, .. } => {
                        println!("order cancelled. id:{maker_order_id}");
                    }
                    _ => {}
                }
            }
        }
    }
}

fn standardize_amount(amount: u64, tick_size: u64) -> u64 {
    amount.saturating_sub(amount % tick_size)
}

async fn place_orders(
    drift: &DriftClient,
    market: &PerpMarket,
    sub_account: Pubkey,
    sub_account_data: &User,
    orders: Vec<OrderParams>,
) {
    let builder = TransactionBuilder::new(
        drift.program_data(),
        sub_account,
        std::borrow::Cow::Borrowed(sub_account_data),
        false,
    );
    
    let tx = builder
        .with_priority_fee(1_000, Some(100_000))
        .cancel_orders((market.market_index, MarketType::Perp), None)
        .place_orders(orders)
        .build();

    match drift.sign_and_send(tx).await {
        Ok(sig) => println!("sent tx: {sig:?}"),
        Err(err) => println!("send tx err: {err:?}"),
    }
}

Key Concepts

Order Types

Fixed Price Limit Orders
  • Set a specific price using the price field
  • Price remains constant until cancelled
  • Good for specific price levels
Floating Limit Orders
  • Use oracle_price_offset instead of fixed price
  • Price dynamically adjusts with oracle
  • Offset in price precision (e.g., $1 = 1_000_000)

Post-Only Orders

All orders use PostOnlyParam::MustPostOnly to ensure they add liquidity and don’t immediately match.

Order Cancellation

Before placing new orders, the bot cancels existing orders:
builder.cancel_orders(
    (market.market_index, MarketType::Perp), 
    None  // Cancel all orders for this market
)

Running the Example

WebSocket Mode (Default)

# Set environment variables
export RPC_URL="https://api.mainnet-beta.solana.com"
export PRIVATE_KEY="your-base58-private-key"
export MAINNET=1

# Run
cargo run --example market-maker

gRPC Mode

# Additional environment variables
export GRPC_URL="your-grpc-endpoint"
export GRPC_X_TOKEN="your-token"

# Run with --grpc flag
cargo run --example market-maker -- --grpc

Next Steps

  • Implement custom quoting logic based on inventory
  • Add risk management (position limits, max loss)
  • Support multiple markets simultaneously
  • Integrate with external price feeds
  • Add performance monitoring and metrics

Build docs developers (and LLMs) love