Indexer: deploy & scanning in production

The @p15/indexer is a dev/ops convenience: it polls the one fixed announce anchor, extracts every encrypted p15 memo, and serves them over HTTP so wallets scan their inbox in a single request instead of N getTransaction RPC calls. It never sees plaintext — ciphertext + the public 1-byte view tag only. Decryption stays client-side with the recipient's scan key.

API contract

Types live in indexer/src/types.ts (the source of truth, imported by the handlers):

  • GET /announcements?before=<sig>&until=<sig>&limit=200AnnouncementsPage ({ items: AnnouncementRow[], nextBefore: string | null }), newest-first.
  • GET /healthHealthResponse ({ ok, rows, rpc }).

The dapp/SDK scanner consumes this via ScanOptions.indexerUrl (NEXT_PUBLIC_INDEXER_URL).

Env

vardefaultmeaning
SOLANA_RPChttps://api.devnet.solana.comcluster to poll
PORT8787HTTP port
DATA_FILE./announcements.jsonJSON persistence path
POLL_MS10000poll interval (ms)

Docker

@p15/stealth is a sibling consumed via file:, which Docker can't see outside the build context — so build from the outer repo root, and build the stealth dist first:

Shell
cd protocol-15
(cd packages/stealth && npm ci && npm run build)
docker build -f indexer/Dockerfile -t p15-indexer .
docker run -p 8787:8787 -v p15-index-data:/data \
  -e SOLANA_RPC=https://api.devnet.solana.com p15-indexer
curl localhost:8787/health

The JSON store persists on the /data volume, so restarts resume the backfill instead of starting over.

Deploy (Railway / Fly)

  • Railway: point at the repo, set the Dockerfile path to indexer/Dockerfile and the build context to the repo root. Add a persistent volume mounted at /data. Set SOLANA_RPC (a paid RPC is strongly recommended — the public devnet RPC 429s hard during the initial backfill; the poller already retries with exponential backoff but a real endpoint is far faster).
  • Fly: fly launch --dockerfile indexer/Dockerfile from the repo root, add a volume for /data, set the same env. Healthcheck GET /health.

RPC fallback (no indexer)

The scanner works without an indexer: when indexerUrl is unset, scanAnnouncementsPage RPC-scans the anchor directly (getSignaturesForAddress + per-tx getTransaction). Tradeoff:

  • With indexer: one HTTP request per page; view tags pre-extracted. Best for wallets with shared infra.
  • Without: N getTransaction calls per page, rate-limit sensitive on public RPC. Fine for low-volume or self-hosted single-user scanning.

Use the indexer when many clients scan the same anchor or when on a public RPC; skip it for one-off local scans.