Skip to Content

DLOB MM

DLOB market making on Drift means placing resting two sided quotes on the decentralized orderbook (orderbook / DLOB) and earning maker rebates when takers trade against you. You can also participate in JIT when relevant. For protocol background, see Market Maker Participation and JIT FAQ.

Maker vs taker

  • Maker: provides liquidity (resting order), earns rebate.
  • Taker: removes liquidity (crosses spread), pays fee.

Always use post-only for maker quotes

Post-only flags ensure your order is never executed as a taker (which would cost fees instead of earning rebates). Drift offers three post-only modes:

FlagBehaviorUse case
MUST_POST_ONLYRejects the order if it would cross the spread and fill as takerDefault for MM, guarantees maker-only execution
TRY_POST_ONLYAmends the price to the best non crossing price if it would crossUseful when you want the order placed regardless, even at a slightly different price
SLIDESimilar to TRY_POST_ONLY but slides price to the best bid/askEnsures placement at the top of book without crossing

Recommendation: Use MUST_POST_ONLY for all quotes. If the order would cross (e.g., oracle moved), you’d rather cancel and re quote at the new price than accidentally take.

import { OrderParams } from "@drift-labs/sdk";
Class OrderParamsReference ↗
PropertyTypeRequired
orderType
OrderType
Yes
marketType
MarketType
Yes
userOrderId
number
Yes
direction
PositionDirection
Yes
baseAssetAmount
BN
Yes
price
BN
Yes
marketIndex
number
Yes
reduceOnly
boolean
Yes
postOnly
PostOnlyParams
Yes
bitFlags
number
Yes
triggerPrice
any
Yes
triggerCondition
OrderTriggerCondition
Yes
oraclePriceOffset
number | null
Yes
auctionDuration
number | null
Yes
maxTs
any
Yes
auctionStartPrice
any
Yes
auctionEndPrice
any
Yes
import { OrderType, PositionDirection, PostOnlyParams } from "@drift-labs/sdk"; await driftClient.placePerpOrder({ orderType: OrderType.LIMIT, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: driftClient.convertToPerpPrecision(1), price: driftClient.convertToPricePrecision(99), postOnly: PostOnlyParams.MUST_POST_ONLY, });
Method DriftClient.placePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Yes
txParams
TxParams
No
subAccountId
number
No
isolatedPositionDepositAmount
any
No
Returns
Promise<string>

Two sided quotes (place in one tx)

import { MarketType, OrderType, PositionDirection, PostOnlyParams } from "@drift-labs/sdk"; await driftClient.placeOrders([ { orderType: OrderType.LIMIT, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: driftClient.convertToPerpPrecision(1), price: driftClient.convertToPricePrecision(99.5), postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.LIMIT, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: driftClient.convertToPerpPrecision(1), price: driftClient.convertToPricePrecision(100.5), postOnly: PostOnlyParams.MUST_POST_ONLY, }, ]);
Method DriftClient.placeOrdersReference ↗
ParameterTypeRequired
params
OrderParams[]
Yes
txParams
TxParams
No
subAccountId
number
No
optionalIxs
TransactionInstruction[]
No
isolatedPositionDepositAmount
any
No
Returns
Promise<string>

Quote around oracle

import { PRICE_PRECISION, convertToNumber } from "@drift-labs/sdk"; const oracle = driftClient.getOracleDataForPerpMarket(0); const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION); console.log(oraclePrice);
Method DriftClient.getOracleDataForPerpMarketReference ↗
ParameterTypeRequired
marketIndex
number
Yes
Returns
OraclePriceData

Oracle offset orders

Oracle offset orders are the most efficient way to quote on Drift. Instead of specifying a fixed price, you set an offset from the oracle price. The order automatically floats with the oracle, so when the oracle moves, your order’s effective price moves with it.

Why this matters: With fixed price limit orders, you need to cancel and replace every time the oracle moves (thousands of transactions per day). With oracle offset orders, you place them once and they track the oracle automatically. A typical MM using oracle offsets sends ~30 transactions per day (just to adjust spread or size) vs thousands with cancel-replace.

How it works:

  • Set orderType: OrderType.ORACLE (not LIMIT)
  • Set oraclePriceOffset instead of price, this is the offset in PRICE_PRECISION units
  • Positive offset = above oracle, negative = below oracle
  • The onchain program evaluates oracle_price + offset at fill time
import { BN, PRICE_PRECISION, MarketType, OrderType, PositionDirection, PostOnlyParams, } from "@drift-labs/sdk"; const spreadOffset = 0.5; // $0.50 from oracle on each side const offsetBN = new BN(spreadOffset * PRICE_PRECISION.toNumber()); await driftClient.placeOrders([ { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: driftClient.convertToPerpPrecision(1), oraclePriceOffset: offsetBN.neg().toNumber(), // bid: oracle - $0.50 postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: driftClient.convertToPerpPrecision(1), oraclePriceOffset: offsetBN.toNumber(), // ask: oracle + $0.50 postOnly: PostOnlyParams.MUST_POST_ONLY, }, ]); console.log("Placed oracle offset quotes, these float with the oracle automatically!");
Method DriftClient.placeOrdersReference ↗
ParameterTypeRequired
params
OrderParams[]
Yes
txParams
TxParams
No
subAccountId
number
No
optionalIxs
TransactionInstruction[]
No
isolatedPositionDepositAmount
any
No
Returns
Promise<string>

Tip: You only need to update oracle offset orders when you want to change your spread or size. The oracle tracking is handled by the protocol at fill time.

Atomic cancel-and-replace with cancelAndPlaceOrders

When you do need to update quotes (e.g., changing spread or size based on inventory), use cancelAndPlaceOrders to atomically cancel existing orders and place new ones in a single transaction. This avoids the risk window where you have no orders on the book (between a cancel and a separate place).

import { BN, PRICE_PRECISION, MarketType, OrderType, PositionDirection, PostOnlyParams, } from "@drift-labs/sdk"; // Atomically cancel all perp orders for market 0 and place new quotes (single tx) const txSig = await driftClient.cancelAndPlaceOrders( { marketType: MarketType.PERP, marketIndex: 0, }, [ { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: driftClient.convertToPerpPrecision(1), oraclePriceOffset: new BN(-0.3 * PRICE_PRECISION.toNumber()).toNumber(), // tighter bid postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: driftClient.convertToPerpPrecision(1), oraclePriceOffset: new BN(0.3 * PRICE_PRECISION.toNumber()).toNumber(), // tighter ask postOnly: PostOnlyParams.MUST_POST_ONLY, }, ] );
Method DriftClient.cancelAndPlaceOrdersReference ↗
ParameterTypeRequired
cancelOrderParams
{ marketType?: MarketType; marketIndex?: number; direction?: PositionDirection; }
Yes
placeOrderParams
OrderParams[]
Yes
txParams
TxParams
No
subAccountId
number
No
Returns
Promise<string>

Inventory aware quoting (basic)

You can widen/tighten one side of your spread based on current inventory to reduce drift. For example, if you’re long, widen the bid (less eager to buy more) and tighten the ask (more eager to sell).

import { BASE_PRECISION, convertToNumber } from "@drift-labs/sdk"; const user = driftClient.getUser(); const position = user.getPerpPosition(0); if (position) { const positionSize = convertToNumber(position.baseAssetAmount, BASE_PRECISION); console.log(`Position: ${positionSize} SOL`); // Skew spread based on inventory const inventorySkew = positionSize * 0.01; // $0.01 per SOL of inventory const bidOffset = -0.5 - Math.max(0, inventorySkew); // widen bid when long const askOffset = 0.5 - Math.min(0, inventorySkew); // tighten ask when long }
Method User.getPerpPositionReference ↗
ParameterTypeRequired
marketIndex
number
Yes
Returns
PerpPosition | undefined

JIT maker (onchain “place-and-make”)

If you’re reacting to onchain taker auctions, Drift exposes a helper that “places a maker order and fills against a taker” atomically. This lets you participate in JIT auctions even while running resting orders, a hybrid approach.

// `takerInfo` comes from your taker discovery / order intake logic. await driftClient.placeAndMakePerpOrder(makerOrderParams, takerInfo);
Method DriftClient.placeAndMakePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Yes
takerInfo
TakerInfo
Yes
referrerInfo
ReferrerInfo
No
txParams
TxParams
No
subAccountId
number
No
Returns
Promise<string>

Risk management basics

Common MM guardrails:

  • Position limits: max long/short size to cap directional exposure
  • Minimum free collateral: ensure you can absorb adverse moves
  • Health / leverage checks: cancel all if leverage exceeds threshold
  • Emergency cancel: cancel all orders on errors, volatility spikes, or stale oracle
import { MarketType } from "@drift-labs/sdk"; // Cancel all orders for a specific market await driftClient.cancelOrders(MarketType.PERP, 0); // Cancel ALL orders across all markets (emergency) await driftClient.cancelOrders();
Method DriftClient.cancelOrdersReference ↗
ParameterTypeRequired
marketType
MarketType
No
marketIndex
number
No
direction
PositionDirection
No
txParams
TxParams
No
subAccountId
number
No
Returns
Promise<string>

Reference implementation

The FloatingPerpMaker in keeper-bots-v2 is a production example of oracle offset quoting. Key patterns it demonstrates:

  • Slot based cooldown: waits MARKET_UPDATE_COOLDOWN_SLOTS (30 slots) before re quoting a market, avoiding excessive transactions
  • Mutex guarded periodic tasks: uses async mutex to prevent overlapping quote updates
  • Position aware sizing: adjusts order size based on MAX_POSITION_EXPOSURE (percentage of account collateral)
  • Watchdog timer: tracks last successful update to detect stale bot state

Gotchas and production tips

  • Oracle offset precision: oraclePriceOffset is in raw PRICE_PRECISION units (1e6). An offset of 500000 = 0.50,not0.50, not 500,000. Double check your math.
  • Oracle offset orders still need updates: while they track the oracle automatically, you must cancel and re place when changing spread width, order size, or the number of levels. The cancelAndPlaceOrders method handles this atomically.
  • 32 order limit per subaccount: if you quote 5 markets x 2 sides x 3 levels = 30 orders, you’re near the limit. Use multiple subaccounts for multi market strategies (see JitMaker config for subaccount per market pattern).
  • MUST_POST_ONLY rejection: if the oracle moves sharply and your offset order would cross the spread, it’s rejected (not silently filled as taker). This is the desired behavior; catch the error and re quote.
  • Spot markets: oracle offset orders work for spot markets too. Use MarketType.SPOT and placeSpotOrder / placeAndMakeSpotOrder.

For production patterns (subscription loops, throttling, priority fees, graceful shutdown), see Bot architecture patterns.

Last updated on