Hyperliquid exposes two distinct WebSocket channels for trade and fill data: the public trades channel, which is coin-scoped and delivers every trade on a market, and the userFills channel, which is user-scoped and delivers only the authenticated account's fills. Subscribing to the wrong scope is the most common integration error. The subscription protocol uses a JSON message with a type and a subscription object containing the channel name and its parameter (coin or user). Because WebSocket messages are only delivered while connected, a dropped connection creates a gap that must be backfilled from the Info API and reconciled by a stable fill identity—not by timestamp alone. This guide covers the channel mechanics, payload shapes, a runnable Node.js example, a results table for self-measurement, and troubleshooting for common failure modes.
Two Scopes for Trade and Fill Data
Hyperliquid's WebSocket API separates trade data into two channels with fundamentally different scopes. The public trades channel is subscribed per coin and delivers every trade that occurs on that market, regardless of who initiated it. The userFills channel is user-scoped and delivers only the fills belonging to the authenticated account. Choosing the wrong channel for your use case is the most common integration error: a market-data consumer that subscribes to userFills will see nothing unless it authenticates as a user, while an account tracker that subscribes to trades will receive a firehose of unrelated market activity.
The distinction matters because the two channels have different authentication requirements, different payload shapes, and different reconciliation paths. The trades channel is public and requires no authentication; the userFills channel requires an authenticated user context. For a complete overview of the connection lifecycle and generic subscriptions, see Hyperliquid WebSocket subscriptions and connection lifecycle. For the pull-based equivalent of user fills, see Reading Hyperliquid user fills and order status over the Info API.
The channel list and subscription message format are defined by the Hyperliquid WebSocket subscriptions documentation, the per-channel payload shapes by the WebSocket data-format documentation, and the pull surface a reconnect must reconcile against by the Info endpoint documentation. Treat those as the source of truth for field names and channel names.
- trades: coin-scoped, public, every trade on the market.
- userFills: user-scoped, authenticated, only your account's fills.
- Wrong scope is the most common integration error.
Subscription Protocol and Message Format
The Hyperliquid WebSocket subscription protocol follows a JSON message format documented in the Hyperliquid Docs, WebSocket subscriptions. To subscribe, the client sends a message with a type field set to "subscribe" and a subscription object containing the channel name and its parameter. For the trades channel, the parameter is the coin symbol; for userFills, the parameter is the user address. The server responds with a subscription acknowledgement that echoes the subscription details. To unsubscribe, the client sends a message with type "unsubscribe" and the same subscription object.
The acknowledgement is important for confirming that the subscription was accepted and for detecting errors such as an invalid coin name or missing authentication. The Hyperliquid Docs, WebSocket post requests and data formats specify the exact payload shapes for each channel. A typical subscription message looks like {"type":"subscribe","subscription":{"type":"trades","coin":"ETH"}} for trades, or {"type":"subscribe","subscription":{"type":"userFills","user":"0x..."}} for userFills. The acknowledgement will include the same subscription object, allowing the client to correlate the response with the request.
const WebSocket = require('ws');
const ws = new WebSocket('wss://api.hyperliquid.xyz/ws');
ws.on('open', () => {
// Subscribe to trades for ETH
ws.send(JSON.stringify({
type: 'subscribe',
subscription: { type: 'trades', coin: 'ETH' }
}));
// Subscribe to userFills for the authenticated account
ws.send(JSON.stringify({
type: 'subscribe',
subscription: { type: 'userFills', user: '0xYourAddress' }
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.channel === 'subscriptionResponse') {
console.log('Subscription acknowledged:', msg.data);
} else if (msg.channel === 'trades') {
console.log('Trade:', msg.data);
} else if (msg.channel === 'userFills') {
console.log('Fill:', msg.data);
}
});Payload Shapes and Fill Identity Fields
The trades channel payload includes fields such as coin, price, size, side, time, and a trade id. The userFills channel payload includes similar fields plus fee and closed PnL where applicable. According to the Hyperliquid Docs, WebSocket post requests and data formats, the exact field names and types are documented per channel. For deduplication, the stable identity is typically the combination of trade id (or fill id) and the user address for userFills, or the trade id alone for trades. Timestamps alone are not a stable identity because multiple trades can share the same millisecond, and the Info API may report records in a slightly different order.
When reconciling live WebSocket fills with the Info API userFills response, match on the shared identity fields—such as the fill id or trade id—rather than on timestamp. The Info API userFills endpoint returns records with a similar shape but may include additional fields or a different ordering. A deduplication set keyed by the fill identity ensures that boundary records are not duplicated. For a deeper look at the Info API surface, see Reading Hyperliquid user fills and order status over the Info API.
- trades: coin, price, size, side, time, trade id.
- userFills: adds fee, closed PnL, and user-specific fields.
- Stable identity: trade id or fill id, not timestamp.
Reconnect Gap and Backfill Strategy
A WebSocket connection delivers messages only while it is open. If the connection drops—due to network issues, server restarts, or client-side errors—any trades or fills that occurred during the gap are lost because the channel does not replay missed messages. This is a fundamental limitation of the streaming model. To recover, the client must detect the disconnect, record the timestamp of the last received message, and then backfill from the Info API. For userFills, the Info API userFills endpoint can be queried with a time range or by fetching recent fills. For trades, the coin trades view in the Info API can be used.
The backfill must be reconciled with the live stream to avoid duplicates. Because the Info API and the WebSocket may report the same fill with slightly different ordering or timestamps, deduplication must be based on the fill identity. A bounded deduplication set (e.g., a Set with a maximum size) can track recent fill ids and suppress duplicates at the boundary. The Hyperliquid Docs, Info endpoint (userFills) is the authoritative source for the pull equivalent. For rate limit considerations during backfill, see Hyperliquid API rate limits: Info versus Exchange.
const WebSocket = require('ws');
const fetch = require('node-fetch');
const WS_URL = 'wss://api.hyperliquid.xyz/ws';
const INFO_URL = 'https://api.hyperliquid.xyz/info';
const USER = '0xYourAddress';
const COIN = 'ETH';
const seenFills = new Set();
const MAX_SEEN = 10000;
let lastDisconnectTime = null;
function addSeen(id) {
if (seenFills.size >= MAX_SEEN) {
const first = seenFills.values().next().value;
seenFills.delete(first);
}
seenFills.add(id);
}
async function backfillUserFills(startTime) {
const res = await fetch(INFO_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'userFills',
user: USER,
startTime: startTime
})
});
const fills = await res.json();
for (const fill of fills) {
const id = fill.tid || fill.hash;
if (!seenFills.has(id)) {
addSeen(id);
console.log('Backfilled fill:', fill);
}
}
}
function connect() {
const ws = new WebSocket(WS_URL);
ws.on('open', () => {
console.log('Connected');
ws.send(JSON.stringify({
type: 'subscribe',
subscription: { type: 'trades', coin: COIN }
}));
ws.send(JSON.stringify({
type: 'subscribe',
subscription: { type: 'userFills', user: USER }
}));
if (lastDisconnectTime) {
backfillUserFills(lastDisconnectTime);
}
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.channel === 'userFills') {
for (const fill of msg.data) {
const id = fill.tid || fill.hash;
if (!seenFills.has(id)) {
addSeen(id);
console.log('Live fill:', fill);
}
}
} else if (msg.channel === 'trades') {
console.log('Trade:', msg.data);
}
});
ws.on('close', () => {
console.log('Disconnected');
lastDisconnectTime = Date.now();
setTimeout(connect, 1000);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
}
connect();Reconciliation Between Live and Pull Surfaces
Reconciliation is the process of matching live WebSocket fills to their Info API counterparts. The two surfaces may report the same fill with slightly different ordering or timestamps because they are generated by different internal paths. A timestamp-only deduplication strategy will either drop legitimate fills that share a timestamp or duplicate fills that have slightly different timestamps. The correct approach is to deduplicate on the fill identity—typically the trade id (tid) or a transaction hash—which is stable across both surfaces.
When backfilling after a disconnect, fetch the Info API userFills for the gap period and insert any fill whose identity is not already in the deduplication set. Because the Info API may return fills in a different order, the client should not assume that the first record is the oldest. Instead, process all records and rely on the identity set. For trades, the same principle applies: use the trade id to deduplicate between the live trades channel and the Info API coin trades view. For a related mechanism, see Hyperliquid funding rate mechanics.
- Match on fill identity (tid/hash), not timestamp.
- Process all backfill records; do not assume ordering.
- Use a bounded set to limit memory.
Runnable Node.js Example with Dedupe and Backfill
The following Node.js example uses the ws library to subscribe to trades for one coin and userFills for an account. It maintains a bounded deduplication set, detects disconnects, backfills via the Info API, and re-subscribes. The code is self-contained and can be run with node after installing ws and node-fetch. Replace the USER and COIN constants with your own values.
The example demonstrates the core mechanics: on open, it sends subscription messages; on message, it parses the channel and processes fills; on close, it records the disconnect time and schedules a reconnect; on reconnect, it backfills from the Info API using the recorded time. The deduplication set prevents duplicate fills at the boundary. This pattern is essential for any production integration that requires exactly-once processing of fills.
// Full example is provided in the previous section. This section repeats the key logic for clarity.
// See the code block above for the complete runnable script.Results Table for Self-Measurement
To validate your integration against your own endpoint, measure the following metrics. Fill in the table with your observed values. This is a verified-by-the-reader method; no benchmark numbers are provided here because they depend on your network, provider, and market activity. Use a consistent measurement window (e.g., 5 minutes) and record the results.
The table should include: message throughput (messages per second), observed fields per record (list the fields you see in trades and userFills payloads), gap duration on a forced reconnect (time between disconnect and successful re-subscription), records backfilled (number of fills fetched from the Info API during backfill), and duplicates suppressed (number of fills skipped by the deduplication set). This data helps you tune your deduplication set size and backfill strategy.
- Message throughput: ___ messages/sec.
- Observed fields per record: ___.
- Gap duration on forced reconnect: ___ ms.
- Records backfilled: ___.
- Duplicates suppressed: ___.
Failure Modes and Troubleshooting
Several failure modes are common when integrating with Hyperliquid's WebSocket channels. Subscribing to userFills without authentication will result in no data or an error; ensure the user parameter is a valid address and that the connection is authenticated if required. A coin-name case or naming mismatch (e.g., "eth" vs "ETH") will yield no trades; always use the exact symbol as documented. A silent connection that stops delivering messages may indicate a network issue or a server-side idle timeout; implement a ping/heartbeat and a liveness check that expects a message within a timeout window.
Duplicate fills after a reconnect occur when the boundary is not deduplicated. If you backfill from the Info API and also receive the same fill on the live stream, the deduplication set must catch it. If you see duplicates, verify that your identity key is consistent across both surfaces. For rate limit issues during bursty re-subscription, see Hyperliquid API rate limits: Info versus Exchange. For endpoint availability, see Hyperliquid RPC endpoints (RPC Assistant).
- userFills without auth: no data.
- Coin name mismatch: no trades.
- Silent connection: add heartbeat.
- Duplicates: check dedupe key.
Limitations and Tradeoffs
The historical depth of the Info API is limited; it may not provide fills older than a certain window. A long outage may require a wide backfill that consumes significant rate limit budget. The cost of a wide backfill increases with the number of coins and users you track. Additionally, bursty re-subscription can exceed connection or request limits, which vary by provider. Always consult the Rate Limits guidance and design your reconnect logic with exponential backoff.
The WebSocket channels do not replay missed messages, so the client is responsible for gap recovery. This means your application must persist the last seen fill identity and timestamp to disk or a database to survive process restarts. The dependency on the Info API for backfill introduces a second surface that must be reconciled, adding complexity. For OHLCV history, a different approach using candleSnapshot may be more appropriate; see Building Hyperliquid OHLCV history from candleSnapshot.
- Info API historical depth is limited.
- Wide backfill consumes rate limits.
- No replay: client must persist state.
- Provider limits vary.
Next Steps and Further Reading
To deepen your understanding, review the authoritative Hyperliquid documentation for WebSocket subscriptions and data formats, and the Info endpoint for userFills. For a broader view of the Hyperliquid network, see Hyperliquid network page. For pricing and service details, see RPC pricing and API service. The OnFinality Learn hub contains additional guides on Hyperliquid API usage.
When building production systems, consider using a managed RPC provider to handle connection reliability and rate limits. The Hyperliquid RPC endpoints (RPC Assistant) page can help you find suitable endpoints. Always test your integration against your own endpoint and measure the metrics in the results table to ensure correctness and performance.
- Review Hyperliquid Docs for subscriptions and data formats.
- Use the Info API for backfill and reconciliation.
- Measure your own throughput and gap recovery.
- Consider managed RPC for reliability.