@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
- Installation
- Architecture
- Wallet model: MetaMask is not required
- Quick start
- Client configuration
- Addresses and signers
- Agent-wallet setup
- MCA and remote signing
- Market discovery
- Account data
- Real-time subscriptions
- Trading flow
- Order operations
- Leverage and margin
- Lending Account top-up
- Funding adapters and withdrawals
- Raw query API
- Errors, cancellation, and timeouts
- Security and lifecycle
- Development
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-sdkThe 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 --> TopUpPerpsClient 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
-> PerpsClientAn 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:
- Keep using the wallet already connected by the host multi-chain application.
- Configure the MCA address-resolution and typed-data-signing callbacks.
- Call
client.authorization.connect(). - Let the SDK restore or generate one agent for the resolved master address.
- If required, approve the agent once through the MCA typed-data callback.
- 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:
- Resolve and checksum the Hyperliquid master address.
- Construct an MCA typed-data signer for that exact address.
- Verify that the signer's reported address equals the master address.
- Load an existing agent unless
forceNewAgentis true. - Validate that the stored private key derives the stored agent address.
- Generate a new random agent when no reusable key exists.
- Query Hyperliquid
userRolefor the agent. - Build and submit
approveAgentthrough MCA only when not already approved. - Query
userRoleagain and require it to point to the expected master. - Persist the agent only after successful verification.
- 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 sessionresolveMasterAddress 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
execand 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
mcaand provideuserAddressplus aPerpsTypedDataSigner.
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:
- Normalizes the account to a checksummed EVM address.
- Loads the market registry to discover relevant perp dexes.
- Queries clearinghouse state for the main dex and each discovered HIP-3 dex.
- Queries spot state, account abstraction, and frontend open orders.
- Canonicalizes HIP-3 position names.
- Merges margin summaries and non-zero positions across dexes.
- Rebases Unified Account equity onto spot USDC when required.
- 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:
userFillsfor executed trades.historicalOrdersfor completed, cancelled, rejected, and other past orders.userFeesfor fee and tier information.userFundingfor funding payments in the selected time range.portfoliofor account-value and perp-PnL histories.- A normalized
{ t, v }[]day account-value series selected fromday, withperpDayas 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
allDexsClearinghouseStatepayloads. - 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: ExchangeResponsePre-trading preparation
When userAddress is configured, preparation performs these steps:
- Query
legalCheckunlesslegalCheck: false. - In the default
regionmode, reject only whenipAllowedis false. - In
strictmode, also requireacceptedTermsanduserAllowed. - Query referral state and set the default
RHEAreferral when unbound. Referral failure is best-effort and never blocks the order. - If
builder.requireApprovalis enabled, querymaxBuilderFeeand submit a master-signed approval when no approval exists. - For a HIP-3 market, query account abstraction and submit
agentSetAbstractionwhen 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:
- Resolve the market and asset ID.
- Use
openOrderssupplied by the caller, or query the configureduserAddress. - Flatten parent orders and their children.
- Find existing position TP/SL or reduce-only trigger orders for the coin.
- Cancel only the TP kinds being replaced and only the SL kinds being replaced.
- Submit the new triggers as a
positionTpslgroup.
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 PerpsWallet-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 PerpsSdkErrorStep 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); // SUCCESSThe 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:
SUBMITTEDWAITING_BORROWWAITING_BRIDGEWAITING_SIGNATURESUBMITTING_PERMITWAITING_PERMITSUBMITTING_EXCHANGEWAITING_LEDGER
Terminal statuses are:
SUCCESSFAILEDACTION_REQUIREDMANUAL_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.authorizationwith the MCA callbacks. The service installs itsMcaTypedDataSignerinternally after approval. - Use a protected local agent or
CallbackActionSignerfor 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-runThe 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.
