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.
Building market-making systems? See MM 3-Layer Stack for mm-types, mm-control-client, and template crates.
Why WASM?
| Property | Benefit |
|---|---|
| Fast | Near-native execution (~1μs per callback) |
| Safe | Sandboxed - can't access filesystem or network |
| Portable | Same binary runs on any CPU/OS |
| Small | Typically 10-50KB per algo |
| Instant | No cold start after initial compile |
CLI Setup
Install the CLI and log in before building algos. See the full CLI Install & Login guide.
curl -fsSL https://raw.githubusercontent.com/Bai-Funds/algo-sdk/main/install.sh | sh
sequence loginSDK Installation
Add the SDK to your Cargo.toml (or use sequence init to scaffold a project automatically):
[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:
rustup target add wasm32-unknown-unknownThe Algo Trait
Every algorithm implements this trait:
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.
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_bookagainst its own venue's book (no extra network hop to act on local liquidity) - Optionally receives
on_nbbowith 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.
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
| Field | Description |
|---|---|
nbbo_bid_px_1e9 / nbbo_ask_px_1e9 | Global best bid/ask across all venues |
nbbo_bid_venue / nbbo_ask_venue | Array 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_ct | Number 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:
| Field | Description |
|---|---|
books.merged | Cross-venue aggregated L2Book (same-price sizes summed across all venues) |
books.book_ct | Number 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 VenueIdhas_depth_for(venue_id)- returnstrueif the venue has bid or ask levelscex_count()/dex_count()- count of CEX vs DEX venues with databook_at_slot(i)/venue_id_at(i)- direct slot access
// 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 levelsPoolBooks (DEX)
For DEX-focused strategies, PoolBooks provides per-pool order books for individual liquidity pools. Read from the fixed WASM memory offset:
let pool_books = unsafe { &*(POOL_BOOKS_WASM_OFFSET as *const PoolBooks) };| Field | Description |
|---|---|
pool_books.pool_ct | Number 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:
| Field | Description |
|---|---|
address | 32-byte pool address (EVM uses first 20, Solana uses all 32) |
fee_bps | Pool fee in basis points |
venue_id | Chain: VENUE_DEX_ARB, VENUE_DEX_SOL, etc. |
protocol_id | Protocol: 1=Uniswap V2, 2=Uniswap V3, 3=Curve, 4=Balancer V2, 5=Aerodrome, 6=Velodrome, 7=Camelot, 8=Raydium CLMM, 9=Orca Whirlpool |
pair_index | 0 for standard 2-token pools |
Helper methods: book_for_pool(addr, pair_index), book_at_slot(i), meta_at_slot(i).
Minimal Example
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
Market data streams in
Exchange sends order book updates via WebSocket. We maintain 20 levels of depth.
Your algo receives updates
Every book change triggers on_book() with the latest L2Book and your AlgoState.
You return actions
Write orders to the Actions buffer (max 16 per callback).
Server executes
Orders pass through risk checks and are sent to the exchange.
You receive callbacks
on_fill() for fills, on_reject() for rejections.
Multi-Venue (Mesh)
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.
Every instance sees its local book
on_book fires on the hosting edge's local updates — zero network hop to act on local liquidity.
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.
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.
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.
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:
| Suffix | Scale | Example | Human |
|---|---|---|---|
_1e9 | × 10⁹ | 50_000_500_000_000 | $50,000.50 |
_1e8 | × 10⁸ | 150_000_000 | 1.5 BTC |
_ns | nanoseconds | 1705420800000000000 | Unix timestamp |
_bps | basis points | 10 | 0.1% |
Mixing up _1e8 and _1e9 will make your orders 10x wrong. Double-check your math!
Conversion Examples
// 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.05Build and Deploy
sequence init my-algo && cd my-algo
sequence build
sequence deploy BTC-USD --startSee the full CLI command reference for all deploy, start, stop, logs, and stats commands. For bundle-based deployment, see Bundles & Promotions.
Constraints
| Rule | Limit | Reason |
|---|---|---|
| Max actions per callback | 16 | Buffer size |
| WASM fuel budget | 1,000,000 ops | Prevent infinite loops |
| Max open orders | 32 | Memory layout |
| Heap allocation | Avoid on hot path | Performance |
| Blocking calls | Not allowed | Would stall execution |
Performance Tips
Do
// 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
// 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 thisSDK 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:
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,
});