> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rail402.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Indexing rules v2

> How the Rail402 Explorer decides which Stellar transactions are x402 settlements, which facilitator settled them, and how sure that attribution is.

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:

| Field | Source |
| - | - |
| Transaction hash | The network hash of the envelope as submitted. For a fee bump this is the outer hash; the inner transaction's hash is kept as `innerHash`. |
| Position | The ledger sequence and the 1-based application order inside the ledger (`txIndex`). |
| Close time | The ledger header's close time, as ISO 8601 UTC. |
| Outcome | The transaction result: `txSuccess` or `txFeeBumpInnerSuccess` is a success, anything else a failure. |
| Fee charged | The result's `feeCharged`, in stroops. |
| Events | The contract events of the transaction meta: the per-operation events of meta version 4, the Soroban events of meta version 3. System, diagnostic and fee events are ignored. |

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:

| Outcome | Meaning | Counted as a payment |
| - | - | - |
| `settlement` | It satisfies R-EXACT, R-UPTO or R-UPTO-BETA. One row per transaction: both schemes settle with one operation. | Yes, by tier (section 6) |
| `failed_attempt` | It failed on chain, but its envelope has the shape of an x402 settlement (section 5). | Never |
| `excluded` | It almost has the shape of an x402 settlement and misses one named check (section 7). Only a count per reason is kept. | Never |
| `none` | Anything else. | Never |

## 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:

| Check | Rule |
| - | - |
| E1 | The transaction succeeded. |
| E2 | The inner transaction has exactly one operation: `invokeHostFunction` invoking a contract's `transfer` with exactly three arguments `(from: Address, to: Address, amount: i128)` and `amount > 0`. |
| E3 | The operation carries exactly one authorization entry. Its credentials are `Address` or `AddressV2`, and name `from`. This reads the entry's fields only; E7 decides whether the entry proves anything. |
| E4 | That entry's root invocation is the same contract's `transfer` with the same arguments, and it has no sub-invocations. |
| E5 | `from` is neither the fee payer, the inner transaction's source nor the operation's source. Someone other than the payer paid the fee: the fee was sponsored. |
| E6 | The operation emitted exactly one `transfer` event from that contract. Its topics are `[transfer, from, to]` or `[transfer, from, to, asset]`, and its amount (an `i128`, or the `amount` field of a muxed transfer's map) equals the argument. A muxed `to` matches by base account and muxed id, a u64; a `to_muxed_id` in the text or hash memo form (a string or bytes) never matches. |
| E7 | Consensus-bound data proves that `from` authorized this transfer, in one of two ways. The asset is verified (section 8): the contract is the Stellar Asset Contract of the asset its event names, whose `transfer` always calls `from.require_auth()`, so the network authenticated the entry before the transaction could succeed. Or `from` is a `G…` account that signed the entry with its own key, as the network checks a Stellar account's signature: the transaction's ledger is not after the entry's signature expiration ledger; the signature is a vector of 1 to 20 `{ public_key, signature }` maps with strictly ascending public keys; one of the keys is `from`'s own; and its signature is a valid ed25519 signature of the entry's payload. The other signatures say nothing about `from` and are not verified. The payload is the SHA-256 of the entry's `HashIdPreimage` on this network: `ENVELOPE_TYPE_SOROBAN_AUTHORIZATION` (network id, nonce, signature expiration ledger and root invocation) for `Address` credentials, `ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS` (the same and the address) for `AddressV2`. |

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:

| Check | Rule |
| - | - |
| U1 | The transaction succeeded. |
| U2 | The inner transaction has exactly one operation: `invokeHostFunction` invoking `settle` on a contract. |
| U3 | That contract is in the upto registry for this network, epoch and ledger, with rule `R-UPTO` (section 6.2). |
| U4 | If the transaction meta shows the contract's instance, the instance runs the WASM whose SHA-256 the registry lists. |
| U5 | The operation emitted exactly one `upto_settled` event from that contract, with topics `[upto_settled, payer: Address, recipient: Address]` and data `{ token: Address, amount: i128, max_amount: i128, facilitator: Address }`, `0 ≤ amount ≤ max_amount`. `upto_settled` events from other contracts are ignored. The payer, recipient, token, amount, cap and facilitator equal the signed `settle(payer, terms, amount)` arguments (`terms.recipient`, `terms.token`, `terms.max_amount`, `terms.facilitator`). |
| U6 | When `amount > 0`: the operation emitted exactly one `transfer` event from `token`, from the payer to the recipient (muxed id included, as in E6), for `amount`. When `amount = 0`: no `transfer` event from `token` leaves the payer. |
| U7 | The payer is neither the fee payer, the inner transaction's source nor the operation's source. |

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:

| Check | Rule |
| - | - |
| B1 | The transaction succeeded. |
| B2 | The inner transaction has exactly one operation: `invokeHostFunction` invoking `settle` on a registry contract with rule `R-UPTO-BETA`. |
| B3 | The operation emitted exactly one `transfer` event, from a token contract, whose `from` is neither the fee payer, the inner transaction's source nor the operation's source, and whose `to_muxed_id`, if any, is a u64. |

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.

| Tier | When | In the attributed x402 totals |
| - | - | - |
| `rail402` | The account belongs to the hosted Rail402 service in that ledger range (sponsor, channel accounts, upto settlement authority). Self-hosted Rail402 instances are not this tier. | Yes, and also shown on their own as Rail402-settled |
| `rail402-beta` | The account belongs to the concluded Rail402 beta, or the row settled through a contract the registry lists with rule `R-UPTO-BETA` (whoever submitted it). | No: shown as concluded beta history |
| `facilitator` | The account belongs to another facilitator, backed by public evidence (its `/supported` signers or its operator's public statement). A facilitator marked `sharedRelayer` also relays transactions that are not x402 payments: its rows carry `attribution.sharedRelayer: true`, and totals show them on their own line. | Yes |
| `shaped` | No registry account matched. Smart-wallet relays, test scripts and unknown facilitators produce the same shape. | No: shown on a separate, labelled line |

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.

| Reason | Why |
| - | - |
| `multi_operation` | More than one operation (E2, U2, B2). The canonical scheme never batches. |
| `multi_auth` | Not exactly one authorization entry (E3). |
| `delegated_credentials` | `AddressWithDelegates` credentials (E3). The canonical facilitator cannot verify them today. |
| `sub_invocations` | The authorized invocation has sub-invocations (E4). |
| `self_paid_fee` | The payer paid its own fee or signed as a source account (E3 source-account credentials, E5, U7, B3). |
| `transfer_event_mismatch` | The events do not prove the payment, or differ from the signed arguments (E6, U5, U6, B3). |
| `unproven_authorization` | Nothing consensus-bound proves that the payer authorized the transfer: the token is not a verified Stellar Asset Contract, and the entry carries no valid signature by the payer's own key (E7). |
| `upto_unregistered_contract` | An `upto_settled` event from the called contract, which the registry does not list (U3). |
| `upto_wasm_mismatch` | A registry contract running other WASM (U4). |
| `malformed` | Undecodable XDR, arguments of the wrong type, a non-positive amount, an authorization that does not match the invocation (E2, E3, E4), or a row field that could not be stored exactly as it is (section 13). |

## 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.