Skip to Content
DevelopersVelocity SDKDLOB (Decentralized Limit Order Book)

DLOB (Decentralized Limit Order Book)

What is the DLOB?

The Decentralized Limit Order Book (DLOB) is Velocity’s on-chain 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 on-chain 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 tradingplace_spot_order, place_and_take_spot_order, place_and_make_spot_order, and fill_spot_order no longer exist on-chain (calls now fail with SpotDlobTradingDisabled). Spot markets still exist for collateral/borrow-lend and for 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 API compatibility, but pass MarketType.SPOT and you’ll only ever see an empty book.

When you’d use the DLOB:

  • Market makers: quote against the current best bid/ask and respond to order flow
  • Keeper/filler bots: identify and fill matchable perp orders for fee rewards
  • Orderbook UIs: display a live aggregated L2 or L3 view of a perp market

SDK Usage

The SDK provides several classes to subscribe to and query the DLOB.

OrderSubscriber

Subscribes to all open user orders in real-time via WebSocket or polling. This is the raw data feed that the DLOB is built from. You need this running before you can maintain a local DLOB.

import { OrderSubscriber } from "@velocity-exchange/sdk"; const orderSubscriber = new OrderSubscriber({ velocityClient, subscriptionConfig: { type: "websocket" }, fastDecode: true, decodeData: true, }); await orderSubscriber.subscribe();
Class OrderSubscriberReference ↗

OrderSubscriber — maintains a live, in-memory map of every `User` account (and therefore every open order) on the program, refreshed via polling, websocket program-account subscription, or gRPC/Laserstream, depending on `config.subscriptionConfig.type`. Used by keeper bots and `DLOBSubscriber` to build a full DLOB without individually subscribing to each trader's `User` account. Emits `orderCreated`/`userUpdated`/`updateReceived` on `eventEmitter` as updates arrive; see `OrderSubscriberEvents`.

PropertyTypeRequired
velocityClient
VelocityClient
Yes
usersAccounts
Map<string, { slot: number; userAccount: UserAccount; }>
Yes
subscription
PollingSubscription | WebsocketSubscription | grpcSubscription
Yes
commitment
Commitment
Yes
eventEmitter
StrictEventEmitter<EventEmitter, OrderSubscriberEvents>
Yes
fetchPromise
Promise<void>
No
fetchPromiseResolver
any
Yes
mostRecentSlot
number
Yes
decodeFn
(name: string, data: Buffer) => UserAccount
Yes
decodeData
boolean
No
fetchAllNonIdleUsers
boolean
No
subscribe
() => Promise<void>
Starts the underlying transport (polling interval, websocket program-account subscription, or gRPC stream) chosen at construction time.
Yes
fetch
() => Promise<void>
One-shot full refresh: fetches every `User` account matching the configured filters via `getProgramAccounts` and feeds each through `tryUpdateUserAccount`. Concurrent calls share the in-flight promise rather than issuing a second `getProgramAccounts` request. Errors are caught and logged, not thrown — the promise still resolves.
Yes
tryUpdateUserAccount
(key: string, dataType: "raw" | "decoded" | "buffer", data: UserAccount | Buffer | string[], slot: number) => void
Applies an incoming update for the `User` account `key`, called by `fetch`, `addPubkey`, and each subscription transport (`PollingSubscription`/`WebsocketSubscription`/`grpcSubscription`). Advances `mostRecentSlot` and always emits `updateReceived`. The stored account is only replaced if `slot` is >= the currently cached slot for that key; for `'raw'`/`'buffer'` inputs, before decoding it also cheaply peeks the account's `lastActiveSlot` field at a fixed byte offset and discards the update if that's older than what's cached, without paying the cost of a full decode. On acceptance, emits `userUpdated`, and `orderCreated` for any orders in the account whose `order.slot` falls in `(previousSlot, slot]`.
Yes
createDLOB
() => DLOB
Creates a new DLOB for the order subscriber to fill. This will allow a caller to extend the DLOB Subscriber with a custom DLOB type.
Yes
getDLOB
(slot: number) => Promise<DLOB>
Builds a fresh `DLOB` from the currently cached `User` accounts. For reduce-only orders, resolves the base asset amount against the trader's existing perp position (via `calculateOrderBaseAssetAmount`, `BASE_PRECISION`, 1e9) rather than trusting the order's own `baseAssetAmount` field, since a reduce-only order's fillable size is capped by the position it reduces.
Yes
getSlot
() => number
Yes
addPubkey
(userAccountPublicKey: PublicKey) => Promise<void>
Fetches a single `User` account directly (bypassing the subscription's filters) and applies it via `tryUpdateUserAccount`. Useful to backfill an account this subscriber's filters would otherwise exclude (e.g. an idle user with no orders). No-ops if the account doesn't exist.
Yes
mustGetUserAccount
(key: string) => Promise<UserAccount>
Returns the cached `UserAccount` for `key`, fetching it on demand via `addPubkey` if not already cached.
Yes
unsubscribe
() => Promise<void>
Stops the underlying transport and clears the entire cached `User` account map.
Yes

DLOBSubscriber

Builds and continuously maintains an aggregated orderbook from the order stream. Use this when you need a live L2/L3 view without manually managing the DLOB state.

import { DLOBSubscriber } from "@velocity-exchange/sdk"; const dlobSubscriber = new DLOBSubscriber({ velocityClient, dlobSource: orderSubscriber, // feeds from your OrderSubscriber slotSource: slotSubscriber, // needed for order expiry/timing updateFrequency: 1000, // rebuild the book every 1000ms }); await dlobSubscriber.subscribe();
Class DLOBSubscriberReference ↗

Keeps a `DLOB` snapshot fresh on a timer and exposes convenience `getL2`/`getL3` accessors that resolve market name/index/type and oracle price data via `velocityClient` so callers don't have to. Until `subscribe()` resolves at least once, `getDLOB()`/`getL2`/`getL3` operate on an empty `DLOB`.

PropertyTypeRequired
velocityClient
VelocityClient
Yes
dlobSource
DLOBSource
Yes
slotSource
SlotSource
Yes
updateFrequency
number
Polling interval in milliseconds between `DLOB` refreshes.
Yes
intervalId
Timeout
Handle of the active polling timer, or `undefined` before `subscribe()`/after `unsubscribe()`.
No
dlob
DLOB
The current `DLOB` snapshot; replaced wholesale on each refresh rather than mutated.
Yes
eventEmitter
StrictEventEmitter<EventEmitter, DLOBSubscriberEvents>
Emits `'update'` after each successful refresh and `'error'` if a refresh throws.
Yes
subscribe
() => Promise<void>
Fetches an initial `DLOB` snapshot (awaited before returning) and then starts a timer that refreshes it every `updateFrequency` ms, emitting `'update'` on success or `'error'` if the fetch throws. No-ops if already subscribed.
Yes
updateDLOB
() => Promise<void>
Fetches a new `DLOB` snapshot at the current slot (from `slotSource`) via `dlobSource.getDLOB` and replaces `this.dlob`.
Yes
getDLOB
() => DLOB
Yes
getL2
({ marketName, marketIndex, marketType, depth, includeVamm, numVammOrders, fallbackL2Generators, latestSlot, }: { marketName?: string; marketIndex?: number; marketType?: MarketType; depth?: number; includeVamm?: boolean; numVammOrders?: number; fallbackL2Generators?: L2OrderBookGenerator[]; latestSlot?: any; }) => L...
Get the L2 (aggregated price/size) order book for a given market, using the current `DLOB` snapshot and the current slot from `slotSource`.
Yes
getL3
({ marketName, marketIndex, marketType, }: { marketName?: string; marketIndex?: number; marketType?: MarketType; }) => L3OrderBook
Get the L3 (individual resting order) book for a given market, using the current `DLOB` snapshot and the current slot from `slotSource`. Does not include fallback (e.g. vAMM) liquidity.
Yes
unsubscribe
() => Promise<void>
Stops the periodic refresh timer, if running. Safe to call when not subscribed.
Yes

SlotSubscriber

Tracks the current Solana slot. Required for timing-sensitive operations like JIT auction windows and order expiry checks.

import { SlotSubscriber } from "@velocity-exchange/sdk"; const slotSubscriber = new SlotSubscriber(connection); await slotSubscriber.subscribe(); const currentSlot = slotSubscriber.getSlot();
Class SlotSubscriberReference ↗

SlotSubscriber — tracks the current slot via `connection.onSlotChange`, with an optional stall-detection resubscribe. Slot updates that are not strictly greater than the currently tracked slot are ignored (protects against out-of-order delivery).

PropertyTypeRequired
connection
any
Yes
currentSlot
number
Yes
subscriptionId
number
No
eventEmitter
StrictEventEmitter<EventEmitter, SlotSubscriberEvents>
Yes
timeoutId
Timeout
No
resubTimeoutMs
number
No
isUnsubscribing
boolean
Yes
receivingData
boolean
Yes
subscribe
() => Promise<void>
Fetches the current slot once via RPC, then subscribes to `onSlotChange` for live updates. Idempotent while already subscribed.
Yes
updateCurrentSlot
any
Yes
setTimeout
any
Yes
getSlot
() => number
Yes
unsubscribe
(onResub?: boolean | undefined) => Promise<void>
Removes the `onSlotChange` listener and cancels the resub timeout.
Yes

DLOB

The core data structure with bid/ask sides and query methods. Under normal usage you access this via dlobSubscriber.getDLOB() rather than instantiating it directly.

import { DLOB } from "@velocity-exchange/sdk"; // Access via DLOBSubscriber (recommended) const dlob = dlobSubscriber.getDLOB();
Class DLOBReference ↗

In-memory order book. Indexes every open order it is given into per-market, per-side sorted `NodeList`s (see `MarketNodeLists`), and provides the crossing/fill-finding logic (`findNodesToFill`) and aggregated book views (`getL2`/`getL3`) that keepers and clients use to predict and drive on-chain fills. A `DLOB` instance is normally built once per slot (e.g. via `initFromUserMap`) rather than mutated indefinitely, since state changes (`insertOrder`, `delete`) must be paired with the caller's own bookkeeping of what's already been applied.

PropertyTypeRequired
openOrders
Map<MarketTypeStr, Set<string>>
Order signatures (`getOrderSignature`) currently open, keyed by market type (`'perp'`/`'spot'`).
Yes
orderLists
Map<MarketTypeStr, Map<number, MarketNodeLists>>
Every market's `MarketNodeLists`, keyed by market type then market index.
Yes
maxSlotForRestingLimitOrders
number
The highest slot `updateRestingLimitOrders` has processed; used to skip redundant re-promotion of taking→resting orders when called with a slot that's already been seen.
Yes
initialized
boolean
Set to `true` once `initFromUserMap` has successfully populated this instance; `initFromUserMap` is then a no-op.
Yes
init
any
Yes
getOpenOrdersForMarketType
any
Yes
getOrderListsForMarketType
any
Yes
tryGetMarketNodeLists
any
Yes
getMarketNodeLists
any
Yes
clear
() => void
Empties every order list and resets the DLOB to its freshly-constructed (uninitialized) state, including `maxSlotForRestingLimitOrders` and `initialized`.
Yes
initFromUserMap
(userMap: UserMap, slot: number) => Promise<boolean>
Populates this DLOB from every open order across every user in `userMap`. For reduce-only orders, the fillable amount is capped via `calculateOrderBaseAssetAmount` against the user's existing perp position for that market, rather than trusting the order's full stated `baseAssetAmount`. No-ops (returns `false` immediately) if this instance has already been initialized — call `clear()` first to rebuild from scratch.
Yes
insertOrder
(order: Order, userAccount: string, slot: number, baseAssetAmount: BN, onInsert?: OrderBookCallback | undefined) => void
Inserts a single on-chain order into the appropriate `NodeList` for its market/side/type. No-ops if the order's status isn't `open`, or if its `orderType` isn't one of the DLOB-supported types (`market`, `limit`, `triggerMarket`, `triggerLimit`, `oracle`). Lazily creates the market's `MarketNodeLists` (via `addOrderList`) on first insert for that market. Which list the order lands in (taking vs. resting limit, floating, market, or inactive trigger) is decided by `getListForOnChainOrder`.
Yes
insertSignedMsgOrder
(order: Order, userAccount: string, baseAssetAmount?: any, onInsert?: OrderBookCallback | undefined) => void
Inserts an off-chain signed-message order (not yet landed on-chain) into the market's `signedMsg` bid/ask list, unconditionally (no status/order-type filtering, unlike `insertOrder`). Lazily creates the market's `MarketNodeLists` on first insert.
Yes
addOrderList
(marketType: MarketTypeStr, marketIndex: number) => void
Creates and registers an empty `MarketNodeLists` (all six order categories, both sides) for `marketIndex`, overwriting any existing lists for that market.
Yes
delete
(order: Order, userAccount: PublicKey, slot: number, onDelete?: OrderBookCallback | undefined) => void
Removes an order from whichever `NodeList` it currently lives in. No-ops if the order's status isn't `open`. First calls `updateRestingLimitOrders(slot)` so a taking-limit order that has since become a resting-limit order is looked up (and removed from) the correct list.
Yes
getListForOnChainOrder
(order: Order, slot: number) => NodeList<any> | undefined
Determines which `NodeList` an order belongs in, given its current state and the slot: a trigger order (`triggerMarket`/`triggerLimit`) that hasn't fired yet goes in `trigger.above`/`trigger.below`; a market/oracle-type order goes in `market`; a limit order with a non-zero `oraclePriceOffset` goes in `floatingLimit`; otherwise a limit order goes in `restingLimit` once its auction is complete or it's post-only (per `isRestingLimitOrder`), and in `takingLimit` while still auctioning.
Yes
getListForOnChainOrderOrThrow
any
Yes
updateRestingLimitOrders
(slot: number) => void
Promotes any `takingLimit` orders across all perp and spot markets whose auction has since completed (per `isRestingLimitOrder`) into their market's `restingLimit` list. No-ops if `slot` is not newer than the last slot this was called with (`maxSlotForRestingLimitOrders`), so it is cheap to call defensively before any read that depends on resting-limit state being current (most getters here do so internally).
Yes
updateRestingLimitOrdersForMarketType
(slot: number, marketTypeStr: MarketTypeStr) => void
Does the `takingLimit` → `restingLimit` promotion (see `updateRestingLimitOrders`) for every market of one market type.
Yes
getOrder
(orderId: number, userAccount: PublicKey) => Order | undefined
Looks up an order by id/owner across every `NodeList` in the DLOB (perp and spot, all categories/sides) via `getNodeLists`. O(number of lists); prefer a narrower lookup (e.g. `NodeList.get`) if you already know the order's market/type.
Yes
findNodesToFill
<T extends MarketType>(marketIndex: number, fallbackBid: any, fallbackAsk: any, slot: number, ts: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, stateAccount: StateAccount, marketAccount: T extends { ...; } ? SpotMarketAccount : PerpMarketAccount) => NodeT...
Top-level entry point for keepers: finds every node in one market that is currently fillable, combining four sources — crossing resting-limit orders (`findRestingLimitOrderNodesToFill`), taking (still-auctioning) orders that cross a maker or fallback price (`findTakingNodesToFill`), expired orders to cancel/settle (`findExpiredNodesToFill`), and unfillable reduce-only orders below the step size to cancel (`findUnfillableReduceOnlyOrdersToCancel`). Returns `[]` immediately if fills are paused for this market (`fillPaused`). The market's `orderTickSize` is read from `marketAccount` and threaded through to every price comparison below so all crossing checks agree with on-chain price standardization.
Yes
getMakerRebate
(marketType: MarketType, stateAccount: StateAccount, marketAccount: SpotMarketAccount | PerpMarketAccount) => { ...; }
Reads the tier-0 maker rebate fraction (`makerRebateNumerator / makerRebateDenominator`) for a market from `stateAccount`'s perp/spot fee structure, then scales the numerator up by the market's `feeAdjustment` percentage if one is set. Used by `findRestingLimitOrderNodesToFill` to size the buffer added to fallback prices so fallback fills aren't triggered by rebate-sized noise.
Yes
mergeNodesToFill
(restingLimitOrderNodesToFill: NodeToFill[], takingOrderNodesToFill: NodeToFill[]) => NodeToFill[]
Merges two `NodeToFill` arrays (typically resting-limit crossings and taking-order crossings for the same market/pass) by taker order signature, concatenating `makerNodes` for any taker that appears in both — e.g. an order that both crosses a resting maker and separately crosses fallback liquidity ends up as one `NodeToFill` with both maker sources.
Yes
findRestingLimitOrderNodesToFill
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, stateAccount: StateAccount, marketAccount: T extends { ...; } ? SpotMarketAccount : PerpMarketAccount, makerRebateNumerator: number, make...
Finds resting-limit-order fills for a market: resting bids/asks that cross each other (`findCrossingRestingLimitOrders`), plus resting asks that cross the fallback bid and resting bids that cross the fallback ask (each skipped entirely if the AMM is paused). The fallback price on each side is tightened by the maker rebate before comparing, so a maker order priced exactly at the rebate-adjusted fallback isn't spuriously flagged as crossing (`fallbackBidWithBuffer = fallbackBid - fallbackBid * makerRebateNumerator / makerRebateDenominator`, and symmetrically for the ask).
Yes
findTakingNodesToFill
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, state: StateAccount, marketAccount: T extends { ...; } ? SpotMarketAccount : PerpMarketAccount, fallbackAsk: any, fallbackBid?: any, tick...
Finds fills for taking (still-auctioning) orders: taking asks crossing resting bids or the fallback bid, and taking bids crossing resting asks or the fallback ask (`findTakingNodesCrossingMakerNodes` / `findNodesCrossingFallbackLiquidity`). Fallback crossing checks are skipped entirely when `isAmmPaused`. For spot markets, a taking order is only allowed to cross the opposite fallback price if doing so wouldn't also require crossing beyond the *other* fallback price (see the inline `fallbackBid`/`fallbackAsk` guards) — this prevents a taking order from routing through DLOB makers priced worse than the AMM.
Yes
findTakingNodesCrossingMakerNodes
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, takerNodeGenerator: Generator<...>, makerNodeGeneratorFn: (marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { ...; } ? OraclePriceD...
Walks `takerNodeGenerator` (taking bids or asks, sorted by arrival slot) against a fresh maker-side generator (built per taker via `makerNodeGeneratorFn`, e.g. `getRestingLimitBids`) and records a `NodeToFill` for every taker/maker pair that `doesCross` accepts, skipping same-user matches. For each match this method also **mutates DLOB state**: it applies the simulated fill to both the maker's and taker's order lists (via `NodeList.update`) so subsequent iterations see updated `baseAssetAmountFilled` and a taker stops matching once fully filled. Because maker nodes (sorted by price) are scanned in order, `doesCross` returning false breaks out of the maker loop entirely — this is correct for resting-limit makers but relies on the maker generator being price-sorted, not time-sorted.
Yes
findNodesCrossingFallbackLiquidity
<T extends MarketType>(marketType: T, slot: number, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, nodeGenerator: Generator<DLOBNode, any, any>, doesCross: (nodePrice: any) => boolean, state: StateAccount, marketAccount: T extends { ...; } ? SpotMarketAccount : PerpMarketAccount...
Scans `nodeGenerator` for nodes that both cross the fallback price (`doesCross`, evaluated against each node's `getLimitPrice`, or crossing unconditionally if the node has no limit price) and have fallback liquidity actually available to fill against. For spot markets, post-only orders are skipped (they can never take against the AMM) and fallback liquidity is always considered available; for perp markets, availability additionally requires `isFallbackAvailableLiquiditySource` (broadly: the order's auction is complete and the oracle is valid enough for AMM fills). Does not mutate any order state — unlike `findTakingNodesCrossingMakerNodes`, fallback fills are expected to be sized/settled on-chain rather than simulated here.
Yes
findExpiredNodesToFill
(marketIndex: number, ts: number, marketType: MarketType, slot?: any) => NodeToFill[]
Finds orders in a market that are eligible to be expired: any non-trigger, non-TIF-limit order whose `maxTs` (plus a 25-second buffer for limit orders, via `isOrderExpired`) has passed the given timestamp. Also proactively removes (not just reports) signed-message orders whose auction window (`order.slot + order.auctionDuration`) has passed `slot`, since those never landed on-chain and have no on-chain expiration to wait for.
Yes
findUnfillableReduceOnlyOrdersToCancel
(marketIndex: number, marketType: MarketType, stepSize: BN) => NodeToFill[]
Finds reduce-only orders across every category/side in a market whose remaining `baseAssetAmount` (as tracked on the node, not necessarily the order's original size) has dropped below the market's minimum step size — meaning the order can never be filled again and should be canceled by a keeper rather than left to linger.
Yes
getTakingBids
<T extends MarketType>(marketIndex: number, marketType: T, slot: number, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...>
Yields taking (still-auctioning) bid nodes for a market — market-bid orders, taking-limit bids, and signed-message bids not yet resting — merged in arrival order (earliest `slot` first, via `getBestNode`). Calls `updateRestingLimitOrders(slot)` first so a signed-message order that has since become a resting-limit order is excluded here.
Yes
getTakingAsks
<T extends MarketType>(marketIndex: number, marketType: T, slot: number, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...>
Same as `getTakingBids`, but for the ask side.
Yes
signedMsgGenerator
(signedMsgOrderList: NodeList<"signedMsg">, filter: (x: DLOBNode) => boolean) => Generator<DLOBNode, any, any>
Filters a `signedMsg` `NodeList`'s nodes by an arbitrary predicate — used to split signed-message orders into "still taking" vs. "now resting" subsets based on `isRestingLimitOrder`.
Yes
getBestNode
<T extends MarketTypeStr>(generatorList: Generator<DLOBNode, any, any>[], oraclePriceData: T extends "spot" ? OraclePriceData : MMOraclePriceData, slot: number, compareFcn: (bestDLOBNode: DLOBNode, currentDLOBNode: DLOBNode, slot: number, oraclePriceData: T extends "spot" ? OraclePriceData : MMOraclePriceData) => bo...
K-way-merges multiple node generators (e.g. one per order category feeding one side of the book) into a single generator ordered by `compareFcn`, skipping nodes that are already fully filled (`isBaseFilled`) or rejected by `filterFcn`. This is the shared core behind `getTakingBids`/`getTakingAsks`/`getRestingLimitBids`/`getRestingLimitAsks`/`getBids`/`getAsks` — each just supplies a different `generatorList` and `compareFcn`.
Yes
getRestingLimitAsks
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined, tickSize?: any) => Generator<...>
Yields resting-limit ask nodes for a market — `restingLimit`, `floatingLimit`, and any `signedMsg` asks that have become resting — merged best-price-first (lowest ask price first, ties broken by `getBestNode`'s underlying comparator). Calls `updateRestingLimitOrders(slot)` first. `tickSize` is threaded into every price comparison via `DLOBNode.getPriceOrThrow`, so pass the market's `orderTickSize` to match on-chain price standardization — omitting it defaults to no rounding (tick of 1).
Yes
getRestingLimitBids
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined, tickSize?: any) => Generator<...>
Same as `getRestingLimitAsks`, but for the bid side (merged best-price-first, highest bid first).
Yes
getAsks
<T extends MarketType>(marketIndex: number, _fallbackAsk: any, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined, tickSize?: any) => Generator<...>
Merges `getTakingAsks` and `getRestingLimitAsks` into a single best-price-first generator (ties broken by earliest arrival slot). Nodes with no resolvable price (e.g. still mid-auction) sort as price `0` — i.e. best — since `getPrice` (not `getPriceOrThrow`) is used here. Unlike `findTakingNodesToFill`/`findNodesToFill`, this does **not** merge in fallback (e.g. vAMM) liquidity; the `fallbackAsk` parameter is currently unused/reserved.
Yes
getBids
<T extends MarketType>(marketIndex: number, _fallbackBid: any, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined, tickSize?: any) => Generator<...>
Merges `getTakingBids` and `getRestingLimitBids` into a single best-price-first generator (ties broken by earliest arrival slot). Nodes with no resolvable price sort as `BN_MAX` — i.e. worst — since a priceless bid shouldn't be preferred over a priced one. Does not merge in fallback (e.g. vAMM) liquidity; the `fallbackBid` parameter is currently unused/reserved.
Yes
findCrossingRestingLimitOrders
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, tickSize?: any) => NodeToFill[]
Finds pairs of resting-limit asks and bids that cross each other (`bidPrice >= askPrice`), assigns maker/taker roles via `determineMakerAndTaker` (post-only orders are always makers; otherwise whichever order's auction finished later is the taker), and simulates the fill by updating both orders' `baseAssetAmountFilled` in their `NodeList`s so subsequent iterations see the reduced remaining size. Same-user matches are skipped. Because both ask and bid generators are price-sorted, the inner loop `break`s as soon as `bidPrice < askPrice` for a given ask, since no later (worse) bid can cross either.
Yes
determineMakerAndTaker
(askNode: DLOBNode, bidNode: DLOBNode) => { takerNode: DLOBNode; makerNode: DLOBNode; } | undefined
Decides which of a crossing ask/bid pair is the maker and which is the taker: if both are post-only, they can't be matched (`undefined`); if exactly one is post-only, it's the maker; otherwise whichever order's auction window (`order.slot + order.auctionDuration`) ends later is treated as the taker (it "arrived crossing" the earlier order).
Yes
getBestAsk
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, tickSize?: any) => any
Gets the best (lowest) resting-limit ask price for a market. Does not consider fallback (e.g. vAMM) liquidity.
Yes
getBestBid
<T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, tickSize?: any) => any
Gets the best (highest) resting-limit bid price for a market. Does not consider fallback (e.g. vAMM) liquidity.
Yes
getStopLosses
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Yields untriggered trigger orders that would close a position in `direction`: for a `long` position, short-direction orders in the `trigger.below` list (stop triggers on a price drop); for a `short` position, long-direction orders in `trigger.above` (stop triggers on a price rise). Includes both `triggerMarket` and `triggerLimit` order types — see `getStopLossMarkets`/`getStopLossLimits` to filter to one.
Yes
getStopLossMarkets
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Same as `getStopLosses`, filtered to `triggerMarket` orders only.
Yes
getStopLossLimits
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Same as `getStopLosses`, filtered to `triggerLimit` orders only.
Yes
getTakeProfits
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Yields untriggered trigger orders that would close a position in `direction` for profit: for a `long` position, short-direction orders in `trigger.above` (take-profit on a price rise); for a `short` position, long-direction orders in `trigger.below` (take-profit on a price drop). Includes both `triggerMarket` and `triggerLimit` order types — see `getTakeProfitMarkets`/`getTakeProfitLimits` to filter to one.
Yes
getTakeProfitMarkets
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Same as `getTakeProfits`, filtered to `triggerMarket` orders only.
Yes
getTakeProfitLimits
(marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any>
Same as `getTakeProfits`, filtered to `triggerLimit` orders only.
Yes
findNodesToTrigger
(marketIndex: number, slot: number, triggerPrice: BN, marketType: MarketType, stateAccount: StateAccount) => NodeToTrigger[]
Finds trigger orders whose condition is now satisfied by `triggerPrice`: `trigger.above` orders with `triggerPrice > order.triggerPrice`, and `trigger.below` orders with `triggerPrice < order.triggerPrice`. Both lists are sorted by trigger price with the nearest-to-triggering order at `head`, so each scan walks from `head` and `break`s at the first order that isn't (yet) triggered. Returns `[]` immediately if the exchange is paused.
Yes
printTop
(velocityClient: VelocityClient, slotSubscriber: SlotSubscriber, marketIndex: number, marketType: MarketType) => void
Debug helper: logs the market's best bid, best ask, and mid price (all resting-limit only, no fallback liquidity), along with each side's spread to the current oracle price, as a percentage.
Yes
getDLOBOrders
() => DLOBOrders
Flattens every order across every `NodeList` (perp and spot, all categories/sides) into a single `DLOBOrders` array of `&#123; user, order &#125;` pairs, in no particular cross-list order.
Yes
getNodeLists
() => Generator<NodeList<DLOBNodeType>, any, any>
Yields every `NodeList` (all ten category/side combinations, per market) across every perp market, then every spot market. Used by `getOrder`/`getDLOBOrders` to walk the entire book.
Yes
getL2
<T extends MarketType>({ marketIndex, marketType, slot, oraclePriceData, depth, fallbackL2Generators, tickSize, }: { marketIndex: number; marketType: T; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; depth: number; fallbackL2Generators?: L2OrderBookGenerator[]; tic...
Get an L2 (aggregated price/size) view of the order book for a given market: resting-limit DLOB liquidity merged with any supplied fallback generators (e.g. the vAMM, via `getVammL2Generator`), then bucketed into up to `depth` levels per side via `createL2Levels`. Does not include taking (still-auctioning) orders — only resting-limit makers and fallback liquidity are represented.
Yes
getL3
<T extends MarketType>({ marketIndex, marketType, slot, oraclePriceData, tickSize, }: { marketIndex: number; marketType: T; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; tickSize?: any; }) => L3OrderBook
Get an L3 (individual resting order) view of the order book for a given market. Only resting-limit orders are included — no taking orders and no fallback (e.g. vAMM) liquidity.
Yes
estimateFillExactBaseAmountInForSide
any
Yes
estimateFillWithExactBaseAmount
<T extends MarketType>({ marketIndex, marketType, baseAmount, orderDirection, slot, oraclePriceData, tickSize, }: { marketIndex: number; marketType: T; baseAmount: BN; orderDirection: PositionDirection; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; tickSize?: any;...
Estimates the quote amount that would be filled for a given base amount, walking resting-limit asks (for a `long`/buy) or bids (for a `short`/sell) from best price outward and summing `price * size` until `baseAmount` is consumed. Does not include fallback (e.g. vAMM) liquidity or taking orders, and does not mutate any order state — this is a read-only estimate, not a simulated fill.
Yes
getBestMakers
<T extends MarketType>({ marketIndex, marketType, direction, slot, oraclePriceData, numMakers, tickSize, }: { marketIndex: number; marketType: T; direction: PositionDirection; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; numMakers: number; tickSize?: any; }) => P...
Collects the pubkeys of up to `numMakers` distinct makers currently resting best-priced on one side of the book — bids for a `long` taker, asks for a `short` taker — in best-price order. Used to pick candidate maker accounts to pass as `remaining_accounts` when submitting a fill instruction. A maker with multiple resting orders at different prices only counts once toward `numMakers`.
Yes

UserMap

Efficiently tracks and caches the accounts of many users simultaneously. Used by liquidation bots and other applications that need to monitor positions across the whole protocol, rather than just orders.

import { UserMap } from "@velocity-exchange/sdk";
Class UserMapReference ↗

In-memory cache of every `User` account on the program, keyed by the `User` account's own public key.

Sync/subscription filtering (see `getFilters`) is built from memcmp filters: the base `getUserFilter()` (matches the `User` account discriminator — required, or every account on chain would match) is always present; `getNonIdleUserFilter()` is added unless `includeIdle` is set (idle accounts are excluded by default to reduce subscription volume); `getUsersWithPoolId(filterByPoolId)` is added when `filterByPoolId` is set; and any `additionalFilters` from the config are appended last. The same filter set is used for the initial `getProgramAccounts` sync and for the live websocket/gRPC subscription, so what you sync is what you keep getting updates for.

Automatically does a full `sync()` whenever the program's `StateAccount.numberOfSubAccounts` changes (new/deleted user accounts), unless `disableSyncOnTotalAccountsChange` is set.

PropertyTypeRequired
userMap
any
Yes
velocityClient
VelocityClient
Yes
eventEmitter
StrictEventEmitter<EventEmitter, UserEvents>
Yes
connection
any
Yes
commitment
any
Yes
includeIdle
any
Yes
filterByPoolId
any
No
additionalFilters
any
No
disableSyncOnTotalAccountsChange
any
Yes
lastNumberOfSubAccounts
any
No
subscription
any
Yes
stateAccountUpdateCallback
any
Yes
decode
any
Yes
mostRecentSlot
any
Yes
syncConfig
any
Yes
syncPromise
any
No
syncPromiseResolver
any
Yes
throwOnFailedSync
any
Yes
subscribe
() => Promise<void>
Populates the map with a full initial `sync()` (no-op if already populated) and starts the configured live subscription (`'websocket'`/`'polling'`/`'grpc'`), plus (unless `disableSyncOnTotalAccountsChange`) a listener that triggers a full re-sync whenever `StateAccount.numberOfSubAccounts` changes.
Yes
addPubkey
(userAccountPublicKey: PublicKey, userAccount?: UserAccount | undefined, slot?: number | undefined, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<...>
Adds `userAccountPublicKey` to the map, creating a `User` for it. By default (`accountSubscription` omitted), subscribes it with a `OneShotUserAccountSubscriber` seeded from `userAccount`/`slot` rather than a live per-account websocket subscription — the map already gets live updates in bulk via its own subscription (`WebsocketSubscription`/ `PollingSubscription`/`grpcSubscription`), so per-`User` subscriptions here would needlessly multiply RPC load.
Yes
has
(key: string) => boolean
Returns true if a `User` account keyed by `key` (the `User` account pubkey, base58) is cached in the map.
Yes
get
(key: string) => User | undefined
gets the User for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getWithSlot
(key: string) => DataAndSlot<User> | undefined
Like `get`, but also returns the slot at which the `User` account was last observed.
Yes
mustGet
(key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<User>
gets the User for a particular userAccountPublicKey, if no User exists, new one is created
Yes
mustGetWithSlot
(key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<DataAndSlot<User>>
Like `mustGet`, but also returns the slot at which the `User` account was observed.
Yes
mustGetUserAccount
(key: string) => Promise<UserAccount>
Like `mustGet`, but returns the underlying `UserAccount` data directly (throws if the `User`'s account is not loaded).
Yes
getUserAuthority
(key: string) => PublicKey | undefined
gets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getDLOB
(slot: number) => Promise<DLOB>
Implements the `DLOBSource` interface: builds a `DLOB` from every subscribed user's open orders.
Yes
updateWithOrderRecord
(record: OrderRecord) => Promise<void>
Ensures an entry exists in the map for `record.user`, adding it via `addPubkey` if not already present.
Yes
updateWithEventRecord
(record: any) => Promise<void>
Incrementally updates the map in response to a single program event, ensuring an entry exists for every `User` account the event references (deposit/funding/liquidation/order/order-action/settle-pnl/new-user records). Unrecognized event types are silently ignored.
Yes
values
() => IterableIterator<User>
Iterates all cached `User` instances.
Yes
valuesWithSlot
() => IterableIterator<DataAndSlot<User>>
Like `values`, but paired with the slot each `User` was last observed at.
Yes
entries
() => IterableIterator<[string, User]>
Iterates all `[userAccountPublicKey, User]` pairs in the map.
Yes
entriesWithSlot
() => IterableIterator<[string, DataAndSlot<User>]>
Like `entries`, but paired with the slot each `User` was last observed at.
Yes
size
() => number
Number of `User` accounts currently cached in the map.
Yes
getUniqueAuthorities
(filterCriteria?: UserAccountFilterCriteria | undefined) => PublicKey[]
Returns a unique list of authorities for all users in the UserMap that meet the filter criteria
Yes
sync
() => Promise<void>
Runs a full sync using the strategy configured in `UserMapConfig.syncConfig` (`'default'` or `'paginated'` — see `SyncConfig`).
Yes
getFilters
any
Builds the memcmp filter set for both the initial sync and the live subscription: always the `User`-account discriminator filter; plus a non-idle filter unless `includeIdle`; plus a pool-id filter if `filterByPoolId` is set; plus any caller-supplied `additionalFilters`.
Yes
defaultSync
any
Syncs the UserMap using the default sync method (single getProgramAccounts call with filters). This method may fail when velocity has too many users. (nodejs response size limits)
Yes
paginatedSync
any
Syncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters). This method is more reliable when velocity has many users.
Yes
unsubscribe
() => Promise<void>
Tears down the live subscription, unsubscribes and removes every cached `User`, and (if registered) removes the `stateAccountUpdate` listener that triggers auto-resync on `numberOfSubAccounts` changes.
Yes
updateUserAccount
(key: string, userAccount: UserAccount, slot: number) => Promise<void>
Applies a fresh `userAccount` observation for `key` at `slot`. If the user is already cached, updates in place only if `slot` is at least as new as the cached slot (stale/out-of-order updates are dropped) and emits `'userUpdate'`. If not cached yet, adds it via `addPubkey`. Also advances `getSlot()`'s tracked most-recent slot.
Yes
updateLatestSlot
(slot: number) => void
Advances the map's tracked most-recent slot to `slot` if it's newer.
Yes
getSlot
() => number
Returns the most recent slot at which any account update has been observed.
Yes

Setting Up a Local DLOB

This is the full setup sequence to get a live, continuously-updated orderbook running:

import { SlotSubscriber, OrderSubscriber, DLOBSubscriber } from "@velocity-exchange/sdk"; // 1. Track the current slot (needed for order expiry) 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 DLOB from the order stream const dlobSubscriber = new DLOBSubscriber({ velocityClient, dlobSource: orderSubscriber, slotSource: slotSubscriber, updateFrequency: 1000, }); await dlobSubscriber.subscribe();
Example DLOB setupReference ↗
TypeScript docs unavailable for DLOB setup.

Getting L2 Orderbook Data

Once subscribed, query the aggregated L2 orderbook (price levels with cumulative size):

import { MarketType, PRICE_PRECISION, BASE_PRECISION, convertToNumber } from "@velocity-exchange/sdk"; const dlob = dlobSubscriber.getDLOB(); const marketIndex = 0; // SOL-PERP // For perp markets, use getMMOracleDataForPerpMarket (returns MMOraclePriceData) const oraclePriceData = velocityClient.getMMOracleDataForPerpMarket(marketIndex); const slot = slotSubscriber.getSlot(); const l2 = dlob.getL2({ marketIndex, marketType: MarketType.PERP, oraclePriceData, slot, depth: 10, // number of price levels per side }); // 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));
Example L2 orderbookReference ↗
TypeScript docs unavailable for L2 orderbook.

Getting Best Bid/Ask

For quick access to the best bid and ask prices without fetching the full orderbook:

import { MarketType, PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk"; const dlob = dlobSubscriber.getDLOB(); const marketIndex = 0; const oraclePriceData = velocityClient.getMMOracleDataForPerpMarket(marketIndex); const slot = slotSubscriber.getSlot(); // 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)); }
Example Best bid/askReference ↗
TypeScript docs unavailable for Best bid/ask.
Last updated on