Skip to main content
These rules decide which Stellar transactions are x402 settlements, which facilitator settled them and how sure that attribution is. The Rail402 Explorer, its Ecosystem Report and the 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_VERSION in the package).
  • Networks: stellar:testnet and stellar:pubnet (CAIP-2).
  • Schemes: exact (the canonical @x402/stellar scheme) and upto (the Rail402 settlement contract).

1. Input

One ledger’s LedgerCloseMeta (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 signed transfer 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 called upto_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 an upto_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 a failed_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 in packages/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-bump or relayer), an epoch and an inclusive ledger range (validFromLedger, validToLedger, open-ended when null).
  • 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-UPTO or R-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 calls transfer 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, then txIndex), 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: an exact row 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 as rules/v1.md in the package.

12. Known limits

  • The exact scheme leaves no x402 marker on chain. Any sponsored single transfer with the payer’s address authorization has the same shape, which is why unattributed matches are shaped and never part of the headline.
  • Settlements that use AddressWithDelegates credentials, batch several operations, or settle through an upto contract that is not in the registry are not counted.
  • On testnet, the concluded beta’s exact settlements were submitted by short-lived test accounts that no public record names, so they are shaped. Its upto settlements 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 as unproven_authorization; in a Stellar Asset Contract they count as usual.
  • A token that never calls require_auth never 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 shaped rows 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 token approve event 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 the rebuild command reads it with --from and --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 the rebuild command reproduces it from the listed ledgers alone (--ledgers-from), with no discovery source.
Each ledger is read in full and verified whatever selected it. For the same range, the 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.