WebSocket API

WebSocket API

Overview

The WebSocket API allows real-time communication with our platform. Use it to stream market data, subscribe to updates, and interact with the order book.

Key Identifier Types

The WebSocket API uses two primary identifier types throughout the trading system:

  • order_id: Unique identifier for individual limit orders. Generated as a 32-character hexadecimal string when a limit order is posted to the orderbook. Used for order management, cancellation, and tracking order lifecycle events.

  • trade_id: Unique identifier for executed trades. Generated as a 32-character hexadecimal string when a market order is filled against limit orders. Used for tracking individual trade executions and settlement records.

Relationship: Multiple trades can reference the same order_id when a single limit order is filled across multiple market orders (partial fills). Each trade execution generates a unique trade_id.

Key Timestamp Types

The WebSocket API uses two timestamp formats that are important to understand:

  • timestamp_ms: Event wrapper timestamps in milliseconds (13 digits) - used for event ordering and processing
  • timestamp: Data payload timestamps, usually in milliseconds (13 digits)
  • creation_timestamp: Order/request creation times in seconds (10 digits)
📖

Learn More: For detailed timestamp format explanations, see the Timestamp Formats section.

📖

Learn More: For detailed explanations and trading scenarios, see the Order ID vs Trade ID Guide.

EnvironmentWebSocket URL
Stagingwss://staging.kyan.sh/ws
Productionwss://api.kyan.sh/ws

Quick Start

  1. Connect to the WebSocket endpoint
  2. Authenticate with your API key
  3. Subscribe to market data channels
  4. Monitor your trades and account state
  5. Execute trades via REST API while monitoring via WebSocket

Supported Markets

  • Trading Pairs: BTC_USDC, ETH_USDC, ARB_USDC
  • Base Tokens: BTC, ETH, ARB
  • Instruments: Options and Perpetual Futures

Instrument Naming Format

Options

  • Format: {PAIR}-{MATURITY}-{STRIKE}-{TYPE}
  • Examples:
    • BTC_USDC-31OCT25-130000-C (BTC call option)
    • ETH_USDC-31OCT25-4700-P (ETH put option)
    • ARB_USDC-31OCT25-0d300-C (ARB call option with decimal strike)

Perpetuals

  • Format: {PAIR}-PERPETUAL
  • Examples: BTC_USDC-PERPETUAL, ETH_USDC-PERPETUAL, ARB_USDC-PERPETUAL

Format Components

  • PAIR: {BASE}_{QUOTE} where BASE is BTC, ETH, or ARB and QUOTE is USDC
  • MATURITY: DDMMMYY format (e.g., "31OCT25" for October 31, 2025)
    • Months: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
  • STRIKE: Integer for BTC/ETH, decimal notation with "d" for ARB (e.g., 1d500 for 1.5)
  • TYPE: C for Call options, P for Put options

Trading Workflow

1. Authentication

All WebSocket connections must be authenticated before accessing any data. Send an AUTH message immediately after connecting.

📖

Learn More: For detailed API key setup and management, see the API Key Guide.

import WebSocket from 'ws';

const wsUrl = 'wss://staging.kyan.sh/ws';
const API_KEY = 'your-api-key-here';

const authMessage = {
  type: 'auth',
  api_key: API_KEY,
};

const ws = new WebSocket(wsUrl);
ws.onopen = () => {
  console.log('WebSocket connection open');
  ws.send(JSON.stringify(authMessage));
  ws.onmessage = (event) => {
    const response = JSON.parse(event.data.toString());
    console.log('Authentication response:', response);
    if (response.success) {
      // Continue with subscriptions
    }
  };
};

2. Market Discovery

After authentication, discover available instruments using get_instruments:

// Get all available instruments for BTC
const message = {
  type: 'get_instruments',
  markets: ['BTC'],
};
ws.send(JSON.stringify(message));

3. Subscribe to Market Data

Subscribe to the channels you need for trading:

// Subscribe to instruments, index prices, and orderbook
const subscribeMessage = {
  type: 'subscribe',
  subscriptions: [
    {
      channel: 'instruments',
      query: { market: 'BTC' },
    },
    {
      channel: 'index_price',
      query: { pair: 'BTC_USDC' },
    },
    {
      channel: 'orderbook_perps',
      query: { pair: 'BTC_USDC' },
    },
  ],
};
ws.send(JSON.stringify(subscribeMessage));

4. Monitor Your Account

Track your trades and account activity:

// Subscribe to your account's trades and transfers
const accountSubscribe = {
  type: 'subscribe',
  subscriptions: [
    {
      channel: 'trade',
      query: { account: 'your-account-address' },
    },
    {
      channel: 'transfer',
      query: { account: 'your-account-address' },
    },
  ],
};
ws.send(JSON.stringify(accountSubscribe));

Available Channels

Channels are organized by their use in a trading workflow:

Market Data Channels

ChannelDescriptionQuery Parameters
index_priceUnderlying asset price updatespair: Trading pair (e.g., "BTC_USDC")
instrumentsAvailable instruments updatesmarket (optional): Base token (e.g., "BTC", "ETH", "ARB"). If omitted, subscribes to all markets
orderbook_optionsOption orderbook updatesinstrument_name or pair (optional), maturity, strike, type, direction, options (see Query Options below)
orderbook_perpsPerpetual orderbook updatesinstrument_name or pair (optional), direction, options (see Query Options below)
orderbook_makerMaker-specific orderbookmaker (required - Ethereum address), pair (optional)
fundingFunding rate updatesinstrument_name: Perpetual instrument name
interest_rateInterest rate changespair (required), expiry (optional)
ivImplied volatility updatespair (required), maturity (optional)

Account-Specific Channels

ChannelDescriptionQuery Parameters
tradeTrade execution eventsaccount (optional - EOA or Smart account address), pair (optional), direction (optional): "buy" or "sell"
transferDeposit and withdrawal eventsaccount (required - EOA or Smart account address), symbol (optional), type (optional): "deposit" or "withdrawal"
account_stateAccount state updatesaccount (required - EOA or Smart account address), pair (optional)
account_liquidationAccount liquidation eventsaccount (required - EOA or Smart account address), pair (optional)
bankruptcyAccount bankruptcy eventsmarket (optional): Trading pair filter
mmpMarket Maker Protection eventssmart_account_address (optional): Smart account address, pair (optional): Trading pair filter

Trading & Maker Channels

ChannelDescriptionQuery Parameters
rfqRequest-for-quote updatesaccount (optional): Smart account address, type (optional): "request" or "response", order_id (optional)
orderbook_makerMaker-specific orderbook eventsmaker (required): Ethereum address of the market maker, pair (optional): Trading pair filter

Query Options

Some channels support additional query options to control subscription behavior:

OptionTypeDefaultDescription
skip_snapshotbooleanfalseSkip the initial orderbook snapshot when subscribing. Only applies to orderbook_options and orderbook_perps channels. When set to true, you'll only receive incremental updates (post_order, cancel_order, trade events) without the initial state.

Example with Query Options:

{
  "channel": "orderbook_options",
  "query": {
    "pair": "BTC_USDC",
    "options": {
      "skip_snapshot": true
    }
  }
}

Event Types

When subscribed to channels, you'll receive events with the following types:

Event TypeDescriptionAssociated Channel
index_priceIndex price updateindex_price
instrumentsInstruments list updateinstruments
post_orderNew order postedorderbook_options, orderbook_perps, orderbook_maker
cancel_orderOrder cancellationorderbook_options, orderbook_perps, orderbook_maker
ob_maker_ordersMaker orders snapshotorderbook_maker
tradeTrade executiontrade
depositDeposit to accounttransfer
withdrawalWithdrawal from accounttransfer
fundingFunding rate updatefunding
interest_rateInterest rate updateinterest_rate
sviStochastic Volatility updateiv
rfq_requestNew RFQ requestrfq
rfq_post_responseNew response to RFQrfq
rfq_cancel_responseCancelled RFQ responserfq
account_stateAccount state updateaccount_state
account_liquidationLiquidation eventaccount_liquidation
bankruptcyAccount bankruptcy eventbankruptcy
mmp_triggeredMMP triggered eventmmp

Message Types

All WebSocket messages support an optional id field for request-response correlation. When provided, the server will include the same id in the response, allowing you to match requests with their corresponding responses.

Example with Request ID:

{
  "type": "auth",
  "id": "auth-request-1",
  "api_key": "your-api-key-here"
}

Response with Request ID:

{
  "kind": "response",
  "type": "auth",
  "id": "auth-request-1",
  "timestamp_ms": 1677721600000,
  "success": true
}

Authentication Messages

AUTH

Authenticate your WebSocket connection.

Request:

{
  "type": "auth",
  "api_key": "your-api-key-here", // Alphanumeric pattern: ^[a-zA-Z0-9_]+$
  "id": "optional-request-id"
}

Note: A valid API key must contain only letters, numbers, or underscores. (pattern: ^[a-zA-Z0-9_]+$).

📖

Learn More: For API key format requirements and troubleshooting, see the API Key Guide.

Success Response:

{
  "kind": "response",
  "type": "auth",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true
}

Error Responses:

  1. Invalid API key format:
{
  "kind": "response",
  "type": "error",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "invalid auth payload [{\"instancePath\":\"/api_key\",\"schemaPath\":\"#/properties/api_key/pattern\",\"keyword\":\"pattern\",\"params\":{\"pattern\":\"^[a-zA-Z0-9_]+$\"},\"message\":\"must match pattern \\\"^[a-zA-Z0-9_]+$\\\"\"}]"
}
  1. API key not found or invalid:
{
  "kind": "response",
  "type": "auth",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "NOT_FOUND"
}
  1. Other validation errors:
{
  "kind": "response",
  "type": "auth",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "Failed to validate api key"
}

Possible error codes:

  • NOT_FOUND - API key doesn't exist
  • FORBIDDEN - API key is disabled
  • KEY_USAGE_EXCEEDED - API key has exceeded usage limits
  • RATELIMITED - Too many requests
  • Session already authorized - Already authenticated on this connection

logout

End your current session.

Request:

{
  "type": "logout"
}

Response:

{
  "kind": "response",
  "type": "logout",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true
}

Market Data Messages

get_instruments

Retrieve available instruments for specified markets.

Request:

{
  "type": "get_instruments",
  "markets": ["BTC", "ETH", "ARB"]
}

Note: The markets array should contain base tokens only (not pairs).

Response:

{
  "kind": "response",
  "type": "get_instruments",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "instruments": [
    "BTC_USDC-31OCT25-130000-C",
    "BTC_USDC-PERPETUAL",
    "ETH_USDC-31OCT25-4700-P"
  ]
}

get_ob_state_by_instruments

Get the current orderbook state for specified instruments.

Request:

{
  "type": "get_ob_state_by_instruments",
  "instrument_names": ["BTC_USDC-31OCT25-130000-C", "BTC_USDC-PERPETUAL"]
}

Note: The instrument_names parameter is optional. If not provided, returns orderbook state for all instruments.

Response:

Both get_ob_state_by_instruments and get_ob_state_by_market return the same response format with type get_ob_state.

get_ob_state_by_market

Get the current orderbook state for all instruments in a specific market.

Request:

{
  "type": "get_ob_state_by_market",
  "market": "BTC",
  "maturity": "31OCT25"
}

Note: The maturity parameter is optional. If provided, filters instruments by expiration date.

Orderbook State Response Format

Both get_ob_state_by_instruments and get_ob_state_by_market requests return responses with type get_ob_state. The response structure varies by instrument type:

For Options:

{
  "kind": "response",
  "type": "get_ob_state",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "state": {
    "BTC_USDC-31OCT25-130000-C": {
      "timestamp": 1677721600000,
      "instrument_name": "BTC_USDC-31OCT25-130000-C",
      "index_price": 45000.5,
      "open_interest": 150.5,
      "last_price": 2500.0,
      "best_bid_price": 2495.0,
      "best_bid_amount": 1.5,
      "best_ask_price": 2505.0,
      "best_ask_amount": 1.0,
      "bids": [
        [2495.0, 1.5],
        [2490.0, 2.0]
      ],
      "asks": [
        [2505.0, 1.0],
        [2510.0, 1.8]
      ],
      "positions": 25,
      "mark_iv": 0.65,
      "bid_iv": 0.64,
      "ask_iv": 0.66,
      "interest_rate": 0.05,
      "settlement_price": 0,
      "greeks": {
        "delta": 0.45,
        "gamma": 0.0012,
        "theta": -45.5,
        "vega": 125.3,
        "rho": 85.2
      }
    }
  }
}

For Perpetuals:

{
  "kind": "response",
  "type": "get_ob_state",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "state": {
    "BTC_USDC-PERPETUAL": {
      "timestamp": 1677721600000,
      "instrument_name": "BTC_USDC-PERPETUAL",
      "index_price": 45000.5,
      "open_interest": 2500000,
      "last_price": 45010.0,
      "current_funding": 0.0001,
      "best_bid_price": 45000.0,
      "best_bid_amount": 20000,
      "best_ask_price": 45020.0,
      "best_ask_amount": 30000,
      "bids": [
        [45000.0, 20000],
        [44980.0, 40000]
      ],
      "asks": [
        [45020.0, 30000],
        [45040.0, 20000]
      ]
    }
  }
}

Subscription Management Messages

get_subscriptions

List your active subscriptions.

Request:

{
  "type": "get_subscriptions"
}

Response:

{
  "kind": "response",
  "type": "get_subscriptions",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "subscriptions": [
    {
      "channel": "index_price",
      "query": {
        "pair": "BTC_USDC"
      }
    },
    {
      "channel": "orderbook_options",
      "query": {
        "instrument_name": "BTC_USDC-31OCT25-130000-C"
      }
    }
  ]
}

subscribe

Subscribe to specific data channels.

Request:

{
  "type": "subscribe",
  "subscriptions": [
    {
      "channel": "index_price",
      "query": {
        "pair": "BTC_USDC"
      }
    },
    {
      "channel": "orderbook_options",
      "query": {
        "instrument_name": "BTC_USDC-31OCT25-130000-C"
      }
    }
  ]
}

Response:

{
  "kind": "response",
  "type": "subscribe",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "subscriptions": [
    {
      "channel": "index_price",
      "query": {
        "pair": "BTC_USDC"
      }
    },
    {
      "channel": "orderbook_options",
      "query": {
        "instrument_name": "BTC_USDC-31OCT25-130000-C"
      }
    }
  ]
}

unsubscribe

Unsubscribe from specific data channels.

Request:

{
  "type": "unsubscribe",
  "subscriptions": [
    {
      "channel": "index_price",
      "query": {
        "pair": "BTC_USDC"
      }
    }
  ]
}

Response:

{
  "kind": "response",
  "type": "unsubscribe",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true,
  "subscriptions": [
    {
      "channel": "index_price",
      "query": {
        "pair": "BTC_USDC"
      }
    }
  ]
}

unsubscribe_all

Unsubscribe from all data channels.

Request:

{
  "type": "unsubscribe_all"
}

Response:

{
  "kind": "response",
  "type": "unsubscribe_all",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": true
}

Subscription Filtering

Many channels support advanced filtering to reduce the volume of events you receive:

Orderbook Filtering

For orderbook_options and orderbook_perps channels, you can filter events by:

  • pair (optional): Subscribe to all instruments for a specific trading pair
  • direction: Filter for only "buy" or "sell" orders
  • options.skip_snapshot: Skip initial orderbook state, receive only incremental updates

Note: Maker filtering has been moved to the dedicated orderbook_maker channel (see below)

Example - Subscribe to all BTC options with buy orders:

{
  "channel": "orderbook_options",
  "query": {
    "pair": "BTC_USDC",
    "direction": "buy"
  }
}

Example - Subscribe to all perpetual markets without initial snapshots:

{
  "channel": "orderbook_perps",
  "query": {
    "options": {
      "skip_snapshot": true
    }
  }
}

Subscription Query Parameters

index_price Channel

Monitor real-time price updates for trading pairs.

{
  "channel": "index_price",
  "query": {
    "pair": "BTC_USDC"
  }
}

orderbook_options Channel

Stream option orderbook updates. Two formats are supported:

Using instrument name directly:

{
  "channel": "orderbook_options",
  "query": {
    "instrument_name": "BTC_USDC-31OCT25-130000-C",
    "direction": "buy",
    "options": {
      "skip_snapshot": true
    }
  }
}

Or using specific parameters (all optional for broad subscriptions):

{
  "channel": "orderbook_options",
  "query": {
    "pair": "BTC_USDC",
    "maturity": "31OCT25",
    "strike": 70000,
    "type": "C",
    "direction": "buy",
    "options": {
      "skip_snapshot": false
    }
  }
}

Note:

  • When using instrument_name, it must match the pattern: {PAIR}-{MATURITY}-{STRIKE}-{TYPE}
  • All parameters except instrument_name are optional when not using instrument name format
  • Use options.skip_snapshot to skip initial orderbook snapshot (default: false)

orderbook_perps Channel

Stream perpetual futures orderbook updates. Two formats are supported:

Using instrument name directly:

{
  "channel": "orderbook_perps",
  "query": {
    "instrument_name": "BTC_USDC-PERPETUAL",
    "direction": "buy",
    "options": {
      "skip_snapshot": true
    }
  }
}

Or using the pair (optional for broad subscription):

{
  "channel": "orderbook_perps",
  "query": {
    "pair": "BTC_USDC",
    "direction": "buy",
    "options": {
      "skip_snapshot": false
    }
  }
}

Note:

  • All parameters except instrument_name are optional when not using instrument name format
  • Use options.skip_snapshot to skip initial orderbook snapshot (default: false)

Note: When using instrument_name, it must match the pattern: {PAIR}-PERPETUAL

trade Channel

Track executed trades for specific accounts.

{
  "channel": "trade",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "pair": "BTC_USDC",
    "direction": "buy"
  }
}

Note:

  • The account address must include the 0x prefix and be exactly 42 characters total
  • Use the direction parameter for filtering trades by direction
  • Valid values for direction: "buy", "sell"

transfer Channel

Monitor deposits and withdrawals for an account.

{
  "channel": "transfer",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "symbol": "USDC",
    "type": "deposit"
  }
}

Note:

  • The symbol parameter is optional. Valid values: "ETH", "BTC", "USDC", "ARB"
  • The type parameter is optional. Valid values: "deposit", "withdrawal"

rfq Channel

Monitor RFQ (Request for Quote) requests and responses.

Query Parameters:

  • account (optional): Smart account address - filters RFQ events for this account
  • type (optional): "request" or "response" - filters by RFQ event type
  • order_id (optional): Specific RFQ order ID to monitor (32 hexadecimal characters)

Examples:

Monitor all RFQ requests from a specific account:

{
  "channel": "rfq",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "type": "request"
  }
}

Monitor all RFQ responses visible to an account:

{
  "channel": "rfq",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "type": "response"
  }
}

Monitor all RFQ requests (market maker perspective):

{
  "channel": "rfq",
  "query": {
    "type": "request"
  }
}

Monitor a specific RFQ order:

{
  "channel": "rfq",
  "query": {
    "order_id": "863509b1a865fa3f502a9f6784122dd7"
  }
}

Monitor all RFQ activity:

{
  "channel": "rfq",
  "query": {}
}

Note: The order_id must be exactly 32 hexadecimal characters. Use different query combinations based on your role (taker vs maker) and what RFQ events you need to monitor.

orderbook_maker Channel

Monitor orderbook events specifically for a market maker's orders. This dedicated channel provides:

  • Initial snapshot of all existing maker orders
  • Real-time updates for all orders posted/cancelled by the specified maker
  • Efficient tracking for market makers managing their own orders

Query Parameters:

  • maker (required): Ethereum address of the market maker (42 characters with 0x prefix)
  • pair (optional): Filter by trading pair (BTC_USDC, ETH_USDC, or ARB_USDC)

Examples:

Monitor all orders for a specific maker:

{
  "channel": "orderbook_maker",
  "query": {
    "maker": "0x1234567890abcdef1234567890abcdef12345678"
  }
}

Monitor maker orders for a specific trading pair:

{
  "channel": "orderbook_maker",
  "query": {
    "maker": "0x1234567890abcdef1234567890abcdef12345678",
    "pair": "BTC_USDC"
  }
}

Important Notes:

  • This channel replaces the previous maker filtering option in orderbook_options and orderbook_perps channels
  • Returns both options and perpetual orders for the specified maker
  • Initial snapshot event type is ob_maker_orders containing all active orders from the maker
  • After the initial snapshot, you'll receive real-time post_order and cancel_order events for that maker's activity
  • Designed for market makers to efficiently monitor their own order activity

funding Channel

Monitor funding rate updates for perpetual futures.

{
  "channel": "funding",
  "query": {
    "instrument_name": "BTC_USDC-PERPETUAL"
  }
}
📖

Learn More: For detailed information about funding rates and calculations, see the Funding Rate Specification.

interest_rate Channel

Track interest rate changes for trading pairs.

{
  "channel": "interest_rate",
  "query": {
    "pair": "BTC_USDC",
    "expiry": "31OCT25"
  }
}

Note: The expiry parameter is optional. If not provided, returns interest rate updates for all expiries.

iv Channel

Monitor implied volatility updates.

{
  "channel": "iv",
  "query": {
    "pair": "BTC_USDC",
    "maturity": "31OCT25"
  }
}

Note: The maturity parameter is optional. If not provided, returns IV updates for all maturities.

account_state Channel

Track account state changes.

{
  "channel": "account_state",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "pair": "BTC_USDC"
  }
}

Note: The pair parameter is optional. If not provided, returns account state updates for all pairs.

account_liquidation Channel

Monitor account liquidation events.

{
  "channel": "account_liquidation",
  "query": {
    "account": "0x1234567890abcdef1234567890abcdef12345678",
    "pair": "BTC_USDC"
  }
}

Note: The pair parameter is optional. If not provided, returns liquidation events for all pairs.

bankruptcy Channel

Monitor account bankruptcy events. Bankruptcy occurs when an account's equity becomes negative after liquidation attempts.

{
  "channel": "bankruptcy",
  "query": {
    "market": "BTC_USDC"
  }
}

Query Parameters:

  • market (optional): Trading pair (BTC_USDC, ETH_USDC, ARB_USDC, or USDC_USDC)

Examples:

Monitor bankruptcy events for a specific market:

{
  "channel": "bankruptcy",
  "query": {
    "market": "ETH_USDC"
  }
}

Monitor all bankruptcy events:

{
  "channel": "bankruptcy",
  "query": {}
}

mmp Channel

Monitor Market Maker Protection (MMP) triggered events. MMP temporarily freezes an account's trading when risk thresholds are exceeded.

{
  "channel": "mmp",
  "query": {
    "smart_account_address": "0x1234567890abcdef1234567890abcdef12345678",
    "pair": "BTC_USDC"
  }
}

Query Parameters:

  • smart_account_address (optional): Smart account address (42 characters with 0x prefix)
  • pair (optional): Trading pair (BTC_USDC, ETH_USDC, or ARB_USDC)

Examples:

Monitor MMP events for a specific account:

{
  "channel": "mmp",
  "query": {
    "smart_account_address": "0x1234567890abcdef1234567890abcdef12345678"
  }
}

Monitor MMP events for a specific trading pair:

{
  "channel": "mmp",
  "query": {
    "pair": "ETH_USDC"
  }
}

Monitor all MMP events:

{
  "channel": "mmp",
  "query": {}
}

Timestamp Formats

WebSocket events use consistent timestamp formatting across all message types:

Event Wrapper Timestamps:

  • timestamp_ms: Event generation time in milliseconds (Unix timestamp)
  • Format: 13-digit number (e.g., 1677721600000)
  • Location: Present in all event and response wrappers

Data Payload Timestamps:

  • timestamp: Data-specific timestamp, usually in milliseconds
  • creation_timestamp: Order/request creation time in seconds (Unix timestamp)
  • Format: Varies by field type:
    • timestamp: 13-digit number (milliseconds) - e.g., 1677721600000
    • creation_timestamp: 10-digit number (seconds) - e.g., 1677721600

Examples:

{
  "kind": "event",
  "type": "trade",
  "timestamp_ms": 1677721600000, // Event time (milliseconds)
  "data": {
    "timestamp": 1677721600000, // Trade time (milliseconds)
    "creation_timestamp": 1677721600 // Order creation (seconds)
  }
}

Important Notes:

  • Always use timestamp_ms for event processing and ordering
  • Parse creation_timestamp as seconds when working with order lifecycle data
  • All timestamps are UTC with no timezone offset

Event Data Structures

index_price Event

{
  "kind": "event",
  "type": "index_price",
  "timestamp_ms": 1677721600000,
  "data": {
    "value": 45000.5,
    "timestamp": 1677721600000
  },
  "subscription": {
    "channel": "index_price",
    "query": {
      "pair": "BTC_USDC"
    }
  }
}

instruments Event

Emitted when the list of available trading instruments is updated. This occurs when new instruments are added (e.g., new option expirations) or when instruments expire and are removed from the active list.

Event Data:

  • updated_at: Unix timestamp in milliseconds of when the instrument list was last updated
  • instruments: Array of instrument names currently available for trading

Subscription Example (Single Market):

{
  "type": "subscribe",
  "subscriptions": [
    {
      "channel": "instruments",
      "query": {
        "market": "BTC"
      }
    }
  ]
}

Subscription Example (All Markets):

{
  "type": "subscribe",
  "subscriptions": [
    {
      "channel": "instruments",
      "query": {}
    }
  ]
}

Event Example:

{
  "kind": "event",
  "type": "instruments",
  "timestamp_ms": 1677721600000,
  "data": {
    "updated_at": 1677721600000,
    "instruments": [
      "BTC_USDC-PERPETUAL",
      "BTC_USDC-31OCT25-130000-C",
      "BTC_USDC-31OCT25-130000-P",
      "BTC_USDC-31OCT25-135000-C",
      "BTC_USDC-31OCT25-135000-P",
      "BTC_USDC-30NOV25-140000-C",
      "BTC_USDC-30NOV25-140000-P"
    ]
  },
  "subscription": {
    "channel": "instruments",
    "query": {
      "market": "BTC"
    }
  }
}

Use Cases:

  • Monitor when new option expirations become available
  • Track when instruments expire and are removed
  • Keep your trading application's instrument list synchronized with the exchange
  • Receive immediate notification when new trading instruments become available

Notes:

  • Initial subscription provides a snapshot of all current instruments for the specified market(s)
  • Subsequent events are sent only when the instrument list changes
  • The instruments array includes both options and perpetuals for the specified market
  • If no market is specified in the query, you'll receive separate events for each market (BTC, ETH, ARB)

post_order Event

Emitted when a new order is posted to the orderbook.

{
  "kind": "event",
  "type": "post_order",
  "timestamp_ms": 1677721600000,
  "data": {
    "instrument_name": "BTC_USDC-31OCT25-130000-C",
    "type": "good_til_cancelled",
    "order_id": "unique-order-id",
    "order_state": "open",
    "filled_amount": 0,
    "price": 2500.0,
    "direction": "buy",
    "post_only": true,
    "maker": "0x1234567890abcdef1234567890abcdef12345678",
    "taker": "0x0000000000000000000000000000000000000000",
    "mmp": false,
    "liquidation": false,
    "creation_timestamp": 1677721600,
    "chain_id": 421614,
    "signature_deadline": 1677721700,
    "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
    "contracts": 1.5
  },
  "subscription": {
    "channel": "orderbook_options",
    "query": {
      "instrument_name": "BTC_USDC-31OCT25-130000-C"
    }
  }
}

cancel_order Event

Emitted when an order is cancelled.

{
  "kind": "event",
  "type": "cancel_order",
  "timestamp_ms": 1677721600000,
  "data": {
    "instrument_name": "BTC_USDC-31OCT25-130000-C",
    "type": "good_til_cancelled",
    "order_id": "unique-order-id",
    "order_state": "open",
    "filled_amount": 0,
    "price": 2640.0,
    "direction": "buy",
    "post_only": false,
    "maker": "0x39ff2600293a6746140c3caaa51bcd62dcb06cbf",
    "taker": "0x0000000000000000000000000000000000000000",
    "mmp": false,
    "liquidation": false,
    "creation_timestamp": 1677721600,
    "chain_id": 421614,
    "signature_deadline": 1677721700,
    "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
    "contracts": 4
  },
  "subscription": {
    "channel": "orderbook_options",
    "query": {
      "instrument_name": "BTC_USDC-31OCT25-130000-C"
    }
  }
}

trade Event

{
  "kind": "event",
  "type": "trade",
  "timestamp_ms": 1677721600000,
  "data": {
    "margin_account_id": 12345,
    "smart_account_address": "0x1234567890abcdef1234567890abcdef12345678",
    "trade_id": "1234567890abcdef1234567890abcdef",
    "order_id": "abcdef1234567890abcdef1234567890",
    "user_id": "trader_001",
    "size": 1.5,
    "order_size": 2.0,
    "filled_amount": 1.5,
    "timestamp": 1677721600000,
    "direction": "buy",
    "index_price": 45000.5,
    "instrument_name": "BTC_USDC-31OCT25-130000-C",
    "average_price": 2500.25,
    "limit_price": 2505.0,
    "liquidation": false,
    "iv": 0.65,
    "taker_fee": 0.0002,
    "maker_fee": 0.0001,
    "systemic_risk_fee": 0.0001,
    "liquidation_fee": 0,
    "realised_funding": 0
  },
  "subscription": {
    "channel": "trade",
    "query": {
      "account": "0x1234567890abcdef1234567890abcdef12345678"
    }
  }
}

Note: The order_size field indicates the total size of the original order, while filled_amount shows the cumulative amount filled so far. This allows tracking partial fills where filled_amount may be less than order_size.

deposit Event

{
  "kind": "event",
  "type": "deposit",
  "timestamp_ms": 1677721600000,
  "data": {
    "margin_account_id": 12345,
    "smart_account_address": "0x1234567890abcdef1234567890abcdef12345678",
    "amount": 10000,
    "action_seq": "abc123...",
    "action": "deposit",
    "symbol": "USDC"
  },
  "subscription": {
    "channel": "transfer",
    "query": {
      "account": "0x1234567890abcdef1234567890abcdef12345678"
    }
  }
}

withdrawal Event

{
  "kind": "event",
  "type": "withdrawal",
  "timestamp_ms": 1677721600000,
  "data": {
    "margin_account_id": 12345,
    "smart_account_address": "0x1234567890abcdef1234567890abcdef12345678",
    "amount": 5000,
    "action_seq": "xyz789...",
    "action": "withdrawal",
    "symbol": "USDC"
  },
  "subscription": {
    "channel": "transfer",
    "query": {
      "account": "0x1234567890abcdef1234567890abcdef12345678"
    }
  }
}

funding Event

{
  "kind": "event",
  "type": "funding",
  "timestamp_ms": 1677721600000,
  "data": {
    "instrument_name": "BTC_USDC-PERPETUAL",
    "timestamp": 1677721600000,
    "spot_price": 45000.5,
    "perp_price": 45010.0,
    "funding_rate": 0.0001
  },
  "subscription": {
    "channel": "funding",
    "query": {
      "instrument_name": "BTC_USDC-PERPETUAL"
    }
  }
}
📖

Learn More: For detailed information about funding rate calculations and payment mechanics, see the Funding Rate Specification.

interest_rate Event

{
  "kind": "event",
  "type": "interest_rate",
  "timestamp_ms": 1677721600000,
  "data": {
    "sid": "INTEREST-BTC_USDC",
    "v": 0.05,
    "timestamp": 1677721600000
  },
  "subscription": {
    "channel": "interest_rate",
    "query": {
      "pair": "BTC_USDC"
    }
  }
}

svi Event

{
  "kind": "event",
  "type": "svi",
  "timestamp_ms": 1677721600000,
  "data": {
    "sid": "SVI-BTC_USDC-31OCT25",
    "alpha": 0.1,
    "beta": 0.2,
    "rho": -0.3,
    "m": 0.0,
    "sigma": 0.4,
    "timestamp": 1677721600000
  },
  "subscription": {
    "channel": "iv",
    "query": {
      "pair": "BTC_USDC",
      "maturity": "31OCT25"
    }
  }
}

rfq_request Event

Emitted when a new RFQ request is created. RFQ requests can contain multiple instruments to create option combinations.

{
  "kind": "event",
  "type": "rfq_request",
  "timestamp_ms": 1751468186897,
  "data": {
    "taker": "0x1234567890abcdef1234567890abcdef12345678",
    "rfq_orders": [
      {
        "instrument_name": "BTC_USDC-31OCT25-130000-C",
        "contracts": 25,
        "direction": "buy"
      },
      {
        "instrument_name": "BTC_USDC-31OCT25-108000-C",
        "contracts": 30,
        "direction": "sell"
      }
    ],
    "order_id": "863509b1a865fa3f502a9f6784122dd7"
  },
  "subscription": {
    "channel": "rfq",
    "query": {}
  }
}

Data Structure Notes:

  • taker: Address of the account requesting quotes
  • rfq_orders: Array of market orders for multiple instruments
  • order_id: 32-character hexadecimal identifier for the RFQ request
  • Multiple instruments enable complex option strategies (spreads, straddles, etc.)

rfq_post_response Event

Emitted when a market maker posts a new response to an RFQ request with pricing. Each fill represents a market order paired with limit orders that would satisfy it.

{
  "kind": "event",
  "type": "rfq_post_response",
  "timestamp_ms": 1751468277275,
  "data": {
    "fills": [
      [
        {
          "direction": "buy",
          "instrument_name": "BTC_USDC-31OCT25-130000-C",
          "contracts": 25
        },
        [
          {
            "direction": "buy",
            "instrument_name": "BTC_USDC-31OCT25-130000-C",
            "type": "good_til_cancelled",
            "contracts": 25,
            "price": 132500,
            "post_only": true,
            "mmp": false,
            "liquidation": false,
            "taker": "0x1234567890AbcdEF1234567890aBcdef12345678",
            "maker": "0x0987654321098765432109876543210987654321",
            "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
            "signature_deadline": 1735689600,
            "order_id": "863509b1a865fa3f502a9f6784122dd7",
            "order_state": "open",
            "filled_amount": 0,
            "creation_timestamp": 1751468277,
            "chain_id": 421614,
            "to_be_filled": 25
          }
        ]
      ],
      [
        {
          "instrument_name": "BTC_USDC-31OCT25-108000-C",
          "contracts": 30,
          "direction": "sell"
        },
        [
          {
            "instrument_name": "BTC_USDC-31OCT25-108000-C",
            "type": "good_til_cancelled",
            "contracts": 30,
            "direction": "sell",
            "price": 150000,
            "post_only": true,
            "mmp": false,
            "liquidation": false,
            "taker": "0x1234567890AbcdEF1234567890aBcdef12345678",
            "maker": "0x0987654321098765432109876543210987654321",
            "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
            "signature_deadline": 1735689600,
            "order_id": "863509b1a865fa3f502a9f6784122dd7",
            "order_state": "open",
            "filled_amount": 0,
            "creation_timestamp": 1751468277,
            "chain_id": 421614,
            "to_be_filled": 30
          }
        ]
      ]
    ],
    "order_id": "863509b1a865fa3f502a9f6784122dd7",
    "taker": "0x1234567890abcdef1234567890abcdef12345678",
    "maker": "0x0987654321098765432109876543210987654321",
    "response_id": "dc937919bb5dac8f50a82ecb16a01719"
  },
  "subscription": {
    "channel": "rfq",
    "query": {}
  }
}

Data Structure Notes:

  • fills: Array of [MarketOrder, LimitOrder[]] pairs for each instrument in the RFQ
  • order_id: References the original RFQ request ID
  • response_id: Unique identifier for this specific response
  • taker and maker: Account addresses for both parties
  • Each limit order includes complete trading details:
    • signature: Cryptographic signature for order validity (see EIP-712 Signatures Guide)
    • signature_deadline: Unix timestamp when signature expires
    • order_state: Current state ("open", "filled", "cancelled")
    • filled_amount: Amount already filled (0 for new orders)
    • creation_timestamp: When the order was created
    • chain_id: Blockchain network identifier
    • to_be_filled: Amount that needs to be filled
  • Multi-instrument responses enable atomic execution of complex option strategies

rfq_cancel_response Event

Emitted when a market maker cancels a previously posted RFQ response. This occurs when the maker withdraws their quote before it has been filled.

{
  "kind": "event",
  "type": "rfq_cancel_response",
  "timestamp_ms": 1751468300000,
  "data": {
    "fills": [],
    "order_id": "863509b1a865fa3f502a9f6784122dd7",
    "taker": "0x1234567890AbcdEF1234567890aBcdef12345678",
    "maker": "0x0987654321098765432109876543210987654321",
    "response_id": "dc937919bb5dac8f50a82ecb16a01719"
  },
  "subscription": {
    "channel": "rfq",
    "query": {}
  }
}

Data Structure Notes:

  • fills: Empty array for cancelled responses
  • order_id: References the original RFQ request ID
  • response_id: Unique identifier of the cancelled response
  • taker and maker: Account addresses for both parties
  • Cancellation removes the response from the orderbook, preventing execution
  • Only the maker who posted the response can cancel it

account_state Event

{
  "kind": "event",
  "type": "account_state",
  "timestamp_ms": 1677721600000,
  "data": {
    "timestamp": 1677721600000,
    "margin_account": 12345,
    "pair": "BTC_USDC",
    "im": 5000.0,
    "mm": 2500.0,
    "matrix_risk": 1200.0,
    "delta_risk": 800.0,
    "roll_risk": 300.0,
    "unrealised_pnl": 1500.0,
    "equity": 15000.0,
    "portfolio_greeks": {
      "delta": 0.45,
      "gamma": 0.0012,
      "vega": 125.3,
      "theta": -45.5,
      "rho": 85.2
    },
    "positions": []
  },
  "subscription": {
    "channel": "account_state",
    "query": {
      "account": "0x1234567890abcdef1234567890abcdef12345678"
    }
  }
}

account_liquidation Event

⚠️ NOTE: This event type is currently not available. While you can subscribe to the account_liquidation channel, no events will be delivered at this time. The event structure will be documented when this feature becomes available.

bankruptcy Event

{
  "kind": "event",
  "type": "bankruptcy",
  "timestamp_ms": 1677721600000,
  "data": {
    "marginAccountId": "550e8400-e29b-41d4-a716-446655440000",
    "market": "ETH_USDC"
  },
  "subscription": {
    "channel": "bankruptcy",
    "query": {
      "market": "ETH_USDC"
    }
  }
}

Data Fields:

  • marginAccountId: Unique identifier of the margin account that went bankrupt (UUID format)
  • market: Trading pair where bankruptcy occurred (e.g., "BTC_USDC", "ETH_USDC")

Note: Bankruptcy events indicate that an account's equity has become negative, typically after unsuccessful liquidation attempts. This represents a loss absorbed by the insurance fund.

mmp_triggered Event

{
  "kind": "event",
  "type": "mmp_triggered",
  "timestamp_ms": 1677721600000,
  "data": {
    "smart_account_address": "0xabCDEF1234567890ABcDEF1234567890aBCDeF12",
    "pair_symbol": "ETH_USDC",
    "frozen_until": 1677721660000,
    "frozen_duration_seconds": 60
  },
  "subscription": {
    "channel": "mmp",
    "query": {
      "smart_account_address": "0xabCDEF1234567890ABcDEF1234567890aBCDeF12"
    }
  }
}

Data Fields:

  • smart_account_address: Address of the smart account that triggered MMP
  • pair_symbol: Trading pair where MMP was triggered (e.g., "BTC_USDC", "ETH_USDC")
  • frozen_until (optional): Unix timestamp in milliseconds when the freeze expires
  • frozen_duration_seconds (optional): Duration of the freeze in seconds

Note: MMP (Market Maker Protection) is triggered when an account exceeds configured risk thresholds. During the freeze period, the account cannot place new orders. This protects market makers from excessive exposure during volatile market conditions.

Common Use Cases

Monitor Index Prices

Subscribe to real-time price updates for trading pairs.

import WebSocket from 'ws';

interface AuthMessage {
  type: 'auth';
  api_key: string;
}

interface SubscribeMessage {
  type: 'subscribe';
  subscriptions: Array<{
    channel: string;
    query: Record<string, any>;
  }>;
}

interface ResponseMessage {
  type: string;
  success: boolean;
  error?: string;
  event?: string;
  data?: any;
}

const wsUrl = 'wss://staging.kyan.sh/ws';
const API_KEY = 'your-api-key-here';

const ws = new WebSocket(wsUrl);

// First authenticate
ws.onopen = () => {
  console.log('WebSocket connection open');
  const authMessage: AuthMessage = {
    type: 'auth',
    api_key: API_KEY,
  };
  ws.send(JSON.stringify(authMessage));
};

// Handle authentication response and subscribe to index prices
ws.onmessage = (event) => {
  const message = JSON.parse(event.data.toString()) as ResponseMessage;

  if (message.type === 'auth' && message.success) {
    console.log('Authentication successful');

    // Subscribe to BTC/USDC index price updates
    const subscribeMessage: SubscribeMessage = {
      type: 'subscribe',
      subscriptions: [
        {
          channel: 'index_price',
          query: {
            pair: 'BTC_USDC',
          },
        },
      ],
    };

    ws.send(JSON.stringify(subscribeMessage));

    // Update the onmessage handler to process events
    ws.onmessage = handleMessages;
  } else if (message.type === 'auth' && !message.success) {
    console.error('Authentication failed:', message.error);
  }
};

// Process incoming messages after subscription
function handleMessages(event: WebSocket.MessageEvent) {
  const message = JSON.parse(event.data.toString()) as ResponseMessage;

  // Handle subscription confirmation
  if (message.type === 'subscribe' && message.success) {
    console.log('Successfully subscribed to channels');
  }

  // Handle index price events
  if (message.event === 'index_price') {
    console.log('New index price:', message.data);
  }
}

Stream Orderbook Updates

Subscribe to orderbook updates for options or perpetuals.

import WebSocket from 'ws';

const wsUrl = 'wss://staging.kyan.sh/ws';
const API_KEY = 'your-api-key-here';

const ws = new WebSocket(wsUrl);

// First authenticate
ws.onopen = () => {
  console.log('WebSocket connection open');
  const authMessage = {
    type: 'auth',
    api_key: API_KEY,
  };
  ws.send(JSON.stringify(authMessage));
};

// Handle authentication response and subscribe to orderbook
ws.onmessage = (event) => {
  const message = JSON.parse(event.data.toString());

  if (message.type === 'auth' && message.success) {
    console.log('Authentication successful');

    // Subscribe to BTC/USDC option orderbook
    const subscribeMessage = {
      type: 'subscribe',
      subscriptions: [
        {
          channel: 'orderbook_options',
          query: {
            instrument_name: 'BTC_USDC-31OCT25-130000-C',
          },
        },
      ],
    };

    ws.send(JSON.stringify(subscribeMessage));

    // Update the onmessage handler to process events
    ws.onmessage = handleMessages;
  } else if (message.type === 'auth' && !message.success) {
    console.error('Authentication failed:', message.error);
  }
};

// Process incoming messages after subscription
function handleMessages(event: WebSocket.MessageEvent) {
  const message = JSON.parse(event.data.toString());

  // Handle subscription confirmation
  if (message.type === 'subscribe' && message.success) {
    console.log('Successfully subscribed to orderbook');
  }

  // Handle orderbook events
  if (message.event === 'post_order') {
    console.log('New order posted:', message.data);
  } else if (message.event === 'cancel_order') {
    console.log('Order cancelled:', message.data);
  }
}

Monitor Account Activity

Track trades, transfers, and other account activities.

import WebSocket from 'ws';

const wsUrl = 'wss://staging.kyan.sh/ws';
const API_KEY = 'your-api-key-here';
const ACCOUNT_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678';

const ws = new WebSocket(wsUrl);

// First authenticate
ws.onopen = () => {
  console.log('WebSocket connection open');
  const authMessage = {
    type: 'auth',
    api_key: API_KEY,
  };
  ws.send(JSON.stringify(authMessage));
};

// Handle authentication response and subscribe to account activity
ws.onmessage = (event) => {
  const message = JSON.parse(event.data.toString());

  if (message.type === 'auth' && message.success) {
    console.log('Authentication successful');

    // Subscribe to account trades and transfers
    const subscribeMessage = {
      type: 'subscribe',
      subscriptions: [
        {
          channel: 'trade',
          query: {
            account: ACCOUNT_ADDRESS,
          },
        },
        {
          channel: 'transfer',
          query: {
            account: ACCOUNT_ADDRESS,
          },
        },
      ],
    };

    ws.send(JSON.stringify(subscribeMessage));

    // Update the onmessage handler to process events
    ws.onmessage = handleMessages;
  } else if (message.type === 'auth' && !message.success) {
    console.error('Authentication failed:', message.error);
  }
};

// Process incoming messages after subscription
function handleMessages(event: WebSocket.MessageEvent) {
  const message = JSON.parse(event.data.toString());

  // Handle subscription confirmation
  if (message.type === 'subscribe' && message.success) {
    console.log('Successfully subscribed to account activity');
  }

  // Handle account events
  if (message.event === 'trade') {
    console.log('New trade:', message.data);
  } else if (message.event === 'deposit') {
    console.log('New deposit:', message.data);
  } else if (message.event === 'withdrawal') {
    console.log('New withdrawal:', message.data);
  }
}

Error Handling

Error responses follow this format:

{
  "kind": "response",
  "type": "error",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "Description of the error"
}

Common error scenarios:

  • Authentication failures:

    • "Failed to validate api key" - Generic validation failure

    • "Not Authorized" - Not authenticated

    • "Session already authorized" - Already authenticated

    • Specific API key error codes: "NOT_FOUND", "FORBIDDEN", "KEY_USAGE_EXCEEDED", "RATELIMITED"

    📖

    Learn More: For API key troubleshooting and error resolution, see the API Key Guide.

  • Invalid subscriptions:

    • "Invalid subscription parameters"
    • "Invalid query for [CHANNEL] channel"
  • Invalid messages:

    • "invalid [MESSAGE_TYPE] payload [error_details]"

Handling Specific Errors

interface ErrorResponse {
  type: string;
  timestampMs: number;
  success: boolean;
  error: string;
}

function handleWebSocketError(error: ErrorResponse): void {
  // Log the error
  console.error(`WebSocket error: ${error.error}`);

  // Handle specific authentication errors
  if (
    error.error.includes('Failed to validate api key') ||
    error.error === 'NOT_FOUND' ||
    error.error === 'FORBIDDEN' ||
    error.error === 'KEY_USAGE_EXCEEDED'
  ) {
    // API key issues - might need to refresh
    console.error('API key error:', error.error);
    // Trigger API key refresh or user notification
  } else if (error.error === 'RATELIMITED') {
    // Rate limiting - implement backoff
    console.warn('Rate limited. Implementing backoff...');
    // Slow down request rate
  } else if (error.error === 'Not Authorized') {
    // Not authenticated - try to authenticate
    console.warn('Not authenticated. Attempting to authenticate...');
    // Attempt to authenticate
  } else if (error.error === 'Session already authorized') {
    // Already authenticated - no action needed
    console.info('Session already authenticated');
  } else if (error.error.includes('Invalid subscription parameters')) {
    // Invalid subscription - check subscription format
    console.error('Invalid subscription:', error.error);
    // Fix subscription parameters
  } else {
    // Generic error handling
    console.error('Unhandled WebSocket error:', error.error);
  }
}

Rate Limiting

Overview

The WebSocket API implements per-session rate limiting to ensure fair usage and system stability. Rate limits are applied to specific message types that query market data.

Rate Limited Messages

The following WebSocket message types have rate limits applied:

  • get_ob_state_by_instruments: 15 requests per minute per session
  • get_ob_state_by_market: 15 requests per minute per session

Rate Limit Implementation

Rate limits use a sliding window algorithm:

  • Window: 60 seconds (1 minute)
  • Limit: 15 requests per session per window
  • Scope: Per WebSocket session (each WebSocket connection has its own independent rate limit)

Rate Limit Responses

When you exceed the rate limit, you'll receive an error response:

{
  "kind": "response",
  "type": "get_ob_state_by_instruments",
  "id": "your-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "Rate limit exceeded: 15 requests per 60 seconds. Reset in 23 seconds. Remaining: 0"
}

Error Response Fields:

  • error: Describes the rate limit violation and reset time
  • The error message includes:
    • Current rate limit (e.g., "15 requests per 60 seconds")
    • Time until reset (e.g., "Reset in 23 seconds")
    • Remaining requests in current window (e.g., "Remaining: 0")

Handling Rate Limits

Best Practices:

  1. Monitor your request rate: Stay within 15 requests per minute for rate-limited endpoints
  2. Implement exponential backoff: When rate limited, wait before retrying
  3. Parse error messages: Extract reset time and remaining requests from error responses
  4. Use subscriptions: Instead of repeatedly polling get_ob_state_*, use channel subscriptions for real-time updates

Example Rate Limit Handler:

function handleRateLimitedResponse(response: any) {
  if (!response.success && response.error?.includes('Rate limit exceeded')) {
    // Extract reset time from error message
    const resetMatch = response.error.match(/Reset in (\d+) seconds/);
    const resetSeconds = resetMatch ? parseInt(resetMatch[1]) : 60;

    console.warn(
      `Rate limited. Waiting ${resetSeconds} seconds before retry...`,
    );

    // Implement backoff strategy
    setTimeout(() => {
      console.log('Rate limit window reset. Safe to retry.');
      // Retry your request here
    }, resetSeconds * 1000);
  }
}

Alternative Approach - Use Subscriptions:

Instead of polling market data with get_ob_state_*, consider subscribing to real-time channels:

// Instead of repeatedly calling get_ob_state_by_instruments
// Use subscription for real-time updates (no rate limit)
ws.send(
  JSON.stringify({
    type: 'subscribe',
    channels: ['orders', 'trades', 'account'],
    query: { pair: 'ETH_USDC' },
  }),
);

Connection Management

Connection Lifecycle and Session Duration

Important: WebSocket client sessions have a maximum duration of 1 hour. For long-running applications, you must implement reconnection logic to maintain persistent data streams. There is no heartbeat/ping-pong mechanism built into the server. It is your responsibility as the client to handle reconnection for long-living sessions.

Reconnection Strategy Example

class WebSocketClient {
  private ws: WebSocket | null = null;
  private readonly url: string;
  private readonly apiKey: string;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectInterval: number = 1000; // Start with 1 second
  private subscriptions: Array<{
    channel: string;
    query: Record<string, any>;
  }> = [];
  private isAuthenticated: boolean = false;

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

  connect(): void {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('Connected to WebSocket');
      this.reconnectAttempts = 0;
      this.reconnectInterval = 1000; // Reset backoff
      this.authenticate();
    };

    this.ws.onclose = (event) => {
      console.log(`Connection closed: ${event.code} - ${event.reason}`);
      this.isAuthenticated = false;

      // Implement exponential backoff reconnection
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        console.log(
          `Attempting to reconnect in ${this.reconnectInterval}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`,
        );

        setTimeout(() => {
          this.reconnectAttempts++;
          this.reconnectInterval = Math.min(this.reconnectInterval * 2, 30000); // Cap at 30 seconds
          this.connect();
        }, this.reconnectInterval);
      } else {
        console.error(
          'Max reconnection attempts reached. Manual reconnection required.',
        );
      }
    };

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

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };
  }

  private authenticate(): void {
    if (!this.ws) return;

    const authMessage = {
      type: 'auth',
      api_key: this.apiKey,
    };

    this.ws.send(JSON.stringify(authMessage));
  }

  private handleMessage(message: any): void {
    if (message.type === 'auth' && message.success) {
      console.log('Authenticated successfully');
      this.isAuthenticated = true;

      // Resubscribe to previous subscriptions after reconnection
      if (this.subscriptions.length > 0) {
        this.resubscribe();
      }
    } else if (message.type === 'auth' && !message.success) {
      console.error('Authentication failed:', message.error);
    } else if (message.event) {
      // Handle events
      console.log(`Event received: ${message.event}`, message.data);
    }
  }

  subscribe(
    subscriptions: Array<{ channel: string; query: Record<string, any> }>,
  ): void {
    if (!this.isAuthenticated) {
      console.warn('Cannot subscribe: not authenticated');
      return;
    }

    // Store subscriptions for reconnection
    this.subscriptions = [...this.subscriptions, ...subscriptions];

    const subscribeMessage = {
      type: 'subscribe',
      subscriptions: subscriptions,
    };

    this.ws?.send(JSON.stringify(subscribeMessage));
  }

  private resubscribe(): void {
    if (this.subscriptions.length > 0) {
      console.log('Resubscribing to channels after reconnection...');
      const subscribeMessage = {
        type: 'subscribe',
        subscriptions: this.subscriptions,
      };
      this.ws?.send(JSON.stringify(subscribeMessage));
    }
  }

  disconnect(): void {
    if (this.ws) {
      // Prevent reconnection attempts
      this.reconnectAttempts = this.maxReconnectAttempts;

      // Send logout message
      const logoutMessage = { type: 'logout' };
      this.ws.send(JSON.stringify(logoutMessage));

      this.ws.close();
      this.ws = null;
      this.isAuthenticated = false;
    }
  }

  // Manual reconnection method
  forceReconnect(): void {
    this.reconnectAttempts = 0;
    this.reconnectInterval = 1000;
    if (this.ws) {
      this.ws.close();
    }
    this.connect();
  }
}

// Usage with proper error handling
const client = new WebSocketClient('wss://staging.kyan.sh/ws', 'your-api-key');
client.connect();

// Subscribe to channels after connection
setTimeout(() => {
  client.subscribe([
    {
      channel: 'index_price',
      query: { pair: 'BTC_USDC' },
    },
  ]);
}, 2000); // Wait for authentication

Best Practices

  1. Connection Management:

    • Always authenticate immediately after connecting
    • Implement robust reconnection logic with exponential backoff - connections timeout after 1 hour.
    • Handle connection drops gracefully and automatically
    • Store subscription state for seamless reconnection
    • Monitor connection health and implement manual reconnection triggers
  2. Subscription Management:

    • Keep track of active subscriptions for reconnection
    • Automatically resubscribe after successful reconnection
    • Unsubscribe from channels you no longer need to reduce bandwidth
    • Validate subscription success before considering them active
  3. Error Handling:

    • Implement comprehensive error handling for all message types
    • Log errors for debugging and monitoring
    • Handle rate limiting with exponential backoff (see Rate Limiting section for details)
    • Differentiate between recoverable and non-recoverable errors
  4. Performance:

    • Batch subscriptions when possible to reduce overhead
    • Process messages efficiently to avoid blocking
    • Consider using worker threads for heavy message processing
    • Implement message queuing during reconnection periods
  5. Security:

    • Never expose API keys in client-side code
    • Use secure WebSocket connections (wss://) only
    • Validate all incoming data before processing
    • Implement proper authentication state management
  6. Reliability:

    • Plan for regular disconnections - client sessions expire after 1 hour
    • Implement connection health monitoring and automatic reconnection for long-running applications
    • Use proper logging to track connection lifecycle events
    • Test reconnection logic thoroughly in development