dex-exec
Minimal async Python library for executing orders on Hyperliquid perps and Uniswap V3 / Sushiswap / Pancakeswap on Arbitrum One.
Why
The official hyperliquid-python-sdk is synchronous, untyped, and awkward to integrate into async applications. No clean Python library exists for V3 AMM execution either. This library fills both gaps with a small, well-typed async API.
Scope: execution only. No signal logic, no strategy framework, no risk management. If you need real-time market data to power your signals, see mackinac — it provides a WebSocket feed with Hawkes process metrics, order book imbalance, and cross-venue arb signals that pairs naturally with this library (see examples/07_mackinac_feed.py).
Install
pip install dex-exec
Requires Python 3.10+.
Hyperliquid — getting started
Step 1 — Create an HL account and fund it
Go to app.hyperliquid.xyz and create an account. Your master wallet is the MetaMask address you connect with. Fund it with USDC on Arbitrum One (bridge via Arbitrum Bridge), then deposit into HL from the app.
Step 2 — Create an agent key
HL uses an agent wallet to sign orders on your behalf. Your master key never touches the API after setup.
import asyncio
from dexec import create_agent
async def setup():
creds = await create_agent(
master_private_key = "0x...", # your master key, used once
agent_name = "my-bot",
testnet = False,
)
print(f"Agent address: {creds.address}")
print(f"Agent private key: {creds.private_key}")
# Store creds.private_key securely — use it for all HLClient calls
asyncio.run(setup())
See examples/04_hl_agent_setup.py for the full flow. After this step, the master key can go back offline.
Step 3 — Place orders
import asyncio
from dexec import HLClient
AGENT_KEY = "0x..." # from create_agent above
MASTER_ADDRESS = "0x..." # your master wallet address
async def main():
async with HLClient(AGENT_KEY, MASTER_ADDRESS) as hl:
bal = await hl.get_balance()
print(f"Account value: ${bal.account_value:,.2f}")
result = await hl.place_order("ETH", "buy", size=0.01, price=2000.0)
print(f"Order: {result.status} cloid={result.cloid}")
asyncio.run(main())
Testnet: pass
testnet=TruetoHLClientandcreate_agent. Get a testnet account at app.hyperliquid-testnet.xyz — faucet USDC is available on the testnet site.
HL order constraints
- Minimum notional: $10 per order (size × price).
- Price tick size: varies by asset. Use the current mark price as a reference — HL rejects prices more than 80% away from it.
- IOC at aggressive price: for market-like fills, use
order_type='ioc'with a price slightly above (buy) or below (sell) the current mark.
AMM — getting started
Step 1 — Fund your Arbitrum wallet
You need:
- ETH on Arbitrum for gas (~$0.01–$0.10 per transaction).
- The token you want to swap on Arbitrum One. For WETH/USDC pairs, you need WETH — not ETH. Wrap it first (see below).
Bridge ETH or tokens from Ethereum mainnet via Arbitrum Bridge or buy directly on Arbitrum via a centralised exchange that supports Arbitrum withdrawals.
Step 2 — Wrap ETH → WETH if needed
Most pairs trade against WETH, not native ETH.
import asyncio
from dexec import AMMClient
async def main():
async with AMMClient(private_key="0x...") as amm:
tx = await amm.wrap_eth(0.05) # wrap 0.05 ETH → WETH
print(f"Wrapped: {tx}")
asyncio.run(main())
Step 3 — Pick a venue and fee tier
Each venue (Uniswap V3, Sushiswap, Pancakeswap) runs pools at different fee tiers. The most liquid pools for common pairs on Arbitrum One:
| Pair | Venue | Fee tier | Notes |
|---|---|---|---|
| WETH/USDC | uni_v3 |
500 (0.05%) | Deepest WETH/USDC pool |
| WETH/USDC | uni_v3 |
3000 (0.3%) | Higher fee, less used |
| WETH/USDC | sushi |
3000 (0.3%) | Sushi has no 0.05% WETH/USDC pool |
| WETH/USDT | uni_v3 |
500 (0.05%) | |
| USDC/USDT | pancake |
100 (0.01%) | Stablecoin pair, tightest fee |
| WBTC/WETH | uni_v3 |
3000 (0.3%) |
You can also check pool depth live on mackinac — the AMM widget shows per-fee-tier liquidity and spread in real time.
If you're unsure which pool has liquidity for a non-standard pair, try get_quote() with each fee tier — it will raise ValueError if the pool doesn't exist.
Step 4 — Quote then swap
Always quote before swapping to compute min_amount_out:
import asyncio
from dexec import AMMClient
async def main():
async with AMMClient(private_key="0x...") as amm:
# Step 1: get a quote (read-only, no gas)
quote = await amm.get_quote("WETH", "USDC", amount_in=0.1,
venue="uni_v3", fee_tier=500)
print(f"Quote: {quote.amount_out:.2f} USDC at ${quote.amount_out/0.1:,.2f}/WETH")
# Step 2: execute with 0.5% slippage tolerance
min_out = quote.amount_out * 0.995
result = await amm.swap("WETH", "USDC", amount_in=0.1,
venue="uni_v3", fee_tier=500,
min_amount_out=min_out)
if result.success:
print(f"Swap tx: {result.tx_hash}")
else:
print(f"Failed: {result.error}")
asyncio.run(main())
Token approval (ERC-20 approve) is handled automatically on the first swap for each token/venue pair.
HLClient reference
async with HLClient(private_key, address, testnet=False) as hl:
# Orders
await hl.place_order(symbol, side, size, price, order_type='gtc', reduce_only=False, cloid=None)
await hl.cancel_order(symbol, cloid)
await hl.cancel_all(symbol=None) # returns count cancelled
# Account
await hl.get_balance() # → AccountBalance
await hl.get_positions() # → list[Position]
await hl.get_open_orders() # → list[Order]
await hl.get_fills() # → list[Fill]
# Streams
async with hl.fills_stream() as fills:
async for fill in fills: ...
async with hl.order_updates_stream() as updates:
async for update in updates: ...
# On-chain
await hl.deposit_usdc(amount_usd, arb_private_key) # → tx_hash
await hl.withdraw_usdc(amount_usd) # → bool
order_type options: 'gtc' (resting limit), 'ioc' (fill or cancel), 'alo' (add liquidity only).
Agent key functions
from dexec import create_agent, approve_agent
# Generate fresh keypair + approve on HL (one-time setup)
creds = await create_agent(master_private_key, agent_name="my-bot", testnet=False)
# → AgentCredentials(address, private_key)
# Approve an existing address as an agent (e.g. key generated externally)
ok = await approve_agent(master_private_key, agent_address, testnet=False)
AMMClient reference
async with AMMClient(private_key=None, rpc_url=None) as amm:
# Wrap ETH → WETH
await amm.wrap_eth(amount_eth) # → tx_hash
# Quote (read-only, no gas)
quote = await amm.get_quote(token_in, token_out, amount_in, venue, fee_tier)
# Swap (auto-approves if needed)
result = await amm.swap(token_in, token_out, amount_in, venue, fee_tier,
min_amount_out, deadline=60)
# Manual approval
await amm.approve_token(token, spender) # → tx_hash
venue options: 'uni_v3', 'sushi', 'pancake'.
Token symbols supported out of the box: WETH, USDC, USDC.e, USDT, WBTC, ARB, LINK, DAI, GMX.
Pass a raw 0x address for any other token (decimals assumed 18 if not in the registry).
Typed return values
@dataclass class AccountBalance:
account_value: float; margin_used: float
free_collateral: float; withdrawable: float
@dataclass class Position:
symbol: str; side: Literal['long','short']; size: float
entry_price: float; unrealized_pnl: float
margin_used: float; leverage: float; liquidation_price: float | None
@dataclass class OrderResult:
success: bool; cloid: str
status: Literal['resting','filled','error']
oid: int | None; error: str | None
@dataclass class SwapQuote:
token_in: str; token_out: str
amount_in: float; amount_out: float
price_impact_pct: float; venue: str; fee_tier: int
@dataclass class SwapResult:
success: bool; tx_hash: str | None
amount_in: float; amount_out: float
venue: str; error: str | None
Examples
| File | What it shows |
|---|---|
examples/01_hl_place_cancel.py |
HL order round-trip on testnet |
examples/02_hl_positions.py |
Poll positions + PnL |
examples/03_hl_fill_stream.py |
Async fill event loop |
examples/04_hl_agent_setup.py |
create_agent() full flow |
examples/05_hl_deposit.py |
USDC deposit to HL |
examples/06_amm_swap.py |
Wrap ETH, quote, and swap on Uniswap V3 |
examples/07_mackinac_feed.py |
mackinac WS signal → HL + AMM basis trade |
Notes
- Arbitrum RPC: defaults to the public
arb1.arbitrum.io/rpcendpoint. For production use Alchemy or Infura — the public endpoint is rate-limited and can drop connections under load. - ERC-20 approvals:
swap()checks allowance and callsapprove(max)automatically on the first swap for each token/router pair. No permit2 in V1. - Single-hop swaps only:
exactInputSinglefor V1 simplicity. Multi-hop routing (e.g. USDT → WETH → ARB) is not yet supported. - Uniswap V4: stubbed, not implemented. V4 uses a materially different router interface.
License
MIT
Metadata
Release files for dex-exec 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| dex_exec-0.1.0.tar.gz | 36.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dex_exec-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.9 kB
Release files / dex_exec-0.1.0.tar.gz
| Download URL | dex_exec-0.1.0.tar.gz |
|---|---|
| Size | 36.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d9e398345c5282893c39bceba565417aea0756d5d4fb60745f93819acec749c0
|
|
BLAKE2b-256 checksum How to use checksums |
035698b19f03f44f39414dcc9289dca30711bde07f42a2b9d9a4bd3efcbe1a36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Release files / dex_exec-0.1.0-py3-none-any.whl
| Download URL | dex_exec-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b36c9935497219fe785f4fb47e13f8eed291602715ad6a28c17e306a2e7b611
|
|
BLAKE2b-256 checksum How to use checksums |
0974df5e6fcdd89fec42593ea9215154f230de41ed00626cda997fec46dfc05f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.9
|
Security for dex-exec 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.