@markpaper/hl-kit
Practical add-on toolkit for Hyperliquid on top of the
@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
/infoand/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
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:
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
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
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
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
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
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
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
// 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-3positionValuequirk. - Assets:
hip3AssetIdfor dexes other thanxyz, spot registry (listSpotAssets,spotToken), spot price decimals (pxDecimals(…, 'spot')). - Errors: several
classifyExchangeErrorkinds use wording that has not been verified against live responses. - Transport: response-size weight surcharge,
500 nulltreated as an invalid request,surplusAlreadyInCap. - Account: spot fee rates,
fetchSubAccounts,fetchUserAbstraction, borrowed stables in equity. - History:
fetchUserFunding/fetchFundingHistorypagination andfundingPayment. - 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
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.
Under Section 4(d) of the license, any redistribution of this software or of a derivative work must retain the 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.