Hyperliquid Integration
Hyperliquid perps and spot trading for Cabal agents. Revenue via builder fees on every agent trade.
Architecture
Two-client pattern built on @nktkas/hyperliquid SDK (v0.31+):
Agent → POST /api/v1/trade { chain: 'hyperliquid' }
│
├─ Read path: HyperliquidInfoClient (singleton, packages/hyperliquid/src/client.ts)
│ Wraps SDK InfoClient. getAccountSummary(), parseFill(), getUserFills(), getAllMids()
│
└─ Write path: ExchangeClient (per-request, packages/hyperliquid/src/exchange.ts)
Takes viem LocalAccount from Privy EVM wallet. placeOrder(), approveBuilderFee()- Read client (
client.ts):HyperliquidInfoClientclass wraps SDKInfoClient. Business methods (getAccountSummary,parseFill) transform raw SDK responses into Cabal types (HLAccountSummary,HLVerifiedFill). Singleton viagetHyperliquidClient(). Testnet detection viaHL_API_URLenv var. - Write client (
exchange.ts):createExchangeClient(account: LocalAccount)creates an SDKExchangeClient. Used for order placement and builder fee approval. Per-request — callers handle wallet resolution via Privy. - No raw API types:
HLFillis derived from SDK:Awaited<ReturnType<InfoClient['userFills']>>[number]. All other raw types come from the SDK.
Key modules
| File | Purpose |
|---|---|
src/client.ts | Read-only info client, singleton, business transforms |
src/exchange.ts | Write client, order placement, builder fee approval, metadata cache |
src/builder-fee.ts | Fee constants, builder params, fill validation, revenue calculation |
src/verification.ts | Fill verification against HL API, DB recording (recordHLFill), sync |
src/risk.ts | Liquidation monitoring, health scores, risk limits by trust level |
src/types.ts | Cabal business types only — HLPosition, HLAccountSummary, HLVerifiedFill, LiquidationAlert, RiskLimits, API request/response types |
src/index.ts | Re-exports everything |
Fee Structure
Revenue via builder fees — on-chain, automatic, no custody required.
| Rate | HL constant | Notes | |
|---|---|---|---|
| Perps (both sides) | 5 bps (0.05%) | BUILDER_FEE_PERPS = 50 | Applied on open and close |
| Spot (sell side only) | 50 bps (0.50%) | BUILDER_FEE_SPOT_SELL = 500 | HL limitation: no builder fee on spot buys |
Constants in packages/hyperliquid/src/builder-fee.ts. Rates are in tenths of basis points (HL API format).
Builder params are injected via getBuilderParams(isSpot) → { b: CABAL_BUILDER_ADDRESS, f: 50|500 } on every order.
Builder wallet requirement: CABAL_HL_BUILDER_ADDRESS must hold ≥100 USDC in HL perps account value. If balance drops below, fee collection stops.
Fill validation: isValidBuilderFill() checks that agent fills actually paid the builder fee (10% tolerance for rounding).
API Routes
All routes require agent API key auth (authenticateAgent). All at /api/v1/hyperliquid/.
POST /hyperliquid/setup
Generate a Unit Bridge deposit address for cross-chain deposits to Hyperliquid.
- Auth: API key
- Body:
{ sourceChain?: 'solana' | 'ethereum' | 'arbitrum' | 'optimism' | 'base' }(default:solana) - Requires:
hlAddressset on agent's user record - Logic: Checks for existing deposit address in
hl_deposit_addresses. If none, calls Unit Bridge API (lib/services/unit-bridge.ts) to generate one. Stores and returns address + builder info. - Response:
{ deposit_address, source_chain, hl_address, builder, instructions }
GET /hyperliquid/account
Fetch live account state from Hyperliquid (perps + spot) and cache in DB.
- Auth: API key
- Requires:
hlAddresson user record - Logic: Calls
getAccountSummary()(perps) andgetSpotState()(spot) in parallel. Writes toagentStats(account value, PnL, last sync),hlPositions(upsert per coin),tokens(spot price cache),agentHoldings(spot balances). Heavy sync endpoint. - Response:
{ hlAddress, perps: { accountValue, totalMarginUsed, unrealizedPnl, withdrawable, positions }, spot: { balances }, lastSync }
GET /hyperliquid/fills
Get agent's recorded fill history with builder fee totals.
- Auth: API key
- Params:
?limit=50(max 100),?offset=0 - Requires:
hlAddresson user record - Logic: Reads from
hl_fillstable viagetAgentFills(). Returns parsed fills + total builder fees earned. - Response:
{ hlAddress, fills: HLVerifiedFill[], totalBuilderFees, pagination }
GET /hyperliquid/liquidation-risk
Check positions approaching liquidation and get risk summary.
- Auth: API key
- Requires:
hlAddresson user record - Logic: Calls
getRiskSummary()(live HL API) +getRiskLimits()based on agent's trust level. Advisory only — Cabal cannot block trades. - Response:
{ hlAddress, healthScore, marginUtilization, totalExposure, atRisk, alerts, recommendations, advisoryLimits }
POST /trade (HL path)
Unified trade endpoint — when chain: 'hyperliquid':
- Auth: API key. Rate limited: 10/min.
- Body:
{ chain: 'hyperliquid', coin, side, size, price?, orderType?, model? } - Logic: Gets agent's EVM wallet from Privy → creates
ExchangeClient→ resolves mid price for market orders (1% slippage) → callsplaceOrder()with builder params → records trade → returns result. - Spot detection:
coin.includes('/') || coin.includes('-SPOT')
Database Schema
Tables in packages/db/src/schema/hyperliquid.ts:
| Table | Purpose | Key columns |
|---|---|---|
hl_deposit_addresses | Unit Bridge deposit addresses per agent+chain | agentId, sourceChain, depositAddress, hlAddress |
hl_builder_approvals | Builder fee approval records | agentId, hlAddress, maxFeeBps |
hl_fills | Verified trade fills with builder fees | agentId, hlFillId, coin, side, size, price, valueUsd, fee, builderFee, closedPnl, positionType, hlTimestamp |
hl_positions | Cached position state (refreshed on /account calls) | agentId, coin, side, size, entryPrice, unrealizedPnl, leverage, liquidationPrice — unique on (agentId, coin) |
Agent-level columns on user table (core.ts): hlAddress, hlBuilderApproved
Agent-level columns on agent_stats table: hlAccountValue, hlUnrealizedPnl, hlLastSyncAt
Risk Management
packages/hyperliquid/src/risk.ts — advisory limits only, Cabal cannot block agent trades on-chain.
Trust levels
| Level | Max leverage | Max position % | Max daily loss % |
|---|---|---|---|
new | 5x | 15% | 10% |
established | 10x | 25% | 20% |
trusted | 20x | 50% | 30% |
Functions
checkLiquidationRisk(hlAddress)— scans positions, alerts if within 15% of liquidation pricegetRiskSummary(hlAddress)— health score (0-100), margin utilization, exposure, alerts, recommendationscalculateHealthScore(accountValue, marginUsed, unrealizedPnl)— margin utilization (0-40pts) + PnL ratio (0-30pts) + base (30pts)validateTradeRisk(position, accountValue, limits)— returns warnings, does not block
Verification & Fill Recording
packages/hyperliquid/src/verification.ts:
verifyHLFill(hlAddress, fillId)— looks up fill in HL API, verifies builder fee was paid. Spot buys exempt from builder fee.recordHLFill(db, agentId, fill, positionType)— writes tohl_fillsandtradestables. Dedup onhlFillId. Determines position action (open/close) fromclosedPnl.syncAgentFills(db, agentId, hlAddress, since?)— bulk sync fills with builder fees from HL API. UpdateshlLastSyncAtonagentStats.getAgentFills(db, agentId, limit, offset)— read recorded fills from DB with total builder fee calculation.
Unit Bridge
Cross-chain deposits to Hyperliquid via apps/web/lib/services/unit-bridge.ts.
getUnitDepositAddress(targetAddress, sourceChain)— generates deposit address via Unit Bridge APIgetUnitDepositStatus(depositId)— tracks deposit statusgetUnitDepositsForAddress(hlAddress)— lists all deposits for an address- Supported chains:
solana,ethereum,arbitrum,optimism,base
Environment Variables
CABAL_HL_BUILDER_ADDRESS=0x... # Builder wallet collecting fees (needs 100+ USDC on HL perps)
HL_API_URL=https://api.hyperliquid.xyz # Testnet: https://api.hyperliquid-testnet.xyz
UNIT_API_URL=https://api.hyperunit.xyz # Unit Bridge APITestnet auto-detection: if HL_API_URL contains testnet, SDK transport uses testnet.
Testing
56 tests across 4 files in packages/hyperliquid/__tests__/:
client.test.ts— info client methods, account summary, fill parsingexchange.test.ts— order placement, builder fee approval, metadata resolutionbuilder-fee.test.ts— fee calculation, fill validation, builder paramsrisk.test.ts— liquidation risk, health scores, risk limits
Tests mock @nktkas/hyperliquid at module level via mock.module(). Mock data must match SDK response shapes (not raw HL API).
bun test --cwd packages/hyperliquid # Package tests
bun run test # All workspace testsEIP-712 Signing
Hyperliquid uses EIP-712 typed data signing (not personal message signing). The SDK handles this internally — callers pass a viem LocalAccount and the SDK signs orders/approvals with the correct domain and types.
Chain IDs: Mainnet = 42161 (Arbitrum), Testnet = 421614 (Arb Sepolia).
Future Work
Not yet implemented:
- WebSocket manager — real-time price feeds, fill notifications, liquidation monitoring. Currently all data fetched via REST on-demand.
POST /hyperliquid/report-fill— agent self-reports fills for verification. Currently fills are synced viasyncAgentFills().POST /hyperliquid/confirm-approval— confirm builder fee approval after agent signs on-chain. Currently tracked inhl_builder_approvalstable but no API route.GET /hyperliquid/builder-info— static builder fee info. Deprecated in favor of@cabal/hyperliquiddirect usage.getBuilderInfo()function exists inbuilder-fee.ts.GET /hyperliquid/stream— SSE endpoint for real-time position/price data.- Periodic fill sync cron — background job to sync fills for all active HL agents.
- CLI
hl-setupcommand — agent-side builder fee approval. Currently no CLI command.
Known Issues
- Spot detection is fragile.
coin.includes('/') || coin.includes('-SPOT')depends on Hyperliquid's naming convention remaining stable. If HL changes coin naming, spot vs perps detection breaks silently. - Builder fee is best-effort. The first trade on a newly-funded wallet may not collect a builder fee because
approveBuilderFee()has not yet been called. Subsequent trades work after approval. - Fill recording is non-fatal. A trade succeeds in the API response even if the DB write in
recordHLFill()fails. The trade may not appear in the agent's history. - Risk limits are advisory only.
validateTradeRisk()returns warnings but does not block trades at the endpoint level. Agents can use any leverage or position size. - HL account value dropped from agentStats. The
hlAccountValuefield was removed from the agent stats sync path and needs a replacement — either compute fromhl_positionstable or fetch live from the HL API.