Diffusal

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.

NetworkWebSocket URL
Testnetwss://api.testnet.diffusal.xyz

Connection Model

  • The public multiplexed stream uses Binance-style SUBSCRIBE/UNSUBSCRIBE/LIST_SUBSCRIPTIONS control messages.
  • RFQ auction broadcasts are available as the global@rfq channel on /ws/public. Hydrate first with GET /api/rfq/active-auctions.
  • The private multiplexed stream uses the same control envelope after a post-connect AUTH message succeeds.
  • RFQ taker quotes are available as rfq.quotes.<requestId> on /ws/private. Hydrate first with GET /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

ChannelKey FormatDescription
Orderbook<symbol>@depthSnapshots and incremental diffs for a single market
Ticker<symbol>@tickerBest bid/ask, index price, open interest for a single market
Trades<symbol>@tradeExecuted trades for a single market
Grouped Orderbook<underlying>@depthOrderbook updates for all instruments under an underlying (e.g. btc@depth)
Grouped Ticker<underlying>@tickerTicker updates for all instruments under an underlying
Grouped Trades<underlying>@tradeTrade updates for all instruments under an underlying
Index<pairSymbol>@indexSpot price and volatility for a trading pair (e.g. eth-usdt@index)
Marketsglobal@marketsSeries creation and settlement lifecycle events
RFQglobal@rfqLive 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 AUTH control 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@depth after 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@rfq on /ws/public for multiplexed access alongside other market data channels.

RFQ Taker Quotes (/ws/rfq/quotes)

  • Auth: SIWE bearer token (passed via authorization header or cookie on handshake)
  • Per-request stream of quotes for a taker's active RFQ auction. Pass requestId via handshake query/headers.
  • Payloads: connected, heartbeat, quote (new quote received), auction_resolved

RFQ MM Stream (/ws/rfq/mm)

  • Auth: SIWE or API key with trade scope; 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:

LimitPublic value
Connections/min60
Messages/min300
Max subscriptions50
  • 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.

See Also

On this page