JIT-only MM
JIT-only market making means you do not keep a standing book. Instead of resting limit orders on the DLOB, you compete in JIT auctions (see Matching Engine for liquidity sources) by reacting to incoming taker orders in real-time.
Why JIT-only?
- No adverse selection from stale quotes , you only commit capital when you choose to fill
- Selective flow , you inspect each taker order and decide if it’s profitable to fill
- Capital efficiency , no capital locked in resting orders that may never fill
- Dynamic pricing , price each fill based on current oracle, inventory, and market conditions
Tradeoff: You need lower-latency infrastructure than normal MM (to react within the auction window), and you may miss fills in fast markets if your bot is slow.
Architecture overview
A JIT-only bot follows this loop:
- Subscribe to auction and order feeds (onchain via
AuctionSubscriber, or offchain via SWIFT) - Filter incoming auctions , check oracle, position limits, toxic flow, profitability
- Price your fill , compute the best price you’re willing to offer given current conditions
- Fill atomically via
placeAndMakePerpOrder, your maker order is placed and matched against the taker in one transaction
Subscribe to auctions / orders
The AuctionSubscriber gives you a stream of active JIT auctions. Use commitment: "processed" for lowest latency.
View AuctionSubscriber import
import { AuctionSubscriber } from "@drift-labs/sdk";Class AuctionSubscriberReference ↗| Name | Type | Default |
|---|---|---|
driftClient | any | |
opts | any | |
resubOpts | any | |
eventEmitter | StrictEventEmitter<EventEmitter, AuctionSubscriberEvents> | |
subscriber | any | |
subscribe | () => Promise<void> | |
unsubscribe | () => Promise<void> |
import { AuctionSubscriber } from "@drift-labs/sdk";
const auctionSubscriber = new AuctionSubscriber({
driftClient,
opts: { commitment: "processed" },
});
await auctionSubscriber.subscribe();For even lower latency, subscribe to SWIFT to receive signed taker orders 100-500ms before they land onchain.
View OrderSubscriber import
import { OrderSubscriber } from "@drift-labs/sdk";Class OrderSubscriberReference ↗Compute auction prices (helpers)
Use getAuctionPrice to compute the current interpolated auction price at any slot. This tells you the worst price the taker would accept right now , you need to offer a price at least this good.
import { getAuctionPrice } from "@drift-labs/sdk";Function getAuctionPriceReference ↗| Name | Type | Default |
|---|---|---|
order | Order | |
slot | number | |
oraclePrice | anyUse MMOraclePriceData source for perp orders, OraclePriceData for spot |
import { getAuctionPrice, convertToNumber, PRICE_PRECISION } from "@drift-labs/sdk";
const currentSlot = await connection.getSlot();
const oracle = driftClient.getOracleDataForPerpMarket(marketIndex);
// Get the current auction price at this slot
const auctionPriceBN = getAuctionPrice(takerOrder, currentSlot, oracle.price);
const auctionPrice = convertToNumber(auctionPriceBN, PRICE_PRECISION);
console.log(`Auction price at slot ${currentSlot}: $${auctionPrice.toFixed(4)}`);See JIT Auctions - Auction pricing for the full interpolation formula and timeline explanation.
Fill as maker (atomic place-and-make)
This pattern places your maker order and fills against the taker in one transaction. You earn maker rebates and the taker gets filled , all atomic.
import {
OrderType,
PositionDirection,
PostOnlyParams,
} from "@drift-labs/sdk";
// Build your maker order (opposite direction of taker)
const makerOrderParams = {
orderType: OrderType.LIMIT,
marketIndex: takerOrder.marketIndex,
direction: PositionDirection.SHORT, // if taker is LONG
baseAssetAmount: takerOrder.baseAssetAmount,
price: driftClient.convertToPricePrecision(myFillPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
};
// takerInfo: includes taker's public keys, user account, and the order to fill
const takerInfo = {
taker: takerPubkey, // PublicKey of taker's user account PDA
takerStats: takerStatsPubkey, // PublicKey of taker's UserStats PDA
takerUserAccount: takerUserAccount, // decoded UserAccount data
order: takerOrder, // the specific Order to fill against
};
await driftClient.placeAndMakePerpOrder(makerOrderParams, takerInfo);Method DriftClient.placeAndMakePerpOrderReference ↗| Name | Type | Default |
|---|---|---|
orderParams | OptionalOrderParams | |
takerInfo | TakerInfo | |
referrerInfo | ReferrerInfo | |
txParams | TxParams | |
subAccountId | number |
Complete fill loop
Here’s a more complete example that ties the pieces together:
import {
AuctionSubscriber,
getAuctionPrice,
getUserStatsAccountPublicKey,
isSignedMsgOrder,
isVariant,
convertToNumber,
PRICE_PRECISION,
BASE_PRECISION,
OrderType,
PositionDirection,
PostOnlyParams,
} from "@drift-labs/sdk";
const MAX_POSITION = 100; // max 100 SOL position
const MIN_SPREAD = 0.02; // minimum $0.02 edge required
const auctionSubscriber = new AuctionSubscriber({
driftClient,
opts: { commitment: "processed" },
});
await auctionSubscriber.subscribe();
// Listen for auction events instead of polling
auctionSubscriber.eventEmitter.on("onAccountUpdate", async (takerUserAccount, pubkey, slot) => {
for (const order of takerUserAccount.orders) {
if (order.baseAssetAmount.isZero() || order.baseAssetAmount.eq(order.baseAssetAmountFilled)) continue;
const userAccount = takerUserAccount;
// Skip SWIFT orders if handling them via SwiftOrderSubscriber
if (isSignedMsgOrder(order)) continue;
const marketIndex = order.marketIndex;
const oracle = driftClient.getMMOracleDataForPerpMarket(marketIndex);
// Check oracle validity
if (!oracle.isValid) continue;
// Check position limits
const user = driftClient.getUser();
const position = user.getPerpPosition(marketIndex);
const currentSize = position
? Math.abs(convertToNumber(position.baseAssetAmount, BASE_PRECISION))
: 0;
const fillSize = convertToNumber(order.baseAssetAmount, BASE_PRECISION);
if (currentSize + fillSize > MAX_POSITION) continue;
// Get current auction price (use slot from the event, not an RPC call)
const auctionPriceBN = getAuctionPrice(order, slot, oracle.price);
const auctionPrice = convertToNumber(auctionPriceBN, PRICE_PRECISION);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);
// Calculate our fill price (oracle + small edge)
const takerIsLong = isVariant(order.direction, "long");
const edge = MIN_SPREAD;
const myFillPrice = takerIsLong
? oraclePrice + edge // sell to long taker above oracle
: oraclePrice - edge; // buy from short taker below oracle
// Check if our price is within the auction range
const isCompetitive = takerIsLong
? myFillPrice <= auctionPrice
: myFillPrice >= auctionPrice;
if (!isCompetitive) continue;
// Fill!
try {
await driftClient.placeAndMakePerpOrder(
{
orderType: OrderType.LIMIT,
marketIndex,
direction: takerIsLong ? PositionDirection.SHORT : PositionDirection.LONG,
baseAssetAmount: order.baseAssetAmount.sub(order.baseAssetAmountFilled),
price: driftClient.convertToPricePrecision(myFillPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
},
{
taker: pubkey,
takerStats: getUserStatsAccountPublicKey(driftClient.program.programId, userAccount.authority),
takerUserAccount: userAccount,
order,
}
);
console.log(`Filled ${fillSize} @ $${myFillPrice.toFixed(4)}`);
} catch (err) {
console.error("Fill failed:", err);
}
}
});Practical filters
Apply risk and filtering checks before filling: oracle validity, position limits, toxic-flow detection, and (if you use both feeds) skip Swift-origin orders via isSignedMsgOrder() so you don’t double-handle. See Bot Architecture - Risk and filtering for shared patterns and code.
import { isSignedMsgOrder } from "@drift-labs/sdk";Function isSignedMsgOrderReference ↗| Name | Type | Default |
|---|---|---|
order | Order |
const oracle = driftClient.getMMOracleDataForPerpMarket(0);Method DriftClient.getMMOracleDataForPerpMarketReference ↗| Name | Type | Default |
|---|---|---|
marketIndex | number |
Using JIT Proxy (JitterSniper / JitterShotgun)
Instead of building fill logic from scratch, use the @drift-labs/jit-proxy library which handles auction timing, transaction building, and retry logic:
import { JitterSniper, PriceType } from "@drift-labs/jit-proxy/lib";
const jitter = new JitterSniper({
auctionSubscriber,
driftClient,
slotSubscriber,
});
await jitter.subscribe();
// The jitter handles auction timing automatically
// You just need to configure pricing and filtersSee the JitMaker bot for a complete production example using JitterSniper/JitterShotgun with:
- Per-market subaccount isolation (1 subaccount per market)
- Volatility-based fill rejection (
isMarketVolatile) - DLOB-aware pricing (excludes own orders from best bid/ask calculation)
- Configurable target leverage and aggressiveness
Gotchas
- Don’t poll
getSlot()per auction , the example above callsgetSlot()for each auction, which is expensive at scale. Instead, use aSlotSubscriberto cache the current slot and read from it synchronously. isSignedMsgOrderfiltering , if you also subscribe to SWIFT, onchain auctions for SWIFT orders will appear inAuctionSubscribertoo. UseisSignedMsgOrder(order)to skip them in your onchain loop (handle them in SWIFT callback instead). See SWIFT API.- One subaccount per market , JIT fills can conflict if two markets try to use the same subaccount simultaneously. The
JitMakerenforces 1:1 subaccount-to-market mapping. - Fill rate tracking , track your fill success rate per market. If it drops below ~20%, your pricing or latency may need adjustment.
Related
- JIT Auctions - Auction mechanics, pricing formula, and timeline
- SWIFT API - Receive orders 100-500ms faster via offchain WebSocket
- Bot Architecture - Priority fees, health monitoring, graceful shutdown
- Normal MM - Resting order approach (can be combined with JIT)
- @drift-labs/jit-proxy - JIT proxy SDK with
JitterSniperandJitterShotgun