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:
| Flag | Behavior | Use case |
|---|---|---|
MUST_POST_ONLY | Rejects the order if it would cross the spread and fill as taker | Default for MM, guarantees maker-only execution |
TRY_POST_ONLY | Amends the price to the best non crossing price if it would cross | Useful when you want the order placed regardless, even at a slightly different price |
SLIDE | Similar to TRY_POST_ONLY but slides price to the best bid/ask | Ensures 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 ↗
Class OrderParamsReference ↗| Property | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.placePerpOrderReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.placeOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.getOracleDataForPerpMarketReference ↗| Parameter | Type | Required |
|---|---|---|
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(notLIMIT) - Set
oraclePriceOffsetinstead ofprice, this is the offset in PRICE_PRECISION units - Positive offset = above oracle, negative = below oracle
- The onchain program evaluates
oracle_price + offsetat 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 ↗
Method DriftClient.placeOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.cancelAndPlaceOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method User.getPerpPositionReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.placeAndMakePerpOrderReference ↗| Parameter | Type | Required |
|---|---|---|
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 ↗
Method DriftClient.cancelOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 mutexto 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:
oraclePriceOffsetis in rawPRICE_PRECISIONunits (1e6). An offset of500000= 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
cancelAndPlaceOrdersmethod 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
JitMakerconfig for subaccount per market pattern). MUST_POST_ONLYrejection: 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.SPOTandplaceSpotOrder/placeAndMakeSpotOrder.
For production patterns (subscription loops, throttling, priority fees, graceful shutdown), see Bot architecture patterns.