The pending-to-confirmed lifecycle
PolyNode detects Polymarket settlements before they confirm on-chain. This means every settlement goes through two stages, delivered as two separate WebSocket events:
settlement — the trade is detected pre-chain (status: "pending")
status_update — the trade confirms in a Polygon block (typically 2–4 seconds later)
Link them together by tx_hash:
const pending = new Map(); // tx_hash → settlement data
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "settlement" && msg.data.status === "pending") {
pending.set(msg.data.tx_hash, {
...msg.data,
received_at: Date.now(),
});
console.log(`PENDING: ${msg.data.taker_side} $${msg.data.taker_size}`);
}
if (msg.type === "status_update") {
const original = pending.get(msg.data.tx_hash);
if (original) {
const leadMs = msg.data.confirmed_at - original.received_at;
console.log(`CONFIRMED: ${msg.data.latency_ms}ms server lead, ${leadMs}ms client lead`);
pending.delete(msg.data.tx_hash);
}
}
};
These are two separate events, not an update to the original event. If you only listen for settlement events, you’ll see pending trades but never know when they confirm.
Handle the snapshot correctly
When you subscribe, PolyNode sends a snapshot of recent events before streaming live data. The snapshot contains both settlement and status_update events mixed together.
The gotcha: Settlement events in the snapshot always have their original status (pending), even if they were confirmed seconds ago. The corresponding status_update is a separate entry in the same snapshot.
If you only process settlement events from the snapshot, you’ll load stale “pending” settlements that are actually already confirmed — and their confirmation event will never arrive as a live message because it was already sent in the snapshot you ignored.
Correct snapshot handling
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "snapshot") {
// Step 1: Collect all status updates from the snapshot
const confirmations = new Map();
for (const ev of msg.events) {
if (ev.type === "status_update" && ev.data) {
confirmations.set(ev.data.tx_hash, ev.data);
}
}
// Step 2: Process settlements, checking for matching confirmations
for (const ev of msg.events) {
if (ev.type === "settlement" && ev.data) {
const confirmation = confirmations.get(ev.data.tx_hash);
if (confirmation) {
// This settlement is already confirmed — use the confirmation data
handleConfirmedSettlement(ev.data, confirmation);
} else if (ev.data.status === "pending") {
// Genuinely pending — will receive a live status_update soon
handlePendingSettlement(ev.data);
}
}
}
return;
}
// Live events — standard handling
if (msg.type === "settlement") handlePendingSettlement(msg.data);
if (msg.type === "status_update") handleStatusUpdate(msg.data);
};
Add a stuck-pending timeout
In rare cases, a status_update might be missed (e.g., network interruption, reconnection timing). Add a timeout so stale pending items don’t get stuck forever:
const PENDING_TIMEOUT_MS = 15_000; // 15 seconds
setInterval(() => {
const now = Date.now();
for (const [hash, data] of pending) {
if (now - data.received_at > PENDING_TIMEOUT_MS) {
console.log(`Timeout: ${hash} — no confirmation after 15s`);
pending.delete(hash);
}
}
}, 5_000);
Under normal conditions, confirmations arrive within 2–5 seconds. A 15-second timeout is generous enough to never fire in normal operation but catches edge cases.
Reconnection with state recovery
When reconnecting, use snapshot_count to recover recent state. The snapshot fills in events you missed during the disconnection:
function connect() {
const ws = new WebSocket("wss://ws.polynode.dev/ws?key=pn_live_YOUR_KEY");
let delay = 1000;
ws.onopen = () => {
delay = 1000;
ws.send(JSON.stringify({
action: "subscribe",
type: "settlements",
filters: { snapshot_count: 100 }, // Catch up on missed events
}));
};
ws.onclose = () => {
setTimeout(connect, Math.min(delay *= 2, 30000));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "heartbeat") return;
// Handle snapshot + live events as above
};
}
The snapshot includes both settlement and status_update events from the recent buffer. Processing both event types from the snapshot (as shown above) ensures you have accurate state immediately after reconnection.
Connection health
The server sends a WebSocket Ping frame every 30 seconds and expects the client to respond with a Pong. If no Pong is received within 90 seconds (3 missed heartbeats), the server closes the connection.
You almost certainly don’t need to do anything. All standard WebSocket libraries respond to Ping with Pong automatically:
- Browser
WebSocket — handled by the browser
- Node.js
ws — automatic by default
- Python
websockets — automatic by default
- Go
gorilla/websocket — automatic with SetPongHandler (default)
If your read loop is blocked (e.g. doing heavy synchronous work before reading the next message), the client cannot send Pong frames and the server will eventually drop the connection. Keep your read loop running — offload heavy processing to a separate thread or task.
Summary
| Pattern | Why |
|---|
Process both settlement and status_update events | Settlements and confirmations are separate events linked by tx_hash |
| Process both event types from the snapshot | Snapshot settlements keep their original pending status — confirmations are separate entries |
| Add a stuck-pending timeout (15s) | Safety net for missed confirmations during reconnection |
Use snapshot_count on reconnect | Recovers events missed during disconnection |
| Keep read loop running | Server drops connections with no Pong after 90s |