Skip to content
Version v0.4 (draft)
ChangelogContributing

Markets

Hyperliquid

How the A2M reference implementation authorizes and executes orders on Hyperliquid.

Hyperliquid is a perpetual futures DEX with USDC margin. In A2M the venue name is hyperliquid. Instruments are Hyperliquid coin names; the reference adapter is configured for BTC and ETH by default.

Orders are placed on a main account that holds the funds. The adapter signs with a Hyperliquid API wallet (also called an agent wallet) that this main account has approved. An API wallet can trade on behalf of the main account but cannot withdraw or transfer funds. The trading agent never sees the key: only the adapter process holds it, and only FairX can reach the adapter.

  1. From the main account, approve an API wallet. On testnet that is done at app.hyperliquid-testnet.xyz/API; on mainnet, at app.hyperliquid.xyz/API.
  2. Configure the venue adapter. It is off by default. Only the adapter process should read the API wallet key; never commit a real key or account address.
Terminal window
HYPERLIQUID_ENABLED=1
HYPERLIQUID_NETWORK=testnet
HYPERLIQUID_ALLOW_MAINNET=0
HYPERLIQUID_ACCOUNT_ADDRESS=0xYourMainAccountAddress
HYPERLIQUID_AGENT_PRIVATE_KEY=your-api-wallet-private-key
HYPERLIQUID_INSTRUMENTS=BTC,ETH

HYPERLIQUID_NETWORK defaults to testnet. Mainnet requires both HYPERLIQUID_NETWORK=mainnet and HYPERLIQUID_ALLOW_MAINNET=1, so a misconfigured environment cannot trade real funds.

The agent does not connect to Hyperliquid itself. It calls the A2M MCP gateway. Finn and FairX sit between the agent and the adapter.

sequenceDiagram
  autonumber
  actor Agent
  participant GW as MCP gateway
  participant Finn
  participant FX as FairX OMS
  participant AD as Venue adapter
  participant HL as Hyperliquid

  Agent->>GW: a2m_place_order
  GW->>Finn: POST /v1/authorize
  Finn-->>GW: signed decision
  GW->>FX: POST /v1/orders
  FX->>AD: gRPC Submit
  AD->>HL: exchange.order
  HL-->>AD: ack or fills
  AD-->>FX: result

The steps in more detail:

  1. The agent calls a2m_place_order on the A2M gateway over MCP, authenticated with an OAuth bearer token.
  2. The gateway asks Finn to authorize the intent (POST /v1/authorize). Finn runs the mandate rules and, on ACCEPT, returns a signed, single-use decision with an expiry.
  3. The gateway sends that decision and its signature to FairX (POST /v1/orders). FairX verifies the signature, checks that the decision is executable, signed for custodial_fairx enforcement, and not expired, then consumes it so it cannot be used twice. Resubmitting a decision that already became an order returns the existing order and does not send again.
  4. Before sending, FairX checks that the venue is healthy, that the order type and time in force are declared capabilities (it never downgrades one to make an order fit), and that the venue’s current price still sits inside the signed collar. If any of those fail, it voids the decision and refuses; nothing is sent.
  5. FairX sends the order once to the Python venue adapter over gRPC (Submit), including a protect price on market orders. It never blindly resends.
  6. The adapter calls Hyperliquid exchange.order, signed by the API wallet, with a cloid derived from the client_order_id.
  7. FairX records the acknowledgement or fills, then reconciles against venue snapshots and user_fills. Applied fills update Finn’s reservation ledger; unused buying power is released when the order ends.
A2M Hyperliquid
client_order_id cloid: 0x plus the first 16 bytes of sha256(client_order_id) (32 hex characters). The same value is recomputed for cancel and reconcile; the adapter does not need a local mapping table.
Market order Hyperliquid has no native market orders. The order is sent as an IOC limit at the protect price: the signed reference price moved by the signed collar, in basis points (a buy may pay up to reference * (1 + collar_bps / 10000); a sell may receive no less than reference * (1 - collar_bps / 10000)), then rounded to a valid Hyperliquid price on the safe side. A buy is never rounded above the protect price, and a sell is never rounded below it. Market orders must use time_in_force=ioc.
Limit price Sent unchanged. If it does not fit Hyperliquid’s precision rules, the adapter rejects the order. The adapter never alters a price or quantity Finn signed.
reduce_only Passed through as reduce_only.
time_in_force gtc to Gtc, ioc to Ioc, post_only to Alo. fok and day are refused, not downgraded.
Account Must match the configured HYPERLIQUID_ACCOUNT_ADDRESS (comparison is case-insensitive). A mismatch is rejected before anything is sent.
  • Order types and times in force: limit with gtc, ioc or post_only; market with ioc only; stop and stop_limit as trigger orders (tpsl: sl). A stop carries the venue’s own TP/SL market slippage tolerance, 10% past the trigger; use stop_limit to set the worst price.
  • Order classes: bracket as one normalTpsl batch (entry, then reduce-only take-profit and stop-loss). The venue has no notional orders or options. Its trailing stop and TWAP are not implemented in the adapter yet; oto and oco are not mapped. replace uses the venue’s modify. Position-level TP/SL is not an OCO and is only declared as the feature position_tpsl.
  • Reduce-only: supported, and passed through to the venue.
  • Size precision: size must be a positive multiple of 10^-szDecimals for the instrument.
  • Price precision: a price may have at most 5 significant figures (integers are always allowed) and at most 6 - szDecimals decimal places on perpetuals. A signed limit that does not fit is rejected.
  • Minimum order value: 10 USDC. A reduce-only order may be below this minimum.
  • Quote currency: USDC.
  • Leverage: an order may carry leverage; the adapter sets it on the coin first (cross margin, unless the coin is isolated-only) and rejects the order if the venue refuses it. Without leverage, the order trades at the account’s current setting, which the adapter does not change. Finn caps that leverage by the rules’ max_leverage, reserves notional / leverage, and does not model funding or liquidation.

These are the states an order can take in FairX, and the transitions the state machine allows. Terminal states are filled, cancelled, rejected, expired and replaced. A bracket whose entry has filled sits in working while its exits rest. An order in unknown changes state only through reconciliation. The full machine, including pending_replace, is in the specification.

stateDiagram-v2
  [*] --> new
  new --> pending_new: sent
  new --> rejected
  pending_new --> acked
  pending_new --> partially_filled
  pending_new --> filled
  pending_new --> rejected
  pending_new --> expired
  pending_new --> unknown: no definitive venue reply
  pending_new --> cancelled
  acked --> partially_filled
  acked --> filled
  acked --> cancelled
  acked --> expired
  acked --> unknown
  partially_filled --> partially_filled: further fills
  partially_filled --> filled
  partially_filled --> cancelled
  partially_filled --> expired
  partially_filled --> unknown
  unknown --> acked: reconcile
  unknown --> partially_filled: reconcile
  unknown --> filled: reconcile
  unknown --> cancelled: reconcile
  unknown --> rejected: reconcile
  unknown --> expired: reconcile
  filled --> [*]
  cancelled --> [*]
  rejected --> [*]
  expired --> [*]
flowchart TD
  Rec["FairX Reconcile"] --> Snap["Adapter snapshot"]
  Snap --> Lookup["query_order_by_cloid<br/>same derived cloid"]
  Lookup --> Fills["user_fills, fill id hl_ plus trade id"]
  Fills --> Dedupe["FairX ignores a fill already applied"]
  Dedupe --> Apply["Apply venue status and fills"]
  Apply --> Ledger["Outbox to Finn: reservation ledger, mandate usage"]
  Apply --> Tca["On filled: FairX records TCA"]
  Lookup --> Missing{"Two definitive snapshots<br/>do not contain the order?"}
  Missing -->|yes, and never acknowledged| Reject["rejected, not found at the venue"]
  Missing -->|not yet| Watch["Keep watching"]
  Cancel["Cancel by cloid"] --> Confirm["Confirm against a snapshot"]
  Confirm -->|cancel visible; fills applied first| Release["Release the reservation"]
  Confirm -->|not yet visible| Pending["A2M_CANCEL_PENDING; reconcile settles it"]
  • Acknowledgement timeout. If the submit deadline is missed, or the venue reply is otherwise ambiguous, the order moves to unknown. FairX never resends. Reconciliation looks the order up with query_order_by_cloid (the same derived cloid) and applies the venue’s status and fills. An order that was never acknowledged, and that two definitive snapshots in a row do not contain (a complete snapshot, or one that lists the id as missing), is eventually rejected as not found at the venue.
  • Cancel. The adapter cancels by cloid. FairX then confirms against a snapshot before releasing the reservation, so a fill that raced the cancel is applied first. If the snapshot does not yet show the cancel, the caller receives A2M_CANCEL_PENDING and reconcile settles it.
  • Fills. Each fill is recorded with id hl_<tid> (Hyperliquid’s trade id) and is ignored if that id has already been applied. Applied fills are delivered to Finn, which updates the reservation ledger (the mandate usage the position limit is checked against). When an order reaches filled, FairX records TCA against the arrival price Finn signed.
  • Expired decision. A decision whose expiry has passed is refused with A2M_DECISION_EXPIRED and voided. Nothing is sent.
  • Venue down. If the venue is unhealthy, or it cannot report its capabilities, FairX fails closed with A2M_VENUE_DOWN and does not send. A missing or stale venue price is also a refusal (A2M_STALE_PRICE).
  • Testnet: verified with real orders and fills, including stop triggers, brackets and replaces.
  • Mainnet: not yet. The adapter will trade mainnet only when both HYPERLIQUID_NETWORK=mainnet and HYPERLIQUID_ALLOW_MAINNET=1 are set.