XMR1/USDC Trading on Hyperliquid
For AI Coding Assistants (Claude Code, Cursor, etc.)
This guide covers building a market maker bot for Wagyu (XMR1/USDC) using the Hyperliquid API.
Important context: XMR1 is a wrapped Monero token that trades as a spot asset on Hyperliquid DEX.
- Exchange: app.wagyu.xyz - Frontend for Hyperliquid (all pairs) with XMR1/USDC included, also handles XMR deposits/withdrawals
- Bridge: wagyu.xyz/bridge - Standalone bridge interface
Since XMR1 trades on Hyperliquid, all the trading mechanics use Hyperliquid's standard spot API - this guide applies to any Hyperliquid spot pair with minor adjustments.
Written based on real production experience.
Table of Contents
- Environment Setup
- Hyperliquid SDK Basics
- Spot vs Perp - Critical Differences
- Order Placement
- Order Modification
- Reading State
- Price & Size Precision
- WebSocket Subscriptions
- External Price Feeds
- Error Handling
- MM Loop Structure
- Inventory Management
- Resilience Patterns
- Common Pitfalls
1. Environment Setup
Dependencies
pip install hyperliquid-python-sdk
pip install eth-account
pip install websockets
pip install aiohttp
Environment Variables
Create a .env file:
# Hyperliquid wallet private key (with 0x prefix)
HL_PRIVATE_KEY=0x...
Loading Environment
import os
from dotenv import load_dotenv
load_dotenv() # Or use: export $(cat .env | xargs)
private_key = os.environ["HL_PRIVATE_KEY"]
2. Hyperliquid SDK Basics
Core Imports
from hyperliquid.info import Info
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
import eth_account
Authentication
# Create wallet from private key
wallet = eth_account.Account.from_key(os.environ["HL_PRIVATE_KEY"])
# Initialize exchange (for placing orders)
exchange = Exchange(
wallet,
constants.MAINNET_API_URL,
account_address=wallet.address
)
# Initialize info (for reading data)
# skip_ws=True means REST-only, no websocket connection
info = Info(constants.MAINNET_API_URL, skip_ws=True)
# Your address for queries
address = wallet.address
API Endpoints
| Environment | URL | Constant |
|---|---|---|
| Mainnet | https://api.hyperliquid.xyz | constants.MAINNET_API_URL |
| Testnet | https://api.hyperliquid-testnet.xyz | constants.TESTNET_API_URL |
QuickNode RPC (Recommended)
For production use, we recommend using QuickNode instead of the public endpoints. Benefits:
- Higher rate limits - Essential for MM bots making frequent requests
- Lower latency - Dedicated infrastructure
- Better reliability - No shared congestion
- WebSocket support - For real-time data feeds
# Using QuickNode endpoint
QUICKNODE_URL = "https://your-endpoint.hyperliquid-mainnet.quiknode.pro/your-api-key/"
QUICKNODE_WS = "wss://your-endpoint.hyperliquid-mainnet.quiknode.pro/your-api-key/"
# Initialize with QuickNode
exchange = Exchange(
wallet,
QUICKNODE_URL,
account_address=wallet.address
)
info = Info(QUICKNODE_URL, skip_ws=True)
# For WebSocket connections
async with websockets.connect(QUICKNODE_WS) as ws:
# Subscribe to feeds...
Setup: Create an account at quicknode.com, add a Hyperliquid endpoint, and use the provided URL.
3. Spot vs Perp - Critical Differences
This is the #1 source of confusion. Pay close attention.
Perpetuals (Perps)
- Use human-readable coin names:
"BTC","ETH","SOL" - Example:
exchange.order(coin="BTC", ...)
Spot Markets
- Use internal asset IDs with
@prefix:"@260","@0" - The ID comes from
spot_meta()["universe"][i]["index"] - However, for order placement use pair name:
"XMR1/USDC" - For order queries, results may show
"@260"
Asset ID Formats (XMR1 appears in THREE formats)
| Format | Where Used |
|---|---|
XMR1 | Balance queries |
XMR1/USDC | Order placement |
@260 | Order queries, fills |
# Always check all three formats
def is_xmr1(coin: str) -> bool:
return coin in ["XMR1", "XMR1/USDC", "@260"]
Discovering Spot Asset IDs
# Run this once to find your pair's ID
meta = info.spot_meta_and_asset_ctxs()
tokens = meta[0].get('tokens', [])
universe = meta[0].get('universe', [])
for pair in universe:
print(f"{pair.get('name')} = tokens {pair.get('tokens')}")
# Example output:
# @0 = tokens [1, 0] (PURR/USDC)
# @260 = tokens [260, 0] (XMR1/USDC)
XMR1/USDC Specifics
| Property | Value |
|---|---|
| XMR1 Token Index | 260 |
| XMR1/USDC Pair ID | @260 |
| USDC Token Index | 0 |
| Base Asset | XMR1 |
| Quote Asset | USDC |
| Price Decimals | 2 |
| Size Decimals | 2 |
| Min Order Value | $10 USDC |
| Maker Fee | 0.01% (1 bps) |
| Taker Fee | 0.035% (3.5 bps) |
Note: All trades should use the pair ID @260 (or "XMR1/USDC" for order placement). The token index 260 refers to the XMR1 token itself, while @260 is the trading pair combining XMR1 (token 260) with USDC (token 0).
4. Order Placement
Basic Order
result = exchange.order(
coin="XMR1/USDC", # Use pair name for placement
is_buy=True, # True = bid, False = ask
sz=1.5, # Size in BASE asset (XMR1)
limit_px=180.50, # Price in QUOTE asset (USDC)
order_type={"limit": {"tif": "Gtc"}},
reduce_only=False # Always False for spot MM
)
# Result structure:
# {
# "status": "ok",
# "response": {
# "type": "order",
# "data": {
# "statuses": [{"resting": {"oid": 12345}}]
# }
# }
# }
Order Types (Time in Force)
# Good-til-cancelled (default for MM)
{"limit": {"tif": "Gtc"}}
# Immediate-or-cancel (fill what you can, cancel rest)
{"limit": {"tif": "Ioc"}}
# Add-liquidity-only (post-only, RECOMMENDED FOR MM)
{"limit": {"tif": "Alo"}}
Important: Use "Alo" (post-only) for market making to:
- Guarantee maker fees (not taker)
- Prevent accidentally crossing the spread
- Order rejected if it would immediately fill
Placing Multiple Orders (Bulk)
# Batch multiple orders in one call
orders = [
{
"coin": "XMR1/USDC",
"is_buy": True,
"sz": 1.0,
"limit_px": 180.00,
"order_type": {"limit": {"tif": "Alo"}},
"reduce_only": False
},
{
"coin": "XMR1/USDC",
"is_buy": True,
"sz": 1.0,
"limit_px": 179.50,
"order_type": {"limit": {"tif": "Alo"}},
"reduce_only": False
}
]
result = exchange.bulk_orders(orders)
5. Order Modification
Modifying orders is cheaper than cancel+replace and preserves queue priority.
Modify Orders (Bulk API)
# Modify existing orders
result = exchange.bulk_modify_orders_new([
{
"oid": int(old_oid), # Must be integer!
"order": {
"coin": "XMR1/USDC",
"is_buy": True,
"sz": 2.0, # New size
"limit_px": 181.00, # New price
"order_type": {"limit": {"tif": "Alo"}},
"reduce_only": False
}
}
])
Modify Strategy
# Pseudocode for efficient order management
def update_orders(target_price, current_orders):
for order in current_orders:
price_diff_bps = abs(order.price - target_price) / target_price * 10000
if price_diff_bps < THRESHOLD_BPS:
# Price close enough, no action needed
continue
elif price_diff_bps < MAX_MODIFY_DISTANCE_BPS:
# Modify existing order (cheaper, keeps queue priority)
exchange.bulk_modify_orders_new([...])
else:
# Too far, cancel and replace
exchange.cancel(coin, order.oid)
exchange.order(...)
Cancel Order
# Cancel single order - OID MUST BE INTEGER
exchange.cancel(
coin="XMR1/USDC",
oid=int(order_id) # Always cast to int!
)
# Cancel all orders for a coin
orders = info.open_orders(wallet.address)
for o in orders:
if o.get("coin") in ["XMR1", "XMR1/USDC", "@260"]:
exchange.cancel("XMR1/USDC", int(o["oid"]))
6. Reading State
Open Orders
orders = info.open_orders(wallet.address)
# Returns list of ALL orders across ALL assets
# [
# {
# "coin": "@260", # Note: returns @260, not XMR1/USDC
# "side": "A", # "A" = ask/sell, "B" = bid/buy
# "limitPx": "185.50", # String!
# "sz": "1.2000", # String!
# "oid": 12345,
# "timestamp": 1704067200000,
# "origSz": "1.5000"
# },
# ...
# ]
# Filter to your pair (check ALL possible formats)
xmr_orders = [
o for o in orders
if o.get("coin") in ["@260", "XMR1", "XMR1/USDC"]
]
# Separate bids and asks
asks = [o for o in xmr_orders if o["side"] == "A"]
bids = [o for o in xmr_orders if o["side"] == "B"]
Balances
state = info.spot_user_state(wallet.address)
# state["balances"] = [
# {
# "coin": "USDC",
# "total": "50000.00", # Total balance
# "hold": "10000.00" # Locked in open orders
# },
# {
# "coin": "XMR1",
# "total": "45.5000",
# "hold": "10.0000"
# }
# ]
for balance in state.get("balances", []):
coin = balance["coin"]
total = float(balance["total"])
hold = float(balance["hold"])
available = total - hold
print(f"{coin}: {available:.4f} available ({total:.4f} total)")
Recent Fills
fills = info.spot_user_fills(wallet.address)
# Returns recent fills
# [
# {
# "coin": "@260",
# "px": "185.50",
# "sz": "1.0",
# "side": "A",
# "time": 1704067200000, # Milliseconds!
# "fee": "0.185",
# "oid": 12345
# }
# ]
# Convert timestamp
import datetime
fill_time = datetime.datetime.fromtimestamp(int(fill["time"]) / 1000)
Order Book (L2)
# Get order book snapshot
book = info.l2_snapshot("XMR1/USDC")
# book = {
# "levels": [
# [
# [{"px": "185.50", "sz": "10.5", "n": 3}], # Bids
# [{"px": "185.60", "sz": "8.2", "n": 2}] # Asks
# ]
# ]
# }
7. Price & Size Precision
The API will reject orders with incorrect precision.
XMR1/USDC Precision
PRICE_DECIMALS = 2 # e.g., 185.50
SIZE_DECIMALS = 2 # e.g., 1.25
MIN_ORDER_USDC = 10 # Minimum order value
def round_price(price: float) -> float:
return round(price, PRICE_DECIMALS)
def round_size(size: float) -> float:
return round(size, SIZE_DECIMALS)
def validate_order(price: float, size: float) -> bool:
return round_price(price) * round_size(size) >= MIN_ORDER_USDC
# Always round before submitting!
order_price = round_price(calculated_price)
order_size = round_size(calculated_size)
if not validate_order(order_price, order_size):
skip_order()
Getting Precision from API
meta = info.spot_meta_and_asset_ctxs()
tokens = meta[0].get('tokens', [])
for token in tokens:
if token.get('name') == 'XMR1':
print(f"Size decimals: {token.get('szDecimals')}")
8. WebSocket Subscriptions
SDK is Synchronous - Use Thread Pool for Async
import concurrent.futures
import asyncio
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
async def get_balance_async():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
executor,
lambda: info.spot_user_state(wallet.address)
)
async def place_order_async(coin, is_buy, sz, px):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
executor,
lambda: exchange.order(coin, is_buy, sz, px, {"limit": {"tif": "Alo"}}, False)
)
Raw WebSocket for Real-Time Fills
import websockets
import json
import asyncio
async def subscribe_fills():
# Use QuickNode WS for production, public endpoint shown here
uri = "wss://api.hyperliquid.xyz/ws" # Or your QuickNode WS URL
async with websockets.connect(uri) as ws:
# Subscribe to user fills
await ws.send(json.dumps({
"method": "subscribe",
"subscription": {
"type": "userFills",
"user": wallet.address
}
}))
async for msg in ws:
data = json.loads(msg)
if data.get("channel") == "userFills":
for fill in data["data"]:
# fill = {
# "coin": "@260",
# "px": "185.50",
# "sz": "1.0",
# "side": "A", # We were the ask (sold)
# "time": 1704067200000,
# "fee": "0.185",
# "oid": 12345
# }
await process_fill(fill)
Order Book Updates
async def subscribe_orderbook():
# Use QuickNode WS for production
uri = "wss://api.hyperliquid.xyz/ws" # Or your QuickNode WS URL
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({
"method": "subscribe",
"subscription": {
"type": "l2Book",
"coin": "XMR1/USDC"
}
}))
async for msg in ws:
data = json.loads(msg)
if data.get("channel") == "l2Book":
book = data["data"]
# Process book update
9. External Price Feeds
Using multiple price feeds provides better reliability and price discovery than relying on a single source.
Recommended Price Feed Architecture
| Exchange | Type | Endpoint |
|---|---|---|
| Hyperliquid XMR Perps | WebSocket/REST | app.hyperliquid.xyz/trade/XMR |
| Kraken | WebSocket | wss://ws.kraken.com |
| Binance | WebSocket | wss://stream.binance.com:9443/ws/xmrusdt@ticker |
| KuCoin (fallback) | REST | https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=XMR-USDT |
Hyperliquid XMR Perps (Same Exchange)
Since XMR perps trade on the same exchange, this can be a convenient price reference:
def get_hl_xmr_perp_price():
"""Get XMR price from Hyperliquid perps"""
meta = info.meta_and_asset_ctxs()
for asset in meta[1]:
if asset.get("coin") == "XMR":
return float(asset["midPx"])
return None
Kraken WebSocket (Recommended Primary)
import websockets
import json
async def kraken_price_feed(price_callback):
uri = "wss://ws.kraken.com"
while True:
try:
async with websockets.connect(uri) as ws:
# Subscribe to XMR/USD ticker
await ws.send(json.dumps({
"event": "subscribe",
"pair": ["XMR/USD"],
"subscription": {"name": "ticker"}
}))
async for msg in ws:
data = json.loads(msg)
# Ticker updates are arrays, not dicts
if isinstance(data, list) and len(data) >= 2:
ticker = data[1]
if isinstance(ticker, dict) and "c" in ticker:
# "c" = last trade price [price, volume]
price = float(ticker["c"][0])
await price_callback(price, "kraken")
except Exception as e:
print(f"Kraken WS error: {e}")
await asyncio.sleep(5) # Reconnect delay
Binance WebSocket
async def binance_price_feed(price_callback):
uri = "wss://stream.binance.com:9443/ws/xmrusdt@ticker"
while True:
try:
async with websockets.connect(uri) as ws:
async for msg in ws:
data = json.loads(msg)
if "c" in data: # "c" = last price
price = float(data["c"])
await price_callback(price, "binance")
except Exception as e:
print(f"Binance WS error: {e}")
await asyncio.sleep(5)
REST Fallback (KuCoin)
import aiohttp
async def kucoin_price_rest():
url = "https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=XMR-USDT"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.json()
if data.get("code") == "200000":
return float(data["data"]["price"])
return None
Price Aggregation Strategy
class PriceFeed:
def __init__(self):
self.prices = {} # source -> (price, timestamp)
self.staleness_threshold = 30 # seconds
def update(self, price: float, source: str):
self.prices[source] = (price, time.time())
def get_mid_price(self) -> float | None:
now = time.time()
valid_prices = []
# Priority order
for source in ["kraken", "binance", "kucoin"]:
if source in self.prices:
price, ts = self.prices[source]
if now - ts < self.staleness_threshold:
valid_prices.append(price)
if not valid_prices:
return None # All feeds stale - pause quoting
# Use median for robustness against outliers
return sorted(valid_prices)[len(valid_prices) // 2]
10. Error Handling
Common Errors and Responses
async def place_order_safe(coin, is_buy, sz, limit_px):
try:
result = exchange.order(
coin=coin,
is_buy=is_buy,
sz=sz,
limit_px=limit_px,
order_type={"limit": {"tif": "Alo"}},
reduce_only=False
)
return result
except Exception as e:
error = str(e).lower()
if "insufficient balance" in error:
# Not enough funds - reduce size or skip
print(f"Insufficient balance for {sz} @ {limit_px}")
return None
elif "rate limit" in error or "429" in error:
# Rate limited - back off
print("Rate limited, backing off...")
await asyncio.sleep(2)
return None
elif "order would cross" in error:
# Price moved, our "post-only" order would take
# This is normal with ALO orders - just skip
return None
elif "invalid price" in error or "invalid size" in error:
# Precision issue
print(f"Invalid price/size: {limit_px}/{sz}")
return None
elif "order not found" in error:
# Trying to modify/cancel non-existent order
# Order probably already filled
return None
else:
# Unknown error - log and continue
print(f"Order error: {e}")
return None
Recommended Practices
# Hyperliquid rate limits (approximate):
# - 10 requests/second for orders
# - 100 requests/second for info queries
# Best practices:
# 1. Batch orders when possible (bulk_orders)
# 2. Use modify instead of cancel+place
# 3. Don't refresh quotes too frequently (2-5 sec minimum)
# 4. Cache info queries where possible
QUOTE_REFRESH_INTERVAL = 2.0 # seconds
MIN_PRICE_CHANGE_BPS = 10 # Don't refresh if price moved < 10bps
11. MM Loop Structure
Basic Market Making Loop
import asyncio
class MarketMaker:
def __init__(self):
self.running = False
self.price_feed = PriceFeed()
self.current_orders = {} # oid -> order info
async def run(self):
self.running = True
# Start price feeds in background
asyncio.create_task(kraken_price_feed(self.price_feed.update))
asyncio.create_task(binance_price_feed(self.price_feed.update))
# Cancel any stale orders from previous session
await self.cancel_all_orders()
# Main loop
while self.running:
try:
await self.quote_cycle()
await asyncio.sleep(QUOTE_REFRESH_INTERVAL)
except Exception as e:
print(f"Quote cycle error: {e}")
await asyncio.sleep(5)
async def quote_cycle(self):
# 1. Get reference price
mid_price = self.price_feed.get_mid_price()
if mid_price is None:
print("No valid price feed - pausing quotes")
return
# 2. Get current inventory
state = await get_balance_async()
inventory = self.get_inventory(state)
# 3. Calculate quotes with inventory skew
skew = self.calculate_skew(inventory)
ask_price = round_price(mid_price * (1 + SPREAD_BPS/10000 - skew/10000))
bid_price = round_price(mid_price * (1 - SPREAD_BPS/10000 - skew/10000))
# 4. Update orders
await self.update_asks(ask_price, inventory)
await self.update_bids(bid_price, state)
async def cancel_all_orders(self):
"""Clean slate on startup"""
orders = info.open_orders(wallet.address)
for o in orders:
if o.get("coin") in ["@260", "XMR1", "XMR1/USDC"]:
try:
exchange.cancel("XMR1/USDC", int(o["oid"]))
except:
pass
print("Cancelled all existing orders")
Layered Quoting
async def update_asks(self, base_ask_price: float, inventory: float):
"""Place multiple ask layers"""
if inventory <= 0:
return # Nothing to sell
NUM_LAYERS = 10
LAYER_SPACING_BPS = 5
size_per_layer = inventory / NUM_LAYERS
for i in range(NUM_LAYERS):
layer_price = base_ask_price * (1 + i * LAYER_SPACING_BPS / 10000)
layer_price = round_price(layer_price)
layer_size = round_size(size_per_layer)
if validate_order(layer_price, layer_size):
await place_order_async(
coin="XMR1/USDC",
is_buy=False,
sz=layer_size,
limit_px=layer_price
)
async def update_bids(self, base_bid_price: float, state: dict):
"""Place multiple bid layers"""
usdc_available = self.get_usdc_available(state)
if usdc_available < 100: # Minimum to quote
return
NUM_LAYERS = 10
LAYER_SPACING_BPS = 5
usdc_per_layer = usdc_available / NUM_LAYERS
for i in range(NUM_LAYERS):
layer_price = base_bid_price * (1 - i * LAYER_SPACING_BPS / 10000)
layer_price = round_price(layer_price)
layer_size = round_size(usdc_per_layer / layer_price)
if validate_order(layer_price, layer_size):
await place_order_async(
coin="XMR1/USDC",
is_buy=True,
sz=layer_size,
limit_px=layer_price
)
12. Inventory Management
Inventory Skewing
When you accumulate too much of one asset, skew quotes to encourage rebalancing:
def calculate_skew(self, inventory_xmr: float) -> float:
"""
Returns skew in basis points.
Positive skew = we want to SELL (lower asks, raise bids)
Negative skew = we want to BUY (raise asks, lower bids)
"""
TARGET_INVENTORY = 0 # Neutral target
SKEW_FACTOR = 0.5 # bps per XMR of imbalance
MAX_SKEW = 50 # Cap skew at 50bps
imbalance = inventory_xmr - TARGET_INVENTORY
skew = imbalance * SKEW_FACTOR
# Cap the skew
skew = max(-MAX_SKEW, min(MAX_SKEW, skew))
return skew
Asymmetric Spreads
For a token you want to distribute, use tighter asks and wider bids:
# Config
ASK_SPREAD_MULTIPLIER = 0.7 # Aggressive asks
BID_SPREAD_MULTIPLIER = 1.0 # Normal bids
BASE_SPREAD_BPS = 40
# Calculation
ask_spread = BASE_SPREAD_BPS * ASK_SPREAD_MULTIPLIER # 28 bps
bid_spread = BASE_SPREAD_BPS * BID_SPREAD_MULTIPLIER # 40 bps
ask_price = mid * (1 + ask_spread/10000)
bid_price = mid * (1 - bid_spread/10000)
Position Limits
MAX_POSITION_XMR = 100 # Max XMR1 to hold
MAX_POSITION_USDC = 200000 # Max USDC to deploy
def check_position_limits(self, inventory_xmr: float, usdc_available: float):
can_buy = inventory_xmr < MAX_POSITION_XMR
can_sell = inventory_xmr > 0
return can_buy, can_sell
13. Resilience Patterns
Immortal Wrapper (Never-Die Pattern)
import time
import sys
def main():
"""Wrapper that restarts the bot on any crash"""
restart_count = 0
last_restart = 0
while True:
try:
print(f"\n{'='*50}")
print(f"Starting MM (restart #{restart_count})")
print(f"{'='*50}\n")
asyncio.run(MarketMaker().run())
except KeyboardInterrupt:
print("\nGraceful shutdown requested")
sys.exit(0)
except SystemExit:
raise
except Exception as e:
restart_count += 1
now = time.time()
# Exponential backoff if crashing too fast
if now - last_restart < 60:
delay = min(300, 5 * (2 ** min(restart_count, 6)))
print(f"Crash #{restart_count}: {e}")
print(f"Restarting too fast, waiting {delay}s...")
time.sleep(delay)
else:
print(f"Crash #{restart_count}: {e}")
print("Restarting in 5s...")
time.sleep(5)
last_restart = now
if __name__ == "__main__":
main()
Clean Slate on Startup
Always cancel existing orders when starting - they may be at stale prices:
async def startup_cleanup(self):
"""Cancel all orders from previous session"""
print("Cleaning up stale orders...")
orders = info.open_orders(wallet.address)
cancelled = 0
for order in orders:
if order.get("coin") in ["@260", "XMR1", "XMR1/USDC"]:
try:
exchange.cancel("XMR1/USDC", int(order["oid"]))
cancelled += 1
except Exception as e:
print(f"Failed to cancel {order['oid']}: {e}")
print(f"Cancelled {cancelled} stale orders")
await asyncio.sleep(1) # Let cancels process
Deadlock Detection
class MarketMaker:
def __init__(self):
self.last_loop_time = time.time()
self.LOOP_TIMEOUT = 300 # 5 minutes
async def run(self):
# Start watchdog
asyncio.create_task(self.watchdog())
while self.running:
self.last_loop_time = time.time()
await self.quote_cycle()
await asyncio.sleep(2)
async def watchdog(self):
"""Force crash if main loop stalls"""
while self.running:
await asyncio.sleep(60)
if time.time() - self.last_loop_time > self.LOOP_TIMEOUT:
print("WATCHDOG: Main loop stalled, forcing crash!")
raise Exception("Deadlock detected")
Database with WAL Mode (for Logging)
import sqlite3
def init_database():
conn = sqlite3.connect("mm_trades.db", timeout=30)
# Enable WAL mode for concurrent access
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA busy_timeout=30000")
conn.execute("""
CREATE TABLE IF NOT EXISTS fills (
id INTEGER PRIMARY KEY,
timestamp INTEGER,
side TEXT,
price REAL,
size REAL,
fee REAL,
oid INTEGER
)
""")
conn.commit()
return conn
14. Common Pitfalls
1. Wrong Asset Format for Order Placement vs Queries
# Order PLACEMENT - use pair name
exchange.order(coin="XMR1/USDC", ...)
# Order QUERIES return @ID format
orders = info.open_orders(addr)
# orders[0]["coin"] == "@260" # NOT "XMR1/USDC"!
2. Not Rounding Prices/Sizes
# WRONG - API will reject
exchange.order(sz=1.23456789, limit_px=185.123456)
# CORRECT - Round to allowed precision
exchange.order(sz=round(1.23456789, 2), limit_px=round(185.123456, 2))
3. Order ID Must Be Integer
# WRONG - OID as string
exchange.cancel("XMR1/USDC", "12345")
# CORRECT - Always cast to int
exchange.cancel("XMR1/USDC", int(oid))
4. Filtering Orders Incorrectly
# WRONG - misses some formats
xmr_orders = [o for o in orders if o["coin"] == "XMR1"]
# CORRECT - check all formats
xmr_orders = [o for o in orders if o["coin"] in ["@260", "XMR1", "XMR1/USDC"]]
5. Not Using Post-Only Orders
# RISKY - Might take liquidity (taker fees + adverse selection)
order_type={"limit": {"tif": "Gtc"}}
# SAFER - Guarantees maker, rejects if would cross
order_type={"limit": {"tif": "Alo"}}
6. Quoting Without Valid Price Feed
# WRONG - Quote anyway with stale price
if price is None:
price = last_known_price # Might be stale!
# CORRECT - Stop quoting
if price is None:
print("No valid price feed, pausing quotes")
return
7. Timestamps in Milliseconds
# Hyperliquid timestamps are in milliseconds
fill_time_seconds = int(fill["time"]) / 1000
fill_datetime = datetime.fromtimestamp(fill_time_seconds)
8. Balance Calculation for Modify vs Cancel+Replace
# For cancel/replace: use FREE balance (total - hold)
available = float(balance["total"]) - float(balance["hold"])
# For modify-in-place: can use TOTAL (funds release when order modified)
available_for_modify = float(balance["total"])
Quick Reference Card
┌─────────────────────────────────────────────────────────────┐
│ HYPERLIQUID SPOT MM CHEAT SHEET │
├─────────────────────────────────────────────────────────────┤
│ Order Placement: "XMR1/USDC" │
│ Order Queries: "@260" (or "XMR1" or "XMR1/USDC") │
│ Balance Queries: "XMR1" │
├─────────────────────────────────────────────────────────────┤
│ Price Decimals: 2 │
│ Size Decimals: 2 │
│ Min Order Value: $10 USDC │
│ Maker Fee: 0.01% (1 bps) │
│ Taker Fee: 0.035% (3.5 bps) │
├─────────────────────────────────────────────────────────────┤
│ Order Side: "A" = Ask/Sell, "B" = Bid/Buy │
│ Order Type: {"limit": {"tif": "Alo"}} for MM │
├─────────────────────────────────────────────────────────────┤
│ Mainnet: constants.MAINNET_API_URL │
│ Testnet: constants.TESTNET_API_URL │
│ QuickNode: https://quicknode.com/docs/hyperliquid │
│ WebSocket: wss://api.hyperliquid.xyz/ws │
├─────────────────────────────────────────────────────────────┤
│ Key Functions: │
│ exchange.order() Place order │
│ exchange.bulk_orders() Place multiple orders │
│ exchange.bulk_modify_orders_new() Modify (cheaper!) │
│ exchange.cancel() Cancel order (int OID!) │
│ info.open_orders() Get all open orders │
│ info.spot_user_state() Get balances │
│ info.spot_user_fills() Get recent fills │
│ info.spot_meta_and_asset_ctxs() Get asset metadata │
├─────────────────────────────────────────────────────────────┤
│ Golden Rules: │
│ 1. Use "XMR1/USDC" for orders, check "@260" in queries │
│ 2. Use ALO (post-only) orders │
│ 3. Round prices/sizes to correct decimals │
│ 4. OID must be int for cancel/modify │
│ 5. Cancel all orders on startup │
│ 6. Handle all errors gracefully │
│ 7. Timestamps are in milliseconds │
└─────────────────────────────────────────────────────────────┘
Example: Minimal Working MM
#!/usr/bin/env python3
"""Minimal XMR1/USDC Market Maker for Hyperliquid"""
import os
import asyncio
import aiohttp
from hyperliquid.info import Info
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
import eth_account
# Config
SPREAD_BPS = 50
REFRESH_INTERVAL = 5
# Setup
wallet = eth_account.Account.from_key(os.environ["HL_PRIVATE_KEY"])
exchange = Exchange(wallet, constants.MAINNET_API_URL, account_address=wallet.address)
info = Info(constants.MAINNET_API_URL, skip_ws=True)
def round_price(p): return round(p, 2)
def round_size(s): return round(s, 2)
async def get_kraken_price():
"""Get XMR price from Kraken"""
async with aiohttp.ClientSession() as s:
async with s.get("https://api.kraken.com/0/public/Ticker?pair=XMRUSD") as r:
data = await r.json()
return float(data["result"]["XXMRZUSD"]["c"][0])
async def mm_loop():
# Cancel existing orders on startup
for o in info.open_orders(wallet.address):
if o.get("coin") in ["@260", "XMR1", "XMR1/USDC"]:
exchange.cancel("XMR1/USDC", int(o["oid"]))
while True:
try:
# Get price from external feed
mid = await get_kraken_price()
# Calculate quotes
ask = round_price(mid * (1 + SPREAD_BPS/10000))
bid = round_price(mid * (1 - SPREAD_BPS/10000))
# Get inventory
state = info.spot_user_state(wallet.address)
xmr_bal = next(
(float(b["total"]) - float(b["hold"])
for b in state.get("balances", []) if b["coin"] == "XMR1"),
0
)
usdc_bal = next(
(float(b["total"]) - float(b["hold"])
for b in state.get("balances", []) if b["coin"] == "USDC"),
0
)
# Place ask if we have XMR1
if xmr_bal >= 0.1:
size = round_size(min(xmr_bal * 0.1, 1.0))
if size * ask >= 10: # Min order value
exchange.order("XMR1/USDC", False, size, ask,
{"limit": {"tif": "Alo"}}, False)
# Place bid if we have USDC
if usdc_bal >= 100:
size = round_size(min(100, usdc_bal * 0.1) / bid)
if size * bid >= 10:
exchange.order("XMR1/USDC", True, size, bid,
{"limit": {"tif": "Alo"}}, False)
print(f"Quoted: {bid} / {ask} (mid: {mid:.2f})")
await asyncio.sleep(REFRESH_INTERVAL)
except Exception as e:
print(f"Error: {e}")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(mm_loop())
Document last updated: 2026-01-20 Based on production XMR1/USDC market maker running on Hyperliquid