@pear-protocol/hyperliquid
v0.1.9
Published
SDK for Pear Protocol Hyperliquid API
Readme
@pear-protocol/hyperliquid
TypeScript SDK for building on Pear Protocol — pair trading on Hyperliquid.
npm install @pear-protocol/hyperliquidimport { PearSdk } from '@pear-protocol/hyperliquid';
const sdk = new PearSdk();
const sdkWithCustomPearHost = new PearSdk({
basePath: 'https://your-pear-api.example.com',
pearWsUrl: 'wss://your-pear-api.example.com/ws',
});1. Market Data
Instruments
Real-time prices, funding rates, and leverage for every listed asset.
const unsub = sdk.market.instrument((instruments) => {
for (const token of instruments) {
console.log(token.assetName, token.markPrice, token.funding);
}
});| Field | Type | Description |
|-------|------|-------------|
| assetName | string | Symbol ("BTC", "ETH") |
| markPrice | number | Mark price |
| oraclePrice | number | Oracle price |
| prevDayPrice | number | Previous day price |
| priceChange24hPercent | number | 24h change % |
| funding | number | Funding rate |
| maxLeverage | number | Max leverage |
Basket Snapshot
Real-time computed metrics for any long/short basket. Weights are decimals (0-1), and the sum of all long + short weights must equal 1.
const unsub = sdk.market.basket(
[{ symbol: 'BTC', weight: 0.25 }, { symbol: 'ETH', weight: 0.25 }],
[{ symbol: 'SOL', weight: 0.5 }],
(snapshot) => {
snapshot.weightedRatio; // geometric mean ratio
snapshot.priceRatio; // long/short price (1L:1S only, else null)
snapshot.price; // single-asset price (1L or 1S only, else null)
snapshot.weightedRatio24hAgo; // weightedRatio 24h ago
snapshot.priceRatio24hAgo; // priceRatio 24h ago (1L:1S only, else null)
snapshot.price24hAgo; // price 24h ago (1L or 1S only, else null)
snapshot.netFundingRate; // weighted net funding in decimal form (e.g. 0.01 for 1%)
snapshot.maxLeverage; // largest max leverage across selected assets
snapshot.maxLeverageBySymbol; // { BTC: 50, ETH: 25, ... }
},
);Ideas (Picks)
Pre-curated trading baskets streamed via WebSocket. Includes actives, watchlist (when authenticated), and AI-generated pairs.
const unsub = sdk.market.picks((baskets) => {
for (const b of baskets) {
b.categories; // ('active' | 'watchlist' | 'ai-picks')[]
b.name; // optional display name
b.longTokens; // TokenSelection[] (symbol + weight)
b.shortTokens;
b.weightedRatio; // computed from live prices
b.netFundingRate;
b.oi; // basket open interest (USD)
b.volume; // basket 24h volume (USD)
b.maxLeverage;
b.maxLeverageBySymbol;
}
});Filter by category on the client:
const aiPicks = baskets.filter((b) => b.categories.includes('ai-picks'));Chart
Historical and real-time OHLCV candle data for any basket or individual asset.
const { chart } = sdk.market;
// 1. Configure tokens
chart.setTokens(
[{ symbol: 'BTC', weight: 0.25 }, { symbol: 'ETH', weight: 0.25 }],
[{ symbol: 'SOL', weight: 0.5 }],
);
// 2. Set candle interval (default: '1h')
chart.setCandleInterval('15m');
// 3. Fetch historical bars (requires chart type)
const bars = await chart.getBars('weighted-ratio', startMs, endMs);
const assetBars = await chart.getAssetBars('BTC', startMs, endMs);
// 4. Real-time basket updates
const subId = chart.subscribeRealtimeBars('weighted-ratio', (bar) => {
console.log(bar.time, bar.close);
});
// 5. Real-time single-asset updates
const assetSubId = chart.subscribeRealtimeAssetBars('BTC', (bar) => {
console.log(bar.time, bar.close);
});
// 6. Unsubscribe
chart.unsubscribeRealtimeBars(subId);
chart.unsubscribeRealtimeBars(assetSubId);
// 7. Get earliest data boundary (useful for "no data" pagination)
const boundary = chart.getEffectiveDataBoundary(); // ms timestamp | null
// 8. Clear cached data (call after changing tokens or interval)
chart.clearCache();
// 9. Cleanup
chart.destroy();Chart Types
| Type | Description |
|------|-------------|
| 'weighted-ratio' | Geometric mean of weighted long basket / short basket prices |
| 'price-ratio' | Long / short price ratio (1L:1S pairs only, else empty) |
| 'performance' | Individual asset performance from a baseline — use with overlays |
| 'price' | Single-asset price |
Candle Intervals
type CandleInterval =
| '1m' | '3m' | '5m' | '15m' | '30m'
| '1h' | '2h' | '4h' | '8h' | '12h'
| '1d' | '3d' | '1w' | '1M';Bar Shape
interface Bar {
time: number; // ms timestamp
open: number;
high: number;
low: number;
close: number;
}Orderbook
Real-time Hyperliquid L2 orderbook snapshots for a single asset. Orderbooks use the same shared Hyperliquid WebSocket connection as sdk.market.chart, and sdk.market.createOrderbook uses the latest instrument mark price for aggregation params.
const orderbook = await sdk.market.createOrderbook({
symbol: 'BTC',
depth: 10,
aggregation: 0,
});
const subId = orderbook.subscribe((snapshot) => {
snapshot.symbol; // 'BTC'
snapshot.bids; // price-descending levels
snapshot.asks; // price-ascending levels
snapshot.aggregation; // active aggregation bucket
snapshot.ts; // ms timestamp
});
const bbo = orderbook.bbo;
bbo.bestBid;
bbo.bestAsk;
bbo.spread;
bbo.spreadPct;
orderbook.setAggregation(100);
const snapshot = orderbook.getSnapshot(); // OrderbookSnapshot | null
orderbook.unsubscribe(subId);
orderbook.destroy();Orderbook Types
interface OrderbookLevel {
price: number;
size: number;
}
interface OrderbookSnapshot {
symbol: string;
bids: OrderbookLevel[];
asks: OrderbookLevel[];
aggregation: number;
ts: number;
}TradingView Datafeed Integration
The SDK chart plugs into TradingView's IBasicDataFeed. Two key methods to wire up:
getBars — Historical Data
Convert TradingView's resolution to a CandleInterval, set it on the chart, then fetch bars. TradingView passes timestamps in seconds — multiply by 1000 for the SDK.
// Map TradingView resolution strings to SDK intervals:
// '1' → '1m', '5' → '5m', '60' → '1h', '240' → '4h', '1D' → '1d', etc.
const sdkInterval = tvResolutionToSdkInterval(resolution);
chart.setCandleInterval(sdkInterval);
const startMs = periodParams.from * 1000;
const endMs = periodParams.to * 1000;
// For basket symbols → chart.getBars(chartType, startMs, endMs)
// For individual assets (e.g. 'LONG:BTC') → chart.getAssetBars('BTC', startMs, endMs)
const bars = await chart.getBars('weighted-ratio', startMs, endMs);
// When no data, use getEffectiveDataBoundary() to tell TradingView where data starts
if (bars.length === 0) {
const boundary = chart.getEffectiveDataBoundary();
onResult([], { noData: true, nextTime: boundary ? boundary / 1000 : undefined });
}subscribeBars — Real-time Updates
Subscribe to live bar updates. Map each TradingView listenerGuid to an SDK subscription ID for cleanup.
// For basket symbols:
const subId = chart.subscribeRealtimeBars('weighted-ratio', (bar) => {
onTick({ time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close });
});
// For individual assets (used in 'performance' overlays):
const subId = chart.subscribeRealtimeAssetBars('BTC', (bar) => {
onTick({ time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close });
});
// Unsubscribe:
chart.unsubscribeRealtimeBars(subId);2. Authentication
EIP-712 signature-based login. Tokens auto-refresh on 401.
// 1. Get message to sign
const { data: msg } = await sdk.api.public.auth.getEIP712Message(address, clientId, chainId);
// 2. Sign with wallet (e.g. wagmi signTypedData)
const signature = await signTypedData({ ... });
// 3. Login
await sdk.api.public.auth.login({
address,
clientId: CLIENT_ID,
details: { signature, timestamp: eip712.timestamp, chainId },
method: 'eip712',
});
// 4. Set address for WebSocket subscriptions
sdk.setAddress(address);Token persistence:
// Restore from storage
sdk.api.setTokens(savedAccessToken, savedRefreshToken);
// Read current tokens
const accessToken = sdk.api.getAccessToken();
const refreshToken = sdk.api.getRefreshToken();
// Listen for refreshes
sdk.api.onTokenRefreshed = (access, refresh) => {
localStorage.setItem('tokens', JSON.stringify({ access, refresh }));
};
sdk.api.onLogout = () => {
localStorage.removeItem('tokens');
};Hyperliquid Readiness Checks
Before trading, the wallet must be onboarded on Hyperliquid and have the required approvals:
const onboarded = await sdk.user.isHyperliquidOnboarded();
const builderOk = await sdk.user.isBuilderApproved();
const agentOk = await sdk.user.isAgentApproved();Hyperliquid onboarding — isHyperliquidOnboarded() checks the Hyperliquid userRole endpoint and returns true only when the wallet role is user.
Builder fee approval — if isBuilderApproved() returns false, the user must send an approveBuilderFee action to the Hyperliquid exchange endpoint (docs):
// Sign and send via Hyperliquid exchange API
{
"type": "approveBuilderFee",
"builder": "0xA47D4d99191db54A4829cdf3de2417E527c3b042",
"maxFeeRate": "0.06%"
}Agent wallet approval — if isAgentApproved() returns false, create and approve via:
await sdk.api.private.agentWallet.create();3. Trading
Open a Position
await sdk.api.private.positions.create({
usdValue: 1000,
leverage: 5,
longAssets: [{ asset: 'BTC', weight: 0.25 }, { asset: 'ETH', weight: 0.25 }],
shortAssets: [{ asset: 'SOL', weight: 0.5 }],
executionType: 'MARKET',
slippage: 0.02,
takeProfit: { type: 'PERCENTAGE', value: 10 },
stopLoss: { type: 'PERCENTAGE', value: -5 },
});Close
await sdk.api.private.positions.close(positionId, { executionType: 'MARKET' });
await sdk.api.private.positions.closeAll({ executionType: 'MARKET' });Adjust Size & Leverage
await sdk.api.private.positions.adjust(positionId, {
adjustmentType: 'INCREASE',
usdValue: 500,
executionType: 'MARKET',
});
await sdk.api.private.positions.adjustLeverage(positionId, {
leverage: 10,
});Update TP/SL
await sdk.api.private.positions.updateRiskParameters(positionId, {
takeProfit: { type: 'PERCENTAGE', value: 15 },
stopLoss: { type: 'PERCENTAGE', value: -8, trailingStop: true },
});Rebalance Weights
Preview then execute weight changes on an existing position:
const { data: plan } = await sdk.api.private.positions.planRebalance(positionId, {
targetWeights: { BTC: 0.3, ETH: 0.2, SOL: 0.5 },
});
// plan.assets — what will be traded
// plan.skippedAssets — what can't be changed and why
await sdk.api.private.positions.rebalance(positionId, {
targetWeights: { BTC: 0.3, ETH: 0.2, SOL: 0.5 },
});Adjust (Advanced)
Set target absolute sizes per asset on an existing position:
await sdk.api.private.positions.adjustAdvance(positionId, [{
longAssets: [{ asset: 'BTC', size: 0.5 }, { asset: 'ETH', size: 2.0 }],
shortAssets: [{ asset: 'SOL', size: 10.0 }],
}]);TWAP Orders
await sdk.api.private.positions.create({
usdValue: 5000,
leverage: 3,
longAssets: [{ asset: 'BTC', weight: 0.5 }],
shortAssets: [{ asset: 'ETH', weight: 0.5 }],
executionType: 'TWAP',
twapDuration: 3600,
twapInterval: 300,
});Spot Orders
await sdk.api.private.orders.spot({
coin: 'HYPE',
isBuy: true,
sz: 100,
limitPx: null,
});Cancel Orders
await sdk.api.private.orders.cancel(orderId);
await sdk.api.private.orders.cancelTwap(orderId);4. Tracking
All subscriptions return an unsubscribe function. Require sdk.setAddress() first.
Positions
Enriched with real-time mark prices from Hyperliquid.
const unsub = sdk.user.positions((positions) => {
for (const pos of positions) {
pos.positionId;
pos.entryRatio;
pos.markRatio;
pos.unrealizedPnl;
pos.unrealizedPnlPercentage;
pos.marginUsed;
pos.positionValue;
pos.stopLoss;
pos.takeProfit;
for (const asset of [...pos.longAssets, ...pos.shortAssets]) {
asset.coin;
asset.entryPrice;
asset.actualSize;
asset.leverage;
asset.unrealizedPnl;
asset.currentWeight;
asset.fundingPaid;
asset.liquidationPrice;
}
}
});Orders
sdk.user.orders((orders) => {
for (const ord of orders) {
ord.orderId;
ord.orderType; // 'MARKET' | 'TRIGGER' | 'TWAP' | 'TP' | 'SL'
ord.status; // 'OPEN' | 'PROCESSING' | 'EXECUTED'
ord.longAssets;
ord.shortAssets;
}
});Account & History
sdk.user.account((account) => {
account.accountMode;
account.withdrawableByCollateral;
});
const unsubMargin = sdk.user.basketMargin(
[{ symbol: 'BTC', weight: 0.5 }],
[{ symbol: 'ETH', weight: 0.5 }],
(basket) => {
const margin = basket.calculate({
leverageBySymbol: { BTC: 5, ETH: 5 },
notionalUsd: 1000,
});
margin.maxSizeUsd;
margin.canOpen;
margin.perCollateral;
margin.availableMarginByCollateral;
},
);
const maxSize = sdk.user.getMaxBasketSize(
[{ symbol: 'BTC', weight: 0.5 }],
[{ symbol: 'ETH', weight: 0.5 }],
{ leverageBySymbol: { BTC: 5, ETH: 5 } },
);
sdk.user.accountSummary((summary) => { ... });
sdk.user.tradeHistories((histories) => { ... });
sdk.user.twapDetails((twaps) => { ... });
sdk.user.notifications((notifications) => {
for (const n of notifications) {
n.category; // 'TP_ORDER_FILLED' | 'POSITION_LIQUIDATED' | ...
n.parameters; // typed per category
n.is_read;
}
});5. REST API Reference
Public (no auth)
await sdk.api.public.markets.list({ searchText: 'BTC', sort: 'volume' });
await sdk.api.public.markets.active();
await sdk.api.public.fills.list({ address: '0x...', assetName: 'BTC' });
await sdk.api.public.stats.addressStats('0xabc,0xdef');Private (requires auth)
// Account
await sdk.api.private.accounts.summary();
await sdk.api.private.portfolio.get();
// Agent wallet
await sdk.api.private.agentWallet.get();
await sdk.api.private.agentWallet.create();
// API keys
await sdk.api.private.apiKeys.list();
await sdk.api.private.apiKeys.create({ name: 'Bot Key' });
// Positions — see Trading section above
await sdk.api.private.positions.list();
await sdk.api.private.positions.create({ ... });
await sdk.api.private.positions.close(id, { ... });
await sdk.api.private.positions.closeAll({ ... });
await sdk.api.private.positions.adjust(id, { ... });
await sdk.api.private.positions.adjustAdvance(id, [{ longAssets: [{ asset, size }], shortAssets: [{ asset, size }] }]);
await sdk.api.private.positions.adjustLeverage(id, { ... });
await sdk.api.private.positions.updateRiskParameters(id, { ... });
await sdk.api.private.positions.planRebalance(id, { targetWeights });
await sdk.api.private.positions.rebalance(id, { targetWeights });
// Orders
await sdk.api.private.orders.list({ page: 1, limit: 50, status: 'EXECUTED' });
await sdk.api.private.orders.open();
await sdk.api.private.orders.twap();
await sdk.api.private.orders.cancel(orderId);
await sdk.api.private.orders.cancelTwap(orderId);
await sdk.api.private.orders.spot({ ... });
await sdk.api.private.orders.triggers({ category: 'TP' });
await sdk.api.private.orders.kalshiTriggers({ category: 'active', search: '', cursor: '', pageSize: 20 });
// History
await sdk.api.private.tradeHistory.list({ limit: 100 });
await sdk.api.private.notifications.list({ limit: 50 });
await sdk.api.private.notifications.markRead({ ids: ['...'] });
// Watchlist
await sdk.api.private.watchlist.get();
await sdk.api.private.watchlist.toggle({ longAssets: [...], shortAssets: [...] });
// Sync
await sdk.api.private.sync.fills({ user, fills, assetPositions });6. Error Handling
All API methods throw PearApiError on failure. Each error carries a machine-readable code, HTTP statusCode, and human-readable message.
import { PearApiError, PearErrorCode } from '@pear-protocol/hyperliquid';
try {
await sdk.api.private.positions.close(positionId, { executionType: 'MARKET' });
} catch (err) {
if (err instanceof PearApiError) {
err.statusCode; // 404
err.code; // 'POSITION_NOT_FOUND'
err.message; // 'Position abc123 not found or already closed'
err.originalError; // raw AxiosError (for headers, response body, etc.)
}
}Handling by Status Code
try {
await sdk.api.private.positions.create({ ... });
} catch (err) {
if (!(err instanceof PearApiError)) throw err;
switch (err.statusCode) {
case 400: showValidationError(err.message); break;
case 404: showNotFound(err.message); break;
case 408: retryAfterDelay(); break; // ACTIVE_TRADE_TIMEOUT
case 409: showConflict(err.message); break;
case 429: backoff(); break;
default: showGenericError(); break;
}
}Handling by Error Code
if (err instanceof PearApiError && err.code === PearErrorCode.ACTIVE_TRADE_TIMEOUT) {
// Another trade is in progress, retry shortly
await sleep(2000);
return retry();
}Network Errors
When the server is unreachable, PearApiError is thrown with statusCode: 0 and code: 'NETWORK_ERROR'.
if (err instanceof PearApiError && err.statusCode === 0) {
showOfflineMessage();
}Error Codes Reference
| Code | Status | When |
|------|--------|------|
| POSITION_NOT_FOUND | 404 | Position doesn't exist or already closed |
| POSITION_NOT_OPEN | 400 | Position exists but isn't open |
| POSITION_UNAUTHORIZED | 403 | Position belongs to another user |
| ORDER_NOT_FOUND | 404 | Order doesn't exist |
| INVALID_ORDER_STATUS | 400 | Order can't be cancelled in current status |
| INVALID_ORDER_TYPE | 400 | Wrong order type for the endpoint |
| ACTIVE_TRADE_TIMEOUT | 408 | Another trade is still processing |
| UNSUPPORTED_EXECUTION_TYPE | 400 | Execution type not supported for this action |
| DUPLICATE_TRIGGER | 409 | Position already has an active trigger |
| INVALID_ADDRESS | 400 | Malformed wallet address |
| MISSING_REQUIRED_FIELD | 400 | Required parameter missing |
| INVALID_FIELD_VALUE | 400 | Parameter value is invalid |
| UNSUPPORTED_TRIGGER_TYPE | 400 | Trigger type not supported |
| INVALID_POSITION_STRUCTURE | 400 | Position assets don't match trigger requirements |
| INVALID_LADDER_CONFIG | 400 | Missing or invalid ladder configuration |
| TWAP_DURATION_REQUIRED | 400 | TWAP order missing duration |
| TWAP_INSUFFICIENT_VALUE | 400 | Asset value too low for TWAP chunking |
| INVALID_RISK_PARAMETERS | 400 | Can't update TP/SL for this position |
| HL_CANCEL_FAILED | 500 | Failed to cancel order on Hyperliquid |
| LEVERAGE_CONFIG_FAILED | 400 | Leverage configuration failed |
| VAULT_WALLET_NOT_FOUND | 404 | Vault wallet doesn't exist |
| VAULT_UNAUTHORIZED | 403 | Vault doesn't belong to this user |
| VAULT_UNSUPPORTED_TOKEN | 400 | Token not supported for this vault operation |
| INTERNAL_ERROR | 500 | Unexpected server error |
| NETWORK_ERROR | 0 | Server unreachable |
7. Cleanup
sdk.destroy(); // closes all WebSocket connections and subscriptions