# DLOB MM

> Canonical: https://docs.velocity.exchange/developers/market-makers/dlob-mm

> **Warning:**
>
> 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](/developers/concepts/propamm-and-clob-order-flow.md).

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`.

  
```js
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

  
```js
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

  
```js
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 from `oraclePriceOffset`, not from the order type
- 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

> **Don't use `OrderType.ORACLE` for maker quotes.** Onchain, `OrderType::Oracle` is 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 by `oraclePriceOffset`, which works on a `LIMIT` order just as well. The examples below correctly use `OrderType.LIMIT` with `oraclePriceOffset` set.

> **`oraclePriceOffset` is a `BN`, not a `number`.** It's stored onchain 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.

  
```js
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.

  
```js
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).

  
```js
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.

  
```js
// `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

  
```js
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](./orderbook-and-matching#accessing-the-dlob). 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-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, 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 `cancelAndPlaceOrders` method 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 `JitMaker` config for subaccount per market pattern).
- **`MUST_POST_ONLY` rejection:** 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`, and `fill_spot_order` no 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 `orderTickSize` onchain, and the DLOB does the same when computing effective/auction prices client-side. See [Orderbook & Matching: tick size](/developers/market-makers/orderbook-and-matching.md#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](/developers/market-makers/bot-architecture.md).
