Typed, asynchronous Hyperliquid REST client for Python.
Version 1 has two explicit entry points:
InfoClientis credential-free and only calls the Info API.AsyncHyperliquidowns one shared HTTP transport and exposes concrete.infoand.exchangeclients. It always requires an account address and signing key.
The client uses aiohttp and preserves Hyperliquid's JSON-shaped requests and
responses as TypedDict. Small value inputs such as Builder remain frozen,
slotted dataclasses; responses are not wrapped in runtime model layers.
pip install async-hyperliquidWith uv:
uv add async-hyperliquidRead-only callers do not need an address, API wallet, or generated private key.
info_url may be an official endpoint, a self-hosted node, or a compatible
third-party provider. The URL is used exactly as supplied, so include the
provider's complete /info path.
import asyncio
from async_hyperliquid import InfoClient
from async_hyperliquid.types import Network
async def main() -> None:
async with InfoClient(
network=Network.MAINNET,
info_url="https://provider.example/hyperliquid/info",
) as info:
mids = await info.all_mids()
positions = await info.positions(
"0x0000000000000000000000000000000000000000"
)
print(mids.get("BTC"), positions)
asyncio.run(main())Omit info_url to use Network.MAINNET.info_url or
Network.TESTNET.info_url.
Trading is deliberately separate from read-only access. AsyncHyperliquid
requires both credentials. Read from client.info, call client.exchange for
Info-independent signed actions, and use root workflows when a request needs
both market data and signed execution.
import asyncio
import os
from async_hyperliquid import AsyncHyperliquid
from async_hyperliquid.types import Network, PlaceOrderRequest, TimeInForce, limit_order_type
async def main() -> None:
async with AsyncHyperliquid(
os.environ["HL_ADDR"],
os.environ["HL_SK"],
vault_address=os.environ["HL_SUB"],
network=Network.TESTNET,
) as client:
order: PlaceOrderRequest = {
"coin": "BTC",
"is_buy": True,
"sz": 0.001,
"px": 50_000,
"is_market": False,
"order_type": limit_order_type(TimeInForce.ALO),
}
result = await client.place_limit_order(order)
print(result)
asyncio.run(main())HL_SK is the API-wallet private key. It is parsed locally and is never sent
to an Info or Exchange provider. HL_AK is the corresponding public API-wallet
address; it is not a signing key and it is not a portfolio address.
Set vault_address= when the signer trades on behalf of a Hyperliquid vault or
subaccount:
client = AsyncHyperliquid(
os.environ["HL_ADDR"],
os.environ["HL_SK"],
vault_address=os.environ["HL_SUB"],
network=Network.MAINNET,
)The address is normalized once and becomes the client's execution target.
Execution-scoped L1 actions such as orders and cancels sign and post with that
target, and account-dependent helpers such as close_positions query it.
Root-scoped administration actions sign as the main account; protocol-specific
transfers encode the vault/subaccount in their own action fields. Omit
vault_address to trade the main account. A client cannot be retargeted after
construction; concurrent targets should use separately owned API wallet/client
pairs.
One place_orders call creates one action, one signature, and one HTTP POST.
Use it instead of looping when the orders belong in one atomic Hyperliquid
batch.
import asyncio
import os
from async_hyperliquid import AsyncHyperliquid
from async_hyperliquid.types import Network, PlaceOrderRequest, TimeInForce, limit_order_type
async def main() -> None:
orders: tuple[PlaceOrderRequest, ...] = (
{
"coin": "BTC",
"is_buy": True,
"sz": 0.001,
"px": 50_000,
"is_market": False,
"order_type": limit_order_type(TimeInForce.GTC),
},
{
"coin": "ETH",
"is_buy": False,
"sz": 0.01,
"px": 4_000,
"is_market": False,
"order_type": limit_order_type(TimeInForce.GTC),
},
)
async with AsyncHyperliquid(
os.environ["HL_ADDR"],
os.environ["HL_SK"],
vault_address=os.environ["HL_SUB"],
network=Network.TESTNET,
) as client:
result = await client.place_orders(orders)
print(result)
asyncio.run(main())PlaceOrderRequest is the one placement vocabulary for direct and batch order
methods, and is_market is explicit on every request. ModifyOrderRequest
adds oid to the same shared order fields.
LimitOrderOption and TriggerOrderOption mirror the protocol's nested order
type, cloid is the only client-order-ID spelling, and order attribution uses
Builder.
place_orders is the only batch placement pipeline. It accepts market and
non-market requests together when they resolve to the same venue, normalizes
only the market subset in one batched mid-price phase (one allMids call per
distinct DEX), and rejects a spot/perpetual mixture before signing. Use
place_market_order for one market request and place_orders for a market
batch.
Builder fees are expressed in tenths of a basis point and are capped at 100
for perpetual batches and 1000 for spot or outcome batches. The venue is
selected from resolved metadata; a spot buy with builder attribution is not
rejected locally. Outcome order prices use the 0.00001 USDC tick and must be
between 0.00001 and 0.99999 USDC. Minimum order notional is validated by
the Exchange, not by this SDK.
AsyncHyperliquid.place_order(...) deliberately keeps the expanded 0.5 call
shape. is_market=True selects market-price discovery; otherwise the nested
order_type selects limit or trigger placement. place_orders consumes typed
requests, and batch_place_orders is the same function—not a forwarding
wrapper.
Order placement, cancellation, modification, TWAP, leverage, margin, and token
actions that resolve coin metadata live on AsyncHyperliquid. The concrete
ExchangeClient owns only Info-independent action construction, nonce/signing,
vault targeting, and submission; it never holds an InfoClient.
trigger_px delays a TWAP until the market reaches its trigger; stop_px
stops it at the configured price. Either keyword may be supplied independently:
async def place_advanced_twap(client: AsyncHyperliquid) -> None:
result = await client.place_twap(
"BTC",
True,
0.01,
30,
trigger_px=105_000.0,
stop_px=95_000.0,
)
print(result)The client derives the protocol's trigger-direction flag from the current mark price. Callers supply the price, not the wire-level flag.
close_position, close_positions, and close_all_positions close the full
live size. They expose no size or slippage override. One workflow performs one
position query and submits all required reduce-only market orders in one
Exchange batch.
Network is the only signing-domain selector. URLs never decide whether an
action is signed for mainnet or testnet.
| Setting | Responsibility |
|---|---|
network |
Signing domain and official endpoint defaults |
info_url |
Exact URL used only by InfoClient |
exchange_url |
Exact URL used only by ExchangeClient |
This supports independently routing reads and writes:
import asyncio
import os
from async_hyperliquid import AsyncHyperliquid
from async_hyperliquid.types import Network
async def main() -> None:
async with AsyncHyperliquid(
os.environ["HL_ADDR"],
os.environ["HL_SK"],
vault_address=os.environ["HL_SUB"],
network=Network.MAINNET,
info_url="http://127.0.0.1:3001/info",
exchange_url="https://trading-provider.example/exchange",
) as client:
print(await client.info.mid_price("BTC"))
asyncio.run(main())The example still signs for mainnet. The self-hosted Info node receives unsigned Info requests. Its metadata and prices nevertheless determine the asset ids, precision, and limit prices used to build signed actions, so an Info provider attached to an authenticated client is trusted order-construction input. The Exchange provider receives the signed action envelope, never the signing key. Redirects are rejected; each configured URL is the exact request destination.
The library does not add endpoint fallback, provider authentication, health checks, or load balancing. Applications own those policies.
A custom Exchange provider is a trusted execution boundary. It can observe
replayable signed envelopes, delay or censor them, and fabricate a well-shaped
acknowledgement. A custom Info provider used only through standalone
InfoClient remains read-only, but one used by AsyncHyperliquid must also be
independently trusted because its data shapes signing intent. expires_after
limits only L1 actions that expose that parameter; it does not add an expiry to
user-signed fund actions. Reconcile through an independently trusted
InfoClient endpoint before resubmitting an indeterminate action.
Constructors do not create asynchronous resources. Prefer async with, or call
open() and close() explicitly.
When no session is supplied, the client owns and closes one session. When an
aiohttp.ClientSession is supplied, the client borrows it and never closes it.
AsyncHyperliquid.info and .exchange share exactly one transport.
Do not place an Authorization header or provider cookie on that shared
session when Info and Exchange use different origins: session-wide credentials
would be sent to both. Attach credentials with host-scoped aiohttp middleware,
or use separate clients/sessions for separately authenticated providers.
import os
from aiohttp import ClientHandlerType, ClientRequest, ClientResponse, ClientSession
from async_hyperliquid import AsyncHyperliquid
async def provider_auth(
request: ClientRequest, handler: ClientHandlerType
) -> ClientResponse:
if request.url.host == "trading-provider.example":
request.headers["Authorization"] = f"Bearer {os.environ['PROVIDER_TOKEN']}"
return await handler(request)
async def main() -> None:
async with ClientSession(middlewares=(provider_auth,)) as session:
async with AsyncHyperliquid(
os.environ["HL_ADDR"],
os.environ["HL_SK"],
vault_address=os.environ["HL_SUB"],
info_url="http://127.0.0.1:3001/info",
exchange_url="https://trading-provider.example/exchange",
session=session,
):
...The default timeout has finite total, connect, and socket-read budgets. A
custom aiohttp.ClientTimeout must keep its total budget finite and positive;
optional phase budgets are validated when provided.
All library errors derive from HyperliquidError. Import detailed error types
from async_hyperliquid.errors.
A timeout, connection failure, non-success HTTP response, or untrusted JSON
response after submitting a signed action raises IndeterminateActionError.
The client does not retry signed actions automatically: the server may already
have accepted the nonce. Reconcile using Info calls such as order_status,
open_orders, or user_fills before deciding whether to submit another
action.
Nonce ordering is local to one ExchangeClient. One API wallet private key
must therefore have exactly one live owner submitting Exchange actions.
Multiple processes or services sharing the same API wallet must serialize and
coordinate nonces at the application boundary; this library deliberately does
not provide a distributed nonce service. This restriction does not apply to
credential-free InfoClient instances.
The package includes py.typed.
- Info and Exchange wire responses use exact
TypedDictcontracts. - JSON-shaped order commands use
TypedDict; value objects use frozen, slotted dataclasses. - Public signatures contain no
Anyor unparameterized containers. - Response dictionaries are not copied into runtime model objects.
The default deterministic suite performs no network API calls. Run it separately from the live integration suites:
uv run pytest -q tests/unit tests/contracts tests/oracle tests/public_api tests/package
uv run ruff check src tests benchmarks
uv run ty check src
uv run ty check tests/contracts
uv run ty check tests/integration
uv run ty check tests/oracle
uv run ty check tests/package
uv run ty check tests/public_api
uv run ty check tests/typing
uv run ty check tests/unit
uv run ty check benchmarksThe repository includes a parity-gated CPU benchmark of the real CCXT, official SDK, and async-hyperliquid signing implementations. It reports action hashing, signing-only, and order-to-payload construction separately; imports, initialization, metadata, HTTP, and subprocess startup are outside the timed loops.
uv run --frozen --group benchmark python benchmarks/signing.py --rounds 7 --warmups 1 --iterations 5000See the reproducible benchmark manual for environment setup, fairness gates, exact measurement semantics, JSON output, and result interpretation.
On an Apple M5 with Python 3.12.13 and CoinCurve 21.0.0, three independent complete runs produced this equal-weight, geometric-mean throughput across all five measured operations:
| Library | Overall throughput | Relative to SDK |
|---|---|---|
| async-hyperliquid 1.0.0rc1 | 24,641 ops/s | 1.460x |
| hyperliquid-python-sdk 0.24.0 | 16,874 ops/s | 1.000x |
| CCXT 4.5.71 | 803 ops/s | 0.0476x |
Higher is better. This is a synthetic signing/payload-construction score, not end-to-end order latency. Every report used seven measured rounds after one warmup, and CCXT's CoinCurve signer was verified before timing. The detailed manual contains the machine specification, per-operation median/MAD/p95, throughput, aggregation formula, and exact reproduction command.
The repository also includes a rate-controlled BTC perpetual testnet benchmark for concurrent async-hyperliquid cancellation by order ID (OID) and client order ID (CLOID). Each logical round places 20 ALO orders in one batch: ten buys at 90% of mid and ten sells at 110%, approximately 11 USDC each. It then releases ten OID and ten CLOID independent single-order cancellations through a shared start gate. See the live benchmark safety and reproduction manual before running it because it submits real testnet orders.
The validated testnet run uses concurrency=20 (10 OID + 10 CLOID) single-order cancellation requests per measured round.
| Identifier | Individual median (ms) | Individual p95 (ms) | Round-max median (ms) | Round-max p95 (ms) |
|---|---|---|---|---|
| OID | 916.78 | 1003.99 | 948.75 | 1131.51 |
| CLOID | 913.98 | 1001.03 | 945.31 | 1026.93 |
See the detailed methodology, distributions, and artifacts.
The credential-free Info command always runs the complete suite against both MAINNET and TESTNET:
uv run pytest -q tests/integration/test_info.pyOn the first HTTP 429 response, the Info integration client waits 60 seconds and retries the request once; a second 429 skips the affected case. A TESTNET 5xx response emits a warning and skips the affected case, while the same MAINNET failure remains a test failure.
Signed Exchange integration is testnet-only and uses IS_MAINNET as its only
network safety gate. Set it explicitly to false when running the suite;
missing, empty, true, or malformed values hard-fail before credentials or
clients are used:
IS_MAINNET=false uv run pytest -q tests/integration/exchangePytest and VS Code always collect the Info and Exchange cases. There are no additional integration-suite execution flags.
The local .env.local roles are:
| Variable | Role |
|---|---|
HL_ADDR |
master account address and portfolio identity |
HL_PK |
master account private key for master-only actions |
HL_AK |
API-wallet public address used only for role/key validation |
HL_SK |
API-wallet private key used for signed trading |
HL_SUB |
subaccount execution and portfolio address |
Tests validate that each private key derives the declared public address, that
local SDK and async-hyperliquid payloads match exactly, that HL_AK is an API
wallet for HL_ADDR, and that HL_SUB belongs to HL_ADDR. Private keys,
real signatures, and real payloads are never included in assertion messages,
logs, or fixtures.
Version 1 intentionally removes the dynamic facade, flat forwarding methods, legacy aliases, mutable endpoint reassignment, and embedded EVM client. See the 0.5 to 1.0 migration guide.
MIT. This community project is not affiliated with Hyperliquid.