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
| Network | WebSocket URL |
|---|---|
| Testnet | wss://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:
| Code | Meaning |
|---|---|
4001 | Authentication required |
4003 | Insufficient scope |
4009 | Unknown channel |
4029 | Rate 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.
| Field | Type | Description |
|---|---|---|
portfolioId | number | Portfolio containing the position (internal routing detail for MMM wallets) |
seriesId | string | Series identifier (bytes32 hex) |
symbol | string | Human-readable symbol (e.g. BTC-80000-C-1711612800) |
optionBalance | string | Position size in WAD (18 decimals). Positive = long, negative = short |
premiumBalance | string | Accumulated premium in WAD |
markPrice | string | Current Black-Scholes mark price in WAD |
unrealizedPnl | string | Unrealized 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).
| Field | Type | Description |
|---|---|---|
orderId | string | Unique order identifier |
status | string | Order status |
filled | string | Filled quantity in WAD |
remaining | string | Remaining 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.
| Field | Type | Description |
|---|---|---|
tradeId | string | Unique trade identifier |
orderId | string | Associated order ID (absent for RFQ fills) |
seriesId | string | Series identifier (bytes32 hex) |
symbol | string | Human-readable symbol |
portfolioId | number | Portfolio where the position landed (internal routing detail for MMM wallets) |
side | string | "buy" or "sell" |
price | string | Fill price in WAD |
size | string | Fill size in WAD |
fee | string | Fee charged in WAD |
timestamp | number | Unix 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.
| Field | Type | Description |
|---|---|---|
portfolioId | number | Portfolio identifier (MMM wallets should treat this as internal routing detail) |
deposit | string | Total deposited collateral in WAD |
equity | string | Current equity in WAD |
initialMargin | string | Initial margin requirement in WAD |
maintenanceMargin | string | Maintenance margin requirement in WAD |
maxWithdraw | string | Maximum withdrawable amount in WAD |
isHealthy | boolean | Whether 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.
| Field | Type | Description |
|---|---|---|
quoteId | string | Quote identifier |
seriesId | string | Series identifier (bytes32 hex) |
side | string | "buy" or "sell" |
price | string | Quoted price in WAD |
size | string | Quote size in WAD |
status | string | "outbid", "filled", "expired", or "filled_early" |
timestamp | number | Unix 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.
| Field | Type | Description |
|---|---|---|
walletAddress | string | Wallet address (checksummed) |
level | string | Alert level: "safe", "warning", "critical", or "liquidated" |
marginRatio | string | Current margin ratio in WAD |
liquidationDistance | string | Distance to liquidation threshold in WAD |
maintenanceMargin | string | Maintenance margin requirement in WAD |
equity | string | Current equity in WAD |
timestamp | number | Unix 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.
| Field | Type | Description |
|---|---|---|
portfolioId | number | Portfolio identifier |
user | string | Wallet address (checksummed) |
action | string | "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
}| Field | Type | Description |
|---|---|---|
type | string | "connected", "heartbeat", "quote_arrived", "quote_superseded", "auction_resolved", or "error" |
requestId | string | Auction request ID |
timestamp | number | Unix timestamp (milliseconds) |
mmAddress | string | Market maker address (on quote_arrived) |
price | string | Quote price in WAD (on quote_arrived) |
size | string | Quote size in WAD (on quote_arrived) |
isBest | boolean | Whether this is the current best quote (on quote_arrived) |
winnerMmAddress | string | Winning MM address (on auction_resolved) |
winnerPrice | string | Winning price (on auction_resolved) |
totalQuotes | number | Total 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:
-
Initial snapshot -- Immediately after a successful SUBSCRIBE, the server sends the current state for each subscribed channel. For example, subscribing to
positiondelivers all your current open positions as individual messages. -
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
| Limit | Value |
|---|---|
| Connections/min | 60 |
| Messages/min | 300 |
| Max subscriptions | 50 (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
- WebSocket Streams -- Overview of all WebSocket endpoints
- Authentication -- SIWE session auth and bearer tokens
- Rate Limits -- Connection and message rate limits
- Margin System -- Margin calculations relevant to collateral and liquidation channels