Chapter 09 · Indexer and data
Indexer and data
Events to tables, the GraphQL API and the two REST endpoints the app reads.
The indexer is a Ponder app that watches the hook, the factory and the burner from the deploy block and keeps five tables. It serves GraphQL and two small REST endpoints. Everything price-sensitive (quotes, gate checks, fee previews) is read from the chain by the web app; the indexer is for lists, counters and history.
Tables
| Table | Key | Holds |
|---|---|---|
pool | poolId | token, quote, orientation, creator, block mask and snipe cap, opening price and tick, seed liquidity, launch block, swap and buy counts, gross quote volume, fees, tokens burned, pot balance and count, last winner, last price, timestamps |
swap | block-logIndex | one row per HookSwap: trader, recipient, isBuy, gross quote, token amount, fee, block, timestamp, tx hash |
potEvent | block-logIndex | advanced, won or claimed with account, amount and count |
claim | block-logIndex | creator, protocol or sweep with pool id or quote, recipient, amount |
burn | block-logIndex | one row per FlywheelBurner.Burned: quoteIn (USDC, 6 decimals), harcBurned |
Quote amounts are stored in each pool's own raw quote units (10⁻⁶ USDC or satoshis). They are never converted to USD or summed across pools with different quotes; the UI labels every number by its quote and formats it with the quote's decimals. lastPrice is quoteAmount × 1e18 / tokenAmount of the most recent swap, in raw quote units per whole token.
Event handling worth knowing
LaunchedandRegisteredboth upsert the pool row, so the row exists whichever event the indexer sees first in a block.BlocksSetonly fires for pools with a non-zero mask; pools without blocks keepblockMask = 0.HookSwap.quoteAmountis gross (consumed by the pool plus fee) for buys and gross output before fee for sells, sovolumeQuoteis comparable across the two directions.PotWonresetspotBalanceto zero and records the winner;PotClaimeddoes not change the pool row.- Arc's sub-second blocks can share a timestamp, so every ordering key is
block-logIndex;timestampis kept for display only.
API
| Endpoint | Returns |
|---|---|
POST /graphql | The generated schema for every table with filters, ordering and cursors. The app queries pools, swaps, potEvents, claims and burns. |
GET /pools/24h | { items: [{ poolId, volume24h, trades24h }] } computed from swap rows in the last 24 hours. |
GET /burns | Recent buyback rows plus totalQuoteSpent and totalBurned for the $HARC page and the landing. |
The web app polls the indexer every 15 seconds on Discover and the token page. The landing's live data layer reads /pools/24h and /burns directly from the browser, which is why the indexer must allow the site's origin.
Running it
cd indexer && cp .env.example .env.local # fill from contracts/deployments/arc.json
npm run dev # or: npx ponder start --schema hookarc_arc
Required variables: PONDER_CHAIN_ID (5042), PONDER_RPC_URL, PONDER_HOOK_ADDRESS, PONDER_FACTORY_ADDRESS, PONDER_BURNER_ADDRESS, PONDER_START_BLOCK (the manifest's deployBlock) and DATABASE_SCHEMA. The web app finds the indexer through NEXT_PUBLIC_INDEXER_URL. The public Arc RPC limits eth_getLogs to about 10,000 blocks per request and rate-limits readily; a dedicated RPC (Alchemy, QuickNode, dRPC and Blockdaemon all serve Arc) is the sensible choice for a long backfill.
indexer/ponder.schema.tsthe five tablesindexer/src/index.tsevery event handlerindexer/src/api//pools/24h and /burnsweb/lib/indexer.tsthe queries the app runs