npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rhea-finance/crosschain-perps-sdk

v0.1.1

Published

Headless TypeScript SDK for cross-chain perpetual trading on Hyperliquid.

Readme

Cross-chain Perps SDK

A headless TypeScript SDK for the Hyperliquid perpetual-trading workflows used by the RHEA multi-chain application. It provides market discovery, account aggregation, MCA and remote signing, optional direct EVM signing, agent approval, order execution, HIP-3 support, real-time subscriptions, Unified Account calculations, and Lending Account top-ups.

The package contains no React components and does not manage application UI state. Applications own wallet connection, secure key storage, user prompts, analytics, notifications, and rendering.

Contents

Capabilities and boundaries

Included

  • Hyperliquid mainnet and testnet endpoint configuration.
  • Main-dex and HIP-3 perpetual-market discovery.
  • Global HIP-3 asset-ID calculation.
  • Market mids, metadata, books, candles, funding, trades, and asset contexts.
  • Multi-dex clearinghouse aggregation.
  • Spot state, positions, orders, fills, funding, fees, and portfolio history.
  • Unified Account equity, withdrawable balance, leverage, and risk calculations.
  • MCA or chain-signature master approval with protected local agent signing.
  • Callback-based remote agent action signing.
  • Optional direct EVM action and EIP-712 signing for EVM-only applications.
  • Agent-wallet creation, recovery, approval, and approval checks.
  • Limit, market, reduce-only, TP/SL, cancel, modify, close, leverage, and isolated-margin actions.
  • RHEA referral preparation, region checks, optional builder approval, and automatic HIP-3 Unified Account activation.
  • WebSocket reconnection, resubscription, account-feed normalization, and mids REST refresh after reconnect.
  • Lending Account top-up job creation, retry, polling, history, permit refresh, and pending-submission recovery.

Application-provided integrations

The SDK deliberately accepts callbacks for operations that depend on the host application's wallet and lending environment:

  • MCA-to-Hyperliquid master-address derivation.
  • Wallet-specific MCA typed-data signing and chain-signature receipt polling.
  • Lending quote acquisition.
  • Borrow-business preparation.
  • Permit signing or MCA signature-task preparation.
  • NEAR-wallet and legacy-Zcash borrow authorization.
  • Optional generic cross-chain deposit and withdrawal execution.
  • Persistent storage for generated agent keys and pending top-up payloads.

The Lending Account flow is therefore headless but not wallet-provider specific. The application performs wallet interactions; the SDK validates and orchestrates the resulting transfer job.

Installation

pnpm add @rhea-finance/crosschain-perps-sdk

The SDK uses ethers internally to operate the generated Hyperliquid agent key. Applications do not need to install or import it for the normal MCA authorization flow. Install ethers@5 directly only when using the low-level direct-signer APIs yourself. No SDK path connects MetaMask automatically.

The package requires Node.js 18 or a browser runtime with fetch, WebSocket, AbortController, and Web Crypto support. Custom fetch and WebSocket implementations can be supplied for other runtimes.

The package exports both ESM and CommonJS builds:

import { PerpsClient } from "@rhea-finance/crosschain-perps-sdk";
const { PerpsClient } = require("@rhea-finance/crosschain-perps-sdk");

Funding-only types and services are also available from:

import type {
  LendingTopUpAuthorizer,
  LendingTopUpPendingStore,
} from "@rhea-finance/crosschain-perps-sdk/funding";

Architecture

flowchart LR
  Wallet["Any wallet supported by multi-chain"] --> MCA["MCA / chain-signature layer"]
  MCA --> TypedSigner["McaTypedDataSigner"]
  OptionalEvm["Optional direct EVM signer"] --> TypedSigner
  TypedSigner --> Approval["Approve Hyperliquid agent once"]
  Approval --> Agent["Protected local or remote agent key"]
  Agent --> ActionSigner["Agent action signer"]
  App["Application"] --> Client["PerpsClient"]
  Client --> Authorization["authorization"]
  Client --> Markets["markets"]
  Client --> Account["account"]
  Client --> Trading["trading"]
  Client --> Agents["agents"]
  Client --> Stream["stream"]
  Client --> Funding["funding"]
  Client --> TopUp["lendingTopUp"]
  Markets --> Info["Hyperliquid Info API"]
  Account --> Info
  Trading --> Exchange["Hyperliquid Exchange API"]
  Agents --> Exchange
  Stream --> WS["Hyperliquid WebSocket"]
  TopUp --> Jobs["RHEA transfer-job API"]
  TypedSigner --> Agents
  Authorization --> TypedSigner
  Authorization --> Agents
  TypedSigner --> Funding
  ActionSigner --> Trading
  Wallet --> App
  App --> TopUp

PerpsClient is the service container. A single instance shares resolved network configuration, HTTP behavior, market caches, and WebSocket lifecycle across all services.

Wallet model: MetaMask is not required

The SDK does not access window.ethereum, request a browser-wallet connection, or depend on MetaMask, WalletConnect, or another EVM wallet UI. Only the host multi-chain application connects the user's wallet.

Hyperliquid accounts use EVM-format addresses and Hyperliquid actions use EIP-712 or secp256k1-compatible signatures. This is a signature format requirement, not a requirement to connect an EVM browser wallet. The existing multi-chain MCA or chain-signature layer can produce those addresses and signatures through SDK callbacks.

The integration has five distinct roles:

| Role | Responsibility | | --- | --- | | Connected wallet | Any wallet already supported by multi-chain. It remains owned and connected by the host application. | | Hyperliquid master account | The EVM-format address derived or controlled by MCA. Pass it as userAddress. | | Master typed-data signer | McaTypedDataSigner delegates agent approval, builder approval, and withdrawal signatures to multi-chain. | | Hyperliquid agent | A generated, master-approved trading key stored by the application. It is not a user-connected wallet. | | L1 action signer | Signs orders with the approved agent key, either locally or through CallbackActionSigner. |

The default multi-chain flow is:

Existing multi-chain wallet
  -> MCA or chain-signature service
  -> Hyperliquid master address
  -> McaTypedDataSigner approves a generated agent once
  -> protected agent signer submits orders
  -> PerpsClient

An approved Hyperliquid agent is an internal trading key, not another wallet the user must connect. The agent can be held by protected application storage or a remote signing service. The user's existing multi-chain wallet is needed for master-level approval, not for every order. Directly connecting an EVM wallet as the master remains an optional EVM-only mode.

Quick start

Read-only client

import { PerpsClient } from "@rhea-finance/crosschain-perps-sdk";

const client = new PerpsClient({ network: "mainnet" });

try {
  const markets = await client.markets.list({ includeDexes: [] });
  const btc = await client.markets.resolve("BTC");

  console.log(markets.length);
  console.log({
    coin: btc.coin,
    assetId: btc.assetId,
    markPrice: btc.context.markPx,
    sizeDecimals: btc.asset.szDecimals,
    maxLeverage: btc.asset.maxLeverage,
  });
} finally {
  client.destroy();
}

Passing includeDexes: [] loads only the main dex. Omitting includeDexes loads all USDC-collateralized perp dexes returned by Hyperliquid.

Trading client using the existing multi-chain signer

import {
  type HyperliquidAgentStore,
  PerpsClient,
} from "@rhea-finance/crosschain-perps-sdk";

// Back this interface with encrypted application storage or a remote vault.
const agentStore: HyperliquidAgentStore = {
  load: (masterAddress) => protectedAgentStorage.load(masterAddress),
  save: (masterAddress, agent) =>
    protectedAgentStorage.save(masterAddress, agent),
  remove: (masterAddress) => protectedAgentStorage.remove(masterAddress),
};

const client = new PerpsClient({
  network: "mainnet",
  mca: {
    mcaId,
    signerWallet: {
      chain: connectedWallet.chain,
      identityKey: connectedWallet.identityKey,
    },
    agentStore,

    // Adapt multi-chain's getMcaMappedEvmAddress/chainsig.js call here.
    resolveMasterAddress: ({
      mcaId,
      nearNetwork,
      nearRpcUrls,
      derivationPath,
      chainSignatureContractId,
      signal,
    }) =>
      multiChainAccount.resolveHyperliquidAddress({
        mcaId,
        nearNetwork,
        nearRpcUrls,
        derivationPath,
        chainSignatureContractId,
        signal,
      }),

    // Adapt multi-chain's signHyperliquidAccountTypedData call here.
    signTypedData: async ({
      mcaId,
      signerWallet,
      typedData,
      digest,
      nearRpcUrls,
      relayerNearGasAmount,
      signal,
      onWalletSigned,
      onSignatureRequestSubmitted,
    }) => {
      const result = await multiChainSigning.signHyperliquidAccountTypedData({
        mode: "mca",
        mca: mcaId,
        signerWallet,
        typedData,
        digest,
        nearRpcUrls,
        relayerNearGasAmount,
        signal,
        onWalletSigned,
        onSignatureRequestSubmitted,
      });

      if (!result.signatureParts) {
        throw new Error("MCA did not return a Hyperliquid signature");
      }
      return result.signatureParts;
    },
  },
});

try {
  // Resolves the master address, restores or creates an agent, approves it
  // through MCA when necessary, verifies the role, persists the key, and
  // installs both master and agent signers into the client.
  const session = await client.authorization.connect({
    onWalletSigned: () => showStatus("Wallet signature received"),
    onSignatureRequestSubmitted: () =>
      showStatus("Waiting for the Hyperliquid chain signature"),
  });

  console.log(session.masterAddress, session.agent.address);

  const account = await client.account.getSnapshot(session.masterAddress);
  console.log(account.portfolioValue, account.withdrawable);

  const result = await client.trading.placeMarketOrder({
    coin: "BTC",
    side: "buy",
    size: "0.001",
    slippageBps: 100,
  });

  console.log(result);
} finally {
  client.destroy();
}

The named multiChainAccount, multiChainSigning, connectedWallet, and protectedAgentStorage objects are adapters supplied by the host application. The SDK deliberately does not import the multi-chain application's wallet UI or stores. authorization.connect() owns the reusable authorization state machine around those callbacks, and it never asks the user to connect an EVM wallet.

Client configuration

interface PerpsClientConfig {
  network?: "mainnet" | "testnet";
  endpoints?: Partial<{
    info: string;
    exchange: string;
    websocket: string;
    statuspage: string;
  }>;
  fetch?: typeof fetch;
  webSocket?: WebSocketFactory;
  timeoutMs?: number;
  headers?: Record<string, string>;

  signer?: PerpsActionSigner | ethers.Signer;
  typedDataSigner?: PerpsTypedDataSigner;
  userAddress?: string;
  mca?: McaAuthorizationConfig;
  agentStore?: HyperliquidAgentStore;
  vaultAddress?: string | null;

  referralCode?: string | false;
  legalCheck?: boolean;
  legalCheckMode?: "region" | "strict";
  builder?: {
    address: string;
    feeTenthsBps: number;
    requireApproval?: boolean;
    maxFeeRate?: string;
  };

  marketCacheTtlMs?: number;
  fundingAdapter?: CrossChainFundingAdapter;
  lendingTopUp?: {
    baseUrl: string;
    authorizer: LendingTopUpAuthorizer;
    pendingStore?: LendingTopUpPendingStore;
  };
}

Configuration behavior

| Option | Behavior | | --- | --- | | network | Defaults to mainnet. Controls API URLs and signature chain IDs. | | endpoints | Overrides individual default endpoints. Useful for proxies and tests. | | fetch | Replaces global fetch. | | webSocket | Creates WebSocket-compatible connections for non-browser runtimes. | | timeoutMs | HTTP timeout. Defaults to 15 seconds. | | headers | Added to every SDK HTTP request. | | signer | Signs Hyperliquid L1 actions. In multi-chain this is normally the approved agent's local ethers.Wallet; use CallbackActionSigner when the agent key is remote. | | typedDataSigner | Signs master-account EIP-712 actions. Use McaTypedDataSigner with the multi-chain signing callback. | | userAddress | Master account used for account queries and pre-trading preparation. | | mca | High-level multi-chain authorization callbacks and MCA identity. Enables client.authorization.connect(). | | agentStore | Agent storage for direct mode. mca.agentStore takes precedence in MCA mode. An in-memory store is used when omitted. | | vaultAddress | Optional Hyperliquid vault address included in signed actions. | | referralCode | Defaults to RHEA. Set to false to skip referral preparation. | | legalCheck | Defaults to enabled when userAddress is configured. Set to false to skip it. | | legalCheckMode | region checks ipAllowed; strict also requires accepted terms and userAllowed. | | builder | Overrides the default zero-fee RHEA builder code. | | marketCacheTtlMs | Market-registry cache lifetime. Defaults to 60 seconds. | | lendingTopUp | Enables client.lendingTopUp. |

MCA authorization configuration

interface McaAuthorizationConfig {
  mcaId: string;
  signerWallet: { chain: string; identityKey: string };
  resolveMasterAddress?: (
    request: ResolveMcaMasterAddressRequest
  ) => Promise<string>;
  signTypedData: (
    request: SignMcaTypedDataRequest
  ) => Promise<{ r: string; s: string; v: number }>;

  nearNetwork?: "mainnet" | "testnet";
  nearRpcUrls?: readonly string[];
  derivationPath?: string;
  chainSignatureContractId?: string;
  relayerNearGasAmount?: string;
  agentStore?: HyperliquidAgentStore;
  agentName?: string;
}

The defaults match the multi-chain Hyperliquid integration:

| Setting | Mainnet default | | --- | --- | | nearNetwork | mainnet | | nearRpcUrls | https://free.rpc.fastnear.com, then https://rpc.mainnet.near.org | | derivationPath | hyperliquid-mainnet-v1 | | chainSignatureContractId | v1.signer | | agentName | rhea-agent |

Pass multi-chain's configured config_near.nodeUrl as the first nearRpcUrls item when the application must use exactly the same NEAR RPC. The SDK passes the complete ordered list to both MCA callbacks; the adapter is responsible for failover and receipt polling. On testnet, the default list is https://rpc.testnet.near.org.

resolveMasterAddress may be omitted only when userAddress is already provided. signTypedData is always required in MCA mode because the first connection may need to approve an agent.

Addresses and signers

Hyperliquid integrations commonly use two different identities:

| Identity | Purpose | | --- | --- | | Master account | Owns balances, positions, referral state, and agent approvals. | | Agent account | Signs frequent L1 actions without exposing the master key. |

The recommended multi-chain setup is:

  1. Keep using the wallet already connected by the host multi-chain application.
  2. Configure the MCA address-resolution and typed-data-signing callbacks.
  3. Call client.authorization.connect().
  4. Let the SDK restore or generate one agent for the resolved master address.
  5. If required, approve the agent once through the MCA typed-data callback.
  6. Let the SDK verify the agent role before saving and activating the key.

Do not pass the agent address as userAddress. Doing so queries the wrong account and prevents correct referral, open-order, and HIP-3 preparation.

Only one end-user wallet connection is needed. The master address and agent are Hyperliquid signing identities managed behind that connection. In the normal multi-chain trading flow, the agent is required, but it is not a second user-connected wallet.

Agent-wallet setup

Agent setup is the normal multi-chain trading path. The user's connected wallet authorizes it once through MCA, after which the application uses the protected agent key for frequent orders. This prevents every order from requiring another wallet signature prompt.

Recommended: managed authorization

const session = await client.authorization.connect();

console.log({
  masterAddress: session.masterAddress,
  typedDataSigner: session.typedDataSigner,
  agentAddress: session.agent.address,
  createdNow: session.agentCreated,
  approvalSubmittedNow: session.approvalSubmitted,
  approved: session.approved,
});

The managed flow is intentionally ordered:

  1. Resolve and checksum the Hyperliquid master address.
  2. Construct an MCA typed-data signer for that exact address.
  3. Verify that the signer's reported address equals the master address.
  4. Load an existing agent unless forceNewAgent is true.
  5. Validate that the stored private key derives the stored agent address.
  6. Generate a new random agent when no reusable key exists.
  7. Query Hyperliquid userRole for the agent.
  8. Build and submit approveAgent through MCA only when not already approved.
  9. Query userRole again and require it to point to the expected master.
  10. Persist the agent only after successful verification.
  11. Install the master typed-data signer, master query address, and agent action signer into the client.

Use forceNewAgent for deliberate key rotation:

const rotated = await client.authorization.connect({
  forceNewAgent: true,
  agentName: "rhea-agent-2",
});

This creates and approves a replacement but does not revoke the old agent on Hyperliquid. Revocation is a separate master-level operation and is not currently exposed by the SDK.

To remove the locally stored key and detach the current trading signer:

await client.authorization.clearLocalAgent(session.masterAddress);

This is local cleanup only; it does not revoke the agent on Hyperliquid. The next connect() creates another agent unless the application restores one.

MemoryAgentStore is convenient for tests and short-lived processes. A production HyperliquidAgentStore must encrypt the private key at rest, separate records by master address and environment, implement remove(), and avoid logging or analytics capture of the stored object.

Low-level manual authorization

Use the client.agents methods only when the application needs to own each state transition itself.

const agent = client.agents.create("rhea-agent");
const approved = await client.agents.isApproved(agent.address, masterAddress);

if (!approved) {
  await client.agents.approve(agent, masterTypedDataSigner);
}

Recover an existing key with client.agents.fromPrivateKey(privateKey, agentName). Agent names are normalized to letters, digits, _, and -, with a maximum length of 16 characters. The manual path does not persist or activate the agent automatically.

MCA and remote signing

This is the primary master-account signing model for integration into the RHEA multi-chain application. It is used to approve the agent and other master-level actions. Once approved, the agent signs normal orders without invoking the user's connected wallet.

How this maps to multi-chain

The SDK owns the Hyperliquid-specific authorization state machine, while the two configured callbacks adapt the wallet-specific code that already exists in multi-chain:

sequenceDiagram
  participant App as Host application
  participant Auth as AuthorizationService
  participant MCA as Multi-chain MCA adapter
  participant HL as Hyperliquid API

  App->>Auth: connect()
  Auth->>MCA: resolveMasterAddress(request)
  MCA-->>Auth: MCA-derived EVM address
  Auth->>HL: userRole(agentAddress)
  alt agent is not approved
    Auth->>MCA: signTypedData(approveAgent typed data)
    MCA-->>Auth: EVM signature parts
    Auth->>HL: exchange(approveAgent)
    Auth->>HL: userRole(agentAddress)
  end
  Auth->>Auth: verify, persist, and activate agent
  Auth-->>App: approved session

resolveMasterAddress corresponds to multi-chain's chainsig.js mapped EVM address derivation using contract v1.signer and path hyperliquid-mainnet-v1. signTypedData corresponds to signHyperliquidAccountTypedData and receives the exact typed data and digest that Hyperliquid requires.

The adapter remains responsible for the wallet branch because it owns the connected wallet session and relayer credentials:

  • A NEAR wallet can call MCA exec and poll the configured NEAR RPC until the chain-signature receipt is available.
  • The legacy Zcash path can request its business signature and continue through the existing relay flow.
  • Other supported wallets can sign the MCA business payload, submit it to the relayer, and poll for the resulting chain signature.
  • A direct EVM application can bypass mca and provide userAddress plus a PerpsTypedDataSigner.

These branches belong inside the host callback, not inside the SDK, because the SDK has no wallet session, MCA contract client, relayer authentication, or UI. The SDK passes the network, ordered NEAR RPC list, derivation parameters, connected-wallet identity, cancellation signal, and progress callbacks so the adapter does not need to reconstruct Hyperliquid state.

The progress hooks are notifications, not automatic detectors. The MCA adapter must invoke onWalletSigned() after the connected wallet completes its signature and onSignatureRequestSubmitted() after the chain-signature request has been submitted.

Master-account EIP-712 signing

Use McaTypedDataSigner when an MCA relayer or chain-signature service signs human-readable typed data.

import {
  McaTypedDataSigner,
  PerpsClient,
} from "@rhea-finance/crosschain-perps-sdk";

const masterTypedDataSigner = new McaTypedDataSigner(
  mcaDerivedEvmAddress,
  async ({ typedData, digest }) => {
    const signature = await mcaRelayer.signTypedDataDigest({
      accountId: mcaId,
      typedData,
      digest,
    });

    return {
      r: signature.r,
      s: signature.s,
      v: signature.v,
    };
  }
);

const client = new PerpsClient({
  userAddress: mcaDerivedEvmAddress,
  typedDataSigner: masterTypedDataSigner,
});

The callback receives both the typed data and its canonical EIP-712 digest. This is suitable for agent approval, builder approval, and withdrawal actions. The multi-chain signing backend is responsible for returning the EVM-format { r, s, v } signature expected by Hyperliquid, regardless of which wallet the user originally connected.

Remote L1 action signing

Use CallbackActionSigner only when the approved agent key is held by a remote service instead of protected local storage.

import { CallbackActionSigner } from "@rhea-finance/crosschain-perps-sdk";

const actionSigner = new CallbackActionSigner(
  agentAddress,
  async (action, nonce, options) => {
    return remoteAgentSigner.signHyperliquidAction({
      action,
      nonce,
      vaultAddress: options?.vaultAddress,
      expiresAfter: options?.expiresAfter,
    });
  }
);

client.setSigner(actionSigner);

The callback must return { r, s, v } for the exact action and nonce supplied by the SDK.

Optional direct EVM master mode

For a standalone EVM-only application, the master account can be a directly connected EVM wallet. PerpsClient accepts its ethers.Signer where appropriate and wraps L1 signers with EvmActionSigner. This mode may use a browser wallet, but the normal multi-chain/MCA path does not need it.

Market discovery

Load the registry

const registry = await client.markets.getRegistry();

console.log(registry.markets);
console.log(registry.byCoin["BTC"]);
console.log(registry.byCoin["xyz:TSLA"]);
console.log(registry.mids);

Market names use these canonical forms:

  • Main dex: BTC, ETH, SOL.
  • HIP-3 dex: xyz:TSLA, where the dex prefix is lowercase and the symbol is uppercase.

The registry filters delisted assets and HIP-3 dexes whose collateral token does not match the main USDC collateral token.

Control HIP-3 loading

// Main dex only.
const mainMarkets = await client.markets.list({ includeDexes: [] });

// Main dex plus xyz.
const selectedMarkets = await client.markets.list({
  includeDexes: ["xyz"],
});

// Refresh even when the cache is valid.
const refreshed = await client.markets.list({ force: true });

Resolve one market

const market = await client.markets.resolve("xyz:tsla");

console.log({
  canonicalCoin: market.coin,
  displaySymbol: market.displaySymbol,
  dex: market.dex,
  dexIndex: market.dexIndex,
  localIndex: market.localIndex,
  globalAssetId: market.assetId,
  sizeDecimals: market.asset.szDecimals,
  maxLeverage: market.asset.maxLeverage,
  markPrice: market.context.markPx,
});

resolve() normalizes the input and throws MARKET_NOT_FOUND when the market is absent.

Books, candles, and funding

const book = await client.markets.getOrderBook("BTC", {
  nSigFigs: 5,
  mantissa: 2,
});

const candles = await client.markets.getCandles(
  "BTC",
  "1h",
  Date.now() - 24 * 60 * 60 * 1_000,
  Date.now()
);

const funding = await client.markets.getFundingHistory(
  "BTC",
  Date.now() - 7 * 24 * 60 * 60 * 1_000
);

mantissa is sent only when nSigFigs is 5, matching Hyperliquid's request format.

Account data

Trading snapshot

const snapshot = await client.account.getSnapshot(masterAddress);

console.log({
  abstraction: snapshot.abstraction,
  positions: snapshot.positions,
  openOrders: snapshot.openOrders,
  accountValue: snapshot.accountValue,
  withdrawable: snapshot.withdrawable,
  unrealizedPnl: snapshot.unrealizedPnl,
  unifiedLeverage: snapshot.unifiedAccountLeverage,
  unifiedRiskRatio: snapshot.unifiedAccountRatio,
});

getSnapshot() performs the following work:

  1. Normalizes the account to a checksummed EVM address.
  2. Loads the market registry to discover relevant perp dexes.
  3. Queries clearinghouse state for the main dex and each discovered HIP-3 dex.
  4. Queries spot state, account abstraction, and frontend open orders.
  5. Canonicalizes HIP-3 position names.
  6. Merges margin summaries and non-zero positions across dexes.
  7. Rebases Unified Account equity onto spot USDC when required.
  8. Calculates withdrawable USDC, portfolio value, unrealized PnL, leverage, and Unified Account risk ratio.

Full account details

const details = await client.account.getDetails(masterAddress, {
  fundingStartTime: Date.now() - 90 * 24 * 60 * 60 * 1_000,
});

console.log(details.fills);
console.log(details.historicalOrders);
console.log(details.fees);
console.log(details.funding);
console.log(details.portfolio);
console.log(details.portfolioDayAccountValue);

If fundingStartTime is omitted, the SDK uses a 400-day lookback. Fee, funding, and portfolio failures are treated as optional where multi-chain does the same; the primary snapshot and order/fill requests still reject on error.

getDetails() is the complete convenience call. It runs getSnapshot() and getHistory() concurrently and returns one merged result. Use it for an account page that needs both current balances and historical panels.

History without a balance snapshot

const history = await client.account.getHistory(masterAddress, {
  fundingStartTime: Date.now() - 30 * 24 * 60 * 60 * 1_000,
});

console.log({
  fills: history.fills,
  historicalOrders: history.historicalOrders,
  fees: history.fees,
  funding: history.funding,
  rawPortfolio: history.portfolio,
  dayAccountValue: history.portfolioDayAccountValue,
});

This call matches the slower query group used by multi-chain:

  1. userFills for executed trades.
  2. historicalOrders for completed, cancelled, rejected, and other past orders.
  3. userFees for fee and tier information.
  4. userFunding for funding payments in the selected time range.
  5. portfolio for account-value and perp-PnL histories.
  6. A normalized { t, v }[] day account-value series selected from day, with perpDay as its fallback.

getHistory() does not query a lending balance or a connected wallet balance. Its address is always the Hyperliquid master address returned by client.authorization.connect(). Lending Account transfer history is exposed separately by client.lendingTopUp.getHistory().

Portfolio history helpers

Hyperliquid returns portfolio data as named tuple groups. The root package exports the same transformations needed by the multi-chain charts:

import {
  getAccountValueHistoryForPeriod,
  getPerpsPnlHistoryForPeriod,
  getPortfolioSeriesChangePercent,
  getPortfolioSeriesLastValue,
} from "@rhea-finance/crosschain-perps-sdk";

const raw = await client.account.getPortfolio(masterAddress);
const accountValue = getAccountValueHistoryForPeriod(raw, "month");
const perpPnl = getPerpsPnlHistoryForPeriod(raw, "month");

console.log({
  latestAccountValue: getPortfolioSeriesLastValue(accountValue),
  accountValueChangePercent:
    getPortfolioSeriesChangePercent(accountValue),
  perpPnl,
});

Supported periods are day, week, month, and allTime. Account-value helpers read day, week, month, and allTime; perp-PnL helpers read perpDay, perpWeek, perpMonth, and perpAllTime. Invalid numeric values are filtered out, an empty series has a null last value, and percentage change is null when there are fewer than two points or the first value is effectively zero.

Targeted account queries

const [orders, fills, fees, portfolio, activeAsset] = await Promise.all([
  client.account.getOpenOrders(masterAddress),
  client.account.getFills(masterAddress),
  client.account.getFees(masterAddress),
  client.account.getPortfolio(masterAddress),
  client.account.getActiveAssetData(masterAddress, "BTC"),
]);

Use getActiveAssetData() for Hyperliquid-authoritative leverage, availableToTrade, and maxTradeSzs values.

Time-bounded and ledger queries are also available:

const startTime = Date.now() - 7 * 24 * 60 * 60 * 1_000;
const endTime = Date.now();

const [fills, funding, ledger] = await Promise.all([
  client.account.getFillsByTime(masterAddress, startTime, endTime),
  client.account.getFunding(masterAddress, startTime, endTime),
  client.account.getNonFundingLedgerUpdates(masterAddress, startTime),
]);

All timestamps are Unix milliseconds. Pass an AbortSignal as the last argument of an individual query when the page or request can be cancelled.

Real-time subscriptions

Subscriptions connect lazily. The first subscription opens the socket; the last unsubscribe removes only its wire subscription. Call destroy() to close the client completely.

Mids

const unsubscribeMids = client.stream.subscribeMids((message) => {
  const mids = message.data;
  console.log(mids.BTC);
});

After a reconnect, a PerpsClient-owned stream refreshes mids through the REST API and dispatches the refreshed map to mids subscribers.

Trades, book, and candles

const unsubscribeTrades = client.stream.subscribeTrades("BTC", (message) => {
  console.log(message.data);
});

const unsubscribeBook = client.stream.subscribeOrderBook(
  "BTC",
  (message) => console.log(message.data.levels),
  { nSigFigs: 5, mantissa: 2 }
);

const unsubscribeCandles = client.stream.subscribeCandles(
  "BTC",
  "1m",
  (message) => console.log(message.data)
);

Normalized account stream

const unsubscribeAccount = client.stream.subscribeAccount(
  masterAddress,
  (event) => {
    switch (event.type) {
      case "clearinghouse":
        renderPositions(event.state.assetPositions);
        break;
      case "openOrders":
        renderOrders(event.orders);
        break;
      case "fills":
        renderFills(event.fills);
        break;
      case "spot":
        renderSpotBalances(event.spot.balances);
        break;
    }
  },
  { dexes: ["xyz"] }
);

The normalized account subscription:

  • Merges allDexsClearinghouseState payloads.
  • Canonicalizes bare HIP-3 symbols to dex:SYMBOL.
  • Subscribes to open orders once per requested dex.
  • Deduplicates open orders by order ID.
  • Handles user-fill snapshot and incremental payloads.
  • Subscribes to spot state.

The main dex is always included. Pass every HIP-3 dex whose orders must be tracked. The SDK does not infer the dex list from an application store.

Reconnect notification and cleanup

const offReconnect = client.stream.onReconnect(() => {
  console.log("Hyperliquid WebSocket reconnected");
});

// Component or screen cleanup.
unsubscribeMids();
unsubscribeTrades();
unsubscribeBook();
unsubscribeCandles();
unsubscribeAccount();
offReconnect();

// Application shutdown or client replacement.
client.destroy();

Trading flow

Every order submitted through placeOrder(), placeMarketOrder(), closePosition(), or setPositionTpSl() follows a deterministic flow.

sequenceDiagram
  participant App as Application
  participant SDK as TradingService
  participant Info as Info API
  participant Signer as Action Signer
  participant Exchange as Exchange API

  App->>SDK: placeOrder(input)
  SDK->>Info: resolve market and precision
  SDK->>Info: legal/referral/builder checks
  alt HIP-3 account is not Unified
    SDK->>Signer: sign agentSetAbstraction
    SDK->>Exchange: enable Unified Account
  end
  SDK->>SDK: validate minimum and wire precision
  SDK->>Signer: sign action + nonce
  Signer-->>SDK: r, s, v
  SDK->>Exchange: submit signed action
  Exchange-->>SDK: statuses or error
  SDK-->>App: ExchangeResponse

Pre-trading preparation

When userAddress is configured, preparation performs these steps:

  1. Query legalCheck unless legalCheck: false.
  2. In the default region mode, reject only when ipAllowed is false.
  3. In strict mode, also require acceptedTerms and userAllowed.
  4. Query referral state and set the default RHEA referral when unbound. Referral failure is best-effort and never blocks the order.
  5. If builder.requireApproval is enabled, query maxBuilderFee and submit a master-signed approval when no approval exists.
  6. For a HIP-3 market, query account abstraction and submit agentSetAbstraction when the account is not Unified or Portfolio Margin.

Call preparation explicitly when a UI needs to complete setup before the user confirms an order:

await client.trading.prepareTrading("xyz:TSLA");

Orders include the zero-fee RHEA builder code by default. Override it only when the integration intentionally uses another builder:

const client = new PerpsClient({
  signer: agentSigner,
  userAddress: masterAddress,
  builder: {
    address: "0x...",
    feeTenthsBps: 0,
    requireApproval: false,
  },
});

When requireApproval is true, also provide typedDataSigner because builder approval is a master-account action.

Order operations

Prices and sizes are strings to avoid accidental floating-point formatting in wire payloads. The SDK uses Hyperliquid's price and size formatters immediately before signing.

Limit order

const response = await client.trading.placeOrder({
  coin: "BTC",
  side: "buy",
  price: "62000",
  size: "0.001",
  orderType: { limit: { tif: "Gtc" } },
});

Supported limit time-in-force values are:

  • Gtc: good until cancelled.
  • Ioc: immediate or cancel.
  • Alo: add liquidity only.
  • FrontendMarket: market-style limit with a short expiration.

The default order type is Gtc.

Market order

Hyperliquid market orders are submitted as FrontendMarket limit orders using a slippage-adjusted mid price.

const response = await client.trading.placeMarketOrder({
  coin: "ETH",
  side: "sell",
  size: "0.05",
  slippageBps: 75,
});

For a buy, the wire price is above the current mid; for a sell, it is below the mid. slippageBps defaults to 100 and must be between 0 and 10,000.

Entry order with TP and SL

await client.trading.placeOrder({
  coin: "BTC",
  side: "buy",
  price: "62000",
  size: "0.001",
  tp: "68000",
  sl: "59000",
});

The SDK submits the entry and trigger children in one normalTpsl order group. Both trigger children are market triggers and reduce-only.

Reduce-only order

await client.trading.placeOrder({
  coin: "BTC",
  side: "sell",
  price: "64000",
  size: "0.001",
  reduceOnly: true,
});

The application should ensure that the side and size reduce the existing position. Hyperliquid remains the final authority for reduce-only validation.

Close a position at market

await client.trading.closePosition({
  coin: "BTC",
  size: "0.001",
  isLong: true,
  slippageBps: 200,
});

isLong: true creates a reduce-only sell. isLong: false creates a reduce-only buy.

Cancel one order

await client.trading.cancelOrder("BTC", orderId);

Cancel all provided orders for one market

const orders = await client.account.getOpenOrders(masterAddress);
await client.trading.cancelAll("BTC", orders);

cancelAll() does not fetch orders internally. It filters the supplied list by canonical coin name and returns null when nothing matches.

Modify an order

const orders = await client.account.getOpenOrders(masterAddress);
const order = orders.find((row) => row.oid === orderId);

if (!order) throw new Error("Order not found");

await client.trading.modifyOrder(order, "62500", "0.0015");

For TP/SL orders, the SDK preserves trigger semantics and determines TP versus SL from both orderType and triggerCondition.

Replace position TP/SL

await client.trading.setPositionTpSl({
  coin: "BTC",
  size: "0.001",
  isLong: true,
  tp: "68000",
  sl: "59000",
});

The replacement flow is:

  1. Resolve the market and asset ID.
  2. Use openOrders supplied by the caller, or query the configured userAddress.
  3. Flatten parent orders and their children.
  4. Find existing position TP/SL or reduce-only trigger orders for the coin.
  5. Cancel only the TP kinds being replaced and only the SL kinds being replaced.
  6. Submit the new triggers as a positionTpsl group.

To avoid an additional REST request, pass a current order snapshot:

await client.trading.setPositionTpSl({
  coin: "xyz:TSLA",
  size: "2",
  isLong: false,
  sl: "250",
  openOrders: currentOpenOrders,
});

Leverage and margin

Update leverage and margin mode

await client.trading.updateLeverage("BTC", 10, "cross");
await client.trading.updateLeverage("xyz:TSLA", 5, "isolated");

The SDK enforces integer leverage between 1 and the market's maxLeverage. Cross mode is rejected for strict-isolated markets.

Adjust isolated margin

// Add 100 USDC of margin to a long isolated position.
await client.trading.adjustIsolatedMargin("BTC", 100, true);

// Remove 25 USDC from a short isolated position.
await client.trading.adjustIsolatedMargin("BTC", -25, false);

deltaUsd is converted to Hyperliquid's 1e6 integer unit. isLong controls the action's isBuy field. Margin adjustment is rejected for markets whose policy does not support this operation.

Set account abstraction explicitly

await client.trading.setAccountAbstraction("unifiedAccount");
await client.trading.setAccountAbstraction("portfolioMargin");
await client.trading.setAccountAbstraction("disabled");

HIP-3 orders automatically enable Unified Account when the current abstraction is not trading-ready.

Account calculation helpers

The root package exports reusable pure functions, including:

import {
  calculateUnifiedAccountRatio,
  getClearinghouseAccountValueUsd,
  getCrossTradingAvailableMarginUsd,
  getHyperliquidMaxTradeNotionalUsd,
  getHyperliquidWithdrawableUsdc,
  getTotalPortfolioUsd,
  getUnifiedAccountLeverage,
  rebaseUnifiedClearinghouseOnSpot,
} from "@rhea-finance/crosschain-perps-sdk";

These helpers contain the same Unified Account de-duplication and spot-rebase rules used by AccountService.

Lending Account top-up

The supported top-up path is:

RHEA Lending Account -> borrow USDC -> Intents bridge -> Hyperliquid Perps

Wallet-originated top-up flows are not hard-coded into LendingTopUpService. The host application supplies the lending quote and an authorizer that performs wallet-specific permit and borrow authorization. The authorizer should reuse the wallet already selected in multi-chain; the top-up flow does not request MetaMask or a second wallet connection.

End-to-end flow

sequenceDiagram
  participant App as Application
  participant Quote as Lending / Intents quote
  participant Auth as LendingTopUpAuthorizer
  participant SDK as LendingTopUpService
  participant Jobs as Transfer-job API

  App->>Quote: request USDC top-up quote
  Quote-->>App: depositAddress, amountIn/out, minAmountOut
  App->>SDK: execute(input + quote)
  SDK->>SDK: require minAmountOut >= 10 USDC
  SDK->>Auth: authorize(input)
  Auth->>Auth: prepare permit and borrow business
  Auth->>Auth: sign or create MCA signature task
  Auth-->>SDK: borrow authorization + permit data
  SDK->>SDK: save pending payload
  SDK->>Jobs: POST /borrow-top-ups
  alt request fails
    SDK->>Jobs: retry after 1 second
    SDK->>Jobs: retry after 3 seconds
  end
  Jobs-->>SDK: submitted job
  SDK->>SDK: remove pending payload
  loop until terminal status
    SDK->>Jobs: GET /transfer-jobs/:jobId
    Jobs-->>SDK: status, progress, nextPollMs
    SDK-->>App: onUpdate(job)
  end
  SDK-->>App: SUCCESS job or PerpsSdkError

Step 1: obtain a quote

The application must provide this normalized quote:

interface LendingTopUpQuote {
  depositAddress: string;
  amountIn: string;
  amountOut: string;
  minAmountOut: string;
  timeEstimate?: number | string;
  amountInFormatted?: string;
  amountOutFormatted?: string;
}

amountIn, amountOut, and minAmountOut are raw integer strings. For USDC, minAmountOut uses six decimals and must be at least 10000000, which is 10 USDC.

Step 2: implement the authorizer

import type {
  LendingTopUpAuthorizer,
  LendingTopUpAuthorizeInput,
} from "@rhea-finance/crosschain-perps-sdk/funding";
import {
  ensureBorrowBusinessFollowsPermitBusiness,
} from "@rhea-finance/crosschain-perps-sdk/funding";

const authorizer: LendingTopUpAuthorizer = {
  async authorize(input: LendingTopUpAuthorizeInput) {
    const permit = await prepareDepositPermit({
      owner: input.targetAddress,
      value: input.quote.minAmountOut,
      accountMode: input.accountMode,
      signal: input.signal,
    });

    let business = await prepareBorrowBusiness({
      mcaId: input.mcaId,
      tokenId: input.tokenId,
      amountBorrowRaw: input.amountBorrowRaw,
      amountTokenRaw: input.amountTokenRaw,
      targetAddress: input.targetAddress,
      quote: input.quote,
      signal: input.signal,
    });

    business = ensureBorrowBusinessFollowsPermitBusiness(
      business,
      permit.business
    );

    const borrow = await authorizeBorrowBusiness(business, input);

    return {
      borrow,
      permitSignature: permit.permitSignature,
      permitRequest: permit.permitRequest,
      signatureTask: permit.signatureTask,
      intentNonces: permit.intentNonces,
    };
  },
};

The example's wallet and lending functions are application-defined. The SDK calls the authorizer exactly once before it creates a transfer job.

Use accountMode: "mca" for the normal multi-chain flow. accountMode: "evm" is available for an EVM-only host, but it still does not prescribe MetaMask; the authorizer controls how the signature is obtained.

Borrow authorization modes

The authorizer must return one of three borrow variants.

Relayer authorization

Use when the business payload is signed and submitted by a relayer.

return {
  borrow: {
    mode: "relayer",
    signerWallet: { EVM: identityKeyWithout0x },
    business,
    signature,
    attachDeposit,
  },
  permitSignature,
};

NEAR wallet authorization

Use when the NEAR wallet has already submitted the borrow transaction.

return {
  borrow: {
    mode: "near_wallet",
    signerWallet: { Near: nearAccountId },
    business,
    txHash: nearTransactionHash,
  },
  permitSignature,
};

Legacy Zcash authorization

Use when the legacy Zcash flow produces a deposit address instead of a direct business signature.

return {
  borrow: {
    mode: "zcash_legacy",
    signerWallet: { Zcash: zcashIdentityKey },
    business,
    zcashDepositAddress,
  },
  permitRequest,
  signatureTask,
};

Use the exported formatter when possible:

import {
  formatLendingSignerWallet,
} from "@rhea-finance/crosschain-perps-sdk/funding";

const signerWallet = formatLendingSignerWallet("evm", evmAddress);

For EVM identities, the formatter removes the 0x prefix. Other chain identity keys are preserved. The EVM field identifies the Hyperliquid or MCA signing identity in the relayer payload; it does not mean that an EVM browser wallet was connected.

Step 3: implement pending-submission storage

Pending storage protects the short interval between authorization and a successful /borrow-top-ups response.

import type {
  LendingTopUpJobPayload,
  LendingTopUpPendingStore,
} from "@rhea-finance/crosschain-perps-sdk/funding";

const STORAGE_KEY = "pending-hyperliquid-lending-topups";

function readPending(): LendingTopUpJobPayload[] {
  const raw = localStorage.getItem(STORAGE_KEY);
  return raw ? JSON.parse(raw) : [];
}

function writePending(payloads: LendingTopUpJobPayload[]): void {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(payloads));
}

const pendingStore: LendingTopUpPendingStore = {
  list() {
    return readPending();
  },

  save(payload: LendingTopUpJobPayload) {
    const current = readPending();
    const next = [
      ...current.filter(
        (row) => row.clientRequestId !== payload.clientRequestId
      ),
      payload,
    ];
    writePending(next);
  },

  remove(clientRequestId: string) {
    writePending(
      readPending().filter(
        (row) => row.clientRequestId !== clientRequestId
      )
    );
  },
};

Use encrypted or protected storage when the payload contains authorization material that must not be exposed to unrelated scripts.

Step 4: configure the client

const client = new PerpsClient({
  network: "mainnet",
  lendingTopUp: {
    baseUrl: `${indexerBaseUrl}/api/v1/perps/hyperliquid`,
    authorizer,
    pendingStore,
  },
});

client.lendingTopUp is undefined when lendingTopUp configuration is not provided.

Step 5: execute and display progress

const controller = new AbortController();

const finalJob = await client.lendingTopUp!.execute({
  accountMode: "mca",
  mcaId,
  hyperliquidUserAddress: masterAddress,
  targetAddress: masterAddress,
  tokenId: lendingUsdcTokenId,
  amountBorrowRaw: "25000000",
  amountTokenRaw: "25000000",
  symbol: "USDC",
  quote: {
    depositAddress: quote.depositAddress,
    amountIn: quote.amountIn,
    amountOut: quote.amountOut,
    minAmountOut: quote.minAmountOut,
    timeEstimate: quote.timeEstimate,
    amountInFormatted: quote.amountInFormatted,
    amountOutFormatted: quote.amountOutFormatted,
  },
  signal: controller.signal,
  onUpdate(job) {
    updateProgress({
      status: job.status,
      progress: job.progress,
      message: job.message,
      hashes: job.txHashes,
    });
  },
});

console.log(finalJob.status); // SUCCESS

The SDK generates a unique clientRequestId unless one is provided. Supply a stable ID when the application needs to correlate analytics or UI state with a specific request.

Job statuses

Non-terminal statuses include:

  • SUBMITTED
  • WAITING_BORROW
  • WAITING_BRIDGE
  • WAITING_SIGNATURE
  • SUBMITTING_PERMIT
  • WAITING_PERMIT
  • SUBMITTING_EXCHANGE
  • WAITING_LEDGER

Terminal statuses are:

  • SUCCESS
  • FAILED
  • ACTION_REQUIRED
  • MANUAL_REVIEW

execute() resolves only for SUCCESS. Other terminal statuses throw PerpsSdkError with the final job in error.details.

Resume payloads that were not submitted

Call this once after application startup or account restoration:

const results = await client.lendingTopUp!.resumePendingJobs();

for (const result of results) {
  if (result.status === "rejected") {
    console.error("Pending top-up could not be resubmitted", result.reason);
  }
}

This method retries creation of each stored payload and removes only payloads that receive a transfer job successfully. It returns Promise.allSettled() so one failed payload does not stop the others.

Query a job or transfer history

const job = await client.lendingTopUp!.getJob(jobId);

const history = await client.lendingTopUp!.getHistory({
  hyperliquidUserAddress: masterAddress,
  page: 1,
  pageSize: 20,
});

Refresh a permit after action is required

When the backend reports requiredAction: "REFRESH_PERMIT", obtain a new permit through the application signer and submit it:

const refreshed = await client.lendingTopUp!.refreshPermit(jobId, {
  permitSignature: nextPermitSignature,
  permitRequest: nextPermitRequest,
  signatureTask: nextSignatureTask,
});

Only include the fields produced by the selected account and signer mode.

Funding adapters and withdrawals

client.funding contains direct withdrawal signing and an optional generic adapter boundary. The concrete multi-chain wallet integrations are not bundled into the core package.

Direct Hyperliquid withdrawal

const result = await client.funding.withdrawDirect({
  amount: "25",
  destinationAddress: arbitrumAddress,
  signer: session.typedDataSigner,
});

The amount must be greater than the fixed 1 USDC Hyperliquid withdrawal fee. The session signer is the same MCA-backed McaTypedDataSigner used for agent approval. It signs the withdraw3 action through the connected multi-chain wallet, so no direct EVM wallet connection is required.

Generic cross-chain adapter

import type {
  CrossChainFundingAdapter,
} from "@rhea-finance/crosschain-perps-sdk/funding";

const fundingAdapter: CrossChainFundingAdapter = {
  quoteDeposit: (request) => intents.quoteDeposit(request),
  executeDeposit: (request) => wallets.executeDeposit(request),
  quoteWithdraw: (request) => intents.quoteWithdraw(request),
  executeWithdraw: (request) => jobs.executeWithdraw(request),
  getExecution: (id) => jobs.getExecution(id),
};

const client = new PerpsClient({ fundingAdapter });

This adapter is optional and separate from the dedicated Lending Account top-up service.

Raw query API

client.api exposes every Hyperliquid query currently used by multi-chain. Use service methods first when aggregation is required; use the raw API for specialized data access.

Markets and status

await client.api.getPerpDexs();
await client.api.getPerpCategories();
await client.api.getMeta("xyz");
await client.api.getMetaAndAssetCtxs("xyz");
await client.api.getAllPerpMetas();
await client.api.getAllMids("xyz");
await client.api.getL2Book("BTC", { nSigFigs: 5, mantissa: 2 });
await client.api.getCandleSnapshot("BTC", "1h", startTime, endTime);
await client.api.getFundingHistory("BTC", startTime, endTime);
await client.api.getExchangeStatus();
await client.api.getHyperliquidStatuspageSummary();

Account and trading data

await client.api.getClearinghouseState(masterAddress, "xyz");
await client.api.getSpotClearinghouseState(masterAddress);
await client.api.getUserAbstraction(masterAddress);
await client.api.getActiveAssetData(masterAddress, "xyz:TSLA");
await client.api.getUserFees(masterAddress);
await client.api.getOpenOrders(masterAddress);
await client.api.getUserFills(masterAddress);
await client.api.getUserFillsByTime(masterAddress, startTime, endTime);
await client.api.getHistoricalOrders(masterAddress);
await client.api.getUserFunding(masterAddress, startTime, endTime);
await client.api.getPortfolio(masterAddress);
await client.api.getUserRole(agentAddress);
await client.api.getReferralInfo(masterAddress);
await client.api.getMaxBuilderFee({
  user: masterAddress,
  builder: builderAddress,
});
await client.api.getOrderStatus(masterAddress, orderId);
await client.api.getUserNonFundingLedgerUpdates(masterAddress, startTime);
await client.api.getLegalCheck(masterAddress);

Raw methods accept an optional AbortSignal. For most methods it is the final argument. Pass it as options.signal to getL2Book(). When calling getMaxBuilderFee() with an object, pass the signal as the second argument.

Errors, cancellation, and timeouts

SDK failures use PerpsSdkError where the SDK controls the boundary.

import {
  PerpsSdkError,
  PerpsClient,
} from "@rhea-finance/crosschain-perps-sdk";

try {
  await client.trading.placeOrder({
    coin: "BTC",
    side: "buy",
    price: "62000",
    size: "0.00001",
  });
} catch (error) {
  if (error instanceof PerpsSdkError) {
    console.error(error.code, error.message, error.details);
  } else {
    throw error;
  }
}

Error codes are:

| Code | Meaning | | --- | --- | | INVALID_ARGUMENT | Input failed local validation. | | MARKET_NOT_FOUND | The requested market is absent from the registry. | | SIGNER_REQUIRED | A trading or typed-data signer is missing. | | HTTP_ERROR | Transport, timeout, or non-2xx HTTP failure. | | API_ERROR | Hyperliquid or transfer-job API rejected the operation. | | WEBSOCKET_ERROR | No usable WebSocket implementation is available. |

Hyperliquid exchange status errors are converted to readable messages when a known error pattern exists. The original status is retained in details.

Abort a request or top-up poll

const controller = new AbortController();

const request = client.account.getSnapshot(
  masterAddress,
  controller.signal
);

controller.abort(new Error("Screen closed"));
await request;

For top-ups, pass the same signal to execute(). The SDK passes it to the authorizer and uses it for submission, retry delays, polling requests, and polling delays. The host authorizer must forward or observe the signal for its own wallet and lending operations to be cancellable.

HTTP requests use the client-level timeout. An aborted operation may have already reached an external service; always query the transfer job or account state before assuming no side effect occurred.

Security and lifecycle

Agent keys

  • Never embed agent private keys in browser source code.
  • Encrypt persisted agent keys or use a protected keystore.
  • Treat an approved agent as capable of submitting trading actions.
  • Verify agent approval against the intended master address before trading.

Master signers

  • Use the master signer only for actions that require it.
  • Prefer an approved agent for frequent order actions.
  • In multi-chain, configure client.authorization with the MCA callbacks. The service installs its McaTypedDataSigner internally after approval.
  • Use a protected local agent or CallbackActionSigner for orders.
  • Do not request MetaMask as an additional wallet connection.
  • Verify typed-data domain, network, action, amount, and destination in remote signer UIs.

Lending payloads

  • Keep permit and borrow authorization payloads scoped to the intended quote.
  • Preserve nonce ordering between permit and borrow businesses.
  • Do not log signatures, agent private keys, or confidential wallet material.
  • Use protected storage for pending job payloads.

Client lifecycle

const client = new PerpsClient(config);

try {
  await runPerpsApplication(client);
} finally {
  client.destroy();
}

destroy() clears subscriptions, reconnect timers, and ping timers, and closes the active WebSocket. Create a new client when network or endpoint configuration changes.

Development

pnpm install
pnpm type-check
pnpm test
pnpm build
npm pack --dry-run

The verification suite covers market IDs, signing and nonce behavior, account aggregation, Unified Account calculations, order precision, TP/SL replacement, pre-trading setup, query parity, WebSocket subscriptions, funding adapters, and Lending Account top-up jobs.