Velocity ProtocolDevelopers
Market Makers

Market Maker Quickstart

Get a two-sided market maker running in under 10 minutes. This guide places a bid and an ask, then refreshes them as the oracle price moves.

Prerequisites

  • Node.js + TypeScript project
  • Velocity SDK installed: bun add @velocity-exchange/sdk
  • Funded Solana account with collateral deposited into a spot market (the accepted collateral mints are configured onchain, per spot market)
  • Basic familiarity with async/await

RPC choice matters. The default https://api.mainnet-beta.solana.com is rate-limited and unsuitable for production bots: a quoting loop hits 429 errors within minutes. Use a dedicated RPC provider, and for WebSocket subscriptions one that supports accountSubscribe.

Step 1: Initialize VelocityClient

Set up your connection and subscribe to market data.

import { Connection } from "@solana/web3.js";
import { Wallet, VelocityClient, loadKeypair } from "@velocity-exchange/sdk";

const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = new Wallet(loadKeypair("~/.config/solana/id.json"));

const velocityClient = new VelocityClient({
  connection,
  wallet,
  env: "mainnet-beta",
});


await velocityClient.subscribe();

// Initialize the user account (if first time)
// const [txSig] = await velocityClient.initializeUserAccount(0);

Step 2: Get oracle price

Read the current oracle price to calculate the bid/ask spread.

import { PRICE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";

const marketIndex = 0; // SOL-PERP
const oracle = velocityClient.getOracleDataForPerpMarket(marketIndex);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);

console.log(`Oracle price: $${oraclePrice}`);

Step 3: Place two-sided quotes

Place a bid (buy) below oracle and an ask (sell) above oracle. PostOnlyParams.MUST_POST_ONLY stops either order from crossing, so every fill settles on the maker side.

import {
  PRICE_PRECISION,
  convertToNumber,
  MarketType,
  OrderType,
  PositionDirection,
  PostOnlyParams
} from "@velocity-exchange/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 = velocityClient.getOracleDataForPerpMarket(marketIndex);
const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);

const bidPrice = oraclePrice - spread;
const askPrice = oraclePrice + spread;

await velocityClient.placeOrders([
  {
    orderType: OrderType.LIMIT,
    marketType: MarketType.PERP,
    marketIndex,
    direction: PositionDirection.LONG,
    baseAssetAmount: velocityClient.convertToPerpPrecision(size),
    price: velocityClient.convertToPricePrecision(bidPrice),
    postOnly: PostOnlyParams.MUST_POST_ONLY,
  },
  {
    orderType: OrderType.LIMIT,
    marketType: MarketType.PERP,
    marketIndex,
    direction: PositionDirection.SHORT,
    baseAssetAmount: velocityClient.convertToPerpPrecision(size),
    price: velocityClient.convertToPricePrecision(askPrice),
    postOnly: PostOnlyParams.MUST_POST_ONLY,
  },
]);

console.log(`Placed bid @ $${bidPrice}, ask @ $${askPrice}`);

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 "@velocity-exchange/sdk";

const marketIndex = 0;
const spread = 0.5;
const size = 0.1;

setInterval(async () => {
  try {
    // Check current position
    const user = velocityClient.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 velocityClient.cancelOrders(MarketType.PERP, marketIndex);

    // Re-fetch oracle price
    const oracle = velocityClient.getOracleDataForPerpMarket(marketIndex);
    const oraclePrice = convertToNumber(oracle.price, PRICE_PRECISION);

    const bidPrice = oraclePrice - spread;
    const askPrice = oraclePrice + spread;

    // Place fresh two-sided quotes
    await velocityClient.placeOrders([
      {
        orderType: OrderType.LIMIT,
        marketType: MarketType.PERP,
        marketIndex,
        direction: PositionDirection.LONG,
        baseAssetAmount: velocityClient.convertToPerpPrecision(size),
        price: velocityClient.convertToPricePrecision(bidPrice),
        postOnly: PostOnlyParams.MUST_POST_ONLY,
      },
      {
        orderType: OrderType.LIMIT,
        marketType: MarketType.PERP,
        marketIndex,
        direction: PositionDirection.SHORT,
        baseAssetAmount: velocityClient.convertToPerpPrecision(size),
        price: velocityClient.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 seconds

This cancel-and-replace loop sends two transactions every 10 seconds, roughly 17,000 per day. Production desks quote with oracle offset orders instead: they float with the oracle on their own, so the transaction count drops to about 30 per day.

Next steps

This example is a starting point. Production market makers also need:

  • Oracle offset orders: orders that automatically track oracle price, drastically reducing transactions (DLOB MM)
  • Inventory management: adjust spread based on position size (DLOB 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, an order meant as a maker quote can cross the spread and execute as taker, paying the taker fee instead of earning the maker rebate
  • Using PRICE_PRECISION wrong: oracle prices are in PRICE_PRECISION (1e6), base amounts in BASE_PRECISION (1e9). Mixing them up causes orders at wildly wrong prices
  • Not initializing user account: first-time users must call velocityClient.initializeUserAccount() before placing orders. The SDK will throw User account not found otherwise
  • 32-order limit: each Velocity 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:

  • DLOB MM: comprehensive quoting strategies including oracle offset orders
  • Bot Architecture: subscription loops, throttling, priority fees
  • FloatingPerpMaker in keeper-bots-v2: the production reference for oracle offset quoting. It lives in the velocity-v1 monorepo, which is not public yet