Signature errors on Hyperliquid often produce cryptic messages that don’t point to the actual cause. This guide covers the most common issues developers encounter, based on real problems reported in the Hyperliquid developer community.
The address isn’t random—it’s the recovered address from your signature. Hyperliquid uses ECDSA signature recovery to determine who signed the message. When your signature is malformed, the recovery produces a different (invalid) address each time.
The action hash depends on msgpack serialization, which is order-sensitive. Different key orders produce different hashes, leading to different recovered addresses.
# WRONG - unpredictable key order in Python dictaction = {"sz": "0.001", "coin": "BTC", "is_buy": True}# CORRECT - use SDK helper functions that ensure consistent orderingfrom hyperliquid.utils.signing import order_request_to_order_wireaction = order_request_to_order_wire(order_request)
Hyperliquid L1 actions require signing with chainId 1337, but your wallet is connected to Arbitrum (42161), Optimism (10), or another chain. Browser wallets enforce that the signing chain matches the connected chain.
Create an agent wallet for trading operations. The agent signs server-side with chainId 1337, avoiding the browser mismatch entirely:
import { ExchangeClient, HttpTransport } from "@nktkas/hyperliquid";import { privateKeyToAccount } from "viem/accounts";// Agent wallet signs with chainId 1337 automaticallyconst agentAccount = privateKeyToAccount(AGENT_PRIVATE_KEY);const client = new ExchangeClient({ transport: new HttpTransport(), wallet: agentAccount});// User's browser wallet only signs approveAgent (chainId 0x66eee)// which matches Arbitrum, so no mismatch
import { ExchangeClient, HttpTransport } from "@nktkas/hyperliquid";import { privateKeyToAccount } from "viem/accounts";// First: user approves agent via browser wallet (one-time)const agentAddress = await approveAgentWithBrowserWallet();// Then: agent handles all tradingconst agentClient = new ExchangeClient({ transport: new HttpTransport(), wallet: privateKeyToAccount(agentPrivateKey)});// No chainId conflicts because browser wallet never signs L1 actions
import { ExchangeClient, HttpTransport } from "@nktkas/hyperliquid";import { privateKeyToAccount } from "viem/accounts";const account = privateKeyToAccount("0x..."); // your private keyconst client = new ExchangeClient({ transport: new HttpTransport(), wallet: account});const result = await client.order({ orders: [{ a: 0, // Asset index (0 = BTC) b: true, // is_buy p: "50000", // price s: "0.001", // size r: false, // reduce_only t: { limit: { tif: "Gtc" } } }], grouping: "na"});
import { ExchangeClient, HttpTransport } from "@nktkas/hyperliquid";import { Wallet } from "ethers";const wallet = new Wallet("0x..."); // your private keyconst client = new ExchangeClient({ transport: new HttpTransport(), wallet});// Same API as with viemconst result = await client.order({...});
If you’re building a HIP-3 DEX and need to push oracle prices, you’ll encounter reserveRequestWeight—an action that uses a unique hybrid signing pattern.
# The SDK handles this internally for HIP-3 operations# If implementing manually, use phantom agent construction# but sign with the user's wallet (not an agent wallet)action = { "type": "reserveRequestWeight", "asset": 110000, # HIP-3 asset index "weight": 1000}# Uses phantom agent hash BUT requires user wallet signaturesignature = sign_l1_action( user_wallet, # NOT agent_wallet action, None, timestamp, None, True)