Skip to content

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): HyperliquidInfoClient class wraps SDK InfoClient. Business methods (getAccountSummary, parseFill) transform raw SDK responses into Cabal types (HLAccountSummary, HLVerifiedFill). Singleton via getHyperliquidClient(). Testnet detection via HL_API_URL env var.
  • Write client (exchange.ts): createExchangeClient(account: LocalAccount) creates an SDK ExchangeClient. Used for order placement and builder fee approval. Per-request — callers handle wallet resolution via Privy.
  • No raw API types: HLFill is derived from SDK: Awaited<ReturnType<InfoClient['userFills']>>[number]. All other raw types come from the SDK.

Key modules

FilePurpose
src/client.tsRead-only info client, singleton, business transforms
src/exchange.tsWrite client, order placement, builder fee approval, metadata cache
src/builder-fee.tsFee constants, builder params, fill validation, revenue calculation
src/verification.tsFill verification against HL API, DB recording (recordHLFill), sync
src/risk.tsLiquidation monitoring, health scores, risk limits by trust level
src/types.tsCabal business types only — HLPosition, HLAccountSummary, HLVerifiedFill, LiquidationAlert, RiskLimits, API request/response types
src/index.tsRe-exports everything

Fee Structure

Revenue via builder fees — on-chain, automatic, no custody required.

RateHL constantNotes
Perps (both sides)5 bps (0.05%)BUILDER_FEE_PERPS = 50Applied on open and close
Spot (sell side only)50 bps (0.50%)BUILDER_FEE_SPOT_SELL = 500HL 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: hlAddress set 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: hlAddress on user record
  • Logic: Calls getAccountSummary() (perps) and getSpotState() (spot) in parallel. Writes to agentStats (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: hlAddress on user record
  • Logic: Reads from hl_fills table via getAgentFills(). 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: hlAddress on 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) → calls placeOrder() with builder params → records trade → returns result.
  • Spot detection: coin.includes('/') || coin.includes('-SPOT')

Database Schema

Tables in packages/db/src/schema/hyperliquid.ts:

TablePurposeKey columns
hl_deposit_addressesUnit Bridge deposit addresses per agent+chainagentId, sourceChain, depositAddress, hlAddress
hl_builder_approvalsBuilder fee approval recordsagentId, hlAddress, maxFeeBps
hl_fillsVerified trade fills with builder feesagentId, hlFillId, coin, side, size, price, valueUsd, fee, builderFee, closedPnl, positionType, hlTimestamp
hl_positionsCached 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

LevelMax leverageMax position %Max daily loss %
new5x15%10%
established10x25%20%
trusted20x50%30%

Functions

  • checkLiquidationRisk(hlAddress) — scans positions, alerts if within 15% of liquidation price
  • getRiskSummary(hlAddress) — health score (0-100), margin utilization, exposure, alerts, recommendations
  • calculateHealthScore(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 to hl_fills and trades tables. Dedup on hlFillId. Determines position action (open/close) from closedPnl.
  • syncAgentFills(db, agentId, hlAddress, since?) — bulk sync fills with builder fees from HL API. Updates hlLastSyncAt on agentStats.
  • 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 API
  • getUnitDepositStatus(depositId) — tracks deposit status
  • getUnitDepositsForAddress(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 API

Testnet 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 parsing
  • exchange.test.ts — order placement, builder fee approval, metadata resolution
  • builder-fee.test.ts — fee calculation, fill validation, builder params
  • risk.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 tests

EIP-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 via syncAgentFills().
  • POST /hyperliquid/confirm-approval — confirm builder fee approval after agent signs on-chain. Currently tracked in hl_builder_approvals table but no API route.
  • GET /hyperliquid/builder-info — static builder fee info. Deprecated in favor of @cabal/hyperliquid direct usage. getBuilderInfo() function exists in builder-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-setup command — 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 hlAccountValue field was removed from the agent stats sync path and needs a replacement — either compute from hl_positions table or fetch live from the HL API.