Three programs sit between the chain and the page. The indexer turns contract logs into tables. The API turns those tables, live chain reads, market data and its own database into the JSON every page reads. Seven bots inside the API process send the routine transactions that keep positions, hedges and pools current. None of them holds your funds or can sign for you, and of everything the bots send, only two kinds of call need their key.
The indexer
apps/indexer is a Ponder 0.16 app. One handler per event writes Postgres (or the bundled PGlite under .ponder/ when DATABASE_URL is empty), and the tables are served as GraphQL on port 42072. Every GraphQL document the API sends lives in apps/api/src/indexer/queries.ts, so the indexer's schema can change without any browser noticing.
Sources
| Family | Contracts | Starts at | Writes |
|---|---|---|---|
| Uniswap | v3 factory and every pool it creates (a Ponder factory source over PoolCreated), the v3 position manager, the v4 PoolManager, the v4 PositionManager's Transfer | PONDER_POOLS_START_BLOCK, by default the manifest's deployBlock | pool, poolHourStat, priceCache, position, rangeUptime, positionEvent, feesCollected, v4Position |
| Levee contracts | FeeRouter, ReferralRegistry, RangeOrders, PositionKeeper, VaultFactory, every vault clone, LaunchPipeline, PreMarketPerp | the manifest's deployBlock | rangeOrder, keeperEnrollment, keeperAction, vault, vaultShare, vaultEvent, graduation, perp*, referral |
| Levee pools | DammHook, StockHook, DlmmFactory and every pair it creates, Router, FeeCollector, BuybackV2 | deployBlock, when the manifest has a venue | ammPool, ammSwap, routerSwap, feeConversion, buyback, ammStat |
| Pons | the Pons factory: launches and graduations | PONS_LAUNCH_START_BLOCK and PONS_GRADUATION_START_BLOCK | ponsToken |
The Levee sources take their addresses from PONDER_MANIFEST and their ABIs from @levee/abi. With no manifest they are still declared, at the zero address and capped at block 0, so they fetch nothing while the generated event types stay the same. Vault clones are one source over VaultFactory.VaultCreated, using the union of the three vault ABIs; three sources would index every vault three times. DAMM and stock pools are v4 pools, so the PoolManager handlers pass any pool whose hook is Levee's to the pool handlers, and the hook's own fee event completes the swap row in the same transaction. Pons token metadata is filled in by two batched block jobs, about every 2 min live, instead of one RPC call per event.
How derived fields are computed
Handlers stay thin; anything with an edge case is a pure function in src/logic/, tested from test/. Tests cannot sit next to the code, because Ponder runs every file under src/ except src/api/ as an indexing file.
- Prices. USDG is $1 by definition. WETH is the latest WETH/USDG price, stored under the id
'WETH'. Any other token in a pool quoted by USDG or WETH is pool price times the quote's price. Volume is counted on the quote side only, so a pushed pool price cannot inflate it; a pool with no USDG or WETH side adds 0 USD. - Pool TVL. While a pool has at most 200 open positions (
TVL_EXACT_POSITION_LIMIT), TVL is the exact value of those positions. Above that, the active liquidity is valued as one position spanning 953 ticks (about ±10 %) around the current tick. v4 pools always use the second method: liquidity added to a v4 pool outside the position manager (by a hook or a custom router) has no position in the logs to sum. TVL refreshes onMint,Burnand the first swap of each hour. - v4 positions. A v4 position NFT logs no liquidity of its own. The position manager calls the
PoolManagerwith the token id as the salt, so itsModifyLiquidity(sender = the position manager) carries the pool, the ticks and the liquidity delta, and theTransfercarries the owner. The principal is recomputed from the delta at the pool price. Fees are not columns: v4 pays them out inside each liquidity change, and the logs do not itemise them.leveemarks rows in a DAMM or stock pool. - APR and Seasoned.
poolHourStatis an hourly rollup; the 24 h window is the last 24 buckets.apr24hPctisfeeAprFromPoolfrom@levee/core, andestablishedisisEstablished: at least 3 days old, market cap above $1M and at least 100 trades in 24 h. A token with no known supply has no market cap and is never Seasoned. - Range uptime. A position earns only while the tick is in
[tickLower, tickUpper). Each swap credits the time since the last one if the tick before that swap was in range, for v3 and v4 positions alike. The clock only moves forward. - Loss against holding. The entry amounts at the first deposit's price, compared with the position now plus everything withdrawn. Collected fees are left out on purpose.
Every address and hash is stored in lowercase. Token amounts, liquidity and timestamps are bigint and travel through GraphQL as strings. USD figures and percentages are double precision, whole percents, for display and ranking only.
Rows that look wrong but are not
| What you see | Why |
|---|---|
| A keeper enrollment changes its key | PositionKeeper.Rebalanced mints a new NFT. The old keeperEnrollment row gets succeededBy, the new one succeeds, and the lifetime totals carry over. |
A perp is liquidated, never closed | Liquidated fires before Closed in the same transaction, and the close handler keeps the liquidated status. |
vault.paused is false, yet the vault is paused | The column mirrors the HedgePaused and HedgeResumed events. A stale report pauses the vault with no transaction and no log, so compare hedgeAsOf with the vault's maxHedgeAge, or call paused(). |
| A holder's shares look too low | Share transfers are not indexed. vaultShare.shares is clamped at zero, so a sent share is attributed to the wrong holder but never counted twice. |
referral.earned holds strings | Raw token amounts routinely exceed 2^53. A Taken with no referrer writes nothing, and Claimed is not indexed. |
Running it
Ponder reserves /health, /ready, /status and /metrics. src/api/index.ts adds /healthz (a parseable { ok: true }), the GraphQL endpoint at / and /graphql, and Ponder's read-only SQL client at /sql/*. Gate traffic on /ready: it turns 200 only once the historical backfill is done, while /health and /healthz are 200 as soon as the process is up.
In production the image (apps/indexer/Dockerfile, built from the repo root) runs as a non-root user with no .env file inside, and settings.ts stops it at start without Postgres, an explicit PONDER_RPC_URL, chain 4663 or a start block, or when PONDER_MANIFEST names a missing file. scripts/start.sh gives every deploy its own schema, levee_<deploy id>: Ponder will not start a changed build on a schema another build used, so the new deploy backfills beside the old one, which keeps serving until the new one's /ready passes. Every deploy shares the RPC cache, so that backfill is mostly local. The live deploy's tables are also published as views in levee_indexer, and a restart of the same deploy resumes from its last checkpoint.
apps/api/src/indexer/queries.schema.test.ts fails when a GraphQL document in queries.ts asks for a column ponder.schema.ts does not declare, so that mistake never reaches a page as an empty card.
The API
apps/api is one Fastify process. loadConfig parses the environment once into a Config, and buildApp(config, deps) takes every dependency injected, so no module reads process.env on its own and the tests run the real routes against fakes. Optional pieces degrade instead of failing: no DATABASE_URL means in-memory storage, an unreachable indexer means empty answers and indexer: 'down' on /health, no manifest means empty vault, launch and pool answers, and VENUE defaults to the simulated book.
Routes
The API keeps its own route names. The Markets page reads GET /radar and the Ideas page reads /theses.
| Route | Serves | Answers from | Cache |
|---|---|---|---|
GET /radar | Markets | the chain pool snapshot merged with indexer pools, Levee pool rows, market data | 15 s |
GET /markets, GET /candles?market=&tf=&limit= | Trade, charts | market data; tf is one of 1m 5m 15m 1h 4h 1d, limit 1 to 1000 (300 by default) | 15 s, 60 s |
GET /portfolio/:address and /performance/… | Portfolio | indexer positions, vault shares, range orders and enrollments; perp venue; chain reads for live pool prices, pending fees and pools the indexer lacks; entry-time candles for cost basis | none |
GET /vaults | Earn | manifest and graduated vaults, chain reads, the indexer's harvest log, market data | 30 s |
GET /launch?address= | Launch | indexer pools and graduations, chain reads of LaunchPipeline and PreMarketPerp; your positions read fresh | 30 s for the shared part |
GET /pools?kind=, /pools/:id, /stocks, /protocol | Pools, Stocks, Protocol | indexer pool tables and live pool state, priced with market data; stock oracles and buyback state read per request | shared pool load, 15 s |
GET /pons?tab=&q=&limit=&cursor=, /pons/:token, /amm/vaults | Launch, Earn | ponsToken rows; DLMM and stock vaults read from their factories | shared pool load, 15 s |
GET /theses, /theses/:id, /theses/leaderboard, POST /theses, POST /theses/:id/mirror | Ideas | the database, plus indexer fees and PnL for the leaderboard | none |
GET /referral/:address | Ideas | the indexer's referral row, each token priced at its mark | none |
GET /points/:address, POST /points/recompute | Ideas | points events and Ideas in the database | none |
POST /perp/onboard, /bind, /order, /cancel, /withdraw-intent; GET /perp/account/:address, /positions/:address, /orders/:address | Trade | the perp venue | none |
POST /contact | contact form | the database; name and concern required, handle optional | none |
GET /health | status banner | a 1 s probe of the indexer's /ready, reused for 15 s | 15 s |
Cursors ride in the x-next-cursor header, so list routes answer bare arrays. /pons returns at most 100 rows per page (30 by default). /health answers exactly {ok, venue, indexer, db, signer, markets} (each ok, down or off, and 503 when the database or the signer is down) and nothing about the configuration.
Every error is {error, code}: bad_request 400, unauthorized 401, forbidden 403, not_found 404, conflict 409, rate_limited 429, unavailable 503, or a more specific code such as invalid_address, stale_ts, nonce_used, duplicate_thesis or manifest_missing.
The wire contract
The web's types are the contract. apps/api/src/routes/types/web.ts copies the interfaces of apps/web/lib/api.ts, and web.types.test.ts fails when a field is added, removed or renamed on either side. The API's own models keep whole percents and {items, nextCursor} pages and convert on the way out (toRadarWireRow, toPortfolioWire, routes/wire.ts): percent fields become fractions, and a candle's t in milliseconds becomes time in seconds.
How Markets rows are built
buildRadar is a pure function. The pool set comes from chain/poolSnapshot.ts, which asks the v3 factory for every tokenized asset against USDG and WETH on the four fee tiers and reads each pool's tick, liquidity and balances in Multicall3 batches. chain/poolStats.ts adds 24 h volume, fees, trades, age and market cap from each pool's own Swap logs, read incrementally into hourly buckets. Indexer rows replace snapshot rows for the same pool, and the snapshot keeps every pool the indexer never saw. The server warms the snapshot at start and every 60 s, so no request waits for it.
A pool names its row through the side that is not USDG or WETH, joined to a Lighter market through @levee/abi's TOKENS, or through the token's own symbol when there is no market. The table is the union of pools and perps. A symbol's best pool has the highest fee APR among pools holding at least 1 % of its pool TVL, so a dust pool cannot name the row. established (Seasoned) is isEstablished on that pool. A pool whose 24 h stats are not yet known answers lpApr: null rather than 0 %. Levee's own pools add one row each with source: 'levee'.
Carry is basisSignal from @levee/core: pool APR plus hourly funding times 8 760, at least 8 % to watch and 20 % for strong. The wire's basisSignal reads it as follows: pool and perp give lp-and-short (Hedge) when carry clears the watch bar and neutral otherwise; a pool alone gives neutral if Seasoned and avoid if not; a perp alone gives long-perp; neither gives avoid.
Market data
MarketData is one cached view of every symbol with a fallback chain. Lighter REST is the source of truth: stats from /api/v1/orderBookDetails, funding from /api/v1/funding-rates, with the WebSocket stream filling gaps between polls. A crypto symbol without a Lighter mark falls back to Hyperliquid; a stock, index or commodity falls back to Yahoo. A symbol nothing answers for still comes back, with mark: null and source: 'none'. Funding and open interest exist only on Lighter. Concurrent requests for the same data share one upstream call.
Ideas and mirroring
POST /theses verifies an EIP-712 signature under { name: 'Levee', version: '1', chainId } over Thesis { positionRef, side, text, ts }. The market is display metadata and is not signed. positionRef is v3:<tokenId>, perp:<venue>:<market>:<id> or vault:<address>; side is long, short or lp; text is at most 500 characters; ts must be within 300 s of the server clock. Because ts is in the message, one signature makes one post: a repeat is 409 duplicate_thesis, enforced under a race by the theses_replay_unique index. The leaderboard ranks authors by realised fees plus realised PnL across their indexed positions.
POST /theses/:id/mirror executes nothing. It returns zapMint calldata on zapV3 for a v3 position (the author's pair, fee and ticks, sizeUsd worth of the paying token or a zero amount without one, 100 bps slippage, a 20 min deadline), deposit(0, you) for a vault, or, given sizeUsd and a live Lighter mark, a sized market order for a perp.
Perp requests
Every /perp/* route forwards to the configured venue. VENUE=sim is SimVenue: an in-memory book on real marks that credits each new account 10 000 USDG, fills market orders 5 bps worse than the mark and accrues funding hourly. VENUE=lighter is LighterVenue, an HTTP client for services/signer that signs every request with the shared SIGNER_SECRET: an HMAC over a timestamp, a one-time nonce, the path and the body, so the secret never crosses the network. The API never holds an order key, and there is no withdraw route: /perp/withdraw-intent returns a payload for your wallet to sign, and on the sim answers 503 sim_no_withdraw.
Each write body carries auth: { nonce, ts, signature }, an EIP-712 signature over PerpAction { action, account, payloadHash, nonce, ts }. payloadHash is the keccak256 of the body without auth, as canonical JSON, so a signature fits exactly one body; action is fixed by the route. requirePerpAuth checks, in order: a valid address (400), a well-formed envelope (401), ts within 300 s (401 stale_ts), the signature recovering to address (401), and an unused nonce for that address (409 nonce_used). The nonce is spent last, only for a request that verified, so a forged request cannot burn a nonce you are about to use. The message builder is perpAuth.ts in @levee/core, which the web's lib/perpAuth.ts uses too.
Limits and hardening
- Every
POST,PUT,PATCHandDELETEshares a budget ofRATE_LIMIT_WRITES_PER_MIN(30) per route group and client IP, where the group is the first segment of the matched route pattern. A refusal is 429 withRetry-After. Reads are never limited./contactadds its own 5 per minute per IP. - The limiter lives in the process, so each replica grants the full budget.
TRUST_PROXYdecides what the client IP is and is off by default; turn it on only behind a proxy that setsX-Forwarded-For.CORS_ORIGINis a comma-separated list of scheme-and-host origins.*and bare host names are refused, and withNODE_ENV=productionthe API will not start without it.POST /points/recomputeneedsx-admin-secret, compared in constant time; withoutADMIN_SECRETit answers 503.
When the API is not there
Each page hook in apps/web/lib/data.ts wraps its fetcher in React Query with no retry and a 15 s stale time. In a development or staging build, a failed request falls back to a fixture from lib/fixtures/ and the page shows a "Sample data" tag. A production build never shows a fixture: the page renders its empty state and a status notice. The sample portfolio, referral and points belong to one sample address, so any other address gets an empty answer. Writes need a manifest, not the API.
The bots
The bots live in apps/api/src/bots and run inside the API process. startBots returns without starting anything, and logs which variable is missing, unless BOTS, BOT_PRIVATE_KEY and MANIFEST_PATH are all set. A bot is one tick(): it reads, decides and returns a list of actions, each a transaction hash, a venue order id or a skipped reason. Every skip carries its reason.
The runner's rule is that a bot must never take the API down: every tick is wrapped and its errors logged, a tick still running when its interval comes round is skipped rather than stacked, timers never hold up a shutdown, and every bot runs once at start. The hedger ticks every HEDGER_INTERVAL_MS (30 s), the four pool bots every AMM_BOT_INTERVAL_MS (15 s), and the keeper and graduator every BOT_INTERVAL_MS (60 s).
The send path
Every bot write goes through ChainClient.simulateAndSend:
simulateContract, aneth_callagainst pending state. A revert is the usual result, not an error: the position no longer qualifies, the order is not fillable yet, the token is too young. It costs nothing, which is why the bots can run on a loop.- For a send paid by a bounty,
estimateContractGastimesgetGasPricegives its cost, and the send is skipped when the bounty is under 3 times that (GAS_BOUNTY_MULTIPLE). The margin absorbs a base-fee jump between the estimate and the block. A bounty that could not be priced skips before the estimate: a missed harvest costs the owner nothing, while sending blind would turn one market-data outage into unguarded gas for every position. writeContract, then one confirmation. A reverted receipt counts as a failure.
ActionGuard, keyed by action and target, mutes a target for COOLDOWN_MS (5 min) after a success, while the indexer catches up, and for FAIL_BACKOFF_MS (15 min) after a failure, so a position that always reverts costs one simulation per quarter hour. The state is in memory on purpose: a restarted bot rebuilds everything from the chain and the indexer, and the contracts' own intervals are the real guard. It also means two replicas would send twice, so run the bots on one.
Reads are batched through Multicall3, which the bots' viem chain must declare, or every batch falls back to one call per read.
Keeper
One keeper tick runs three sweeps in order.
- Enrolled positions. The indexer's active enrollments, then
shouldHarvestandshouldRebalancefor each in one Multicall3 batch, thenharvestorrebalancewhere the contract says yes. The views are the contract's own answer, so the bot never re-implements the owner's policy. The bounty isbountyBpsof the position's pending fees (uncollectedFeesUsd, which the indexer client reads from the chain), converted to wei at the ETH mark, and the same figure for a rebalance. - Range orders.
fillable(orderId)for every open order, thenfillonly where it says yes. The bounty is 1 % ofamountInUsd(RANGE_ORDER_BOUNTY_BPS100). - Manifest vaults.
LPVault.harvest()has no "worth it" view, so the bot simulates it once perVAULT_HARVEST_INTERVAL_MS(6 h) per vault and sends whenever the simulation passes, markedbounty: { kind: 'none' }. The vault's ownminIntervalrejects the rest.
Hedger
A hedged vault's LP position changes its exposure as the price moves, so the short must be resized. For each vault whose manifest name contains "Hedged", plus any address in HEDGED_VAULTS, the hedger reads positionTokenId() and asset() on the vault, the position manager's positions(tokenId) and the pool's slot0(). The vault's asset (WETH) is the numeraire and is not hedged; the other pool token is the risk asset, whichever side of the pair it sorts to. lpDelta from @levee/core gives the target short as the position's balance of that token. The market is the Lighter symbol for the risk asset unless VAULT_MARKETS names another.
The hedger acts when a report is due (HEDGE_REPORT_INTERVAL_MS, 15 min) or when the gap between target and current short exceeds HEDGE_DRIFT_BPS (200) of the target. hedgeOrders applies the dead band, rounds down to the lot size (HEDGE_LOT_SIZE 0.001) and drops anything under HEDGE_MIN_NOTIONAL_USD ($50), returning zero or one market order; a buy is always reduce-only. The order goes to the venue under the vault's hedge account: its funder's address, read once from fundingConfig().funderKey, because the margin sits in the funder's Lighter account. An account with no API key bound is skipped with an alert and no report, never read as holding no short, and two vaults on one account are both skipped. Then reportHedge(notionalUsd, unrealizedPnlUsd, equityUsd, asOf) writes that account's state on chain in USD times 1e6, with asOf from the bot's clock less 10 s, so the latest block, which trails the clock, never sees it as from the future. That report is what the vault's totalAssets() and its staleness pause rest on. It pays no bounty and goes out whenever it simulates.
The seed vaults are deployed with maxHedgeAge of 1 h and maxEquityJumpBps of 1500, so a 15 min cadence fits four reports into the staleness window. The hedger never calls fundHedge (that is the funder's key) or StructuredVault.reportSleeve.
Graduator
LaunchPipeline checks only age and pool depth on chain; the rest of the Seasoned screen is the indexer's flag, and this bot joins the two. For every pool the indexer marks established whose launch token (the side that is not USDG or WETH) has not graduated, it reads firstSeen(token) in one batch. Zero means track(token), which starts the three-day clock. Three days or more means graduate(token), which clones the token's LPVault. Anything in between is skipped without a log line. Neither call pays a bounty. ThinPool, TooYoung, NotTracked and AlreadyGraduated are the normal result for most tokens on most ticks, so they are logged as skips and the 15 min backoff stops a re-simulation every minute.
Pool duties
Four bots look after Levee's own pools. They need the manifest's venue object, pay their own gas with no bounty, and share the send path above.
| Bot | Acts when | Sends | Notes |
|---|---|---|---|
dlmmVaultRebalancer | a DLMM vault's needsRebalance() (the active bin more than half the half-width from the range centre) has held for DLMM_SUSTAIN_S (120 s) and the vault's 5 min interval has passed | rebalance(activeId, DLMM_ID_SLIPPAGE), 2 bins of slippage | only the factory's keeper() (or the protocol owner) may call it; with another key the bot reports the vault as due and sends nothing. The 120 s wait is the guard: holding a pushed price that long means trading against every arbitrageur |
stockVaultRebalancer | rebalanceStatus() returns ready: the session changed or the oracle drifted, the oracle is fresh, the pool is near the oracle and the 5 min interval has passed | rebalance() | keeper only; a failed attempt is retried after STOCK_VAULT_RETRY_S (60 s) |
limitOrderSettler | DlmmLimitOrders.readyBooks() lists filled batches | executeMany for up to LIMIT_ORDER_BATCH (25) books | anyone may call it; no cooldown after a success and a 60 s retry, because an unexecuted fill trades back if the price returns |
buybackPoker | BuybackV2's reference price is at least BUYBACK_POKE_MIN_GAP (1 %) from the pool price and at least BUYBACK_POKE_EVERY_S (600 s) old | poke() | anyone may call it; keeps a buyback from waiting on an old reference after a fast move |
Reference
Indexer settings
| Name | Allowed | Default | Effect |
|---|---|---|---|
PONDER_CHAIN_ID | 4663 or 31337 | 4663 | 31337 only for a plain anvil; a fork of Robinhood Chain keeps 4663. Production accepts 4663 only. |
PONDER_RPC_URL | URL | the public Robinhood RPC | Needs state at past blocks; the public endpoint keeps only recent state. Required in production. |
PONDER_RPC_MAX_RPS | 0 or more | 15 | Request rate cap; 0 turns the throttle off for a local node. |
PONDER_MANIFEST | path | image: /app/deployments/manifest.json | contracts/deployments/[network].json. Without it only Uniswap is indexed; in production a path to a missing file stops the start. |
PONDER_POOLS_START_BLOCK | block | deployBlock | First block of the Uniswap sources. With no manifest: required in production, 0 otherwise. |
PONS_GRADUATION_START_BLOCK | block | 26 841 846 | First block for Pons graduations; ignored without a venue. |
PONS_LAUNCH_START_BLOCK | block | deployBlock | First block for Pons launches, about 13 000 a day. |
DATABASE_URL | URL | empty = PGlite | Postgres; production refuses PGlite. |
PONDER_DEPLOY_ID | string | RAILWAY_DEPLOYMENT_ID | Gives each deploy its own schema, levee_[id]. |
DATABASE_SCHEMA | name | levee_indexer | The schema when there is no deploy id. Never public, where the API keeps its tables. |
DATABASE_VIEWS_SCHEMA | name | levee_indexer | With a deploy id: where the live deploy is published as views. |
PONDER_PORT / PORT | port | 42072 | Listen port of scripts/start.sh. |
API settings
| Name | Allowed | Default | Effect |
|---|---|---|---|
PORT | port | 8792 | HTTP port. |
DATABASE_URL | URL | - | Unset: in-memory storage, lost on restart. |
INDEXER_URL | URL | http://127.0.0.1:42072 | GraphQL endpoint and the /ready probe. |
RPC_URL / CHAIN_ID | URL / id | public RPC / 4663 | Chain reads, the bot wallet, and the EIP-712 domain of Ideas and perp requests. |
MANIFEST_PATH | path | - | Unset: vault, launch and pool routes answer empty, v3 mirroring answers 503, bots stay off. |
VENUE | sim or lighter | sim | lighter forwards to the signer. |
SIGNER_URL / SIGNER_SECRET | URL / string | http://127.0.0.1:8793 | Rotate the secret on both services together. |
LIGHTER_BASE_URL / LIGHTER_WS_URL | URL | api.rh.lighter.xyz | Market data source of truth. |
CACHE_MARKETS_MS / CACHE_CANDLES_MS | ms | 15 000 / 60 000 | Market data caches. |
ADMIN_SECRET | string | - | Unlocks POST /points/recompute. |
NODE_ENV | development, test, production | development | production requires CORS_ORIGIN. |
RATE_LIMIT_WRITES_PER_MIN | more than 0 | 30 | Per route group and client IP. |
TRUST_PROXY | bool or proxy list | off | Only behind a proxy that sets X-Forwarded-For. |
CORS_ORIGIN | origins | - | Unset: any origin is reflected, development only. |
Bot settings
| Name | Allowed | Default | Effect |
|---|---|---|---|
BOTS | names, comma-separated | empty = off | keeper, hedger, graduator, dlmmVaultRebalancer, buybackPoker, limitOrderSettler, stockVaultRebalancer; an unknown name stops the API at start. |
BOT_PRIVATE_KEY | hex key | - | A separate hot key; its address is the HEDGER the deploy names. |
BOT_INTERVAL_MS | ms | 60 000 | Keeper and graduator. |
HEDGER_INTERVAL_MS | ms | 30 000 | Hedger. |
AMM_BOT_INTERVAL_MS | ms | 15 000 | The four pool bots. |
COOLDOWN_MS / FAIL_BACKOFF_MS | ms | 300 000 / 900 000 | ActionGuard mutes after a success and after a failure. |
VAULT_HARVEST_INTERVAL_MS | ms | 21 600 000 | How often each manifest vault harvest is simulated (6 h). |
HEDGED_VAULTS / VAULT_MARKETS | addresses / vault:MARKET[:BASE] | empty | Extra hedged vaults; market overrides. |
HEDGE_REPORT_INTERVAL_MS | ms | 900 000 | Report cadence; keep it well under maxHedgeAge (1 h on the seed vaults). |
HEDGE_DRIFT_BPS | bps | 200 | Drift that forces a re-hedge between reports. |
HEDGE_MIN_NOTIONAL_USD / HEDGE_LOT_SIZE | USD / base units | 50 / 0.001 | Smallest adjustment sent; sizes round down to the lot. |
HEDGER_MODE | live, report-only, off | live | Kill switch: report-only places no order but keeps reporting; off does nothing. |
HEDGE_MAX_ORDER_USD / HEDGE_DAILY_LOSS_USD | USD | 25 000 / 2 500 | Largest single hedge order (at most the signer's MAX_ORDER_NOTIONAL_USD); hedge loss per UTC day that stops orders. |
HEDGE_MAX_NOTIONAL_USD | USD | off | Optional ceiling on one vault's short; off, a vault is hedged in full. |
DLMM_SUSTAIN_S / DLMM_ID_SLIPPAGE | s / bins | 120 / 2 | DLMM vault rebalancer. |
BUYBACK_POKE_EVERY_S / BUYBACK_POKE_MIN_GAP | s / fraction | 600 / 0.01 | Buyback poker. |
LIMIT_ORDER_BATCH | books | 25 | Limit-order books per executeMany. |
STOCK_VAULT_RETRY_S | s | 60 | Retry delay after a failed stock vault rebalance. |
Known gaps
GET /referral/:addressalways reportsreferredCount: 0; the indexer counts referrals, but the API does not read the column.- Nothing in the API writes a points event yet, so every points total is zero.
- No bot calls
StructuredVault.reportSleeve, so the Income note's sleeve is reported only by hand. - The chain pool snapshot finds v3 pools only.
apps/indexer/ponder.config.tssources, start blocks and the no-manifest fallbackapps/indexer/ponder.schema.tsevery table and columnapps/indexer/src/logic/pricing, TVL, stats, uptime and the Levee state machinesapps/api/src/app.tsbuildApp, the write budget and the error shapeapps/api/src/config.tsevery environment variableapps/api/src/radar.tsMarkets rows and the Carry chipapps/api/src/chain/poolSnapshot.tsthe pool set read from the chainapps/api/src/perp/auth.tsrequirePerpAuth and its order of checksapps/api/src/routes/types/web.types.test.tsthe wire guardapps/api/src/bots/chain.tssimulateAndSend and the gas ruleapps/api/src/bots/runner.tswhich bots start, and their intervalsapps/api/src/bots/keeper, hedger, graduator and the four pool botsapps/web/lib/data.tsthe fixture rule and isSampleData