Skip to content

API ReferenceRunning and deploying

Reference

Running and deploying

Requirements

  • Node.js ≥ 22 and pnpm (API)
  • Foundry (contracts; forge build is 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.

KeyMeaning
name, chainIdDisplay name and chain id (preflight checks it against the RPC)
rpcUrlPublic RPC handed to wallets by /config (the server's own is RPC_URL)
explorer, explorerApiBlock explorer and its Etherscan-compatible API (contract verification)
multicall3, permit2Canonical addresses (checked by preflight)
usdcQuote 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 }
positionManagersOptional: { v3: [{ address, factory }], v4: [{ address, poolManager }] } (otherwise discovered)
venueLabelsOptional: { "<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.

VariableDefaultMeaning
NETWORKelysium-testnetNetwork file
RPC_URLnetwork rpcUrlServer-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_MS1500Polling interval without WebSocket
LOGS_MAX_RANGE0 (no cap)Max block span per eth_getLogs (10000 for Elysium community RPCs)
V4_LOOKBACK_BLOCKSnetwork filev4 pools indexed if active within this window
SNAPSHOT_PATHdata/snapshot.jsonSnapshot file
REDIS_URL—Shared snapshot and rate-limit counters
PORT3000HTTP port
CORS_ORIGINS*Comma-separated allowed origins
RATE_LIMIT_PER_MIN120Public 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_MS5000Keeper loop
FEE_CONVERT_MIN_USD5Minimum balance worth converting
EXECUTOR_FROM_BLOCK, ORDERBOOK_FROM_BLOCK0Deployment blocks: speeds up history and order indexing
EVENTS_DB_PATHdata/events.dbEvent store (SQLite)
EVENTS_BACKFILL_HOURS168One-time backfill window on first start
EVENTS_RETENTION_DAYS90Events, prices, TVL history kept
TRUST_PROXY0Proxy 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_URLfrom the requestAbsolute base for share-card image URLs (e.g. https://api.lane.markets)
EVENTS_BACKUP_HOURS, EVENTS_BACKUP_KEEP6, 4Event-store backups in data/backups/ (0 = off)
LP_DISCOVERY_BLOCKS2,000,000Window scanned once for position managers (then incremental)
V4_SCAN_MAX200,000Newest v4 position ids scanned for ownership
INDEXER_BACKGROUNDonoff on local forks/tests: skips background loading
ALERT_WEBHOOK_URL—Discord or Slack incoming webhook for alerts
ALERT_LAG_BLOCKS200Alert when the indexer is this far behind the chain
ALERT_SYNC_AGE_S120Alert when no sync succeeded for this long
ALERT_KEEPER_MIN_HYPE0.05Alert 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/v1

Without 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 start

Deploy

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 explorer

This 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 preflight

One 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 HYPE

Deploys (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

  1. Discover: NETWORK=elysium-mainnet RPC_URL=<rpc> LOGS_MAX_RANGE=10000 pnpm discover writes networks/elysium-mainnet.draft.json (factories with their WHYPE, v4 PoolManagers, USDC candidates, launchpads) with a _review list of uncertain items.
  2. Review against Kinetiq's published addresses (impostor tokens exist), then rename to elysium-mainnet.json.
  3. Preflight the network file.
  4. Deploy and verify.
  5. Preflight the deployment.
  6. 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 api

Set 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:

RPCeth_getLogs capNote
rpc.agoradex.app10,000 blocksWent down under load during the rehearsal (502, then "Application failed to respond")
rpc-live.boxa.club10,000 blocks
rpc.eleos.fun2,000 blocks
rpc.elixirpad.fun1,000 blocks
rpc.thekleos.pro1,000 blocks for address-less queriesFactory 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.

MeasureResult
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
Memory200–615 MB RSS: give the service ≥ 1 GB
QuotesHYPE→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/health always answers 200 (deploy healthchecks) with fresh, reasons, lagBlocks, lastSyncSecondsAgo and the keeper's balance. GET /v1/health?strict=1 answers 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 over data/events.db, start it.
  • Size: testnet bots make ~800k swaps a day, so set EVENTS_RETENTION_DAYS=14 there. 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_RANGE set if needed
  • Service memory ≥ 1 GB; TRUST_PROXY=1 on Railway
  • ALERT_WEBHOOK_URL set; an uptime monitor on /v1/health?strict=1
  • Railway volume backups enabled (event store and snapshot)
  • REDIS_URL when running more than one instance
  • Keeper on one instance only, its key funded with HYPE and allow-listed
  • CORS_ORIGINS restricted to your front-ends if the API isn't meant to be public
  • API_KEYS for partners; RATE_LIMIT_PER_MIN for the public
  • TLS in front (reverse proxy); WebSocket upgrade allowed on /v1/ws/prices
  • EXECUTOR_FROM_BLOCK / ORDERBOOK_FROM_BLOCK set to the deployment blocks
  • Owner key stored offline; know how to call setPaused(true)
lane.markets · Make the most of every move.