Skip to main content
Trading on Hyperliquid uses a two-part signing scheme. Two different keys sign two different classes of action, and understanding the split is the key to understanding why trades don’t trigger a wallet popup.
  • The user’s L1 wallet — any EVM-compatible signer the user connects. It owns the funds.
  • An SDK-managed agent — a separate keypair the SDK creates and the user authorizes once.
The user approves the agent a single time; from then on the agent signs every trade, so there is no wallet prompt per order. Provisioning the agent is part of the one-time Setup flow — this page explains what the two signatures are and why, not the step-by-step API (which Setup covers).

The L1 (user) wallet signature

This is a signature from the user’s own EVM-compatible signer — MetaMask, a smart-contract wallet, a backend signer, anything that can produce an EVM signature. It is the key that ultimately owns the account and its balance. On PerpsClient, hand it to the SDK with setUserWallet(). The low-level createPerpsClient also accepts userWallet, but does not expose the setup and trading helpers. These user-level messages are signed as EIP-712 typed data in the HyperliquidSignTransaction domain with chain id 42161 (Arbitrum) — Hyperliquid’s signature chain. The signer is just an EVM key; it is not tied to Ethereum the chain, protocol, or asset. A browser wallet must be on chain 42161 to sign them: supply a switchChain hook so the SDK can switch it. Without the hook the wallet is used unchanged. On Hyperliquid the L1 wallet is asked to sign only the few actions the protocol requires the account owner to authorize directly:
  • approveAgent — authorizes the agent keypair (the one-time bootstrap that makes popup-free trading possible).
  • approveBuilderFee — authorizes LI.FI’s per-trade builder fee.
  • withdrawal — moves funds off Hyperliquid. Agents cannot withdraw.
  • sendAsset — transfers balance between sub-accounts / venues. Agents cannot move assets.
  • revokeAgent and revokeSessionAgent — free an agent slot. revokeSessionAgent removes the session agent the SDK signs with, targeted by its agentAddress.
  • revokeBuilderFee — withdraws the builder fee approval.
  • accountMode — changes the account’s abstraction mode.
These all either delegate authority or move real funds or account state, so the protocol requires the account owner’s own signature for them — they are mandatory and cannot be delegated to the agent.

The agent signature

An agent (Hyperliquid also calls it an “API wallet” or “approved wallet”) is a key the account owner has authorized to place orders on its behalf — but which cannot withdraw or move funds. That asymmetry is what makes it safe to keep an agent key on the client: the worst it can do is trade. In @lifi/perps-sdk the agent is a standard secp256k1 keypair (the same curve EVM wallets use), generated and held by the SDK — distinct from the user’s wallet. It signs every trading action as EIP-712 typed data with no wallet prompt: placeOrder, placeTriggerOrder, cancelOrders, modifyOrders, updateLeverage, and updatePositionMargin. The agent also signs placeTwapOrder, cancelTwapOrder, and the one-time setReferrer setup step. Agent-signed L1 actions use Hyperliquid’s phantom-agent EIP-712 domain (Exchange, version 1, chain id 1337) over an Agent { source: 'a', connectionId } message. A user wallet cannot sign that domain, which is why setReferrer is agent-signed and sequenced after approveAgent.

How the agent is created

The SDK generates the agent keypair client-side, the first time an agent is needed (during the approveAgent setup step). The agent’s public address is what the user’s wallet signs to approve; the private key never leaves the client. The LI.FI backend and Hyperliquid only ever see the agent’s address, never its private key.

How the agent is stored

The keypair is persisted through a pluggable StorageAdapter. The default adapter encrypts records at rest (AES-GCM-256 via WebCrypto, keyed by a non-extractable key held in IndexedDB) before writing ciphertext to localStorage. Pass your own adapter via storage on hyperliquidProvider({ storage }) — the provider plugin’s own options, not createPerpsClient — for non-browser environments such as a server or a secure enclave.
  • Namespace: lifi-perps-agent:<l1-address>:hyperliquid — one agent per L1 address per provider.
  • Scope: client-side only. The agent private key is never transmitted to the LI.FI backend.

Expiry and re-provisioning

An agent approval can lapse (Hyperliquid policy, or an optional TTL set at approval time), and a user on a new device won’t have the stored key. When that happens, checkSetup() re-surfaces approveAgent and the user re-approves once. Agents are not silently re-provisioned mid-trade — a trading call with no valid agent fails and points you back through Setup, so a re-approval is always an explicit, user-visible step.

Who signs what

Agent lifecycle

Keep the object returned by hyperliquidProvider() in a variable to call the agent methods; perps.getProvider('hyperliquid') is typed as the generic provider and lacks them.
The StorageAdapter contract is get(key): Promise<string \| null>, set(key, value): Promise<void>, and remove(key): Promise<void>. Each agent is stored as a JSON string holding { address, privateKey }. Approved agents appear in the account’s config.agents array — see Account Abstraction / Account Configuration.