disconnected
docs/developers/indexer

Indexer

The naka frontend ships with a lightweight, stateless indexer implemented as a set of Next.js API routes. There is no separate service, no database, and no background process. Every request walks the chain via eth_getLogs and returns the result, with HTTP cache headers in front to absorb load.

This is intentional. Sepolia traffic is light; the protocol is itself stateless above the chain; and the existing Next.js deployment already covers the data-plane footprint.

Endpoints

All endpoints accept ?chainId=<id> (defaults to Sepolia) and return JSON.

GET /api/indexer/stats

Rolling 24-hour aggregates from Buy and Sell events on the NakaHook contract.

{
  "volEthWei": "12345000000000000",
  "swaps": 17,
  "buys": 11,
  "sells": 6,
  "fromBlock": 8123400,
  "toBlock": 8130600
}
  • Window: head - 7200 blocks (≈24h on Sepolia at 12s blocks).
  • Cache: s-maxage=60, stale-while-revalidate=120.

GET /api/indexer/trades?limit=N

The most recent min(limit, 100) trades within a 7-day lookback, sorted newest-first.

{
  "trades": [
    {
      "kind": "buy",
      "user": "0x…",
      "ethValue": "200000000000000000",
      "tokenValue": "10500000000000000000000",
      "tokensLocked": "31500000000000000000",
      "blockNumber": 8130591,
      "blockTimestamp": 1757123412,
      "txHash": "0x…",
      "logIndex": 3
    }
  ]
}
  • Window: 7 days. Limit defaults to 25, capped at 100.
  • Cache: s-maxage=30, stale-while-revalidate=90.
  • Block timestamps are resolved with deduplication so the lookback only fetches each block once even when multiple trades share a block.

GET /api/indexer/flows?hours=N

Hourly buckets of buy and sell ETH volume + counts over the last N hours (default 24, capped 72). Used by the homepage flow chart.

{
  "buckets": [
    {
      "tStart": 1778097600,
      "tEnd": 1778101200,
      "buyEthWei": "1200000000000000000",
      "sellEthWei": "800000000000000000",
      "buys": 3,
      "sells": 2
    }
  ],
  "totals": {
    "inEthWei": "12340000000000000000",
    "outEthWei": "9870000000000000000",
    "netEthWei": "2470000000000000000",
    "buys": 18,
    "sells": 12
  },
  "windowHours": 24
}
  • Bucket boundaries are aligned to wall-clock UTC hours so bars are visually stable as the window slides forward.
  • Block timestamps are deduplicated; events sharing a block reuse one getBlock lookup.
  • Cache: s-maxage=60, stale-while-revalidate=120.

GET /api/indexer/holders?lookbackDays=N

Replays NakaToken Transfer events over a configurable window (default 30 days, capped 180), accumulates per-address balance deltas, and returns the count of addresses with positive net balance excluding 0xdEaD.

{
  "holders": 142,
  "approximate": true,
  "scannedBlocks": 216000
}
  • approximate: true whenever the window does not start at genesis. Holders whose last transfer predates the window are missed; the number trends up as the window grows.
  • Cache: s-maxage=300, stale-while-revalidate=600.

Frontend Integration

Three React Query hooks in lib/hooks/useIndexer.ts:

  • use24hStats() → 60-second poll
  • useRecentTrades(limit) → 30-second poll
  • useHolders() → 5-minute poll
  • useHourlyFlows(hours) → 60-second poll

All three include automatic chain-id binding via wagmi, so switching networks re-fetches against the correct contract set. When the active chain has no deployed contracts (a custom or unsupported chain), the API returns 502 and the UI gracefully shows plus an indexer offline hint.

RPC Configuration

Server-side, the public client in lib/server/publicClient.ts reads:

  1. SEPOLIA_RPC / MAINNET_RPC (server-only. Preferred for paid provider URLs that should never reach the browser bundle).
  2. NEXT_PUBLIC_SEPOLIA_RPC / NEXT_PUBLIC_MAINNET_RPC (fallback).
  3. PublicNode public endpoints (last-resort fallback).

Public RPCs typically cap eth_getLogs at ~5–10k blocks per call, so the client chunks queries via getLogsChunked() with a default chunk size of 5000 blocks. For higher throughput, wire a dedicated provider (Alchemy, Infura, QuickNode) into SEPOLIA_RPC.

When to Replace This

The current implementation handles the early-mainnet case fine. Once daily trades exceed a few hundred or holder count grows past a few thousand, the on-demand getLogs model becomes the wrong shape:

  • Latency: the holders endpoint scans 30 days of transfers per cold miss.
  • Cost: each getLogs chunk is a paid RPC call.
  • Correctness: balance reconstruction misses pre-window holders.

The migration path is a stateful indexer:

  1. Ponder (TypeScript, viem-native). Drop-in replacement, GraphQL out of the box. Schema would mirror these endpoints.
  2. The Graph subgraph. If multi-chain / hosted infra is preferred.
  3. Custom Postgres + cron. If you want full control.

In all three cases, the API surface above stays the same; only lib/server/indexer.ts swaps from eth_getLogs calls to database reads. The frontend hooks and components require no changes.

Local Testing

# from repo root
npm run dev

# stats endpoint
curl 'http://localhost:3000/api/indexer/stats?chainId=11155111' | jq

# trades feed
curl 'http://localhost:3000/api/indexer/trades?limit=10' | jq

# holders count
curl 'http://localhost:3000/api/indexer/holders?lookbackDays=14' | jq

A failing endpoint returns a 502 with { "error": "..." }. Common causes:

  • Contract not deployed on the requested chain (e.g. an unsupported network).
  • RPC rate limit hit (set MAINNET_RPC / SEPOLIA_RPC to a paid provider).
  • Network timeout. The client uses a 20s timeout per request.