WebSocket Connection
Connect to the TradeReady WebSocket server for real-time streaming — auth, heartbeat, subscriptions, and error codes.
The WebSocket server delivers real-time price ticks, candle updates, private order fills, and portfolio snapshots without polling. A single connection can subscribe to up to 10 channels simultaneously.
Connection URL
ws://your-host/ws/v1?api_key=ak_live_...
Or with a JWT token:
ws://your-host/ws/v1?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Replace your-host with your deployed instance (e.g. api.tradeready.io). In local development: ws://localhost:8000/ws/v1.
The WebSocket server authenticates on connection open. If the api_key or token is invalid or missing, the server closes the connection immediately with close code 4401. There is no retry or challenge — obtain a valid key before connecting.
Authentication
The WebSocket server performs its own authentication — it does not share sessions with the REST API middleware. Pass your credentials as a query parameter:
API key (recommended):
ws://localhost:8000/ws/v1?api_key=ak_live_Hx3kP9...
JWT token:
ws://localhost:8000/ws/v1?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
On authentication failure, the connection is closed with code 4401. In your client, handle close code 4401 as a permanent failure (do not reconnect — obtain a valid key first).
Message Format
All messages are JSON objects. Direction matters:
Client → Server (actions you send):
{"action": "subscribe", "channel": "ticker", "symbol": "BTCUSDT"}
{"action": "unsubscribe", "channel": "ticker", "symbol": "BTCUSDT"}
{"action": "pong"}
Server → Client (messages you receive):
{"channel": "ticker", "symbol": "BTCUSDT", "data": {...}}
{"type": "ping"}
{"type": "error", "code": "INVALID_CHANNEL", "message": "Unknown channel."}
Subscribe and Unsubscribe
{"action": "subscribe", "channel": "CHANNEL_NAME", ...channel_params}
{"action": "unsubscribe", "channel": "CHANNEL_NAME", ...channel_params}
On successful subscribe, the server begins streaming data for that channel. On unsubscribe, streaming stops and the subscription slot is freed.
Example — subscribe to BTC price ticks:
{"action": "subscribe", "channel": "ticker", "symbol": "BTCUSDT"}
Example — unsubscribe:
{"action": "unsubscribe", "channel": "ticker", "symbol": "BTCUSDT"}
Subscription Cap
Each connection can hold a maximum of 10 active subscriptions. Attempting to add an 11th subscription returns an error without closing the connection:
{
"type": "error",
"code": "SUBSCRIPTION_LIMIT",
"message": "Maximum of 10 subscriptions per connection reached."
}
To add more subscriptions beyond the cap, either unsubscribe from existing channels first or open a second WebSocket connection.
Heartbeat
The server sends a ping every 30 seconds. You must respond with a pong within 10 seconds or the connection is closed.
This is an application-level heartbeat using JSON messages, not the WebSocket protocol-level ping/pong frames. Your client must send {"action": "pong"} as a text message — a WebSocket PONG frame is not sufficient.
Server sends (every 30s):
{"type": "ping"}
Client must respond within 10s:
{"action": "pong"}
If the server does not receive a pong within 10 seconds, it closes the connection and cleans up all subscriptions.
Reconnection
On disconnect, reconnect with exponential back-off:
| Attempt | Wait before reconnect |
|---|---|
| 1st reconnect | 1 second |
| 2nd reconnect | 2 seconds |
| 3rd reconnect | 4 seconds |
| 4th reconnect | 8 seconds |
| 5th+ | 60 seconds maximum |
After reconnecting, re-subscribe to all your previous channels. The server does not restore subscriptions automatically.
WebSocket Error Messages
When an action fails, the server sends an error message and does not close the connection:
{
"type": "error",
"code": "ERROR_CODE",
"message": "Human-readable description."
}
Error Codes
| Code | Cause | Resolution |
|---|---|---|
UNKNOWN_ACTION | The action field is not subscribe, unsubscribe, or pong | Check the spelling and case of action |
INVALID_CHANNEL | The channel field does not match any known channel | See WebSocket Channels for valid names |
SUBSCRIPTION_LIMIT | Attempted to add an 11th subscription | Unsubscribe from a channel before adding a new one |
INVALID_API_KEY | Auth failed on connection (also sent as close code 4401) | Obtain a valid API key |
Full Client Implementation
The SDK's AgentExchangeWS handles connection, auth, heartbeat, reconnection, and subscriptions automatically:
from agentexchange import AgentExchangeWS
ws = AgentExchangeWS(
api_key="ak_live_...",
base_url="ws://localhost:8000",
)
# Subscribe using decorators
@ws.on_ticker("BTCUSDT")
def handle_btc_price(msg):
price = msg["data"]["price"]
print(f"BTC: ${price}")
@ws.on_ticker("ETHUSDT")
def handle_eth_price(msg):
price = msg["data"]["price"]
print(f"ETH: ${price}")
@ws.on_order_update()
def handle_order(msg):
order = msg["data"]
print(f"Order {order['order_id']}: {order['status']}")
@ws.on_portfolio_update()
def handle_portfolio(msg):
portfolio = msg["data"]
print(f"Equity: ${portfolio['total_equity']}")
# Start streaming (blocks until stopped)
ws.run_forever()import asyncio
import json
import websockets
API_KEY = "ak_live_..."
WS_URL = f"ws://localhost:8000/ws/v1?api_key={API_KEY}"
async def main():
reconnect_delay = 1
while True:
try:
async with websockets.connect(WS_URL) as ws:
print("Connected")
reconnect_delay = 1 # Reset on successful connect
# Subscribe to channels
await ws.send(json.dumps({
"action": "subscribe",
"channel": "ticker",
"symbol": "BTCUSDT",
}))
await ws.send(json.dumps({
"action": "subscribe",
"channel": "orders",
}))
# Message loop
async for raw_message in ws:
msg = json.loads(raw_message)
# Handle heartbeat
if msg.get("type") == "ping":
await ws.send(json.dumps({"action": "pong"}))
continue
# Handle errors
if msg.get("type") == "error":
print(f"Server error [{msg['code']}]: {msg['message']}")
continue
# Handle channel data
channel = msg.get("channel")
if channel == "ticker":
print(f"{msg['symbol']}: ${msg['data']['price']}")
elif channel == "orders":
order = msg["data"]
print(f"Order {order['order_id']}: {order['status']}")
except websockets.exceptions.ConnectionClosedError as e:
if e.code == 4401:
print("Authentication failed. Check your API key.")
return # Do not reconnect
print(f"Disconnected (code {e.code}). Reconnecting in {reconnect_delay}s...")
except Exception as e:
print(f"Error: {e}. Reconnecting in {reconnect_delay}s...")
await asyncio.sleep(reconnect_delay)
reconnect_delay = min(reconnect_delay * 2, 60)
asyncio.run(main())const API_KEY = "ak_live_...";
const WS_URL = `ws://localhost:8000/ws/v1?api_key=${API_KEY}`;
let ws;
let reconnectDelay = 1000;
let subscriptions = [];
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log("Connected");
reconnectDelay = 1000;
// Resubscribe after reconnect
subscriptions.forEach(sub => ws.send(JSON.stringify(sub)));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
// Handle heartbeat
if (msg.type === "ping") {
ws.send(JSON.stringify({ action: "pong" }));
return;
}
// Handle errors
if (msg.type === "error") {
console.error(`[${msg.code}] ${msg.message}`);
return;
}
// Dispatch by channel
switch (msg.channel) {
case "ticker":
console.log(`${msg.symbol}: $${msg.data.price}`);
break;
case "orders":
console.log(`Order ${msg.data.order_id}: ${msg.data.status}`);
break;
case "portfolio":
console.log(`Equity: $${msg.data.total_equity}`);
break;
}
};
ws.onclose = (event) => {
if (event.code === 4401) {
console.error("Authentication failed. Check your API key.");
return; // Do not reconnect
}
console.log(`Disconnected. Reconnecting in ${reconnectDelay}ms...`);
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 60000);
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
function subscribe(channel, params = {}) {
const sub = { action: "subscribe", channel, ...params };
subscriptions.push(sub);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(sub));
}
}
// Usage
connect();
subscribe("ticker", { symbol: "BTCUSDT" });
subscribe("orders");
subscribe("portfolio");Connection Limits
| Limit | Value |
|---|---|
| Max subscriptions per connection | 10 |
| Ping interval | 30 seconds |
| Pong timeout | 10 seconds |
| Auth failure close code | 4401 |
There is no documented limit on the number of simultaneous connections per API key, but avoid opening more connections than necessary.
Related Pages
- WebSocket Channels — channel reference (ticker, candles, orders, portfolio, battle)
- Rate Limits — WebSocket is not subject to HTTP rate limiting
- Authentication — obtaining API keys and JWT tokens