Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development14 min read

Hyperliquid utils Module: Signing, Hashing and Action Encoding

A deep-dive into the hyperliquid-python-sdk utils layer: deterministic action hashing, EIP-712 typed-data signing, float-to-wire formatting, and the failure modes that break signed requests.

TL;DR

The hyperliquid-python-sdk utils layer exists because Hyperliquid's exchange endpoint does not accept a bearer-token JSON body for trading actions. Instead, the client builds a typed action, hashes it deterministically, signs that hash with the API wallet's private key using EIP-712-style typed data, and attaches the signature, nonce, and vault address to the request. The utils module supplies the action hashing helpers, the signature construction, and the float-to-wire formatting helpers that keep the signed bytes identical to the transmitted bytes. Float formatting is a correctness hazard: if the client signs one decimal string and transmits another, the reconstructed hash differs and the exchange rejects the request. This article explains the mechanism, provides runnable Python examples for order placement and multi-action envelopes, and maps the failure modes to diagnostic steps.

Why Hyperliquid Requires a Client-Side Signing Layer

Hyperliquid's exchange API is not a conventional REST API where a bearer token authorizes a JSON body. According to the Hyperliquid API documentation, trading actions must be signed by an API wallet whose address is registered to the account. The exchange verifies that the request was authorized by that wallet, which is the mechanism that lets an API wallet trade without holding withdrawal permission.

The signing layer therefore sits between your application logic and the HTTP transport. It takes a structured action, produces a deterministic hash of that action, signs the hash as EIP-712 typed data over a domain, and wraps the result with a nonce and optional vault address. The hyperliquid-python-sdk exposes this in its utils module so that callers do not reimplement the hashing and encoding rules.

This separation is also why articles about 'hyperliquid utils github' should not become installer instructions. The important artifact is the protocol-level mental model: action, deterministic hash, typed-data signature, nonce/vault envelope, POST. That model transfers across the Python, Rust, and Go SDKs and across CCXT integrations.

  • The exchange endpoint multiplexes typed actions rather than exposing one route per action.
  • The API wallet key authorizes trading actions but is distinct from the account's withdrawal authority.
  • The signed payload must be byte-identical to the payload the server reconstructs.

The Three Responsibilities of the utils Module

The utils layer has three distinct jobs, and conflating them is a common source of integration bugs. First, it hashes the action payload deterministically so that the signed bytes match the exact payload the server reconstructs. Second, it constructs the signature over the typed data, following the EIP-712 structured hashing and signing scheme described in EIP-712. Third, it formats floats into wire strings with a fixed number of decimals.

The float formatting helpers are not cosmetic. Prices and sizes must be stringified with the correct number of decimals for each asset before hashing. If the client signs '1.2345678901' and transmits '1.23456789', the hash differs and the exchange rejects or misprices the order. The utils formatting helpers and the per-coin size and price decimal rules must be used consistently on both sides of the signing boundary.

Address and key handling is the fourth implicit responsibility. The API wallet private key should be loaded from the environment or a secret manager, never committed to source control, and the SDK's wallet object should be the only place the key lives in the process. The OnFinality Learn hub has related material on endpoint selection and error handling that complements this signing-focused article.

  • Action hashing: deterministic serialization of the action payload.
  • Signature construction: typed-data signing over the Hyperliquid domain.
  • Float formatting: fixed-decimal stringification for prices and sizes.
  • Key handling: environment or secret-manager loading, single in-process wallet object.

Action Hashing and the Byte-Identity Requirement

The hash is computed over the action payload after it has been normalized into the wire representation. That means numeric fields must already be strings with the correct decimals, field ordering must follow the SDK's encoder, and optional fields such as vault address must be present or absent consistently. The server reconstructs the same payload from the request body and recomputes the hash; any divergence produces a signature or payload-hash mismatch.

In practice, the safest pattern is to build the action once, format it once with the utils helpers, and then use that same object for both hashing and transmission. Do not format for display and then re-format for transmission, and do not let a JSON serializer reorder or re-quantize fields between the two steps.

The Hyperliquid API error handling and order rejections article covers how the exchange surfaces these mismatches, but the root cause is almost always a formatting or ordering divergence on the client.

  • Normalize numeric fields to strings before hashing.
  • Use one formatted action object for both signing and transmission.
  • Avoid intermediate serializers that reorder or re-quantize fields.

EIP-712 Typed-Data Signing over the Hyperliquid Domain

The signature is produced over typed data rather than over a raw hash of the JSON body. The typed-data structure includes a domain separator and the action type, which binds the signature to the Hyperliquid exchange and prevents replay against a different domain. The EIP-712 specification defines the hashing and signing rules that the SDK follows.

The envelope attached to the request contains the signature components, a nonce, and an optional vault address. Nonces are tracked per signer and every new action must be larger than the smallest nonce in the signer's last 100 and must never have been used before, so a monotonically increasing counter per signer is the practical rule; a replayed or stale nonce is rejected even when the signature itself is valid. The vault address field is only meaningful when the action is executed on behalf of a vault.

Because the typed-data structure is protocol-level, it is identical across clients. A signature produced by the Python SDK and a signature produced by the Rust or Go SDK over the same action and nonce should verify identically. That is why the mental model transfers and why the utils module is best understood as an implementation of the protocol, not as a Python-specific convenience.

  • Domain separator binds the signature to the Hyperliquid exchange.
  • Nonce must be monotonic per signer.
  • Vault address is optional and only set when acting on behalf of a vault.

Runnable Example: Sign and Submit an Order, Then Read It Back

The example below stays inside the utils layer instead of hiding it behind Exchange.order: it builds the wire order, converts it to the signed action, hashes and signs it with sign_l1_action, and prints the exact payload that goes to the exchange endpoint. It assumes the API wallet private key and the master account address are in environment variables. The endpoint is a placeholder here; select one from the Hyperliquid RPC endpoints (RPC Assistant) page or your own infrastructure.

Two details matter more than the boilerplate. First, the payload that is printed is the payload that must be sent: re-serializing the order with a different float representation changes the msgpack bytes and therefore the hash. Second, the signed fields p and s are strings produced by float_to_wire, not floats, because msgpack encodes integers and floats differently and the exchange reconstructs the action from the transmitted strings. To confirm the round trip, read the order back with the info endpoint by oid and compare the reported price and size to the strings you signed.

import json, os
from eth_account import Account
from hyperliquid.utils.constants import MAINNET_API_URL
from hyperliquid.utils.signing import (
    float_to_wire, get_timestamp_ms, order_request_to_order_wire,
    order_wires_to_order_action, sign_l1_action,
)

acct = Account.from_key(os.environ["HL_API_WALLET_PRIVATE_KEY"])  # API wallet

# 1) Build the action exactly as the signed payload will carry it.
req = {"coin": "BTC", "is_buy": True, "sz": 0.001, "limit_px": 60000.0,
       "order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False}
order = order_request_to_order_wire(req, asset=0)      # p and s become strings
action = order_wires_to_order_action([order], grouping="na")

# 2) Sign: msgpack(action) + 8-byte nonce + vault flag -> keccak -> EIP-712 "Agent".
nonce = get_timestamp_ms()
signature = sign_l1_action(acct, action, None, nonce, None, True)

# 3) Transmit exactly the strings that were signed.
payload = {"action": action, "nonce": nonce, "signature": signature, "vaultAddress": None}
print(json.dumps(payload, indent=2))
# POST as JSON to MAINNET_API_URL + "/exchange" with your HTTP client.

print("signed p:", repr(action["orders"][0]["p"]),
      "signed s:", repr(action["orders"][0]["s"]),
      "float_to_wire(60000.0):", repr(float_to_wire(60000.0)))

Reusing One Signed Envelope Across Action Types

The exchange endpoint multiplexes typed actions, so the same signing path applies to orders, cancels, and leverage updates. The field set differs per action, but the envelope structure is the same: action, nonce, signature, and optional vault address. Seeing this in code makes the abstraction concrete and shows where each action's fields diverge.

The example below issues a cancel and an update-leverage request through the same exchange object. Each call internally builds the action, formats it, hashes it, signs it, and attaches a fresh nonce. Each call re-signs a fresh action with its own nonce, so the reader should observe that no separate signing code is needed per action type; the utils layer handles the encoding differences.

import os
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from eth_account import Account

api_key = os.environ["HL_API_WALLET_PRIVATE_KEY"]
account = Account.from_key(api_key)
account_address = os.environ["HL_ACCOUNT_ADDRESS"]

exchange = Exchange(account, constants.MAINNET_API_URL, account_address=account_address)

# Place an order to obtain an oid.
order_result = exchange.order(
    "ETH", True, 0.01, 3000.0, {"limit": {"tif": "Gtc"}}, reduce_only=False
)
print("order_result:", order_result)

# Cancel the order using the same signing path.
if order_result.get("status") == "ok":
    oid = order_result["response"]["data"]["statuses"][0]["resting"]["oid"]
    cancel_result = exchange.cancel("ETH", oid)
    print("cancel_result:", cancel_result)

# Update leverage for the same asset; a different action type, same envelope.
leverage_result = exchange.update_leverage(5, "ETH", is_cross=True)
print("leverage_result:", leverage_result)

Float Formatting as a Correctness Hazard

Float formatting is the most common source of payload-hash mismatches. Each asset has its own size and price decimal rules, and the wire representation must respect them. If the client signs a value with more decimals than the asset allows, or with a different rounding than the transmitted value, the server reconstructs a different payload and the signature check fails.

The utils helpers exist to centralize this formatting. Use them for every numeric field that participates in the signed action, and do not apply additional rounding after formatting. If your application needs to display a price differently, format for display separately from the signed payload.

A useful diagnostic is to log the formatted action immediately before signing and compare it to the request body captured at the transport layer. If the two differ in any numeric string, the mismatch is on the client side and no amount of retrying will fix it.

  • Use the SDK's formatting helpers for all signed numeric fields.
  • Do not re-round after formatting.
  • Log the formatted action and compare it to the transmitted body.

Measuring Your Own Round Trip Against Your Endpoint

Because endpoint behavior varies by provider, the only reliable measurement is one you perform against your own endpoint. The table below is a template: fill it with observations from your environment rather than relying on published numbers. The goal is to distinguish signing failures from transport or risk-engine failures.

Run the same signed action repeatedly and record the outcome category for each attempt. If failures cluster around signature or payload-hash errors, the issue is in the signing layer. If failures cluster around nonce errors, the issue is ordering. If failures cluster around margin or risk responses, the signing layer is working and the problem is account state.

  • Results Table columns: attempt, action type, outcome category, error code, formatted payload hash, transmitted payload hash.
  • Outcome categories: accepted, signature error, payload-hash mismatch, nonce error, risk-engine rejection, transport error.
  • Compare formatted and transmitted hashes for every failed attempt.

Failure Modes and the Diagnostic Step That Distinguishes Them

Signature and address validation errors usually mean the wrong API wallet is signing, or the signing domain does not match the environment. Confirm that the API wallet address is registered to the account and that the SDK is pointed at the correct network. The Hyperliquid API documentation describes the registration model.

Payload-hash mismatches almost always trace to float formatting or field ordering. Compare the formatted action to the transmitted body. Nonce reuse or ordering problems produce errors even when the signature is valid; check that the nonce is monotonic per signer and that concurrent requests do not share a nonce.

Insufficient-margin rejections are risk-engine responses, not signing errors. The signature verified and the action was parsed; the account simply lacked margin. The Hyperliquid API error handling and order rejections article covers the response shapes, and the Hyperliquid API rate limits guide covers throttling behavior that can masquerade as transport failures.

  • Signature error: verify API wallet registration and signing domain.
  • Payload-hash mismatch: compare formatted and transmitted numeric strings.
  • Nonce error: enforce monotonic nonces per signer.
  • Risk-engine rejection: inspect account margin and position state.

Relationship to CCXT and the Rust and Go SDKs

The hashing and typed-data structure is protocol-level and identical across clients. CCXT integrations and the Rust and Go SDKs implement the same action-to-hash-to-signature pipeline, so the mental model transfers even when the API surface differs. This is why the utils layer is best studied as a protocol implementation rather than as a Python-specific utility.

When comparing implementations, focus on three things: how each client normalizes numeric fields, how it constructs the domain separator, and how it manages nonces. Differences in these three areas explain most cross-client discrepancies. The Hyperliquid WebSocket trades and fills channels and Hyperliquid WebSocket ping/pong heartbeat detection articles cover the streaming side, which uses a different authentication path than the signed exchange actions.

  • Compare numeric normalization across clients.
  • Compare domain separator construction.
  • Compare nonce management strategies.

Limitations, Tradeoffs, and Next Steps

The utils layer does not remove the need to understand the protocol. It centralizes formatting and signing, but it cannot prevent a caller from re-formatting a value after signing or from sharing a nonce across concurrent requests. Treat the formatted action as immutable once it has been hashed.

Provider-specific behavior, including endpoint availability and rate limits, varies by provider and should be verified against your own measurements. The RPC pricing and API service pages describe OnFinality's offerings, and the Hyperliquid RPC endpoints (RPC Assistant) page helps with endpoint selection. For network-level context, see the Hyperliquid network page.

As a next step, instrument your client to log the formatted action, the computed hash, and the transmitted body for every request, then run the results-table exercise against your endpoint. That practice turns signing failures from opaque errors into diagnosable events.

  • Treat the formatted action as immutable after hashing.
  • Verify provider behavior with your own measurements.
  • Instrument formatted action, hash, and transmitted body logging.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started