Three programs sit between the chain and the page. The indexer turns contract logs into tables, the API turns those tables, live chain reads and market data into the JSON every page reads, and the bots send the routine transactions that keep positions, hedges and pools current.
The indexer
apps/indexer writes one handler per event into Postgres and serves the tables as GraphQL. It follows every ZKDepth contract in the manifest, the Uniswap v4 PoolManager for pools on ZKDepth's hooks, the two Uniswap position managers and the Pons factory. It does not follow the third party's pools, so a range in one shows its value and pending fees from the chain, and no time in range. Volume is counted on the quote side only, so a pushed pool price cannot inflate it.
Gate traffic on /ready, which turns 200 once the backfill is done. Production needs an RPC with archive state, because handlers read positions at the block of the event. Each build gets its own schema, so a new deploy backfills beside the old one.
The API
apps/api is one Fastify image with three roles. api serves the routes, worker runs the bots and jobs, and agent serves the agent tools over MCP and REST with no database, signer secret or hot key.
| Routes | Serves | Cache |
|---|---|---|
/radar, /markets, /candles, /book, /funding/history | Markets, Trade, charts | 15 s, 60 s for candles |
/portfolio/:address | Portfolio | none |
/vaults, /amm/vaults | Earn | 30 s, 15 s |
/pools, /stocks, /protocol, /pons, /launch | Pools, Stocks, Protocol, Launch | 15 to 30 s |
/perp/* | Trade, through the signer | none |
/theses, /points, /referral, /follows, /lounge/feed | Ideas and Lounge | feed 10 s |
A test fails when a field in the web's types is added, removed or renamed on either side.
MarketData is one cached view of every symbol. Lighter REST is the source of truth, with its WebSocket stream filling gaps, and other public sources fill in where Lighter has no mark. /perp/* forwards to the signer, with each call signed by a shared secret over a timestamp, a one-time nonce, the path and the body. The API never holds an order key, and there is no withdraw route.
Every write shares a budget of 30 a minute per route group and client IP, answered with 429 and Retry-After. Reads are not limited, and /contact has its own 5 a minute.
The bots
Bots start only when BOTS, BOT_PRIVATE_KEY and MANIFEST_PATH are all set. A worker holds a Postgres lease, so a second container stays on standby. Every write simulates first: a revert is the usual result and costs nothing. A target is muted for 5 min after a success and 15 min after a failure.
| Bot | Every | Sends |
|---|---|---|
keeper | 60 s | harvest and rebalance on enrolled positions, fill on crossed range orders, harvest on manifest vaults every 6 h |
hedger | 30 s | hedge orders on Lighter, and reportHedge every 15 min |
graduator | 60 s | graduate for tokens past their 3 day clock |
dlmmVaultRebalancer | 15 s | rebalance once a DLMM vault's drift has held for 120 s |
stockVaultRebalancer | 15 s | rebalance when rebalanceStatus() says ready |
limitOrderSettler | 15 s | executeMany for up to 25 filled DLMM books |
buybackPoker | 15 s | poke when the reference is 1 % off the pool and 600 s old |
feeSweeper | 10 min | flush, convert, unwrapWeth, forwardEth, burnZKDepth, buyback |
spotFeeBooker | 5 min | take for 25 bps fees from the Uniswap path |
perpKeeper | 10 s | liquidate on the perps engine, and pokeFunding at most every 15 min |
Anyone can send what a bot sends, except reportHedge, which only a vault's hedger may call, and the DLMM and stock vault rebalances, which only their factory's keeper may call. Neither role can move funds out of a vault.
The worker also runs jobs that need no key. Points accrual reads indexed events every 5 min into the points ledger, one row per event.
The local stack
scripts/dev-local.sh runs the contracts, indexer, API and web on a fork, with a paper perp venue (VENUE=sim) that production refuses.
apps/indexer/ponder.schema.tsevery table and columnapps/api/src/config.tsevery environment variableapps/api/src/bots/chain.tssimulateAndSend and the gas rule