# Market Maker Quickstart

> Canonical: https://docs.velocity.exchange/developers/market-makers/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

> **Warning:**
>
> **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.

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

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

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

  
```js
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](/developers/market-makers/dlob-mm.md#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](/developers/market-makers/dlob-mm.md#oracle-offset-orders))
- **Inventory management:** adjust spread based on position size ([DLOB MM](/developers/market-makers/dlob-mm.md))
- **Risk controls:** position limits, health checks, emergency cancel ([Bot Architecture](/developers/market-makers/bot-architecture.md))
- **JIT participation:** compete in auctions for better fills ([JIT-only MM](/developers/market-makers/jit-only.md))
- **Efficient subscriptions:** WebSocket or gRPC for lower latency ([Bot Architecture](/developers/market-makers/bot-architecture.md))
- **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](/developers/market-makers/dlob-mm.md): comprehensive quoting strategies including oracle offset orders
- [Bot Architecture](/developers/market-makers/bot-architecture.md): 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
