Sequence/docs
ALGO SDK

Write algos. Compile to WASM. Deploy to the edge.

Custom trading algorithms in Rust, running on Sequence edge servers with sub-20 ms latency — co-located with the exchange, sandboxed, and drivable from a single on_book callback.

L2 Order Book

20 levels of bid/ask depth, live on every callback.

Algo State

Server-managed position and open orders — authoritative.

Actions & Orders

Place and cancel orders. Up to 16 actions per callback.

PnL & Timing

Realized / unrealized PnL, fill latency, round-trip timing.

Logging

HFT-safe, non-blocking logs from inside the hot path.

Strategies & Mesh

Deploy the same algo to many venues and coordinate via messages.

Backtesting

Replay recorded market data through your WASM. Deterministic.

Examples

Full working algos you can deploy as-is.

Note

Building market-making systems? See MM 3-Layer Stack for mm-types, mm-control-client, and template crates.


Why WASM?

PropertyBenefit
FastNear-native execution (~1μs per callback)
SafeSandboxed - can't access filesystem or network
PortableSame binary runs on any CPU/OS
SmallTypically 10-50KB per algo
InstantNo cold start after initial compile

CLI Setup

Install the CLI and log in before building algos. See the full CLI Install & Login guide.

bash
curl -fsSL https://raw.githubusercontent.com/Bai-Funds/algo-sdk/main/install.sh | sh
sequence login

SDK Installation

Add the SDK to your Cargo.toml (or use sequence init to scaffold a project automatically):

toml
[package]
name = "my-algo"
version = "0.1.0"
edition = "2021"
 
[lib]
crate-type = ["cdylib"]
 
[dependencies]
sequence-algo-sdk = { version = "0.3", default-features = false }
 
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

Install the WASM target:

bash
rustup target add wasm32-unknown-unknown

The Algo Trait

Every algorithm implements this trait:

rust
pub trait Algo: Send {
    /// Called on every order book update (100-1000+ times/second).
    fn on_book(
        &mut self,
        book: &L2Book,
        state: &AlgoState,
        features: &OnlineFeatures,
        actions: &mut Actions,
    );
 
    /// Called when your order fills. Default: no-op.
    fn on_fill(&mut self, _fill: &Fill, _state: &AlgoState) {}
 
    /// Called when your order is rejected. Default: no-op.
    fn on_reject(&mut self, _reject: &Reject) {}
 
    /// Shutdown — cancel orders and flatten here. Default: no-op.
    fn on_shutdown(&mut self, _state: &AlgoState, _actions: &mut Actions) {}
 
    /// Heartbeat (1 Hz, optional). Default: no-op.
    fn on_heartbeat(
        &mut self,
        _state: &AlgoState,
        _features: &OnlineFeatures,
        _actions: &mut Actions,
    ) {}
 
    /// Mesh: message from another instance. Default: no-op.
    fn on_message(
        &mut self,
        _from: &str,
        _payload: &[u8],
        _state: &AlgoState,
        _actions: &mut Actions,
    ) {}
 
    /// Mesh: cross-venue NBBO. Default: no-op.
    fn on_nbbo(
        &mut self,
        _nbbo: &NbboSnapshot,
        _books: &VenueBooks,
        _state: &AlgoState,
        _features: &OnlineFeatures,
        _actions: &mut Actions,
    ) {}
}

Single-venue algos only need to implement on_book. Mesh (multi-venue) algos add on_message and optionally on_nbbo — the runtime auto-detects which callbacks your WASM exports and wires them up.

Warning

All callbacks run on a dedicated CPU core on the hot path. Keep them fast:

  • No heap allocations
  • No blocking calls
  • No panics

Multi-Venue (Mesh) Algos

For strategies that trade across multiple exchanges simultaneously — cross-venue arbitrage, spread capture, portfolio rebalancing — deploy the same Algo type as separate instances on different venue edges and have them communicate via labeled messages. See the full Strategies & Mesh guide.

The mesh model keeps execution local to each venue. Every instance:

  • Runs on_book against its own venue's book (no extra network hop to act on local liquidity)
  • Optionally receives on_nbbo with the cross-venue picture
  • Exchanges opaque payloads (up to 256 bytes) with siblings via messaging::send(label, payload)on_message(from_label, payload, …)

The CC routes messages between edges (same-region ~1–2 ms, cross-region over the peer link ~50–100 ms). No shared Strategy/Executor separation — one trait, deployed N times.

rust
use algo_sdk::*;
 
struct MyArb { /* ... */ }
 
impl Algo for MyArb {
    fn on_book(&mut self, book: &L2Book, state: &AlgoState,
               features: &OnlineFeatures, actions: &mut Actions) {
        // Act on local book. Send an intent to the hedger instance if needed.
        // messaging::send("hedger", &payload);
    }
 
    fn on_nbbo(&mut self, nbbo: &NbboSnapshot, books: &VenueBooks,
               state: &AlgoState, features: &OnlineFeatures,
               actions: &mut Actions) {
        // Cross-venue visibility — spot cross-market opportunities.
    }
 
    fn on_message(&mut self, from: &str, payload: &[u8],
                  state: &AlgoState, actions: &mut Actions) {
        // Another instance told us something. React locally.
    }
}
 
export_algo!(MyArb { /* ... */ });

Deploy the same WASM bundle to multiple venues with a [deploy] block per instance in Sequence.toml, each with a distinct label — the label is what sibling instances see in on_message(from, …) and pass to messaging::send(label, …).

Market data types

Strategies receive the same consolidated market data as the rest of the system:

NbboSnapshot

FieldDescription
nbbo_bid_px_1e9 / nbbo_ask_px_1e9Global best bid/ask across all venues
nbbo_bid_venue / nbbo_ask_venueArray slot index of the venue with the best bid/ask
venue_ids[slot]VenueId for each slot (use for leg targeting)
venue_bid_px[slot] / venue_ask_px[slot]Per-venue BBO prices
venue_update_ms[slot]Milliseconds since last update (staleness)
venue_ctNumber of active venues

Important: nbbo_bid_venue is a slot index (0..venue_ct-1), not a VenueId. To get the VenueId: nbbo.venue_ids[nbbo.nbbo_bid_venue as usize].

Helper methods: nbbo_spread_bps(), is_crossed(), is_venue_stale(slot, max_ms), slot_for_venue(venue_id).

VenueBooks

The books parameter gives you full 20-level depth per venue:

FieldDescription
books.mergedCross-venue aggregated L2Book (same-price sizes summed across all venues)
books.book_ctNumber of valid per-venue books
books.venue_ids[i]VenueId for slot i (e.g. VENUE_KRAKEN, VENUE_DEX_ARB)
books.books[i]Full L2Book for venue at slot i

Helper methods:

  • book_for_venue(venue_id) - look up the L2Book for a specific venue by VenueId
  • has_depth_for(venue_id) - returns true if the venue has bid or ask levels
  • cex_count() / dex_count() - count of CEX vs DEX venues with data
  • book_at_slot(i) / venue_id_at(i) - direct slot access
rust
// Access per-venue depth
if let Some(kraken) = books.book_for_venue(VENUE_KRAKEN) {
    log_info!("kraken: {} bids, {} asks, mid={}",
        kraken.bid_ct, kraken.ask_ct, kraken.mid_px_1e9());
}
 
// Use merged cross-venue depth for sizing
let total_bid_depth = books.merged.bid_depth_1e8(5); // top 5 levels

PoolBooks (DEX)

For DEX-focused strategies, PoolBooks provides per-pool order books for individual liquidity pools. Read from the fixed WASM memory offset:

rust
let pool_books = unsafe { &*(POOL_BOOKS_WASM_OFFSET as *const PoolBooks) };
FieldDescription
pool_books.pool_ctNumber of valid pool slots (max 32)
pool_books.metas[i]PoolMeta for slot i: address, fee_bps, venue_id, protocol_id
pool_books.books[i]L2Book for pool at slot i

PoolMeta fields:

FieldDescription
address32-byte pool address (EVM uses first 20, Solana uses all 32)
fee_bpsPool fee in basis points
venue_idChain: VENUE_DEX_ARB, VENUE_DEX_SOL, etc.
protocol_idProtocol: 1=Uniswap V2, 2=Uniswap V3, 3=Curve, 4=Balancer V2, 5=Aerodrome, 6=Velodrome, 7=Camelot, 8=Raydium CLMM, 9=Orca Whirlpool
pair_index0 for standard 2-token pools

Helper methods: book_for_pool(addr, pair_index), book_at_slot(i), meta_at_slot(i).


Minimal Example

rust
use algo_sdk::*;
 
struct MyAlgo {
    next_id: u64,
}
 
impl Algo for MyAlgo {
    fn on_book(
        &mut self,
        book: &L2Book,
        state: &AlgoState,
        _features: &OnlineFeatures,
        actions: &mut Actions,
    ) {
        // Your trading logic here
        if book.spread_bps() > 10 && state.is_flat() {
            self.next_id += 1;
            actions.buy(self.next_id, 10_000_000, book.bids[0].px_1e9);
        }
    }
 
    fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
        log_info!("Filled {} @ {}, pos={}",
            fill.qty_1e8, fill.px_1e9, state.position_1e8);
    }
 
    fn on_reject(&mut self, reject: &Reject) {
        log_warn!("Rejected: {} - {}", reject.order_id, reject.reason());
    }
 
    fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
        actions.cancel_all(state);
    }
}
 
// This macro generates the WASM exports
export_algo!(MyAlgo { next_id: 0 });

Data Flow

Single-Venue

1

Market data streams in

Exchange sends order book updates via WebSocket. We maintain 20 levels of depth.

2

Your algo receives updates

Every book change triggers on_book() with the latest L2Book and your AlgoState.

3

You return actions

Write orders to the Actions buffer (max 16 per callback).

4

Server executes

Orders pass through risk checks and are sent to the exchange.

5

You receive callbacks

on_fill() for fills, on_reject() for rejections.

Multi-Venue (Mesh)

1

Deploy the same WASM to multiple edges

Each [deploy] block in Sequence.toml names a venue and a sibling label. The same algo runs independently on each edge.

2

Every instance sees its local book

on_book fires on the hosting edge's local updates — zero network hop to act on local liquidity.

3

Optionally, cross-venue visibility

If your WASM exports algo_on_nbbo, the CC pushes the aggregated cross-venue NbboSnapshot + VenueBooks to each instance so you can spot cross-market opportunities.

4

Instances message each other

Call messaging::send(label, payload) from any callback. Payloads are opaque bytes (≤256). The CC routes them to the target instance.

5

Siblings react locally

The receiving instance's on_message(from, payload, …) fires on its edge. It places orders against its own OMS — no relay back through the sender.

6

Latency

Same-region mesh messages land in ~1–2 ms. Cross-region (via peer link) in ~50–100 ms.


Units & Scaling

All values use fixed-point integers for precision and speed:

SuffixScaleExampleHuman
_1e9× 10⁹50_000_500_000_000$50,000.50
_1e8× 10⁸150_000_0001.5 BTC
_nsnanoseconds1705420800000000000Unix timestamp
_bpsbasis points100.1%
Warning

Mixing up _1e8 and _1e9 will make your orders 10x wrong. Double-check your math!

Conversion Examples

rust
// Price: $50,000.50 -> 50_000_500_000_000
let price_1e9 = 50_000_500_000_000u64;
let price_f64 = price_1e9 as f64 / 1e9; // 50000.5
 
// Quantity: 1.5 BTC -> 150_000_000
let qty_1e8 = 150_000_000i64;
let qty_f64 = qty_1e8 as f64 / 1e8; // 1.5
 
// Spread: 5 bps -> 0.05%
let spread_bps = 5u32;
let spread_pct = spread_bps as f64 / 100.0; // 0.05

Build and Deploy

bash
sequence init my-algo && cd my-algo
sequence build
sequence deploy BTC-USD --start

See the full CLI command reference for all deploy, start, stop, logs, and stats commands. For bundle-based deployment, see Bundles & Promotions.


Constraints

RuleLimitReason
Max actions per callback16Buffer size
WASM fuel budget1,000,000 opsPrevent infinite loops
Max open orders32Memory layout
Heap allocationAvoid on hot pathPerformance
Blocking callsNot allowedWould stall execution

Performance Tips

Do

rust
// Direct array access (fast)
let bid_px = book.bids[0].px_1e9;
 
// Pre-compute values
let threshold = self.config.spread_bps;
 
// Early returns
if book.spread_bps() < 5 { return; }

Don't

rust
// Heap allocation (slow)
let prices: Vec<u64> = book.bids.iter().map(|l| l.px_1e9).collect();
 
// String formatting in hot path
let msg = format!("Price: {}", book.bids[0].px_1e9); // Allocates!
 
// Complex computation every tick
let vwap = calculate_vwap_from_scratch(); // Cache this

SDK Reference

L2 Book

20 levels of order book depth, spread, microprice

Algo State

Position, open orders, PnL tracking

Actions

Limit, market, IOC, FOK, post-only orders + cancels

PnL & Timing

PnL snapshots, fill latency, timing primitives

Logging

Non-blocking logging for debugging

Strategies & Executors

Sharded multi-venue execution with parallel legs

Examples

Market maker, momentum, grid, TWAP


Example: Speed Test Algo

This algo measures round-trip latency by executing buy/sell pairs:

rust
use algo_sdk::*;
 
struct SpeedTest {
    next_id: u64,
    trips_target: u32,
    trips_completed: u32,
    current_trip: u32,
    awaiting_buy_fill: bool,
    awaiting_sell_fill: bool,
    size_1e8: i64,
    buy_times: [u64; 10],
    sell_times: [u64; 10],
    last_order_ns: u64,
}
 
impl Algo for SpeedTest {
    fn on_book(
        &mut self,
        book: &L2Book,
        _state: &AlgoState,
        _features: &OnlineFeatures,
        actions: &mut Actions,
    ) {
        if self.trips_completed >= self.trips_target { return; }
        if self.awaiting_buy_fill || self.awaiting_sell_fill { return; }
        
        // Start new trip with a buy
        self.current_trip = self.trips_completed;
        self.next_id += 1;
        self.last_order_ns = book.recv_ns;
        self.awaiting_buy_fill = true;
        
        actions.market_buy(self.next_id, self.size_1e8);
        log_info!("BUY: trip={} order={}", self.current_trip + 1, self.next_id);
    }
 
    fn on_fill(&mut self, fill: &Fill, _state: &AlgoState) {
        let latency_ns = fill.recv_ns.saturating_sub(self.last_order_ns);
        let latency_ms = latency_ns / 1_000_000;
        
        if self.awaiting_buy_fill {
            self.buy_times[self.current_trip as usize] = latency_ms;
            self.awaiting_buy_fill = false;
            self.awaiting_sell_fill = true;
            
            // Immediately sell
            self.next_id += 1;
            self.last_order_ns = fill.recv_ns;
            log_warn!("BUY FILL: latency={}ms, sending SELL", latency_ms);
            
        } else if self.awaiting_sell_fill {
            self.sell_times[self.current_trip as usize] = latency_ms;
            self.awaiting_sell_fill = false;
            self.trips_completed += 1;
            
            let buy_ms = self.buy_times[self.current_trip as usize];
            let total_ms = buy_ms + latency_ms;
            log_warn!("ROUND TRIP #{}: {}ms (buy={}ms sell={}ms)",
                self.trips_completed, total_ms, buy_ms, latency_ms);
        }
    }
 
    fn on_reject(&mut self, reject: &Reject) {
        log_error!("REJECT: {}", reject.reason());
        self.awaiting_buy_fill = false;
        self.awaiting_sell_fill = false;
    }
    
    fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions) {
        actions.cancel_all(state);
        log_warn!("=== FINAL: {} trips completed ===", self.trips_completed);
    }
}
 
export_algo!(SpeedTest {
    next_id: 5000,
    trips_target: 5,
    trips_completed: 0,
    current_trip: 0,
    awaiting_buy_fill: false,
    awaiting_sell_fill: false,
    size_1e8: 200_000_000, // 2 units
    buy_times: [0; 10],
    sell_times: [0; 10],
    last_order_ns: 0,
});