> For the complete documentation index, see [llms.txt](https://dev.strixlab.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dev.strixlab.io/guides/market-maker-bot.md).

# Build a Market-Maker Bot

This guide builds a bot that quotes both sides of one outcome around the mid price, requotes when the market moves, and pulls its quotes on the way out. It's the shape most market-making strategies start from.

{% hint style="danger" %}
**Run it on testnet first.** `https://api-staging.strixlab.io/api` is a real order book with fake money. A quoting bug costs you nothing there and real USDC on mainnet.
{% endhint %}

## What the bot does

{% stepper %}
{% step %}

### Track the mid price

Subscribe to the order book over WebSocket and let the SDK maintain the book. The `onMid` callback fires whenever the touch moves.
{% endstep %}

{% step %}

### Quote around it

Place a bid at `mid − halfSpread` and an ask at `mid + halfSpread`, both tick-aligned. Resting orders are maker orders, and **makers pay no fee**.
{% endstep %}

{% step %}

### Requote when it moves

If the mid has moved by more than a threshold, cancel the old pair and place a new one. Requote too eagerly and you spend your rate-limit budget; too lazily and you get picked off.
{% endstep %}

{% step %}

### Always be able to stop

On `SIGINT`, on an unhandled rejection, on a disconnect — `cancelAll()` first, then exit.
{% endstep %}
{% endstepper %}

## Setup

{% code title=".env" %}

```bash
STRIX_BASE_URL=https://api-staging.strixlab.io/api
STRIX_API_KEY=...
STRIX_API_SECRET=...
STRIX_API_PASSPHRASE=...
MM_EVENT="BTC above 100k"
```

{% endcode %}

```bash
npm install strix-sdk
npm install -D tsx
```

## The bot

{% code title="mm-bot.ts" %}

```ts
import { StrixClient, StrixApiError } from 'strix-sdk';

// ── Config ──────────────────────────────────────────────────────────────────
const SPREAD      = 0.04;   // total spread — 2¢ each side of mid
const SIZE        = 50;     // shares per side
const REQUOTE_MIN = 0.005;  // requote when mid moves half a cent
const MAX_POSITION = 500;   // stop adding on a side past this inventory

const client = new StrixClient({
  baseUrl:       process.env.STRIX_BASE_URL!,
  apiKey:        process.env.STRIX_API_KEY!,
  apiSecret:     process.env.STRIX_API_SECRET!,
  apiPassphrase: process.env.STRIX_API_PASSPHRASE!,
});

// ── State ───────────────────────────────────────────────────────────────────
let tokenId  = '';
let tick     = 0.01;
let lastMid: number | null = null;
let liveHashes: string[]   = [];
let quoting  = false;

const alignToTick = (p: number) => Math.round(p / tick) * tick;
const clampPrice  = (p: number) => Math.min(1 - tick, Math.max(tick, p));

// ── Requote ─────────────────────────────────────────────────────────────────
async function requote(mid: number) {
  if (quoting) return;               // one cycle at a time
  quoting = true;
  try {
    // 1. Pull the old quotes. cancelBatch is rate-limit-exempt.
    if (liveHashes.length) {
      await client.orders.cancelBatch(liveHashes).catch(() => {});
      liveHashes = [];
    }

    // 2. Size the new ones against current inventory.
    const { availableBalance, positions } = await client.portfolio.balances();
    const held = Number(positions.find((p) => p.tokenId === tokenId)?.shareAmount ?? 0);

    const bid = clampPrice(alignToTick(mid - SPREAD / 2));
    const ask = clampPrice(alignToTick(mid + SPREAD / 2));

    const orders = [];
    if (held < MAX_POSITION && Number(availableBalance) > bid * SIZE) {
      orders.push({ tokenId, side: 'BUY' as const, price: bid, quantity: SIZE, orderType: 'GTC' as const });
    }
    if (held >= SIZE) {
      orders.push({ tokenId, side: 'SELL' as const, price: ask, quantity: Math.min(SIZE, held), orderType: 'GTC' as const });
    }
    if (!orders.length) return;

    // 3. One batch = one order-write against the rate limit.
    const results = await client.orders.placeBatch(orders);
    for (const r of results) {
      if (r.success) liveHashes.push(r.order.orderHash);
      else console.warn(`rejected #${r.index}: ${r.error}`);
    }

    lastMid = mid;
    console.log(`quoted ${bid.toFixed(3)} / ${ask.toFixed(3)}  (held ${held})`);
  } catch (err) {
    if (err instanceof StrixApiError) console.error(`requote failed ${err.status}: ${err.message}`);
    else throw err;
  } finally {
    quoting = false;
  }
}

// ── Main ────────────────────────────────────────────────────────────────────
async function main() {
  const { serverTime } = await client.serverTime();
  const drift = Math.abs(serverTime - Math.floor(Date.now() / 1000));
  if (drift > 5) throw new Error(`clock drift ${drift}s — sync NTP first`);

  const info = await client.markets.resolve(process.env.MM_EVENT!);
  tokenId = info.yes.tokenId;
  tick    = info.tickSize;
  console.log(`making ${info.marketTitle} · ${info.yes.name} · tick ${tick}`);

  // Fills — log them and let the next mid move resize the quotes.
  client.ws.user.connect();
  client.ws.user.on('trade.settled', (t) => console.log(`FILL ${t.side} ${t.quantity} @ ${t.price}`));
  client.ws.user.on('error', (e) => console.error('ws error:', e.message));

  // Book — the trigger for everything.
  client.ws.orderbook.subscribe([tokenId], {
    depth: 10,
    onMid: ({ yesMid }) => {
      if (yesMid === null) return;                                   // one-sided book
      if (lastMid !== null && Math.abs(yesMid - lastMid) < REQUOTE_MIN) return;
      void requote(yesMid);
    },
  });
}

// ── Shutdown ────────────────────────────────────────────────────────────────
async function shutdown(reason: string) {
  console.log(`\nshutting down (${reason}) — pulling quotes`);
  await client.orders.cancelAll().catch(() => {});
  client.destroy();
  process.exit(0);
}

process.on('SIGINT',  () => void shutdown('SIGINT'));
process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('unhandledRejection', (e) => { console.error(e); void shutdown('unhandledRejection'); });

main().catch(async (err) => { console.error(err); await shutdown('startup failure'); });
```

{% endcode %}

```bash
npx tsx mm-bot.ts
```

## Why it's written that way

<details>

<summary><strong>Why <code>cancelBatch</code> instead of two <code>cancel</code> calls?</strong></summary>

Single-order cancel shares the order-write rate-limit bucket with placements, so under load it can return `429` — and a failed cancel means **the order is still live**. `cancelBatch` and `cancelAll` are exempt, because a cancel only ever removes risk. In a requote loop, always use the bulk paths.

</details>

<details>

<summary><strong>Why one <code>placeBatch</code> instead of two <code>place</code> calls?</strong></summary>

A batch counts as a single order-write against the limit however many orders it carries, and both quotes land in the same instant rather than one spread-width apart. At 300 order-writes per 10 seconds, batching is what lets a bot quote many markets at once.

</details>

<details>

<summary><strong>Why does the bot never assume it filled?</strong></summary>

`place()` returns `status: "OPEN"` even for an order that crosses — matching and settlement are asynchronous, roughly 15–20 seconds. This bot reads inventory from `portfolio.balances()` on each requote and logs `trade.settled` for visibility. Never derive position from placement responses.

</details>

<details>

<summary><strong>Why the <code>quoting</code> flag?</strong></summary>

`onMid` can fire several times while an `await` is in flight. Without the guard, two cycles interleave: the second cancels orders the first hasn't recorded yet, and you end up with orphan quotes you no longer track — the most common way a simple bot ends up long a side it thought it was flat on.

</details>

<details>

<summary><strong>Why check the clock before starting?</strong></summary>

HMAC signatures are rejected when your timestamp is more than 30 seconds from server time. A drifting container clock produces a stream of `401 invalid_signature` errors that look exactly like bad credentials. Checking once at startup turns a confusing failure into a clear one.

</details>

## What to add next

<table><thead><tr><th width="240">Feature</th><th>Approach</th></tr></thead><tbody><tr><td><strong>Inventory skew</strong></td><td>Shift both quotes down as you get long, up as you get short. Cheapest real improvement to a naive two-sided quote.</td></tr><tr><td><strong>Multi-market</strong></td><td>One client, one socket, many tokens. Keep per-token state in a <code>Map</code> and batch requotes across markets.</td></tr><tr><td><strong>Manufactured inventory</strong></td><td><code>positions.split()</code> turns USDC into both legs at once, so you can quote a two-sided market without buying inventory first.</td></tr><tr><td><strong>Staleness guard</strong></td><td>If no book update arrives for N seconds, cancel everything. A silent socket is not a quiet market.</td></tr><tr><td><strong>Wider spreads near resolution</strong></td><td>Watch <code>events.resolution()</code> and back off as the outcome becomes knowable.</td></tr></tbody></table>

{% hint style="warning" %}
**Self-trading is rejected**, not silently crossed. If your bid would take your own resting ask — including the equivalent order on the sibling outcome, translated into YES terms — the placement is refused. Cancel before you requote, exactly as this bot does.
{% endhint %}

Before running anything with real money, work through [Going to Production](/guides/production-checklist.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://dev.strixlab.io/guides/market-maker-bot.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
