- 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 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. OnPerpsClient, 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. -
revokeAgentandrevokeSessionAgent— free an agent slot.revokeSessionAgentremoves the session agent the SDK signs with, targeted by itsagentAddress. -
revokeBuilderFee— withdraws the builder fee approval. -
accountMode— changes the account’s abstraction mode.
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 theapproveAgent 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 pluggableStorageAdapter. 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 byhyperliquidProvider() 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.