How it finds payments
The indexer does not read every ledger of the network. It asks a discovery source, the Lumeus Activity API, for the history of every account in the registry (sponsors, fee payers, channel accounts, settlement authorities, relayers, each within the ledger range the registry gives it) and of every registered upto contract. A contract whose settlements emit an event (R-UPTO) is read
through its own activity. The concluded beta’s contracts (R-UPTO-BETA) emit no event and move no funds
themselves, so their activity is empty; each of their settlements grants the contract an allowance in the
same transaction, and discovery finds them through Lumeus topic search for the token approve events that
name the contract as the spender. The ledgers those histories name are the candidates. For each
candidate the indexer reads the whole ledger (envelopes, results and metas): from Lumeus RPC getLedgers
while the ledger is inside the RPC’s retention of about seven days, otherwise from the public Stellar data
lake. Every ledger is checked against its own hash, transaction-set hash and result hash, and, where
configured (always on mainnet), against the ledger hash a Horizon with full history reports, before its
transactions are classified and stored. Ledgers are fetched ahead of use only up to
EXPLORER_BYTES_IN_FLIGHT_MIB (64 MiB) of raw bytes and decoded one at a time, so a busy stretch of
ledgers never has to fit in memory at once.
Discovery only chooses which ledgers to read; it never supplies a row, so it cannot create a settlement
that is not on chain. A ledger it lists that holds no registry transaction adds that ledger’s own real
rows: unattributed look-alikes, failed attempts and exclusion counts, never an attributed settlement. The
manifest lists every such ledger, the report counts them (ledgersWithoutAttributedRow), and each route
adds at most EXPLORER_MAX_CANDIDATES_PER_ROUND new candidates per round, so a source that lists every
ledger cannot make one round read (or, on mainnet, pay for) an unbounded number of ledgers. A ledger it
leaves out shows up in a full scan of the same range from the data lake, which anyone can run (see
Rebuilding the report).
Each route keeps a checkpoint (the last ledger discovery walked) stored with the candidates it found, page
by page, so a failure loses at most the page under way and restarts lose and repeat nothing. A route keeps
its registry bounds only: a bounded run (EXPLORER_TO_LEDGER) stops its walks at its last ledger, and a later
run resumes there. When Lumeus answers under a new index generation or decoder, the route is walked again
from its first ledger; on testnet, a new generation whose head is far behind the indexed ledgers is a
network reset and stops the indexer until the new epoch is registered (a head that is only behind is
counted, never a stop). The live tail polls the fee-paying routes (sponsors, fee bumps, relayers, upto
contracts) every round and, once they reach the head, channel and settlement-authority routes every
EXPLORER_SLOW_ROUTE_ROUNDS rounds. A candidate whose bytes or anchor are not available yet waits and is
tried again later without holding back newer ones; the report still stops before it.
Pages
Mainnet pages have no prefix; testnet pages live under/testnet. An Explorer that serves testnet only
redirects / to /testnet.
- Both the outer hash of a fee bump and its inner hash open the payment. An upper-case hash redirects (301) to lower case.
/public/…,/pubnet/…and/mainnet/…redirect (301) to the unprefixed path on the same site;/methodologyredirects to this page.- An
M…muxed address opens its base account with?muxed=<id>, which lists only payments received by that id; the totals on that page cover all muxed ids of the base account. - An invalid hash or address, or one nobody knows, answers 404 with the code
explorer_not_found. - Search takes a transaction hash or its first characters, a G…, C… or M… address or its first or last characters, an endpoint URL, an MCP tool name, a facilitator or words from a listing. An exact hash or address opens its page; anything else lists the matches by kind, ten a page. The search box on the overview shows the first matches as you type.
- Lists show ten rows a page; the Bazaar’s filters, sorting and views are links, so every page works without JavaScript.
Three kinds of data
Every page keeps three sources apart and labels them:
The web role reads the Bazaar over HTTP only, never the facilitator’s database. A payment shows
first-party “paid for” when the Bazaar links its transaction to a public listing (Rail402 settled it
and cataloged the resource) and that listing’s payTo is the account the payment paid on chain (a muxed
payTo counts as its base account); a link to anyone else is never shown. Otherwise it is not
cataloged: the Bazaar links no public listing to it, because it was not cataloged, its seller keeps usage
private, or cataloging has not finished. A listing whose seller keeps usage private says “usage private”.
On a network whose Bazaar publishes usage counts only (
PUBNET_BAZAAR_USAGE_DETAIL=counts, pubnet’s
default), the Bazaar links no payment to a listing, so payments show no “paid for” and a listing’s page
shows its payment counts without the payments or their payers.
All Bazaar calls of one page share one deadline of 3 seconds: when the Bazaar does not answer in time,
the page renders from the ledger alone and says so. A payment page is cached for an hour only once its
link is known, or once the Bazaar answered for a payment older than 10 minutes; until then, and whenever
the Bazaar did not answer, for 15 seconds. A Bazaar answer is shared by every visitor for a minute, and a
failure for 5 seconds; a call cut short by the page’s deadline, or refused for the visitor’s own budget
(429, or 503 with Retry-After), is never kept for the next visitor.
With a Bazaar access key (EXPLORER_<NETWORK>_BAZAAR_KEY_FILE, a discovery-internal key of the
facilitator), the Explorer sends the key and an HMAC of each visitor’s rate-limit key with every Bazaar
call, so each visitor draws on its own discovery budget instead of all sharing the Explorer’s address.
The visitor’s address itself is never sent; the identifier is a pseudonym, not anonymity, since the
facilitator’s operator holds the key and could compute it for every IPv4 address. The same goes for the Bazaar’s limit on concurrent discovery
requests per client (DISCOVERY_MAX_IN_FLIGHT_PER_CLIENT, 8 by default; a page makes up to four Bazaar
calls at once): with a key whose record sets forwardedClientRequestsPerMinute, each visitor has it to
itself, and without a key, every visitor shares the Explorer address’s 8.
Payments, sellers, services and listings
- Payments lists attributed payments newest first, with tabs per registry facilitator and a separate tab for unattributed x402-shaped payments (a lower bound, never mixed in), and filters by scheme, asset and time (24 hours, 7 days, 30 days). Pages are keyset-paginated; the CSV download holds the newest 500 payments of the current filters, with the Bazaar listing each one paid for when known.
- Account is a seller page when the address is the payTo of Bazaar listings: its listings with the payments and payers the Bazaar linked to each, a “not linked to a public listing” remainder (the payments the ledger shows it received in the current epoch minus those the Bazaar linked to its public listings over all its history, at zero at least; it also covers private, withdrawn and unlisted usage), upto payments as charged against authorized, and live holdings.
- Services groups the Bazaar’s listings by seller, ranked by payments received on the ledger.
- Listing (
/resource/<id>) shows a listing’s payment requirements, its input and output example, the payments linked to it that paid its payTo on chain, its public version history (the first 25 versions), and how to call it with the CLI, TypeScript, the MCP server or the Playground. A listing the Bazaar does not publish (pending or withdrawn) shows only its state, its history and a warning: no price, no seller and no way to call it. - Facilitators lists the registry’s facilitators with their share of attributed payments; each facilitator page shows its registry accounts with evidence and its current sponsor’s live XLM.
A transaction page
For a settlement the page shows:
A failed transaction with the shape of a settlement is shown as a failed attempt and never counted.
Hashes from the CLI, the SDK and the Playground
The SDK, the CLI, rail402-mcp and the Playground link receipts here by default, by the hash the settle response returns (a fee bump’s outer hash); the inner hash opens the same page. A payment opens seconds after it settles, before the indexer reaches its ledger: a hash the index does not hold is read from the network’s RPC (getTransaction, which keeps about the last seven days; Lumeus
RPC when the operator configures a Lumeus key) and classified with the same rules. Such a page is labelled Live lookup, not yet indexed and is never
cached. A transaction that exists but is not an x402 settlement says which check it missed. The RPC’s
answer must be the transaction asked for: its envelope’s hash on the network (or its inner hash, for a
fee bump) must equal the requested hash, or the answer is 503 explorer_lookup_unavailable.
Live lookups are bounded: the hash syntax is checked first, unknown hashes are remembered for 20 seconds
and found ones for 60 seconds, concurrent lookups of one hash share one RPC call, and each client (an
IPv4 address or an IPv6 /64) gets 6 lookups a minute per network within a budget of 60 a minute per
network, a quarter of which is kept for clients making their first lookup in a minute. Over budget, the
answer is 429 explorer_lookup_busy with Retry-After; many clients together can still use the budget
up for a minute.
When the rules version changes, the indexer reads every ledger of its epoch again when it starts. Until
it has, the pages, the API and the report leave out rows, account totals and reports classified under
other rules: such a row could name a payer the current rules do not prove. Its transaction is looked up
live and classified again, like one the index does not hold.
The Ecosystem Report
Each network’s report covers the indexed ledger range and keeps every kind of activity apart:- Settled by the hosted Rail402 service: payments whose fee payer or source is an account of the hosted Rail402 facilitator in that ledger. Self-hosted instances are not counted here. Network-wide (sampled window), shown next to it when the operator runs the network window job: every ledger of one recent window (for example the last 24 hours), read in full from the public data lake under the same rules, by tier: the hosted Rail402 service, other registered facilitators, and unregistered or unattributed x402-shaped payments. It is the only block that counts facilitators outside the registry. The block states its ledgers and close times, so it is a sample of that window, never lifetime totals (see the network-wide window).
- Registered facilitators (Rail402 included): every settlement attributed to a facilitator in the
public registry, by facilitator, scheme, asset and day, with distinct payers and sellers. This is not
every x402 payment on the network: the index reads only the ledgers that hold transactions of
registered facilitator accounts and upto contracts, so a facilitator outside the registry is never
counted here. The report’s
headlineScopefield says so (registered-facilitators). - Unattributed x402-shaped: transactions with the exact shape of a settlement that no registered account submitted. Smart-wallet relays and test scripts look the same, so they are shown apart and never added to the totals above. Self-payments are counted apart too. The report reads only registry ledgers, so it counts upto look-alikes through registry contracts but not exact-scheme look-alikes elsewhere on the network: a count over every ledger is available only from a full-scan rebuild, whose command the section prints. What the report shows there is a lower bound.
- Concluded Rail402 beta: beta transactions found on chain in the range. The beta ended on
22 October 2026; its settlement contracts cannot be changed and anyone can still call them, so the
registry counts calls through them only up to the beta’s end, whoever submitted them. On testnet, the
beta’s
exactpayments were submitted by short-lived test accounts that no public record names, so they appear as unattributed x402-shaped matches, not as beta history. The beta’s own explorer totals are not reproducible and are not republished. - Methodology and rebuild: the rules version, the registry version and SHA-256, the manifest
SHA-256 and the exact
pnpm explorer:rebuildcommand for the range. - Service availability: a link to where the operator reports it; the report itself only counts payments settled on chain.
Rebuilding the report
The report’s totals are computed by the same code as the rebuild command. Itsmanifest.json lists every ledger it read with its hash, and is byte-identical to the one the command
writes when it rebuilds those ledgers from the public data lake, with no discovery source:
The network-wide window
The report’s other numbers come from registry discovery, which never reads a facilitator outside the registry. The network-wide block measures the whole network instead, over a bounded window: a separate job rebuilds the newestEXPLORER_NETWORK_WINDOW_HOURS (1 to 168) of ledgers with the rebuild command’s
own code (scope full), from the public data lake, and publishes the window’s tier totals and manifest.
Each tier excludes self-payments; the beta and failed attempts are counted apart. Unregistered or
unattributed rows have the exact shape of an x402 settlement with no registry account behind them:
facilitators outside the registry, smart-wallet relays and test scripts, and also transfers that only
look like x402 payments, since the exact scheme leaves no marker on chain.
The block prints the command that rebuilds its window, and the window’s manifest is at
/api/v1/<network>/network-window/manifest:
Snapshots
pnpm --filter @rail402.dev/explorer snapshot --network testnet --out ./snapshot exports a report as
static files: report.html (standalone, no script), report.json, and the rebuild’s
settlements.ndjson, failed_attempts.ndjson, aggregates.json and manifest.json, plus
network-window-manifest.json when a network-wide window is published.
Security
-
Fonts (Space Grotesk, Archivo, JetBrains Mono) are served from the Explorer’s own origin under the SIL
Open Font License, whose texts are at
/fonts/OFL-*.txt; no third-party origin is contacted. - Bazaar answers are untrusted text: each is size-capped, checked field by field, rendered escaped, and an asset is named only when the index verified its contract as a Stellar Asset Contract.
- A verified asset’s bare code is shown only for an allow-listed contract (Circle’s USDC on each network); any other, a USDC of another issuer included, is shown with a short issuer, and “USDC settled” counts the allow-listed contract only.
- Pages are rendered by escaping JSX only; on-chain strings never become markup. Control, zero-width, bidirectional, filler and tag characters are stripped before display.
-
Every response carries
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'andCache-Control: … no-transform. The only script is the Explorer’s own (copy buttons, tabs and filter forms); there is no third-party script. -
Indexed settlements are cached for an hour (never as immutable) when their row carries the current
rules and registry versions, with the row’s versions in the ETag; a row still awaiting re-attribution,
lists and the report are cached briefly; live lookups, not-found answers and errors are
no-store. -
Responses carry
Strict-Transport-Security. -
The web role connects as a read-only database role with a statement timeout and refuses to start
otherwise; it cancels any statement that outlives 2 seconds. Waiting more than 2 seconds for a database
connection, or more than 32 requests at once, answers
503 explorer_unavailablewithRetry-After. Lists are keyset-paginated with at most 100 items, account totals are read from totals the indexer maintains (at most 100 assets per side, verified Stellar assets first, then by payments, with a notice of how many more there are), each client is rate-limited, and/healthchecks the database at most once every two seconds whoever asks.
Run your own
The Explorer runs from one image with two roles. Seeapps/explorer/README.md for every setting and
the database roles, and the read API for the JSON endpoints.
The indexer needs a Lumeus server key for the network it indexes, in a file (EXPLORER_LUMEUS_KEY_FILE).
Give the internet-facing web role its own key for live lookups (EXPLORER_<NETWORK>_LUMEUS_KEY_FILE): a
read-access server key, or a publishable key (lum_pub_…) whose allowed origins include the Explorer’s
public origin, so a compromise of the web role never exposes the indexer’s key.