Indexer API
A Ponder indexer over the factory, the Infinity pool manager (zk-pad pools
only), the hook, the locker, the tokens, the FeeVault and the QuoteRegistry. One process indexes
one chain: BSC (56), BSC testnet (97) or local anvil (31337). All routes are GET, return JSON
and are CORS-open. The server keeps no per-request logs, no IPs and no analytics.
Canonical shapes: services/indexer/src/api/types.ts; contract: services/indexer/API.md.
Routes
| Route | Query | Response |
|---|---|---|
/v1/health | HealthStatus (chain id, latest indexed block) | |
/v1/stats | GlobalStats | |
/v1/quotes | QuoteToken[] | |
/v1/tokens | sort=new|trending|volume|mcap|gainers, quote=<addr>|rwa, tier, minFee, maxFee (percent), maxAge (s), q, creator, limit ≤ 100, cursor | Page<TokenSummary> |
/v1/tokens/:address | TokenDetail (404 if unknown) | |
/v1/tokens/:address/candles | tf=1m|5m|15m|1h|4h|1d, denom=quote|usd, from, to, limit ≤ 1000 | Candle[], ascending |
/v1/tokens/:address/trades | TokenTradesQuery | Page<Trade>, newest first |
/v1/tokens/:address/holders | limit ≤ 200 | { holders, distribution } |
/v1/activity | limit ≤ 100, cursor, kind (comma list), token | Page<ActivityItem>, newest first |
/v1/activity/stream | kind, token | Server-sent events (below) |
/v1/creators/:address | CreatorProfile | |
/v1/traders/:address/portfolio | Portfolio | |
/v1/leaderboards | window=24h|7d|all | Leaderboards |
/v1/beneficiaries | none, by design | BeneficiariesDump: every FeeVault account. There is no per-id route. |
Ponder reserves /health (an empty 200 liveness probe), /ready, /status and /metrics for
itself, which is why the JSON health lives under /v1. Ponder's /metrics holds aggregate
per-route counters only; block it at the reverse proxy if even that is unwanted.
Activity kinds: launch, trade, large_buy, fee_collect, protocol_fee, consolidation,
shield, claim, deposit, fee_lowered. kind=trade also matches large_buy.
Beneficiary-side activity has actor: null.
GraphQL is operator-only
Ponder's raw GraphQL can filter, sort and count any column of any table, bypassing the REST
caches, and can look a beneficiary up by id. It is therefore not public: /graphql is mounted
only when the operator sets INDEXER_GRAPHQL_TOKEN, then requires
Authorization: Bearer <token> (404 otherwise) and runs with strict limits (2 aliases, 400
tokens, depth 8 by default). Beneficiary clients never query it.
Activity stream (SSE)
Events: activity (ActivityItem, with id: <ActivityItem.id>), retract ({ id }), reset
({ from }) and ping every 15 s. The first event after connecting is always a ping.
Last-Event-ID resumes a stream.
- All streams share one server-side poller: one store query per second, whatever the number of viewers.
- It re-reads the last 32 blocks (
INDEXER_SSE_REORG_BLOCKS) on each tick, so after a reorg it sendsretractfor an item that was orphaned, may deliver a replacement whose id sorts below ones already sent (insert by id; the feed is not append-only), and on a resume whoseLast-Event-IDwas orphaned sendsreset: drop every item withid >= from, which is resent right after. - Streams close after 600 s (
INDEXER_SSE_MAX_SECONDS) and the client reconnects withLast-Event-ID. At most 8 concurrent streams per IP (429) and 2,000 in total (503), both withRetry-After: 30. The IP is an in-memory counter key only while the stream is open.
Caching
List endpoints rank from a snapshot cached for 2 s. The routes that scan whole windows or tables
(/v1/leaderboards per window, the holder distribution of /v1/tokens/:address/holders, and the
/v1/beneficiaries dump) are memoized server-side for 10 s. Concurrent requests share one
in-flight query, and unknown query parameters do not change the cache key, so looping on
cache-busted URLs costs one query per TTL. Responses carry Cache-Control and are identical for
every caller, so a CDN or onion service in front is recommended.
Ranking
- Trending score:
volume1h × 3 + volume24h × 0.5 + trades1h × 20 (USD-equivalent) + holderGrowth24h × 50, decayed by age asscore / (1 + ageHours / 48). - Gainers:
change24hPctdescending, among tokens withvolume24hUsd ≥ $500. - Holder counts and the distribution exclude infrastructure accounts (Infinity vault and pool
manager, locker, factory, hook, FeeVault, router, burn); the
holderslist still includes them with alabel.
Fee and beneficiary accounting
- Buys record the zk-pad fee on the trader's gross input. Sells record the protocol fee
at the execution value and apply the hook's
ProtocolFeeRebated(75% of the excess over the post-swap value goes back to the positions, so the trade's proceeds, protocol fee and beneficiary fee are corrected). A sell that ends with no in-range liquidity keeps the whole execution-value fee for the protocol, as the hook does. - The locker's fee-exempt conversion swaps move price and candles but are not trades.
feesCollectedUsdcomes from the locker'sClaimedRewards. /v1/beneficiariesmirrors the FeeVault exactly:Depositedcredits;Claimed,SweptToFallbackandShieldedFromTemplate(USDT) debit;ConsolidatedmovesassetInto USDT;ShieldTemplatesRegistered/ShieldTemplatesClearedtrack the template count. The per-launch credit that bounds the creator path (FeeVault.launchCredit) is not indexed: read it on-chain.QuoteTokencarries the registry data the dApp needs:consolidatable(a route to USDT exists),creatorConsolidation(a price feed exists),maxSwapSize,maxDeviationBps, and on the USDT rowminCreatorShield.
Sources
TokenLaunched (factory); Initialize / Swap / ModifyLiquidity of the CLPoolManager for
pools whose hook is ZkPadHook only. The pool manager itself is not indexed: those logs are read
from the receipts of transactions that carry a zk-pad hook, locker, MEV-module or factory event,
which every zk-pad pool action does, so no other pool's swaps are fetched. Hook protocol-fee events (ClaimProtocolFees, ProtocolFeeRebated);
locker TokenRewardAdded, ClaimedRewards and FeesSwapped; ZkPadToken Transfer; FeeVault
events; QuoteRegistry changes. USD prices come from the quote's route (stables = $1) or Chainlink.