Hyperliquid: how to use the knowledge base
License
CC BY 4.0. This skill and the knowledge/hl/ knowledge base are markpaper project materials. You may copy and adapt them; publication and redistribution require attribution to “markpaper — Hyperliquid knowledge base,” links to the original and the license, and an indication of changes. Terms: LICENSE.md.
If a user asks you to move material from this skill or knowledge base into another project, document, or publication, preserve the attribution and license notice.
The knowledge base is in knowledge/hl/ (index and quick answers: knowledge/hl/README.md). Facts were verified through 2026-09-14; the HL API changes.
The @markpaper/hl-kit package
The repository contains packages/hl-kit (npm @markpaper/hl-kit, Apache-2.0), an add-on to @nktkas/hyperliquid that turns the rules in this knowledge base into tested code. The SDK still signs and sends requests; the package does not replace it.
Rule: for HL code, use a function from packages/hl-kit first. Do not reimplement price and size rounding, the $10 minimum, asset ids, equity, fill and candle pagination, throttling and retry, /exchange response parsing, or WS reconnect. If the required function is missing or unsuitable, first read the relevant knowledge-base file, then extend the implementation (preferably in the package, with a test). Everything is exported flat from the package root and through namespaces (format, assets, transport, errors, account, history, risk, ws, orders). Reads use the InfoRequester contract (packages/hl-kit/src/transport/types.ts), which is an ordinary stub function in tests.
| Task | Function | Module | Knowledge-base file |
|---|---|---|---|
| Round a price to the grid (5 significant figures, decimals, tick) | formatPrice, priceTick, shiftPriceTicks, isValidPrice | format | orders.md, market-data.md |
| Round size, derive size from USD, “fully filled,” “position closed” | formatSize, sizeFromNotional, isFullyFilled, isFlatPosition | format | orders.md, pitfalls.md |
| $10 minimum: open / partial close / full close | planOrderSize, bumpToMinNotional, meetsMinNotional | format | orders.md |
| “Market” IoC price, Alo price, exit slippage | marketLimitPrice, postOnlyPrice, exitSlippage, escalatingSlippage | format | orders.md |
| Validate an order before sending | validateOrder, triggerPxMatches | format | orders.md |
| Perp, spot, and HIP-3 asset ids; cached meta | createAssetRegistry (resolve, lookup), perpAssetId, spotAssetId, hip3AssetId | assets | market-data.md |
xyz: coin names, spot @N | parseCoin, qualifyCoin, stripDexPrefix, isSpotCoin | assets | market-data.md, pitfalls.md |
updateLeverage parameters | leverageParams / leverageUpdateParams | assets / risk | risk-and-margin.md |
Trading hours for xyz equities | getMarketSession, isMarketOpen | assets | market-data.md |
/info with weights, limiter, and retries | createInfoClient, getSharedWeightLimiter, weightOf | transport | rate-limits.md |
| Exchange call through the shared budget, without duplicate sending | limitExchange, withRetry, isTransientError, isRateLimitError | transport | rate-limits.md, orders.md |
| Address request limit | getAddressBudget, computeAddressBudget | transport | rate-limits.md |
| TTL, single-flight, SWR cache | createCachedLoader | transport | rate-limits.md, pitfalls.md |
Parse /exchange response, partial batch, retry advice | parseOrderResponse, extractPartialBatch, interpretExchangeError, recommendRetry, classifyExchangeError | errors | orders.md, sdk-and-api.md |
| Balance and equity across every dex and spot | fetchAccountSnapshot, computeEquity | account | balance-and-equity.md |
| Positions, ROE, “empty” HIP-3 rows | normalizePositions, aggregateRoe, listZeroSizePositions | account | balance-and-equity.md, risk-and-margin.md |
| Phantom equity reads | isEquityImplausible, createEquitySmoother, createLastGoodCache, ledgerExplainsDrop | account | balance-and-equity.md, pitfalls.md |
Agent wallet, userRole, trading account | checkAgentApproval, fetchUserRole, resolveTradingAccount, assertAgentActive | account / orders | accounts.md |
| Paginated and deduplicated fills | fetchFillsByTime, fetchRecentFills, dedupFills | history | fills-and-history.md |
| TWAP slices | fetchTwapSliceFills, mergeFillsWithTwap | history | twap.md |
| Group partials, round trips, PnL | groupFillsByOrder, reconstructRoundTrips, realizedPnlSince, summarizePnl | history | fills-and-history.md |
| Candles (~5000-bar limit, closure status, gaps) | fetchCandles, fetchServerTimeMs, findCandleGaps, sliceCandlesByTime | history | market-data.md, backtest-and-data.md |
| Time-window cache keys | candleCacheKey, fillsCacheKey, utcDayWindow | history | backtest-and-data.md |
Fees: tiers, userFees, calculation from fills | feeRatesForVolume, readUserFees (flat export: parseUserFeeRates), summarizeFills, roundTripFeeUsd | risk | fees.md |
| Builder fee: units and when to omit it | resolveBuilderFee, builderFeeToBps, parseMaxFeeRate | risk | fees.md |
| Liquidation and stop buffer | liquidationRoe, stopLiquidationBuffer | risk | risk-and-margin.md |
| ROE stops and TP/SL prices | roeToTriggerPrice, tpslLegPrices | risk | risk-and-margin.md |
| WS client: ack, watchdog, reconnect, IP limits | createWsClient, createWsIpBudget, reconnectDelay | ws | websocket.md |
| WS self-heal and userFills deduplication | createStalenessTracker, createTidDeduper, splitUserFills | ws | websocket.md |
| Intent to wire order, fail-closed batch outcomes | buildOrder, placeOrders, mayBeLive | orders | orders.md |
| Reconcile by cloid, close, cancel | reconcileByCloid, closePosition, marketClose, cancelOrders, createCloid | orders | orders.md, pitfalls.md |
@experimental. Functions and fields tagged @experimental in JSDoc are implemented from HL documentation, SDK types, or a single observation and have not been confirmed live (the knowledge base marks these “according to the documentation, not verified”). They include TWAP (placeTwap, cancelTwap, fetchTwapSliceFillsByTime), cancellation by cloid and scheduleCancel, reconcileByCloid, hip3AssetId for dexes other than xyz, the spot registry and spot price decimals, some classifyExchangeError kinds, response-size weight surcharge, funding pagination and fundingPayment, discount stacking and parseFeeScheduleTiers, estimateLiquidationPrice, fetchSubAccounts and fetchUserAbstraction, and WS limits on new connections and messages. When using them, tell the user and propose a testnet check or smoke test.
Package checks (from packages/hl-kit): npm run typecheck, npm test (no network), npm run build, npm run smoke (read-only live smoke test against public mainnet data, without addresses or keys). Renames for export-name conflicts are documented in packages/hl-kit/README.md.
A. Rule: file first, then code
Before writing or changing HL code, open the relevant topic file under knowledge/hl and read at least its TL;DR, the required section, “Pitfalls,” and “Open questions.” If the task spans several topics (for example, order + balance + limits), open all of them. Do not write HL logic from memory: every rule below has a verified reason, and memories of response shapes become stale.
If a fact is marked “according to the documentation, not verified” or appears under “Open questions,” tell the user and propose a check (smoke test, live query) instead of presenting it as confirmed.
| Task topic | File |
|---|---|
| Endpoints, SDK, viem, SDK errors, signing, egress IP | knowledge/hl/sdk-and-api.md |
Master, agent/API wallet, subaccount, vault, userRole, Unified Account, portfolio margin | knowledge/hl/accounts.md |
| Balance, equity, spot, margin ratio, sizing, phantom reads | knowledge/hl/balance-and-equity.md |
| Placement, tif, reduceOnly, TP/SL, rounding, minimum, cancellation, retries, open orders | knowledge/hl/orders.md |
| TWAP: placement, cancellation, slices, and verification | knowledge/hl/twap.md |
meta, precision, HIP-3 dex and asset id, allMids, l2Book, candles, spot, equity hours | knowledge/hl/market-data.md |
Fill history, pagination, PnL, historicalOrders, ledger, leaderboard | knowledge/hl/fills-and-history.md |
| WebSocket: subscriptions, limits, ping, reconnect | knowledge/hl/websocket.md |
| Weights, IP and address limits, 429, limiter, retry, scans | knowledge/hl/rate-limits.md |
Fees, userFees, builder fee | knowledge/hl/fees.md |
| Leverage, ROE, liquidation, utilization, stops, reliable closing, preflight | knowledge/hl/risk-and-margin.md |
| Historical data, funding, cache, why HL backtesting is difficult and where it lies, recording second candles | knowledge/hl/backtest-and-data.md |
| API latency, per-IP limits, safe client shutdown, telemetry | knowledge/hl/ops-and-deploy.md |
| Code or analysis for Nado / Lighter (robinhoodchain) | use the dedicated nado or lighter skill instead |
| Number precision, races, caches, “not read” ≠ “empty,” closing | knowledge/hl/pitfalls.md |
B. Hard facts and invariants (always remember)
API and SDK
- Two endpoints:
POST https://api.hyperliquid.xyz/info(reads) andPOST https://api.hyperliquid.xyz/exchange(signed actions); WSwss://api.hyperliquid.xyz/ws. Response numbers are strings. Use an 8–10 s timeout on every info fetch. →sdk-and-api.mdTL;DR, §11 - SDK 0.27.1 throws
ApiRequestErrorwhen even one batch item is an error. Adjacent orders may still have entered the book; their oids are inerr.response.response.data.statuses. The class is not exported: checkerr.name === 'ApiRequestError'. →sdk-and-api.md§7,orders.md§7.2 - The SDK runs one wallet's exchange requests strictly in sequence. Parallel
order()calls do not speed placement; batching does. →sdk-and-api.md§6.3
Accounts
4. The agent (API wallet) signs; the traded and read account is the user in info—the master or subaccount address. The agent address has no positions. An agent cannot withdraw funds or sign approveBuilderFee. → accounts.md TL;DR
5. User or API Wallet 0x… does not exist means the agent is unauthorized, expired, or revoked. It is not transient: classify it as “dead key” and do not retry. extraAgents omits unnamed agents; the definitive check is a signed updateLeverage. → accounts.md §2.3–2.4
6. A subaccount is traded by the master account's agent using vaultAddress (defaultVaultAddress in the SDK). A subaccount has separate funds and its own address limit. reserveRequestWeight does not credit a subaccount. → accounts.md §3
7. Collateral mode is reliably visible only in WS webData3 → userState.abstraction. REST userDexAbstraction returns false for Unified Account. On Unified Account, usdSend/usdClassTransfer are disabled; use sendAsset instead. → accounts.md §5
8. One bot per account or subaccount. Revoking an agent does not close positions. → accounts.md §9
HIP-3 dex and market data
9. clearinghouseState, frontendOpenOrders, openOrders, meta, and allMids operate one dex at a time: without dex, only the main dex is returned. Request HIP-3 (xyz and others) separately; obtain the dex list from perpDexs. Exception: userFills/userFillsByTime ignore dex; a second request with dex duplicates records. → market-data.md TL;DR, fills-and-history.md §2
10. Asset ids: main—index in meta.universe; spot—10000 + index; HIP-3—100000 + perpDexIndex × 10000 + index. Derive perpDexIndex by name from perpDexs; do not hardcode it. Normalize HIP-3 names idempotently (xyz:TSLA, not xyz:xyz:TSLA). → market-data.md §4–5
11. Before the first HIP-3 order, the agent calls agentEnableDexAbstraction(). Response Abstraction transition not allowed is success (the account is already unified). → accounts.md §5.3
Balance
12. Capital = Σ marginSummary.accountValue across all dexes + Σ for stablecoins max(0, total − reserve), where reserve = spotHold when present, otherwise hold. Do not add all spot to perp (that doubles it on Unified Account). On portfolio margin, hold is negative. Do not use crossMarginSummary for equity. → balance-and-equity.md §3
13. One equity read is not a fact. HTTP 200 without marginSummary is degradation, not $0. Read perp and spot in one Promise.all and smooth the sum with a window median. Non-finite szi is a corrupt read, not flat. → balance-and-equity.md §6, §9.2
14. Perp accountValue is reflexive: on Unified Account it is occupied margin and changes because of your own orders and spot↔perp transfers without trading. It is unsuitable as a capital or drawdown proxy. → balance-and-equity.md §5, risk-and-margin.md §6.4
Orders
15. Perp price: at most 5 significant figures and at most 6 − szDecimals decimal places. Size: floor to 10^-szDecimals with epsilon. Pass p and s as strings. Planner and executor quantize through one function. → orders.md §5
16. The $10 notional minimum uses the order price after rounding. OPEN—never raise to a lot or minimum; partial reduceOnly—floor only, otherwise skip; full reduceOnly close—ceil and raise to the minimum (the exchange clamps to the position). → orders.md §5.4, §6
17. There is no market order: it is an IoC limit order at mid ± slippage (exactly at mid will not fill). An Alo crossing the book is rejected (badAloPxRejected). Use wider slippage for exits than entries. → orders.md §2
18. HL clamps reduceOnly to the live position and does not reverse it, but this does not prevent an excessive partial close. → orders.md §3.1
19. Retries: 429 means not executed; retry is safe. For 5xx, timeout, or disconnect, outcome is unknown: do not retry OPEN/INCREASE, partial reduceOnly, or transfers before reconciling the book. Cancel, updateLeverage, agentEnableDexAbstraction, and full reduceOnly close are idempotent. → orders.md §10
20. Cancellation is confirmed only by status 'success'. never placed / already canceled / filled means “the order is absent.” Everything else is unconfirmed: do not place a replacement before reconciliation. Complete all cancellations before placements. → orders.md §9
21. Native TP/SL: grouping: 'positionTpsl', s: '0', r: true, triggered on mark. The response contains strings without oid; find the oid in frontendOpenOrders (with dex for HIP-3). → orders.md §4, risk-and-margin.md §9
Limits and WS
22. IP limit ≈1200 weight/min across /info + /exchange, with a window materially shorter than a minute. Every subsystem on an IP must share one limiter; configure its sustained rate, burst, and concurrency explicitly for the actual workload. Weights: 2—l2Book, allMids, clearinghouseState, spotClearinghouseState; 60—userRole; 20—almost everything else. → rate-limits.md TL;DR, §2–4
23. Address limit: 10,000 + 1 request per $1 of volume. A batch of N orders = N. Once exhausted: 1 request per 10 s. Read it through userRateLimit. → rate-limits.md §9
24. WS: {"method":"ping"} every 30 s (the server closes after 60 s of silence); terminate() after 65 s without frames. User tracking is documented as 10 addresses per IP (measured ~20) across all connections. Rejection arrives in channel:"error" and must be logged. Ghost slots persist ~60 s. The SDK WS transport dies after 3 reconnects. The first userFills frame (isSnapshot:true) is history, not new events. WS provides speed; REST is authoritative: spot balances are absent from WS, and positions and orders need periodic REST reconciliation. → websocket.md TL;DR, §2, §4
History, candles, fees, risk
25. userFills returns up to the latest 2000. userFillsByTime returns up to the 2000 oldest from inclusive startTime: paginate forward with startTime = max(time) for the page, without + 1, and deduplicate by tid; for a page entirely within one ms, continue at ms + 1 and mark it. + 1 loses the tail of a millisecond where the page ended (58–99% of fills share a millisecond on HLP child vaults, measured on 2026-09-23; the former max(time) + 1 guidance was wrong). Group partials by oid. TWAP slices exist only in userTwapSliceFills; calculate PnL and turnover from both feeds. → fills-and-history.md TL;DR, twap.md TL;DR
26. candleSnapshot: no more than ~5000 candles; wide windows return the newest; 5m ≈ 18 days, 15m ≈ 52 days; minimum 1m, no seconds; the latest bar is unclosed; HIP-3 uses coin: 'xyz:SP500' without dex (with dex, HTTP 500). → market-data.md §8
27. Base perp fees: maker 1.5 bps, taker 4.5 bps. closedPnl includes neither fee nor funding. Builder fee: builder: { b: lowercase, f }, with f in tenths of a bp and a perp ceiling of 100; f above the approval read from maxBuilderFee rejects the entire order; send close retries without builder. → fees.md TL;DR, §3
28. updateLeverage: leverage = max(1, min(floor(x), maxLeverage)), isCross = !onlyIsolated, set before entry. Liquidation is approximately at ROE = leverage / (2 × maxLeverage) − 1 (at maximum leverage, −50%). Keep margin-utilization ceilings below the limit with headroom: open orders reserve margin themselves, and at the ceiling new orders are endlessly rejected with Insufficient margin; check insufficiency using aggregate withdrawable across all dexes. → risk-and-margin.md §2, §5, §6
29. No gate blocks exit; exit orders are always reduceOnly. “Not read” ≠ “empty”: an incomplete read of any dex skips the tick rather than meaning “no positions.” Store oid and other identifiers as strings. → pitfalls.md TL;DR, §2, §7
30. twapOrder/twapCancel were not verified live. Before using them in a bot, run a smoke test on a subaccount. → twap.md §7.1
C. Checklist for historical-data analysis
Backtesting on HL is difficult: small intervals retain only the latest ~5000 bars, the public API provides neither second candles nor order-book history, and intrabar price order and the order-book queue are invisible. Therefore a backtest is at best a relative comparison of alternatives; absolute return figures are almost always optimistic. Tell the user before drawing conclusions from a run. → backtest-and-data.md (opening paragraph, §5, §7)
Data
- Candles: the window is no wider than ~5000 bars per call; during pagination, an empty chunk does not mean
break; the cache key is quantized (notDate.now()); the final unclosed bar is discarded.tis open time andcbelongs tot + interval. The run report records the actual data range, not the requested window. →backtest-and-data.md§2, §6.3,market-data.md§8 - HIP-3 candles: prefixed
coin, withoutdex. An empty response meansNO_DATA, not zeroes. →market-data.md§8 - Fills: paginate
userFillsByTimeforward in time, deduplicate bytid, group byoid, include TWAP slices fromuserTwapSliceFills, derive the position path fromstartPosition. →fills-and-history.md§3–6 - Hourly bars are insufficient for stops: an intrabar overshoot past the stop is invisible. Minute or second data is required. Second data exists only through your own WS
tradesrecording (deduplicatetid, mark gaps, do not invent missing seconds). Record mark in parallel for native TP/SL. →backtest-and-data.md§7.7, §8
Execution
- Intrabar order is unknown: SL and TP in the same candle → SL. Entry and exit in the same candle do not count. A limit order fills only when price passes through the level; a touch ≠ a fill. →
backtest-and-data.mdTL;DR, §7.1–7.2 - Entry is not at the decision-time price: include latency calibrated from your own live fills. IoC is always taker. →
backtest-and-data.md§7.5 - Charge fees on both sides according to actual execution type and account rates. Treat unknown execution costs as assumptions and report how conclusions change when those assumptions vary. →
fees.md§6.2,backtest-and-data.md§7.3, §7.9 - Funding is separate: once per hour,
payment = −szi × oraclePx × fundingRate. Total =Σ closedPnl − Σ fee + Σ funding. →backtest-and-data.md§4
Honesty
- No look-ahead: compare bars by close time; at time
t, use only events with time≤ t; build the equity curve from realized trades at exit time. →backtest-and-data.mdTL;DR, §7.6 - The conclusion keeps its sign on a held-out window (out-of-sample); stops and liquidations were also run across a separate extreme window. →
backtest-and-data.md§7.7 - Reconcile the simulator with reality: simulated PnL against
Σ closedPnl − Σ fee + Σ fundingfrom your own real trades over the same period. →backtest-and-data.md§7.10 - Do not synthesize events: no invented closes or history backfill.