Skip to Content

Normal MM

“Normal” 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.

View OrderParams import

import { OrderParams } from "@drift-labs/sdk";
Class OrderParamsReference ↗
NameTypeDefault
orderTypeOrderType
marketTypeMarketType
userOrderIdnumber
directionPositionDirection
baseAssetAmountBN
priceBN
marketIndexnumber
reduceOnlyboolean
postOnlyPostOnlyParams
bitFlagsnumber
triggerPriceany
triggerConditionOrderTriggerCondition
oraclePriceOffsetnumber | null
auctionDurationnumber | null
maxTsany
auctionStartPriceany
auctionEndPriceany
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 ↗
Parameters:
NameTypeDefault
orderParamsOptionalOrderParams
txParamsTxParams
subAccountIdnumber
isolatedPositionDepositAmountany
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 ↗
Parameters:
NameTypeDefault
paramsOrderParams[]
txParamsTxParams
subAccountIdnumber
optionalIxsTransactionInstruction[]
isolatedPositionDepositAmountany
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 ↗
Parameters:
NameTypeDefault
marketIndexnumber
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 , 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 ↗
Parameters:
NameTypeDefault
paramsOrderParams[]
txParamsTxParams
subAccountIdnumber
optionalIxsTransactionInstruction[]
isolatedPositionDepositAmountany
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 ↗
Parameters:
NameTypeDefault
cancelOrderParams{ marketType?: MarketType; marketIndex?: number; direction?: PositionDirection; }
placeOrderParamsOrderParams[]
txParamsTxParams
subAccountIdnumber
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 ↗
Parameters:
NameTypeDefault
marketIndexnumber
Returns:
PerpPosition | undefined

JIT maker (on-chain “place-and-make”)

If you’re reacting to on-chain 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 ↗
Parameters:
NameTypeDefault
orderParamsOptionalOrderParams
takerInfoTakerInfo
referrerInfoReferrerInfo
txParamsTxParams
subAccountIdnumber
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 ↗
Parameters:
NameTypeDefault
marketTypeMarketType
marketIndexnumber
directionPositionDirection
txParamsTxParams
subAccountIdnumber
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