Phoenix: the crankless on-chain order book, and the perps extension
Phoenix is Solana's crankless on-chain limit order book — atomic fills, no separate crank, all in one Rust program. Here's the market architecture and design.
Phoenix is the cleanest fully on-chain limit order book on Solana. Its headline innovation: it's crankless. Earlier on-chain order books (Serum/OpenBook) needed a separate "crank" process to match resting orders and settle fills. Phoenix settles atomically inside the taker's own transaction — no crank, no separate settlement step, no MEV window between match and settle.
Why the crank was a problem
Serum-style books worked like this: makers post orders; takers post orders; a permissionless "crank" transaction later matches them and pushes fills to an event queue; another step settles balances. Three issues:
- Latency. Your fill wasn't final until the crank ran — could be several slots later.
- MEV. The gap between match and settle was a window for extraction.
- Liveness dependence. If nobody cranked, the book stalled.
Phoenix's crankless design
Phoenix collapses match + settle into the taker's instruction. When you send a market order (or a marketable limit order):
Taker submits an order in transaction T:
1. Phoenix walks the resting order book in-program
2. Matches against resting maker orders, computing fills
3. Atomically credits/debits both the taker AND the matched makers'
balances — all inside transaction T
4. Resting orders that got filled are removed from the book
5. Any unfilled taker remainder rests on the book (for limit) or
is cancelled (for IOC/market)
No crank. No event queue to drain. Fill is final when T confirms.The makers don't need to be online or sign anything at fill time — their resting orders already committed their funds to the market. Phoenix moves the funds on their behalf when a taker crosses their price.
The account model
// Market account — the order book itself
pub struct MarketHeader {
pub base_mint: Pubkey, // e.g. SOL
pub quote_mint: Pubkey, // e.g. USDC
pub base_vault: Pubkey, // token vault holding deposited base
pub quote_vault: Pubkey, // token vault holding deposited quote
pub base_lot_size: u64, // smallest tradeable base increment
pub quote_lot_size: u64, // smallest price increment
pub tick_size: u64,
pub authority: Pubkey,
pub fee_recipient: Pubkey,
// followed by the bids tree + asks tree + trader registry (in account data)
}
// Seat — a trader's authorization to place orders on a market
// (Phoenix gates makers via seats; takers can trade without one)
pub struct Seat {
pub market: Pubkey,
pub trader: Pubkey,
pub approval_status: SeatApprovalStatus,
}The order book lives entirely in the market account's data — a pair of red-black-tree-like structures for bids and asks, plus a trader registry tracking each participant's locked and free balances. Reading the book is a single getAccountInfo + deserialize.
Placing orders
import { Client, Side, OrderPacket } from "@ellipsis-labs/phoenix-sdk"
import { Connection, Keypair } from "@solana/web3.js"
const conn = new Connection("https://api.mainnet-beta.solana.com")
const client = await Client.create(conn)
const market = client.marketStates.get(marketAddress)!
// A limit order: rest on the book at a price
const limitOrder: OrderPacket = {
__kind: "Limit",
side: Side.Bid,
priceInTicks: market.floatPriceToTicks(142.50),
numBaseLots: market.rawBaseUnitsToBaseLots(10), // 10 SOL
selfTradeBehavior: 0,
matchLimit: null,
clientOrderId: 0,
useOnlyDepositedFunds: false,
}
const placeIx = client.createPlaceLimitOrderInstruction(limitOrder, marketAddress, trader.publicKey)
// An immediate-or-cancel market buy: cross the book now
const marketOrder: OrderPacket = {
__kind: "ImmediateOrCancel",
side: Side.Bid,
priceInTicks: null, // null = take any price
numBaseLots: market.rawBaseUnitsToBaseLots(5),
minBaseLotsToFill: 0,
numQuoteLots: 0,
minQuoteLotsToFill: 0,
selfTradeBehavior: 0,
matchLimit: null,
clientOrderId: 0,
useOnlyDepositedFunds: false,
}Orders are priced and sized in lots — integer multiples of the market's base_lot_size and quote_lot_size. This keeps all arithmetic in integers (no float drift in the matching engine) and bounds the precision of the book.
Reading the live book
const ladder = market.getUiLadder(10) // top 10 levels each side
console.log("bids:", ladder.bids) // [{ price, quantity }, …]
console.log("asks:", ladder.asks)
// Or get the L3 (per-order) view
const book = market.getLadder()The perps extension
Phoenix's core is a spot CLOB. The perps layer builds the same crankless matching primitive into a derivatives market — positions instead of spot balances, funding payments instead of settlement, mark-price from the book combined with an oracle. The order-matching engine underneath is the same atomic, crankless design; the difference is what gets settled (position deltas + funding vs token balances).
Why this still matters
- It proved fully on-chain CLOBs are viable. The conventional wisdom was "order books need an off-chain sequencer." Phoenix showed you can do it on-chain on Solana with atomic settlement.
- No MEV from match/settle gaps. The atomic fill means there's no window to sandwich between matching and settlement.
- Composable. Because fills are atomic, another program can CPI into Phoenix and know the fill is final within the same transaction — critical for aggregators and structured products.
References
Phoenix is the reference design for crankless on-chain order books. Whether you trade on it, build on it, or just want to understand how a fully-on-chain CLOB settles atomically — the crankless model above is the core idea.