rebuild command apply
them, and @rail402.dev/x402-index implements them. Anyone can apply the same rules to the same ledgers
and the same registry and get the same rows, byte for byte.
The Explorer is an audit aid. Every row points at a transaction that anyone can read on chain, and the
rules below say exactly what was checked.
- Rules version:
2(RULES_VERSIONin the package). - Networks:
stellar:testnetandstellar:pubnet(CAIP-2). - Schemes:
exact(the canonical@x402/stellarscheme) andupto(the Rail402 settlement contract).
1. Input
One ledger’sLedgerCloseMeta (version 0, 1 or 2), as published by the public Stellar data lake or
returned by RPC getLedgers, or one transaction’s envelope, result and meta, as returned by RPC
getTransaction.
For every transaction the rules read:
Addresses are compared as base accounts: an
M… muxed account counts as its G… account, and the
muxed id is kept separately where it matters (the seller’s payToMuxedId).
Fee payer. The fee-bump fee source for a fee bump, otherwise the inner transaction’s source.
Events are bound to the result. For a successful candidate (section 7), the rules recompute
sha256(InvokeHostFunctionSuccessPreImage { returnValue, events }) from the meta’s return value and the
candidate operation’s contract events, and require it to equal the InvokeHostFunctionResult success
hash in the transaction’s result. A ledger header commits to its results, so once a ledger’s header,
transaction set and results are verified, the events the rules read are as consensus-bound as the
envelope. A mismatch means the meta was altered or belongs to another transaction: it raises an error
(events_unbound) and the indexer stops.
Unknown data stops the indexer. A transaction meta version other than 0 to 4, or a ledger that cannot
be decoded, raises an error. The indexer stops at that ledger instead of skipping it, so a protocol change
can never silently drop settlements. Every other unexpected value is a non-match, never an error: an
envelope whose accounts or addresses do not read is none, on a single transaction as in a ledger, and
arguments of the wrong type are excluded: malformed.
2. Outcomes
Each transaction gets exactly one outcome:3. R-EXACT
The canonical facilitator rebuilds the buyer’s signedtransfer invocation with its own account as the
transaction source and may wrap it in a fee bump. A fee bump is optional, so no check requires one. A
successful transaction is an exact settlement when all of these hold:
The amount in the row comes from the event. The payer is
from (a G… account or a C… contract); the
seller is to.
Why E7. The network authenticates an address authorization entry only when a contract calls
require_auth for that address. A token that is not a Stellar Asset Contract decides that for itself, and
whoever deploys it chooses its code: a token whose transfer never asks for from’s authorization
succeeds with an entry that nobody signed, naming any account, and emits whatever event it likes. E3, E4
and E6 read what the transaction and the token say; only E7 shows that the payer agreed. A failed attempt
(section 5) names no payer, so it is not checked against E7.
4. R-UPTO
Any contract can emit an event calledupto_settled, so the event alone never counts. A successful
transaction is an upto settlement when all of these hold:
The row takes the payer, the recipient (seller), the token, the amount, the cap (
maxAmount) and the
settlement authority (upto.authority) from the event. A zero-amount settlement is a row with amount 0;
a zero settlement that never reached the chain is not on-chain evidence and is not indexed.
R-UPTO-BETA
A concluded early design of the upto contract settled without anupto_settled event. Its contracts are
listed in the registry with rule R-UPTO-BETA, and only those contracts can match:
The payer, the seller, the token and the amount come from that
transfer event. The cap is the amount of
the approve event the same token emitted for the payer, when there is exactly one; otherwise it is empty.
5. Failed attempts
A failed transaction is afailed_attempt when its envelope alone passes the checks that do not need
events: E2 to E5 for exact, or U2 to U4 (B2 for a beta contract) for upto. The payer of a failed
upto is not known, so U7 cannot be checked. It records the hash, the scheme, the fee payer, the fee charged
and the result code (outer, inner and operation codes joined with :). Failed attempts are shown for
transparency and never counted as payments.
6. Attribution
6.1 Tiers
The rules check the fee payer, then the inner transaction’s source, then the operation’s source against the facilitator registry for the transaction’s network, epoch and ledger. The first account that the registry lists for that ledger decides the facilitator. A registry account counts only inside its own ledger range, so adding a facilitator later attributes past ledgers the same way on every rebuild, whatever order things were discovered in.
Payments where the payer is also the seller (self-payments) are kept out of the headline totals and
counted separately.
6.2 The registry
The registry is a JSON file per network inpackages/x402-index/registry/. It changes only by a reviewed
commit, and every change bumps its registryVersion, which every row records.
- An entry has an address, a role (
sponsor,channel,upto-authority,fee-bumporrelayer), an epoch and an inclusive ledger range (validFromLedger,validToLedger, open-ended whennull). - Every entry carries at least one piece of public evidence: its URL, the SHA-256 of the archived copy and the archived file, committed next to the registry. For a statement on a third-party page (an issue or a comment), the archived file is an extract: the statement, its URL, date and author, and the SHA-256 of the full page it was quoted from.
- One address belongs to at most one facilitator in any ledger.
- An upto contract entry carries the contract, its WASM SHA-256, the rule (
R-UPTOorR-UPTO-BETA) and a ledger range. The WASM hash is checked on chain before an entry is added. - When a sponsor rotates, the old range is closed and a new range is added.
6.3 Testnet resets
The testnet is reset from time to time, and ledger numbers start again. Every row is keyed by(network, epoch, txHash). For stellar:pubnet the epoch is pubnet; for stellar:testnet it is the
date of the reset, from the registry’s epochs list.
7. Exclusions
Successful transactions that are close to an x402 settlement but miss one check are counted per reason, so the totals can be audited. A transaction is a candidate when its first contract call callstransfer with three arguments, or calls settle on a registry contract or on a contract that itself
emitted an upto_settled event in that call. Checks run in the order of the tables above; the first failure decides
the reason.
8. Assets
The asset of a row is the token contract (asset.contract). A Stellar Asset Contract’s transfer event
names its asset in the fourth topic (CODE:ISSUER, or native). The row shows that code and issuer as
verified only when the contract id of that asset on this network
(Asset(code, issuer).contractId(passphrase)) equals the contract that emitted the event. Otherwise the
asset is a custom SEP-41 token: code and issuer are empty and verified is false, whatever the
event claims. A token can name itself after any asset; only the contract id proves it. For exact, a
verified asset also proves the payer’s authorization (E7).
9. Fee sponsorship
fee.payer is the fee payer, fee.chargedStroops the fee charged, fee.bump whether a fee bump wrapped
the transaction, and fee.sponsored is true when the fee payer is not the payer. E5 and U7 make every
settlement row sponsored.
10. Determinism
The same ledgers, rules version and registry version give the same rows in the same order (ledger, thentxIndex), with no clock, randomness or network access in the classifier.
11. Versioning
- A change that can add, remove or alter a row or an exclusion count is a new rules version. The old version stays documented here, and published reports name the version they used.
- A change that only adds registry entries or closes ranges is a new registry version, not a new rules version. Rebuilding with the new registry re-attributes history deterministically. The Explorer’s indexer does the same with its stored rows when it starts: rows from an older registry version are re-attributed in place, and ledgers are indexed again when the upto contract entries or the rules version changed. Until then the Explorer’s pages, API and report leave out rows, totals and reports of another rules version, and a transaction of such a row is looked up and classified again.
- Fixes to wording that do not change any outcome keep the version.
- Version 2 added E7 and the exclusion
unproven_authorization: anexactrow names its payer only when consensus-bound data proves that the payer authorized the transfer. Version 1 took the payer from the entry’s fields and the token’s event alone, so its rows in tokens other than Stellar Asset Contracts can name an account that never signed. Version 1 is kept asrules/v1.mdin the package.
12. Known limits
- The
exactscheme leaves no x402 marker on chain. Any sponsored singletransferwith the payer’s address authorization has the same shape, which is why unattributed matches areshapedand never part of the headline. - Settlements that use
AddressWithDelegatescredentials, batch several operations, or settle through an upto contract that is not in the registry are not counted. - On testnet, the concluded beta’s
exactsettlements were submitted by short-lived test accounts that no public record names, so they areshaped. Itsuptosettlements are beta history through its contracts. - E7 sees only the transaction. A Stellar account’s signers, their weights and its thresholds are ledger
state, so in a token that is not a Stellar Asset Contract the rules accept the signature of the payer’s
own key without checking that key’s weight, and do not count an account that authorized only through
its other signers. A contract (
C…) payer of such a token is never proven: its own code decides its authorization. Both are excluded asunproven_authorization; in a Stellar Asset Contract they count as usual. - A token that never calls
require_authnever consumes the entry’s nonce either, so an entry that the payer really signed can be submitted again until its signature expiration ledger, and each copy is a row. - Attribution is as good as the registry’s evidence. A shared relayer’s rows can include transfers that were not x402 payments.
- A registry-scope result (§14) reads only the ledgers its discovery source lists. A settlement in which
no registry account pays the fee or moves funds, and which does not go through a registry upto contract,
is not found there, and its exclusion counts and unattributed
shapedrows cover only the ledgers read. A beta upto contract (R-UPTO-BETA) emits no event of its own and moves no funds itself, so it is found through the tokenapproveevent that names it as the spender, which each of its settlements carries in the same transaction; a beta settlement without that event in its transaction would be found only when another route lists its ledger. A full-scope result has none of these limits.
13. Stored fields
Event data and envelope fields are chosen by whoever submits a transaction, and a token contract can emit any bytes. A row is kept only when every field has its expected shape:G… and C… strkeys, 64-hex
hashes, decimal amounts that fit 39 digits, a u64 muxed id, an asset code of 1 to 12 letters and digits,
and text with no NUL character, valid as UTF-8 and at most 2,048 characters. A row that breaks one of
these is excluded as malformed, so a hostile transaction is counted and never stops an indexer.
14. Scope
The rules apply to one transaction at a time, so the rows of a ledger do not depend on which other ledgers are read. Every result names its scope:full: every ledger of a range, as therebuildcommand reads it with--fromand--to. The Explorer’s network-wide window is a full-scope result of a recent range, so it also counts settlements of facilitators outside the registry.registry: within a range, only the ledgers that hold transactions of the registry’s facilitator accounts (every role) and upto contracts, as a discovery source lists them; the manifest lists each ledger read with its hash. A listed ledger that holds no registry transaction adds only its own rows (never an attributed one), and is listed like any other. The Explorer indexes this scope, and therebuildcommand reproduces it from the listed ledgers alone (--ledgers-from), with no discovery source.
rail402,
rail402-beta and facilitator rows of a registry-scope result equal those of a full-scope result,
except for a settlement in which a registry account took part only as a transaction or operation source,
without paying the fee or moving funds (§12); a full-scope rebuild of the range shows any such case.