Skip to Content
DevelopersMarket MakersOrderbook & Matching

Orderbook & Matching

Understanding how Velocity’s orderbook and matching engine work is essential for market makers. This page covers the DLOB architecture, liquidity priority, and how to access orderbook data.

What is the DLOB?

The DLOB (Decentralized Limit Order Book) is Velocity’s offchain orderbook that aggregates all resting limit orders across user accounts. It provides a unified orderbook view for matching and price discovery while keeping order storage onchain.

Why offchain?

Storing a sorted orderbook onchain would be prohibitively expensive. Instead, Velocity stores orders in user accounts (up to 32 per user), and the DLOB server:

Scan all user accounts

Read all relevant UserAccount state from chain.

Extract resting limit orders

Pull active, fillable limit orders from each account.

Sort into a bid/ask orderbook

Group and order quotes by price levels.

Serve via WebSocket and HTTP

Publish snapshots and updates for UIs and bots.

This gives you Binance-style orderbook depth without onchain sorting costs.

How matching works

When a taker order arrives, it’s matched in this liquidity priority order:

JIT Auction (first ~10 slots / ~5 seconds)

The taker order enters an auction where market makers compete to fill at the best price. This is the highest-priority liquidity source because it produces the best price discovery, and makers actively compete in realtime. The auction price interpolates from the start price toward the end price over the auction duration, creating a Dutch auction dynamic.

Why JIT first? It gives takers better prices than stale resting orders. Makers can price based on the latest oracle, inventory, and market conditions rather than prices set minutes ago.

See JIT Auctions for the full auction lifecycle and pricing math.

DLOB (Decentralized Limit Order Book)

After the JIT auction, any unfilled portion matches against resting limit orders. These are sorted by price-time priority: best price wins, and among tied prices, the earliest order fills first. DLOB liquidity is “committed”: the orders are onchain and can’t be pulled during matching.

AMM (Automated Market Maker)

If DLOB doesn’t fully fill the order, the remainder executes against Velocity’s AMM. The AMM uses a constant product curve adjusted by the oracle price, so it always provides liquidity but at worse prices (higher fees + price impact). Think of the AMM as the liquidity of last resort: it guarantees fills, but at a cost.

The AMM can also step in earlier, as a JIT maker inside a DLOB match (competing alongside resting DLOB orders for the same fill) rather than only as the final fallback. This only happens when a set of hard gates are all clear: AmmFill isn’t paused for the market, the AMM doesn’t have too much drawdown, and the oracle (including the MM oracle, when active) is valid and not too volatile. If any of these gates trips, the AMM sits out of that DLOB match entirely — the fill matches against DLOB-resting orders only, with no AMM JIT competition. This is a hard, all-or-nothing gate distinct from the auction-timing rules that govern whether the AMM fills a fresh taker order immediately (see JIT Auctions).

Spot markets have no order-book matching at all: place_spot_order / place_and_make_spot_order / fill_spot_order don’t exist on-chain, and there’s no external-DEX (Serum/Phoenix/OpenBook) fulfillment path either — both were removed. Spot markets exist only for collateral and borrow-lend; the liquidity-priority waterfall above applies to perp markets only.

The waterfall in practice:

A 10 SOL market buy might fill like this:

  • 4 SOL via JIT at oracle + 0.02% (maker competed for this)
  • 3 SOL via DLOB at oracle + 0.05% (resting limit order)
  • 3 SOL via AMM at oracle + 0.12% (price impact on the curve)

The taker gets a blended price better than any single source would provide.

What is “indicative” liquidity?

When you query the DLOB orderbook, you’ll see two types of liquidity:

  • Committed (DLOB) liquidity: real onchain resting limit orders. These are guaranteed to be available for matching at their stated price.
  • Indicative liquidity: estimated liquidity from the AMM (vAMM) at various price levels. This is what the AMM would fill at each price based on its curve, but it’s not a firm order; it’s a projection. Indicative liquidity also includes offchain indicative quotes published by market makers who signal intent to provide liquidity without committing onchain.

Why does this matter? When reading orderbook depth, indicative liquidity gives a more complete picture of available liquidity (DLOB + AMM), but only DLOB orders are firm commitments. The AMM’s price and available depth can shift with oracle updates.

In the L2 API response, the sources field on each price level tells you where the liquidity comes from. See REST API below.

DLOB architecture

DLOB responsibilities

Orderbook state:

  • Bids sorted descending by price
  • Asks sorted ascending by price
  • Grouped by price levels
  • Market depth aggregation

Order filtering:

  • Excludes expired orders
  • Filters by market type
  • Handles order flags (post-only, reduce-only)
  • Shows only valid, fillable orders
  • Excludes orders in markets that aren’t Active (or, for reduce-only-only order types, ReduceOnly) — see market status below

Real-time updates:

  • Subscribes to UserAccount changes via RPC
  • Updates orderbook when orders placed/canceled/filled
  • Publishes updates via WebSocket

Order lifecycle

User places limit order

UserAccount is updated onchain.

DLOB detects new order

The order is picked up via RPC subscriptions.

Order added to DLOB

The order is inserted at the correct price level.

Orderbook update broadcast

WebSocket clients receive refreshed depth.

Order fills

UserAccount is marked filled and DLOB removes the order.

Update broadcast

Clients receive the reduced depth after fill.

The DLOB never modifies onchain state, it’s read-only.

DLOB vs onchain state

Onchain (source of truth)

  • Orders stored in UserAccount
  • Fills processed by Velocity program
  • Settlement and PnL calculation
  • Collateral and margin checks

DLOB (aggregated view)

  • Sorted orderbook for matching
  • Depth aggregation
  • Real-time orderbook updates
  • Convenient API for UIs and bots

If DLOB goes down, orders are still onchain and valid, they just won’t be visible in the aggregated orderbook until DLOB recovers.

Market status

Every perp and spot market has a MarketStatus that gates whether it’s fillable at all: Initialized (0), Active (1), ReduceOnly (2), Settlement (3), Delisted (4). Only Active markets accept new risk-increasing orders; ReduceOnly markets accept only orders that shrink an existing position; Settlement/Delisted markets are winding down and shouldn’t appear as tradable in a market maker’s UI or bot config at all.

If you maintain a custom (non-IDL) decoder for PerpMarket/SpotMarket, note that these discriminant values are Velocity-specific — the deprecated FundingPaused/AmmPaused/FillPaused/WithdrawPaused variants that used to sit between Active and ReduceOnly were removed, so ReduceOnly/Settlement/Delisted now sit at 2/3/4 instead of 6/7/8. Decoding via the SDK’s MarketStatus class (MarketStatus.ACTIVE, etc.) or the IDL avoids this entirely — only a decoder that hardcodes the old numeric values would misread the status.

Tick size

Every perp/spot market has an orderTickSize (PerpMarketAccount.orderTickSize / SpotMarketAccount.orderTickSize, PRICE_PRECISION units). The on-chain program standardizes every auction price and oracle-offset limit price to this tick size before comparing it against anything else — long orders floor to the nearest tick, short orders ceil.

If you compute prices client-side (rather than letting the program clamp them), pass the market’s tick size through so your numbers match what the program will actually use:

  • getAuctionPrice(order, slot, oraclePrice, tickSize) (see JIT Auctions)
  • getLimitPrice(order, oraclePriceData, slot, tickSize, fallbackPrice)
  • DLOBNode.getPrice(oraclePriceData, slot, tickSize)

All four tickSize parameters are optional and default to ONE (no effective rounding) for backward compatibility — but on any market with orderTickSize > 1, omitting it means your client-computed price can disagree with the program’s by up to one tick, which is enough to misjudge whether a maker price is inside or outside the current auction range. Pass perpMarket.orderTickSize (or spotMarket.orderTickSize) explicitly.

Accessing the DLOB

REST API (L2/L3)

The hosted DLOB server provides L2 (aggregated price levels) and L3 (individual orders) endpoints. This is the simplest way to get an orderbook snapshot.

Endpoint is provisional. dlob.velocity.exchange below follows Velocity’s <sub>.velocity.exchange hosted-endpoint convention but hasn’t been confirmed as the final production hostname — check with the team before hardcoding it.

L2 orderbook (aggregated by price level):

GET https://dlob.velocity.exchange/l2?marketName=SOL-PERP&depth=10&includeVamm=true&includeIndicative=true

Parameters:

  • marketName: e.g. SOL-PERP, BTC-PERP, SOL (spot)
  • depth: number of price levels per side (default 10)
  • includeVamm=true: include AMM (vAMM) indicative liquidity in the book
  • includeIndicative=true: include offchain indicative quotes from market makers

L2 response structure:

{ "bids": [ { "price": "99500000", "size": "15000000", "sources": { "dlob": "5000000", "vamm": "10000000" } } ], "asks": [ { "price": "100500000", "size": "12000000", "sources": { "dlob": "8000000", "vamm": "4000000" } } ] }

Important: The sources field is an object mapping source names to size strings (in raw precision), not a flat string. Common source keys are "dlob" (resting limit orders) and "vamm" (AMM indicative liquidity). Prices and sizes are in raw precision (divide by PRICE_PRECISION / BASE_PRECISION).

L3 orderbook (individual orders):

GET https://dlob.velocity.exchange/l3?marketName=SOL-PERP

Returns individual orders with maker addresses, useful for identifying specific resting orders.

Fetching in TypeScript:

interface L2Level { price: string; size: string; sources: Record<string, string>; // e.g. { dlob: "5000000", vamm: "10000000" } } interface L2Response { bids: L2Level[]; asks: L2Level[]; } const response = await fetch( "https://dlob.velocity.exchange/l2?marketName=SOL-PERP&depth=10&includeVamm=true&includeIndicative=true" ); const orderbook: L2Response = await response.json(); for (const bid of orderbook.bids) { const price = Number(bid.price) / 1e6; // PRICE_PRECISION = 1e6 const size = Number(bid.size) / 1e9; // BASE_PRECISION = 1e9 const dlobSize = bid.sources.dlob ? Number(bid.sources.dlob) / 1e9 : 0; const vammSize = bid.sources.vamm ? Number(bid.sources.vamm) / 1e9 : 0; console.log(`Bid $${price.toFixed(2)}: ${size.toFixed(4)} (dlob: ${dlobSize.toFixed(4)}, vamm: ${vammSize.toFixed(4)})`); }

WebSocket (realtime updates)

For live orderbook feeds with subsecond updates:

const ws = new WebSocket("wss://dlob.velocity.exchange/ws"); ws.send(JSON.stringify({ type: "subscribe", channel: "orderbook", marketType: "perp", market: "SOL-PERP", grouping: 10 })); ws.onmessage = (event) => { const update = JSON.parse(event.data); // Handle orderbook update };

SDK DLOB class (local orderbook)

Build and maintain orderbook locally for maximum control. This subscribes directly to onchain UserAccount changes and builds the orderbook in-process, giving you the lowest latency view.

import { DLOBSubscriber, OrderSubscriber, SlotSubscriber, MarketType } from "@velocity-exchange/sdk"; const slotSubscriber = new SlotSubscriber(connection); await slotSubscriber.subscribe(); const orderSubscriber = new OrderSubscriber({ velocityClient, subscriptionConfig: { type: "websocket" }, fastDecode: true, decodeData: true, }); await orderSubscriber.subscribe(); const dlobSubscriber = new DLOBSubscriber({ velocityClient, dlobSource: orderSubscriber, slotSource: slotSubscriber, updateFrequency: 1000, }); await dlobSubscriber.subscribe(); // getBestBid/getBestAsk live on the DLOB snapshot itself, not the subscriber. // Pass the market's tick size so client-side rounding matches the on-chain standardization. const dlob = dlobSubscriber.getDLOB(); const slot = slotSubscriber.getSlot(); const perpMarket = velocityClient.getPerpMarketAccount(marketIndex); const oracleData = velocityClient.getMMOracleDataForPerpMarket(marketIndex); const bestBid = dlob.getBestBid(marketIndex, slot, MarketType.PERP, oracleData, perpMarket.orderTickSize); const bestAsk = dlob.getBestAsk(marketIndex, slot, MarketType.PERP, oracleData, perpMarket.orderTickSize); // Both are `BN | undefined` -- undefined means there's no resting-limit bid/ask right now // (this ignores AMM fallback liquidity; it's DLOB-only, by design).
Example DLOB local setupReference ↗
TypeScript docs unavailable for DLOB local setup.

Auction subscriber (JIT auctions)

Subscribe via AuctionSubscriber to get active auctions; see JIT Auctions for setup and place-and-make flow.

Detecting Swift orders

Use isSignedMsgOrder(order) to identify SWIFT-origin orders when subscribed to both SWIFT and onchain feeds. See SWIFT API for details.

DLOB matching mechanics

After the JIT auction, remaining order size matches against DLOB using price-time priority:

Best price wins

Lowest ask fills buys first; highest bid fills sells first.

Time priority for tied prices

Earlier orders fill first (FIFO at each price level).

Walk the book

Continue across price levels until fully filled or liquidity is exhausted.

Example (taker buying 10 SOL):

  • DLOB has: 3 SOL @ 100,5SOL@100, 5 SOL @ 100.10, 4 SOL @ $100.20
  • Taker fills: 3 @ 100,5@100, 5 @ 100.10, 2 @ $100.20

If DLOB doesn’t fully fill, remaining size goes to AMM.

Matching examples

Example 1: Market order (long)

Order enters JIT auction at oracle + 0.1%

The taker order starts in auction mode.

Two JIT makers fill 60% at oracle + 0.02%

Competitive makers provide the first fills.

Remaining 40% fills against DLOB at oracle + 0.05%

Residual size is matched against resting book liquidity.

AMM not needed (fully filled)

Order is fully filled before fallback liquidity is required.

Example 2: Large market order

JIT auction fills 40% at oracle + 0.03%

Best early pricing comes from active makers.

DLOB fills 30% at oracle + 0.08%

Next portion matches against resting orders.

AMM fills remaining 30% at oracle + 0.15% (price impact)

Final size is absorbed by AMM with more slippage.

Example 3: Limit order at DLOB price

Order bypasses JIT (not crossing spread)

Since it does not cross, it does not trigger auction flow.

Rests on DLOB as maker order

Order is posted to the book and waits for taker flow.

Earns maker rebate when filled

Fill is credited as maker liquidity.

Performance characteristics

DLOB performance:

  • Latency: 1-2 seconds from onchain placement to DLOB update
  • Depth: Aggregates thousands of orders across hundreds of users
  • Uptime: Redundant servers ensure high availability
  • WebSocket: Handles thousands of concurrent connections

For low-latency bots:

Subscribing directly to onchain UserAccounts (via RPC or gRPC) can be faster than waiting for DLOB updates. The SDK’s OrderSubscriber with type: "websocket" gives you the rawest feed.

Gotchas

  • includeVamm=true is essential: without it, the L2 endpoint only shows DLOB resting orders, which may be sparse for smaller markets. The vAMM provides significant depth. Always include it for a realistic view of available liquidity.
  • includeIndicative=true for full picture: indicative quotes from market makers (see Indicative Quotes) are only included when this flag is set. Without it, you may underestimate available liquidity.
  • Prices are raw precision: L2 response prices are in PRICE_PRECISION (1e6) and sizes in BASE_PRECISION (1e9). Forgetting to divide produces nonsensical numbers.
  • L2 sources is an object, not a string: each price level’s sources field maps source names to size strings. Don’t try to parse it as a flat value.
  • DLOB server can lag: the hosted DLOB server typically updates within 1-2 seconds of onchain changes, but during high load it can lag further. For latency-sensitive strategies, build the orderbook locally using DLOBSubscriber + OrderSubscriber.
  • Oracle offset orders in the DLOB: oracle offset orders appear at their effective price (oracle + offset) in the DLOB. When the oracle moves, their DLOB price updates automatically. This means the orderbook shifts without any onchain transactions.
Last updated on