Skip to main content

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

  1. Environment Setup
  2. Hyperliquid SDK Basics
  3. Spot vs Perp - Critical Differences
  4. Order Placement
  5. Order Modification
  6. Reading State
  7. Price & Size Precision
  8. WebSocket Subscriptions
  9. External Price Feeds
  10. Error Handling
  11. MM Loop Structure
  12. Inventory Management
  13. Resilience Patterns
  14. 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

EnvironmentURLConstant
Mainnethttps://api.hyperliquid.xyzconstants.MAINNET_API_URL
Testnethttps://api.hyperliquid-testnet.xyzconstants.TESTNET_API_URL

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)

FormatWhere Used
XMR1Balance queries
XMR1/USDCOrder placement
@260Order 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

PropertyValue
XMR1 Token Index260
XMR1/USDC Pair ID@260
USDC Token Index0
Base AssetXMR1
Quote AssetUSDC
Price Decimals2
Size Decimals2
Min Order Value$10 USDC
Maker Fee0.01% (1 bps)
Taker Fee0.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.

ExchangeTypeEndpoint
Hyperliquid XMR PerpsWebSocket/RESTapp.hyperliquid.xyz/trade/XMR
KrakenWebSocketwss://ws.kraken.com
BinanceWebSocketwss://stream.binance.com:9443/ws/xmrusdt@ticker
KuCoin (fallback)RESThttps://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
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
# 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