Diffusal

Private WebSocket

Authenticated private WebSocket streams for positions, orders, fills, collateral, RFQ, liquidation, portfolio, and RFQ taker quote events

The private multiplexed stream at /ws/private provides real-time account-level updates after bearer token authentication. For an overview of all WebSocket endpoints, see WebSocket Streams.


Base URL

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

Connection & Authentication Flow

The private stream requires authentication before any channel subscriptions are accepted. Connect first, then authenticate with a bearer token obtained from the SIWE auth flow.

Client                                Server
  |                                      |
  |--- WebSocket connect --------------->|
  |<-- connection open ------------------|
  |                                      |
  |--- AUTH { bearer token } ----------->|
  |<-- { status: "authenticated" } ------|
  |                                      |
  |--- SUBSCRIBE { channels } ---------->|
  |<-- ack ------------------------------|
  |<-- initial snapshots (per channel) --|
  |<-- live updates ---------------------|
  |                                      |
  |--- UNSUBSCRIBE { channels } -------->|
  |<-- ack ------------------------------|

Requirements:

  • Minimum scope: read
  • Auth methods: Bearer token (from SIWE session) or API key
  • The connection stays open without auth, but all SUBSCRIBE requests will be rejected until AUTH succeeds

Control Messages

All client-to-server messages follow the same envelope:

{
  "method": "METHOD_NAME",
  "params": [],
  "id": 1
}

The id field is optional but recommended for correlating responses.

AUTH

Authenticate the connection with a bearer token or API key.

{
  "method": "AUTH",
  "authorization": "Bearer eyJhbGciOiJIUzI1NiIs...",
  "id": 1
}

SUBSCRIBE

Subscribe to one or more private channels. Channels are subscribed atomically -- if any channel name is invalid, the entire request is rejected.

{
  "method": "SUBSCRIBE",
  "params": ["position", "order", "fill"],
  "id": 2
}

UNSUBSCRIBE

Unsubscribe from one or more channels.

{
  "method": "UNSUBSCRIBE",
  "params": ["position"],
  "id": 3
}

LIST_SUBSCRIPTIONS

Query the current set of active subscriptions.

{
  "method": "LIST_SUBSCRIPTIONS",
  "id": 4
}

Server Responses

Acknowledgement

Successful SUBSCRIBE/UNSUBSCRIBE:

{
  "result": null,
  "id": 2
}

Auth Success

{
  "result": {
    "status": "authenticated",
    "walletAddress": "0x1234...abcd",
    "scopes": ["read", "trade"]
  },
  "id": 1
}

Subscription List

Response to LIST_SUBSCRIPTIONS:

{
  "result": ["position", "order", "fill"],
  "id": 4
}

Error

{
  "error": {
    "code": 4001,
    "msg": "Authentication required"
  },
  "id": 2
}

Common error codes:

CodeMeaning
4001Authentication required
4003Insufficient scope
4009Unknown channel
4029Rate limit exceeded

Data Envelope

All channel data is wrapped in a stream envelope:

{
  "stream": "position",
  "data": { ... }
}

Heartbeat

Sent every 30 seconds to keep the connection alive:

{
  "data": {
    "type": "heartbeat",
    "timestamp": 1711612800000
  }
}

Private Channels

Eight channels are available on the private stream. Seven use flat channel names; one (rfq.quotes.<requestId>) is parameterized. After subscribing, you receive an initial snapshot of current state, followed by live updates as events occur.

MMM wallets should treat portfolioId on private stream payloads as an internal routing detail. Operationally, MMM cash lives in portfolio 0, while trading portfolios are auto-routed by series.

position

Real-time wallet-level position snapshots and updates. For MMM wallets, any portfolioId values in these payloads are internal routing details.

FieldTypeDescription
portfolioIdnumberPortfolio containing the position (internal routing detail for MMM wallets)
seriesIdstringSeries identifier (bytes32 hex)
symbolstringHuman-readable symbol (e.g. BTC-80000-C-1711612800)
optionBalancestringPosition size in WAD (18 decimals). Positive = long, negative = short
premiumBalancestringAccumulated premium in WAD
markPricestringCurrent Black-Scholes mark price in WAD
unrealizedPnlstringUnrealized P&L in WAD
{
  "stream": "position",
  "data": {
    "portfolioId": 1,
    "seriesId": "0xabc123...def",
    "symbol": "BTC-80000-C-1711612800",
    "optionBalance": "1000000000000000000",
    "premiumBalance": "-250000000000000000",
    "markPrice": "3200000000000000000000",
    "unrealizedPnl": "150000000000000000"
  }
}

order

Order status changes (placed, partially filled, fully filled, cancelled).

FieldTypeDescription
orderIdstringUnique order identifier
statusstringOrder status
filledstringFilled quantity in WAD
remainingstringRemaining quantity in WAD
{
  "stream": "order",
  "data": {
    "orderId": "0x789...abc",
    "status": "partially_filled",
    "filled": "500000000000000000",
    "remaining": "500000000000000000"
  }
}

fill

Trade execution events. Emitted each time an order or RFQ quote is filled.

FieldTypeDescription
tradeIdstringUnique trade identifier
orderIdstringAssociated order ID (absent for RFQ fills)
seriesIdstringSeries identifier (bytes32 hex)
symbolstringHuman-readable symbol
portfolioIdnumberPortfolio where the position landed (internal routing detail for MMM wallets)
sidestring"buy" or "sell"
pricestringFill price in WAD
sizestringFill size in WAD
feestringFee charged in WAD
timestampnumberUnix timestamp (milliseconds)
{
  "stream": "fill",
  "data": {
    "tradeId": "trade-001",
    "seriesId": "0xabc123...def",
    "symbol": "ETH-2500-P-1711612800",
    "portfolioId": 1,
    "side": "buy",
    "price": "120000000000000000000",
    "size": "1000000000000000000",
    "fee": "500000000000000000",
    "timestamp": 1711612800000
  }
}

collateral

Margin and equity updates for each portfolio. Emitted when deposits, withdrawals, or position changes affect margin health.

FieldTypeDescription
portfolioIdnumberPortfolio identifier (MMM wallets should treat this as internal routing detail)
depositstringTotal deposited collateral in WAD
equitystringCurrent equity in WAD
initialMarginstringInitial margin requirement in WAD
maintenanceMarginstringMaintenance margin requirement in WAD
maxWithdrawstringMaximum withdrawable amount in WAD
isHealthybooleanWhether the portfolio passes margin check
{
  "stream": "collateral",
  "data": {
    "portfolioId": 1,
    "deposit": "10000000000000000000000",
    "equity": "9850000000000000000000",
    "initialMargin": "3200000000000000000000",
    "maintenanceMargin": "1600000000000000000000",
    "maxWithdraw": "8250000000000000000000",
    "isHealthy": true
  }
}

rfq

RFQ quote status updates. Relevant for market makers tracking their submitted quotes.

FieldTypeDescription
quoteIdstringQuote identifier
seriesIdstringSeries identifier (bytes32 hex)
sidestring"buy" or "sell"
pricestringQuoted price in WAD
sizestringQuote size in WAD
statusstring"outbid", "filled", "expired", or "filled_early"
timestampnumberUnix timestamp (milliseconds)
{
  "stream": "rfq",
  "data": {
    "quoteId": "quote-abc-123",
    "seriesId": "0xabc123...def",
    "side": "sell",
    "price": "3100000000000000000000",
    "size": "2000000000000000000",
    "status": "filled",
    "timestamp": 1711612830000
  }
}

liquidation

Liquidation risk alerts. Emitted when margin health changes cross alert thresholds.

FieldTypeDescription
walletAddressstringWallet address (checksummed)
levelstringAlert level: "safe", "warning", "critical", or "liquidated"
marginRatiostringCurrent margin ratio in WAD
liquidationDistancestringDistance to liquidation threshold in WAD
maintenanceMarginstringMaintenance margin requirement in WAD
equitystringCurrent equity in WAD
timestampnumberUnix timestamp (milliseconds)
{
  "stream": "liquidation",
  "data": {
    "walletAddress": "0x1234...abcd",
    "level": "warning",
    "marginRatio": "1250000000000000000",
    "liquidationDistance": "500000000000000000000",
    "maintenanceMargin": "1600000000000000000000",
    "equity": "2100000000000000000000",
    "timestamp": 1711612850000
  }
}

portfolio

Portfolio creation and deletion events.

FieldTypeDescription
portfolioIdnumberPortfolio identifier
userstringWallet address (checksummed)
actionstring"created" or "deleted"
{
  "stream": "portfolio",
  "data": {
    "portfolioId": 3,
    "user": "0x1234...abcd",
    "action": "created"
  }
}

rfq.quotes.<requestId>

Live quote updates for a specific RFQ auction. This is a parameterized channel -- the <requestId> segment is the auction's request ID. The authenticated wallet must match the auction's taker address; subscribing to an auction you did not initiate returns an auth error.

Bootstrap first with GET /api/rfq/quotes/:requestId to hydrate existing quotes, then subscribe for live updates.

{
  "method": "SUBSCRIBE",
  "params": ["rfq.quotes.abc123-def456"],
  "id": 5
}
FieldTypeDescription
typestring"connected", "heartbeat", "quote_arrived", "quote_superseded", "auction_resolved", or "error"
requestIdstringAuction request ID
timestampnumberUnix timestamp (milliseconds)
mmAddressstringMarket maker address (on quote_arrived)
pricestringQuote price in WAD (on quote_arrived)
sizestringQuote size in WAD (on quote_arrived)
isBestbooleanWhether this is the current best quote (on quote_arrived)
winnerMmAddressstringWinning MM address (on auction_resolved)
winnerPricestringWinning price (on auction_resolved)
totalQuotesnumberTotal quotes received (on auction_resolved)

Quote arrived (new best quote):

{
  "stream": "rfq.quotes.abc123-def456",
  "data": {
    "type": "quote_arrived",
    "requestId": "abc123-def456",
    "timestamp": 1711612810000,
    "mmAddress": "0x9876...fedc",
    "price": "3150000000000000000000",
    "size": "1000000000000000000",
    "isBest": true
  }
}

Auction resolved:

{
  "stream": "rfq.quotes.abc123-def456",
  "data": {
    "type": "auction_resolved",
    "requestId": "abc123-def456",
    "timestamp": 1711612830000,
    "winnerMmAddress": "0x9876...fedc",
    "winnerPrice": "3150000000000000000000",
    "totalQuotes": 3
  }
}

Subscription Lifecycle

When you subscribe to a channel, the server delivers data in two phases:

  1. Initial snapshot -- Immediately after a successful SUBSCRIBE, the server sends the current state for each subscribed channel. For example, subscribing to position delivers all your current open positions as individual messages.

  2. Live updates -- After the snapshot, the server streams incremental updates as events occur on-chain or in the matching engine. Updates arrive as individual { stream, data } envelopes.

When you unsubscribe, the server stops sending updates for that channel. Resubscribing triggers a fresh snapshot.

SUBSCRIBE ["position", "collateral"]
    |
    +-- snapshot: position (msg 1 of N)
    +-- snapshot: position (msg 2 of N)
    +-- ...
    +-- snapshot: collateral (current state)
    |
    +-- live: position update (on trade)
    +-- live: collateral update (on margin change)
    +-- live: position update (on settlement)
    |
UNSUBSCRIBE ["position"]
    |
    +-- live: collateral update (still subscribed)

Rate Limits

LimitValue
Connections/min60
Messages/min300
Max subscriptions50 (configurable per tier)
  • 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 tier details.


Complete Example

const ws = new WebSocket("wss://api.testnet.diffusal.xyz/ws/private");

ws.addEventListener("open", () => {
  // Step 1: Authenticate
  ws.send(
    JSON.stringify({
      method: "AUTH",
      authorization: "Bearer eyJhbGciOiJIUzI1NiIs...",
      id: 1,
    }),
  );
});

ws.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);

  // Handle auth response
  if (msg.id === 1 && msg.result?.status === "authenticated") {
    console.log("Authenticated as", msg.result.walletAddress);

    // Step 2: Subscribe to channels
    ws.send(
      JSON.stringify({
        method: "SUBSCRIBE",
        params: ["position", "collateral", "fill", "liquidation"],
        id: 2,
      }),
    );
    return;
  }

  // Handle subscription ack
  if (msg.id === 2 && msg.result === null) {
    console.log("Subscribed successfully");
    return;
  }

  // Handle errors
  if (msg.error) {
    console.error(`Error ${msg.error.code}: ${msg.error.msg}`);
    return;
  }

  // Handle heartbeats
  if (msg.data?.type === "heartbeat") {
    return;
  }

  // Handle channel data
  if (msg.stream) {
    switch (msg.stream) {
      case "position":
        console.log("Position update:", msg.data.symbol, msg.data.optionBalance);
        break;
      case "collateral":
        console.log("Collateral:", msg.data.equity, "healthy:", msg.data.isHealthy);
        break;
      case "fill":
        console.log("Fill:", msg.data.side, msg.data.size, "@", msg.data.price);
        break;
      case "liquidation":
        console.log("Liquidation alert:", msg.data.level);
        break;
    }
  }
});

ws.addEventListener("close", (event) => {
  console.log("Connection closed:", event.code, event.reason);
});

See Also

On this page