Market Maker Quickstart
Get a simple market maker running in under 10 minutes. This guide shows how to place two-sided quotes that automatically update with oracle prices.
Prerequisites
- Node.js + TypeScript project
- Drift SDK installed:
npm i @drift-labs/sdk - Funded Solana account with USDC collateral
- Basic familiarity with async/await
⚠️ RPC choice matters: The default
https://api.mainnet-beta.solana.comis rate-limited and unsuitable for production bots. Use a dedicated RPC provider (Helius, Triton, etc.) or you’ll hit 429 errors within minutes. For WebSocket subscriptions, you need a provider that supportsaccountSubscribe.
Step 1: Initialize DriftClient
Set up your connection and subscribe to market data.
View DriftClient import
import { Connection } from "@solana/web3.js";
import { Wallet, DriftClient, loadKeypair } from "@drift-labs/sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = new Wallet(loadKeypair("~/.config/solana/id.json"));
const driftClient = new DriftClient({
connection,
wallet,
env: "mainnet-beta",
});
await driftClient.subscribe();
// Initialize your user account (if first time)
// const [txSig] = await driftClient.initializeUserAccount(0);Class DriftClientReference ↗Step 2: Get oracle price
Read the current oracle price to calculate your bid/ask spread.
import { PRICE_PRECISION, convertToNumber } from "@drift-labs/sdk";
const marketIndex = 0; // SOL-PERP
const oracle = driftClient.getOracleDataForPerpMarket(marketIndex);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);
console.log(`Oracle price: $${oraclePrice}`);Method DriftClient.getOracleDataForPerpMarketReference ↗| Name | Type | Default |
|---|---|---|
marketIndex | number |
Step 3: Place two-sided quotes
Place a bid (buy) below oracle and an ask (sell) above oracle. Using PostOnlyParams.MUST_POST_ONLY ensures your orders never cross and you always earn maker rebates.
import {
PRICE_PRECISION,
convertToNumber,
MarketType,
OrderType,
PositionDirection,
PostOnlyParams
} from "@drift-labs/sdk";
const marketIndex = 0; // SOL-PERP
const spread = 0.5; // $0.50 spread on each side
const size = 0.1; // 0.1 SOL per order
// Fetch oracle price for spread calculation
const oracle = driftClient.getOracleDataForPerpMarket(marketIndex);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);
const bidPrice = oraclePrice - spread;
const askPrice = oraclePrice + spread;
await driftClient.placeOrders([
{
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
marketIndex,
direction: PositionDirection.LONG,
baseAssetAmount: driftClient.convertToPerpPrecision(size),
price: driftClient.convertToPricePrecision(bidPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
},
{
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
marketIndex,
direction: PositionDirection.SHORT,
baseAssetAmount: driftClient.convertToPerpPrecision(size),
price: driftClient.convertToPricePrecision(askPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
},
]);
console.log(`Placed bid @ $${bidPrice}, ask @ $${askPrice}`);Method DriftClient.placeOrdersReference ↗| Name | Type | Default |
|---|---|---|
params | OrderParams[] | |
txParams | TxParams | |
subAccountId | number | |
optionalIxs | TransactionInstruction[] | |
isolatedPositionDepositAmount | any |
Step 4: Monitor and update
Check for fills and cancel/replace orders when the oracle moves. This complete example runs a loop that refreshes quotes every 10 seconds.
import {
PRICE_PRECISION,
BASE_PRECISION,
convertToNumber,
MarketType,
OrderType,
PositionDirection,
PostOnlyParams,
} from "@drift-labs/sdk";
const marketIndex = 0;
const spread = 0.5;
const size = 0.1;
setInterval(async () => {
try {
// Check current position
const user = driftClient.getUser();
const position = user.getPerpPosition(marketIndex);
if (position) {
const posSize = convertToNumber(position.baseAssetAmount, BASE_PRECISION);
console.log(`Current position: ${posSize} SOL`);
}
// Cancel all existing orders for this market
await driftClient.cancelOrders(MarketType.PERP, marketIndex);
// Re-fetch oracle price
const oracle = driftClient.getOracleDataForPerpMarket(marketIndex);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);
const bidPrice = oraclePrice - spread;
const askPrice = oraclePrice + spread;
// Place fresh two-sided quotes
await driftClient.placeOrders([
{
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
marketIndex,
direction: PositionDirection.LONG,
baseAssetAmount: driftClient.convertToPerpPrecision(size),
price: driftClient.convertToPricePrecision(bidPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
},
{
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
marketIndex,
direction: PositionDirection.SHORT,
baseAssetAmount: driftClient.convertToPerpPrecision(size),
price: driftClient.convertToPricePrecision(askPrice),
postOnly: PostOnlyParams.MUST_POST_ONLY,
},
]);
console.log(`Updated quotes: bid $${bidPrice.toFixed(2)} / ask $${askPrice.toFixed(2)}`);
} catch (err) {
console.error("Error updating quotes:", err);
}
}, 10_000); // Update every 10 secondsMethod DriftClient.cancelOrdersReference ↗| Name | Type | Default |
|---|---|---|
marketType | MarketType | |
marketIndex | number | |
direction | PositionDirection | |
txParams | TxParams | |
subAccountId | number |
Tip: This cancel-and-replace approach sends ~2 transactions every 10 seconds. For production, consider oracle offset orders which float with the oracle automatically and require only ~30 txs/day.
Next steps
This basic example gets you started, but production market makers need:
- Oracle offset orders , orders that automatically track oracle price, drastically reducing transactions (Normal MM)
- Inventory management , adjust spread based on position size (Normal MM)
- Risk controls , position limits, health checks, emergency cancel (Bot Architecture)
- JIT participation , compete in auctions for better fills (JIT-only MM)
- Efficient subscriptions , WebSocket or gRPC for lower latency (Bot Architecture)
- Multiple markets , quote across markets simultaneously
Common pitfalls
- Forgetting
PostOnlyParams, without it, your “maker” orders can cross the spread and execute as taker, paying fees instead of earning rebates - Using
PRICE_PRECISIONwrong , oracle prices are inPRICE_PRECISION(1e6), base amounts inBASE_PRECISION(1e9). Mixing them up causes orders at wildly wrong prices - Not initializing user account , first-time users must call
driftClient.initializeUserAccount()before placing orders. The SDK will throwUser account not foundotherwise - 32-order limit , each Drift subaccount supports a maximum of 32 open orders. Cancel stale orders or use multiple subaccounts for multi-market strategies
For production patterns and best practices, see:
- Normal MM - comprehensive quoting strategies including oracle offset orders
- Bot Architecture - subscription loops, throttling, priority fees
- keeper-bots-v2
FloatingPerpMaker- production reference for oracle offset quoting