Skip to main content

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​

RouteQueryResponse
/v1/healthHealthStatus (chain id, latest indexed block)
/v1/statsGlobalStats
/v1/quotesQuoteToken[]
/v1/tokenssort=new|trending|volume|mcap|gainers, quote=<addr>|rwa, tier, minFee, maxFee (percent), maxAge (s), q, creator, limit ≤ 100, cursorPage<TokenSummary>
/v1/tokens/:addressTokenDetail (404 if unknown)
/v1/tokens/:address/candlestf=1m|5m|15m|1h|4h|1d, denom=quote|usd, from, to, limit ≤ 1000Candle[], ascending
/v1/tokens/:address/tradesTokenTradesQueryPage<Trade>, newest first
/v1/tokens/:address/holderslimit ≤ 200{ holders, distribution }
/v1/activitylimit ≤ 100, cursor, kind (comma list), tokenPage<ActivityItem>, newest first
/v1/activity/streamkind, tokenServer-sent events (below)
/v1/creators/:addressCreatorProfile
/v1/traders/:address/portfolioPortfolio
/v1/leaderboardswindow=24h|7d|allLeaderboards
/v1/beneficiariesnone, by designBeneficiariesDump: 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 sends retract for 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 whose Last-Event-ID was orphaned sends reset: drop every item with id >= from, which is resent right after.
  • Streams close after 600 s (INDEXER_SSE_MAX_SECONDS) and the client reconnects with Last-Event-ID. At most 8 concurrent streams per IP (429) and 2,000 in total (503), both with Retry-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 as score / (1 + ageHours / 48).
  • Gainers: change24hPct descending, among tokens with volume24hUsd ≥ $500.
  • Holder counts and the distribution exclude infrastructure accounts (Infinity vault and pool manager, locker, factory, hook, FeeVault, router, burn); the holders list still includes them with a label.

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. feesCollectedUsd comes from the locker's ClaimedRewards.
  • /v1/beneficiaries mirrors the FeeVault exactly: Deposited credits; Claimed, SweptToFallback and ShieldedFromTemplate (USDT) debit; Consolidated moves assetIn to USDT; ShieldTemplatesRegistered / ShieldTemplatesCleared track the template count. The per-launch credit that bounds the creator path (FeeVault.launchCredit) is not indexed: read it on-chain.
  • QuoteToken carries the registry data the dApp needs: consolidatable (a route to USDT exists), creatorConsolidation (a price feed exists), maxSwapSize, maxDeviationBps, and on the USDT row minCreatorShield.

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.