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

# WebSocket — push actionable arb signals

> Subscribe to /v1/arb/ws and receive logical + endgame opportunities the moment the engine detects them. No polling, no quota burn.

A persistent WebSocket connection that pushes every new **`tier=logical`**
and **`tier=endgame`** opportunity at the exact moment the arb engine
detects it — typically within 100ms of the underlying price/book
event that triggered it.

<Note>
  **Plus tier and above only.** Free and Pro stay on REST polling
  (`GET /v1/arb/live`). This is the headline Plus differentiator —
  no quota burn, no missed signals.
</Note>

## Endpoint

```
wss://api.polyquantlab.com/v1/arb/ws
```

### Query parameters

| Param     | Required | Description                                                  |
| --------- | -------- | ------------------------------------------------------------ |
| `api_key` | yes      | Your PolyQuantLab API key (Plus or above)                    |
| `ticker`  | no       | Comma-separated subset of `BTC`, `ETH`, `SOL`. Omit for all. |

WebSockets can't carry custom `Authorization` headers reliably across
proxies, so auth is via query param. Use HTTPS-equivalent transport
security (this URL is TLS — never `ws://`).

## Message types

Every message is a single JSON object with a `type` discriminator.

### `ready` (sent once, on connect)

```json theme={null}
{
  "type": "ready",
  "ticker_filter": ["BTC", "ETH"],
  "tier": "plus",
  "channel": "pql:arb:actionable"
}
```

### `opportunity` (zero or many — pushed as they fire)

Same shape as items in the `opportunities` array from
`GET /v1/arb/live`, with a `type: "opportunity"` field added. The
`tier` field is the actionable filter — only `"logical"` or
`"endgame"` are pushed; `"stable"` / `"stale"` rows are kept off the
push channel by design (they're not tradable signals).

```json theme={null}
{
  "type": "opportunity",
  "as_of": "2026-06-06T04:00:02.118+00:00",
  "market_id": "0x4a8f31...c2",
  "ticker": "BTC",
  "event_type": "5m",
  "question": "Will Bitcoin price be above $109,500 at 04:05 UTC?",
  "tier": "endgame",
  "direction": "BUY_YES",
  "fill_price": 0.82,
  "expected_pnl_per_share": 0.167,
  "...": "(remaining fields identical to /v1/arb/live)"
}
```

### `ping` (every 25s if no opportunities)

Sent so a long-idle connection doesn't get killed by intermediaries.
Clients can ignore the body — receiving any message is enough proof
of liveness. No client pong required.

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

## Minimal Python client

```python theme={null}
import os
import json
import asyncio
import websockets

URL = (
    "wss://api.polyquantlab.com/v1/arb/ws"
    f"?api_key={os.environ['POLYQUANTLAB_API_KEY']}"
)

async def main():
    async with websockets.connect(URL, ping_interval=20, ping_timeout=30) as ws:
        async for raw in ws:
            msg = json.loads(raw)
            t = msg.get("type")

            if t == "ready":
                print(f"subscribed: {msg}")
            elif t == "opportunity":
                handle(msg)
            elif t == "ping":
                pass  # ignore; just liveness
            else:
                print(f"unknown message type: {msg}")

def handle(opp):
    print(
        f"[{opp['tier'].upper():7s}] {opp['ticker']} {opp['event_type']} "
        f"size_at=${opp['fill_price']:.3f} "
        f"edge=${opp['expected_pnl_per_share']:.4f}/sh "
        f"τ={opp['seconds_to_resolution']}s"
    )
    # … submit the order via your Polymarket SDK here …

asyncio.run(main())
```

## Reconnect handling

Networks die. Add a backoff loop so a transient drop doesn't lose
you signals for the rest of the day:

```python theme={null}
import asyncio, websockets

async def run_forever():
    backoff = 1.0
    while True:
        try:
            async with websockets.connect(URL) as ws:
                backoff = 1.0  # success — reset
                async for raw in ws:
                    handle(json.loads(raw))
        except (websockets.ConnectionClosed, OSError) as e:
            print(f"disconnected: {e}; reconnect in {backoff:.1f}s")
            await asyncio.sleep(backoff)
            backoff = min(backoff * 2, 30.0)
```

## What you don't get

* **No historical replay.** Connect = subscribe from now. If you
  disconnect for 5 minutes and reconnect, any signals fired during
  the gap are lost. Use `GET /v1/arb/audit?window=24h` to backfill
  if needed for analysis (note: settled-only, not real-time).
* **No order submission.** PolyQuantLab is a research / signal API;
  execution stays on Polymarket. Pipe the message into your
  py-clob-client loop — see [Auto-trade logical arbs](/docs/guides/auto-trade-logical-arbs).
* **No `stable` / `stale` rows.** Those have \~0 expected edge after
  fees; surfacing them on the push channel would just waste your
  socket bandwidth and tempt bad trades. They remain available via
  REST `GET /v1/arb/live`.

## Comparison vs REST polling

|                              | REST `GET /v1/arb/live`           | WS `/v1/arb/ws`  |
| ---------------------------- | --------------------------------- | ---------------- |
| Tier                         | Free / Pro / Plus                 | Plus / above     |
| Latency from detect → client | poll interval (3-10 s)            | \~50-200 ms      |
| Rate-limit consumption       | yes — counts toward `req/s`       | none             |
| Best for                     | dashboard polling, batch analysis | auto-trader bots |
| Reconnect handling           | not needed                        | you handle it    |
| `tier=stable` / `stale` rows | included                          | excluded         |
