Skip to content
Alchemy Logo

Migrating from the Hyperliquid API

Beta

HyperCore APIs are in beta. Message fields, limits, and behavior may change, including breaking changes. To request access, contact [email protected].

Alchemy's HyperCore streams use the same stream names and the same core data as Hyperliquid's public WebSocket (wss://api.hyperliquid.xyz/ws), with a few request and envelope differences. Code written for the public API needs small changes before it works here.

  • Book updates every block. The public l2Book feed is throttled; Alchemy sends a market's book whenever it changes.
  • Many markets or addresses in one subscription, or every market at once.
  • Order-level books (l4BookUpdates) and incremental L2 books (l2BookDiff).
  • Fill replay after a reconnect (fromBlock) and a per-subscription seq to detect missed messages.
  • Block number and block time on every data message.

Hyperliquid public APIAlchemy
Endpointwss://api.hyperliquid.xyz/wswss://hypercore-mainnet.g.alchemy.com/v2/<API_KEY>
Subscription IDNonesubscriptionId required on every request
One market"coin": "BTC""coins": ["BTC"]
All marketsOne subscription per marketOmit coins (or send [])
One user"user": "0x…""users": ["0x…"]
UnsubscribeRepeat the subscription object{"method":"unsubscribe","subscriptionId":…}
Data shapedata: {coin, time, levels}data: {blockNumber, blockTime, isSnapshot, books: [{coin, levels}]}
Fillsdata: {user, fills: [fill]}data.fills: [[user, fill]], one [user, fill] pair per fill
Heartbeat{"method":"ping"}Send a WebSocket protocol ping frame every 30 seconds instead. {"method":"ping"} returns an invalid_params error. Idle connections close after about 90 seconds (see heartbeats)
TradestradesUse allFills: one record per side, both sides share tid
ReplayNonefromBlock on fill streams

Public API:

{"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}

Alchemy:

{"method":"subscribe","subscriptionId":"btc-book","subscription":{"type":"l2Book","coins":["BTC"]}}

Public API data:

{"channel":"l2Book","data":{"coin":"BTC","time":1791999593526,"levels":[[…bids],[…asks]]}}

Alchemy data:

{"seq":1,"channel":"l2Book","subscriptionId":"btc-book","data":{"blockNumber":1183759643,"blockTime":1791999593526,"isSnapshot":true,"books":[{"coin":"BTC","levels":[[…bids],[…asks]]}]}}

Price levels (px, sz, n) and fill objects use the same fields as the public API. Fills arrive as [user, fill] pairs, so read fill.coin, not pair.coin.

  • Sending coin instead of coins. Unsupported fields are ignored, which turns a one-market subscription into an all-markets subscription. Always use the array form.
  • Omitting subscriptionId. The request is rejected.
  • Changing the case of coin names. Names are case-sensitive: BTC works, but btc is treated as an unknown market. It's accepted and returns empty data (for example "bbo":[null,null]), so a wrong case looks like a quiet market.
  • Detecting gaps with block numbers. Book streams skip blocks with no change. Use seq.
  • Reading data as a single market. Alchemy groups markets in data.books (or data.fills, data.diffs). Iterate over the array. data.mids is a map of coin to price.

Run both feeds side by side for a few minutes and compare values by coin and block time. Book levels and fills should match exactly; Alchemy delivers more frequent book updates because the public feed is throttled.

Was this page helpful?