Skip to main content
If you run an indexer, wallet backend or anything else that turns ledgers into rows, this guide adds two checks to your pipeline: They’re independent. Start with whichever fits. Lint is a five-line change; replay takes an afternoon the first time, then catches the bugs lint can’t see.

Lint

The least setup. It installs Go from your go.mod, runs the linter, and annotates findings inline on the PR.
.github/workflows/istravex.yml
The job fails on any unsuppressed finding. Before you turn it on, run it once locally and go through what it reports. The rules are tuned to stay quiet on correct code (one finding across all of wallet-backend), but when one fires on code you’ve reviewed and know is fine, suppress it with a reason:
Each rule’s page (IX001, IX002, IX003) explains what it looks for and what the fix usually is.

Replay

The linter reads code. Replay checks behavior: feed your indexer the corpus’s real ledgers, then ask it what it stored.

1. Point your indexer at the mock RPC

The mock RPC serves the corpus’s canonical ledgers through the same getLedgers interface as Stellar RPC, so your indexer runs unmodified. Only the URL and passphrase change: Most indexers are configured for one network at a time. Start with pubnet, which covers the two patterns with the most real-world bugs (fee-bumps and muxed accounts), and add the others as separate runs.
Only the fixture ledgers exist, so the range is sparse: mainnet has ledger 65,474,279 and 65,474,309 and nothing in between. If your ingestion insists on contiguous ledgers, request each fixture’s ledger on its own (startLedger = the fixture’s ledger, limit = 1). That’s exactly what the wallet-backend pilot does. See the ledger range is sparse.
The ledger numbers come from the fixture list:

2. Write down what you stored

After ingestion, look up each fixture’s transaction in your store and write what you find to observed.json, using the same keys as the fixture’s expected_indexed:
observed.json
Use null, or leave the key out, when your indexer stored nothing for a fixture. This is the only part that’s specific to your codebase, usually a short query or a call into your repository layer. For a complete real example, see the wallet-backend adapter: it runs wallet-backend’s own ProcessLedger against the mock RPC and prints what it produced.
Write 64-bit values as strings if you can. The scorer below compares numbers by their exact source text, so 1764988090084626463 written as a bare number still scores correctly. But if your adapter is in JavaScript and the value passed through Number on the way out, the damage is already done. That’s the bug this fixture exists to catch.

3. Score it

Save this next to your adapter. It fetches each fixture’s expectations, compares field by field, prints a verdict per fixture and exits non-zero unless every one is correct.
istravex-score.mjs
It needs Node 22 or later (for the source argument to the JSON.parse reviver). A failing run looks like this:
The verdicts mean the same as everywhere else in Istravex: correct, incorrect (stored, but a field differs) and dropped (the ledger supports a record you didn’t store).

4. Wire it into CI

.github/workflows/istravex.yml
The hosted RPC allows 300 requests a minute, which is plenty for a CI run over a handful of ledgers. If you’d rather have no network dependency at all, run the mock RPC inside the job: clone Istravex at a tag, run npm ci, and start npm run rpc -- --network pubnet --port 8000 in the background, then point RPC_URL at http://localhost:8000.

When replay fails

Every fixture maps to a rule in the hardening guide that explains the mechanism and the fix: If you believe the fixture is wrong rather than your indexer, open an issue. Expectations come from independent sources, but that doesn’t make them infallible.