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.