Blog

No mock data: how every number on the site gets a block number

We deleted every seed file, every growth engine and every random walk. What is left is an indexer with two rules it will not bend, a price chart that goes flat when nothing trades, and a volume figure that is honestly a count.

September 4, 2026 8 min readdataengineering

A launchpad frontend has a strong incentive to look alive. Empty lists and flat charts do not convert. The industry answer is seed data: plausible tokens, a random-walk price series, a “volume” figure with a dollar sign in front of it. We did this too, early on. Then we deleted all of it — the commit is b9a4c4a, “index pons markets into a backend data layer, delete all mock data” — and wrote down the rules that would stop it coming back.

The two rules

  1. Every number written to the database comes from a chain read, and every row keeps the block and transaction it came from. There is no growth curve, no jitter, no backfill of plausible-looking history. If the API serves a figure, you can check it against the explorer.
  2. A price point is either a real Swap log or a real slot0 read. They are labelled swap and sample, the former carries a txHash, and they are never merged or interpolated. When nothing trades, the chart goes flat, because nothing traded.

What the indexer does with them

scripts/pons-indexer.ts is a long-running worker. It sweeps eth_getLogs for launch events on every factory it knows, and for each new token reads name, symbol, decimals, logo, description and socials from the token contract — pons tokens are self-describing, and so are ours — plus the canonical pool, the launch record, and the graduation status from the factory. The row that lands in markets has block, blockHash and hash from the launch transaction.

Every pass after that refreshes each market’s snapshot: slot0 → price, pool balances → liquidity, graduationStatus → progress, and a count of Swap logs in the last 24 hours → volume. Reserves are re-read rather than adjusted by event deltas, so a missed log costs one stale poll rather than permanent drift.

Things we deliberately named honestly

  • live.volume is the count of trades in 24 hours. We did not compute notional volume, so we do not display it. A number called “volume” that is actually a count is less misleading than a dollar amount that is actually a guess.
  • live.stale flips true when a snapshot is older than five minutes. The UI shows the old number and says it is old, rather than pretending it is current.
  • Liquidity USD values both sides of the pool at the current price. The usual shortcut — double the WETH side — is only fair for a balanced constant-product pool, and a V3 position is not balanced.
  • /api/config returns simulated: false and dataSource: "indexed-onchain". If anyone ever adds a simulated mode, that flag is where a banner has to be gated. It is a promise encoded in the API shape.

The polling endpoint

The browser does not talk to an RPC to render anything. It polls GET /api/data?kind=updates, which returns three monotonic counters and answers 304 Not Modified against an ETag. Only when a counter moves does the client fetch the heavier markets payload. Cheap polling, expensive fetches only on real change.

The cost of honesty

On a public RPC the first full sweep takes tens of minutes, because each new token costs about eight paced contract reads and Robinhood’s endpoint rate-limits hard. A fresh install shows an empty list until the indexer has run. We think an empty list that is true beats a full one that is not.