WebSocket Streams
Real-time multiplexed stream endpoints, control messages, and channel keys
Diffusal exposes multiplexed websocket endpoints for public market data and authenticated private account channels. The public paths below remain the integration contract even after the backend moved websocket ownership onto a dedicated realtime service.
Generated Reference
- Public websocket AsyncAPI reference:
/reference/ws - Raw public AsyncAPI spec:
/asyncapi-ws.json - Exact generated stream/channel links remain stable, for example Public multiplexed stream and Instrument depth channel.
- Contributor-only backend references cover non-public realtime capabilities separately and are not published on the public docs route.
Base URLs
The current public deployment in this checkout is Monad testnet.
| Network | WebSocket URL |
|---|---|
| Testnet | wss://api.testnet.diffusal.xyz |
Connection Model
- The public multiplexed stream uses Binance-style
SUBSCRIBE/UNSUBSCRIBE/LIST_SUBSCRIPTIONScontrol messages. - RFQ auction broadcasts are available as the
global@rfqchannel on/ws/public. Hydrate first withGET /api/rfq/active-auctions. - The private multiplexed stream uses the same control envelope after a post-connect
AUTHmessage succeeds. - RFQ taker quotes are available as
rfq.quotes.<requestId>on/ws/private. Hydrate first withGET /api/rfq/quotes/:requestId. - Contributor-only backend references document non-public realtime authentication and routing details separately.
Multiplexed Streams
Public Multiplexed (/ws/public)
- Auth: none
- Methods:
SUBSCRIBE,UNSUBSCRIBE,LIST_SUBSCRIPTIONS - Market data channels only work on
/ws/public.
Public Channels
| Channel | Key Format | Description |
|---|---|---|
| Orderbook | <symbol>@depth | Snapshots and incremental diffs for a single market |
| Ticker | <symbol>@ticker | Best bid/ask, index price, open interest for a single market |
| Trades | <symbol>@trade | Executed trades for a single market |
| Grouped Orderbook | <underlying>@depth | Orderbook updates for all instruments under an underlying (e.g. btc@depth) |
| Grouped Ticker | <underlying>@ticker | Ticker updates for all instruments under an underlying |
| Grouped Trades | <underlying>@trade | Trade updates for all instruments under an underlying |
| Index | <pairSymbol>@index | Spot price and volatility for a trading pair (e.g. eth-usdt@index) |
| Markets | global@markets | Series creation and settlement lifecycle events |
| RFQ | global@rfq | Live RFQ auction lifecycle events (created, resolved) |
Example: Subscribe to Multiple Channels
{
"method": "SUBSCRIBE",
"params": ["btc-82000-c-1736409600@depth", "eth-usdt@index", "global@markets", "global@rfq"],
"id": 1
}Channel Payloads
Orderbook (@depth)
{
"stream": "btc-82000-c-1736409600@depth",
"data": {
"type": "snapshot",
"symbol": "BTC-82000-C-1736409600",
"bids": [{ "price": "0.045", "size": "10" }],
"asks": [{ "price": "0.048", "size": "5" }],
"sequence": 1,
"timestamp": 1711612800000
}
}Subsequent updates use "type": "diff" with only changed levels. Use sequence to reconcile with REST snapshots.
Ticker (@ticker)
{
"stream": "btc-82000-c-1736409600@ticker",
"data": {
"symbol": "BTC-82000-C-1736409600",
"indexPrice": "82150.00",
"bestBid": "0.045",
"bestAsk": "0.048",
"bestBidSize": "10",
"bestAskSize": "5",
"openInterest": "150",
"lastTradeAt": 1711612800
}
}Mark price and Greeks are computed client-side using @diffusal/algorithms with oracle data from the @index channel.
Trades (@trade)
{
"stream": "btc-82000-c-1736409600@trade",
"data": {
"id": "abc123",
"symbol": "BTC-82000-C-1736409600",
"price": "0.046",
"size": "5",
"side": "buy",
"timestamp": 1711612800
}
}Index (@index)
{
"stream": "eth-usdt@index",
"data": {
"spotPrice": "2159.50",
"volatility": "0.65",
"timestamp": 1711612800
}
}Provides the inputs needed for client-side Black-Scholes computation (spot price and implied volatility).
Markets (global@markets)
{
"stream": "global@markets",
"data": {
"type": "series_created",
"seriesId": "0xabc...",
"pairId": "0x123...",
"expiry": 1711612800,
"strike": "82000000000000000000000",
"isCall": true
}
}Events: series_created (new option series listed) and series_settled (series expired and settled, includes settlementPrice).
Private Multiplexed (/ws/private)
- Auth: connect first, then send an
AUTHcontrol message - Channels:
position,order,fill,collateral,rfq,liquidation,portfolio,rfq.quotes.<requestId> - Methods:
AUTH,SUBSCRIBE,UNSUBSCRIBE,LIST_SUBSCRIPTIONS - Market data channels are not supported here; sending
BTC@depthafter auth will be rejected as an unknown private channel.
For detailed private WebSocket documentation including full channel schemas, message formats, and code examples, see Private WebSocket.
RFQ Broadcast (/ws/rfq/stream)
- Auth: none
- Public stream of RFQ auction lifecycle events. Clients receive an immediate connected event, a snapshot of currently active auctions, then live auction create/resolve events and periodic heartbeats.
- Use this dedicated endpoint for focused RFQ monitoring. Alternatively, subscribe to
global@rfqon/ws/publicfor multiplexed access alongside other market data channels.
RFQ Taker Quotes (/ws/rfq/quotes)
- Auth: SIWE bearer token (passed via
authorizationheader orcookieon handshake) - Per-request stream of quotes for a taker's active RFQ auction. Pass
requestIdvia handshake query/headers. - Payloads:
connected,heartbeat,quote(new quote received),auction_resolved
RFQ MM Stream (/ws/rfq/mm)
- Auth: SIWE or API key with
tradescope; market makers must be MMM-enabled - Payloads:
auth_success,heartbeat,rfq_request,quote_rejected,trade_filled
WebSocket Rate Limits
All public WebSocket connections are rate-limited:
| Limit | Public value |
|---|---|
| Connections/min | 60 |
| Messages/min | 300 |
| Max subscriptions | 50 |
- Connection limit exceeded — connection silently closed
- Message limit exceeded — error response (code
4029), connection stays open - Subscription limit exceeded — error response (code
4029), existing subscriptions unaffected
See Rate Limits for REST tier details and the public websocket caps.
Payload Notes
- Numeric values follow the same precision model as REST endpoints.
- Multiplexed streams wrap payloads in
{ "stream": "...", "data": ... }envelopes.
Reconnection Pattern
Use exponential backoff with jitter to handle disconnections gracefully. The server may close connections for rate limiting (code 4029), maintenance, or idle timeout.
function connectWithBackoff(url: string, maxRetries = 10) {
let attempt = 0;
function connect() {
const ws = new WebSocket(url);
ws.addEventListener("open", () => {
attempt = 0; // reset on successful connection
// Re-subscribe to channels after reconnect
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: ["btc-usdt@index", "global@rfq"],
id: 1,
}),
);
});
ws.addEventListener("close", (event) => {
if (attempt >= maxRetries) {
console.error("Max reconnection attempts reached");
return;
}
// Exponential backoff: 1s, 2s, 4s, 8s... capped at 30s
const baseDelay = Math.min(1000 * 2 ** attempt, 30_000);
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
attempt++;
console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${attempt})...`);
setTimeout(connect, delay);
});
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data as string);
// Handle messages...
});
}
connect();
}Subscribe-After-Auth Sequence
For the private stream (/ws/private), you must authenticate before subscribing to any channel. Sending a SUBSCRIBE before a successful AUTH will be rejected.
const ws = new WebSocket("wss://api.testnet.diffusal.xyz/ws/private");
ws.addEventListener("open", () => {
// Step 1: Authenticate with bearer token from SIWE flow
ws.send(
JSON.stringify({
method: "AUTH",
params: { token: "your-bearer-token" },
id: 1,
}),
);
});
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data as string);
if (msg.status === "authenticated") {
// Step 2: Now safe to subscribe to private channels
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: ["position", "order", "fill", "collateral"],
id: 2,
}),
);
}
if (msg.stream) {
// Step 3: Handle channel data
console.log(`[${msg.stream}]`, msg.data);
}
});Heartbeat / Ping-Pong
The server sends periodic ping frames at the WebSocket protocol level. Most client libraries respond with pong automatically. If you are using a raw WebSocket implementation, ensure your client responds to pings to avoid idle disconnection.
For application-level keepalive, you can send a LIST_SUBSCRIPTIONS message as a lightweight heartbeat:
// Send a heartbeat every 30 seconds to keep the connection alive
const heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ method: "LIST_SUBSCRIPTIONS", id: 99 }));
}
}, 30_000);
// Clean up on close
ws.addEventListener("close", () => {
clearInterval(heartbeatInterval);
});The server will respond with the current subscription list, confirming the connection is alive. If no response is received within 10 seconds, consider the connection stale and trigger a reconnect.