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

# Stream Quote Requests

> Subscribe to all active quote requests in real time via Server-Sent Events.

Open a Server-Sent Events (SSE) stream to receive all active quote requests in real time. This is the primary integration path for market makers — use it instead of polling. The stream emits new requests as they arrive, updates when requests change, and notifies when requests expire.

## Authentication

API key required. Pass your market maker API key in the `X-API-Key` header.

```
X-API-Key: <key>
```

## SSE event types

This stream sends seven event names. Their spelling isn't consistent: `quote_request` and `quote_request_expired` are underscore-separated, while `quote_request:updated` is colon-separated. The subsections below — and their `<Warning>`s — cover why:

| Event                   | Fires when                                                                          |
| ----------------------- | ----------------------------------------------------------------------------------- |
| `connected`             | Once, immediately after connecting.                                                 |
| `snapshot_begin`        | Start of the initial active request snapshot (on connect or reconnect).             |
| `quote_request`         | A new quote request is created, or an active one is replayed during the snapshot.   |
| `quote_request:updated` | A request you've already seen bumps to a new `version` (legs or bet amount edited). |
| `snapshot_complete`     | The initial snapshot finishes; the stream switches to incremental updates only.     |
| `quote_request_expired` | A previously seen request is no longer active (committed, cancelled, or expired).   |
| `cashout_request`       | An early cashout auction opens on a position. Broadcast to every market maker.      |

### `connected`

Sent immediately on connection. Confirms your identity.

```json theme={null}
{
  "mm_id": "mm-uuid-1234"
}
```

### `snapshot_begin`

Sent at the start of the initial snapshot (on new connection or reconnect). All currently active quote requests follow before `snapshot_complete`.

```json theme={null}
{
  "page_size": 500
}
```

### `quote_request`

Sent when a **new** quote request is created, and once for each active request replayed during the initial snapshot. This is the primary event you price against.

```json theme={null}
{
  "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40",
  "user_id": "did:privy:cm3abc...",
  "input_legs": [
    {"market_ticker": "KXBTC-26JUN01-T72500", "side": "yes", "venue": "kalshi"},
    {"market_ticker": "KXBTC-26JUN01-T73000", "side": "no", "venue": "kalshi"}
  ],
  "legs": [
    {
      "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6",
      "event_ticker": "KXBTC-26JUN01",
      "market_ticker": "KXBTC-26JUN01-T72500",
      "side": "yes",
      "venue": "kalshi"
    },
    {
      "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "event_ticker": "KXBTC-26JUN01",
      "market_ticker": "KXBTC-26JUN01-T73000",
      "side": "no",
      "venue": "kalshi"
    }
  ],
  "bet_amount": 25,
  "user_stake": 24.75,
  "implied_probability": 0,
  "version": 1,
  "request_hash": "sha256:9f86d08...",
  "expires_at": "2026-06-01T18:45:30.000Z"
}
```

### `quote_request:updated`

Sent when a request you have **already seen** changes to a new `version` — the user edited the legs or the bet amount. The payload is identical to `quote_request` (with the incremented `version` and new `request_hash`). Any quote you submitted for the prior version is automatically invalidated; re-price and submit a new quote against the new `version` and `request_hash`. You do not need to withdraw the prior quote — it never blocks the new submission.

<Warning>
  This is a distinct event name from `quote_request`. If your client only registers a listener for `quote_request`, it will miss every version bump. Register a listener for `quote_request:updated` as well (or, with a raw SSE parser, branch on the `event:` field).
</Warning>

```json theme={null}
{
  "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40",
  "user_id": "did:privy:cm3abc...",
  "input_legs": [ "..." ],
  "legs": [ "..." ],
  "bet_amount": 50,
  "user_stake": 49.50,
  "implied_probability": 0,
  "version": 2,
  "request_hash": "sha256:3b1f9c2...",
  "expires_at": "2026-06-01T18:46:10.000Z"
}
```

### `snapshot_complete`

Sent when the initial snapshot is finished. After this, the stream switches to incremental updates only.

```json theme={null}
{
  "pages": 2,
  "total": 87
}
```

### `quote_request_expired`

Sent when a previously seen quote request is no longer active. Remove it from your local state. The `reason` field tells you why it closed, and lets you distinguish a lost auction from an abandoned or timed-out one:

| `reason`    | Meaning                                                                                                                                           |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `committed` | The user committed to a quote. The auction is over — check `won`. This can arrive well before `expires_at`; there is no minimum auction duration. |
| `cancelled` | The user abandoned the request before committing to any quote.                                                                                    |
| `expired`   | The request reached `expires_at` with no commit.                                                                                                  |

`won` is `true` only when `reason` is `committed` **and your quote was the one selected**; otherwise `false` (including every `cancelled`/`expired` close). A `committed` close with `won: false` means you were out-bid — the user took another market maker's quote. The winning maker's identity and odds are never disclosed.

`won` is **omitted** on the rare `committed` close where the outcome is indeterminate (e.g. the event was produced by a previous server instance during a deploy). Treat an absent `won` as *unknown* — don't record it as a loss.

```json theme={null}
{
  "id": "1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40",
  "reason": "committed",
  "won": false
}
```

### `cashout_request`

Sent when a user opens an [early cashout auction](/guides/early-cashout) on a position, and once for each active auction replayed during the initial snapshot. These arrive on the same stream and are **broadcast to every market maker** — including positions you do not back. Bid with [`PUT /v1/mm/cashout-requests/{id}/quote`](/api-reference/quote-service-mm/cashout-quote). To pass, submit nothing; there is no decline endpoint. The auction lives about 10 seconds — see [Early cashout](/guides/early-cashout).

<Warning>
  This is a distinct event name from `quote_request`, with a different payload — it carries `cashout_request_id` (not `id`) and the on-chain `position_id`. Branch on the SSE `event:` field (or register a dedicated `cashout_request` listener) to tell a cashout auction apart from a forward quote request.
</Warning>

```json theme={null}
{
  "cashout_request_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
  "position_id": "9f86d08818844f86d08818844f86d088",
  "legs": [
    {
      "id": "8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6",
      "event_ticker": "KXBTC-26JUN01",
      "market_ticker": "KXBTC-26JUN01-T72500",
      "side": "yes",
      "venue": "kalshi",
      "resolution": "unresolved"
    }
  ],
  "user_stake": 24.75,
  "mm_risk": 75,
  "total_payout": 100,
  "payout_odds": 4.0,
  "version": 1,
  "expires_at": "2026-06-01T18:46:00.000Z"
}
```

<ResponseField name="cashout_request_id" type="string" required>
  The auction id. Pass it as `{id}` when you
  [bid](/api-reference/quote-service-mm/cashout-quote). Note the name: this payload has **no** `id`
  field, unlike `quote_request`.
</ResponseField>

<ResponseField name="position_id" type="string" required>
  The on-chain position id (32 hex chars, no `0x`). Use it to look the position up in your own book
  and decide whether you are its counterparty — that determines your economics.
</ResponseField>

<ResponseField name="legs" type="array" required>
  The position's legs. Each carries `id`, `event_ticker`, `market_ticker`, `side` (`yes` / `no`),
  `venue`, and `resolution`.

  `resolution` is one of `unresolved`, `won`, `lost`, `void`, and it is your only protection against
  pricing an already-decided parlay. **`void` overrides the others**: any void leg cancels the whole
  position at settlement and refunds the stake, so the position is worth roughly `user_stake` even
  when a sibling leg reads `lost`. Absent a void leg, one `lost` leg makes the parlay worthless.
</ResponseField>

<ResponseField name="user_stake" type="number" required>
  Decimal USDC. The net stake the counterparty underwrites (`bet_amount − taker_fee`) — the same
  quantity as on `quote_request`, and the reference point for the exit price: a bid above it means
  the user leaves in profit, below it at a loss.
</ResponseField>

<ResponseField name="mm_risk" type="number" required>
  Decimal USDC. The counterparty's locked risk on the position. Pricing context only — do **not**
  compute the counterparty bid ceiling from it. The server derives that ceiling from
  `total_payout − user_stake`; the two are stored separately and can disagree in the last decimal.
</ResponseField>

<ResponseField name="total_payout" type="number" required>
  Decimal USDC. What the position pays if every leg wins, and the hard ceiling on any bid:
  `price_micro <= round(total_payout * 1e6)`, or the bid is rejected `price_above_max_payout`.
</ResponseField>

<ResponseField name="payout_odds" type="number" required>
  The accepted quote's payout multiplier — pricing context, **not** an amount and never a bid term.
  For auctions on positions created before this field was carried it is reconstructed from
  `total_payout / user_stake` and can be off in the 5th or 6th decimal, so do not reconcile against
  it exactly.
</ResponseField>

<ResponseField name="version" type="integer" required>
  The auction's version, `1` today. A cash-out auction is never re-versioned mid-flight the way a
  forward quote request can be, so there is no `cashout_request:updated` event to handle.
</ResponseField>

<ResponseField name="expires_at" type="string" required>
  ISO 8601 close time of the **auction**, about 10 s out. Bid before it. This is not the position's
  market-end horizon, which is not on this payload — so you cannot pre-filter for the near-close
  cutoff, and a bid on a position close to its market end is rejected `MARKET_NEAR_CLOSE`. Treat
  that as an ordinary lost auction.
</ResponseField>

Every field above is always present. For the full auction mechanics, see
[Early cashout](/guides/early-cashout).

## Reconnection

Every SSE event includes an `id:` field. Persist this value. On reconnect, pass it as the `last_event_id` query parameter to replay any events you missed instead of receiving the full snapshot again.

```
GET /v1/mm/quote-requests/stream?last_event_id=evt_abc123
```

The taker-fee rate is read once per connection (server-side), so `user_stake` on a long-lived stream reflects the rate in effect when you connected. If the fee rate changes, reconnect to pick up `user_stake` values computed at the new rate.

## Notes

* **Net stake.** `user_stake` is the net stake the MM underwrites: `user_stake = bet_amount − taker_fee`, where `taker_fee = floor(bet_amount_micro × taker_fee_bps / 10000)` is computed in integer microUSDC (1 USDC = 1e6 micro) — e.g. a 25.00 bet at 100 bps gives a 0.25 fee and `user_stake` 24.75. Price `payout_odds` and size collateral against `user_stake`, **not** `bet_amount`. `bet_amount` stays the user's gross wager but is not the quoting base when the taker fee is on (`taker_fee_bps > 0`); when the fee is off, `user_stake == bet_amount`.
* The stream automatically excludes quote requests created by your own user account.
* A version change arrives as `quote_request:updated` (a new request arrives as `quote_request`). Your prior version quote is invalidated automatically — it leaves the book immediately and never blocks a new quote. Re-price and [submit](/api-reference/quote-service-mm/submit-quote) a new quote with the updated `version` and `request_hash`; you don't need to withdraw the old one.

## Errors

| Status | Code           | Description                 |
| ------ | -------------- | --------------------------- |
| 401    | `UNAUTHORIZED` | Missing or invalid API key. |

<RequestExample>
  ```bash curl theme={null}
  curl -N https://api.totalis.trade/v1/mm/quote-requests/stream \
    -H "X-API-Key: $API_KEY" \
    -H "Accept: text/event-stream"
  ```

  ```javascript JavaScript theme={null}
  // Native EventSource can't send an X-API-Key header — read the SSE stream
  // with fetch instead (works in browsers and Node 18+).
  const res = await fetch('https://api.totalis.trade/v1/mm/quote-requests/stream', {
    headers: { 'X-API-Key': API_KEY, Accept: 'text/event-stream' },
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let sep;
    while ((sep = buffer.indexOf('\n\n')) !== -1) {
      const frame = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      const event = frame.match(/^event: (.+)$/m)?.[1];
      const data = frame.match(/^data: (.+)$/m)?.[1];
      if (!event || !data) continue;
      const payload = JSON.parse(data);

      switch (event) {
        case 'connected':
          console.log('Connected as', payload.mm_id);
          break;

        case 'quote_request':
          // payload.bet_amount is the gross wager; price + size against net user_stake
          console.log('New request:', payload.id, payload.user_stake ?? payload.bet_amount, 'USDC');
          break;

        case 'quote_request:updated':
          // Your prior-version quote is invalidated; submit a new one
          console.log('Request updated to version', payload.version, '— re-price:', payload.id);
          break;

        case 'quote_request_expired': {
          // reason: 'committed' | 'cancelled' | 'expired'; won: true only if you were selected.
          // won may be absent on a committed close (indeterminate) — treat as unknown, not a loss.
          const { id, reason, won } = payload;
          const outcome = reason === 'committed'
            ? (won === undefined ? 'unknown' : won ? 'won' : 'lost (out-bid)')
            : reason;
          console.log('Request closed:', id, outcome);
          break;
        }

        case 'cashout_request':
          // Distinct from quote_request: the key is payload.cashout_request_id (NOT payload.id).
          // Broadcast to every MM, ~10s to bid. Re-value the position and bid ONE all-in
          // price_micro (what the user receives) on PUT /v1/mm/cashout-requests/{id}/quote.
          console.log('Cashout auction:', payload.cashout_request_id, 'position', payload.position_id);
          break;
      }
    }
  }
  ```

  ```python Python theme={null}
  import requests
  import json

  url = 'https://api.totalis.trade/v1/mm/quote-requests/stream'
  headers = {
      'X-API-Key': API_KEY,
      'Accept': 'text/event-stream',
  }

  with requests.get(url, headers=headers, stream=True) as resp:
      event_type = None
      for line in resp.iter_lines(decode_unicode=True):
          if line.startswith('event: '):
              event_type = line[7:]
          elif line.startswith('data: '):
              payload = json.loads(line[6:])
              if event_type == 'quote_request':
                  print(f"New request: {payload['id']}")
                  # Price the legs and submit a quote
              elif event_type == 'quote_request:updated':
                  print(f"Request {payload['id']} updated to v{payload['version']} — re-price")
                  # Prior-version quote invalidated; submit a new one
              elif event_type == 'quote_request_expired':
                  # reason: 'committed' | 'cancelled' | 'expired'; won True only if you were selected.
                  # won may be absent on a committed close (indeterminate) — treat as unknown, not a loss.
                  reason = payload.get('reason', 'expired')
                  won = payload.get('won')
                  if reason == 'committed':
                      outcome = 'unknown' if won is None else ('won' if won else 'lost (out-bid)')
                  else:
                      outcome = reason
                  print(f"Request closed: {payload['id']} — {outcome}")
              elif event_type == 'cashout_request':
                  # Distinct from quote_request: the key is cashout_request_id (NOT id).
                  # Broadcast to every MM, ~10s to bid. Bid ONE all-in price_micro (what the
                  # user receives) on PUT /v1/mm/cashout-requests/{id}/quote.
                  print(f"Cashout auction: {payload['cashout_request_id']} position {payload['position_id']}")
              event_type = None
  ```
</RequestExample>

<ResponseExample>
  ```text SSE stream theme={null}
  event: connected
  data: {"mm_id":"mm-uuid-1234"}

  event: snapshot_begin
  data: {"page_size":50}

  event: quote_request
  data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","user_id":"did:privy:cm3abc...","input_legs":[{"market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"legs":[{"id":"8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6","event_ticker":"KXBTC-26JUN01","market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"bet_amount":25,"user_stake":24.75,"implied_probability":0,"version":1,"request_hash":"sha256:9f86d08...","expires_at":"2026-06-01T18:45:30.000Z"}

  event: snapshot_complete
  data: {"pages":1,"total":3}

  event: quote_request
  data: {"id":"f7e8d9c0-1234-5678-abcd-ef0123456789","user_id":"did:privy:cm9xyz...","input_legs":[{"market_ticker":"KXETH-26JUN01-T3000","side":"no","venue":"kalshi"}],"legs":[{"id":"d4e5f6a7-8901-23bc-def4-567890abcdef","event_ticker":"KXETH-26JUN01","market_ticker":"KXETH-26JUN01-T3000","side":"no","venue":"kalshi"}],"bet_amount":10,"user_stake":9.90,"implied_probability":0,"version":1,"request_hash":"sha256:b5c6d7e...","expires_at":"2026-06-01T18:50:00.000Z"}

  event: quote_request:updated
  data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","user_id":"did:privy:cm3abc...","input_legs":[{"market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"legs":[{"id":"8ccf2c5d-3a61-4707-93f2-b6f0a0f0c0c6","event_ticker":"KXBTC-26JUN01","market_ticker":"KXBTC-26JUN01-T72500","side":"yes","venue":"kalshi"}],"bet_amount":50,"user_stake":49.50,"implied_probability":0,"version":2,"request_hash":"sha256:3b1f9c2...","expires_at":"2026-06-01T18:46:10.000Z"}

  event: quote_request_expired
  data: {"id":"1a6d1f06-9d4f-47cb-994b-3bdfbbef7e40","reason":"committed","won":false}
  ```
</ResponseExample>
