WebSocket API
Real-time data streaming for Hypercall options trading.
Check out the Interactive WebSocket API Reference for a better browsing experience with live examples and schema details.
Download the AsyncAPI specification for programmatic use.
WebSocket message objects may gain new fields without advance notice. Clients must ignore fields they do not recognize. Hypercall does not remove fields from an existing message schema.
Connection
Connect to wss://HOST/ws:
Endpoints:
- Production:
wss://api.hypercall.xyz/ws - Local:
ws://localhost:3000/ws
Testnet is temporarily disabled until Hypercall acquires more testnet HYPE.
Compression
/ws supports RFC 7692 permessage-deflate. Enable it in your client to cut market-data bandwidth: on captured production traffic, independently compressed frames are about 45% smaller, and index price batches about 70% smaller.
- Your client offers the extension in the upgrade request, for example
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits. - When the server accepts, the
101response carriesSec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover. Each server message is then compressed on its own, without a shared sliding window, and messages under a few hundred bytes are still sent uncompressed. Your library decompresses transparently. - If the response has no
Sec-WebSocket-Extensionsheader, every frame is uncompressed. Compression is enabled per environment, so clients must handle both; the JSON messages are identical either way. - Your client may compress what it sends. The server inflates each client message up to 256 KiB and closes the connection if a message would exceed that.
Browsers negotiate the extension automatically. Most libraries need no change:
Python (websockets enables compression by default):
import websockets
async with websockets.connect("wss://api.hypercall.xyz/ws", compression="deflate") as ws:
...
Node.js (ws):
import WebSocket from "ws";
const ws = new WebSocket("wss://api.hypercall.xyz/ws", { perMessageDeflate: true });
Rust: the hypercall-client WsClient negotiates permessage-deflate by default (see Example: Rust Client). Plain tungstenite and tokio-tungstenite do not implement it yet, so they connect uncompressed. For compression with your own socket, use a client that implements it, such as yawc:
let url = "wss://api.hypercall.xyz/ws".parse()?;
let mut ws = yawc::WebSocket::connect(url)
.with_options(yawc::Options::default().with_low_latency_compression())
.await?;
Wallet Identification
To receive data on authenticated channels (orders, fills, portfolio), identify your wallet after connecting by sending an Authenticate message:
{"type": "Authenticate", "wallet": "0x1234..."}
The server responds with confirmation:
{"type": "Authenticated", "wallet": "0x1234..."}
After receiving Authenticated, you can subscribe to authenticated channels. If the wallet address is invalid, the server responds with an Error message and the connection remains open.
The ?wallet= query parameter is still supported for backward compatibility but is deprecated and will be removed in a future release. Prefer the message-based approach above.
Connection Liveness
The server enforces a WebSocket heartbeat:
- Sends a
Pingcontrol frame every 20 seconds - Expects a matching
Pongwithin 60 seconds - Closes the connection with close code
1008and reasonpong timeoutif the client stops answering
Browser WebSocket implementations handle ping/pong automatically. Many Rust websocket libraries, including tungstenite and tokio-tungstenite, also handle control-frame ping/pong for you. Check your client's library documentation before adding manual Pong handling. Custom or raw socket implementations must reply to Ping frames with Pong.
Slow Consumer Recovery
The server closes a /ws connection that cannot drain outbound data within the configured message, encoded-byte, queue-age, or socket-write safety ceiling. When the connection can still accept a close frame, the server uses code 1008 and a compact JSON reason:
{"error":"slow_consumer","class":"ordered_public","cause":"message_age","recovery":"snapshot_resubscribe"}
The reason fields are:
| Field | Meaning |
|---|---|
class | Delivery class whose frame crossed the safety boundary. |
cause | message_limit, byte_limit, message_age, or write_timeout. |
recovery | Required next action, such as resubscribe, snapshot_resubscribe, portfolio_refetch, or rest_reconcile. |
After any disconnect, reconnect, identify the wallet again when required, resubscribe, and reconcile current state before processing new events. Ordered public channels require a fresh snapshot. Private event channels require reconciliation through the authoritative REST surface because cursor replay is not available yet. A fully stalled connection may terminate before it can read the close reason, so clients must use this recovery flow for an unclean close too.
Use separate connections for high-rate public market data and authenticated commands or private streams. Delivery classes select metrics and recovery behavior, but frames on one connection still share one ordered socket write path. A stalled public write can therefore delay later private frames on that same connection until the write deadline closes it.
Subscribing to Channels
Send a JSON message to subscribe:
{"type": "Subscribe", "channel": "orderbook"}
To unsubscribe:
{"type": "Unsubscribe", "channel": "orderbook"}
You'll receive a confirmation:
{"type": "Subscribed", "channel": "orderbook"}
Symbol Filtering
The order_updates and fills channels support an optional symbols filter. When provided, the server only sends messages whose underlying matches one of the specified symbols.
{"type": "Subscribe", "channel": "order_updates", "symbols": ["BTC"]}
Both bare underlyings ("BTC") and full instrument names ("BTC-20260131-100000-C") are accepted. To add more symbols, send another Subscribe. To remove specific symbols:
{"type": "Unsubscribe", "channel": "order_updates", "symbols": ["BTC"]}
When no symbols are specified, all updates for your wallet are forwarded.
The public orderbook, trades, indicative_market_data, and indicative_market_data_v2 channels also accept symbols, matched against exact instrument symbols (case-insensitive), not underlyings:
{"type": "Subscribe", "channel": "orderbook", "symbols": ["BTC-20260131-100000-C"]}
Without symbols, these channels deliver every instrument.
Options Chain Filtering
The options_chain channel supports filtering by instrument symbols, expiry date, and option type:
{
"type": "Subscribe",
"channel": "options_chain",
"symbols": ["BTC-20260131-100000-C"],
"expiry": "2026-01-31",
"option_type": "call"
}
| Filter | Values | Default |
|---|---|---|
symbols | Array of full instrument symbols (e.g., ["BTC-20260131-100000-C"]) | All instruments |
expiry | Date string "YYYY-MM-DD", matched against the UTC calendar date of each instrument's expiry | All expiries |
option_type | "call", "put", "both", or omit for both | Both |
Batched Delivery
indicative_market_data_v2 carries the same rows as indicative_market_data, several rows per frame:
{"type": "Subscribe", "channel": "indicative_market_data_v2", "symbols": ["BTC-20260131-100000-C"]}
The server confirms with a batch marker:
{"type": "Subscribed", "channel": "indicative_market_data_v2", "batch": true}
It then holds each row for at most one batch window, 25 ms by default, and sends the rows published in that window as one IndicativeMarketDataBatch frame. A batch is also sent early once it reaches 256 rows.
- Detecting support: only a confirmation with
"batch": truemeans the server batches. Servers released before this channel confirm any channel name without the marker and then send nothing, and current servers answer an unknown channel with anunknown_channelerror. In either case, unsubscribe fromindicative_market_data_v2and subscribe toindicative_market_dataon the same connection. - Latency: a row arrives up to one window later than on
indicative_market_data. On production traffic with the default window, rows waited 12 ms on average and never more than 25 ms. Use the v1 channel if you act on individual quote updates within milliseconds. - Bandwidth: batching alone saves about 7% of bytes. The large saving comes with
permessage-deflate: compressed batches are about 9.8 times smaller than uncompressed one-row frames for an unfiltered subscription, and about 8.3 times smaller for one options chain, against about 1.8 times for rows compressed one by one. - Order: rows in
updatesare in publish order, and batches arrive in order. Rows are not conflated, so one instrument can appear more than once in a batch. Apply the rows in order and you reach the same state as on the v1 channel. - Snapshot: the subscribe-time snapshot also arrives as batch frames.
- Filters:
symbolsworks as onindicative_market_data. Narrowing the filter or unsubscribing first sends any rows already pending under the old filter, then theUnsubscribedconfirmation, so no row is lost or duplicated. - Independent channels: a connection can subscribe to both channels with different filters.
indicative_market_datais unchanged.
Available Channels
| Channel | Auth Required | Description |
|---|---|---|
orderbook | No | L2 orderbook updates for all symbols |
trades | No | Public trade feed |
market_updates | No | Market listing changes (created/deleted/expired) |
options_chain | No | Incremental options chain updates (filterable by symbols, expiry, option_type) |
index_prices | No | Real-time spot/index prices for all underlyings |
indicative_market_data | No | Allowlisted quote-provider stream. Not generally available yet |
indicative_market_data_v2 | No | The same stream in batch frames. See Batched Delivery |
order_updates | Yes | Your order status changes (filterable by symbol) |
fills | Yes | Your trade fills (filterable by symbol) |
portfolio | Yes | Your position and balance updates |
liquidation | Yes | Your liquidation state changes |
rfq | Yes | RFQ quotes, status updates, and fill notifications |
experimental_order_updates, experimental_combo_fills | Yes | Experimental combo channels. See Experimental Combo API |
experimental_combo_trades | No | Experimental public combo trades |
Competition and leaderboard data are not served by api.hypercall.xyz, over WebSocket or REST. Subscribing to the retired competition or competition_engagement channels returns an Error. The trading app reads competitions over HTTP from the analytics service at https://api-ui.hypercall.xyz, for example GET /v2/competitions, GET /v2/competitions/{id}, GET /v2/competitions/leaderboard, and GET /v2/competitions/all-time/leaderboard.
Message Types
Place Order (Authenticated)
Place an order through the WebSocket command path.
{
"type": "PlaceOrder",
"wallet": "0x1234...",
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"size": "1",
"price": "100",
"tif": "gtc",
"route": "book_only",
"client_id": "my-order-1",
"nonce": 1780000000000,
"signature": "0x..."
}
The nonce above is illustrative. Generate a fresh one for every order from the current Unix time in milliseconds, for example Date.now() in JavaScript. A copied fixed value falls outside the accepted window and is rejected.
| Field | Type | Description |
|---|---|---|
wallet | string | Wallet address that owns the order |
symbol | string | Option symbol |
side | string | "Buy" or "Sell" |
size | string | Contract size, exactly matching the signed value |
price | string | Limit price, exactly matching the signed value |
tif | string | Optional time-in-force, defaults to "gtc" |
route | string | Use "book_only". An omitted route is still accepted but deprecated, and its sunset date has passed. |
client_id | string | Optional client order ID |
reduce_only | boolean | Optional, default false. A reduce-only order signs PlaceOrderReduceOnly and requires an explicit route |
mmp_enabled | boolean | Optional, default false |
builder_code_address | string | Optional builder code wallet |
nonce | integer | Required signing nonce: the current Unix time in milliseconds. It must fall within two days before and one day after the current time. Each nonce is accepted once per signer, and REST and WebSocket orders share the same nonce space. A replayed frame is rejected. |
signature | string | EIP-712 PlaceOrder signature |
The connection must have sent Authenticate first; otherwise the server answers {"type": "Error", "message": "Not authenticated"}. The signer must be wallet or an approved agent for it.
WebSocket PlaceOrder currently dispatches directly to the orderbook. route="best_execution" and route="rfq_only" are rejected on WebSocket because this path does not run RPI/RFQ routing yet. Use POST /order for best_execution.
An admitted order is answered on the same connection with an OrderResult:
{
"type": "OrderResult",
"order_id": 12345,
"status": "ACKED",
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"price": "100",
"size": "1"
}
reason is added when the order is not accepted. Validation, signature, and rate-limit failures are sent as an Error whose message starts with Order placement failed:. Subsequent status changes arrive on order_updates.
Orderbook Update
L2 orderbook snapshot/update for a symbol.
{
"type": "OrderbookUpdate",
"symbol": "BTC-20260131-100000-C",
"option_token_address": "0x...",
"bids": [["95000.5", "10.5"], ["94999.0", "25.0"]],
"asks": [["95001.0", "8.0"], ["95002.5", "15.0"]],
"timestamp": 1737331200000
}
| Field | Type | Description |
|---|---|---|
symbol | string | Option symbol |
option_token_address | string? | On-chain option token address, when known |
bids | array | Bid levels as [price, size] tuples, size is in human-readable contracts |
asks | array | Ask levels as [price, size] tuples, size is in human-readable contracts |
timestamp | integer | Unix timestamp (milliseconds) |
Trade
Public trade event.
{
"type": "Trade",
"symbol": "BTC-20260131-100000-C",
"price": "0.0523",
"size": "5.0",
"side": "Buy",
"timestamp": 1737331200000
}
| Field | Type | Description |
|---|---|---|
symbol | string | Option symbol |
price | string | Trade price in USD |
size | string | Trade size in contracts |
side | string | Aggressor side (Buy or Sell) |
timestamp | integer | Unix timestamp (milliseconds) |
Fill (Authenticated)
Your trade fill notification.
{
"type": "Fill",
"order_id": 12345,
"fill_id": 67890,
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"price": "0.0523",
"size": "5.0",
"timestamp": 1737331200000,
"wallet_address": "0x1234...abcd",
"fee": "0",
"trade_id": 99999,
"is_taker": true,
"builder_code_address": null,
"builder_code_fee": null,
"instrument_type": "option"
}
| Field | Type | Description |
|---|---|---|
order_id | integer | Your order ID |
fill_id | integer | Fill ID |
symbol | string | Option symbol |
side | string | Your side of the trade (Buy or Sell) |
price | string | Fill price in USD |
size | string | Fill size in contracts |
timestamp | integer | Unix timestamp (milliseconds) |
wallet_address | string | Your wallet address |
fee | string | Trading fee charged. Returns 0 while launch venue fees are disabled |
trade_id | integer | Unique trade ID |
is_taker | boolean | Whether you were the taker |
builder_code_address | string? | Builder code wallet (if any) |
builder_code_fee | string? | Builder code fee. Returns null while launch venue fees are disabled |
instrument_type | string | "option" |
Portfolio Update (Authenticated)
Sent on the portfolio channel for positions, balances, margin, and Greeks. On subscribe the server sends an Initial snapshot, then incremental updates.
Each frame's type is its update kind:
| Update kind | Fields besides timestamp |
|---|---|
Initial | positions |
PositionUpdate | position |
BalanceUpdate | total_margin_used |
MarginUpdate | span_margin, margin_summary, margin_mode, total_margin_used, available_balance |
GreeksUpdate | per_leg, aggregate |
timestamp on portfolio frames is Unix seconds, unlike the millisecond timestamps on other channels.
Greeks update example:
{
"type": "GreeksUpdate",
"timestamp": 1737331200,
"per_leg": [
{
"symbol": "BTC-20260131-100000-C",
"quantity": "2.0",
"delta": 0.91,
"gamma": 0.003,
"theta": -0.12,
"vega": 0.44,
"iv": 0.63
}
],
"aggregate": {
"delta": 0.91,
"gamma": 0.003,
"theta": -0.12,
"vega": 0.44,
"iv": 0.63
}
}
For empty portfolios, Greeks updates use:
per_leg: []aggregate: null
Order Update (Authenticated)
Order status change notification. PARTIALLY_FILLED updates are included.
filled_size is the cumulative quantity filled across the order's executions,
not the quantity of the latest fill.
{
"type": "OrderUpdate",
"order_id": 12345,
"client_id": "my-order-1",
"request": {
"price": "0.0523",
"size": "10.0",
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"tif": "gtc"
},
"status": "PARTIALLY_FILLED",
"filled_size": "0.75",
"timestamp": 1767225600000,
"reason": null,
"wallet_address": "0x0000000000000000000000000000000000000001",
"instrument_type": "option"
}
status is one of ACKED, OPEN, PARTIALLY_FILLED, FILLED, CANCELED, or REJECTED. client_id is omitted when the order has none. instrument_type is "option" or "perp".
Market Update
Market listing changes.
Market Created:
{
"type": "MarketUpdate",
"action": "Created",
"symbol": "BTC-20260131-100000-C",
"strike": "100000",
"is_call": true,
"underlying": "BTC",
"expiry": 1738281600,
"timestamp": 1737331200000
}
Market Expired:
{
"type": "MarketUpdate",
"action": "Expired",
"symbol": "BTC-20260131-100000-C",
"strike": "100000",
"is_call": true,
"underlying": "BTC",
"expiry": 1738281600,
"timestamp": 1738281600000
}
action is Created, Expired, Deleted (carries only symbol and timestamp), or ComboCreated (see Experimental Combo API).
Options Chain Update
Sent on options_chain when a listing or its top of book changes. action is Upsert or Remove.
{
"type": "OptionsChainUpdate",
"action": "Upsert",
"currency": "BTC",
"expiry": 1769846400,
"row": {
"strike": 100000.0,
"call": {
"symbol": "BTC-20260131-100000-C",
"bid_price_usd": 1520.0,
"ask_price_usd": 1580.0
}
},
"timestamp": 1737331200000
}
An Upsert row carries one leg (call or put) with the leg fields of the options chain snapshot: symbol, option_token_address, bid and ask price, IV, size in contracts and USD notional, greeks_abs, and greeks_cash (the example above is abbreviated). Values that are not available are null; the leg that is not part of the update is omitted. A Remove carries currency, expiry, strike, option_type, symbol, and timestamp when an instrument is deleted, expires, or enters pending settlement. expiry is Unix seconds.
Position Expired (Authenticated)
Sent on the portfolio channel when your position settles at expiry.
{
"type": "PositionExpired",
"wallet_address": "0x1234...abcd",
"symbol": "BTC-20260131-100000-C",
"position_size": "10.0",
"settlement_price": "105000",
"settlement_value": "500.0",
"settlement_entry_price": "100.0",
"cost_basis": "1000.0",
"net_pnl": "-500.0",
"timestamp": 1738281600000
}
settlement_entry_price, cost_basis, and net_pnl are null when not available.
Liquidation State Change (Authenticated)
Sent on the liquidation channel when your account's liquidation state changes.
{
"type": "LiquidationStateChange",
"wallet_address": "0x1234...abcd",
"previous_state": "healthy",
"new_state": "pre_liquidation",
"liquidation_mode": "partial",
"margin_mode": "standard",
"equity": "10000.0",
"mm_required": "10500.0",
"maintenance_margin": "-500.0",
"shortfall": "500.0",
"partial_liquidation": { "...": "..." },
"full_liquidation": null,
"timestamp": 1737331200000
}
| Field | Description |
|---|---|
previous_state, new_state | healthy, pre_liquidation, in_liquidation, or liquidated |
liquidation_mode | partial, full, or null when no liquidation flow is active |
margin_mode | standard or portfolio |
equity | Account equity |
mm_required | Maintenance margin requirement |
maintenance_margin | Equity minus the maintenance margin requirement |
shortfall | Amount by which the account is under maintenance margin |
partial_liquidation | Partial liquidation progress (entry time, target equity, escalation deadline, active liquidation orders, bonus), or null |
full_liquidation | Full liquidation auction details (auction ID, start time, margin needed, and on completion the winner, bonus, and resolution transaction), or null |
See Liquidation for what each state means.
Index Price Update
Batched spot/index prices for all underlyings.
{
"type": "IndexPriceUpdate",
"prices": [
{"underlying": "BTC", "price": "97250.50", "timestamp": 1737331200000},
{"underlying": "ETH", "price": "3200.00", "timestamp": 1737331199980},
{"underlying": "NVDA", "price": "182.40", "timestamp": 1737331199950}
],
"timestamp": 1737331199950
}
| Field | Type | Description |
|---|---|---|
prices | array | Array of {underlying, price, timestamp} entries for each tracked underlying |
prices[].underlying | string | Underlying symbol (e.g., "BTC", "ETH") |
prices[].price | string | Current spot/index price in USD |
prices[].timestamp | integer | Source observation time for this underlying, as Unix milliseconds |
timestamp | integer | Oldest source observation time included in prices, as Unix milliseconds |
Indicative Market Data
Allowlisted quote-provider stream with aggregated best bid/ask from registered quote providers. This channel is not generally available yet. Use REST market data and authenticated order/fill/portfolio channels unless Hypercall has enabled quote-provider streaming for your integration.
{
"type": "IndicativeMarketData",
"instrument": "BTC-20260131-100000-C",
"best_bid": "0.0520",
"best_ask": "0.0530",
"indicative_bid_size": "50.0",
"indicative_ask_size": "25.0",
"num_providers": 3,
"published_at": 1737331200004,
"timestamp": 1737331200000
}
| Field | Type | Description |
|---|---|---|
instrument | string | Option symbol |
best_bid | string | Optional best aggregated bid price |
best_ask | string | Optional best aggregated ask price |
bid_iv | number | Optional implied volatility of best bid |
ask_iv | number | Optional implied volatility of best ask |
indicative_bid_size | string | Optional total bid size across providers |
indicative_ask_size | string | Optional total ask size across providers |
num_providers | integer | Number of active quote providers |
rfq_provider_quotes | array | Optional per-provider quotes: wallet, bid_price, ask_price, max_bid_size, max_ask_size, updated_at |
published_at | integer | API-clock time this row was published, Unix milliseconds. Measure the age of rfq_provider_quotes against this, not your local clock |
timestamp | integer | Time of the latest provider quote, Unix milliseconds |
Indicative Market Data Batch
Sent on the indicative_market_data_v2 channel (see Batched Delivery). Each element of updates has the same fields as an IndicativeMarketData message, without type.
{
"type": "IndicativeMarketDataBatch",
"flushed_at": 1737331200025,
"updates": [
{
"instrument": "BTC-20260131-100000-C",
"best_bid": "0.0520",
"best_ask": "0.0530",
"num_providers": 3,
"published_at": 1737331200004,
"timestamp": 1737331200000
},
{
"instrument": "BTC-20260131-105000-C",
"best_bid": "0.0310",
"best_ask": "0.0322",
"num_providers": 2,
"published_at": 1737331200004,
"timestamp": 1737331200001
}
]
}
| Field | Type | Description |
|---|---|---|
flushed_at | integer | API-clock time the batch was sent, Unix milliseconds. flushed_at - published_at is the batching delay of a row |
updates | array | Rows published during the window, oldest first |
RFQ Quotes (Authenticated)
Quotes received in response to your RFQ submission.
{
"type": "RfqQuotes",
"rfq_id": "550e8400-e29b-41d4-a716-446655440000",
"quotes": [
{
"quote_id": "660e8400-e29b-41d4-a716-446655440001",
"net_premium": "52.30",
"expires_at": 1737331225000
}
],
"status": "quoted",
"taker_wallet": "0x1234...abcd"
}
RFQ Status Update (Authenticated)
Status change for an RFQ you submitted.
{
"type": "RfqStatusUpdate",
"rfq_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "executed",
"taker_wallet": "0x1234...abcd"
}
Clock Sync
Send ClockSync with any nonce string to get an API-clock sample on the same connection, for example to bound transport delay:
{"type": "ClockSync", "nonce": "abc"}
{"type": "ClockSynced", "nonce": "abc", "server_at": 1737331200000}
server_at is Unix milliseconds. No authentication is needed.
Error
Server error message. Match on code when present, not on message.
{
"type": "Error",
"message": "Unknown channel 'foobar'",
"code": "unknown_channel",
"channel": "foobar"
}
| Field | Type | Description |
|---|---|---|
message | string | Human-readable description |
code | string | Optional stable reason. unknown_channel: a Subscribe named a channel that does not exist |
channel | string | Optional. The channel a rejected Subscribe named |
A Subscribe to a channel that is not listed in Available Channels is answered with the unknown_channel error, and no subscription is created. Earlier servers confirmed any name and then sent nothing.
Other errors carry only message. Examples: Authentication required for this channel when you subscribe to an authenticated channel before Authenticate, Not authenticated for PlaceOrder before Authenticate, and Invalid wallet address: ....
Authentication
Authenticated channels require a wallet identification message after connecting:
{"type": "Authenticate", "wallet": "0x1234567890abcdef..."}
Messages on authenticated channels are filtered to only show data for your wallet. No signature is required for WebSocket connections.
Example: Python Client
import asyncio
import websockets
import json
async def main():
uri = "wss://api.hypercall.xyz/ws"
async with websockets.connect(uri) as ws:
# Identify the wallet before subscribing to authenticated channels.
await ws.send(json.dumps({
"type": "Authenticate",
"wallet": "0xYourWallet"
}))
# Subscribe to orderbook
await ws.send(json.dumps({
"type": "Subscribe",
"channel": "orderbook"
}))
# Subscribe to fills for BTC only
await ws.send(json.dumps({
"type": "Subscribe",
"channel": "fills",
"symbols": ["BTC"]
}))
# Listen for messages
async for message in ws:
data = json.loads(message)
print(f"Received: {data['type']}")
asyncio.run(main())
Example: TypeScript Client
const ws = new WebSocket("wss://api.hypercall.xyz/ws");
ws.onopen = () => {
ws.send(JSON.stringify({ type: "Authenticate", wallet: "0xYourWallet" }));
// Subscribe to channels
ws.send(JSON.stringify({ type: "Subscribe", channel: "orderbook" }));
// Subscribe to order updates filtered to BTC
ws.send(JSON.stringify({
type: "Subscribe",
channel: "order_updates",
symbols: ["BTC"],
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(`Received: ${msg.type}`);
if (msg.type === "OrderbookUpdate") {
console.log(`${msg.symbol}: ${msg.bids.length} bids, ${msg.asks.length} asks`);
}
};
Example: Rust Client
The hypercall-client crate's WsClient identifies the wallet, restores subscriptions after a reconnect, and answers server pings for you. It offers permessage-deflate compression by default and falls back to uncompressed frames when the server does not accept it, with the same JSON messages either way. Call with_permessage_deflate(false) to stop offering it.
use hypercall_client::WsClient;
#[tokio::main]
async fn main() -> hypercall_client::Result<()> {
let ws = WsClient::new();
ws.connect("https://api.hypercall.xyz", None).await?;
ws.subscribe(vec!["orderbook", "index_prices"]).await?;
if let Some(msg) = ws.wait_for_message(|_| true, 5_000).await {
println!("Received: {msg:?}");
}
Ok(())
}