Skip to main content

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​

URLPurposeFrame format
wss://api.rocky.exchange/ws/<stream>One streambare 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​

StreamCadencePage
<symbol>@depth5, @depth10, @depth20book snapshot every 250 msPartial Book Depth
<symbol>@tradeper fillTrade Streams
<symbol>@kline_1mevery 1 s while a bar is open, plus once on closeKline and Ticker
<symbol>@tickerevery 1 sKline 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​

LimitValueBehaviour when exceeded
Streams per connection32 (counted as requested, before de-duplication)Close 1008
Concurrent connections per client IP16 (default; operator-configurable, exempt IPs uncapped)HTTP 429 {"code":-1003,…} at the upgrade — no 101
Server ping interval20 s—
Silence tolerated60 s (three missed pongs)Server closes the socket
Buffered frames256 per stream, plus a 256-frame queue per connectionSlow 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 @trade it 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 textCause
no streams requested/stream without streams=, or an empty/all-empty list.
too many streams: N requested, 32 allowedList 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_SUBSCRIPTIONS messages; no id-tagged responses.
  • No @aggTrade, @bookTicker, @miniTicker, !ticker@arr, !miniTicker@arr, @depth / @depth@100ms diff streams, @kline_<interval> other than 1m, or @avgPrice.
  • No user-data streams and no /ws-api order 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 lastUpdateId is a timestamp.