Matching Engine
The matching engine lives entirely in memory, maintaining a separate OrderBook for each token ID. When an order arrives, the engine tries to match it against available resting liquidity, produces fill records, and hands the outcome back to the order route for database persistence.
Architecture
Section titled “Architecture”The engine is a MatchingEngine struct backed by a DashMap<String, RwLock<OrderBook>>, where the key is the token ID. This layout supports concurrent reads across different tokens while serializing writes within each individual book.
Each OrderBook maintains two sides:
| Side | Data Structure | Ordering |
|---|---|---|
| Bids | BTreeMap<Reverse<Decimal>, VecDeque<OrderEntry>> | Highest price first |
| Asks | BTreeMap<Decimal, VecDeque<OrderEntry>> | Lowest price first |
Orders sitting at the same price level are queued FIFO — the earliest arrival fills first. Together, this implements price-time priority: the most aggressive price always wins, and ties break by arrival time.
Order Submission
Section titled “Order Submission”When submit_order() runs:
- The engine locates (or creates) the
OrderBookfor the given token - An
OrderEntryis built from the order ID, owner, price, size, order type, and expiration - The book’s
submit()method tries to match:- BUY orders cross against asks priced at or below the order price
- SELL orders cross against bids priced at or above the order price
- Each crossing produces a
Fillcontaining maker order ID, taker order ID, price, and size - Whatever size remains is placed on the book as a resting order (for GTC/GTD types)
- The engine returns a
SubmitResultwith fills, remaining size, and status
Status Values
Section titled “Status Values”| Status | Meaning |
|---|---|
LIVE | Order is resting on the book with unfilled size |
MATCHED | Order was fully filled immediately |
CANCELED | Order was rejected (post-only crossing, FOK not fully filled) |
Post-Only Orders
Section titled “Post-Only Orders”A post-only order is guaranteed to land on the book as a maker. If a post-only BUY would cross the best ask (or a post-only SELL would cross the best bid), the engine rejects it outright with status CANCELED instead of letting it execute as a taker.
// Server-side check in submit_post_only():let would_cross = match side { "BUY" => best_ask.map_or(false, |ask| entry.price >= ask), _ => best_bid.map_or(false, |bid| entry.price <= bid),};if would_cross { return (vec![], entry.remaining_size, "CANCELED");}Order Types and Matching Behavior
Section titled “Order Types and Matching Behavior”| Type | Matching | Remainder |
|---|---|---|
| GTC | Fill what crosses, rest the remainder | Rests on book until filled or cancelled |
| GTD | Same as GTC | Rests until filled, cancelled, or expiration |
| FOK | Must fill entirely or not at all | Rejected if insufficient resting liquidity |
| FAK | Fill what is available immediately | Unfilled portion is cancelled |
GTD Expiration
Section titled “GTD Expiration”A periodic sweep across all books removes expired GTD orders through expire_orders(now_epoch). Any order whose expiration timestamp has passed gets pulled from the book, and its ID is returned so the corresponding database record can be marked CANCELED.
Weekly Restart
Section titled “Weekly Restart”The matching engine goes through a scheduled weekly restart for updates and memory reclamation.
| Detail | Value |
|---|---|
| Schedule | Every Tuesday at 11:00 UTC |
| Duration | Approximately 60-90 seconds |
| Behavior | clear_all() wipes in-memory books, then orders are restored from the database |
While the restart is in progress, the API responds with HTTP 425 (Too Early) on order-related endpoints. Clients should retry with exponential backoff.
Retry Example
Section titled “Retry Example”use std::time::Duration;
let mut delay = Duration::from_secs(1);
for _ in 0..10 { let order = client .limit_order() .token_id(token_id) .price(price) .size(size) .side(side) .build() .await?; let signed = client.sign(&signer, order).await?;
match client.post_order(signed).await { Ok(response) => return Ok(response), Err(e) if e.is_status(425) => { eprintln!("Engine restarting, retrying in {delay:?}..."); tokio::time::sleep(delay).await; delay = (delay * 2).min(Duration::from_secs(30)); continue; } Err(e) => return Err(e), }}Order Restoration from DB
Section titled “Order Restoration from DB”At startup (and after each weekly restart), the engine rebuilds its in-memory state by calling restore_order(). Every order with status LIVE is loaded from the database and inserted into the appropriate book side without triggering any matching logic.
The restore() method on OrderBook slots the entry directly into the bids or asks structure, keeping the original created_at timestamp intact so time priority remains accurate.
// During server startup:engine.restore_order( token_id, condition_id, side, OrderEntry { order_id, owner, maker_address, price, remaining_size: original_size - size_matched, order_type, expiration, created_at, },);Engine Methods
Section titled “Engine Methods”| Method | Description |
|---|---|
submit_order() | Match and place an order |
cancel_order() | Remove an order from the in-memory book |
restore_order() | Re-insert a resting order (startup) |
best_bid() | Highest bid price for a token |
best_ask() | Lowest ask price for a token |
midpoint() | Average of best bid and best ask |
spread() | Difference between best ask and best bid |
depth() | All bid and ask price levels with aggregated sizes |
implied_price() | Midpoint price used as the implied probability |
expire_orders() | Remove expired GTD orders across all books |
clear_all() | Wipe all in-memory books (weekly restart) |
Next Steps
Section titled “Next Steps”- Orderbook — Read the orderbook via the REST API -> orderbook
- Orders Overview — Order types and states -> orders/overview