WebSocket Streams
Status: Implemented.
Server-pushed market data over WebSocket, in the Binance Spot stream format. Public, unauthenticated, rate-governed by a per-IP connection cap rather than request weight.
Endpoints
| URL | Purpose | Frame format |
|---|---|---|
wss://api.rocky.exchange/ws/<stream> | One stream | bare frame |
wss://api.rocky.exchange/stream?streams=<a>/<b>/<c> | Combined streams (/-separated) | {"stream":"<name>","data":<frame>} |
Sandbox hosts are listed in your Integration Pack.
Available streams
| Stream | Cadence | Page |
|---|---|---|
<symbol>@depth5, @depth10, @depth20 | book snapshot every 250 ms | Partial Book Depth |
<symbol>@trade | per fill | Trade Streams |
<symbol>@kline_1m | every 1 s while a bar is open, plus once on close | Kline and Ticker |
<symbol>@ticker | every 1 s | Kline and Ticker |
<symbol>@depth (diff depth) | — | Not yet implemented |
Stream names
<symbol>@<kind>, where <symbol> is the market symbol lowercased with the hyphen kept:
cbtc-usdcb@trade ✔
ceth-cbtc@depth10 ✔
CBTC-USDCB@trade ✖ uppercase anywhere is refused
cbtcusdcb@trade ✖ the hyphen is part of the name
cbtc-usdcb@depth ✖ diff depth is not implemented
cbtc-usdcb@kline_5m ✖ only kline_1m
This is the one deliberate departure from Binance naming, where symbols have no separator (btcusdt@trade). Rocky keeps the hyphen because folding it would make distinct asset pairs collide. Frames themselves carry the canonical uppercase symbol ("s":"CBTC-USDCB"), matching every REST payload.
The symbol set is the live exchangeInfo list (ledger.markets, spot and active), refreshed every 60 s; a delisted market is refused at subscribe time. Open @depth* streams for a delisted symbol are closed (and the socket ends once every stream on it has closed); open @trade, @kline_1m and @ticker streams for it simply go silent.
Connection governance
| Limit | Value | Behaviour when exceeded |
|---|---|---|
| Streams per connection | 32 (counted as requested, before de-duplication) | Close 1008 |
| Concurrent connections per client IP | 16 (default; operator-configurable, exempt IPs uncapped) | HTTP 429 {"code":-1003,…} at the upgrade — no 101 |
| Server ping interval | 20 s | — |
| Silence tolerated | 60 s (three missed pongs) | Server closes the socket |
| Buffered frames | 256 per stream, plus a 256-frame queue per connection | Slow consumers skip frames (see below) |
- The server sends a WebSocket ping every 20 s. Answer with a pong (every mainstream client library does this automatically). Any inbound frame — pong, ping, or text — counts as proof of life.
- The proxy timeout is 300 s, so an idle but pinging socket stays up indefinitely.
- A peer that stops reading is treated the same way: a frame that cannot be written within 60 s closes the socket.
- A connection that cannot keep up does not apply back-pressure to other subscribers: frames it missed are dropped and it resumes from the current frame. For depth this is the right recovery (a stale book is worthless); for
@tradeit means a very slow client can miss prints and should reconcile via REST.
Close code 1008 reasons
A bad subscription is refused after the upgrade with a close frame (code 1008, "policy violation") so that the reason is readable — a browser client would otherwise see only an opaque handshake failure.
| Reason text | Cause |
|---|---|
no streams requested | /stream without streams=, or an empty/all-empty list. |
too many streams: N requested, 32 allowed | List longer than 32 entries. |
unknown stream: <name> | Unknown symbol, unsupported kind, wrong casing, empty list member, or a diff-depth / unsupported-interval name. The offending name is quoted (truncated to 96 bytes). One bad name refuses the whole list. |
Protocol notes
- No subscribe/unsubscribe protocol. The stream list is fixed by the URL. Text or binary frames you send are ignored (not an error); to change streams, open a new connection.
- Duplicate names in a combined list are collapsed; you receive each frame once.
- Combined-stream frames echo the exact name you subscribed with in
stream. - No
listenKey/ user-data streams; order and account state are REST-only.
Example
import asyncio, json, websockets # pip install websockets
async def main():
url = "wss://api.rocky.exchange/stream?streams=cbtc-usdcb@depth5/cbtc-usdcb@trade"
async with websockets.connect(url) as ws: # ping/pong handled by the library
async for raw in ws:
msg = json.loads(raw)
print(msg["stream"], msg["data"])
asyncio.run(main())
Differences from Binance
- Stream names are lowercase with a hyphen (
cbtc-usdcb@trade). - No
SUBSCRIBE/UNSUBSCRIBE/LIST_SUBSCRIPTIONSmessages; noid-tagged responses. - No
@aggTrade,@bookTicker,@miniTicker,!ticker@arr,!miniTicker@arr,@depth/@depth@100msdiff streams,@kline_<interval>other than1m, or@avgPrice. - No user-data streams and no
/ws-apiorder entry over WebSocket. - Trade ids are UUID strings.
- Connection cap is 16 per IP (Binance: 300 per 5 min); refused at the upgrade with HTTP 429.
- Depth streams are 250 ms snapshots (Binance: 100 ms / 1000 ms) whose
lastUpdateIdis a timestamp.