Skip to main content

Documentation Index

Fetch the complete documentation index at: https://polynode.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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.

Fills

Individual trades, one per message. Pre-confirmation, flat format, no array parsing. The fastest way to track trades.

Settlements

Full transaction bundles with all fills in a trades[] array plus status lifecycle tracking.

Trades

Confirmed on-chain fills. Same data as settlements but after block confirmation with exact receipt values.

Up to 5 second edge

Settlements and fills detected before on-chain confirmation. Typically 3-5 seconds (1-2 blocks) early.

Filtered subscriptions

Subscribe by wallet, token, market slug, side, size, or event type. Only receive what you need.

Enriched events

Every event includes market title, outcome name, slug, and image. No secondary lookups needed.

Crypto price feeds

Real-time prices for BTC, ETH, SOL, BNB, XRP, DOGE, and HYPE (~1/second each). Subscribe with "type": "chainlink" on the same connection.

Oracle resolution stream

UMA Optimistic Oracle events: market resolutions, disputes, proposals, and admin actions. Subscribe with "type": "oracle" to track the full resolution lifecycle.
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 →

Quick start

1

Get an API key

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.
2

Stream trades (30 seconds)

Save this as stream.js and run with API_KEY=pn_live_... node stream.js:
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);
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.
3

What you'll see

Every message is one flat event with everything you need. Here’s a real captured fill:
{
  "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.
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 for all options.
If you need the full transaction format with nested fills and status lifecycle, subscribe to settlements instead:
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 for the full field reference.

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.

Multiple streams, one connection

PolyNode supports multiple subscription types on the same WebSocket:
StreamPurposeSubscribe
FillsIndividual pre-confirmation trades, one flat event per fill. Simplest format.{"action": "subscribe", "type": "fills"}
SettlementsPre-confirmation transaction bundles with all fills in a trades[] array (default){"action": "subscribe", "type": "settlements"}
TradesConfirmed on-chain fills with exact receipt values{"action": "subscribe", "type": "trades"}
PricesPrice-moving events for specific markets{"action": "subscribe", "type": "prices"}
BlocksNew Polygon block notifications with Polymarket stats{"action": "subscribe", "type": "blocks"}
WalletsAll activity for specified wallets{"action": "subscribe", "type": "wallets"}
MarketsAll activity for specified markets{"action": "subscribe", "type": "markets"}
Large TradesWhale alerts ($1K+ by default){"action": "subscribe", "type": "large_trades"}
GlobalFull firehose of all event types{"action": "subscribe", "type": "global"}
OracleUMA resolution lifecycle (resolutions, disputes, flags){"action": "subscribe", "type": "oracle"}
ChainlinkReal-time crypto prices (7 feeds, ~1/sec each){"action": "subscribe", "type": "chainlink"}
All can run simultaneously on the same connection. See Subscriptions & Filters 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:
{"action": "ping"}
Response:
{"type": "pong", "ts": 1774429169820}
Any message you send (subscribe, unsubscribe, ping) resets the server’s liveness timer.
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 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())
// 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);
};

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:
{
  "type": "error",
  "code": "invalid_json",
  "message": "Message is not valid JSON."
}
Error codeCause
invalid_jsonMessage is not valid JSON
invalid_messageUnknown action or missing required fields
unresolved_slugsSlug not found in market metadata
unresolved_condition_idsCondition 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:
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.