Debugging signatures and wire shapes
A signature over wrong bytes still verifies, so HyperCore recovers a stranger's address and says it does not exist; the fix is almost always the encoding.
An Ethereum-style signature doesn’t carry the signer’s address. The verifier recovers it from the signature and the message hash. Sign the wrong bytes and recovery still succeeds, deterministically returning a stranger’s address, which HyperCore looks up and rejects:
L1 error: User or API Wallet 0x0123... does not exist.Must deposit before performing actions. User: 0x123...The docs list these two for an incorrect signature. They read like account problems but are usually serialisation bugs, and neither says where the bytes differ.
Say you sign an order whose decimal strings are "0.5000" and "10.00". The correct hash, the third vector below, comes only from "0.5" and "10". Hash the zeros and the signature recovers a stranger’s address.
The error shows how far the request got
Section titled “The error shows how far the request got”In effect, the server parses the body, re-encodes and hashes it, recovers a signer, looks it up, then validates and executes. The first stage that fails decides the response:
| Response | Got as far as | Means |
|---|---|---|
HTTP 422, plain text Failed to deserialize ... |
Parsing | The JSON matches no action shape: wrong type name, missing or extra field, wrong field type. The signature was never looked at |
Unable to recover signer |
Parsed | The shape is valid; the signature could not be turned into an address at all |
User or API Wallet 0x... does not exist |
Parsed and recovered | The recovered address is unknown. Either your bytes differ from the server’s, or the API wallet is not approved, or not approved on this network |
Must deposit before performing actions |
Parsed and recovered | The recovered address is not a funded account. Same causes, or a genuinely new, unfunded account |
Invalid multi-sig outer signer |
Multi-sig envelope | Zero-padded r or s in the inner signatures, or a checksummed address; see multi-sig |
Price must be divisible by tick size. |
Parsed and validated | A real tick violation, or a trailing-zero mismatch between the body and the signed bytes |
- Account or encoding? If the error names your signer’s address, the account really is unknown or unfunded, even with perfect encoding. If not, and it changes when the input changes (the docs’ diagnosis), the encoding is wrong.
- Which encoding? If L1 actions from a key work but a user-signed action from the same key recovers a stranger, the EIP-712 type table is wrong, not the msgpack.
- Load, not encoding.
too many pending post requestson a WebSocket post is about load (WebSocket).
Checklist: where the bytes usually differ
Section titled “Checklist: where the bytes usually differ”| Check | Right | What wrong looks like |
|---|---|---|
| Scheme | L1 path for L1 actions, typed data for user-signed ones | Mixing them is the first common error the documentation lists |
| Key order | The server’s order: type, orders, grouping, builder; a, b, p, s, r, t, c |
An object built from an unordered or sorted map |
| Trailing zeros | "0.65", "10", "123" |
"0.650", "10.0", or the docs’ own userOutcome example "123.0" |
| Letter case | Every address lowercase: builder b, destination, vault, multiSigUser, outerSigner |
A checksummed address; destination is typed string, so its case is hashed |
| Integer width | Smallest msgpack width; 2^32 and above as uint64 | An oid of 2^32 or more encoded as a float64 |
| Omit or null | f and a omitted when false, cloid omitted when unset, null merge amounts |
f: false or a: false in the hash, which the exchange rejects |
| Network | source "a" with hyperliquidChain "Mainnet" on the mainnet host, "b" and "Testnet" on testnet |
A valid signature for the other network |
| Chain id | 1337 for every L1 action; for user-signed ones, the domain chain id equal to signatureChainId |
An L1 action signed under 42161 |
| Nonce | One value in the body, in the hash, and in the action’s time or nonce field |
time differing from the outer nonce |
| Optional fields | vaultAddress and expiresAfter in the hash exactly when they are in the body |
Hashing one and not sending it, or the reverse |
| Markers | 0x01 before a vault address, 0x00 before expiresAfter, 0x00 alone for no vault |
0x01 before expiresAfter, or a 0x00 expiry marker with no expiry |
| Type table | Exact field names, types, order and primary type: HyperliquidTransaction:Withdraw for withdraw3 |
A field swapped, or Withdraw3 |
| Tuple lists | Sorted before signing in HIP-3 and HIP-4 deployer actions; keywordToValue as sorted pairs |
Sorted after signing, or sent as an object |
| Signature | {r, s, v} with v 27 or 28; minimal-hex r and s inside multi-sig payloads |
A 65-byte hex string, or zero-padded inner values |
| API wallet | Approved on this network and not expired | An approval made on testnet, or one past its valid_until |
Local recovery is not on the list: it only shows your code agrees with itself, while the server rebuilds the payload from the action it parsed, which is where the divergence lives.
The deserialisation probe
Section titled “The deserialisation probe”Because parsing comes first, a dummy signature safely tests whether the exchange accepts a shape:
SIG='{"r":"0x1111111111111111111111111111111111111111111111111111111111111111","s":"0x2222222222222222222222222222222222222222222222222222222222222222","v":27}'curl -sS -X POST https://api.hyperliquid-testnet.xyz/exchange \ -H 'Content-Type: application/json' \ -d "{\"action\":{\"type\":\"noop\"},\"nonce\":1,\"signature\":$SIG}"Replace the action with the shape under test:
- HTTP 422
Failed to deserialize: the shape is wrong, or no longer exists. Unable to recover signer,does not exist, orMust deposit before performing actions: the shape parsed.
Nothing can take effect: a made-up signature recovers no account that holds anything. Probe both hosts, since mainnet and testnet have parsed different shapes for the same action. This is also how undocumented actions are found and documented ones confirmed dead.
Failed to deserialize is a catch-all that names no field: remove optional fields one at a time.
Golden test vectors
Section titled “Golden test vectors”Pin signing code with known-good vectors in unit tests, so an encoder change that alters a byte fails a test instead of an order. Capture each new action’s vector from a reference implementation before shipping; when one fails, log the msgpack bytes as hex and the connectionId and compare byte by byte, as the docs advise.
Four public vectors, reproduced with the code on signing; the L1 vectors use nonce 1700000000000, no vault and no expiresAfter:
Order action {"type":"order","orders":[{"a":100017580,"b":true,"p":"0.55","s":"10","r":false,"t":{"limit":{"tif":"Gtc"}}}],"grouping":"na"} connectionId 0x4b5e3a6100e09595c2d6832bc12e92580c2fc43f9979f3496babd393acbef3c5
Cancel action {"type":"cancel","cancels":[{"a":100017580,"o":12345}]} connectionId 0x44fbf1da741dc60547399e5c8bb94a3fc7b3a46c600419590ee152efba9164a9
Order written with trailing zeros ("0.5000", "10.00"), stripped before hashing action {"type":"order","orders":[{"a":100000090,"b":true,"p":"0.5","s":"10","r":false,"t":{"limit":{"tif":"Gtc"}}}],"grouping":"na"} connectionId 0x7870e83874912e8953a1bc1ab37dff9bec4deeb971d49c46736937899130fb88
usdSend (user-signed), signatureChainId 0xa4b1 message {"hyperliquidChain":"Mainnet","destination":"0x0d1d9635d0640821d15e323ac8adadfa9c111414","amount":"1","time":1690393044548} signature 0xeca6267bcaadc4c0ae1aed73f5a2c45fcdbb7271f2e9356992404e5d4bad75a3572e08fe93f17755abadb7f84be7d1e9c4ce48bb5633e339bc430c672d5a20ed1b recovers 0xcd49bbac6e85fdeb167eb7ca41a945d2b8758f6f- The third matches only once
"0.5000"and"10.00"are stripped to"0.5"and"10". - The fourth reproduces only with
destinationlowercase; the checksummed address signs differently. - A user-signed vector needs no private key: recover the address from the typed data and signature, and compare.
Passing the vectors is not enough to submit. Their nonces are far outside the live window, so an action signed with them is refused even when every byte is right; use a live nonce (nonces).
For the byte layout this page debugs, see Signing from first principles. For every error by cause, see Error responses.