README.md
v0.3.0 · 11.6 KB
# @markpaper/hl-kit
Practical add-on toolkit for [Hyperliquid](https://hyperliquid.xyz) on top of the
[`@nktkas/hyperliquid`](https://github.com/nktkas/hyperliquid) SDK.
The SDK signs and sends requests. `hl-kit` does **not** replace it and does not duplicate it: it turns
practical rules of the Hyperliquid API, each with its reason and check date, into small, tested
functions — the parts that usually get rewritten (and broken) in every project:
- price and size rounding that the exchange accepts, the $10 minimum and when a size may be bumped;
- asset ids for perps, spot and HIP-3 dexes, with a validated, cached meta registry;
- a weight-aware throttle for `/info` and `/exchange`, and retries that never double-send an order;
- full account equity (every perp dex + spot stables) that survives account-mode changes;
- fills history with pagination and TWAP slices, round trips, candles with closure status;
- fees, builder fee units, leverage, liquidation and stop math;
- a long-lived WebSocket client with ack tracking, a silence watchdog and reconnect;
- safe order placement with fail-closed per-order outcomes and reconciliation by cloid.
Most modules are pure functions. Everything that reads from Hyperliquid takes an `InfoRequester` —
a plain function `(body, opts?) => Promise<json>` — so you can use the kit's throttled client, a raw
`fetch`, or an SDK `InfoClient` wrapped in a function, and stub it in tests.
## Install
```sh
npm i @markpaper/hl-kit @nktkas/hyperliquid viem
```
Requires **Node.js 22.12+** (the same floor as `@nktkas/hyperliquid` 0.33). ESM only.
`@nktkas/hyperliquid` and `ws` are optional peer dependencies: read-only use (formatting, registry,
equity, history) needs neither. Node 22+ and browsers have a global `WebSocket`; install `ws` only if you
want its socket options (see `NODE_WS_SOCKET_OPTIONS`) or run in a runtime without a global `WebSocket`.
## Imports
Everything is exported flat from the package root, and each module is also available as a namespace:
```ts
import { formatPrice, createAssetRegistry } from '@markpaper/hl-kit';
import { format, assets, transport, errors, account, history, risk, ws, orders } from '@markpaper/hl-kit';
```
| Namespace | What it covers |
| --- | --- |
| `format` | price/size rounding, min notional, slippage prices, pre-send validation |
| `assets` | asset ids, coin names, meta registry, leverage params, US-equity market hours |
| `transport` | throttled `/info` client, weight limiter, retry, HTTP errors, address budget, cache |
| `errors` | parsing `/exchange` responses, error classification, retry advice |
| `account` | snapshot of all dexes, equity, positions, fees, agents, roles |
| `history` | fills, TWAP slices, round trips, PnL, candles, funding, cache keys |
| `risk` | fee tiers, fill accounting, builder fee, margin, liquidation, stop/TP trigger prices, fee-tier arithmetic |
| `ws` | WebSocket client, limits, subscription keys, self-heal, dedup |
| `orders` | intent → wire order, placement, reconciliation, close, cancel, TWAP, agent check |
### Renamed exports
A few names exist in more than one module. In the flat export they are resolved like this (the
namespaces keep the original names):
| Flat export | Source | Note |
| --- | --- | --- |
| `parseUserFees` | `account.parseUserFees` | returns `UserFees`; pairs with `fetchUserFees` |
| `parseUserFeeRates` | `risk.parseUserFees` | returns `AccountFeeRates` (adds staking discount, spot bps); pairs with `readUserFees` |
| `normalizeAddress` | `account.normalizeAddress` | throws `HlAccountError` |
| `normalizeWsAddress` | `ws.normalizeAddress` | throws `WsSubscriptionError` |
| `PositionSide` | `risk.PositionSide` | `'long' \| 'short'` |
| `FillPositionSide` | `history.PositionSide` | `'LONG' \| 'SHORT'` |
| `isSpotCoin` | `assets.isSpotCoin` | identical to `history.isSpotCoin` |
| `MarketKind`, `Numeric` | `format` | identical types in `assets` / `risk` |
| `CandleInterval` | `history` | identical type in `ws` |
## Quick examples
Addresses below are placeholders (`0xYOUR_ADDRESS`).
### Transport and assets
```ts
import { createInfoClient, createAssetRegistry, createWeightLimiter } from '@markpaper/hl-kit';
// Workload policy is explicit; the exchange ceiling is not a safe application default.
const limiter = createWeightLimiter({
weightPerMinute: config.weightPerMinute,
burstCapacity: config.burstCapacity,
maxConcurrent: config.maxConcurrent,
});
const info = createInfoClient({ network: 'mainnet', limiter });
// One process-wide registry: TTL cache, single-flight, stale-on-error, validated meta.
const registry = createAssetRegistry(info);
const btc = await registry.resolve('BTC'); // { assetId: 0, szDecimals, maxLeverage, ... }
const lookup = await registry.lookup('xyz:TSLA'); // 'listed' | 'unlisted' | 'unknown'
if (lookup.status === 'unknown') {
// Meta is unavailable right now. Never treat this as a delisting.
}
```
### Balance and equity
```ts
import { fetchAccountSnapshot, computeEquity, normalizePositions } from '@markpaper/hl-kit';
const snapshot = await fetchAccountSnapshot(info, '0xYOUR_ADDRESS'); // every perp dex + spot, one tick
const equity = computeEquity(snapshot);
// total = Σ perp accountValue (all dexes) + free stables; null when the spot leg is unreadable
console.log(equity.total, equity.perpEquity, equity.spotFreeStables, equity.marginRatio, equity.trusted);
for (const p of normalizePositions(snapshot)) console.log(p.coin, p.side, p.size, p.roe);
```
### Rounding and an order
```ts
import { ExchangeClient, HttpTransport } from '@nktkas/hyperliquid';
import { privateKeyToAccount } from 'viem/accounts';
import { formatPrice, sizeFromNotional, validateOrder, placeOrders, reconcileByCloid } from '@markpaper/hl-kit';
const px = formatPrice('97123.456', { szDecimals: btc.szDecimals, mode: 'passive', side: 'buy' });
const sz = sizeFromNotional(25, px, btc.szDecimals);
const check = validateOrder({ px, sz, szDecimals: btc.szDecimals }); // { ok, issues, p, s, notional }
const exchange = new ExchangeClient({ transport: new HttpTransport(), wallet: privateKeyToAccount('0x...') });
const result = await placeOrders(exchange, registry, [
{ coin: 'BTC', side: 'buy', size: '0.001', price: '95000', tif: 'Alo' },
{ coin: 'ETH', side: 'sell', size: '0.01', market: { slippage: 0.01 } }, // IoC at mid - 1%
], { info });
for (const o of result.orders) console.log(o.coin, o.outcome.status);
if (result.needsReconcile) {
// Some order may be live without confirmation: block new placements on those markets first.
for (const cloid of result.reconcileCloids) await reconcileByCloid(info, '0xYOUR_ADDRESS', cloid);
}
```
`closePosition`, `marketClose`, `cancelOrders`, `limitExchange` (exchange calls through the same
explicit limiter), `interpretExchangeError` / `recommendRetry` and `getAddressBudget` cover the rest of the
order lifecycle.
### Fills history with TWAP
```ts
import {
fetchFillsByTime, fetchTwapSliceFills, mergeFillsWithTwap, summarizePnl,
} from '@markpaper/hl-kit';
const since = Date.now() - 7 * 86_400_000;
const fills = await fetchFillsByTime(info, '0xYOUR_ADDRESS', { startTime: since }); // paged, dedup by tid
const twap = await fetchTwapSliceFills(info, '0xYOUR_ADDRESS', { startTime: since });
const all = mergeFillsWithTwap(fills.fills, twap.fills, { from: since });
console.log(all.length);
console.log(summarizePnl(fills.fills).net); // closedPnl - fees (+ funding when passed)
```
### Candles
```ts
import { fetchCandles, fetchServerTimeMs, findCandleGaps } from '@markpaper/hl-kit';
const serverTimeMs = await fetchServerTimeMs(info);
const res = await fetchCandles(info, {
coin: 'xyz:TSLA', // HIP-3 coin is sent prefixed, without `dex`
interval: '1h',
startTime: serverTimeMs - 30 * 86_400_000,
serverTimeMs, // decides whether the newest bar is closed
});
console.log(res.candles.length, res.historyLimited, findCandleGaps(res.candles, '1h'));
```
HL keeps only about 5000 most recent bars per interval; `historyLimited` tells you the window was cut.
### Fees and liquidation
```ts
import {
readUserFees, feeRatesForVolume, roundTripFeeUsd, liquidationRoe, stopLiquidationBuffer,
} from '@markpaper/hl-kit';
const fees = await readUserFees(info, '0xYOUR_ADDRESS'); // userAddRate = maker, userCrossRate = taker
console.log(fees.makerBps, fees.takerBps, feeRatesForVolume(6_000_000).takerBps);
roundTripFeeUsd(10_000, fees.takerBps); // both sides, for backtests
liquidationRoe(10, btc.maxLeverage); // ROE at which the position is liquidated
stopLiquidationBuffer(-0.25, 10, btc.maxLeverage); // <= 0: the stop never fires before liquidation
```
### WebSocket
```ts
// Optional on Node 22+: the global WebSocket is used when `WebSocket` is omitted.
import WebSocket from 'ws';
import { createWsClient, NODE_WS_SOCKET_OPTIONS, type WebSocketConstructorLike } from '@markpaper/hl-kit';
const client = createWsClient({
WebSocket: WebSocket as unknown as WebSocketConstructorLike, // not needed on Node 22+ / browsers
socketOptions: NODE_WS_SOCKET_OPTIONS,
});
client.onError((e) => console.warn(e));
const off = client.subscribe({ type: 'trades', coin: 'BTC' }, (trades) => console.log(trades.length));
// later
off();
client.close();
```
WS gives speed, REST is the truth: reconcile positions, orders and balances through REST periodically.
Spot balances are not delivered over WS.
## Experimental parts
Anything tagged `@experimental` in JSDoc comes from documentation, SDK types or a single observation
and was not verified live. It is implemented only where a wrong guess fails safe; the tag
describes the risk. Check these on testnet before relying on them:
- **TWAP**: `placeTwap`, `cancelTwap`, `fetchTwapSliceFillsByTime`.
- **Orders**: cancel by cloid, `scheduleCancel` / `clearScheduledCancel` (dead man's switch),
`reconcileByCloid`, close-size estimation for the HIP-3 `positionValue` quirk.
- **Assets**: `hip3AssetId` for dexes other than `xyz`, spot registry (`listSpotAssets`, `spotToken`),
spot price decimals (`pxDecimals(…, 'spot')`).
- **Errors**: several `classifyExchangeError` kinds use wording that has not been verified against live responses.
- **Transport**: response-size weight surcharge, `500 null` treated as an invalid request,
`surplusAlreadyInCap`.
- **Account**: spot fee rates, `fetchSubAccounts`, `fetchUserAbstraction`, borrowed stables in equity.
- **History**: `fetchUserFunding` / `fetchFundingHistory` pagination and `fundingPayment`.
- **Risk**: staking/referral discount stacking, `parseFeeScheduleTiers`, `estimateLiquidationPrice`.
- **WebSocket**: connection-rate and message-rate limits, some subscription options and fields.
## Knowledge base
The rules behind every function are documented, with the reason and the check date, in
the Hyperliquid knowledge base `knowledge/hl/` of the markpaper repository (start from
`knowledge/hl/README.md`). The knowledge base is licensed separately under CC BY 4.0.
## Development
```sh
npm run typecheck
npm test # unit tests, no network
npm run build
npm run smoke # read-only live check against public mainnet data (no addresses, no keys)
```
## License
Apache License 2.0 — see [LICENSE](LICENSE).
Under Section 4(d) of the license, any redistribution of this software or of a derivative work must
retain the [NOTICE](NOTICE) file and credit **"markpaper — hl-kit"**.
## Disclaimer
This is not financial advice and not a recommendation to trade. The Hyperliquid API, limits, fees and
response shapes change without notice. Verify behaviour against the official documentation and on
**testnet** before trading real funds, start with minimal size, and use the software at your own risk.
The software is provided "as is", without warranties of any kind.