Skip to Content

DLOB MM

DLOB market making on Velocity 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.

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). Velocity offers three post-only modes:

FlagBehaviorUse case
MUST_POST_ONLYReverts the transaction if the order would cross the spread and fill as takerDefault for MM, guarantees maker-only execution (or a clear failure to react to)
TRY_POST_ONLYTransaction still succeeds, but the order is silently not placed if it would crossUseful when you’d rather skip a stale quote than fail the whole tx
SLIDEAmends the price to the best non-crossing price if it would crossEnsures placement at the top of book without crossing, at whatever price that requires

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.

Quoting basics

A two-sided quote means placing a bid (buy) and an ask (sell) simultaneously. The spread is the difference between your bid and ask prices, and that gap is where you earn. The tighter the spread, the more competitive you are but the less you earn per fill. Wider spreads earn more per fill but attract less flow.

Each quote is an OrderParams object passed to a placement method. Key fields are direction (long for bid, short for ask), price or oraclePriceOffset, baseAssetAmount, and postOnly. The examples below show the most common patterns.

import { OrderType, PositionDirection, PostOnlyParams } from "@velocity-exchange/sdk"; await velocityClient.placePerpOrder({ orderType: OrderType.LIMIT, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: velocityClient.convertToPerpPrecision(1), price: velocityClient.convertToPricePrecision(99), postOnly: PostOnlyParams.MUST_POST_ONLY, });
Method VelocityClient.placePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` / `triggerPrice` / `oraclePriceOffset` (signed) / `auctionStartPrice` / `auctionEndPrice` are PRICE_PRECISION (1e6).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to place the order for; defaults to the active sub-account.
No
isolatedPositionDepositAmount
any
If set and the order increases the position, a transfer into an isolated-margin position (token-mint precision) is prepended in the same transaction.
No
Returns
Promise<string>

Two sided quotes (place in one tx)

import { MarketType, OrderType, PositionDirection, PostOnlyParams } from "@velocity-exchange/sdk"; await velocityClient.placeOrders([ { orderType: OrderType.LIMIT, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: velocityClient.convertToPerpPrecision(1), price: velocityClient.convertToPricePrecision(99.5), postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.LIMIT, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: velocityClient.convertToPerpPrecision(1), price: velocityClient.convertToPricePrecision(100.5), postOnly: PostOnlyParams.MUST_POST_ONLY, }, ]);
Method VelocityClient.placeOrdersReference ↗
ParameterTypeRequired
params
OrderParams[]
Orders to place; `baseAssetAmount` is BASE_PRECISION (1e9) for perp (token-mint precision for spot), `price`/`triggerPrice`/`oraclePriceOffset` are PRICE_PRECISION (1e6).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to place the orders for; defaults to the active sub-account.
No
optionalIxs
TransactionInstruction[]
Extra instructions to prepend to the transaction.
No
isolatedPositionDepositAmount
any
If set and `params` has exactly one perp order that increases the position, a transfer into an isolated-margin position (token-mint precision) is prepended before placing. Ignored for batches of more than one order.
No
Returns
Promise<string>

Quote around oracle

import { PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk"; const oracle = velocityClient.getOracleDataForPerpMarket(0); const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION); console.log(oraclePrice);
Method VelocityClient.getOracleDataForPerpMarketReference ↗
ParameterTypeRequired
marketIndex
number
Perp market index.
Yes
Returns
OraclePriceData

Oracle offset orders

Oracle offset orders are the most efficient way to quote on Velocity. 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

oraclePriceOffset is a BN, not a number. It’s stored on-chain as an i64, so the SDK’s OrderParams.oraclePriceOffset type is BN. Pass the BN directly (don’t call .toNumber() on it) — the examples below do this correctly.

import { BN, PRICE_PRECISION, MarketType, OrderType, PositionDirection, PostOnlyParams, } from "@velocity-exchange/sdk"; const spreadOffset = 0.5; // $0.50 from oracle on each side const offsetBN = new BN(spreadOffset * PRICE_PRECISION.toNumber()); await velocityClient.placeOrders([ { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: velocityClient.convertToPerpPrecision(1), oraclePriceOffset: offsetBN.neg(), // bid: oracle - $0.50 postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: velocityClient.convertToPerpPrecision(1), oraclePriceOffset: offsetBN, // ask: oracle + $0.50 postOnly: PostOnlyParams.MUST_POST_ONLY, }, ]); console.log("Placed oracle offset quotes, these float with the oracle automatically!");
Method VelocityClient.placeOrdersReference ↗
ParameterTypeRequired
params
OrderParams[]
Orders to place; `baseAssetAmount` is BASE_PRECISION (1e9) for perp (token-mint precision for spot), `price`/`triggerPrice`/`oraclePriceOffset` are PRICE_PRECISION (1e6).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to place the orders for; defaults to the active sub-account.
No
optionalIxs
TransactionInstruction[]
Extra instructions to prepend to the transaction.
No
isolatedPositionDepositAmount
any
If set and `params` has exactly one perp order that increases the position, a transfer into an isolated-margin position (token-mint precision) is prepended before placing. Ignored for batches of more than one order.
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 "@velocity-exchange/sdk"; // Atomically cancel all perp orders for market 0 and place new quotes (single tx) const txSig = await velocityClient.cancelAndPlaceOrders( { marketType: MarketType.PERP, marketIndex: 0, }, [ { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.LONG, baseAssetAmount: velocityClient.convertToPerpPrecision(1), oraclePriceOffset: new BN(-0.3 * PRICE_PRECISION.toNumber()), // tighter bid postOnly: PostOnlyParams.MUST_POST_ONLY, }, { orderType: OrderType.ORACLE, marketType: MarketType.PERP, marketIndex: 0, direction: PositionDirection.SHORT, baseAssetAmount: velocityClient.convertToPerpPrecision(1), oraclePriceOffset: new BN(0.3 * PRICE_PRECISION.toNumber()), // tighter ask postOnly: PostOnlyParams.MUST_POST_ONLY, }, ] );
Method VelocityClient.cancelAndPlaceOrdersReference ↗
ParameterTypeRequired
cancelOrderParams
{ marketType?: MarketType; marketIndex?: number; direction?: PositionDirection; }
Filters for which open orders to cancel; see `cancelOrders` for semantics (`undefined` fields are not filtered on).
Yes
placeOrderParams
OrderParams[]
Orders to place after the cancel; `baseAssetAmount` is BASE_PRECISION (1e9), `price`/`triggerPrice`/`oraclePriceOffset` are PRICE_PRECISION (1e6).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to operate on; defaults to the active sub-account.
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 "@velocity-exchange/sdk"; const user = velocityClient.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, Velocity 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 velocityClient.placeAndMakePerpOrder(makerOrderParams, takerInfo);
Method VelocityClient.placeAndMakePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Maker order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` is PRICE_PRECISION (1e6). Must have `orderType: LIMIT`, `postOnly` set, and be IOC.
Yes
takerInfo
TakerInfo
The taker account/order to fill against (`takerInfo.order.orderId` must be open).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account placing the maker order; defaults to the active sub-account.
No
takerEscrow
RevenueShareEscrowAccount
The taker's decoded `RevenueShareEscrow`. Required to attach the escrow when the taker is referred but their order carries no builder code — the builder case is detected automatically from `takerInfo.order`.
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 "@velocity-exchange/sdk"; // Cancel all orders for a specific market await velocityClient.cancelOrders(MarketType.PERP, 0); // Cancel ALL orders across all markets (emergency) await velocityClient.cancelOrders();
Method VelocityClient.cancelOrdersReference ↗
ParameterTypeRequired
marketType
MarketType
Only cancel orders of this market type (`PERP`/`SPOT`); combined with `marketIndex` to scope to one perp or spot market.
No
marketIndex
number
Only cancel orders in this market index.
No
direction
PositionDirection
Only cancel orders on this side (`LONG`/`SHORT`).
No
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to cancel orders for; defaults to the active sub-account.
No
Returns
Promise<string>

Reference implementation

Resting orders you place are picked up by the hosted DLOB server — apps/dlob-server in the velocity-v1 monorepo  — which is what backs the REST/WebSocket orderbook described in Orderbook & Matching. You don’t need to run it yourself to make markets; it’s listed here in case you want to run your own instance or read its source for how order filtering/aggregation works.

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.
  • No spot DLOB trading: Velocity removed spot order-book trading entirely (place_spot_order, place_and_make_spot_order, and fill_spot_order no longer exist on-chain). Spot markets still exist for collateral and borrow-lend, but everything in this guide (placeOrders, MarketType.PERP, etc.) only applies to perp markets — there’s no spot equivalent to quote against.
  • Tick size: limit and oracle-offset prices are standardized to the market’s orderTickSize on-chain, and the DLOB does the same when computing effective/auction prices client-side. See Orderbook & Matching — tick size if you compute prices manually instead of letting the program clamp them.

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

Last updated on