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.
/v1/ws-tokenwss://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.
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:
> { "action": "subscribe", "symbol": "BTCUSDT", "interval": "1m" }
> { "action": "unsubscribe", "symbol": "BTCUSDT", "interval": "1m" }> { "action": "subscribe_screener", "sort": "volsurge", "interval": "5m", "algo": "SCORE" }
> { "action": "unsubscribe_screener", "sort": "volsurge", "interval": "5m", "algo": "SCORE" }| Field | Values |
|---|---|
interval | 1m 5m 15m 1h 4h 1d 1w. Required on every message. |
sort | trend volatility volsurge volume trades funding correlation. The screener categories. |
algo | The ranking metric within the category, same whitelists as Rankings. Omit for the category default. |
exchange | Venue the subscription reads. See Exchanges. Omit for Binance. Part of the subscription identity: the same pair on two venues is two subscriptions. |
> { "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
< { "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.
< { "type": "screener", "sort": "volsurge", "interval": "5m", "algo": "SCORE",
"rows": [ { "s": "WIFUSDT", "v": 2.41 }, { "s": "SOLUSDT", "v": 1.87 } ] }< { "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_limiterror 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
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();