Skip to main content
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.
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.

Endpoint

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

Query parameters

ParamRequiredDescription
api_keyyesYour PolyQuantLab API key (Plus or above)
tickernoComma-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)

{
  "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).
{
  "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.
{ "type": "ping" }

Minimal Python client

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:
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.
  • 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/liveWS /v1/arb/ws
TierFree / Pro / PlusPlus / above
Latency from detect → clientpoll interval (3-10 s)~50-200 ms
Rate-limit consumptionyes — counts toward req/snone
Best fordashboard polling, batch analysisauto-trader bots
Reconnect handlingnot neededyou handle it
tier=stable / stale rowsincludedexcluded