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

# Markets Stream

> Stream real-time Polymarket market rows with filter or ids mode and configurable cadence.

<Note>
  **Room ID:** `polymarket_markets_stream` \
  **Endpoint:** `wss://api.struct.to/ws` \
  **Rate:** 0.025 credits per message
</Note>

Low-latency push feed of market rows — same shape `GET /polymarket/market` returns, including per-outcome price with `latest_block` + `latest_confirmed_at` watermarks. The server maintains an in-memory cache of **open** markets only, refreshed via a slow full poll plus a fast 500ms newest-first poll, and merged live from the `prediction_condition_metrics` and `prediction_trades` Kafka streams with per-timeframe block/timestamp ordering.

No initial snapshot is pushed on subscribe. Clients seed from `GET /polymarket/market` and then apply deltas from this stream.

<Tip>
  **Related guides:** [Building a live trending feed](/guides/real-time-trending-events-and-markets) seeds from the markets list and stays warm via this stream, and [Ranking markets by CLOB rewards](/guides/clob-liquidity-markets-dashboard) tracks live reward changes through it.
</Tip>

## Subscription model

Each client has up to **8 active slots per room** (4 cadences × 2 modes). Re-subscribing to the same `(interval_ms, mode)` pair replaces the previous subscription. Unsubscribe one slot with `action: "unsubscribe"` plus `interval_ms` and `mode`, or clear everything with `action: "unsubscribe_all"`.

* **Cadence (`interval_ms`):** `500`, `1000`, `3000`, or `10000`.
* **Filter mode:** same validation as the REST list endpoint (timeframe, search length, list caps). `search` is a case-insensitive substring match on `title`. No sort / limit — you get every matching row that changed.
* **Ids mode:** any combination of `condition_ids`, `market_slugs`, and `event_slugs` (matches all child markets of those events). Max **500 ids total** per subscription.

Updates fire only when a cache row is dirtied by (a) a fresh Kafka metric snapshot with `latest_block >= cached`, (b) a confirmed trade with `block >= cached`, (c) a slow-poll field diff, or (d) the fast newest-first poll discovering a brand-new market. Quiet markets produce zero messages.

Each outcome in `outcomes[]` carries `latest_block` and `latest_confirmed_at` (Unix seconds) — the block/ts of the most recent price write from `prediction_position_metrics`. Consumers can use these to reject out-of-order price merges locally.

## Subscribe

### Message fields

| Field           | Type                                                | Required                    | Description                                             |
| --------------- | --------------------------------------------------- | --------------------------- | ------------------------------------------------------- |
| `action`        | `"subscribe" \| "unsubscribe" \| "unsubscribe_all"` | Yes                         | Slot lifecycle action.                                  |
| `interval_ms`   | `500 \| 1000 \| 3000 \| 10000`                      | For subscribe / unsubscribe | Flush cadence. Defaults to `1000` if omitted.           |
| `mode`          | `"filter" \| "ids"`                                 | No                          | Subscription mode. Defaults to `filter`.                |
| `filter`        | `MarketsStreamFilter`                               | `mode=filter` only          | Filter body; all fields optional.                       |
| `condition_ids` | `string[]`                                          | `mode=ids` only             | 0x-prefixed lowercase 32-byte hex.                      |
| `market_slugs`  | `string[]`                                          | `mode=ids` only             | Market slug strings.                                    |
| `event_slugs`   | `string[]`                                          | `mode=ids` only             | Event slugs — matches every child market of each event. |

### Filter fields (`mode=filter`)

Supports the same filters as the REST markets list: `search`, `categories`, `exclude_categories`, `tags`, `exclude_tags`, `min_volume` / `max_volume`, `min_txns` / `max_txns`, `min_unique_traders` / `max_unique_traders`, `min_liquidity` / `max_liquidity`, `min_holders` / `max_holders`, `start_time` / `end_time`, `has_rewards`, and `timeframe` (`1m`, `5m`, `30m`, `1h`, `6h`, `24h`, `7d`, `30d`).

`status` is **not** accepted — the cache only holds open markets.

### Example — filter mode

```json theme={null}
{
  "type": "join_room",
  "payload": {
    "room_id": "polymarket_markets_stream"
  }
}
```

```json theme={null}
{
  "type": "room_message",
  "payload": {
    "room_id": "polymarket_markets_stream",
    "message": {
      "action": "subscribe",
      "interval_ms": 1000,
      "mode": "filter",
      "filter": {
        "search": "bitcoin",
        "categories": ["crypto"],
        "min_volume": 50000,
        "timeframe": "24h",
        "has_rewards": true
      }
    }
  }
}
```

### Example — ids mode

```json theme={null}
{
  "type": "room_message",
  "payload": {
    "room_id": "polymarket_markets_stream",
    "message": {
      "action": "subscribe",
      "interval_ms": 500,
      "mode": "ids",
      "condition_ids": ["0xabc123..."],
      "market_slugs": ["will-bitcoin-hit-100k"],
      "event_slugs": ["bitcoin-price-markets"]
    }
  }
}
```

### Unsubscribe one slot

```json theme={null}
{
  "type": "room_message",
  "payload": {
    "room_id": "polymarket_markets_stream",
    "message": {
      "action": "unsubscribe",
      "interval_ms": 500,
      "mode": "ids"
    }
  }
}
```

### Response

```json theme={null}
{
  "type": "markets_stream_subscribe_response",
  "room_id": "polymarket_markets_stream",
  "data": {
    "mode": "ids",
    "interval_ms": 500,
    "condition_ids": ["0xabc123..."],
    "market_slugs": ["will-bitcoin-hit-100k"],
    "event_slugs": ["bitcoin-price-markets"],
    "rejected": [],
    "error": null
  }
}
```

## Events

### `markets_stream_update`

Server-pushed event fired only for rows that changed AND matched this subscription since the last flush tick. `data` contains **full** market rows — not deltas — so clients should merge by `condition_id`. Each outcome in `data[i].outcomes` carries `latest_block` + `latest_confirmed_at` price-update watermarks.

#### Envelope

| Field         | Type                           | Description                                               |
| ------------- | ------------------------------ | --------------------------------------------------------- |
| `type`        | `"markets_stream_update"`      | Envelope discriminator.                                   |
| `room_id`     | `"polymarket_markets_stream"`  | Room identifier.                                          |
| `mode`        | `"filter" \| "ids"`            | The mode this subscription was created with.              |
| `interval_ms` | `500 \| 1000 \| 3000 \| 10000` | The cadence slot this event is flushed under.             |
| `data`        | `MarketResponse[]`             | Full market rows, same shape as `GET /polymarket/market`. |

#### `MarketResponse`

| Field                   | Type                                        | Description                                                             |
| ----------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
| `condition_id`          | `string`                                    | 0x-prefixed condition ID. **Required.**                                 |
| `id`                    | `string \| null`                            | Market ID.                                                              |
| `market_slug`           | `string \| null`                            | URL-safe market slug.                                                   |
| `question`              | `string \| null`                            | Market question.                                                        |
| `title`                 | `string \| null`                            | Market title.                                                           |
| `description`           | `string \| null`                            | Long description.                                                       |
| `image_url`             | `string \| null`                            | CDN image URL.                                                          |
| `oracle`                | `string \| null`                            | Oracle contract address.                                                |
| `status`                | `string`                                    | Market status. **Required** (cache holds only open markets).            |
| `created_time`          | `integer \| null`                           | Unix seconds.                                                           |
| `start_time`            | `integer \| null`                           | Unix seconds.                                                           |
| `game_start_time`       | `integer \| null`                           | Unix seconds (sports markets).                                          |
| `closed_time`           | `integer \| null`                           | Unix seconds.                                                           |
| `end_time`              | `integer \| null`                           | Unix seconds.                                                           |
| `accepting_orders`      | `boolean \| null`                           | Whether CLOB is accepting new orders.                                   |
| `uma_resolution_status` | `string \| null`                            | UMA oracle resolution status.                                           |
| `is_neg_risk`           | `boolean \| null`                           | Whether this market uses the neg-risk exchange.                         |
| `market_maker_address`  | `string \| null`                            | Market maker contract.                                                  |
| `creator`               | `string \| null`                            | Wallet address of creator.                                              |
| `category`              | `string \| null`                            | Category label.                                                         |
| `volume_usd`            | `number \| null`                            | Lifetime USD volume.                                                    |
| `liquidity_usd`         | `number \| null`                            | Current USD liquidity.                                                  |
| `highest_probability`   | `number \| null`                            | Highest outcome probability (0 – 1).                                    |
| `total_holders`         | `integer \| null`                           | Unique holder count.                                                    |
| `total_daily_rate`      | `number \| null`                            | Combined daily reward rate across sponsors.                             |
| `winning_outcome`       | `MarketOutcome \| null`                     | Resolved winning outcome, when applicable.                              |
| `outcomes`              | `MarketOutcome[]`                           | Market outcomes.                                                        |
| `clob_rewards`          | `ClobReward[]`                              | Active reward configs.                                                  |
| `tags`                  | `string[]`                                  | Tag labels attached to the market.                                      |
| `event_slug`            | `string \| null`                            | Parent event slug.                                                      |
| `resolution_source`     | `string \| null`                            | Resolution source URL.                                                  |
| `metrics`               | `Record<Timeframe, SimpleTimeframeMetrics>` | Keyed by timeframe (`1m`, `5m`, `30m`, `1h`, `6h`, `24h`, `7d`, `30d`). |
| `relevance_score`       | `number \| null`                            | Search relevance score, when applicable.                                |

#### `MarketOutcome`

| Field                 | Type              | Description                                             |
| --------------------- | ----------------- | ------------------------------------------------------- |
| `name`                | `string`          | Outcome label (e.g. `"Yes"`).                           |
| `price`               | `number \| null`  | Latest traded price of this outcome token (0 – 1).      |
| `position_id`         | `string \| null`  | ERC-1155 outcome token ID (decimal string).             |
| `outcome_index`       | `integer \| null` | 0-indexed outcome position.                             |
| `latest_block`        | `integer \| null` | Block of the most recent price update for this outcome. |
| `latest_confirmed_at` | `integer \| null` | Unix seconds of the most recent price update.           |

#### `SimpleTimeframeMetrics`

| Field            | Type      | Description                            |
| ---------------- | --------- | -------------------------------------- |
| `volume`         | `number`  | USD volume within the window.          |
| `fees`           | `number`  | USD fees within the window.            |
| `txns`           | `integer` | Trade count within the window.         |
| `unique_traders` | `integer` | Unique wallet count within the window. |

#### `ClobReward`

| Field                  | Type              | Description                                    |
| ---------------------- | ----------------- | ---------------------------------------------- |
| `id`                   | `string`          | Reward config ID. **Required.**                |
| `condition_id`         | `string`          | Market condition ID. **Required.**             |
| `asset_address`        | `string \| null`  | Reward token contract.                         |
| `rewards_amount`       | `number \| null`  | Total rewards remaining.                       |
| `rewards_daily_rate`   | `number \| null`  | Rewards emitted per day.                       |
| `start_date`           | `string \| null`  | ISO date the reward starts emitting.           |
| `end_date`             | `string \| null`  | ISO date the reward stops emitting.            |
| `rewards_max_spread`   | `number \| null`  | Max spread eligible for rewards (probability). |
| `rewards_min_size`     | `number \| null`  | Min order size eligible for rewards (USD).     |
| `native_daily_rate`    | `number \| null`  | Native (Polymarket) daily rate.                |
| `sponsored_daily_rate` | `number \| null`  | Sponsored daily rate.                          |
| `total_daily_rate`     | `number \| null`  | Combined daily rate.                           |
| `sponsors_count`       | `integer \| null` | Number of active sponsors.                     |

#### Example

```json theme={null}
{
  "type": "markets_stream_update",
  "room_id": "polymarket_markets_stream",
  "mode": "ids",
  "interval_ms": 500,
  "data": [
    {
      "condition_id": "0xabc123...",
      "id": "m_1",
      "market_slug": "will-bitcoin-hit-100k",
      "question": "Will Bitcoin hit $100k by Dec 31, 2026?",
      "title": "Bitcoin $100k by EOY 2026",
      "description": "Resolves YES if BTC closes above $100,000 on any day before Dec 31, 2026.",
      "image_url": "https://cdn.struct.to/markets/btc-100k.png",
      "oracle": "0xoracle...",
      "status": "open",
      "created_time": 1743400000,
      "start_time": 1743500000,
      "game_start_time": null,
      "closed_time": null,
      "end_time": 1798761600,
      "accepting_orders": true,
      "uma_resolution_status": null,
      "is_neg_risk": false,
      "market_maker_address": "0xmm...",
      "creator": "0xcreator...",
      "category": "crypto",
      "volume_usd": 125000.5,
      "liquidity_usd": 42000.0,
      "highest_probability": 0.65,
      "total_holders": 312,
      "total_daily_rate": 150.0,
      "winning_outcome": null,
      "outcomes": [
        {
          "name": "Yes",
          "price": 0.65,
          "position_id": "12345678901234567",
          "outcome_index": 0,
          "latest_block": 65000000,
          "latest_confirmed_at": 1743500000
        },
        {
          "name": "No",
          "price": 0.35,
          "position_id": "98765432109876543",
          "outcome_index": 1,
          "latest_block": 65000000,
          "latest_confirmed_at": 1743500000
        }
      ],
      "clob_rewards": [
        {
          "id": "1",
          "condition_id": "0xabc123...",
          "asset_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
          "rewards_amount": 18250.0,
          "rewards_daily_rate": 50.0,
          "start_date": "2026-01-01",
          "end_date": "2026-12-31",
          "rewards_max_spread": 0.04,
          "rewards_min_size": 20.0,
          "native_daily_rate": 100.0,
          "sponsored_daily_rate": 50.0,
          "total_daily_rate": 150.0,
          "sponsors_count": 1
        }
      ],
      "tags": ["crypto", "bitcoin"],
      "event_slug": "bitcoin-price-markets",
      "resolution_source": "https://example.com/btc-resolution",
      "metrics": {
        "24h": { "volume": 3200.0, "fees": 6.4, "txns": 42, "unique_traders": 18 },
        "7d": { "volume": 22000.0, "fees": 44.0, "txns": 310, "unique_traders": 121 }
      },
      "relevance_score": null
    }
  ]
}
```
