Overview

A market maker profits by quoting both sides of the orderbook and capturing the spread. This recipe shows how to build a basic bot using the Provider API’s REST and WebSocket endpoints.
Market making involves significant risk. This is educational code, not production-ready. Always start on testnet and use proper risk management.

Architecture

Price Monitor — Connects via WebSocket to stream real-time mid prices and detect drift from your quoted spread. Order Manager — Uses REST to place bid/ask quotes and cancel stale orders when price moves.

Step 1: Configuration

const CONFIG = {
  provider: "hyperliquid",
  symbol: "ETH",
  // Spread: 5 bps on each side of mid
  spreadBps: 5,
  // Order size
  orderSize: "0.5",
  // Refresh interval (ms)
  refreshInterval: 5000,
  // Max position size before hedging
  maxPosition: "5.0",
};

Step 2: Track Mid Price via WebSocket

let midPrice = 0;

const ws = new WebSocket(
  `wss://api.perps.studio/ws/v1/perps?apiKey=${API_KEY}`
);

ws.onopen = () => {
  ws.send(JSON.stringify({
    event: "subscribe",
    data: {
      provider: CONFIG.provider,
      channel: "ticker",
      params: { symbol: CONFIG.symbol },
    },
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.channel === "ticker" && msg.data.markPrice) {
    midPrice = parseFloat(msg.data.markPrice);
  }
};

Step 3: Quote Management

async function refreshQuotes() {
  if (midPrice === 0) return;

  // Cancel existing orders
  const openOrders = await getOpenOrders();
  for (const order of openOrders) {
    await cancelOrder(order.orderId);
  }

  // Calculate bid/ask prices
  const spread = midPrice * (CONFIG.spreadBps / 10000);
  const bidPrice = (midPrice - spread).toFixed(2);
  const askPrice = (midPrice + spread).toFixed(2);

  // Check current position to avoid exceeding limits
  const positions = await getPositions();
  const currentSize = parseFloat(
    positions.find((p) => p.symbol === CONFIG.symbol)?.size ?? "0"
  );

  // Place bid (if not too long)
  if (currentSize < parseFloat(CONFIG.maxPosition)) {
    await placeOrder({
      symbol: CONFIG.symbol,
      side: "buy",
      type: "limit",
      size: CONFIG.orderSize,
      price: bidPrice,
    });
  }

  // Place ask (if not too short)
  if (currentSize > -parseFloat(CONFIG.maxPosition)) {
    await placeOrder({
      symbol: CONFIG.symbol,
      side: "sell",
      type: "limit",
      size: CONFIG.orderSize,
      price: askPrice,
    });
  }
}

// Refresh quotes on interval
setInterval(refreshQuotes, CONFIG.refreshInterval);

Step 4: Helper Functions

const BASE = "https://api.perps.studio/v1";
const headers = {
  "X-API-Key": API_KEY,
  "Content-Type": "application/json",
};

async function getOpenOrders() {
  return fetch(
    `${BASE}/perps/${CONFIG.provider}/account/${WALLET}/orders`,
    { headers },
  ).then((r) => r.json());
}

async function getPositions() {
  return fetch(
    `${BASE}/perps/${CONFIG.provider}/account/${WALLET}/positions`,
    { headers },
  ).then((r) => r.json());
}

async function placeOrder(params: Record<string, string>) {
  return fetch(`${BASE}/perps/${CONFIG.provider}/orders`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      ...params,
      wallet: WALLET,
      signature: await signOrder(params),
      nonce: Date.now(),
    }),
  }).then((r) => r.json());
}

async function cancelOrder(orderId: string) {
  return fetch(`${BASE}/perps/${CONFIG.provider}/orders`, {
    method: "DELETE",
    headers,
    body: JSON.stringify({
      symbol: CONFIG.symbol,
      orderId,
      wallet: WALLET,
      signature: await signCancel(orderId),
      nonce: Date.now(),
    }),
  });
}

Enhancements

Track your inventory (net position) and skew quotes accordingly. If you accumulate a long position, lower the bid and raise the ask to encourage sells.
Widen your spread during high volatility (detect via funding rate or price velocity). Tighten during calm periods.
Instead of a single bid/ask, place multiple orders at different price levels to provide deeper liquidity.
If you get filled on one provider, hedge the position on another provider to remain delta-neutral. See the Cross-Provider Arb recipe.