First-party signing for Hyperliquid.
Hyperliquid signs two kinds of payloads, both EIP-712 typed data sealed with a
secp256k1 signature (%{r, s, v} with v = 27 + recovery_id):
- L1 actions (
sign_l1_action/2) — order / cancel / transfer actions. The action is MessagePack-serialised (packb), concatenated with the nonce, optional vault address and optionalexpiresAfter, hashed with Keccak-256 into aconnectionId, wrapped in anAgentphantom struct and signed under theExchangedomain (chainId: 1337). - User-signed actions (
sign_user_signed_action/3) — USD transfers, withdrawals, approvals. Plain EIP-712 typed data under theHyperliquidSignTransactiondomain (chainId: 421614), no msgpack.
Credentials
Hyperliquid authenticates with an EVM wallet, not an API key/secret pair. The
EVM private key is carried in credentials.secret; the wallet address (when
needed for vault threading) in credentials.api_key.
L1 action packing
pack_l1_action!/1 writes the field order from Hyperliquid's exchange docs
and official Python SDK. The supported L1 actions cover orders, cancellations,
isolated-margin updates, TWAPs, sub-account transfers, and vault transfers.
Unknown shapes raise instead of falling back to unordered map serialization.
Summary
Functions
Computes the 32-byte Keccak-256 action hash (the EIP-712 connectionId).
Signs an L1 action, returning %{r, s, v} byte-equal to Bourse signL1Action.
Signs a user-signed action under the HyperliquidSignTransaction domain.
Functions
@spec action_hash(map(), String.t() | nil, non_neg_integer(), non_neg_integer() | nil) :: binary()
Computes the 32-byte Keccak-256 action hash (the EIP-712 connectionId).
Computes keccak256(packb(action) ‖ nonce ‖ vault ‖ expiresAfter) as defined
by Hyperliquid's signing contract.
vault_address and expires_after are optional (nil to omit).
@spec sign(Bourse.Signing.request(), Bourse.Credentials.t(), Bourse.Signing.config()) :: Bourse.Signing.signed_request()
Bourse.Signing.Behaviour entry point.
Expects the L1 or user-signed action and nonce in request.params
(:action/:nonce, with optional :vault_address/:expires_after). Produces
the Hyperliquid POST /exchange envelope —
%{"action", "nonce", "signature"} (+ vaultAddress) — as the JSON request body.
Unified callers receive a fully built :action from the internal Hyperliquid
request-shape layer; raw callers may still hand-feed :action as an override.
@spec sign_l1_action(map(), non_neg_integer(), keyword()) :: Bourse.Signing.Crypto.signature()
Signs an L1 action, returning %{r, s, v} byte-equal to Bourse signL1Action.
Options:
:private_key(required) —0x-prefixed EVM private key hex:vault_address— vault / sub-account address (omitted whennil):expires_after— action expiry, ms (omitted whennil):testnet—trueflips the phantom-agentsourceto"b"(defaultfalse)
@spec sign_user_signed_action(map(), map(), keyword()) :: Bourse.Signing.Crypto.signature()
Signs a user-signed action under the HyperliquidSignTransaction domain.
message_types is the EIP-712 type map (e.g.
%{"HyperliquidTransaction:UsdSend" => [...]}); message carries the typed
values. Returns %{r, s, v} byte-equal to Bourse signUserSignedAction.
Options: :private_key (required).