Skip to main content
This example demonstrates how to monitor and fill swift taker orders as a maker. Swift makers compete to provide the best execution for takers while earning fees.

What It Does

The swift maker:
  • Subscribes to incoming swift orders
  • Filters orders based on strategy
  • Creates matching maker orders
  • Atomically places and fills swift orders
  • Earns maker fees on successful fills

Complete Source Code

use drift_rs::{
    swift_order_subscriber::SignedOrderInfo,
    types::{
        MarketId, OrderParams, OrderType, PositionDirection, PostOnlyParam,
    },
    DriftClient, RpcClient, Wallet,
};
use futures_util::StreamExt;
use solana_pubkey::Pubkey;

#[tokio::main]
async fn main() {
    let _ = env_logger::init();
    let _ = dotenv::dotenv();
    
    let wallet: Wallet = (drift_rs::utils::load_keypair_multi_format(
        &std::env::var("PRIVATE_KEY").expect("base58 PRIVATE_KEY set"),
    )
    .unwrap())
    .into();

    // Choose a sub-account for order placement
    let filler_subaccount = wallet.default_sub_account();

    let use_mainnet = std::env::var("MAINNET").is_ok();
    let context = if use_mainnet {
        drift_rs::types::Context::MainNet
    } else {
        drift_rs::types::Context::DevNet
    };
    
    let rpc_url = std::env::var("RPC_URL")
        .unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
    let drift = DriftClient::new(context, RpcClient::new(rpc_url), wallet)
        .await
        .expect("initialized client");
    
    let _ = drift
        .subscribe_blockhashes()
        .await
        .expect("subscribed blockhashes");

    // Subscribe to filler account (used when building Txs)
    let _ = drift
        .subscribe_account(&filler_subaccount)
        .await
        .expect("subscribed");

    // Choose markets to monitor
    let market_ids: Vec<MarketId> = ["sol-perp"]
        .iter()
        .map(|m| drift.market_lookup(m).expect("market found"))
        .collect();

    let mut swift_order_stream = drift
        .subscribe_swift_orders(&market_ids, Some(true), None, None)
        .await
        .expect("subscribed swift orders");

    // Watch orders
    loop {
        tokio::select! {
            biased;
            _ = tokio::signal::ctrl_c() => {
                println!("swift maker shutting down...");
                break;
            }
            swift_order = swift_order_stream.next() => {
                match swift_order {
                    Some(order) => {
                        let _handle = tokio::spawn(
                            try_fill(drift.clone(), filler_subaccount, order)
                        );
                    }
                    None => {
                        println!("swift order stream finished");
                        break;
                    }
                }
            }
        }
    }
}

/// Try to fill a swift order
async fn try_fill(
    drift: DriftClient, 
    filler_subaccount: Pubkey, 
    swift_order: SignedOrderInfo
) {
    // TODO: filter `swift_order.order_params()` depending on strategy params
    println!("new swift order: {swift_order:?}");
    
    let taker_order = swift_order.order_params();
    let taker_subaccount = swift_order.taker_subaccount();

    // Fetch taker accounts inline
    // TODO: for better fills maintain a gRPC map of user accounts
    let (taker_account_data, taker_stats, tx_builder) = tokio::try_join!(
        drift.get_user_account(&taker_subaccount), // always hits RPC
        drift.get_user_stats(&swift_order.taker_authority), // always hits RPC
        drift.init_tx(&filler_subaccount, false)
    )
    .unwrap();

    // Build the fill transaction
    // It places the swift order for the taker and fills it
    let tx = tx_builder
        .place_and_make_swift_order(
            OrderParams {
                order_type: OrderType::Limit,
                market_index: taker_order.market_index,
                market_type: taker_order.market_type,
                direction: match taker_order.direction {
                    PositionDirection::Long => PositionDirection::Short,
                    PositionDirection::Short => PositionDirection::Long,
                },
                // TODO: fill at price depending on strategy params
                // this always attempts to fill at the best price for the _taker_
                price: taker_order
                    .auction_start_price
                    .expect("start price set")
                    .unsigned_abs(),
                // Try fill the whole order amount
                base_asset_amount: taker_order.base_asset_amount,
                post_only: PostOnlyParam::MustPostOnly,
                bit_flags: OrderParams::IMMEDIATE_OR_CANCEL_FLAG,
                ..Default::default()
            },
            &swift_order,
            &taker_account_data,
            &taker_stats.referrer,
        )
        .build();

    match drift.sign_and_send(tx).await {
        Ok(sig) => {
            println!("sent fill: {sig}");
        }
        Err(err) => {
            println!("fill failed: {err}");
        }
    }
}

Key Concepts

Swift Order Stream

Subscribe to incoming swift orders:
let mut swift_order_stream = drift
    .subscribe_swift_orders(
        &market_ids,  // Markets to monitor
        Some(true),   // Include auction orders
        None,         // No specific order type filter
        None,         // No specific direction filter
    )
    .await?;

Order Matching Logic

The maker creates an opposing order:
OrderParams {
    direction: match taker_order.direction {
        PositionDirection::Long => PositionDirection::Short,  // Opposite
        PositionDirection::Short => PositionDirection::Long,
    },
    price: taker_order.auction_start_price.unwrap().unsigned_abs(),
    base_asset_amount: taker_order.base_asset_amount,
    ..Default::default()
}

Atomic Fill Transaction

The transaction builder creates an atomic operation:
tx_builder.place_and_make_swift_order(
    maker_order_params,
    &swift_order,           // The taker's signed order
    &taker_account_data,    // Taker's account data
    &taker_stats.referrer,  // Referrer for fees
)
This:
  1. Places the taker’s swift order on-chain
  2. Places your maker order
  3. Matches them together
  4. All in one transaction (atomic)

Strategy Considerations

Filtering Orders

Implement custom filters based on your strategy:
let taker_order = swift_order.order_params();

// Filter by market
if taker_order.market_index != 0 {
    return; // Only fill SOL-PERP
}

// Filter by size
if taker_order.base_asset_amount < MIN_SIZE {
    return; // Too small
}
if taker_order.base_asset_amount > MAX_SIZE {
    return; // Too large
}

// Filter by price
let oracle_price = get_oracle_price(taker_order.market_index);
let start_price = taker_order.auction_start_price.unwrap();
if (start_price - oracle_price).abs() > MAX_SPREAD {
    return; // Spread too wide
}

Pricing Strategy

Choose when to fill in the auction: Early Fill (Best for Taker):
price: taker_order.auction_start_price.unwrap().unsigned_abs()
  • Highest chance of winning
  • Lowest profit margin
  • Good for volume
Late Fill (Best for Maker):
price: taker_order.auction_end_price.unwrap().unsigned_abs()
  • Lower chance of winning
  • Highest profit margin
  • Risk of missing fill
Mid-Point:
let start = taker_order.auction_start_price.unwrap();
let end = taker_order.auction_end_price.unwrap();
price: ((start + end) / 2).unsigned_abs()
  • Balanced approach
  • Moderate profit and success rate

Risk Management

// Check maker inventory
let position = filler_account_data
    .get_perp_position(taker_order.market_index)?;

if position.base_asset_amount.abs() > MAX_POSITION {
    return; // Position limit reached
}

// Check if fill would exceed limit
let new_position = match taker_order.direction {
    PositionDirection::Long => {
        position.base_asset_amount - taker_order.base_asset_amount as i64
    }
    PositionDirection::Short => {
        position.base_asset_amount + taker_order.base_asset_amount as i64
    }
};

if new_position.abs() > MAX_POSITION {
    return; // Would exceed position limit
}

Performance Optimization

Use gRPC Account Map

Instead of RPC calls for each order:
// Initial setup
let account_map = drift.backend().account_map();

// Subscribe to all user accounts
drift.grpc_subscribe(
    grpc_url,
    grpc_x_token,
    GrpcSubscribeOpts::default().usermap_on(),
    true,
).await?;

// Fast account access (no RPC call)
let taker_account_data = account_map
    .get_user_account(&taker_subaccount)?;

Parallel Processing

Process multiple orders concurrently:
swift_order = swift_order_stream.next() => {
    match swift_order {
        Some(order) => {
            // Spawn async task for each order
            tokio::spawn(try_fill(
                drift.clone(), 
                filler_subaccount, 
                order
            ));
        }
        None => break,
    }
}

Pre-fetch Blockhashes

// Subscribe to blockhashes
let _ = drift.subscribe_blockhashes().await?;

// Transactions will use cached blockhashes (faster)

Running the Example

# Set environment variables
export PRIVATE_KEY="your-base58-key"
export RPC_URL="https://api.devnet.solana.com"
export MAINNET=0  # Use DevNet for testing

# Run
cargo run --example swift-maker

# The bot will print:
# - New swift orders as they arrive
# - Fill transaction signatures
# - Any errors

Monitoring

Track your maker performance:
struct MakerStats {
    orders_seen: u64,
    orders_attempted: u64,
    orders_filled: u64,
    orders_failed: u64,
    total_volume: u64,
    total_fees_earned: i64,
}

let mut stats = MakerStats::default();

swift_order = swift_order_stream.next() => {
    stats.orders_seen += 1;
    
    if should_fill(&order) {
        stats.orders_attempted += 1;
        
        match try_fill(&order).await {
            Ok(_) => {
                stats.orders_filled += 1;
                stats.total_volume += order.base_asset_amount;
            }
            Err(_) => {
                stats.orders_failed += 1;
            }
        }
    }
}

Error Handling

Common errors and solutions: “Taker account not found”
  • Account hasn’t been synced via gRPC
  • Use RPC fallback or maintain usermap
“Insufficient liquidity”
  • Maker doesn’t have enough collateral
  • Check margin before attempting fill
“Order already filled”
  • Another maker beat you to it
  • Normal in competitive environment
“Price out of bounds”
  • Auction parameters changed
  • Oracle price moved significantly

Best Practices

  1. Test on DevNet First: Always test strategies on DevNet
  2. Implement Position Limits: Prevent excessive exposure
  3. Monitor Performance: Track fill rate and profitability
  4. Handle Errors Gracefully: Don’t crash on failed fills
  5. Use gRPC for Speed: RPC calls are too slow for competitive filling
  6. Parallel Processing: Handle multiple orders simultaneously
  7. Risk Management: Filter orders based on size and price

Advanced Features

Multi-Market Maker

let market_ids: Vec<MarketId> = [
    "sol-perp",
    "btc-perp", 
    "eth-perp",
]
.iter()
.map(|m| drift.market_lookup(m).unwrap())
.collect();

let mut swift_order_stream = drift
    .subscribe_swift_orders(&market_ids, Some(true), None, None)
    .await?;

Custom Order Routing

async fn try_fill(order: SignedOrderInfo) {
    // Route to different handlers based on order characteristics
    match (&order.order_params().market_index, order.order_params().base_asset_amount) {
        (0, amt) if amt > LARGE_SIZE => {
            handle_large_sol_order(order).await
        }
        (0, _) => {
            handle_small_sol_order(order).await
        }
        _ => {
            handle_other_order(order).await
        }
    }
}

Profitability

Maker fees are earned from:
  • Maker Rebate: Paid to makers for providing liquidity
  • Spread Capture: Difference between auction start and fill price
  • Volume Incentives: Some markets offer additional maker incentives

Next Steps

  • Implement inventory management strategies
  • Add position hedging with spot markets
  • Build a dashboard for monitoring
  • Optimize fill prices with ML models
  • Add market making on multiple venues

Build docs developers (and LLMs) love