> ## Documentation Index
> Fetch the complete documentation index at: https://docs.convallax.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Become a Market Maker

> Get started as an independent market maker on Convallax — get an API key, subscribe to the quote-request stream over SSE, submit quotes via REST, listen for wins on the post-trade WebSocket, sign your own settlement orders, and earn the spread.

Market makers on Convallax are **fully independent entities**. You hold your own wallet keys, you quote with your own wallet address, and you sign your own on-chain settlement orders. The backend never holds your keys and never signs on your behalf — it is purely a relay and matching engine.

You never hold option inventory. Collateral is locked **only when a trade actually fills**: `fill()` mints option tokens atomically and pulls collateral in the same transaction. All you need ready is USDC and two one-time USDC approvals.

## The Three-Channel Transport

Maker integration uses a **three-channel split**. Each channel does one job:

| Channel                  | Transport | Direction      | What it carries                                                                                                                         |
| ------------------------ | --------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **1 — Quote requests**   | SSE       | server → maker | Live `quote_request` events ([Quote Request Stream](/guides/quote-stream))                                                              |
| **2 — Quote submission** | REST      | maker → server | [`POST /v1/mm/quotes`](/api-reference/market-maker/submit-quote) and the [confirm signature](/api-reference/market-maker/confirm-quote) |
| **3 — Post-trade**       | WebSocket | server → maker | [`quote:accepted` / `quote:confirmed` / `quote:rejected`](/guides/websocket)                                                            |

<Info>
  You must keep **both** the SSE quote-request stream (channel 1) and the post-trade WebSocket (channel 3) open at the same time: the SSE stream tells you *what to quote*, and the WebSocket tells you *when you won*. Quotes and confirmation signatures go out over REST (channel 2).
</Info>

All three channels authenticate with the same per-MM API key. The SSE stream and the REST endpoints use the **`X-API-Key`** header (or `?apiKey=`); the WebSocket uses `?apiKey=` on the URL or an `auth` message.

## What You Need

<Steps>
  <Step title="An API key">
    A **per-MM API key** generated from the [Convallax dashboard](/settings) (Settings → API Keys). The key maps to your stable `makerId` and works immediately. See the [Authentication guide](/guides/authentication).
  </Step>

  <Step title="Your own wallet + key">
    You quote with your **own wallet address** (it becomes `Order.maker`) and sign settlement orders with that wallet's private key. The backend never holds it.
  </Step>

  <Step title="USDC + two approvals">
    Settlement is **atomic mint-on-fill** — there is no inventory to pre-mint and no ERC-1155 approvals. You only need USDC and two one-time USDC approvals, depending on which side of the trade you take:

    * **Posting collateral** (you become the writer): `usdc.approve(coreAddress, collateral)`. When a fill executes, `ConvallaxCore` pulls your collateral and mints fresh option tokens to the holder.
    * **Paying premium** (you are buying the option from the taker): `usdc.approve(settlementAddress, premium)`.

    For a standard long fill (the taker buys, `makerSelling = true`), **you post collateral** → approve **Core**. For a short fill (the taker writes, `makerSelling = false`), **you pay premium** → approve **Settlement**.

    <Tip>
      A maker that quotes **both** directions should approve **both** Core (for collateral) and Settlement (for premium) for USDC. There are **no** `setApprovalForAll` / ERC-1155 approvals anywhere in the model.
    </Tip>

    <Warning>
      Set your USDC approvals **before** quoting. When you win, you only have a few seconds (the confirmation deadline) to sign — there is no time to send approval transactions. Collateral itself is never locked until a trade actually fills.
    </Warning>
  </Step>
</Steps>

## How It Works

<Steps>
  <Step title="Subscribe to the quote-request stream (SSE)">
    Open an SSE connection to the maker stream with your API key in the `X-API-Key` header. On connect you receive a `connected` event, then a snapshot of currently open requests, then live `quote_request` events.

    ```
    GET https://api.convallax.com/v1/mm/quote-requests/stream
    X-API-Key: <your-key>
    ```

    ```
    event: connected
    data: { "makerId": "mm-alpha", "serverTime": "2026-06-15T12:00:00.000Z" }
    ```

    Each `quote_request` carries the trade parameters and an SSE `id:` for reconnect/replay. See the [Quote Request Stream guide](/guides/quote-stream) for the full event list and `Last-Event-ID` semantics.
  </Step>

  <Step title="Connect the post-trade WebSocket">
    In parallel, open the post-trade WebSocket so you learn the moment one of your quotes wins. On auth you receive a `connected` message with `protocolVersion: 3`.

    ```
    wss://api.convallax.com/maker/v1/ws?apiKey=<your-key>
    ```

    ```json theme={null}
    {
      "type": "connected",
      "protocolVersion": 3,
      "makerId": "mm-alpha",
      "authenticated": true,
      "serverTime": "2026-06-15T12:00:00.000Z"
    }
    ```
  </Step>

  <Step title="Submit your quote (REST)">
    For each `quote_request`, compute your price and `POST /v1/mm/quotes`. The `quote.maker` field **must be your own wallet address** — it becomes `Order.maker` and you must be able to sign for it.

    ```bash theme={null}
    POST /v1/mm/quotes
    X-API-Key: <your-key>
    {
      "requestId": "req-789",
      "quote": {
        "maker": "0xYourMakerWallet...",
        "side": "buy",
        "price": 0.18,
        "size": 100,
        "expires_in_ms": 5000
      }
    }
    ```

    The response returns a **server-generated** `quoteId` that is stable across updates for the same maker + request:

    ```json theme={null}
    { "success": true, "quoteId": "8f3c2b1a-..." }
    ```

    Store that `quoteId` keyed by `requestId` — you will need it to confirm if you win. You can **update your quote** at any time while the request is open by re-POSTing for the same `requestId`; only your latest quote is kept.
  </Step>

  <Step title="Win, sign, and confirm (WebSocket → REST)">
    When the trader commits and **your** quote wins, the post-trade WebSocket pushes a `quote:accepted` message containing the exact `Order`, the EIP-712 `domain`/`types`, and a `confirmationDeadline`:

    ```json theme={null}
    {
      "type": "quote:accepted",
      "quoteId": "8f3c2b1a-...",
      "requestId": "req-789",
      "order": { "maker": "0xYourMakerWallet...", "seriesId": "...", "optionAmount": "10000000", "premiumAmount": "1200000", "makerSelling": true, "taker": "0xTrader...", "validUntil": 1717189320, "nonce": 1717189200042 },
      "domain": { "name": "ConvallaxRFQSettlement", "version": "1", "chainId": 80002, "verifyingContract": "0x3E583BC44157c91F8c534372A3e91c371841107A" },
      "types": { "Order": [ /* field definitions */ ] },
      "confirmationDeadline": "2026-06-15T12:00:11.000Z"
    }
    ```

    Sign the `order` with your own wallet key and **POST the signature** to the confirm endpoint **before the deadline**:

    ```bash theme={null}
    POST /v1/mm/quotes/8f3c2b1a-.../confirm
    X-API-Key: <your-key>
    { "signature": "0x..." }
    ```

    The backend verifies that your signature recovers to `order.maker`, then pushes `quote:confirmed` to you (and `quote:rejected` to other makers).
  </Step>

  <Step title="Settle">
    Once you confirm, the taker receives your signed order and calls `fill()` on `ConvallaxRFQSettlement`. In a single atomic transaction, `fill()` calls `ConvallaxCore.mintFor(...)` to pull collateral from the writer, mint fresh option tokens to the holder, and move the premium — all or nothing. For a long fill you post collateral and the option is minted to the taker; for a short fill the taker posts collateral and the option is minted to you.
  </Step>

  <Step title="Reclaim collateral after settlement">
    On a long fill **you are the writer** — your USDC collateral stays locked in `ConvallaxCore` until the series expires and is settled. Once the series is settled (see the [Settlement guide](/guides/settlement)), reclaim your share of the leftover collateral with a single call:

    ```js theme={null}
    const core = new ethers.Contract(
      coreAddress,
      ["function claimWriterCollateral(uint256 seriesId) external"],
      wallet,
    );
    await core.claimWriterCollateral(seriesId);
    ```

    You receive `writerCollateral × writerPoolRemaining / totalCollateral` — your original collateral minus whatever was paid out to holders. This is a **one-shot** call per series (your recorded collateral is zeroed after). Poll [`GET /v1/series`](/api-reference/series/list) with `settled=true` to find expired series you wrote, then claim. See [`claimWriterCollateral()`](/api-reference/contracts/claim-writer) for details.
  </Step>
</Steps>

## Confirmation Deadline & Fallback

When a taker commits, the winning maker has a limited window (`MAKER_CONFIRMATION_MS`, default 10 seconds) to sign and POST the signature:

* **You confirm in time** → the taker settles against your signed order; you receive `quote:confirmed`.
* **You miss the deadline** → the backend falls back to the **next-best** maker's quote and asks them to confirm; you receive `quote:rejected`.
* **No maker confirms** → the request stays **open** and the taker can retry.

<Warning>
  If you miss the deadline you lose the trade to the next-best maker. Keep your USDC approvals (Core for collateral, Settlement for premium) set ahead of time so signing is instant.
</Warning>

## Core Quoting Loop

| Step      | Channel   | Action                                                                                                                     |
| --------- | --------- | -------------------------------------------------------------------------------------------------------------------------- |
| Subscribe | SSE       | `GET /v1/mm/quote-requests/stream` with `X-API-Key` — receive `connected`, snapshot, then `quote_request` events           |
| Connect   | WS        | `wss://.../maker/v1/ws?apiKey=...` — receive `connected` (protocol v3)                                                     |
| Quote     | REST      | `POST /v1/mm/quotes` with your `maker` wallet; store the returned `quoteId`; update anytime while open                     |
| Confirm   | WS → REST | On `quote:accepted`, sign and `POST /v1/mm/quotes/:quoteId/confirm` before the deadline                                    |
| Settle    | on-chain  | Taker calls `fill()` — atomic mint-on-fill (collateral locked + premium paid + tokens minted in one tx)                    |
| Reclaim   | on-chain  | After the series settles, call `claimWriterCollateral(seriesId)` to recover your leftover collateral (one-shot per series) |

## Winner Selection

| Taker action        | Winning quote                |
| ------------------- | ---------------------------- |
| **Buying** options  | **Lowest** valid price wins  |
| **Selling** options | **Highest** valid price wins |

## Quote Validation Rules

Your quote must pass these validation checks:

| Rule         | Constraint                                                                                                                                      |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Side         | Must match the taker's requested side (echoes `trade.side`)                                                                                     |
| Maker        | `quote.maker` must be a wallet you can sign for                                                                                                 |
| Price        | Per **one** option, in `(0, 1)`                                                                                                                 |
| Size (buy)   | The **max** whole options you'll fill at this price. Must be ≥ 1. The actual fill is `min(floor(trade.budgetUsd / price), size)` (partial fill) |
| Size (sell)  | Must be ≥ 50% of the requested `trade.size`                                                                                                     |
| No-arbitrage | Price ≤ max payoff (i.e. ≤ `1 − K` for calls, ≤ `K` for puts, where K = strikeBps/100)                                                          |

<Note>
  **Buy vs sell sizing.** On a **buy** the taker submits a USDC budget (`trade.budgetUsd`), not a contract count. You quote a **per-option `price`** and a **`size` = the most whole options you're willing to write at that price**. The taker receives `floor(budgetUsd / price)` options, capped by your `size` (a partial fill if your `size` is smaller). On a **sell** the taker submits a contract count (`trade.size`) and you quote the `size` you'll take. Either way, `price` is always per one option.
</Note>

## The Order You Sign (EIP-712)

```solidity theme={null}
struct Order {
    address maker;        // Your wallet (= quote.maker)
    uint256 seriesId;     // Option series (= ERC-1155 token ID)
    uint256 optionAmount; // Option tokens (6 decimals)
    uint256 premiumAmount;// USDC total premium (6 decimals)
    bool    makerSelling; // true when the taker is buying
    address taker;        // Restricted to this taker address
    uint256 validUntil;   // Order expiry (Unix timestamp)
    uint256 nonce;        // Replay protection
}
```

| Domain field        | Value                                        |
| ------------------- | -------------------------------------------- |
| `name`              | `ConvallaxRFQSettlement`                     |
| `version`           | `1`                                          |
| `chainId`           | `80002` (Polygon Amoy)                       |
| `verifyingContract` | `0x3E583BC44157c91F8c534372A3e91c371841107A` |

## Check Relay Status

Verify connectivity and see how many makers are online:

```bash theme={null}
GET /maker/v1/status
```

```json theme={null}
{
  "protocolVersion": 3,
  "makersConnected": 3,
  "socketsConnected": 4,
  "streamPath": "/v1/mm/quote-requests/stream",
  "quotePath": "/v1/mm/quotes",
  "wsPath": "/maker/v1/ws"
}
```

`makersConnected` counts makers subscribed to the SSE quote-request stream; `socketsConnected` counts live post-trade WebSocket sockets. See the [Relay Status reference](/api-reference/market-maker/status).

## Environment Variables

If you are running your own relay instance, these variables control auth and quote request behavior:

| Variable                | Default          | Description                                                |
| ----------------------- | ---------------- | ---------------------------------------------------------- |
| `MAKER_API_KEYS`        | —                | JSON array mapping API keys to `makerId`s (recommended)    |
| `MAKER_API_KEY`         | —                | Legacy single-key mode (maps to `makerId` `"default"`)     |
| `QUOTE_REQUEST_TTL_MS`  | `300000` (5 min) | How long quote requests stay open (ms)                     |
| `MAKER_CONFIRMATION_MS` | `10000`          | Winning-maker confirmation deadline (ms, range 2000–60000) |

If no maker keys are configured, the relay runs in **OPEN dev mode** (anonymous makerId, accepts anyone) — see the [Authentication guide](/guides/authentication).

## Dependencies (Node.js bots)

Market-maker bots run in **Node.js**, not in a browser. Install these packages before running the example below:

```bash theme={null}
npm install eventsource ws ethers
```

| Package                                                    | Why                                                                                                                                                                                                      |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`eventsource`](https://www.npmjs.com/package/eventsource) | The browser's built-in `EventSource` **cannot** set custom headers like `X-API-Key`. The npm package can — you need it to authenticate the SSE quote-request stream. (Totalis MMs use the same pattern.) |
| [`ws`](https://www.npmjs.com/package/ws)                   | WebSocket client for the post-trade channel (`quote:accepted`, etc.).                                                                                                                                    |
| [`ethers`](https://www.npmjs.com/package/ethers)           | Sign the EIP-712 `Order` when you win (`wallet.signTypedData`).                                                                                                                                          |

<Note>
  If your SSE client cannot set headers at all, you can pass the key on the URL instead: `GET .../quote-requests/stream?apiKey=<your-key>`. The npm `eventsource` package with `headers: { 'X-API-Key': ... }` is still the recommended approach.
</Note>

## Complete Worked Example Bot

A minimal bot that subscribes to the SSE stream, submits quotes via REST, listens on the WebSocket for wins, and signs winning orders with `ethers`:

```javascript theme={null}
import EventSource from 'eventsource';
import WebSocket from 'ws';
import { Wallet } from 'ethers';

const API = 'https://api.convallax.com';
const WS = 'wss://api.convallax.com';
const API_KEY = process.env.MAKER_API_KEY;
const wallet = new Wallet(process.env.MM_PRIVATE_KEY);

// Map requestId -> server-generated quoteId so we can confirm on accept.
const quoteIds = new Map();

function computePrice(option, trade) {
  // Your pricing model here. Return a price for ONE option, in (0, 1).
  return 0.12;
}

// The most whole options you're willing to write at `price`.
// Buys send a USDC budget; sells send a contract count.
function computeSize(option, trade, price) {
  if (trade.side === 'buy') {
    const wanted = Math.floor(trade.budgetUsd / price); // what the taker would get
    return Math.min(wanted, yourMaxContractsForRisk(option, price));
  }
  return trade.size; // sell: match the taker's requested contracts (or ≥ 50%)
}

// --- Channel 1: SSE quote-request stream (server -> maker) ---
const stream = new EventSource(`${API}/v1/mm/quote-requests/stream`, {
  headers: { 'X-API-Key': API_KEY },
});

stream.addEventListener('connected', (e) =>
  console.log('Stream connected as', JSON.parse(e.data).makerId)
);

stream.addEventListener('quote_request', async (e) => {
  const req = JSON.parse(e.data);
  const { option, trade } = req.params;
  const price = computePrice(option, trade);

  // --- Channel 2: submit/update the quote over REST ---
  const res = await fetch(`${API}/v1/mm/quotes`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
    body: JSON.stringify({
      requestId: req.requestId,
      quote: {
        maker: wallet.address,        // MUST be your own wallet
        side: trade.side,             // must match the taker's side
        price,                        // per ONE option, in (0, 1)
        size: computeSize(option, trade, price), // max whole options you'll fill
      },
    }),
  });
  const body = await res.json();
  if (body.success) quoteIds.set(req.requestId, body.quoteId);
});

stream.addEventListener('quote_request_expired', (e) => {
  quoteIds.delete(JSON.parse(e.data).requestId);
});

stream.onerror = () => console.log('Stream disconnected — will retry');

// --- Channel 3: post-trade WebSocket (server -> maker) ---
const ws = new WebSocket(`${WS}/maker/v1/ws?apiKey=${API_KEY}`);
ws.on('ping', () => ws.pong());

ws.on('message', async (raw) => {
  const msg = JSON.parse(raw);

  switch (msg.type) {
    case 'connected':
      console.log(`WebSocket connected (protocol v${msg.protocolVersion})`);
      break;

    case 'quote:accepted': {
      // You won — sign with your own key and confirm over REST before the deadline.
      const signature = await wallet.signTypedData(msg.domain, msg.types, msg.order);
      await fetch(`${API}/v1/mm/quotes/${msg.quoteId}/confirm`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
        body: JSON.stringify({ signature }),
      });
      console.log(`Confirmed quote ${msg.quoteId} for ${msg.requestId}`);
      break;
    }

    case 'quote:confirmed':
      console.log(`Won and confirmed: ${msg.requestId}`);
      break;

    case 'quote:rejected':
      console.log(`Lost ${msg.requestId}: ${msg.reason}`);
      break;
  }
});
```

<Info>
  Before going live, set your USDC approvals — `usdc.approve(coreAddress, ...)` if you'll post collateral, and `usdc.approve(settlementAddress, ...)` if you'll pay premium — so settlement succeeds the instant a taker fills your signed order. No inventory minting or ERC-1155 approvals are needed.
</Info>

## Provide Liquidity at Scale

If you're interested in becoming a liquidity provider at scale on Convallax, contact us at [support@convallax.com](mailto:support@convallax.com) to discuss integration support and partnership terms.
