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

# WebSocket Overview

> Real-time Polymarket trades, 3-5 seconds before on-chain confirmation.

polynode's WebSocket is the fastest way to get Polymarket trade data. Every fill is delivered **3-5 seconds before on-chain confirmation** (1-2 blocks early). Subscribe to `fills` to get one clean event per trade, or `settlements` for the full transaction lifecycle.

<CardGroup cols={3}>
  <Card title="Fills" icon="bolt" href="/websocket/subscribing#fills">
    Individual trades, one per message. Pre-confirmation, flat format, no array parsing. The fastest way to track trades.
  </Card>

  <Card title="Settlements" icon="file-lines" href="/websocket/subscribing#settlements">
    Full transaction bundles with all fills in a `trades[]` array plus status lifecycle tracking.
  </Card>

  <Card title="Trades" icon="arrow-right-arrow-left" href="/websocket/subscribing#trades">
    Confirmed on-chain fills. Same data as settlements but after block confirmation with exact receipt values.
  </Card>

  <Card title="Combos" icon="layer-group" href="/websocket/combos">
    Polymarket combo executions and confirmations with enriched leg metadata.
  </Card>
</CardGroup>

<CardGroup cols={2}>
  <Card title="Up to 5 second edge" icon="clock">
    Settlements and fills detected before on-chain confirmation. Typically 3-5 seconds (1-2 blocks) early.
  </Card>

  <Card title="Filtered subscriptions" icon="filter">
    Subscribe by wallet, token, market slug, side, size, or event type. Only receive what you need.
  </Card>

  <Card title="Enriched events" icon="layer-group">
    Every event includes market title, outcome name, slug, and image. No secondary lookups needed.
  </Card>

  <Card title="Crypto price feeds" icon="chart-line" href="/crypto/overview">
    Real-time prices for BTC, ETH, SOL, BNB, XRP, DOGE, and HYPE (\~1/second each). Subscribe with `"type": "chainlink"` on the same connection.
  </Card>

  <Card title="Oracle resolution stream" icon="scale-balanced">
    UMA Optimistic Oracle events: market resolutions, disputes, proposals, and admin actions. Subscribe with `"type": "oracle"` to track the full resolution lifecycle.
  </Card>
</CardGroup>

<Tip>
  **Save \~60% bandwidth with compression.** Add `&compress=zlib` to your connection URL and decompress binary frames with standard `inflateRaw`. Zero latency impact, recommended for production. [See compression docs →](/websocket/compression)
</Tip>

## Quick start

<Steps>
  <Step title="Get an API key">
    ```bash theme={null}
    curl -s -X POST https://api.polynode.dev/v1/keys \
      -H "Content-Type: application/json" \
      -d '{"name": "my-app"}'
    ```

    Save the `pn_live_...` key from the response.
  </Step>

  <Step title="Stream trades (30 seconds)">
    Save this as `stream.js` and run with `API_KEY=pn_live_... node stream.js`:

    <CodeGroup>
      ```javascript Node.js theme={null}
      const WebSocket = require("ws");

      const API_KEY = process.env.API_KEY || "YOUR_API_KEY";
      const DURATION = 30000;

      const ws = new WebSocket("wss://ws.polynode.dev/ws?key=" + API_KEY);

      ws.on("open", () => {
        ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
        console.log("Streaming trades for " + (DURATION / 1000) + "s...\n");
      });

      let count = 0;
      ws.on("message", (raw) => {
        const msg = JSON.parse(raw);
        if (msg.type !== "event") return;

        count++;
        const d = msg.data;
        console.log({
          outcome: d.token_label,
          side: d.side,
          price: d.price,
          shares: d.shares_normalized,
          user: d.user,
          market: d.title
        });
      });

      ws.on("close", () => console.log("\nDone. " + count + " trades."));
      setTimeout(() => ws.close(), DURATION);
      ```

      ```python Python theme={null}
      import asyncio, json, websockets, time

      API_KEY = "YOUR_API_KEY"
      DURATION = 30

      async def stream():
          url = f"wss://ws.polynode.dev/ws?key={API_KEY}"
          async with websockets.connect(url) as ws:
              await ws.send(json.dumps({"action": "subscribe", "type": "fills"}))
              print(f"Streaming trades for {DURATION}s...\n")

              count = 0
              end = time.time() + DURATION
              async for message in ws:
                  if time.time() > end:
                      break
                  msg = json.loads(message)
                  if msg.get("type") != "event":
                      continue
                  count += 1
                  d = msg["data"]
                  print(f"{d['token_label']:5} {d['side']:4} {d['shares_normalized']:>8.2f} shares @ {d['price']:<8} | {d['user'][:12]}... | {d['title'][:50]}")
              print(f"\nDone. {count} trades.")

      asyncio.run(stream())
      ```
    </CodeGroup>

    You'll see individual trades streaming in, one per line. Each event is a single fill with clear fields for outcome, side, price, shares, and the wallet involved.
  </Step>

  <Step title="What you'll see">
    Every message is one flat event with everything you need. Here's a real captured fill:

    ```json theme={null}
    {
      "type": "event",
      "data": {
        "side": "BUY",
        "price": 0.85,
        "shares_normalized": 1.67,
        "token_label": "Up",
        "title": "Bitcoin Up or Down - April 30, 2:50PM-2:55PM ET",
        "user": "0xe54bbd9dcef861af50bf2c4dcc354a87e87bff96",
        "taker": "0x70412cbf28288d47ece30002be45efff5d2d5c73",
        "order_hash": "0x40169bc2d2af4dded1fbe4d2655838f2b79ba4a9aabc548b8fec3649e66ed9f4",
        "tx_hash": "0xef710f81783210de00d90fd477d3c190c3acd657cf7dd5dae31188c53ded2e4e",
        "market_slug": "btc-updown-5m-1777575000",
        "condition_id": "0x6fe03c3c781f62d456518a3a6b2b95c0d0bee427243bcef06f5b37b3f1c395b7",
        "token_id": "28011179493432352359106676407817901302975340324989108630179591254583544078282",
        "shares": 1670000,
        "timestamp": 1777575164,
        "block_number": null,
        "log_index": null
      }
    }
    ```

    `block_number: null` means this was detected **before** the block confirmed. Pre-confirmation delivery, 3-5 seconds ahead of on-chain settlement. One event per fill, no arrays to iterate, all fields included.
  </Step>
</Steps>

<Info>
  **`fills` vs `settlements`** — both stream the same trades at the same speed. `fills` gives you one flat event per trade (simplest). `settlements` gives you the full transaction bundle with a `trades[]` array and lifecycle tracking via `status_update` events. Start with `fills`, move to `settlements` when you need the full lifecycle. See [Subscriptions](/websocket/subscribing) for all options.
</Info>

<Accordion title="Settlement-based quick start (full lifecycle)">
  If you need the full transaction format with nested fills and status lifecycle, subscribe to `settlements` instead:

  ```javascript theme={null}
  const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");

  ws.onopen = () => {
    ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === "settlement") {
      const d = msg.data;
      console.log(`${d.taker_side} ${d.taker_size} @ ${d.taker_price} | ${d.market_title}`);
      for (const fill of d.trades) {
        console.log(`  ${fill.side} ${fill.size} @ ${fill.price} | maker: ${fill.maker}`);
      }
    }
  };
  ```

  See [Settlement Event](/websocket/events/settlement) for the full field reference.
</Accordion>

## Connection URL

```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY
```

With compression (\~60% bandwidth savings):

```
wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY&compress=zlib
```

The API key is passed as a query parameter. Keys starting with `pn_live_` or `qm_live_` are accepted.

## Connection limits

Most apps only need one Event WebSocket connection. A single connection can carry multiple filtered subscriptions at once, including fills, settlements, trades, blocks, wallet filters, market filters, oracle events, and Chainlink price feeds.

| Tier                      | Concurrent Event WebSockets | Filter items per subscription | Session limit |
| ------------------------- | --------------------------- | ----------------------------- | ------------- |
| **Free**                  | 1                           | 10,000                        | 1 hour/day    |
| **Starter** (\$50/mo)     | 10                          | 10,000                        | Unlimited     |
| **Growth** (\$200/mo)     | 50                          | 10,000                        | Unlimited     |
| **Enterprise** (\$750/mo) | Unlimited                   | Custom                        | Unlimited     |

<Note>
  Event WebSocket limits apply to `ws.polynode.dev`. Orderbook WebSocket limits for `ob.polynode.dev` are listed separately in the [Orderbook Stream](/orderbook/overview) docs.
</Note>

## Multiple streams, one connection

PolyNode supports multiple subscription types on the same WebSocket:

| Stream           | Purpose                                                                             | Subscribe                                                                 |
| ---------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Fills**        | Individual pre-confirmation trades, one flat event per fill. Simplest format.       | `{"action":"subscribe","type":"fills"}`                                   |
| **Settlements**  | Pre-confirmation transaction bundles with all fills in a `trades[]` array (default) | `{"action":"subscribe","type":"settlements"}`                             |
| **Trades**       | Confirmed on-chain fills with exact receipt values                                  | `{"action":"subscribe","type":"trades"}`                                  |
| **Prices**       | Price-moving settlement and trade events for specific markets                       | `{"action":"subscribe","type":"prices"}`                                  |
| **Combos**       | Polymarket combo executions and confirmations with enriched legs                    | `{"action":"subscribe","type":"combos"}`                                  |
| **Blocks**       | New Polygon block notifications with Polymarket stats                               | `{"action":"subscribe","type":"blocks"}`                                  |
| **Wallets**      | All activity for specified wallets                                                  | `{"action":"subscribe","type":"wallets","filters":{"wallets":["0x..."]}}` |
| **Markets**      | All activity for specified markets                                                  | `{"action":"subscribe","type":"markets","filters":{"tokens":["..."]}}`    |
| **Deposits**     | USDC.e and Polymarket USD deposit/withdrawal activity                               | `{"action":"subscribe","type":"deposits"}`                                |
| **Large Trades** | Whale alerts (\$1K+ by default)                                                     | `{"action":"subscribe","type":"large_trades"}`                            |
| **Global**       | Full firehose of the default public event set                                       | `{"action":"subscribe","type":"global"}`                                  |
| **Oracle**       | UMA resolution lifecycle (resolutions, disputes, flags)                             | `{"action":"subscribe","type":"oracle"}`                                  |
| **Chainlink**    | Real-time crypto prices (7 feeds, \~1/sec each)                                     | `{"action":"subscribe","type":"chainlink"}`                               |

All can run simultaneously on the same connection. See [Subscriptions & Filters](/websocket/subscribing) for full details on each type, including default event types and available filters.

## Heartbeat

The server sends a heartbeat every 30 seconds:

* A WebSocket-level **Ping** frame (handled automatically by WS clients)
* A text message: `{"type": "heartbeat", "ts": 1772386305181}`

If no heartbeat arrives within \~35 seconds, the connection is dead — reconnect.

The server monitors client liveness via incoming messages and Pong frames. If no activity is received from the client within 5 minutes, the server closes the connection with a close frame explaining the reason. Standard WebSocket clients handle Pong automatically, which counts as activity.

### Application-level keepalive

Send a ping message to confirm the connection is alive:

```json theme={null}
{"action": "ping"}
```

Response:

```json theme={null}
{"type": "pong", "ts": 1774429169820}
```

Any message you send (subscribe, unsubscribe, ping) resets the server's liveness timer.

<Warning>
  **Running behind a reverse proxy (Railway, Render, Heroku, AWS ALB, etc.)?** Some cloud platform proxies intercept WebSocket Ping/Pong control frames at the proxy layer and don't forward them to your application. This means the server never receives your client's automatic Pong responses, and the connection gets dropped for inactivity.

  **The fix:** Send `{"action": "ping"}` every 30-60 seconds from your application code. These are regular text frames that pass through any proxy. This is the recommended keepalive method for any containerized or cloud-hosted deployment.

  ```python theme={null}
  # Python example — keepalive for cloud-hosted clients
  import asyncio, json, websockets

  async def stream():
      url = "wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY"
      async with websockets.connect(url, ping_interval=None, ping_timeout=None) as ws:
          await ws.send(json.dumps({"action": "subscribe", "type": "fills"}))

          async def keepalive():
              while True:
                  await asyncio.sleep(30)
                  await ws.send(json.dumps({"action": "ping"}))

          ping_task = asyncio.create_task(keepalive())
          try:
              async for message in ws:
                  msg = json.loads(message)
                  if msg["type"] in ("pong", "heartbeat"):
                      continue
                  # handle events...
          finally:
              ping_task.cancel()

  asyncio.run(stream())
  ```

  ```javascript theme={null}
  // Node.js example — keepalive for cloud-hosted clients
  const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");

  ws.onopen = () => {
    ws.send(JSON.stringify({ action: "subscribe", type: "fills" }));
    setInterval(() => ws.send(JSON.stringify({ action: "ping" })), 30000);
  };
  ```
</Warning>

## Connection lifecycle

```
Connect → wss://ws.polynode.dev/ws?key=pn_live_...
    ↓
Send → {"action": "subscribe", "type": "fills"}
    ↓
Receive ← {"type": "subscribed", "subscriber_id": "...", "subscription_id": "...:1"}
    ↓
Send → {"action": "subscribe", ...}        (additional subscriptions stack)
Receive ← {"type": "subscribed", "subscription_id": "...:2"}
    ↓
Receive ← {"type": "event", "data": {...}} (live fills)
Receive ← Ping frame                       (every 30s)
Send   → Pong frame                        (automatic)
Receive ← {"type": "heartbeat", ...}       (every 30s)
Send   → {"action": "ping"}               (optional, recommended if cloud-hosted)
Receive ← {"type": "pong", "ts": ...}     (keepalive response)
    ↓
Send → {"action": "unsubscribe", "subscription_id": "...:1"}  (remove one)
Send → {"action": "unsubscribe"}                               (remove all)
Receive ← {"type": "unsubscribed"}
```

## Error handling

Invalid messages return structured errors:

```json theme={null}
{
  "type": "error",
  "code": "invalid_json",
  "message": "Message is not valid JSON."
}
```

| Error code                 | Cause                                     |
| -------------------------- | ----------------------------------------- |
| `invalid_json`             | Message is not valid JSON                 |
| `invalid_message`          | Unknown action or missing required fields |
| `unresolved_slugs`         | Slug not found in market metadata         |
| `unresolved_condition_ids` | Condition ID not found in metadata        |

## Reconnection

PolyNode does not send a close frame before disconnecting. Implement reconnection with exponential backoff and use the `since` filter to fill any gaps:

```javascript theme={null}
let delay = 1000;
let lastEventTs = null;

function connect() {
  const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_...");
  ws.onopen = () => {
    delay = 1000;
    const filters = lastEventTs ? { since: lastEventTs } : {};
    ws.send(JSON.stringify({ action: "subscribe", type: "fills", filters }));
  };
  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.timestamp) lastEventTs = msg.timestamp;
    if (msg.type === "snapshot") {
      // Process snapshot events (gap-fill from last disconnect)
      for (const e of msg.events) {
        if (e.timestamp) lastEventTs = Math.max(lastEventTs || 0, e.timestamp);
      }
    }
  };
  ws.onclose = () => setTimeout(connect, Math.min(delay *= 2, 30000));
}
```

The `since` filter returns all events after that timestamp within your tier's lookback window (free: 30s, starter: 2min, growth/enterprise: 5min). When `since` is set, it overrides `snapshot_count`. For outages longer than your lookback window, use the REST API to backfill.
