DLOB (Decentralized Limit Order Book)
What is the DLOB?
The Decentralized Limit Order Book (DLOB) is Drift’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. Drift’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.
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 orders for fee rewards
- Orderbook UIs: display a live aggregated L2 or L3 view of the 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 "@drift-labs/sdk";
const orderSubscriber = new OrderSubscriber({
driftClient,
subscriptionConfig: { type: "websocket" },
fastDecode: true,
decodeData: true,
});
await orderSubscriber.subscribe();Class OrderSubscriberReference ↗
Class OrderSubscriberReference ↗| Property | Type | Required |
|---|---|---|
driftClient | DriftClient | 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 | () => void | Yes |
mostRecentSlot | number | Yes |
decodeFn | (name: string, data: Buffer) => UserAccount | Yes |
decodeData | boolean | No |
fetchAllNonIdleUsers | boolean | No |
subscribe | () => Promise<void> | Yes |
fetch | () => Promise<void> | Yes |
tryUpdateUserAccount | (key: string, dataType: "raw" | "decoded" | "buffer", data: UserAccount | Buffer | string[], slot: number) => void | Yes |
createDLOB | (protectedMakerParamsMap?: ProtectMakerParamsMap | undefined) => DLOBCreates 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, protectedMakerParamsMap?: ProtectMakerParamsMap | undefined) => Promise<DLOB> | Yes |
getSlot | () => number | Yes |
addPubkey | (userAccountPublicKey: PublicKey) => Promise<void> | Yes |
mustGetUserAccount | (key: string) => Promise<UserAccount> | Yes |
unsubscribe | () => Promise<void> | 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 "@drift-labs/sdk";
const dlobSubscriber = new DLOBSubscriber({
driftClient,
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 ↗
Class DLOBSubscriberReference ↗| Property | Type | Required |
|---|---|---|
driftClient | DriftClient | Yes |
dlobSource | DLOBSource | Yes |
slotSource | SlotSource | Yes |
updateFrequency | number | Yes |
intervalId | Timeout | No |
dlob | DLOB | Yes |
eventEmitter | StrictEventEmitter<EventEmitter, DLOBSubscriberEvents> | Yes |
protectedMakerView | boolean | Yes |
subscribe | () => Promise<void> | Yes |
getProtectedMakerParamsMap | () => ProtectMakerParamsMap | undefined | Yes |
updateDLOB | () => Promise<void> | 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 order book for a given market. | Yes |
getL3 | ({ marketName, marketIndex, marketType, }: { marketName?: string; marketIndex?: number; marketType?: MarketType; }) => L3OrderBookGet the L3 order book for a given market. | Yes |
unsubscribe | () => Promise<void> | Yes |
SlotSubscriber
Tracks the current Solana slot. Required for timing-sensitive operations like JIT auction windows and order expiry checks.
import { SlotSubscriber } from "@drift-labs/sdk";
const slotSubscriber = new SlotSubscriber(connection);
await slotSubscriber.subscribe();
const currentSlot = slotSubscriber.getSlot();Class SlotSubscriberReference ↗
Class SlotSubscriberReference ↗| Property | Type | Required |
|---|---|---|
connection | any | Yes |
currentSlot | number | Yes |
subscriptionId | number | Yes |
eventEmitter | StrictEventEmitter<EventEmitter, SlotSubscriberEvents> | Yes |
timeoutId | Timeout | No |
resubTimeoutMs | number | No |
isUnsubscribing | boolean | Yes |
receivingData | boolean | Yes |
subscribe | () => Promise<void> | Yes |
updateCurrentSlot | any | Yes |
setTimeout | any | Yes |
getSlot | () => number | Yes |
unsubscribe | (onResub?: boolean | undefined) => Promise<void> | 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 "@drift-labs/sdk";
// Access via DLOBSubscriber (recommended)
const dlob = dlobSubscriber.getDLOB();Class DLOBReference ↗
Class DLOBReference ↗| Property | Type | Required |
|---|---|---|
openOrders | Map<MarketTypeStr, Set<string>> | Yes |
orderLists | Map<MarketTypeStr, Map<number, MarketNodeLists>> | Yes |
maxSlotForRestingLimitOrders | number | Yes |
initialized | boolean | Yes |
protectedMakerParamsMap | ProtectMakerParamsMap | Yes |
init | any | Yes |
clear | () => void | Yes |
initFromUserMap | (userMap: UserMap, slot: number) => Promise<boolean>initializes a new DLOB instance | Yes |
insertOrder | (order: Order, userAccount: string, slot: number, isUserProtectedMaker: boolean, baseAssetAmount: BN, onInsert?: OrderBookCallback | undefined) => void | Yes |
insertSignedMsgOrder | (order: Order, userAccount: string, isUserProtectedMaker: boolean, baseAssetAmount?: any, onInsert?: OrderBookCallback | undefined) => void | Yes |
addOrderList | (marketType: MarketTypeStr, marketIndex: number) => void | Yes |
delete | (order: Order, userAccount: PublicKey, slot: number, isUserProtectedMaker: boolean, onDelete?: OrderBookCallback | undefined) => void | Yes |
getListForOnChainOrder | (order: Order, slot: number, isProtectedMaker: boolean) => NodeList<any> | undefined | Yes |
updateRestingLimitOrders | (slot: number) => void | Yes |
updateRestingLimitOrdersForMarketType | (slot: number, marketTypeStr: MarketTypeStr) => void | Yes |
getOrder | (orderId: number, userAccount: PublicKey) => Order | undefined | 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... | Yes |
getMakerRebate | (marketType: MarketType, stateAccount: StateAccount, marketAccount: SpotMarketAccount | PerpMarketAccount) => { ...; } | Yes |
mergeNodesToFill | (restingLimitOrderNodesToFill: NodeToFill[], takingOrderNodesToFill: NodeToFill[]) => NodeToFill[] | 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... | 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) => N... | 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: MarketType, oraclePriceData: T extends { ...; } ? Ora... | 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... | Yes |
findExpiredNodesToFill | (marketIndex: number, ts: number, marketType: MarketType, slot?: any) => NodeToFill[] | Yes |
findUnfillableReduceOnlyOrdersToCancel | (marketIndex: number, marketType: MarketType, stepSize: BN) => NodeToFill[] | Yes |
getTakingBids | <T extends MarketType>(marketIndex: number, marketType: T, slot: number, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...> | Yes |
getTakingAsks | <T extends MarketType>(marketIndex: number, marketType: T, slot: number, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...> | Yes |
signedMsgGenerator | (signedMsgOrderList: NodeList<"signedMsg">, filter: (x: DLOBNode) => boolean) => Generator<DLOBNode, any, any> | 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... | Yes |
getRestingLimitAsks | <T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...> | Yes |
getRestingLimitBids | <T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...> | Yes |
getAsks | <T extends MarketType>(marketIndex: number, _fallbackAsk: any, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...>This will look at both the taking and resting limit asks | Yes |
getBids | <T extends MarketType>(marketIndex: number, _fallbackBid: any, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData, filterFcn?: DLOBFilterFcn | undefined) => Generator<...>This will look at both the taking and resting limit bids | Yes |
findCrossingRestingLimitOrders | <T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData) => NodeToFill[] | Yes |
determineMakerAndTaker | (askNode: DLOBNode, bidNode: DLOBNode) => { takerNode: DLOBNode; makerNode: DLOBNode; } | undefined | Yes |
getBestAsk | <T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData) => any | Yes |
getBestBid | <T extends MarketType>(marketIndex: number, slot: number, marketType: T, oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData) => any | Yes |
getStopLosses | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
getStopLossMarkets | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
getStopLossLimits | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
getTakeProfits | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
getTakeProfitMarkets | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
getTakeProfitLimits | (marketIndex: number, marketType: MarketType, direction: PositionDirection) => Generator<DLOBNode, any, any> | Yes |
findNodesToTrigger | (marketIndex: number, slot: number, triggerPrice: BN, marketType: MarketType, stateAccount: StateAccount) => NodeToTrigger[] | Yes |
printTop | (driftClient: DriftClient, slotSubscriber: SlotSubscriber, marketIndex: number, marketType: MarketType) => void | Yes |
getDLOBOrders | () => DLOBOrders | Yes |
getNodeLists | () => Generator<NodeList<DLOBNodeType>, any, any> | Yes |
getL2 | <T extends MarketType>({ marketIndex, marketType, slot, oraclePriceData, depth, fallbackL2Generators, }: { marketIndex: number; marketType: T; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; depth: number; fallbackL2Generators?: L2OrderBookGenerator[]; }) => L2Order...Get an L2 view of the order book for a given market. | Yes |
getL3 | <T extends MarketType>({ marketIndex, marketType, slot, oraclePriceData, }: { marketIndex: number; marketType: T; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; }) => L3OrderBookGet an L3 view of the order book for a given market. Does not include fallback liquidity sources | Yes |
estimateFillExactBaseAmountInForSide | any | Yes |
estimateFillWithExactBaseAmount | <T extends MarketType>({ marketIndex, marketType, baseAmount, orderDirection, slot, oraclePriceData, }: { marketIndex: number; marketType: T; baseAmount: BN; orderDirection: PositionDirection; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; }) => BN | Yes |
getBestMakers | <T extends MarketType>({ marketIndex, marketType, direction, slot, oraclePriceData, numMakers, }: { marketIndex: number; marketType: T; direction: PositionDirection; slot: number; oraclePriceData: T extends { spot: unknown; } ? OraclePriceData : MMOraclePriceData; numMakers: number; }) => PublicKey[] | 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 "@drift-labs/sdk";Class UserMapReference ↗
Class UserMapReference ↗| Property | Type | Required |
|---|---|---|
userMap | any | Yes |
driftClient | DriftClient | 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 | Yes |
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> | Yes |
addPubkey | (userAccountPublicKey: PublicKey, userAccount?: UserAccount | undefined, slot?: number | undefined, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<...> | Yes |
has | (key: string) => boolean | Yes |
get | (key: string) => User | undefinedgets the User for a particular userAccountPublicKey, if no User exists, undefined is returned | Yes |
getWithSlot | (key: string) => DataAndSlot<User> | undefined | 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>> | Yes |
mustGetUserAccount | (key: string) => Promise<UserAccount> | Yes |
getUserAuthority | (key: string) => PublicKey | undefinedgets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned | Yes |
getDLOB | (slot: number, protectedMakerParamsMap?: ProtectMakerParamsMap | undefined) => Promise<DLOB>implements the DLOBSource interface
create a DLOB from all the subscribed users | Yes |
updateWithOrderRecord | (record: OrderRecord) => Promise<void> | Yes |
updateWithEventRecord | (record: any) => Promise<void> | Yes |
values | () => IterableIterator<User> | Yes |
valuesWithSlot | () => IterableIterator<DataAndSlot<User>> | Yes |
entries | () => IterableIterator<[string, User]> | Yes |
entriesWithSlot | () => IterableIterator<[string, DataAndSlot<User>]> | Yes |
size | () => number | 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> | Yes |
getFilters | any | Yes |
defaultSync | anySyncs the UserMap using the default sync method (single getProgramAccounts call with filters).
This method may fail when drift has too many users. (nodejs response size limits) | Yes |
paginatedSync | anySyncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters).
This method is more reliable when drift has many users. | Yes |
unsubscribe | () => Promise<void> | Yes |
updateUserAccount | (key: string, userAccount: UserAccount, slot: number) => Promise<void> | Yes |
updateLatestSlot | (slot: number) => void | Yes |
getSlot | () => number | 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 "@drift-labs/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({
driftClient,
subscriptionConfig: { type: "websocket" },
fastDecode: true,
decodeData: true,
});
await orderSubscriber.subscribe();
// 3. Build and maintain the DLOB from the order stream
const dlobSubscriber = new DLOBSubscriber({
driftClient,
dlobSource: orderSubscriber,
slotSource: slotSubscriber,
updateFrequency: 1000,
});
await dlobSubscriber.subscribe();Example DLOB setupReference ↗
Example DLOB setupReference ↗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 "@drift-labs/sdk";
const dlob = dlobSubscriber.getDLOB();
const marketIndex = 0; // SOL-PERP
// For perp markets, use getMMOracleDataForPerpMarket (returns MMOraclePriceData)
const oraclePriceData = driftClient.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 ↗
Example L2 orderbookReference ↗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 "@drift-labs/sdk";
const dlob = dlobSubscriber.getDLOB();
const marketIndex = 0;
const oraclePriceData = driftClient.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 ↗
Example Best bid/askReference ↗Best bid/ask.