src/ws/types.ts
v0.3.0 · 13 KB
// Subscription and frame types for the Hyperliquid WebSocket API.
//
// Every number inside a payload (prices, sizes, accountValue, szi, pnl, ...)
// arrives as a STRING. Keep them as strings until you convert them with
// decimal-safe code; never feed them straight into float arithmetic.
//
// Spot balances are NOT delivered over the WebSocket: `allDexsClearinghouseState`
// carries perp state only. Poll REST `spotClearinghouseState` (a cache TTL of
// ~5 minutes is enough) and add it to every WS update, otherwise the
// spot part of equity silently "disappears" on each push.
/** `0x`-prefixed hex address. The client lowercases it before sending. */
export type HexAddress = `0x${string}`;
export type CandleInterval =
| '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M';
// ---------------------------------------------------------------------------
// Subscriptions (the `subscription` object of `{method:"subscribe"}`)
// ---------------------------------------------------------------------------
/**
* Trade tape of one coin. HIP-3 coins use the dex prefix (`xyz:TSLA`), spot coins `@107` / `PURR/USDC`.
* Coin names are case-sensitive: an unknown or wrong-case coin makes the server drop the whole socket
* (live, 2026-09-15). The first frame right after subscribe contains recent trades, so frames repeat
* after every (re)subscribe: dedupe by `tid` (see {@link createTidDeduper}).
* @experimental The frame shape was only spot-checked on a live socket (main, HIP-3) on 2026-09-15.
*/
export interface TradesSubscription { type: 'trades'; coin: string }
/**
* Order book of one coin. Frames are pushed ONLY when the book changes, so silence on a quiet
* market is normal: detect a dead socket by the silence of all frames (pong included), not by `l2Book`.
* `nSigFigs` / `mantissa` aggregation is documented but was not verified live. The server
* echoes the subscription with extra fields (`nSigFigs:null, mantissa:null, fast:false`); ack matching
* handles that. An unknown or wrong-case coin makes the server drop the socket (live, 2026-09-15).
*/
export interface L2BookSubscription {
type: 'l2Book';
coin: string;
/** @experimental Level aggregation by significant figures (documented, unverified). */
nSigFigs?: 2 | 3 | 4 | 5 | null;
/** @experimental Only valid together with `nSigFigs: 5` (documented, unverified). */
mantissa?: 2 | 5 | null;
}
/**
* Live candles; the last bar is updated until it closes. Minimum interval is `1m`.
* @experimental Frame shape follows the public docs; not checked against a live socket.
*/
export interface CandleSubscription { type: 'candle'; coin: string; interval: CandleInterval }
/**
* Mid prices of every coin of one dex in one frame. `dex: ""` is the main dex and is dropped from the
* canonical form. Main-dex frames carry no `dex` field, HIP-3 frames carry it (`dex: "xyz"`, keys
* `xyz:COIN`). An unknown dex is acknowledged but never pushes.
* @experimental Only spot-checked on a live socket on 2026-09-15.
*/
export interface AllMidsSubscription { type: 'allMids'; dex?: string }
/**
* Cheap top of book (best bid / best ask).
* @experimental Frame shape follows the public docs; not checked against a live socket.
*/
export interface BboSubscription { type: 'bbo'; coin: string }
/**
* Mark / oracle price and current funding of one coin. Spot coins (`@107`) answer on the
* `activeSpotAssetCtx` channel with a spot context (no funding / oracle); the client routes both
* channels to this subscription. An unknown coin gets `Invalid subscription {...}` on the error channel.
* @experimental Only spot-checked on a live socket on 2026-09-15.
*/
export interface ActiveAssetCtxSubscription { type: 'activeAssetCtx'; coin: string }
/**
* Asset contexts of every perp dex.
* @experimental Not described in the knowledge base; shape taken from the SDK type declarations.
*/
export interface AllDexsAssetCtxsSubscription { type: 'allDexsAssetCtxs' }
/**
* Fills of one user. The FIRST frame after every (re)subscribe has `isSnapshot: true` and contains
* HISTORY, not new fills: applying it as new fills doubles positions/PnL.
* Uses one slot of the per-IP unique-user limit.
*/
export interface UserFillsSubscription {
type: 'userFills';
user: string;
/** @experimental Aggregates partial fills by time, like REST `userFillsByTime` (declared, unverified). */
aggregateByTime?: boolean;
}
/**
* Order status changes of one user. `open` arrives BEFORE the HTTP response of the order placement,
* and an Alo rejection arrives twice (placement response + `badAloPxRejected` here): dedupe by oid.
* Frames carry no `user` field, so with several `orderUpdates` users on one client every handler
* receives every `orderUpdates` frame. Uses one unique-user slot.
*/
export interface OrderUpdatesSubscription { type: 'orderUpdates'; user: string }
/**
* Fills / funding / liquidation / non-user-cancel events of one user; frames arrive on channel `user`
* and carry no `user` field (same routing caveat as `orderUpdates`). Uses one unique-user slot.
* @experimental Not verified live; shape taken from the SDK type declarations.
*/
export interface UserEventsSubscription { type: 'userEvents'; user: string }
/**
* Aggregated user state blob. After a position closes the snapshot can lag for seconds.
* Uses one unique-user slot. Does not carry spot balances in a trustworthy way: use REST.
* @experimental Only mentioned in the knowledge base; the frame shape is not documented there.
*/
export interface WebData2Subscription { type: 'webData2'; user: string }
/**
* Aggregated user blob. The ONLY place with the exact collateral mode string
* (`data.userState.abstraction`). Uses one unique-user slot.
*/
export interface WebData3Subscription { type: 'webData3'; user: string }
/**
* Full perp state of one user across all dexes (HIP-3 included). A snapshot arrives 0.5-2 s after
* subscribe - EVEN for addresses the server then refuses to track - and afterwards only on changes.
* An empty account stays silent after the first snapshot. No spot balances. Uses one unique-user slot.
*/
export interface AllDexsClearinghouseStateSubscription { type: 'allDexsClearinghouseState'; user: string }
/** Escape hatch for subscription types this module does not model. A string `user` field is counted as a tracked user. */
export interface GenericSubscription { type: string; [key: string]: unknown }
export type KnownSubscription =
| TradesSubscription
| L2BookSubscription
| CandleSubscription
| AllMidsSubscription
| BboSubscription
| ActiveAssetCtxSubscription
| AllDexsAssetCtxsSubscription
| UserFillsSubscription
| OrderUpdatesSubscription
| UserEventsSubscription
| WebData2Subscription
| WebData3Subscription
| AllDexsClearinghouseStateSubscription;
export type WsSubscription = KnownSubscription | GenericSubscription;
// ---------------------------------------------------------------------------
// Frame payloads (`data` of `{channel, data}`)
// ---------------------------------------------------------------------------
export interface WsTrade {
coin: string;
/** Aggressor (taker) side: `B` = buy, `A` = sell. */
side: 'B' | 'A';
px: string;
sz: string;
time: number;
hash: string;
tid: number;
/** `[buyer, seller]` */
users: [string, string];
}
export interface WsBookLevel { px: string; sz: string; n: number }
export interface WsL2Book {
coin: string;
time: number;
levels: [bids: WsBookLevel[], asks: WsBookLevel[]];
}
export interface WsCandle {
/** Open time, ms. */ t: number;
/** Close time, ms. */ T: number;
/** Coin. */ s: string;
/** Interval. */ i: CandleInterval;
o: string; c: string; h: string; l: string;
/** Volume in base units. */ v: string;
/** Number of trades. */ n: number;
}
export interface WsAllMids { mids: Record<string, string>; dex?: string }
export interface WsBbo { coin: string; time: number; bbo: [bid: WsBookLevel | null, ask: WsBookLevel | null] }
export interface WsPerpAssetCtx {
markPx: string;
oraclePx: string;
midPx?: string | null;
funding: string;
openInterest: string;
dayNtlVlm: string;
prevDayPx: string;
premium?: string | null;
impactPxs?: [string, string] | null;
dayBaseVlm?: string;
}
/** Context of a spot coin (channel `activeSpotAssetCtx`). */
export interface WsSpotAssetCtx {
markPx: string;
midPx?: string | null;
prevDayPx: string;
dayNtlVlm: string;
dayBaseVlm?: string;
circulatingSupply?: string;
totalSupply?: string;
coin?: string;
[key: string]: unknown;
}
/** Perp coins deliver {@link WsPerpAssetCtx}; spot coins (`@107`) deliver {@link WsSpotAssetCtx}. */
export interface WsActiveAssetCtx { coin: string; ctx: WsPerpAssetCtx | WsSpotAssetCtx }
/** @experimental Shape from the SDK type declarations. */
export interface WsAllDexsAssetCtxs { ctxs: Array<[dex: string, ctxs: WsPerpAssetCtx[]]> }
export interface WsFill {
coin: string;
px: string;
sz: string;
side: 'B' | 'A';
time: number;
startPosition: string;
dir: string;
closedPnl: string;
hash: string;
oid: number;
crossed: boolean;
fee: string;
tid: number;
feeToken: string;
builderFee?: string;
cloid?: string;
twapId?: number | null;
[key: string]: unknown;
}
export interface WsUserFills {
user: string;
fills: WsFill[];
/** `true` on the first frame after each (re)subscribe: history, not new fills. */
isSnapshot?: boolean;
}
/**
* Statuses confirmed on a live socket: `open`, `badAloPxRejected`; `filled` / `canceled` are observed
* too. The rest match `historicalOrders` snapshots; any `...Rejected` / `...Canceled` may appear.
*/
export type WsOrderStatus =
| 'open' | 'filled' | 'canceled' | 'triggered' | 'rejected' | 'badAloPxRejected'
| 'marginCanceled' | 'reduceOnlyCanceled' | 'selfTradeCanceled' | 'siblingFilledCanceled'
| 'scheduledCancel' | 'liquidatedCanceled' | 'delistedCanceled'
| (string & {});
export interface WsOrderUpdate {
order: {
coin: string;
side: 'B' | 'A';
limitPx: string;
/** REMAINING size. */
sz: string;
oid: number;
origSz: string;
timestamp: number;
cloid?: string;
reduceOnly?: boolean;
};
status: WsOrderStatus;
/** Event time; fall back to `order.timestamp` when missing. */
statusTimestamp: number;
}
/** @experimental Shape from the SDK type declarations, not verified live. */
export type WsUserEvent =
| { fills: WsFill[] }
| { funding: { time: number; coin: string; usdc: string; szi: string; fundingRate: string; nSamples: number | null } }
| { liquidation: { lid: number; liquidator: string; liquidated_user: string; liquidated_ntl_pos: string; liquidated_account_value: string } }
| { nonUserCancel: Array<{ coin: string; oid: number }> }
| Record<string, unknown>;
/** @experimental The knowledge base does not document the webData2 frame shape. */
export interface WsWebData2 { user?: string; [key: string]: unknown }
export interface WsWebData3 {
userState: {
user: string;
/** `disabled` (manual), `unifiedAccount`, `dexAbstractionEnabled` (legacy); other values may appear. */
abstraction?: string;
dexAbstractionEnabled?: boolean;
[key: string]: unknown;
};
perpDexStates: unknown[];
[key: string]: unknown;
}
export interface WsAssetPosition {
position?: {
/** May be missing: resolve the name from `meta.universe[asset].name` of the SAME dex. */
coin?: string;
asset?: number;
szi: string;
unrealizedPnl: string;
marginUsed: string;
entryPx: string;
/** `value` can transiently be 0 or missing: do not compute ROE stops from it on that tick. */
leverage?: { value: number | string };
[key: string]: unknown;
};
[key: string]: unknown;
}
export interface WsClearinghouseState {
marginSummary?: { accountValue?: string; totalMarginUsed?: string };
crossMarginSummary?: { accountValue?: string };
assetPositions?: WsAssetPosition[];
time?: number;
[key: string]: unknown;
}
/**
* Every frame is an authoritative full snapshot; a dex missing from the array has no state.
* `dex` `''` (or `null`) is the main HL perp dex. No spot balances here - use REST.
* Sanity rule before money decisions: positions present but `totalMarginUsed == 0` means an
* incomplete payload - fall back to REST.
*/
export interface WsAllDexsClearinghouseState {
user: string;
clearinghouseStates: Array<[dex: string | null, state: WsClearinghouseState]>;
}
/** Maps a subscription `type` to the `data` of its frames. */
export interface WsEventMap {
trades: WsTrade[];
l2Book: WsL2Book;
candle: WsCandle;
allMids: WsAllMids;
bbo: WsBbo;
activeAssetCtx: WsActiveAssetCtx;
allDexsAssetCtxs: WsAllDexsAssetCtxs;
userFills: WsUserFills;
orderUpdates: WsOrderUpdate[];
userEvents: WsUserEvent;
webData2: WsWebData2;
webData3: WsWebData3;
allDexsClearinghouseState: WsAllDexsClearinghouseState;
}
/** `data` type delivered to a handler of subscription `S`. */
export type WsEventFor<S> = S extends { type: infer T }
? T extends keyof WsEventMap ? WsEventMap[T] : unknown
: unknown;
/** Raw server frame envelope. */
export interface WsFrame { channel: string; data?: unknown }