Indexer & explorer
Surfacing the pool's public state without deanonymizing anyone.
Clients should not each pull the whole Merkle tree from the node on every sync, and a shielded pool wants a public explorer. The indexer streams chain events into Postgres; the explorer renders them.
Indexer
A service that polls the node's event log and writes to Postgres (Neon in
production, an embedded Postgres locally). It ingests, incrementally and
idempotently (natural keys + ON CONFLICT DO NOTHING, gated on a persisted
last-seq cursor):
- Pool ops — from
getPoolEvents(sinceSeq): per-op kind, public amounts, fee, recipient/relayer, root, output commitments (with leaf index), and nullifiers. - Bridge events — deposits and withdrawals.
- Chain height — slot and transaction count snapshots.
Its schema is shaped so the SDK can rebuild a user's Merkle path from
commitments (ordered by leaf index) and check spent-state from nullifiers —
replacing direct getPoolState pulls from the node. The
SDK's IndexerDataSource reads this API; the node-direct path
remains a fallback, and the two are cross-checked to produce identical roots and
paths.
Explorer — public, opaque, links nothing
The explorer is read-only and shows only the public column from What's public vs private:
- Overview — TVL, tree size, nullifiers spent, shield/unshield/relayed counts, and a recent-activity feed of opaque events (a private transfer shows "amount hidden").
- Pool — current root, tree fill, recent-roots window, relayer activity.
- Bridge — deposits and withdrawals (public by nature).
- Search — look up a commitment, nullifier, root, slot, or op and get that single opaque record.
The privacy constraint is structural
Search returns a single record and links nothing. There is deliberately no query for "which commitment did this nullifier spend" or "which unshield matches this shield." The indexer stores nothing that reconstructs those links, so the explorer cannot show them even in principle. This restraint is itself a demonstration of the privacy property.