DLOB MM
The DLOB is to be removed. An order will rest in one CLOB market account instead
of in the User account of its owner. The fill route already moves that way: a
taker remainder that can rest goes onto the book, not into User.orders. This
page describes the DLOB as it works now. New work belongs on the CLOB book. See
PropAMM and CLOB Order Flow.
DLOB market making on Velocity means resting two-sided quotes on the decentralized orderbook and earning a maker rebate when takers trade against them. A maker provides liquidity with a resting order and earns the rebate; a taker removes liquidity by crossing the spread and pays the fee. Nothing stops a maker from doing both: resting quotes and JIT auction participation run side by side.
Always use post-only for maker quotes
The whole strategy depends on staying on the maker side of every fill. A quote that crosses and executes as a taker pays a fee instead of earning a rebate, which inverts the economics of the intended trade. Post-only flags are what prevent that, and Velocity offers three of them:
| 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 skipping a stale quote beats failing the whole transaction |
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 |
Use MUST_POST_ONLY for all quotes. If the oracle moves and an order would cross, cancelling and requoting at the new price is a better outcome than taking by accident, and a reverted transaction makes that visible.
Quoting basics
A two-sided quote is a bid and an ask placed at the same time. The gap between them is the spread, and that gap is where the strategy earns. A tighter spread attracts more flow and earns less per fill; a wider one earns more per fill and sees less flow.
Each quote is an OrderParams object passed to a placement method. The fields that matter are direction (long for the bid, short for the ask), price or oraclePriceOffset, baseAssetAmount, and postOnly.
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,
});Placing both sides in one transaction
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,
},
]);Reading the oracle price
import { PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";
const oracle = velocityClient.getOracleDataForPerpMarket(0);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);
console.log(oraclePrice);Oracle offset orders
A fixed-price limit order goes stale the moment the oracle moves, so quoting with fixed prices means cancelling and replacing on every oracle tick: thousands of transactions a day, each one a chance to be late.
An oracle offset order carries an offset from the oracle price instead of a price. Its effective price moves with the oracle, so a single placement keeps tracking. A desk quoting this way sends roughly 30 transactions per day, and only to change spread or size.
How it works:
- Keep
orderType: OrderType.LIMIT: oracle tracking comes fromoraclePriceOffset, not from the order type - 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
Don't use
OrderType.ORACLEfor maker quotes. Onchain,OrderType::Oracleis classified as a market/auction (taker-style) order, not a restable maker order: it's for takers who want to execute immediately at a price relative to the oracle, not for resting liquidity. Oracle-tracking for resting orders is controlled entirely byoraclePriceOffset, which works on aLIMITorder just as well. The examples below correctly useOrderType.LIMITwithoraclePriceOffsetset.
oraclePriceOffsetis aBN, not anumber. It's stored onchain 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.LIMIT,
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.LIMIT,
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!");Tip: oracle offset orders need an update only to change spread or size. The oracle tracking is handled by the protocol at fill time.
Atomic cancel-and-replace with cancelAndPlaceOrders
When quotes do need updating (e.g., changing spread or size based on inventory), cancelAndPlaceOrders atomically cancels existing orders and places new ones in a single transaction. That avoids the window with no orders on the book that a cancel followed by a separate place leaves open.
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.LIMIT,
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.LIMIT,
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,
},
]
);Inventory-aware quoting
Widening or tightening one side of the spread against current inventory reduces drift. A long position calls for a wider bid (less eager to buy more) and a tighter 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
}JIT maker (onchain place-and-make)
For a bot reacting to onchain taker auctions, Velocity exposes a helper that places a maker order and fills against a taker atomically. A maker can run this alongside resting orders, a hybrid approach.
// `takerInfo` comes from the bot's taker discovery / order intake logic.
await velocityClient.placeAndMakePerpOrder(makerOrderParams, takerInfo);Risk management basics
Common MM guardrails:
- Position limits: max long/short size to cap directional exposure
- Minimum free collateral: keep enough headroom to 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();Reference implementation
Resting orders are picked up by the hosted DLOB server, which is what backs the REST and WebSocket orderbook described in Orderbook & Matching. Making markets does not require running one.
Its source lives at apps/dlob-server in the velocity-v1 monorepo, which is not public yet. Read it to run a private instance, or to see exactly how order filtering and aggregation work.
The FloatingPerpMaker in keeper-bots-v2 is a production example of oracle offset quoting. Key patterns it demonstrates:
- Wall-clock cooldown: waits
MARKET_UPDATE_COOLDOWN_MS(12 seconds), converted to slots at the live slot duration, before requoting a market, which keeps the transaction count down - 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= $0.50, not $500,000. Double check the math. - Oracle offset orders still need updates: while they track the oracle automatically, changing spread width, order size, or the number of levels still takes a cancel and replace. The
cancelAndPlaceOrdersmethod handles this atomically. - 32 order limit per subaccount: quoting 5 markets x 2 sides x 3 levels = 30 orders sits 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 an offset order would cross the spread, the order is rejected rather than silently filled as taker. This is the desired behavior; catch the error and requote.- No spot DLOB trading: Velocity removed spot order-book trading entirely (
place_spot_order,place_and_make_spot_order, andfill_spot_orderno longer exist onchain). 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
orderTickSizeonchain, and the DLOB does the same when computing effective/auction prices client-side. See Orderbook & Matching: tick size if prices are computed manually rather than left for the program to clamp.
For production patterns (subscription loops, throttling, priority fees, graceful shutdown), see Bot architecture patterns.