229 lines
5.8 KiB
Markdown
229 lines
5.8 KiB
Markdown
# WebSocket
|
|
|
|
Three channels for real-time data. Market and sports channels are public; user channel requires API credentials.
|
|
|
|
## Channels
|
|
|
|
| Channel | Endpoint | Auth |
|
|
|---------|----------|------|
|
|
| Market | `wss://ws-subscriptions-clob.polymarket.com/ws/market` | No |
|
|
| User | `wss://ws-subscriptions-clob.polymarket.com/ws/user` | Yes |
|
|
| Sports | `wss://sports-api.polymarket.com/ws` | No |
|
|
|
|
## Market Channel
|
|
|
|
Public. Subscribes by **asset IDs** (token IDs).
|
|
|
|
### Subscribe
|
|
|
|
```json
|
|
{
|
|
"assets_ids": ["TOKEN_ID_1", "TOKEN_ID_2"],
|
|
"type": "market",
|
|
"custom_feature_enabled": true
|
|
}
|
|
```
|
|
|
|
Set `custom_feature_enabled: true` to enable `best_bid_ask`, `new_market`, and `market_resolved` events.
|
|
|
|
### Event Types
|
|
|
|
| Event | Trigger | Key Fields |
|
|
|-------|---------|------------|
|
|
| `book` | On subscribe + when trade affects book | `bids[]`, `asks[]`, `hash`, `timestamp` |
|
|
| `price_change` | Order placed or cancelled | `price_changes[]` with `price`, `size`, `side`, `best_bid`, `best_ask` |
|
|
| `last_trade_price` | Trade executed | `price`, `side`, `size`, `fee_rate_bps` |
|
|
| `tick_size_change` | Price hits >0.96 or <0.04 | `old_tick_size`, `new_tick_size` |
|
|
| `best_bid_ask` | Top-of-book changes | `best_bid`, `best_ask`, `spread` |
|
|
| `new_market` | Market created | `question`, `assets_ids`, `outcomes` |
|
|
| `market_resolved` | Market resolved | `winning_asset_id`, `winning_outcome` |
|
|
|
|
Events requiring `custom_feature_enabled: true`: `best_bid_ask`, `new_market`, `market_resolved`.
|
|
|
|
**`tick_size_change` is critical for bots** — if tick size changes and you use the old one, orders are rejected.
|
|
|
|
A `price_change` with `size: "0"` means the price level was removed from the book.
|
|
|
|
### Example Messages
|
|
|
|
```json
|
|
// book
|
|
{
|
|
"event_type": "book",
|
|
"asset_id": "TOKEN_ID",
|
|
"market": "0xCONDITION_ID",
|
|
"bids": [{"price": ".48", "size": "30"}],
|
|
"asks": [{"price": ".52", "size": "25"}],
|
|
"timestamp": "123456789000",
|
|
"hash": "0x..."
|
|
}
|
|
```
|
|
|
|
```json
|
|
// price_change
|
|
{
|
|
"event_type": "price_change",
|
|
"market": "0xCONDITION_ID",
|
|
"price_changes": [{
|
|
"asset_id": "TOKEN_ID",
|
|
"price": "0.5",
|
|
"size": "200",
|
|
"side": "BUY",
|
|
"hash": "...",
|
|
"best_bid": "0.5",
|
|
"best_ask": "1"
|
|
}],
|
|
"timestamp": "..."
|
|
}
|
|
```
|
|
|
|
## User Channel
|
|
|
|
Authenticated. Subscribes by **condition IDs** (market IDs), not asset IDs. The `markets` field is optional — omit it to receive events for all markets.
|
|
|
|
### Subscribe
|
|
|
|
```json
|
|
{
|
|
"auth": {
|
|
"apiKey": "your-api-key",
|
|
"secret": "your-api-secret",
|
|
"passphrase": "your-passphrase"
|
|
},
|
|
"markets": ["0xCONDITION_ID"],
|
|
"type": "user"
|
|
}
|
|
```
|
|
|
|
### Event Types
|
|
|
|
| Event | Trigger |
|
|
|-------|---------|
|
|
| `trade` | Trade lifecycle: MATCHED, MINED, CONFIRMED, RETRYING, FAILED |
|
|
| `order` | Order lifecycle: PLACEMENT, UPDATE, CANCELLATION |
|
|
|
|
### Trade Message
|
|
|
|
```json
|
|
{
|
|
"event_type": "trade",
|
|
"id": "trade-uuid",
|
|
"market": "0xCONDITION_ID",
|
|
"asset_id": "TOKEN_ID",
|
|
"side": "BUY",
|
|
"size": "10",
|
|
"price": "0.57",
|
|
"status": "MATCHED",
|
|
"maker_orders": [{ "order_id": "0x...", "matched_amount": "10", "price": "0.57" }],
|
|
"type": "TRADE"
|
|
}
|
|
```
|
|
|
|
### Order Message
|
|
|
|
```json
|
|
{
|
|
"event_type": "order",
|
|
"id": "0xORDER_ID",
|
|
"market": "0xCONDITION_ID",
|
|
"asset_id": "TOKEN_ID",
|
|
"side": "SELL",
|
|
"price": "0.57",
|
|
"original_size": "10",
|
|
"size_matched": "0",
|
|
"type": "PLACEMENT"
|
|
}
|
|
```
|
|
|
|
Order types: `PLACEMENT`, `UPDATE` (partial fill), `CANCELLATION`.
|
|
|
|
## Sports Channel
|
|
|
|
No subscription message needed. Connect and receive all active sports data.
|
|
|
|
```json
|
|
// sport_result
|
|
{ "type": "sport_result", ... } // Live scores, periods, status
|
|
```
|
|
|
|
## Dynamic Subscribe / Unsubscribe
|
|
|
|
Modify subscriptions without reconnecting:
|
|
|
|
```json
|
|
// Market channel — subscribe to more
|
|
{ "assets_ids": ["NEW_TOKEN_ID"], "operation": "subscribe", "custom_feature_enabled": true }
|
|
|
|
// Market channel — unsubscribe
|
|
{ "assets_ids": ["OLD_TOKEN_ID"], "operation": "unsubscribe" }
|
|
|
|
// User channel — subscribe to more markets
|
|
{ "markets": ["0xNEW_CONDITION_ID"], "operation": "subscribe" }
|
|
```
|
|
|
|
## Heartbeat
|
|
|
|
### Market & User Channels
|
|
Send `PING` every **10 seconds**. Server responds with `PONG`.
|
|
|
|
```typescript
|
|
const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market");
|
|
|
|
ws.onopen = () => {
|
|
// Subscribe...
|
|
setInterval(() => ws.send("PING"), 10_000);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
if (event.data === "PONG") return;
|
|
const msg = JSON.parse(event.data);
|
|
// handle msg.event_type
|
|
};
|
|
```
|
|
|
|
### Sports Channel
|
|
Server sends `ping` every 5 seconds. Respond with `pong` within 10 seconds or connection closes.
|
|
|
|
## Full TypeScript Example
|
|
|
|
```typescript
|
|
const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market");
|
|
|
|
ws.onopen = () => {
|
|
ws.send(JSON.stringify({
|
|
type: "market",
|
|
assets_ids: ["TOKEN_ID"],
|
|
custom_feature_enabled: true,
|
|
}));
|
|
setInterval(() => ws.send("PING"), 10_000);
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
if (event.data === "PONG") return;
|
|
const msg = JSON.parse(event.data);
|
|
switch (msg.event_type) {
|
|
case "book":
|
|
console.log("Book snapshot:", msg.bids.length, "bids", msg.asks.length, "asks");
|
|
break;
|
|
case "price_change":
|
|
for (const pc of msg.price_changes) {
|
|
console.log(`${pc.side} ${pc.size}@${pc.price} (best: ${pc.best_bid}/${pc.best_ask})`);
|
|
}
|
|
break;
|
|
case "last_trade_price":
|
|
console.log(`Trade: ${msg.side} ${msg.size}@${msg.price}`);
|
|
break;
|
|
case "tick_size_change":
|
|
console.log(`Tick: ${msg.old_tick_size} → ${msg.new_tick_size}`);
|
|
break;
|
|
}
|
|
};
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
- **Connection closes immediately**: send subscription message right after open
|
|
- **Drops after ~10s**: you're not sending PING heartbeats
|
|
- **No messages**: verify asset IDs are correct and markets are active
|
|
- **Auth failed (user channel)**: check API credentials haven't expired
|