WebSocket stream

Push instead of poll: live candles for up to 200 pair/timeframe subscriptions and live screener rankings, over one authenticated socket: the same feed that drives the Osiris charts.

POST/v1/ws-token
WSwss://osiristrader.com:8081/v1?token=…

Anything live should come from this stream, not from REST polling. One socket connection replaces thousands of candle polls, delivers ticks with sub-second latency straight from our ingest pipeline, and costs you one REST request per five minutes (the connection token) against your quota. If your integration has a loop that fetches the same endpoint every few seconds, this page is how you delete it.

Connecting

Authentication is a two-step handshake. First, exchange your API key for a short-lived connection token over REST; this keeps your long-lived key out of WebSocket URLs, which get logged by proxies and browser dev tools. Then open the socket with that token; the gateway validates it during the HTTP upgrade and rejects invalid or expired tokens with 401 before a socket ever exists.

Step 1: mint a token (valid 5 minutes, reusable for reconnects within that window)
curl -X POST "https://osiristrader.com/api/v1/ws-token" \
  -H "Authorization: Bearer osk_YOUR_KEY"

{
  "token": "wst_9fb2bd4c31b5bd3b9d54…",
  "expiresIn": 300,
  "url": "wss://osiristrader.com:8081/v1?token=wst_9fb2bd4c31b5bd3b9d54…"
}

The token authorizes new connections for 5 minutes; an established connection is never cut by its token expiring; it lives until you close it or fail our keepalive. For reconnect logic: reuse the token if it is still fresh, otherwise mint a new one first.

Subscribing

All client → server messages are small JSON objects with an action. Two subscription types exist, multiplexed over the same socket:

Live candles: one subscription per pair + timeframe
> { "action": "subscribe",   "symbol": "BTCUSDT", "interval": "1m" }
> { "action": "unsubscribe", "symbol": "BTCUSDT", "interval": "1m" }
Live screener rankings: a whole sorted view, pushed every ~3 s
> { "action": "subscribe_screener",   "sort": "volsurge", "interval": "5m", "algo": "SCORE" }
> { "action": "unsubscribe_screener", "sort": "volsurge", "interval": "5m", "algo": "SCORE" }
FieldValues
interval1m 5m 15m 1h 4h 1d 1w. Required on every message.
sorttrend volatility volsurge volume trades funding correlation. The screener categories.
algoThe ranking metric within the category, same whitelists as Rankings. Omit for the category default.
exchangeVenue the subscription reads. See Exchanges. Omit for Binance. Part of the subscription identity: the same pair on two venues is two subscriptions.
Combined-coverage screener: one ranked view across several venues
> { "action": "subscribe_screener", "coverage": "all",
    "exchange": "binance", "exchanges": "binance,bybit,okx",
    "sort": "volatility", "interval": "5m", "algo": "" }

With coverage: "all" the gateway ranks the union of the venues named in exchanges (comma-separated), each pair scored on the venue resolved for it; the exchange field becomes the preferred venue for coins listed on several. Pushes echo coverage and the canonical exchanges list back.

What you receive

Kline ticks: an immediate snapshot on subscribe, then every update
< { "type": "kline", "s": "BTCUSDT", "tf": "1m",
    "k": { "t": 1783877000000, "o": 63981.5, "h": 64010.0, "l": 63960.1, "c": 63999.9, "v": 120.4 },
    "closed": false }

closed: false is the live bar mutating in place: same t, growing h/l/v, moving c. closed: true marks the bucket's final values; the next tick opens a new t. Maintaining a candle series is therefore one rule: same t → replace last bar; new t → append. The immediate snapshot on subscribe means you render instantly even on pairs that trade rarely.

Screener pushes: the full sorted view, every ~3 s
< { "type": "screener", "sort": "volsurge", "interval": "5m", "algo": "SCORE",
    "rows": [ { "s": "WIFUSDT", "v": 2.41 }, { "s": "SOLUSDT", "v": 1.87 } ] }
Errors: currently only the subscription caps
< { "type": "error", "code": "sub_limit", "message": "At most 200 kline subscriptions per connection." }

Limits & keepalive

  • 200 kline and 10 screener subscriptions per connection. Beyond that, each attempt is answered with the sub_limit error and ignored; existing subscriptions are unaffected.
  • The gateway pings every 30 s; standard WebSocket libraries answer pongs automatically. A client that stops answering is terminated, so if you hand-rolled the transport, make sure pongs go out.
  • Duplicate subscribes are safe and idempotent; the gateway answers each with a fresh snapshot. That makes "just re-send all my subscriptions" the correct and complete recovery strategy.

A production-shaped client

JavaScript: token refresh, reconnect with backoff, resubscribe
const KEY = process.env.OSIRIS_KEY;
const subs = [ { action: "subscribe", symbol: "BTCUSDT", interval: "1m" } ];
let backoff = 1000;

async function connect() {
    const res = await fetch("https://osiristrader.com/api/v1/ws-token", {
        method: "POST", headers: { Authorization: "Bearer " + KEY },
    });
    const { url } = await res.json();

    const ws = new WebSocket(url);
    ws.onopen = () => {
        backoff = 1000;                       // healthy again
        subs.forEach(s => ws.send(JSON.stringify(s)));  // idempotent recovery
    };
    ws.onmessage = ev => {
        const msg = JSON.parse(ev.data);
        if (msg.type === "kline") onKline(msg);
    };
    ws.onclose = () => {
        setTimeout(connect, backoff);
        backoff = Math.min(backoff * 2, 30_000);
    };
}
connect();
Choosing REST vs WebSocket: the stream carries candles and screener rankings. Metrics, levels, densities and the market snapshot are REST; they recompute on 2-60 s cadences, so polling them at their natural rate is already optimal. The hybrid that most integrations land on: WebSocket for price, REST at matching cadence for analytics, webhooks for conditions.