WebSocket API

The Dualis Finance WebSocket API provides real-time data streams for prices, pool updates, position changes, liquidation events, governance activity, and user notifications. The endpoint is powered by Fastify's built-in WebSocket support.

Endpoint

WebSocket URL
GET ws://localhost:4000/v1/ws

In production, use the secure variant: wss://api.dualis.finance/v1/ws. The server accepts standard WebSocket upgrade requests.

Authentication

After establishing the WebSocket connection, the client must send an authentication message within 30 seconds. Connections that fail to authenticate within this window are terminated automatically.

Auth Message
{
  "type": "auth",
  "token": "your-jwt-token"
}

On successful authentication, the server responds with:

Auth Response
{
  "type": "auth",
  "status": "ok",
  "userId": "usr_abc123"
}
Auth Timeout
The server enforces a strict 30-second authentication window. If your client does not send the auth message in time, the connection is closed with WebSocket close code 4001.

Subscribing to Channels

Once authenticated, subscribe to one or more data channels. Each subscription message specifies a channel name and optional filter parameters:

Subscribe Message
{
  "type": "subscribe",
  "channel": "prices",
  "params": {
    "assets": ["ETH", "BTC", "USDC"]
  }
}

To unsubscribe from a channel:

Unsubscribe Message
{
  "type": "unsubscribe",
  "channel": "prices"
}

Available Channels

ChannelDescriptionUpdate Frequency
pricesReal-time asset price updates from oracle feeds~1s per asset
poolsPool state changes: utilization, rates, TVLOn change (typically every block)
positionsUser position updates: supply balances, borrow balances, health factorsOn change
liquidationsLiquidation events: trigger, auction start, settlementOn event
governanceProposal creation, voting updates, execution resultsOn event
notificationsUser-specific alerts: health factor warnings, reward claims, system noticesOn event

Message Format

All server-sent messages follow a consistent envelope structure:

Server Message
{
  "type": "data",
  "channel": "prices",
  "timestamp": "2026-02-28T10:30:00.000Z",
  "data": {
    "asset": "ETH",
    "price": "3245.67",
    "change24h": -0.0182,
    "source": "oracle_aggregated"
  }
}

Keepalive

The server sends WebSocket ping frames every 30 seconds. Clients must respond with pong frames (most WebSocket libraries handle this automatically). If the server receives no pong within 30 seconds, it considers the connection dead and closes it.

Clients can also send application-level heartbeat messages:

Heartbeat
{
  "type": "ping"
}

The server responds with:

Heartbeat Response
{
  "type": "pong",
  "timestamp": "2026-02-28T10:30:15.000Z"
}

Client Example

A complete TypeScript client that connects, authenticates, subscribes to price updates, and handles reconnection:

ws-client.ts
const WS_URL = 'ws://localhost:4000/v1/ws';
const AUTH_TOKEN = 'your-jwt-token';

let ws: WebSocket;
let reconnectTimer: ReturnType<typeof setTimeout>;

function connect() {
  ws = new WebSocket(WS_URL);

  ws.onopen = () => {
    // Authenticate within 30s window
    ws.send(JSON.stringify({ type: 'auth', token: AUTH_TOKEN }));
  };

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

    switch (msg.type) {
      case 'auth':
        if (msg.status === 'ok') {
          // Subscribe to channels after auth
          ws.send(JSON.stringify({
            type: 'subscribe',
            channel: 'prices',
            params: { assets: ['ETH', 'BTC'] },
          }));
          ws.send(JSON.stringify({
            type: 'subscribe',
            channel: 'positions',
          }));
        }
        break;

      case 'data':
        console.log(`[${msg.channel}]`, msg.data);
        break;

      case 'error':
        console.error('WS error:', msg.message);
        break;
    }
  };

  ws.onclose = (event) => {
    console.log(`Connection closed: ${event.code}`);
    // Reconnect after 3 seconds
    reconnectTimer = setTimeout(connect, 3000);
  };

  ws.onerror = () => {
    ws.close();
  };
}

connect();
React Integration
In the Dualis frontend, WebSocket state is managed by a Zustand 5 store that handles connection lifecycle, automatic reconnection, and channel subscriptions. Components subscribe to specific slices of the store for efficient re-rendering.

Error Messages

The server sends error messages for invalid requests or server-side issues:

Close CodeReason
4001Authentication timeout (30s exceeded)
4002Invalid or expired JWT token
4003Unknown channel name
4004Rate limit exceeded
1011Internal server error