WebSockets
Stream live prediction-market events over a single persistent connection — no polling, no rate-limit juggling. The first live stream delivers Polymarket trades as they execute, with server-side filtering by wallet, market, outcome, and more so you only receive the events you care about.
Connection
Open a WebSocket connection to the gateway and authenticate with your Heisenberg API token as a Bearer credential. Your token is verified at the connection upgrade, so only authenticated clients stream.
wss://ripple.heisenberg.so/wsAuthorization: Bearer YOUR_API_TOKENConnect from your backend
Authorization header — the standard setup for production streaming. Keep your API token server-side.Event Types
Every stream is identified by an event type. Today polymarket.trade is live, the first of more to come. Access to each type is governed by your plan. Call GET /event-types for the catalog you're authorized for — it returns each type's field schema, including a per-field filterable flag that tells you exactly which fields you can filter on.
curl https://ripple.heisenberg.so/event-types \
-H "Authorization: Bearer YOUR_API_TOKEN"[
{
"name": "polymarket.trade",
"label": "polymarket_trades_realtime",
"description": "",
"schema": {
"fields": [
{ "name": "slug", "type": "string", "filterable": true },
{ "name": "proxy_wallet", "type": "string", "filterable": true },
{ "name": "outcome", "type": "string", "filterable": true },
{ "name": "price", "type": "float", "filterable": false }
]
}
}
]Abridged — schema.fields lists all 24 fields of polymarket.trade. See Event Messages for the full payload.
Subscribe
Once connected, send a subscribe frame. Each frame can open multiple subscriptions at once — pass one entry per stream in the subscriptions array. Omit filters (or pass {}) to receive every event of that type.
Subscribe to all trades
{
"action": "subscribe",
"subscriptions": [
{ "type": "polymarket.trade", "filters": {} }
]
}Subscribe to one market
{
"action": "subscribe",
"subscriptions": [
{
"type": "polymarket.trade",
"filters": { "slug": "sol-updown-15m-1779670800" }
}
]
}The server replies with a subscribed acknowledgement listing the event types it granted and a unique id for each subscription. Keep each id — you need it to unsubscribe. The filters you sent are echoed back when present.
{
"action": "subscribed",
"types": ["polymarket.trade"],
"subscriptions": [
{
"id": "cede7755-c0a6-41b8-b316-ca4377dedb5d",
"type": "polymarket.trade",
"filters": { "slug": "sol-updown-15m-1779670800" }
}
]
}Control messages vs. events
action field (subscribed, error, rate_limit, …). Data events instead carry a type and a data object. Branch on whichever field is present. Mixed frames are handled gracefully — the acknowledgement grants every type your plan includes, so the rest of the subscription always goes through.Filters
Filters are matched server-side with exact equality against the event's top-level data fields, before delivery — so you receive only the events you want. For polymarket.trade, the filterable fields are:
proxy_walletslugcondition_idevent_slugoutcomeoutcome_indexsideassetmakertakertransaction_hashorder_hash
These are the fields you can filter on — GET /event-types is the authoritative source. To track several wallets or markets independently, open one subscription per target in the same subscribe frame.
Build a Subscription
Pick your filters and copy the generated subscribe frame — or a ready-to-run Node or Python client — straight into your app.
{
"action": "subscribe",
"subscriptions": [
{
"type": "polymarket.trade",
"filters": {}
}
]
}Event Messages
Each event is a JSON frame carrying the event type, its source, the delivery timestamp, and a data object with the trade. Here is a live polymarket.trade event (long hashes abbreviated):
{
"type": "polymarket.trade",
"source": "polymarket-trade-scraper",
"timestamp": "2026-05-27T13:50:00.167247+00:00",
"data": {
"id": "0x8902f2…498832_0x31ba8c…a5aa09",
"side": "BUY",
"size": 11,
"price": 0.53,
"timestamp": "2026-05-27T13:49:52+00:00",
"transaction_hash": "0x8902f299…d6d498832",
"order_hash": "0x31ba8c8c…454aa5aa09",
"block_number": 87514326,
"condition_id": "0x9ad19824…2d7f2d9",
"asset": "67025362…363793",
"title": "Bitcoin Up or Down - May 27, 9:50AM-9:55AM ET",
"slug": "btc-updown-5m-1779889800",
"event_slug": "btc-updown-5m",
"outcome": "Up",
"outcome_index": 0,
"icon": "https://polymarket-upload.s3…/BTC+fullsize.png",
"maker": "0x55338c…d1b8220",
"taker": "0x6833c0…1b5cfe1",
"proxy_wallet": "0x55338c…d1b8220",
"name": null,
"pseudonym": null,
"bio": null,
"profile_image": null,
"profile_image_optimized": null
}
}Filterable fields
The fields you can filter on (from /event-types). Every event also carries the additional non-filterable fields shown in the payload above.
| Field | Type | Description |
|---|---|---|
proxy_wallet | string | Trader's proxy wallet |
slug | string | Market slug |
condition_id | string | Market condition ID |
event_slug | string | Parent event slug |
outcome | string | Outcome traded (e.g. Up, No) |
outcome_index | integer | Index of the traded outcome |
side | string | Trade direction — BUY or SELL |
asset | string | Outcome token (asset) ID |
maker | string | Maker wallet address |
taker | string | Taker wallet address |
transaction_hash | string | On-chain transaction hash |
order_hash | string | On-chain order hash |
Unsubscribe
Stop one or more streams by sending an unsubscribe frame with the subscription_ids returned when you subscribed. The server confirms with an unsubscribed acknowledgement.
{
"action": "unsubscribe",
"subscription_ids": ["cede7755-c0a6-41b8-b316-ca4377dedb5d"]
}{
"action": "unsubscribed",
"subscription_ids": ["cede7755-c0a6-41b8-b316-ca4377dedb5d"]
}Connection Lifecycle
Heartbeat
Connections are kept healthy by a built-in ping/pong heartbeat. Most WebSocket libraries respond to pings automatically, so there's nothing to implement — your stream stays alive as long as your client is connected.
Reconnection
Connections are long-lived. For a resilient client, reconnect automatically with exponential backoff and re-send your subscribe frames once reconnected — the standard pattern for any streaming integration. A close with code 4001 is simply a cue to reconnect to a fresh upstream feed, and deploys roll out with graceful connection draining so reconnects stay clean. You can confirm gateway health any time with GET /health (returns 200 OK).
Rate Limits & Quotas
Each plan includes streaming allowances sized for production workloads. A few simple practices keep your integration efficient and well within them:
- •Active subscriptions — a single connection holds many subscriptions at once; your plan sets how many run concurrently.
- •Message rate — events are delivered at your plan's throughput. If you reach it, the gateway sends a single
rate_limitnotice for that window and keeps your connection open. Filters keep your feed focused and comfortably within budget. - •Subscribe in one frame — open all your subscriptions in a single
subscribeframe. It's the fastest way to start streaming and keeps your client tidy. - •One connection is all you need — a single connection carries every subscription, so there's rarely a reason to open more. It's the simplest setup and scales effortlessly.
{
"action": "rate_limit",
"message": "message rate limit reached"
}Scale up any time
Errors & Notices
The gateway always answers — any protocol issue comes back as a clear, in-band error message that names the relevant types and a human-readable message, so your client knows exactly what to do. For example, requesting a type your plan doesn't include yet:
{
"action": "error",
"types": ["polymarket.does_not_exist"],
"message": "unauthorized: polymarket.does_not_exist"
}Filtering on a non-filterable field:
{
"action": "error",
"types": ["polymarket.trade"],
"message": "non-filterable keys for polymarket.trade: not_a_real_field"
}Authentication is checked at the connection upgrade with a standard 401, so only valid tokens stream.
Full Example
A minimal client that connects, subscribes to one market, prints each trade, and reconnects on 4001.
Node.js (ws)
import WebSocket from 'ws';
function connect() {
const ws = new WebSocket('wss://ripple.heisenberg.so/ws', {
headers: { Authorization: 'Bearer ' + process.env.HEISENBERG_TOKEN },
});
ws.on('open', () => {
ws.send(JSON.stringify({
action: 'subscribe',
subscriptions: [
{ type: 'polymarket.trade', filters: { slug: 'btc-updown-5m-1779889800' } },
],
}));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.action === 'subscribed') {
console.log('subscribed:', msg.subscriptions.map((s) => s.id));
} else if (msg.action === 'error' || msg.action === 'rate_limit') {
console.warn(msg.action + ':', msg.message);
} else if (msg.type === 'polymarket.trade') {
const t = msg.data;
console.log(t.side, t.size, '@', t.price, '·', t.slug);
}
});
// The ws library answers server pings automatically.
ws.on('close', (code) => {
if (code === 4001) setTimeout(connect, 1000); // upstream interrupted — reconnect + re-subscribe
});
}
connect();Python (websockets)
import asyncio, json, os
import websockets
async def main():
url = "wss://ripple.heisenberg.so/ws"
headers = {"Authorization": f"Bearer {os.environ['HEISENBERG_TOKEN']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
await ws.send(json.dumps({
"action": "subscribe",
"subscriptions": [
{"type": "polymarket.trade", "filters": {"slug": "btc-updown-5m-1779889800"}},
],
}))
async for raw in ws:
msg = json.loads(raw)
if "action" in msg: # subscribed / error / rate_limit
print(msg)
elif msg.get("type") == "polymarket.trade":
t = msg["data"]
print(t["side"], t["size"], "@", t["price"], "·", t["slug"])
asyncio.run(main())Need More Streams?
More event types are on the way. To request early access or a dedicated event type, reach us at contact@heisenberg.so.