Open-source paper trading backend for HyperLiquid. Swap api.hyperliquid.xyz for your HyPaper URL in your existing HL bot and it just works — same request/response shapes, same WebSocket protocol, no wallet signing required.
HL WebSocket Feed ──> Worker Process ──> Redis (all hot state)
- price updates |
- order matching | reads
- fill execution v
Hono API Server
POST /exchange
POST /info
POST /hypaper
|
EventBus <──────────────+
|
v
WebSocket Server (/ws)
- allMids, l2Book
- orderUpdates, userFills
- Worker streams live market data from HyperLiquid via WebSocket and fills paper orders on every price tick
- Redis holds all state: prices, positions, orders, fills, balances
- API mirrors HL's endpoints so existing bots need minimal code changes
- WebSocket pushes real-time updates to connected clients using HL's subscribe/unsubscribe protocol
git clone https://github.com/GigabrainGG/HyPaper.git
cd hypaper-backend
npm install
docker compose up -d # starts Redis
npm run dev # starts server with hot reloadServer runs on http://localhost:3000. WebSocket at ws://localhost:3000/ws.
Copy .env.example to .env and edit as needed:
DATABASE_URL=postgresql://localhost:5432/hypaper
REDIS_URL=redis://localhost:6379
HL_WS_URL=wss://api.hyperliquid.xyz/ws
HL_API_URL=https://api.hyperliquid.xyz
PORT=3000
DEFAULT_BALANCE=100000
LOG_LEVEL=info
FEES_ENABLED=true
FEE_RATE_TAKER=0.00035
FEE_RATE_MAKER=0.0001
FUNDING_ENABLED=true
FUNDING_INTERVAL_MS=28800000Bring your own Redis — any Redis 7+ works (local, Docker, Upstash, Redis Cloud, etc.).
Once the backend is running and connected to Redis/Postgres, you can exercise the live HTTP + WebSocket surface with a paper wallet:
npm run test:realtimeUseful options:
npm run test:realtime -- --base-url http://localhost:3000 --wallet 0xpaperbot --coin BTCThe script:
- checks
GET /health - tests supported
POST /hypaperactions - tests supported
POST /exchangeactions (order,cancel,cancelByCloid,updateLeverage) - verifies
POST /infolocal paper-state queries and proxied live-market queries - connects to
WebSocket /wsand verifiesallMids,l2Book,orderUpdates, anduserFills - uses only paper balances and resets the test account at the end by default
HyPaper has no authentication. This mirrors HL's public info API and simplifies integration.
/infois fully public. User-specific queries passuser(wallet address) in the request body, just like HL./exchangerequires awalletfield in the request body to identify the user. Accounts are auto-created on first use with the configured default balance./hypaperusesuserin the request body.
Any string works as a wallet address — use your real 0x... address, a test address, or any identifier you like.
Health check.
Mirrors HL's info endpoint. Send {"type": "..."} in the body.
Served from Redis (paper state):
| type | body | description |
|---|---|---|
allMids |
— | Mid prices for all assets |
clearinghouseState |
{"user": "0x..."} |
Positions, margin summary, account value |
openOrders |
{"user": "0x..."} |
Open orders |
frontendOpenOrders |
{"user": "0x..."} |
Open orders with extra fields (tif, trigger, etc.) |
userFills |
{"user": "0x..."} |
Recent fills |
userFillsByTime |
{"user": "0x...", "startTime": ..., "endTime": ...} |
Fills filtered by time |
orderStatus |
{"oid": 123} |
Status of a specific order |
activeAssetCtx |
{"coin": "BTC"} |
Asset context (funding, OI, mark price) |
Proxied to real HL (live market data):
| type | description |
|---|---|
meta |
Universe metadata (asset names, decimals, max leverage) |
metaAndAssetCtxs |
Meta + all asset contexts |
candleSnapshot |
OHLCV candles |
fundingHistory |
Historical funding rates |
l2Book |
L2 order book |
Any unrecognized type is also proxied to HL.
Mirrors HL's exchange endpoint. Send {"wallet": "0x...", "action": {...}} in the body.
| action.type | description |
|---|---|
order |
Place orders (limit, IOC, ALO, trigger/TP-SL) |
cancel |
Cancel orders by asset + oid |
cancelByCloid |
Cancel orders by client order ID |
updateLeverage |
Set leverage + cross/isolated for an asset |
Example — place a limit buy:
curl -s http://localhost:3000/exchange \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0xYourAddress",
"action": {
"type": "order",
"grouping": "na",
"orders": [{
"a": 0, "b": true, "p": "90000", "s": "0.01",
"r": false, "t": {"limit": {"tif": "Gtc"}}
}]
}
}'Example — cancel an order:
curl -s http://localhost:3000/exchange \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0xYourAddress",
"action": {"type": "cancel", "cancels": [{"a": 0, "o": 1}]}
}'Paper-trading-specific endpoints (not part of HL's API).
| type | body | description |
|---|---|---|
resetAccount |
{"user": "0x..."} |
Wipe all positions, orders, fills. Reset balance. |
setBalance |
{"user": "0x...", "balance": 500000} |
Set account balance |
getAccountInfo |
{"user": "0x..."} |
Get userId, balance, creation time |
Mirrors HL's WebSocket API. Connect to ws://localhost:3000/ws.
Subscribe to channels:
{"method": "subscribe", "subscription": {"type": "allMids"}}
{"method": "subscribe", "subscription": {"type": "l2Book", "coin": "BTC"}}
{"method": "subscribe", "subscription": {"type": "orderUpdates", "user": "0x..."}}
{"method": "subscribe", "subscription": {"type": "userFills", "user": "0x..."}}Unsubscribe:
{"method": "unsubscribe", "subscription": {"type": "allMids"}}Server pushes:
{"channel": "allMids", "data": {"mids": {"BTC": "42500", ...}}}
{"channel": "l2Book", "data": {"coin": "BTC", "levels": [...], "time": ...}}
{"channel": "orderUpdates", "data": [{"order": {...}, "status": "filled", ...}]}
{"channel": "userFills", "data": {"isSnapshot": false, "user": "0x...", "fills": [...]}}All channels are open — no authentication required.
Point your bot's base URL at HyPaper and pass the wallet address:
# Before (real HL — uses wallet signing)
exchange = HyperliquidExchange(base_url="https://api.hyperliquid.xyz")
# After (HyPaper — pass wallet in body)
exchange = HyperliquidExchange(base_url="http://localhost:3000")If your SDK sends nonce/signature/vaultAddress, that's fine — HyPaper ignores them. You only need to add a wallet field to /exchange requests.
Orders are matched against live mid prices from HyperLiquid on every WebSocket tick:
- Limit buy fills when
midPx <= limitPx - Limit sell fills when
midPx >= limitPx - Stop loss (sell/close long) triggers when
midPx <= triggerPx - Stop loss (buy/close short) triggers when
midPx >= triggerPx - Take profit (sell/close long) triggers when
midPx >= triggerPx - Take profit (buy/close short) triggers when
midPx <= triggerPx
Time-in-force behavior:
- GTC — fills immediately if price crosses, otherwise rests on book
- IOC — fills immediately or rejects
- ALO — rejects if it would fill immediately (post-only)
Maker/Taker fees are enabled by default and match HyperLiquid's fee schedule:
- Taker (IOC, GTC immediate fill, triggers): 3.5 bps (
0.00035) - Maker (rested limit orders filled by price movement): 1 bp (
0.0001) crossed: truein fills indicates a taker fill,falsefor maker- Disable with
FEES_ENABLED=false
Funding rates are applied every 8 hours (matching HL's schedule):
- Funding rates are sourced from live HyperLiquid market data
- Longs pay when the funding rate is positive, shorts receive
- Tracked per-position via
cumFunding,cumFundingSinceOpen,cumFundingSinceChange - Disable with
FUNDING_ENABLED=false
docker compose --profile prod up -dThe Dockerfile produces a production image. Provide REDIS_URL and you're set:
docker build -t hypaper .
docker run -p 3000:3000 -e REDIS_URL=redis://your-redis:6379 hypaper| Platform | Notes |
|---|---|
| Railway | Managed Redis add-on, WebSocket support, auto-detects Dockerfile |
| Fly.io | Native WS support, Upstash Redis add-on |
| VPS (Hetzner, DO) | docker compose --profile prod up -d, full control |
WebSocket support is required — platforms like Vercel/Cloudflare Workers won't work.
src/
├── api/
│ ├── middleware/auth.ts # Auto-create accounts on first use
│ ├── middleware/rate-limit.ts
│ ├── routes/exchange.ts # POST /exchange
│ ├── routes/info.ts # POST /info
│ ├── routes/hypaper.ts # POST /hypaper
│ └── server.ts # Hono app setup
├── engine/
│ ├── fill.ts # Fill history queries
│ ├── margin.ts # Margin checks + PnL calculations
│ ├── order.ts # Order validation + placement
│ └── position.ts # Clearinghouse state builder
├── store/
│ ├── keys.ts # Redis key schema
│ └── redis.ts # Redis connection
├── types/
│ ├── hl.ts # HyperLiquid API type mirrors
│ ├── order.ts # Internal order types
│ └── position.ts # Internal position types
├── utils/
│ ├── id.ts # Sequence ID generation
│ ├── logger.ts # Pino logger
│ └── math.ts # decimal.js wrappers
├── worker/
│ ├── index.ts # Worker startup, eventBus, market data seeding
│ ├── funding-worker.ts # Periodic funding fee application
│ ├── order-matcher.ts # Core matching engine
│ ├── price-updater.ts # WS message → Redis + eventBus
│ └── ws-client.ts # HL WebSocket with reconnect
├── ws/
│ ├── server.ts # Outbound WebSocket server (/ws)
│ └── types.ts # WS message + event bus types
├── config.ts # Zod-validated env config
└── index.ts # Entry point
- Runtime: Node.js + TypeScript
- HTTP: Hono + @hono/node-server
- WebSocket: ws
- State: Redis via ioredis
- Math: decimal.js (no floating point)
- Validation: zod
- Logging: pino
Contributions welcome. Please open an issue first for large changes.
MIT