Diffusal

WebSocket API

Real-time data streams specification

The WebSocket API provides real-time updates for market data and user account information.

Note: All trading pairs are denominated against USDC (e.g., BTC-USDC, ETH-USDC). USDC is used as the collateral currency and all settlements are in USDC.


Connection

Endpoint

EnvironmentURL
Localws://localhost:8080/ws
Productionwss://api.diffusal.xyz/ws

Connection Lifecycle

CLIENT                                  SERVER
  │                                       │
  ├── WebSocket Connect ─────────────────►│
  │                                       │
  │◄── Connection Acknowledged ───────────┤
  │    { type: "connected", timestamp }   │
  │                                       │
  ├── Subscribe to Channels ─────────────►│
  │                                       │
  │◄── Subscription Confirmed ────────────┤
  │                                       │
  │    ┌─────────────────────────────┐    │
  │    │ Continuous Real-time Updates│    │
  │◄───│ (trades, orderbook, tickers)│────┤
  │    └─────────────────────────────┘    │
  │                                       │
  ├── Unsubscribe ───────────────────────►│
  │                                       │
  ├── Close Connection ──────────────────►│
  │                                       │

Message Format

All messages use JSON format with a type field to identify the message type.

Client → Server

FieldTypeDescription
typestringMessage type
idstringRequest ID (for correlation)
...variesType-specific fields

Server → Client

FieldTypeDescription
typestringMessage type
idstringRequest ID (for subscription confirmations)
channelstringChannel name (for snapshot and update types)
dataobjectPayload data (for snapshot and update types)
timestampnumberServer timestamp (milliseconds)

Note: Data messages (snapshot and update) wrap the payload in a data field along with channel and timestamp.


Authentication

Private channels require authentication. Authenticate after connecting, before subscribing to private channels.

Obtaining the JWT Token

The SIWE authentication flow establishes a session via HTTP cookie. The JWT token for WebSocket authentication is obtained from this session:

EnvironmentJWT Source
BrowserAutomatically included with WebSocket connection via cookie; manual auth message optional
Non-browserExtract from session cookie or Authorization header from prior SIWE request

Extracting JWT from SIWE Session

For non-browser environments (Node.js, mobile apps, etc.), extract the JWT token from the SIWE verification response:

import { createSiweMessage } from "viem/siwe";

const API_URL = "https://api.diffusal.xyz";

// Step 1: Request nonce
const nonceRes = await fetch(
  `${API_URL}/api/auth/siwe/nonce?walletAddress=${walletAddress}`
);
const { nonce } = await nonceRes.json();

// Step 2: Create and sign SIWE message
const message = createSiweMessage({
  domain: "diffusal.xyz", // Use 'localhost' for local development
  address: walletAddress,
  statement: "Sign in to Diffusal",
  uri: API_URL,
  version: "1",
  chainId: chainId,
  nonce: nonce,
});

const signature = await walletClient.signMessage({
  account: walletAddress,
  message: message,
});

// Step 3: Verify and get session
const verifyRes = await fetch(`${API_URL}/api/auth/siwe/verify`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, signature }),
  credentials: "include",
});

// Step 4: Extract JWT from Set-Cookie header
const setCookie = verifyRes.headers.get("set-cookie");
const sessionToken = setCookie?.match(/session=([^;]+)/)?.[1];

if (!sessionToken) {
  throw new Error("Failed to extract session token");
}

// Step 5: Use token in WebSocket auth message
const ws = new WebSocket("wss://api.diffusal.xyz/ws");

ws.onopen = () => {
  ws.send(
    JSON.stringify({
      type: "auth",
      token: sessionToken,
    })
  );
};

Note: The session token is valid for 7 days. Store it securely and refresh before expiry.

Auth Message

Send after connecting for non-browser environments or explicit authentication:

FieldTypeDescription
type"auth"Message type
tokenstringJWT token from SIWE session

Auth Response

Success:

FieldTypeDescription
type"auth_success"Auth succeeded
addressstringAuthenticated address

Failure:

FieldTypeDescription
type"auth_error"Auth failed
errorstringError message

Subscription Management

Subscribe

FieldTypeDescription
type"subscribe"Message type
idstringRequest ID
channelsarrayChannel names to subscribe

Unsubscribe

FieldTypeDescription
type"unsubscribe"Message type
idstringRequest ID
channelsarrayChannel names to unsubscribe

Subscription Response

FieldTypeDescription
type"subscribed" / "unsubscribed"Response type
idstringRequest ID
channelsarrayAffected channels

Public Channels

No authentication required.

orderbook:{symbol}

Real-time order book updates for a series.

Channel Name: orderbook:BTC-82000-C-1736409600

Snapshot Message (on subscribe):

{
  "type": "snapshot",
  "channel": "orderbook:BTC-82000-C-1736409600",
  "data": {
    "bids": [{ "price": "1000000000000000000", "size": "5000000000000000000" }],
    "asks": [{ "price": "1100000000000000000", "size": "3000000000000000000" }]
  },
  "timestamp": 1706500000000
}

Update Message:

{
  "type": "update",
  "channel": "orderbook:BTC-82000-C-1736409600",
  "data": {
    "bids": [{ "price": "1000000000000000000", "size": "6000000000000000000" }],
    "asks": []
  },
  "timestamp": 1706500001000
}

Level Format (in data.bids / data.asks):

FieldTypeDescription
pricestringPrice level (WAD)
sizestringNew size (0 = removed)

trades:{symbol}

Real-time trade notifications for a series.

Channel Name: trades:BTC-82000-C-1736409600

Trade Message:

{
  "type": "update",
  "channel": "trades:BTC-82000-C-1736409600",
  "data": {
    "id": "0x...",
    "price": "1050000000000000000",
    "size": "1000000000000000000",
    "side": "buy",
    "timestamp": 1706500000
  },
  "timestamp": 1706500000000
}

Trade Data Fields:

FieldTypeDescription
idstringTrade ID
pricestringExecution price (WAD)
sizestringTrade size (WAD)
sidestringTaker side ("buy"/"sell")
timestampnumberExecution time (seconds)

ticker:{symbol}

Real-time ticker updates (prices, Greeks, stats).

Channel Name: ticker:BTC-82000-C-1736409600

Ticker Message:

{
  "type": "update",
  "channel": "ticker:BTC-82000-C-1736409600",
  "data": {
    "markPrice": "1050000000000000000",
    "indexPrice": "95000000000000000000000",
    "bestBid": "1000000000000000000",
    "bestAsk": "1100000000000000000",
    "delta": "550000000000000000",
    "gamma": "1000000000000000",
    "vega": "50000000000000000000",
    "theta": "-10000000000000000",
    "rho": "5000000000000000",
    "iv": "800000000000000000",
    "volume24h": "1000000000000",
    "trades24h": 42
  },
  "timestamp": 1706500000000
}

Ticker Data Fields:

FieldTypeDescription
markPricestringMark price (WAD)
indexPricestringSpot price (WAD)
bestBidstringBest bid (WAD or null)
bestAskstringBest ask (WAD or null)
deltastringDelta (WAD)
gammastringGamma (WAD)
vegastringVega (WAD)
thetastringTheta (WAD)
rhostringRho (WAD)
ivstringImplied volatility
volume24hstring24h volume (USDC)
trades24hnumber24h trade count

oracle:{pairId}

Real-time oracle price updates.

Channel Name: oracle:0x... (pairId as bytes32)

Oracle Message:

{
  "type": "update",
  "channel": "oracle:0x...",
  "data": {
    "pairId": "0x...",
    "spotPrice": "95000000000000000000000",
    "volatility": "800000000000000000",
    "timestamp": 1706500000
  },
  "timestamp": 1706500000000
}

Oracle Data Fields:

FieldTypeDescription
pairIdstringPair identifier
spotPricestringSpot price (WAD)
volatilitystringIV (WAD)
timestampnumberOracle timestamp (secs)

markets

Market-wide updates (new series, settlements).

Channel Name: markets

Series Created:

{
  "type": "update",
  "channel": "markets",
  "data": {
    "event": "series_created",
    "seriesId": "0x...",
    "symbol": "BTC-82000-C-1736409600",
    "pairId": "0x...",
    "strike": "100000000000000000000000",
    "expiry": 1735257600,
    "isCall": true
  },
  "timestamp": 1706500000000
}

Series Settled:

{
  "type": "update",
  "channel": "markets",
  "data": {
    "event": "series_settled",
    "seriesId": "0x...",
    "settlementPrice": "95000000000000000000000"
  },
  "timestamp": 1706500000000
}

Private Channels (Auth Required)

These channels require authentication. Authenticate before subscribing - subscribing without authentication returns an auth_required error.

positions

Position updates across all portfolios.

Channel Name: positions

Position Update:

{
  "type": "update",
  "channel": "positions",
  "data": {
    "portfolioId": 1,
    "seriesId": "0x...",
    "symbol": "BTC-82000-C-1736409600",
    "optionBalance": "1000000000000000000",
    "premiumBalance": "50000000000000000000",
    "markPrice": "1050000000000000000",
    "unrealizedPnl": "25000000000000000000"
  },
  "timestamp": 1706500000000
}

Position Data Fields:

FieldTypeDescription
portfolioIdnumberPortfolio ID
seriesIdstringSeries ID
symbolstringSeries symbol
optionBalancestringNew balance (WAD)
premiumBalancestringPremium balance
markPricestringCurrent mark (WAD)
unrealizedPnlstringPosition PnL (WAD)

collateral

Collateral and margin updates.

Channel Name: collateral

Collateral Update:

{
  "type": "update",
  "channel": "collateral",
  "data": {
    "portfolioId": 1,
    "deposit": "10000000000",
    "equity": "10500000000",
    "initialMargin": "2000000000",
    "maintenanceMargin": "1600000000",
    "maxWithdraw": "8500000000",
    "isHealthy": true
  },
  "timestamp": 1706500000000
}

Collateral Data Fields:

FieldTypeDescription
portfolioIdnumberPortfolio ID
depositstringUSDC deposit
equitystringTotal equity
initialMarginstringIM requirement
maintenanceMarginstringMM threshold
maxWithdrawstringMax withdrawable
isHealthybooleanHealth status

orders

Order status updates.

Channel Name: orders

Order Update:

{
  "type": "update",
  "channel": "orders",
  "data": {
    "orderId": "0x...",
    "status": "partially_filled",
    "filled": "500000000000000000",
    "remaining": "500000000000000000"
  },
  "timestamp": 1706500000000
}

Order Cancelled:

{
  "type": "update",
  "channel": "orders",
  "data": {
    "orderId": "0x...",
    "status": "cancelled",
    "reason": "user_cancelled"
  },
  "timestamp": 1706500000000
}

Order Data Fields:

FieldTypeDescription
orderIdstringOrder ID
statusstringNew status
filledstringFilled amount
remainingstringRemaining amount
reasonstringCancellation reason (if cancelled)

fills

Trade fill notifications for the user.

Channel Name: fills

Fill Message:

{
  "type": "update",
  "channel": "fills",
  "data": {
    "tradeId": "0x...",
    "orderId": "0x...",
    "seriesId": "0x...",
    "symbol": "BTC-82000-C-1736409600",
    "portfolioId": 1,
    "side": "buy",
    "price": "1050000000000000000",
    "size": "1000000000000000000",
    "fee": "1000000",
    "timestamp": 1706500000
  },
  "timestamp": 1706500000000
}

Fill Data Fields:

FieldTypeDescription
tradeIdstringTrade ID
orderIdstringOrder ID (if limit order)
seriesIdstringSeries ID
symbolstringSeries symbol
portfolioIdnumberPortfolio ID
sidestring"buy" or "sell"
pricestringFill price (WAD)
sizestringFill size (WAD)
feestringFee amount (USDC)
timestampnumberFill time (seconds)

Heartbeat

The server sends periodic heartbeats to keep connections alive.

Heartbeat Interval: 15 seconds

Heartbeat Message (Server → Client):

FieldTypeDescription
type"heartbeat"Heartbeat
timestampnumberServer time

Pong Message (Client → Server):

FieldTypeDescription
type"pong"Heartbeat reply

Clients should respond with a pong message. Connections are closed after 30 seconds of inactivity (no messages or pongs received).


Message Ordering

Subscription Flow

When subscribing to a channel, messages arrive in this order:

  1. Subscribe request sent by client
  2. Snapshot sent by server (current state of the channel)
  3. Subscription confirmation (subscribed message)
  4. Update messages (subsequent changes)

Ordering Guarantees

Message TypeOrdering Guarantee
SnapshotsSent before subscription confirmation
UpdatesApproximate chronological order; strict ordering not guaranteed
Heartbeats15 second intervals

Note: Updates reflect state changes since the snapshot. In fast-moving markets, some updates may arrive out of order. Use the timestamp field for sequencing.


Error Handling

Error Message

FieldTypeDescription
type"error"Error occurred
idstringRequest ID (if applicable)
codestringError code
messagestringError message

Error Codes

CodeDescription
invalid_messageMalformed message
unknown_channelChannel does not exist
auth_requiredChannel requires auth
rate_limitedToo many requests
internal_errorServer error

Connection Limits

LimitValue
Max connections per IP10
Max subscriptions per connection50
Max message size16 KB
Heartbeat interval15 seconds
Idle timeout30 seconds
Message rate limit100/second

Reconnection Strategy

WebSocket connections may disconnect due to network issues, server restarts, or the 30-second idle timeout. Implement automatic reconnection for production applications.

class DiffusalWebSocket {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private baseDelay = 1000; // 1 second
  private subscriptions = new Set<string>();
  private token: string | null = null;
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  connect(token?: string) {
    this.token = token ?? this.token;
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log("WebSocket connected");
      this.reconnectAttempts = 0;

      // Re-authenticate if token exists
      if (this.token) {
        this.ws?.send(
          JSON.stringify({
            type: "auth",
            token: this.token,
          })
        );
      }
    };

    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);

      // Re-subscribe after successful auth
      if (msg.type === "auth_success" && this.subscriptions.size > 0) {
        this.ws?.send(
          JSON.stringify({
            type: "subscribe",
            id: "resub-" + Date.now(),
            channels: Array.from(this.subscriptions),
          })
        );
      }

      // Respond to heartbeats
      if (msg.type === "heartbeat") {
        this.ws?.send(JSON.stringify({ type: "pong" }));
      }
    };

    this.ws.onclose = (event) => {
      if (event.code !== 1000) {
        // Not a clean close
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };
  }

  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error("Max reconnection attempts reached");
      return;
    }

    // Exponential backoff: 1s, 2s, 4s, 8s, 16s
    const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts);
    this.reconnectAttempts++;

    console.log(
      `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
    );
    setTimeout(() => this.connect(), delay);
  }

  subscribe(channels: string[]) {
    channels.forEach((ch) => this.subscriptions.add(ch));

    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(
        JSON.stringify({
          type: "subscribe",
          id: "sub-" + Date.now(),
          channels: channels,
        })
      );
    }
  }

  unsubscribe(channels: string[]) {
    channels.forEach((ch) => this.subscriptions.delete(ch));

    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(
        JSON.stringify({
          type: "unsubscribe",
          id: "unsub-" + Date.now(),
          channels: channels,
        })
      );
    }
  }

  close() {
    this.ws?.close(1000); // Clean close
  }
}

// Usage
const ws = new DiffusalWebSocket("wss://api.diffusal.xyz/ws");
ws.connect(sessionToken);
ws.subscribe(["orderbook:BTC-82000-C-1736409600", "positions"]);

Reconnection Checklist

StepActionPurpose
1Use exponential backoffAvoid overwhelming server
2Re-authenticateJWT may still be valid (7-day lifetime)
3Re-subscribe to channelsRestore previous subscription state
4Handle snapshotsData may have changed during disconnect

Handling Idle Timeout

The server closes connections after 30 seconds of inactivity. To prevent disconnection:

  • Respond to heartbeat messages with pong
  • Heartbeats are sent every 15 seconds
  • If you miss 2 heartbeats, the connection closes

Example Flow

Public Market Data

1. Connect to ws://localhost:8080/ws

2. Receive: { "type": "connected" }

3. Send: {
     "type": "subscribe",
     "id": "1",
     "channels": ["orderbook:BTC-82000-C-1736409600", "trades:BTC-82000-C-1736409600"]
   }

4. Receive: {
     "type": "subscribed",
     "id": "1",
     "channels": ["orderbook:BTC-82000-C-1736409600", "trades:BTC-82000-C-1736409600"]
   }

5. Receive: {
     "type": "snapshot",
     "channel": "orderbook:BTC-82000-C-1736409600",
     "data": { "bids": [...], "asks": [...] }
   }

6. Receive updates as they occur...

Private Account Data

1. Connect to ws://localhost:8080/ws

2. Receive: { "type": "connected" }

3. Send: {
     "type": "auth",
     "token": "eyJhbGciOiJIUzI1NiIs..."
   }

4. Receive: {
     "type": "auth_success",
     "address": "0x..."
   }

5. Send: {
     "type": "subscribe",
     "id": "2",
     "channels": ["positions", "fills", "collateral"]
   }

6. Receive: { "type": "subscribed", "id": "2", "channels": [...] }

7. Receive position/fill/collateral updates as they occur...

On this page