TradeReady.io
WebSocket

WebSocket Connection

Connect to the TradeReady WebSocket server for real-time streaming — auth, heartbeat, subscriptions, and error codes.

Download .md

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:

AttemptWait before reconnect
1st reconnect1 second
2nd reconnect2 seconds
3rd reconnect4 seconds
4th reconnect8 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

CodeCauseResolution
UNKNOWN_ACTIONThe action field is not subscribe, unsubscribe, or pongCheck the spelling and case of action
INVALID_CHANNELThe channel field does not match any known channelSee WebSocket Channels for valid names
SUBSCRIPTION_LIMITAttempted to add an 11th subscriptionUnsubscribe from a channel before adding a new one
INVALID_API_KEYAuth 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

LimitValue
Max subscriptions per connection10
Ping interval30 seconds
Pong timeout10 seconds
Auth failure close code4401

There is no documented limit on the number of simultaneous connections per API key, but avoid opening more connections than necessary.


On this page