Velocity ProtocolDevelopers
Velocity SDK

DLOB (Decentralized Limit Order Book)

The DLOB is to be removed. An order will rest in one CLOB market account instead of in the User account of its owner. The fill route already moves that way: a taker remainder that can rest goes onto the book, not into User.orders. This page describes the DLOB as it works now. New work belongs on the CLOB book. See PropAMM and CLOB Order Flow.

What is the DLOB?

The Decentralized Limit Order Book (DLOB) is Velocity's onchain representation of all resting limit orders across all users. Unlike a traditional centralized order book maintained by an exchange, the DLOB is constructed locally by reading onchain user accounts and aggregating their open limit orders into a price-ordered book.

When a new order arrives, keepers and market makers query the DLOB to find matching resting orders. Velocity's matching engine then executes fills between the incoming taker and the resting makers on the DLOB, or routes to the AMM as a fallback.

Velocity removed spot DLOB trading: place_spot_order, place_and_take_spot_order, place_and_make_spot_order, and fill_spot_order no longer exist onchain (calls now fail with SpotDlobTradingDisabled). Spot markets still exist for collateral, borrow-lend, and swaps, but they can no longer be traded on an order book. In practice the DLOB only ever contains MarketType.PERP orders: the APIs below still accept a marketType parameter for compatibility, but a MarketType.SPOT query only ever returns an empty book.

Three kinds of integration read it. Market makers quote against the current best bid and ask and react to order flow. Keeper and filler bots scan it for matchable perp orders to fill for the fee reward. Orderbook UIs render the aggregated L2 or L3 view of a perp market.

SDK Usage

A local DLOB is assembled from three subscribers, and all three must be running before any query returns a useful answer.

ClassRole
OrderSubscriberThe raw feed. Subscribes to every open user order over websocket, polling, or gRPC, and emits orderCreated, userUpdated, and updateReceived.
SlotSubscriberThe clock. Tracks the current Solana slot, which the book needs to price auctions and drop expired orders.
DLOBSubscriberThe book. Rebuilds an aggregated DLOB from the order feed on a fixed interval and serves L2 and L3 views off it.
DLOBThe data structure itself, with bid and ask sides and the query methods. Reach it through dlobSubscriber.getDLOB() rather than constructing one.

UserMap is the related cache for accounts rather than orders. Liquidation bots and anything that has to watch positions across the whole protocol use it instead of, or alongside, OrderSubscriber. See SDK Internals.

Setting up a local DLOB

The full sequence, in dependency order. updateFrequency is the rebuild interval in milliseconds: lower it for a market-making loop, raise it for a dashboard.

import { DLOBSubscriber, OrderSubscriber, SlotSubscriber } from "@velocity-exchange/sdk";

// 1. Track the current slot, needed for order expiry and auction pricing.
const slotSubscriber = new SlotSubscriber(connection);
await slotSubscriber.subscribe();

// 2. Subscribe to all open orders across all users.
const orderSubscriber = new OrderSubscriber({
  velocityClient,
  subscriptionConfig: { type: "websocket" },
  fastDecode: true,
  decodeData: true,
});
await orderSubscriber.subscribe();

// 3. Build and maintain the book from the order stream.
const dlobSubscriber = new DLOBSubscriber({
  velocityClient,
  dlobSource: orderSubscriber, // feeds from the OrderSubscriber above
  slotSource: slotSubscriber,  // needed for order expiry and timing
  updateFrequency: 1000,       // rebuild every 1,000 ms
});
await dlobSubscriber.subscribe();

const dlob = dlobSubscriber.getDLOB();

Call unsubscribe() on all three on shutdown so the websocket handles and rebuild interval are released.

Getting L2 orderbook data

Once subscribed, query the aggregated L2 orderbook: price levels with cumulative size at each.

dlob.getL2() on its own returns resting-limit maker liquidity only: it does not include the vAMM. For a book that matches what a taker actually trades against, call dlobSubscriber.getL2({ ..., includeVamm: true }) instead, or pass getVammL2Generator(...) output as fallbackL2Generators when calling dlob.getL2() directly.

import { BASE_PRECISION, MarketType, PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";

const marketIndex = 0; // SOL-PERP

// Includes vAMM liquidity alongside resting-limit makers.
const l2 = dlobSubscriber.getL2({
  marketIndex,
  marketType: MarketType.PERP,
  includeVamm: true,
});

// l2.bids and l2.asks are arrays of { price: BN, size: BN }
console.log("Top bid:", convertToNumber(l2.bids[0].price, PRICE_PRECISION),
            "size:", convertToNumber(l2.bids[0].size, BASE_PRECISION));
console.log("Top ask:", convertToNumber(l2.asks[0].price, PRICE_PRECISION),
            "size:", convertToNumber(l2.asks[0].size, BASE_PRECISION));

Getting best bid and ask

For the top of book without building the full L2 array:

import { MarketType, PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";

const dlob = dlobSubscriber.getDLOB();
const marketIndex = 0;
const slot = slotSubscriber.getSlot();
const oraclePriceData = velocityClient.getMMOracleDataForPerpMarket(marketIndex, slot);

// Returns BN | undefined (undefined if no orders on that side)
const bestBid = dlob.getBestBid(marketIndex, slot, MarketType.PERP, oraclePriceData);
const bestAsk = dlob.getBestAsk(marketIndex, slot, MarketType.PERP, oraclePriceData);

if (bestBid && bestAsk) {
  console.log("Best bid:", convertToNumber(bestBid, PRICE_PRECISION));
  console.log("Best ask:", convertToNumber(bestAsk, PRICE_PRECISION));
  console.log("Spread:", convertToNumber(bestAsk.sub(bestBid), PRICE_PRECISION));
}