Reference
Running and deploying
Requirements
- Node.js ≥ 22 and pnpm (API)
- Foundry (contracts;
forge buildis also needed by the API for the TokenProbe bytecode) - An Elysium RPC. The public one works; a private one with WebSocket is better in production
- Optional: Redis
Configuration
Network file
Everything chain-specific lives in api/networks/<name>.json, selected with NETWORK (default elysium-testnet). The API refuses to start on an unfilled file.
| Key | Meaning |
|---|---|
name, chainId | Display name and chain id (preflight checks it against the RPC) |
rpcUrl | Public RPC handed to wallets by /config (the server's own is RPC_URL) |
explorer, explorerApi | Block explorer and its Etherscan-compatible API (contract verification) |
multicall3, permit2 | Canonical addresses (checked by preflight) |
usdc | Quote currency for /price |
v3Factories[] | { address, wrappedNative }: each V3 factory with the WHYPE its pools use |
v2Factories[] | { address, feeBps } |
v4 | { managers[], nativeWrapper?, lookbackBlocks } |
solidlyFactories[], slipstreamFactories[], algebraFactories[] | Optional: Solidly/Velodrome v2, Slipstream and Algebra factories to pin (they're also auto-discovered) |
launchpad | { factory, router } or absent |
verifiedTokens[] | Curated list: { address, symbol, name, decimals, logoURI, tags } |
positionManagers | Optional: { v3: [{ address, factory }], v4: [{ address, poolManager }] } (otherwise discovered) |
venueLabels | Optional: { "<factory, PoolManager or hook address>": "Kleos" }, names shown on the Pools page |
The allow-listed WHYPEs are the distinct wrappedNative values of the V3 factories.
Environment
Locally, pnpm start, pnpm e2e and pnpm keys:limit read .env at the project root (or the file in $LANE_ENV_FILE). It is git-ignored, never committed. On Railway, the same names are service variables.
| Variable | Default | Meaning |
|---|---|---|
NETWORK | elysium-testnet | Network file |
RPC_URL | network rpcUrl | Server-side RPC (may contain a key; never exposed). Several, comma-separated: requests fail over in order |
WS_URL | — | WebSocket RPC: new blocks trigger a sync immediately |
POLL_MS | 1500 | Polling interval without WebSocket |
LOGS_MAX_RANGE | 0 (no cap) | Max block span per eth_getLogs (10000 for Elysium community RPCs) |
V4_LOOKBACK_BLOCKS | network file | v4 pools indexed if active within this window |
SNAPSHOT_PATH | data/snapshot.json | Snapshot file |
REDIS_URL | — | Shared snapshot and rate-limit counters |
PORT | 3000 | HTTP port |
CORS_ORIGINS | * | Comma-separated allowed origins |
RATE_LIMIT_PER_MIN | 120 | Public tier, per IP |
API_KEYS | — | Partner keys, key:perMinute,… (Access) |
EXECUTOR_ADDRESS | — | Required for /quote, /history |
ORDERBOOK_ADDRESS | — | Enables orders |
FEE_COLLECTOR_ADDRESS | — | Enables the fee converter |
KEEPER_PRIVATE_KEY | — | Keeper wallet (fills orders, converts fees). Needs HYPE for gas; must be allow-listed in the OrderBook and FeeCollector |
KEEPER_POLL_MS | 5000 | Keeper loop |
FEE_CONVERT_MIN_USD | 5 | Minimum balance worth converting |
EXECUTOR_FROM_BLOCK, ORDERBOOK_FROM_BLOCK | 0 | Deployment blocks: speeds up history and order indexing |
EVENTS_DB_PATH | data/events.db | Event store (SQLite) |
EVENTS_BACKFILL_HOURS | 168 | One-time backfill window on first start |
EVENTS_RETENTION_DAYS | 90 | Events, prices, TVL history kept |
TRUST_PROXY | 0 | Proxy hops to trust for the client IP (1 on Railway), so per-IP limits see users, not the proxy |
APP_URL | / | Where share pages send people |
PUBLIC_URL | from the request | Absolute base for share-card image URLs (e.g. https://api.lane.markets) |
EVENTS_BACKUP_HOURS, EVENTS_BACKUP_KEEP | 6, 4 | Event-store backups in data/backups/ (0 = off) |
LP_DISCOVERY_BLOCKS | 2,000,000 | Window scanned once for position managers (then incremental) |
V4_SCAN_MAX | 200,000 | Newest v4 position ids scanned for ownership |
INDEXER_BACKGROUND | on | off on local forks/tests: skips background loading |
ALERT_WEBHOOK_URL | — | Discord or Slack incoming webhook for alerts |
ALERT_LAG_BLOCKS | 200 | Alert when the indexer is this far behind the chain |
ALERT_SYNC_AGE_S | 120 | Alert when no sync succeeded for this long |
ALERT_KEEPER_MIN_HYPE | 0.05 | Alert when the keeper's balance drops below this |
Tests only: ELYSIUM_RPC_URL (testnet fork tests), ELYSIUM_MAINNET_RPC_URL and MAINNET_FORK_BLOCK (mainnet launchpad fork tests), FORK_BLOCK (testnet pin).
Run locally
cd contracts && forge build && cd ../api
pnpm install
cp .env.example .env # fill EXECUTOR_ADDRESS etc. if deployed
pnpm start # first run: backfill; next runs: snapshot + replay. API at http://localhost:3000/v1Without a deployment
pnpm dev:proxy starts a JSON-RPC proxy that injects the compiled Executor at 0x…0e0e0e into every eth_call/eth_estimateGas with a state override. Quotes and simulations then run against live state with no contract deployed:
pnpm dev:proxy # :8546
RPC_URL=http://127.0.0.1:8546 EXECUTOR_ADDRESS=0x00000000000000000000000000000000000e0e0e pnpm startDeploy
cd contracts
WRAPPED_NATIVES=0x757a…,0xCD57… KEEPER=0xKeeper TREASURY=0xTreasury \
forge script script/Deploy.s.sol --rpc-url elysium --broadcast --private-key $DEPLOYER_KEY
RPC_URL=… VERIFIER_URL=<explorerApi> script/verify.sh # verify on the explorerThis deploys the Executor (fee 5 bps), the OrderBook (keeper allow-listed) and the FeeCollector (set as the Executor's fee recipient). RPC aliases elysium and elysium_mainnet are in foundry.toml.
Preflight
NETWORK=elysium-testnet pnpm preflight
NETWORK=elysium-testnet EXECUTOR_ADDRESS=… ORDERBOOK_ADDRESS=… FEE_COLLECTOR_ADDRESS=… pnpm preflightOne PASS/FAIL line per check, exit code 1 on any failure. It checks the chain id, Multicall3/Permit2 code, USDC and every WHYPE (deposit works), each factory's interface, and, with addresses given, the deployment: allow-listed WHYPEs, fee ≤ cap, OrderBook and FeeCollector pointing to the Executor, fees going to the FeeCollector.
End-to-end test
cd api && pnpm e2e # PRIVATE_KEY in .env, ≥ 0.03 HYPEDeploys (or reuses) the contracts, verifies them, runs the preflight, then real swaps (V3, V2, V4 with hook, split, Permit2, native round trips, launchpad), a limit order and a 2-slice DCA filled by the keeper, a cancel with refund, and an escrow check. Each step checks received ≥ minOut and that the Executor ends empty. Report: api/data/e2e-report-<chain>.json.
Dry run on a fork: RPC_URL=http://127.0.0.1:8545 PRIVATE_KEY=<anvil key> E2E_SKIP_VERIFY=1 pnpm e2e against anvil --fork-url ….
Mainnet launch runbook
- Discover:
NETWORK=elysium-mainnet RPC_URL=<rpc> LOGS_MAX_RANGE=10000 pnpm discoverwritesnetworks/elysium-mainnet.draft.json(factories with their WHYPE, v4 PoolManagers, USDC candidates, launchpads) with a_reviewlist of uncertain items. - Review against Kinetiq's published addresses (impostor tokens exist), then rename to
elysium-mainnet.json. - Preflight the network file.
- Deploy and verify.
- Preflight the deployment.
- Start:
NETWORK=elysium-mainnet EXECUTOR_ADDRESS=… ORDERBOOK_ADDRESS=… FEE_COLLECTOR_ADDRESS=… KEEPER_PRIVATE_KEY=… WS_URL=… REDIS_URL=… pnpm start.
Railway
The repo ships a Dockerfile and railway.json (healthcheck on /v1/health). From the repo root:
railway init -n lane
railway add -s api -v NETWORK=elysium-testnet
railway volume add -m /app/api/data # keeps the snapshot across deploys
railway up --ci -s api
railway domain -s apiSet TRUST_PROXY=1 (Railway's edge is one proxy hop). Custom domain: railway domain api.lane.markets -s api prints the DNS record to create. lane.markets is on Vercel DNS: add CNAME api → vbrhnshg.up.railway.app there (Vercel dashboard → Domains → lane.markets → DNS records, or vercel dns add lane.markets api CNAME vbrhnshg.up.railway.app). TLS is automatic once it resolves. PUBLIC_URL=https://api.lane.markets and APP_URL=https://lane.markets are set on the service.
Operator keys: pnpm keys:limit <prefix> <perMinute> (run where the event store lives, e.g. railway run) raises a self-serve key.
Set secrets (EXECUTOR_ADDRESS, ORDERBOOK_ADDRESS, FEE_COLLECTOR_ADDRESS, KEEPER_PRIVATE_KEY, API_KEYS, a private RPC_URL/WS_URL) in the service's variables, never in the repo. Keep one replica if the keeper runs there.
RPC requirements
Measured on Nov 6 against Elysium mainnet state:
| RPC | eth_getLogs cap | Note |
|---|---|---|
| rpc.agoradex.app | 10,000 blocks | Went down under load during the rehearsal (502, then "Application failed to respond") |
| rpc-live.boxa.club | 10,000 blocks | |
| rpc.eleos.fun | 2,000 blocks | |
| rpc.elixirpad.fun | 1,000 blocks | |
| rpc.thekleos.pro | 1,000 blocks for address-less queries | Factory discovery is address-less |
The indexer adapts to any cap it is told about ("at most N blocks"), splits on "internal error", and fails over between the endpoints listed in RPC_URL. But a cold start on mainnet sends thousands of eth_getLogs and multicalls, and every quote makes 4–6 RPC calls: use a private RPC with WebSocket in production (and list a community one after it as a fallback).
Mainnet rehearsal (Nov 6)
The whole stack ran against mainnet state with NETWORK=elysium-mainnet-rehearsal (a rehearsal-only file: pre-launch draft, unconfirmed WHYPE/USDC, launchpad labels), quotes through the dev proxy.
| Measure | Result |
|---|---|
| Ready (V2/V3 loaded) | 16 s cold, 9 s from snapshot |
| v4 discovery | ~5,900 pools over 14 PoolManagers in ~10 min on community RPCs |
| Memory | 200–615 MB RSS: give the service ≥ 1 GB |
| Quotes | HYPE→USDC, HYPE→Kleos token, HYPE→Ascend token: routed and simulated OK (2–3.6 s through two community RPCs) |
| Load (testnet, public RPC) | 1 user: p50 1.05 s; ~2.4 quotes/s max, errors from ~5 concurrent users: RPC-bound |
Fixed during the rehearsal: launchpad pools (zero liquidity at the edge of their one-sided position) were treated as dead and had no route; RPC caps worded differently were not recognised; v4 key lookup made ~6,500 requests; v4 discovery restarted from zero after any failure; v4 Initialize logs crashed the event store.
To rerun it: NETWORK=elysium-mainnet-rehearsal RPC_URL=<rpc1>,<rpc2> LOGS_MAX_RANGE=10000 pnpm start (add the dev proxy for quotes, see Without a deployment).
Monitoring and backups
GET /v1/healthalways answers 200 (deploy healthchecks) withfresh,reasons,lagBlocks,lastSyncSecondsAgoand the keeper's balance.GET /v1/health?strict=1answers 503 unless ready and fresh: point an uptime monitor at it.ALERT_WEBHOOK_URL: a message when the indexer lags or stops syncing, or when the keeper runs low, and another when it's resolved.- Event store backups: a consistent copy in
data/backups/every 6 h (4 kept), made in a worker thread. They live on the same volume: also enable Railway's volume backups for off-site copies. Restore: stop the service, copy a backup overdata/events.db, start it. - Size: testnet bots make ~800k swaps a day, so set
EVENTS_RETENTION_DAYS=14there. Mainnet (~17k swaps a day at the rehearsal) fits the 90-day default.
Production checklist
- Private RPC with WebSocket (
RPC_URL,WS_URL), a community RPC listed after it as fallback;LOGS_MAX_RANGEset if needed - Service memory ≥ 1 GB;
TRUST_PROXY=1on Railway -
ALERT_WEBHOOK_URLset; an uptime monitor on/v1/health?strict=1 - Railway volume backups enabled (event store and snapshot)
-
REDIS_URLwhen running more than one instance - Keeper on one instance only, its key funded with HYPE and allow-listed
-
CORS_ORIGINSrestricted to your front-ends if the API isn't meant to be public -
API_KEYSfor partners;RATE_LIMIT_PER_MINfor the public - TLS in front (reverse proxy); WebSocket upgrade allowed on
/v1/ws/prices -
EXECUTOR_FROM_BLOCK/ORDERBOOK_FROM_BLOCKset to the deployment blocks - Owner key stored offline; know how to call
setPaused(true)