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:
| Flag | Behavior | Use case |
|---|---|---|
MUST_POST_ONLY | Reverts the transaction if the order would cross the spread and fill as taker | Default for MM, guarantees maker-only execution (or a clear failure to react to) |
TRY_POST_ONLY | Transaction still succeeds, but the order is silently not placed if it would cross | Useful when you’d rather skip a stale quote than fail the whole tx |
SLIDE | Amends the price to the best non-crossing price if it would cross | Ensures 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 ↗
Method VelocityClient.placePerpOrderReference ↗| Parameter | Type | Required |
|---|---|---|
orderParams | OptionalOrderParamsOrder to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` /
`triggerPrice` / `oraclePriceOffset` (signed) / `auctionStartPrice` / `auctionEndPrice` are
PRICE_PRECISION (1e6). | Yes |
txParams | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-account to place the order for; defaults to the active sub-account. | No |
isolatedPositionDepositAmount | anyIf 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 ↗
Method VelocityClient.placeOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-account to place the orders for; defaults to the active sub-account. | No |
optionalIxs | TransactionInstruction[]Extra instructions to prepend to the transaction. | No |
isolatedPositionDepositAmount | anyIf 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 ↗
Method VelocityClient.getOracleDataForPerpMarketReference ↗| Parameter | Type | Required |
|---|---|---|
marketIndex | numberPerp 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(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
oraclePriceOffsetis aBN, not anumber. It’s stored on-chain as ani64, so the SDK’sOrderParams.oraclePriceOffsettype isBN. Pass theBNdirectly (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 ↗
Method VelocityClient.placeOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-account to place the orders for; defaults to the active sub-account. | No |
optionalIxs | TransactionInstruction[]Extra instructions to prepend to the transaction. | No |
isolatedPositionDepositAmount | anyIf 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 ↗
Method VelocityClient.cancelAndPlaceOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
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 | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-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 ↗
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, 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 ↗
Method VelocityClient.placeAndMakePerpOrderReference ↗| Parameter | Type | Required |
|---|---|---|
orderParams | OptionalOrderParamsMaker order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price`
is PRICE_PRECISION (1e6). Must have `orderType: LIMIT`, `postOnly` set, and be IOC. | Yes |
takerInfo | TakerInfoThe taker account/order to fill against (`takerInfo.order.orderId` must be open). | Yes |
txParams | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-account placing the maker order; defaults to the active sub-account. | No |
takerEscrow | RevenueShareEscrowAccountThe 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 ↗
Method VelocityClient.cancelOrdersReference ↗| Parameter | Type | Required |
|---|---|---|
marketType | MarketTypeOnly cancel orders of this market type (`PERP`/`SPOT`); combined with
`marketIndex` to scope to one perp or spot market. | No |
marketIndex | numberOnly cancel orders in this market index. | No |
direction | PositionDirectionOnly cancel orders on this side (`LONG`/`SHORT`). | No |
txParams | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-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 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.- No spot DLOB trading: Velocity removed spot order-book trading entirely (
place_spot_order,place_and_make_spot_order, andfill_spot_orderno 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
orderTickSizeon-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.