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

# Threat model

> What can go wrong when Rail402 verifies, settles and catalogs x402 payments on Stellar, what Stellar changes about each threat, and the code and tests that answer it.

This page covers the Rail402 facilitator (`/verify`, `/settle`, `/supported`), the Bazaar catalog and search,
the `upto` settlement contract, the buyer side: the SDK, the `rail402` CLI and the `rail402-mcp` server
that let agents pay, and the [Playground](/playground/overview), a page and server that pay on the testnet
from a browser. For each threat it says what Stellar makes different, which mitigations answer it, where
they live in the code, which tests prove them, and what risk remains.

Every threat is an entry in [`docs/security/threats.json`](https://github.com/tolgayayci/rail402/blob/main/docs/security/threats.json).
The tables below are generated from it, and a unit test fails when a cited source file no longer contains the
cited symbol, when a cited test no longer exists, or when a cited conformance case is not declared by its
harness. The [operational test report](/security/operational-tests) shows the exercises that run these tests
and what they observed.

## Invariants

The design rests on seven properties. Each threat below is an attempt to break one of them.

1. **The facilitator is never the source of payment funds.** It sponsors fees through a fee bump and nothing
   else; any payment that makes a facilitator account a payer, a source or a party to an authorization is
   refused.
2. **A signed authorization moves funds at most once.** The Soroban host consumes its nonce, and Rail402's
   settlement ledger answers every repeat from one record.
3. **The payer's signature fixes the recipient and the amount, or the cap.** Changing either after signing
   fails signature verification on chain.
4. **The sponsor's exposure is bounded** per settlement by the fee ceiling and per hour by the fee budget.
5. **A listing is owned by the `payTo` that was settled,** and is published only after its own origin
   confirms it.
6. **Rail402 and its buyer tools never dial a non-public address** on someone else's behalf, unless loopback
   is explicitly allowed for a local test network.
7. **An agent cannot spend beyond the caps its owner set,** which are checked immediately before signing.

## Assets

| Asset | What is protected |
| - | - |
| Payer funds | Tokens in buyers' accounts, G or C, that a payment authorizes. |
| Seller revenue | What a seller is owed and whether it is told correctly that it was paid. |
| Sponsor XLM | The fee sponsor's balance, which pays every settlement fee and the reserves of channels and the upto authority. |
| Catalog and search integrity | That listings describe real, paid resources owned by their payTo, and that search returns them faithfully. |
| Agent budgets | The caps an agent's owner set for the SDK, CLI and MCP server. |
| Secrets | The sponsor seed on the facilitator; buyer signing keys in the SDK, CLI and MCP server. |
| Operator infrastructure | Private networks and metadata services reachable from where Rail402 or an agent runs. |
| Availability | That payments and discovery keep working, or fail with a coded, retryable answer. |

## Trust boundaries

| Boundary | Between | What crosses it |
| - | - | - |
| TB1 | Buyer and seller | HTTP or MCP between a paying client and a resource server. |
| TB2 | Seller and facilitator | /verify, /settle, /supported and the discovery API. |
| TB3 | Facilitator and Stellar | The RPC, the network and its pending transactions, which anyone can observe. |
| TB4 | Facilitator and its state | Postgres, the process itself, its keys and its configuration. |
| TB5 | Facilitator and the open internet | Origin checks and stellar.toml fetches to hosts that sellers choose. |
| TB6 | Agent, MCP server and paid URLs | An agent runtime, the buyer SDK or MCP server holding its key, and the URLs it is asked to pay. |
| TB7 | Contracts | The upto settlement contract, token contracts and smart accounts, on chain. |
| TB8 | Playground page and server | The Playground page in a visitor's browser, holding a testnet key, and its server: the relay to sellers, the Bazaar and facilitator proxies, the seller check and the lab sellers. |

## Summary

STRIDE letters: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of
privilege. "Accepted risk" means the threat cannot be fully closed by a facilitator (for example a payer racing
its own payment within one ledger); the residual risk says what bounds it.

| ID | Topic | Threat | STRIDE | Status | Tests |
| - | - | - | - | - | - |
| R1 | Replay | The same authorization is settled twice | TE | Mitigated | 6 |
| R2 | Replay | The same authorization in a new envelope | T | Mitigated | 3 |
| R3 | Replay | An authorization replayed on another network | T | Mitigated | 3 |
| R4 | Replay | Paid once, served many times | SE | Accepted risk | 5 |
| R5 | Replay | An upto authorization settled for zero stays usable | T | Mitigated | 4 |
| F1 | Front-running | A copied exact authorization lands first | D | Accepted risk | 2 |
| F2 | Front-running | The seller is told the payment failed although it settled | R | Mitigated | 4 |
| F3 | Front-running | A third party settles an upto payment for another amount by submitting it | TE | Mitigated | 6 |
| F4 | Front-running | Another holder of the authority settles first | T | Mitigated | 3 |
| F5 | Front-running | A payload holder fixes the upto amount through the facilitator's /settle | T | Accepted risk | 2 |
| S1 | Fee sponsorship | A payment passes simulation and fails at apply | D | Accepted risk | 3 |
| S2 | Fee sponsorship | An authorization expires between submission and inclusion | D | Accepted risk | 3 |
| S3 | Fee sponsorship | A payment that is expensive to settle | D | Mitigated | 3 |
| S4 | Fee sponsorship | A flood of valid small payments | D | Accepted risk | 5 |
| S5 | Fee sponsorship | The sponsor falls below its reserve | D | Mitigated | 4 |
| S6 | Fee sponsorship | Duplicate submissions of one settlement | D | Mitigated | 3 |
| S7 | Fee sponsorship | Restoring archived state inside a buyer's payment | D | Mitigated | 4 |
| P1 | Catalog poisoning | Forged listing metadata or price | ST | Mitigated | 6 |
| P2 | Catalog poisoning | Taking over another seller's listing | SE | Mitigated | 4 |
| P3 | Catalog poisoning | Hostile JSON Schema in listing metadata | DI | Mitigated | 5 |
| P4 | Catalog poisoning | Encoded traversal in a route template | T | Mitigated | 3 |
| P5 | Catalog poisoning | Wash settlements to flood or rank the catalog | TD | Accepted risk | 4 |
| P6 | Catalog poisoning | One payment used for many listings | T | Mitigated | 3 |
| P7 | Catalog poisoning | Injection through reasons and response headers | TD | Mitigated | 2 |
| P8 | Catalog poisoning | Duplicate listings through URL spellings | T | Mitigated | 3 |
| P9 | Catalog poisoning | Planting a seller's MCP tool listing | ST | Mitigated | 5 |
| P10 | Catalog poisoning | Unverifiable content of mcp\:// tools | ST | Accepted risk | 1 |
| X1 | SSRF | Origin checks to private or metadata addresses | IE | Mitigated | 4 |
| X2 | SSRF | DNS rebinding | IE | Mitigated | 2 |
| X3 | SSRF | Redirects to internal hosts | IE | Mitigated | 3 |
| X4 | SSRF | Oversized or slow answers | D | Mitigated | 3 |
| X5 | SSRF | stellar.toml fetches | I | Mitigated | 2 |
| X6 | SSRF | Paid calls to agent-chosen URLs | IE | Mitigated | 3 |
| A1 | Smart accounts | A payment signed by a key the account does not accept | S | Mitigated | 3 |
| A2 | Smart accounts | \_\_check\_auth that passes simulation and fails at apply | D | Accepted risk | 2 |
| A3 | Smart accounts | Policy events refuse exact payments from policy accounts | D | Accepted risk | 0 |
| A4 | Smart accounts | A spending limit that does not cover upto | E | Mitigated | 4 |
| A5 | Smart accounts | Bypassing the policy through the Default rule | E | Mitigated | 3 |
| A6 | Smart accounts | Concurrent payments beyond the account's own budget | D | Accepted risk | 2 |
| M1 | MCP paid calls | An agent spends beyond its caps | E | Mitigated | 5 |
| M2 | MCP paid calls | Payments on networks, assets or to recipients not allowed | E | Mitigated | 3 |
| M3 | MCP paid calls | Secrets in tool schemas, results, errors or logs | I | Mitigated | 7 |
| M4 | MCP paid calls | A retry pays twice | T | Mitigated | 5 |
| M5 | MCP paid calls | Prompt-injected listings and bait-and-switch sellers | ST | Accepted risk | 6 |
| M6 | MCP paid calls | A tampered payment or a false settlement report | TR | Mitigated | 12 |
| M7 | MCP paid calls | Another local process drives the MCP server over HTTP | SE | Mitigated | 3 |
| U1 | The upto contract | Settling more than the cap | TE | Mitigated | 4 |
| U2 | The upto contract | Changing the recipient, token, expiry, nonce or tree | T | Mitigated | 6 |
| U3 | The upto contract | Settling one authorization twice | T | Mitigated | 4 |
| U4 | The upto contract | Settling with an authority the payer did not name | SE | Mitigated | 3 |
| U5 | The upto contract | The allowance left by a partial settlement | E | Mitigated | 5 |
| U6 | The upto contract | Zero settlements paid by the sponsor | D | Accepted risk | 5 |
| U7 | The upto contract | The settlement contract archived | D | Mitigated | 5 |
| U8 | The upto contract | A different or changed settlement contract | TE | Mitigated | 4 |
| U9 | The upto contract | Token differences between SAC and SEP-41 contracts | T | Mitigated | 4 |
| U10 | The upto contract | The settlement authority's key is compromised | E | Accepted risk | 3 |
| U11 | The upto contract | A served buyer defaults before settlement | R | Accepted risk | 0 |
| E1 | exact payments | Tampering with amount, recipient or asset | T | Mitigated | 3 |
| E2 | exact payments | The facilitator made a party to the payment | E | Mitigated | 4 |
| E3 | exact payments | Extra invocations riding on the payment | TE | Mitigated | 4 |
| E4 | exact payments | An authorization valid for too long | T | Mitigated | 3 |
| E5 | exact payments | Misreading a custom token's failures and decimals | T | Mitigated | 3 |
| O1 | Operations | A crash after submission | DR | Mitigated | 5 |
| O2 | Operations | Channel sequence collisions | D | Mitigated | 4 |
| O3 | Operations | The RPC or the settlement store is unavailable | D | Mitigated | 5 |
| O4 | Operations | Settlements past the RPC's retention | R | Mitigated | 2 |
| O5 | Operations | All channels busy | D | Mitigated | 3 |
| O6 | Operations | The sponsor secret is compromised | EI | Accepted risk | 3 |
| O7 | Operations | Degraded search indexing | D | Mitigated | 2 |
| PG1 | The Playground | The relay used to reach private networks or as an open proxy | SID | Mitigated | 17 |
| PG2 | The Playground | Another site drives the relay from a visitor's browser | SI | Mitigated | 3 |
| PG3 | The Playground | Script injected into the page uses the testnet key | TE | Mitigated | 5 |
| PG4 | The Playground | The seller check used to scan ports or private hosts | ID | Mitigated | 3 |
| PG5 | The Playground | The lab sellers take real payments or pollute the catalog | TD | Mitigated | 7 |
| PG6 | The Playground | The stored wallet read or overspent | IE | Mitigated | 4 |
| PG7 | The Playground | Payments or the key leak into logs, traces or storage | I | Mitigated | 4 |

## Replay

A Stellar payment authorization is not a signed transaction but a Soroban authorization entry: a signature
over one contract invocation, a nonce and an expiration ledger, bound to the network by its passphrase. The
host consumes the nonce when the entry is used, so funds can move only once. What remains for the facilitator
is not to pay for duplicates, to give every repeat the same answer, and to stop a seller from serving one
payment many times.

### R1. The same authorization is settled twice

Mitigated · Tampering, Elevation of privilege · boundary TB2 (Seller and facilitator) · asset: Payer funds

**Threat.** A seller, a buyer or anyone holding a payment sends the same PAYMENT-SIGNATURE to /settle again, one after another or many at once, hoping the payer is charged more than once.

**What Stellar makes different.** An exact or upto authorization is a Soroban authorization entry whose nonce the host consumes when the entry is used; a second use fails on chain with Error(Auth, ExistingValue). The nonce lives under the payer's own address, for G and C accounts alike, until the signature expires. A duplicate that reaches the network is included and fails, so the sponsor still pays its fee.

**Preconditions.** A copy of a payment payload that was already sent to /settle. **Impact.** Without protection: one wasted sponsor fee per duplicate. The payer can never be charged twice, because the host refuses the second use of the nonce.

**Mitigations**

* One settlement record per (network, payer, nonce), claimed before any RPC work; later requests answer from the record instead of submitting. ([`packages/store-postgres/migrations/0001_settlement.sql`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/migrations/0001_settlement.sql), `UNIQUE (network, payer, nonce)`)
* The settle path looks the authorization up first and answers a repeat with the recorded response, marked as a repeat. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `answerRepeat`)
* A request whose terms differ from the recorded request is refused, never given the stored success. ([`packages/facilitator/src/fingerprint.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/fingerprint.ts), `requestHash`)
* Verify reads the ledger and refuses an authorization that is being settled or already was. ([`packages/facilitator/src/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/scheme.ts), `alreadySubmitted`)

**Tests**

* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): answers a repeated settle with the original result and moves funds once
* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): settles concurrent duplicates exactly once, all callers getting the same result
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): settles 5 concurrent repeats once: one transfer, one original, identical bodies
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): never answers a request for different terms with the stored success
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): reports a settlement in progress to verify, then the used authorization
* [`packages/facilitator/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/testing.ts): grants exactly one claim per authorization, even under concurrency

**Conformance runs.** `live-matrix` case `replay-idempotent`, `live-matrix` case `replay-concurrent`, `live-matrix` case `replay-late`.

**Residual risk.** None for funds. A repeat costs a database read, and rate limits bound how often it can be asked.

### R2. The same authorization in a new envelope

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Sponsor XLM

**Threat.** An attacker wraps an already settled authorization entry in a transaction of different bytes (another source, sequence or fee) so it is not recognised as a repeat.

**What Stellar makes different.** The authorization is bound to its invocation, not to the transaction, so it can be placed in any envelope. The host refuses the consumed nonce at apply time; a facilitator that only compared envelope bytes would submit it and pay the fee.

**Preconditions.** A copy of a settled authorization entry. **Impact.** One wasted sponsor fee per attempt if it reached the network.

**Mitigations**

* The settlement key is the payer's address and nonce, read from the authorization entry, not the envelope hash. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `keyOf`)
* A different envelope for a recorded authorization is refused with the idempotency conflict, naming the recorded transaction. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `conflict`)

**Tests**

* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): refuses the same authorization in another envelope, naming the recorded transaction
* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): refuses a settled authorization in a different envelope, and verify names the replay
* [`packages/facilitator/test/state.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/state.test.ts): reports a conflict when the same authorization arrives in a different envelope

**Conformance runs.** `live-matrix` case `replay-rejected`.

**Residual risk.** None: nothing is submitted.

### R3. An authorization replayed on another network

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Payer funds

**Threat.** A payment signed for stellar:testnet is presented for stellar:pubnet, or the reverse.

**What Stellar makes different.** The signed preimage (HashIdPreimageSorobanAuthorization) includes the network id, the SHA-256 of the network passphrase, so a signature is only valid on the network it was made for.

**Preconditions.** A signed payment for one network. **Impact.** None on chain: signature verification fails on the other network.

**Mitigations**

* The network id is part of every signed preimage the buyer's guard decodes and checks. ([`packages/stellar/src/auth.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/auth.ts), `networkIdForPassphrase`)
* A request is served only on the network its requirements name, and a payload for another network is refused. ([`apps/rail402/src/app.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/app.ts), `invalid_network`)

**Tests**

* [`packages/stellar/test/auth.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/auth.test.ts): is SHA-256 of the passphrase
* [`packages/sdk/test/decide.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/decide.test.ts): refuses another network, whether in the preimage or in the requested passphrase
* [`apps/rail402/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/app.test.ts): refuses networks and schemes it does not serve without calling the core

**Conformance runs.** `rejection-audit` case `unknown-network`.

**Residual risk.** None.

### R4. Paid once, served many times

Accepted risk · Spoofing, Elevation of privilege · boundary TB1 (Buyer and seller) · asset: Seller revenue

**Threat.** A buyer pays once and sends the same PAYMENT-SIGNATURE several times, concurrently or later. A stock upfront seller calls /settle without /verify and serves on success, and an idempotent facilitator answers every repeat with the same success.

**What Stellar makes different.** Nothing Stellar-specific allows it: it follows from answering repeats with the stored result, which the x402 idempotency requirement asks for. The Stellar part is that the answer carries the one settlement transaction hash, which a seller can deduplicate on.

**Preconditions.** A seller that serves on a successful /settle without remembering which settlements it already served. **Impact.** The seller serves a paid resource more than once for one payment. The buyer is charged once.

**Mitigations**

* Every repeated /settle answer carries a repeat marker in EXTENSION-RESPONSES (rail402.settlement.repeat). ([`packages/facilitator/src/responses.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/responses.ts), `markRepeat`)
* The seller helpers refuse a second request whose settlement is a repeat or a transaction already served. ([`packages/sdk/src/seller/dedupe.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/seller/dedupe.ts), `withSettleDedupe`)
* In the authorization flow, verify refuses an authorization that is being settled or was. ([`packages/facilitator/src/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/scheme.ts), `alreadySubmitted`)

**Tests**

* [`apps/rail402/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/app.test.ts): sends the recorded body and moves the repeat marker into EXTENSION-RESPONSES
* [`packages/sdk/test/seller.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/seller.test.ts): trusts Rail402's repeat marker, even for a settlement it never saw
* [`packages/sdk/test/seller.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/seller.integration.test.ts): does not serve a replayed payment signature again
* [`packages/sdk/test/seller.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/seller.integration.test.ts): serves an upfront settlement once when the facilitator answers its repeat
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): marks the repeat in extension responses, and counts and meters it once

**Conformance runs.** `live-matrix` case `replay-idempotent`.

**Residual risk.** An upfront seller on stock middleware that does not wrap its facilitator client with withSettleDedupe, or deduplicate PAYMENT-RESPONSE.transaction itself, can serve a repeat. The seller helpers page tells upfront sellers to wrap it.

### R5. An upto authorization settled for zero stays usable

Mitigated · Tampering · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** A seller settles an upto payment for zero, then tries to charge the same authorization again for a positive amount.

**What Stellar makes different.** An on-chain zero settlement still uses the payer's authorization entry, so the host consumes its nonce like any other settlement. An off-chain zero (UPTO\_ZERO\_SETTLEMENT=offchain) sends nothing, so the nonce stays unused on chain until the signature expires.

**Preconditions.** An upto payment settled for zero. **Impact.** Without protection, a second settlement of the same authorization for up to the cap.

**Mitigations**

* The upto contract consumes the payer's nonce for a zero settlement too. ([`contracts/stellar/upto/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/lib.rs), `pub fn settle`)
* An off-chain zero is a terminal record in the settlement ledger, so a later request for any amount is answered from it or refused as a conflict; only the named authority can settle on chain, and it is Rail402's. ([`packages/facilitator/src/upto-scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/upto-scheme.ts), `offchainZero`)

**Tests**

* [`contracts/stellar/upto/src/test/signed.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/signed.rs): a\_zero\_settlement\_also\_consumes\_the\_nonce
* [`packages/facilitator/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto.integration.test.ts): settles zero on-chain: the nonce is consumed and no funds move
* [`packages/facilitator/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto.integration.test.ts): settles zero off-chain when configured: no transaction, the authorization used up in the ledger
* [`packages/facilitator/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/testing.ts): finishes an upto zero-amount success without an envelope, only for the claim's owner

**Conformance runs.** `live-matrix` case `upto-zero`.

**Residual risk.** With off-chain zeros and STORE=memory, a restart forgets the record; the authorization then stays settleable by Rail402's own authority until it expires. Use Postgres wherever off-chain zeros are enabled.

## Front-running

Because the authorization is bound to its invocation rather than to a transaction, anyone who sees it before it
lands can submit it in a transaction of their own. That can only make the signed payment happen, exactly as
signed; it cannot redirect it. The cost is Rail402's own failed transaction, and the risk is telling a seller
"failed" for a payment that settled.

### F1. A copied exact authorization lands first

Accepted risk · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Sponsor XLM

**Threat.** Someone who sees the settlement envelope (the seller, an RPC operator, a validator's pending queue) copies the payer's authorization entry into a transaction of their own and gets it included before Rail402's.

**What Stellar makes different.** A Soroban authorization is a bearer instrument for exactly one invocation: whoever submits it can only make the signed transfer happen, from the payer to payTo for the signed amount. It cannot be redirected. Rail402's own transaction then fails on the consumed nonce and its fee is spent.

**Preconditions.** Access to the envelope between signing and inclusion, and an account to pay the copy's fee. **Impact.** One wasted sponsor fee per event. The attacker pays its own fee and gains nothing; the payment settles as signed.

**Mitigations**

* Rail402 submits as soon as the settle-time simulation passes, which leaves little time to copy the entry. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `execute`)
* Signed envelopes and authorizations are redacted from logs. ([`apps/rail402/src/logger.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/logger.ts), `REDACTED_PATHS`)
* Wasted fees are counted, so griefing is visible and bounded by the hourly budget. ([`apps/rail402/src/metrics.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/metrics.ts), `wasted`)

**Tests**

* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): answers success with the attacker's hash; the payment moves once and our fee is wasted
* [`apps/rail402/test/logger.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/logger.test.ts): redacts secret fields at the top level and one level down

**Residual risk.** The wasted fee is not recoverable. Each event costs the attacker its own fee too.

### F2. The seller is told the payment failed although it settled

Mitigated · Repudiation · boundary TB2 (Seller and facilitator) · asset: Seller revenue

**Threat.** After a front-run, Rail402's transaction fails with a consumed nonce and the seller is told the payment failed, although the payer was charged.

**What Stellar makes different.** The chain holds the proof: the nonce entry's last-modified ledger, and in that ledger a successful transaction whose address credential (payer, nonce, signature expiration) and root invocation are byte-equal to the payer's signed entry, with the matching token transfer event.

**Preconditions.** A front-run as in F1. **Impact.** Without reconciliation the seller withholds a resource that was paid for.

**Mitigations**

* A failed settlement whose nonce was consumed is reconciled: Rail402 finds the other transaction on chain, proves it carried the same signed invocation after the request was claimed, and answers success with its hash. ([`packages/facilitator/src/third-party.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/third-party.ts), `findThirdPartySettlement`)
* Proof requires a byte-equal root invocation and credential; a different invocation for the same nonce stays a failure. ([`packages/facilitator/src/third-party.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/third-party.ts), `signedAuthorizationOf`)

**Tests**

* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): answers success with the attacker's hash; the payment moves once and our fee is wasted
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): refuses an authorization consumed before the request was claimed
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): refuses when the consuming transaction carried a different invocation for the same nonce
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): refuses an authorization whose nonce was never consumed

**Residual risk.** A public RPC keeps only a window of ledgers; a settlement reconciled after that window stays failed. Reconciliation runs immediately after the failure, well inside the window.

### F3. A third party settles an upto payment for another amount by submitting it

Mitigated · Tampering, Elevation of privilege · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** Someone holding an upto payload (the buyer, the seller, a TLS-terminating proxy, a log, an RPC operator who sees the settlement envelope) submits it themselves for another amount up to the cap, such as zero or the whole cap, before or instead of the seller's settlement.

**What Stellar makes different.** The payer's signature covers the cap, not the amount. The upto contract therefore also requires the authorization of the settlement authority named in the signed terms, and Rail402 makes that authority the operation's source account, so its authorization is the transaction's own signature and cannot be copied into another transaction.

**Preconditions.** A copy of an upto payload. **Impact.** None: the network refuses a settlement that the settlement authority did not authorize, and Rail402's settlement proceeds. The same request through the facilitator's own /settle is F5.

**Mitigations**

* The contract requires terms.facilitator to authorize settle(payer, terms, amount), so the authority's authorization covers one amount and nothing else. ([`contracts/stellar/upto/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/lib.rs), `terms.facilitator.require_auth()`)
* Rail402 signs as the authority through a source-account credential that exists only inside its own transaction. ([`packages/upto/src/terms.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/terms.ts), `authorityEntry`)

**Tests**

* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): a\_different\_submitter\_cannot\_settle
* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): missing\_facilitator\_authorization\_is\_rejected
* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): facilitator\_authorization\_for\_another\_amount\_is\_rejected
* [`contracts/stellar/upto/src/test/signed.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/signed.rs): another\_source\_account\_cannot\_act\_as\_the\_authority
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): cannot be settled by a payload holder for another amount: the host refuses without the authority
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): cannot be landed by an attacker without the settlement authority; Rail402's settlement proceeds

**Residual risk.** None against anyone without the settlement authority's key; a holder of that key is U10.

### F4. Another holder of the authority settles first

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Seller revenue

**Threat.** Two replicas sharing the sponsor secret, or a stale process, settle the same upto payment; one lands first with a different amount than the seller asked for.

**What Stellar makes different.** Only a transaction signed by the authority can land; the landed amount is readable from the settle arguments and the upto\_settled event.

**Preconditions.** Two processes that hold the same authority key. **Impact.** Without reconciliation the seller gets a false failure, or a success for an amount it did not request.

**Mitigations**

* The later settlement is answered with the landed transaction when it settled the same amount, and with settle\_upto\_stellar\_settled\_elsewhere, naming both amounts, when it did not. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `settle_upto_stellar_settled_elsewhere`)

**Tests**

* [`packages/facilitator/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto.integration.test.ts): succeeds with that transaction when it settled the same amount
* [`packages/facilitator/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto.integration.test.ts): answers settled\_elsewhere when it settled a different amount
* [`packages/facilitator/test/front-run.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/front-run.integration.test.ts): answers with the authority's own earlier transaction for the same amount; our fee is wasted

**Residual risk.** None beyond one wasted fee.

### F5. A payload holder fixes the upto amount through the facilitator's /settle

Accepted risk · Tampering · boundary TB2 (Seller and facilitator) · asset: Seller revenue

**Threat.** Someone holding an upto payload asks the facilitator's own /settle to settle it for another amount up to the cap, such as zero or the whole cap, before the seller does.

**What Stellar makes different.** The payer's signature covers the cap, not the amount, and the facilitator is the settlement authority: whichever accepted /settle request comes first decides the amount it authorizes, and the payer's nonce then makes the authorization single-use.

**Preconditions.** A copy of an upto payload and access to the facilitator's /settle (keyless on the public testnet service). **Impact.** The first accepted request fixes the amount anywhere in \[0, cap] to payTo: a buyer who settles zero first leaves the seller unpaid for that request, and anyone who settles the cap first charges the buyer the full cap. The seller's own later request for a different amount is refused as an idempotency conflict. Funds never go elsewhere and never exceed the cap.

**Mitigations**

* The facilitator settles an authorization once: the first accepted amount wins, and a later request for a different amount is refused with settle\_upto\_stellar\_idempotency\_conflict naming the amount that holds it. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `idempotencyConflict`)
* Operators can require API keys for /verify and /settle per network, so only known sellers can settle; the payload travels only from buyer to seller over TLS, and sellers settle before releasing a buffered response. ([`apps/rail402/src/app.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/app.ts), `requireApiKey`)

**Tests**

* [`apps/rail402/test/upto-settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto-settlement.integration.test.ts): lands one transaction for 20 concurrent settlements of different amounts and refuses the rest
* [`apps/rail402/test/upto-settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto-settlement.integration.test.ts): lets the first settle request fix the amount: a payload holder's zero turns the seller's settle into a conflict

**Residual risk.** Accepted for payload holders on a keyless facilitator: a buyer or anyone who sees the payload can fix the amount through /settle before the seller (unpaid seller compute and one sponsor fee, or the buyer charged up to its own cap). Operators who need more require API keys for /settle.

## Fee sponsorship

The sponsor pays the fee of every transaction it submits, and Soroban charges resource fees whether the
transaction succeeds or fails. Every refusal Rail402 can make before signing costs nothing; a transaction that
is included and fails costs its fee. Rail402 refuses whatever it can see coming, names and counts what it
cannot, and bounds the total.

### S1. A payment passes simulation and fails at apply

Accepted risk · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Sponsor XLM

**Threat.** A payer spends the balance, or changes a smart account's state, after the settle-time simulation and before inclusion, so the settlement is included and fails.

**What Stellar makes different.** Soroban resource fees are charged when a transaction is included, success or failure; the sponsor pays through the fee bump. Simulation runs against the latest ledger, so a change made in the ledgers between simulation and inclusion is not seen.

**Preconditions.** A payer (or its smart-account policy) able to act within about one ledger of the settlement. **Impact.** One wasted sponsor fee per event, never more than the fee ceiling. No funds move.

**Mitigations**

* Settle simulates again with the current ledger, so a change made before settle is refused without submitting. ([`packages/facilitator/src/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/scheme.ts), `settle`)
* The failure is named from the transaction's diagnostic events and counted as wasted fee. ([`packages/facilitator/src/explain.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/explain.ts), `explainFailedTransaction`)
* The hourly fee budget and the reserve floor bound the total a griefer can burn. ([`packages/facilitator/src/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/guard.ts), `SponsorBudget`)

**Tests**

* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): refuses at settle, with nothing submitted, a payment whose balance moved after verify
* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): names a balance spent between the settle-time simulation and inclusion; the fee is spent and counted as wasted
* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): stops settling once the hourly fee budget is committed, before any RPC work

**Residual risk.** The race inside one ledger cannot be closed by a facilitator that does not hold the funds. Each event costs the payer an on-chain transaction of its own.

### S2. An authorization expires between submission and inclusion

Accepted risk · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Sponsor XLM

**Threat.** A payment is settled with an authorization whose expiration ledger passes while the transaction waits for inclusion.

**What Stellar makes different.** Validity runs through signatureExpirationLedger inclusive; the transaction's time bound can outlive it. An included transaction whose authorization expired fails and its fee is spent.

**Preconditions.** A payment settled at the very end of its validity window. **Impact.** One wasted sponsor fee.

**Mitigations**

* Settle refuses an authorization that expires within a one-ledger margin of the current ledger. ([`packages/facilitator/src/preflight.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/preflight.ts), `invalid_exact_stellar_signature_expiration_too_soon`)
* An upto settlement's ledger bounds end at the expiration ledger, so the network refuses it before inclusion and no fee is spent. ([`packages/upto/src/facilitator/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/scheme.ts), `buildTransaction`)

**Tests**

* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): holds the lower edge at the current ledger plus a one-ledger margin
* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): names an authorization that expires between submission and inclusion; the fee is spent
* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): refuses at the network an upto settlement held past expiration\_ledger; no fee is spent

**Conformance runs.** `live-matrix` case `expiry-window`.

**Residual risk.** For exact, a transaction that waits longer than the margin in the network's queue can still fail this way; it is named and counted as wasted.

### S3. A payment that is expensive to settle

Mitigated · Denial of service · boundary TB2 (Seller and facilitator) · asset: Sponsor XLM

**Threat.** A payer submits a payment whose settlement needs large resources: a heavy \_\_check\_auth, a token that writes a lot of state, or rent for entries it extends.

**What Stellar makes different.** Soroban fees depend on the instructions, ledger reads and writes, and rent of the invocation, which the payer's account and the token contract choose. The fee is known from simulation before anything is signed.

**Preconditions.** A smart account or token contract whose calls are costly. **Impact.** Without a ceiling, an arbitrary fee per settlement.

**Mitigations**

* Verify and settle refuse a payment whose simulated fee exceeds MAX\_TX\_FEE\_STROOPS (UPTO\_MAX\_TX\_FEE\_STROOPS for upto), before signing. ([`packages/facilitator/src/policy.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/policy.ts), `DEFAULT_MAX_TRANSACTION_FEE_STROOPS`)
* The inclusion-fee bid is capped. ([`packages/facilitator/src/fees.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/fees.ts), `InclusionFeeOracle`)

**Tests**

* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): refuses a payment whose settlement would cost more than the fee ceiling, before signing anything
* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): enforces the fee ceiling
* [`packages/facilitator/test/fees.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fees.test.ts): bids the configured percentile, clamped between floor and cap

**Residual risk.** A payment just under the ceiling costs up to the ceiling. Operators accepting tokens that pay rent on read raise the ceiling knowingly (the TEST6 token needs about 1,000,000 stroops on testnet).

### S4. A flood of valid small payments

Accepted risk · Denial of service · boundary TB2 (Seller and facilitator) · asset: Sponsor XLM

**Threat.** Someone makes many real, valid payments of the smallest amount to drain the sponsor through fees.

**What Stellar makes different.** Every valid settlement costs the sponsor a fee of tens of thousands of stroops, far more than a one-unit USDC payment is worth.

**Preconditions.** Funds for many small payments. **Impact.** Sponsor XLM spent at the attacker's pace.

**Mitigations**

* An hourly fee budget, shared by replicas through the settlement ledger, stops new settlements once reached; pubnet refuses to start without one. ([`packages/facilitator/src/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/guard.ts), `maxSpendPerHourStroops`)
* Per-client rate limits and API keys on /verify and /settle; API keys must be decided explicitly for pubnet. ([`apps/rail402/src/rate-limit.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/rate-limit.ts), `RateLimiter`)
* Per-asset minimum amounts for exact payments and upto caps (an upto settlement may still be zero; see U6). ([`apps/rail402/src/config.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/config.ts), `parseAssets`)

**Tests**

* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): stops settling once the hourly fee budget is committed, before any RPC work
* [`apps/rail402/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/app.test.ts): rate-limits per client with Retry-After and a coded body
* [`apps/rail402/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/app.test.ts): rate-limits an IPv6 client by its /64 and a mapped IPv4 client as IPv4
* [`apps/rail402/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/app.test.ts): enforces API keys when the network requires them
* [`apps/rail402/test/config.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/config.test.ts): requires explicit, safe mainnet settings

**Conformance runs.** `rejection-audit` case `rate-limited`, `rejection-audit` case `api-key-required`.

**Residual risk.** On a free, keyless testnet facilitator the per-client rate limit (IPv6 clients are grouped by /64) and, where configured, the hourly budget are the only bounds. A client with many addresses passes the rate limit, so the budget is the real bound: once it is spent, every settlement is refused until the hour moves on, which trades availability for the sponsor's balance. On pubnet the budget is required, and pricing, keys and minimums are an operator decision.

### S5. The sponsor falls below its reserve

Mitigated · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Availability

**Threat.** The sponsor's balance runs low, from normal use or an attack, until fee bumps start failing mid-settlement.

**What Stellar makes different.** The sponsor also carries the base reserves of the channel accounts and the upto authority, which it sponsors; below them it cannot pay fees at all.

**Preconditions.** Sustained settlement volume or an attack as in S4. **Impact.** Settlements fail after signing; the service becomes unavailable.

**Mitigations**

* Settlements are refused below SPONSOR\_RESERVE\_XLM before any RPC work, with a retryable code. ([`packages/facilitator/src/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/guard.ts), `minBalanceStroops`)
* /ready fails below MIN\_SPONSOR\_BALANCE\_XLM and /health reports it. ([`apps/rail402/src/health.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/health.ts), `sponsor_below_floor`)

**Tests**

* [`packages/facilitator/test/fees.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fees.test.ts): refuses while the sponsor is below its reserve, admits otherwise
* [`apps/rail402/test/observability.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/observability.integration.test.ts): counts refusals below the reserve and reports the sponsor below its floor
* [`apps/rail402/test/runtime.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/runtime.integration.test.ts): fails while the sponsor holds less than MIN\_SPONSOR\_BALANCE\_XLM
* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): refuses while the sponsor guard is closed, then settles once it reopens

**Residual risk.** None beyond downtime until the sponsor is refunded.

### S6. Duplicate submissions of one settlement

Mitigated · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Sponsor XLM

**Threat.** Retries, concurrent requests or two replicas submit the same settlement more than once.

**What Stellar makes different.** Each submitted transaction consumes a channel sequence number and pays its own fee; the second one fails on the consumed nonce.

**Preconditions.** Concurrent or repeated requests for one payment. **Impact.** Wasted fees and sequence conflicts.

**Mitigations**

* One claim per authorization across replicas; only the claim holder signs and submits. ([`packages/store-postgres/src/settlement-ledger.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/src/settlement-ledger.ts), `claim`)
* A crashed settlement is resumed from its recorded envelope bytes, never rebuilt. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `recordingSponsor`)

**Tests**

* [`packages/facilitator/test/concurrency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/concurrency.integration.test.ts): with injected duplicates, without a collision
* [`apps/rail402/test/replicas.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/replicas.integration.test.ts): with every duplicate sent to the other replica
* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): submits one transaction for 20 concurrent identical settlements

**Conformance runs.** `concurrency` case `distinct-sequences`.

**Residual risk.** None.

### S7. Restoring archived state inside a buyer's payment

Mitigated · Denial of service · boundary TB7 (Contracts) · asset: Sponsor XLM

**Threat.** A settlement touches an archived contract instance or entry, and the transaction would restore it, billing the restoration rent to the sponsor as part of the payment.

**What Stellar makes different.** Since protocol 23 a freshly simulated invocation of an archived persistent entry restores it automatically inside the transaction, and the fee payer pays the rent.

**Preconditions.** An archived settlement contract, token instance or balance entry. **Impact.** Rent paid by the sponsor on behalf of whoever let the entry expire.

**Mitigations**

* A simulation that reports archived state is refused: the upto settlement contract with invalid\_upto\_stellar\_settlement\_contract\_archived, other entries with the archived-state code; nothing is submitted. ([`packages/upto/src/facilitator/simulation.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/simulation.ts), `archivedStateRejection`)
* Exact payments name archived state from the simulation's restore preamble. ([`packages/facilitator/src/explain.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/explain.ts), `explainSimulation`)

**Tests**

* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): refuses when the settlement contract must be restored, and names other archived state
* [`packages/facilitator/test/upto-scheme.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto-scheme.test.ts): refuses to pay for restoring an archived settlement contract
* [`packages/facilitator/test/explain.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/explain.test.ts): names archived ledger state when simulation asks for a restore
* [`apps/rail402/test/upto-archival.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto-archival.integration.test.ts): refuses payments while the contract is archived, and settles once an operator restores it

**Residual risk.** None; restoration is an operator action (U7).

## Catalog poisoning

The Bazaar catalogs resources automatically after a successful settlement. The settlement proves one thing,
that a payment reached `payTo`; everything else in the resource description came from the buyer's request and
must be confirmed at the resource's own origin before it is published.

### P1. Forged listing metadata or price

Mitigated · Spoofing, Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A buyer (or a seller's buggy client) echoes a resource block with a false description, price or schema in the payment, hoping the catalog publishes it.

**What Stellar makes different.** Nothing on chain describes the resource; the settlement proves only that a payment to payTo happened. The metadata must be confirmed at its origin.

**Preconditions.** One real settlement to the seller. **Impact.** Misleading listings shown to buyers and agents.

**Mitigations**

* An HTTP resource, or an MCP tool at an http(s) endpoint, is published only after its own 402 (for a tool, the answer to an unpaid tools/call over MCP) confirms payTo, price and metadata; what is published always comes from that answer, so echoed metadata never changes a listing on its own. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `checkOrigins`)
* MCP tools are asked through safe-fetch with the MCP client: the tool must be declared in tools/list and its unpaid call must answer with the x402 PaymentRequired. ([`packages/bazaar/src/mcp-origin.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/mcp-origin.ts), `probeMcpTool`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): does not let echoed metadata change a listing until the origin confirms it
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): never lists an HTTP resource whose own response is not a 402
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): quarantines a listing whose origin does not name its owner
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): lists an MCP tool at an https endpoint only once its unpaid call confirms it
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): quarantines a tool its server does not sell to the listed payTo, with a coded reason
* [`packages/bazaar/test/mcp-origin.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/mcp-origin.test.ts): reads a tool's PaymentRequired from its unpaid call, sending the example arguments

**Conformance runs.** `live-bazaar` case `forged-metadata`, `live-bazaar` case `not-x402`.

**Residual risk.** A seller can describe its own resource falsely; buyers bind the price and recipient of a listing when paying (M6). Tools under an mcp\:// URL have no origin to check (P10).

### P2. Taking over another seller's listing

Mitigated · Spoofing, Elevation of privilege · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** An attacker pays a URL that belongs to another seller, but to its own payTo, to take over or modify that listing.

**What Stellar makes different.** The settled payTo is the only identity the facilitator has proven, from a settlement that moved value to it. An upto zero moves nothing and proves nothing, so it never catalogs.

**Preconditions.** A settlement for the victim's URL to the attacker's account. **Impact.** Payments for a listed resource redirected to the attacker.

**Mitigations**

* A listing is bound to the settled payTo; another payTo is refused with bazaar\_owner\_conflict, and ownership moves only when the origin names the new payTo. ([`packages/bazaar/src/codes.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/codes.ts), `bazaar_owner_conflict`)
* A settlement that moved nothing (an upto zero, on-chain or off) never catalogs. A settlement that moved one base unit does not help either: a listing is published only with what its own origin answers (P1, P9). ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `unprovenSettlement`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): never lets a different payTo modify a listing
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): transfers ownership only when the origin names the new payTo
* [`apps/rail402/test/bazaar.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/bazaar.integration.test.ts): refuses a different payTo for the same resource, with a coded reason
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): does not catalog a settlement that moved nothing, however high its cap

**Conformance runs.** `live-bazaar` case `owner-conflict`.

**Residual risk.** None for resources with an origin. A tool under an mcp\:// URL is scoped to its owner, so it cannot be taken over, but its first content is whatever its first payer echoed (P10).

### P3. Hostile JSON Schema in listing metadata

Mitigated · Denial of service, Information disclosure · boundary TB2 (Seller and facilitator) · asset: Availability

**Threat.** Metadata carries a schema with a catastrophic regular expression, a combinatorial check, deep nesting or an external \$ref.

**What Stellar makes different.** None: this is a property of the catalog, which validates schemas it receives in payments.

**Preconditions.** One settlement carrying the schema. **Impact.** Catalog workers stalled, or the service fetching arbitrary URLs.

**Mitigations**

* Schemas compile and validate in a worker with a time budget and a bounded queue; external references are refused. ([`packages/bazaar/src/schema-sandbox.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/schema-sandbox.ts), `SchemaSandbox`)

**Tests**

* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): stops a catastrophic-backtracking pattern within its budget and keeps serving
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): stops a combinatorial uniqueItems check within its budget
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): refuses oversized and deeply nested documents before compiling them
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): finds external references in every schema position, and only there
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): refuses validations beyond its queue at once instead of delaying everyone

**Conformance runs.** `live-bazaar` case `schema-rejected`.

**Residual risk.** None.

### P4. Encoded traversal in a route template

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A routeTemplate hides ../, NUL, CR/LF or a backslash behind percent-encoding so that a listing matches, or later fills, paths it should not.

**What Stellar makes different.** Nothing in the parsing is Stellar-specific. What a template changes is which paths are paid to one Stellar account: a listing and every path its template claims stay bound to the payTo proven by the settled transfer (P2), so a template can only widen a seller's own listing, and only over paths its own route matched.

**Preconditions.** One settlement carrying the template. **Impact.** A listing that claims paths of another resource, or a buyer tool that fills a path outside the seller's route.

**Mitigations**

* Templates are percent-decoded before the safety check, and used only when they match the paid path segment by segment. ([`packages/bazaar/src/url.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/url.ts), `isSafeTemplate`)
* Buyers refuse traversal when filling route parameters. ([`packages/sdk/src/listing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/listing.ts), `expandRoute`)

**Tests**

* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): refuses route templates that decode to NUL, CR, LF, a backslash or an empty segment
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): uses a routeTemplate only when it is safe and matches the paid path
* [`packages/sdk/test/discovery.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/discovery.test.ts): fills route parameters and refuses traversal

**Conformance runs.** `live-bazaar` case `route-template`.

**Residual risk.** None.

### P5. Wash settlements to flood or rank the catalog

Accepted risk · Tampering, Denial of service · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A seller pays itself through a second account many times to create many listings or to look popular.

**What Stellar makes different.** Settlements are cheap on Stellar, so volume is a weak signal of quality.

**Preconditions.** Two accounts and a little USDC. **Impact.** Catalog spam and manipulated rankings.

**Mitigations**

* New listings are rate-limited per owner, per payer and for the whole catalog. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `maxNewListingsPerPayerPerHour`)
* Search never ranks by settlement count: ties break by origin verification, then listing age. ([`packages/bazaar/src/wire.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/wire.ts), `trustRank`)
* Self-payments are not cataloged. ([`packages/bazaar/src/codes.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/codes.ts), `bazaar_self_payment`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): bounds how many listings one payTo may create per hour
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): bounds the new listings one payer's settlements may create per hour
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): rejects self-payments
* [`packages/search/test/search.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/search/test/search.test.ts): serves filter-only queries by trust, then listing age, never by settlements bought

**Residual risk.** A patient seller can still create listings up to the limits; each needs a working origin that answers 402.

### P6. One payment used for many listings

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** The same settled transaction is presented with different resource blocks to catalog several resources.

**What Stellar makes different.** A transaction hash identifies exactly one settlement on chain.

**Preconditions.** One settlement. **Impact.** Listings without payments behind them.

**Mitigations**

* Each settlement transaction catalogs at most one resource (a unique index on the observation's transaction). ([`packages/store-postgres/migrations/0005_catalog_integrity.sql`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/migrations/0005_catalog_integrity.sql), `listing_observations_transaction`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): catalogs at most one resource per settlement
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): records a repeated settlement once and never duplicates the listing
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): does not catalog a payment settled without a transaction

**Conformance runs.** `live-bazaar` case `settlement-reused`, `live-bazaar` case `duplicate-settlement`.

**Residual risk.** None.

### P7. Injection through reasons and response headers

Mitigated · Tampering, Denial of service · boundary TB2 (Seller and facilitator) · asset: Availability

**Threat.** Metadata crafted so that a rejection reason quoting it breaks headers, logs or clients, or grows EXTENSION-RESPONSES beyond what proxies accept.

**What Stellar makes different.** EXTENSION-RESPONSES travels in the same /settle answer as the settled transaction hash. The payment is final on the ledger whatever happens to the header, so a header that breaks the answer would hide a settled payment from the seller; the bound keeps that answer intact.

**Preconditions.** One settlement with crafted metadata. **Impact.** Broken settle responses or log injection.

**Mitigations**

* Reasons are bounded and stripped of control characters; EXTENSION-RESPONSES stays within 4 KB. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `boundedReason`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): bounds a rejection reason that quotes the buyer's input
* [`packages/bazaar/test/memory-catalog.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/memory-catalog.test.ts): stays within the header budget whatever the outcome carries

**Conformance runs.** `live-bazaar` case `header-bounded`.

**Residual risk.** None.

### P8. Duplicate listings through URL spellings

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A seller or attacker lists one resource many times by varying case, ports, dot segments or percent-encoding.

**What Stellar makes different.** A spelling variant would be a second key with no owner, escaping the binding to the payTo proven on chain (P2). Listings are keyed by the canonical URL per network (stellar:testnet, stellar:pubnet), so one resource has one owner per network.

**Preconditions.** Settlements for the variant URLs. **Impact.** Duplicate results, and a second listing that escapes the owner binding of the first.

**Mitigations**

* URLs are canonicalised (case, default ports, dot segments, percent-encoding, query and fragment) before they key a listing. ([`packages/bazaar/src/url.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/url.ts), `canonicalizeUrl`)

**Tests**

* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): canonicalises case, default ports, dot segments, query and fragment
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): normalizes percent-encoding so equivalent paths are one resource
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): treats equivalent spellings of one URL as one listing

**Residual risk.** None.

### P9. Planting a seller's MCP tool listing

Mitigated · Spoofing, Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A payer pays a seller's payTo one base unit (or settles one unit under an upto cap of any size) while echoing the seller's MCP endpoint and tool name with its own description, input schema or price, hoping to publish, freeze or domain-verify that content under the seller's identity, or to use up the seller's quota of new listings.

**What Stellar makes different.** The settlement proves a payment to payTo, not who wrote the echoed tool description. Dust costs the payer almost nothing and the sponsor pays the fee, so a settlement alone cannot vouch for content.

**Preconditions.** A trustline and one base unit of the asset; the seller's MCP endpoint and tool name. **Impact.** Spoofed tool descriptions (prompt injection) and prices shown to agents under the seller's verified identity; the seller's own tools not cataloged.

**Mitigations**

* A tool at an http(s) endpoint stays pending until the tool itself answers an unpaid tools/call with a PaymentRequired naming the owner; until an origin check has confirmed it, the tool must also offer the settled scheme, network, asset, payTo and price (cap), or the listing is quarantined with origin\_payment\_mismatch. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `samePayment`)
* A later settlement the tool itself asked for is checked again and replaces planted content with the tool's own; a planted settlement cannot change or withdraw a confirmed tool. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `checkOrigins`)
* Domain trust is granted only to origin-verified listings, and needs the two-way SEP-1 binding. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `enrich`)
* Only listings the owner's origin confirmed count toward the owner's hourly quota. ([`packages/store-postgres/src/catalog-store.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/src/catalog-store.ts), `countCreatedBy`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): never publishes a tool description or price planted with
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): never lets a planted settlement withdraw or change a tool its origin confirmed
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): updates an MCP tool's metadata once its origin confirms the change
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): lets a domain vouch for an MCP tool listed under its host
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): does not charge an owner's quota for listings other payers created and its origin never confirmed

**Conformance runs.** `live-bazaar` case `mcp-tool`.

**Residual risk.** Tools under an mcp\:// URL (P10). A planted settlement can quarantine a tool that was never confirmed until the tool's next real settlement.

### P10. Unverifiable content of mcp\:// tools

Accepted risk · Spoofing, Tampering · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** A tool under an mcp\:// URL names no server, so nothing but the settlement vouches for its description, schema and price; whoever first pays its owner for it chooses them.

**What Stellar makes different.** As P9: a settlement proves the payment, not the content.

**Preconditions.** One settlement of one base unit to the owner, before the tool's first real settlement. **Impact.** A misleading description or price on the owner's mcp\:// tool listing.

**Mitigations**

* mcp\:// tools are scoped to their owner and published at trust settled, which they never leave: no domain can vouch for them. ([`packages/bazaar/src/catalog.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/catalog.ts), `verifiable`)
* The Rail402 SDK does not call mcp\:// resources, and its default pubnet policy pays MCP listings only at domain\_verified. ([`packages/sdk/src/policy/policy.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/policy.ts), `resolvePolicy`)

**Tests**

* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): publishes an MCP tool under an mcp\:// URL at once, at trust settled, keeping its first content

**Residual risk.** The first content of an mcp\:// tool is chosen by its first payer and kept. Sellers who want their own content listed serve their tools at an http(s) endpoint.

## SSRF

Cataloging makes the facilitator fetch URLs that sellers choose, and paying makes an agent fetch URLs that a
listing or a prompt suggests. Both run through a fetch that resolves the host itself, refuses non-public
addresses at dial time, follows no redirect it has not re-checked, and caps sizes and times.

### X1. Origin checks to private or metadata addresses

Mitigated · Information disclosure, Elevation of privilege · boundary TB5 (Facilitator and the open internet) · asset: Operator infrastructure

**Threat.** A listing's resource URL points at a private, loopback, link-local or cloud metadata address, so the catalog's origin check reaches the operator's internal network.

**What Stellar makes different.** Cataloging runs after any settlement, so anyone who can pay a cent can make the facilitator fetch a URL.

**Preconditions.** One settlement with a crafted resource URL. **Impact.** Requests from the facilitator's network position; internal data in listing metadata.

**Mitigations**

* Hosts are resolved and every address checked against a block list at dial time, IP literals before any lookup. ([`packages/bazaar/src/origin.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/origin.ts), `safeLookup`)
* Private and loopback addresses are refused unless DISCOVERY\_ALLOW\_LOOPBACK is set, which is refused together with pubnet. ([`apps/rail402/src/config.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/config.ts), `DISCOVERY_ALLOW_LOOPBACK`)

**Tests**

* [`packages/bazaar/test/origin.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/origin.test.ts): refuses a host whose DNS answers are not public, before connecting
* [`packages/bazaar/test/egress.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/egress.test.ts): refuses a private IP literal for origin checks unless loopback is allowed
* [`packages/bazaar/test/security.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/security.test.ts): classifies public and non-public addresses
* [`apps/rail402/test/config.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/config.test.ts): refuses loopback discovery fetches on a service that serves pubnet

**Residual risk.** None known. The RPC URL is operator configuration and trusted.

### X2. DNS rebinding

Mitigated · Information disclosure, Elevation of privilege · boundary TB5 (Facilitator and the open internet) · asset: Operator infrastructure

**Threat.** A hostname answers with a public address when checked and a private one when connected.

**What Stellar makes different.** The SEP-1 domain check fetches stellar.toml from a host the seller names, so a seller's own DNS zone is the natural rebinding vector. That fetch uses the same pinned lookup as origin checks.

**Preconditions.** Control of a DNS zone. **Impact.** As X1.

**Mitigations**

* The address that is checked is the address that is dialled: the lookup used by the connection validates every answer. ([`packages/safe-fetch/src/lookup.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/src/lookup.ts), `pinnedLookup`)

**Tests**

* [`packages/safe-fetch/test/egress.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/egress.integration.test.ts): checks every DNS answer, so a rebinding name cannot reach a private address
* [`packages/safe-fetch/test/address.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/address.test.ts): refuses an empty answer and a mixed one, without naming the address

**Residual risk.** None.

### X3. Redirects to internal hosts

Mitigated · Information disclosure, Elevation of privilege · boundary TB5 (Facilitator and the open internet) · asset: Operator infrastructure

**Threat.** A public origin answers with a redirect to a private address.

**What Stellar makes different.** Rail402 reads stellar.toml only from the listing's own host and follows no redirect for it, so a seller cannot point the SEP-1 check at an internal address.

**Preconditions.** Control of a public origin. **Impact.** As X1.

**Mitigations**

* Origin checks and SEP-1 fetches follow no redirects; buyers follow only same-origin redirects, re-checking each hop, and none after paying. ([`packages/safe-fetch/src/redirect.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/src/redirect.ts), `nextHop`)

**Tests**

* [`packages/bazaar/test/origin.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/origin.test.ts): does not follow redirects
* [`packages/safe-fetch/test/egress.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/egress.integration.test.ts): never follows a redirect towards a private or other destination
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): never follows a redirect after paying

**Conformance runs.** `rejection-audit` case `mcp-redirect-private`.

**Residual risk.** None.

### X4. Oversized or slow answers

Mitigated · Denial of service · boundary TB5 (Facilitator and the open internet) · asset: Availability

**Threat.** An origin answers with a huge, compressed or endless body, or trickles bytes to hold connections open.

**What Stellar makes different.** SEP-1 limits stellar.toml to 100 KB; Rail402 enforces that limit on the decoded file.

**Preconditions.** Control of an origin. **Impact.** Memory exhaustion or stalled workers.

**Mitigations**

* Body and header caps apply after decoding, with header, idle and total timeouts. ([`packages/safe-fetch/src/limits.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/src/limits.ts), `DEFAULT_LIMITS`)

**Tests**

* [`packages/safe-fetch/test/egress.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/egress.integration.test.ts): stops oversized and compressed-bomb answers at the decoded limit
* [`packages/safe-fetch/test/fetch.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/fetch.test.ts): times out waiting for headers, on an idle body, and in total
* [`packages/bazaar/test/stellar-toml.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/stellar-toml.test.ts): refuses a file larger than SEP-1 allows

**Residual risk.** None.

### X5. stellar.toml fetches

Mitigated · Information disclosure · boundary TB5 (Facilitator and the open internet) · asset: Operator infrastructure

**Threat.** The domain check fetches /.well-known/stellar.toml from a host the seller chose.

**What Stellar makes different.** SEP-1 lets a domain claim Stellar accounts; Rail402 reads ACCOUNTS, and the owner's home\_domain, to raise an origin-verified listing's trust to domain\_verified only when both name each other, which means fetching from the seller's host.

**Preconditions.** One listed resource. **Impact.** As X1.

**Mitigations**

* The SEP-1 fetch uses the same dial-time address check and size cap as origin checks. ([`packages/bazaar/src/stellar-toml.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/stellar-toml.ts), `fetchStellarTomlAccounts`)

**Tests**

* [`packages/bazaar/test/stellar-toml.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/stellar-toml.test.ts): never connects to a private address unless loopback is explicitly allowed
* [`packages/bazaar/test/egress.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/test/egress.test.ts): refuses a private IP literal for stellar.toml

**Residual risk.** None.

### X6. Paid calls to agent-chosen URLs

Mitigated · Information disclosure, Elevation of privilege · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** An agent, possibly steered by text in a listing, asks pay\_and\_call (or the SDK or CLI) to call a private address, sending a payment or the agent's request there.

**What Stellar makes different.** None: the buyer's egress runs on the agent's machine.

**Preconditions.** An agent that follows URLs from untrusted content. **Impact.** Requests to the agent's internal network, with a payment header attached.

**Mitigations**

* Every buyer request goes through safe-fetch: public destinations only, the address pinned at dial time, redirects re-checked. ([`packages/safe-fetch/src/fetch.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/src/fetch.ts), `createSafeFetch`)

**Tests**

* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): refuses private destinations before sending anything
* [`packages/safe-fetch/test/egress.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/egress.integration.test.ts): refuses IP-literal URLs to this machine in every spelling, before connecting
* [`packages/safe-fetch/test/egress.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/egress.integration.test.ts): refuses a name that resolves to a private address, without naming the address

**Conformance runs.** `rejection-audit` case `mcp-private-address`.

**Residual risk.** None.

## Smart accounts

A `C…` payer is a contract account whose `__check_auth` decides whether a payment is authorized. Nothing about
it can be checked offline: only executing it tells, and its answer can change between simulation and
inclusion. Rail402 simulates in enforcing mode at verify and again at settle; policy composition with `upto`
is proved in contract tests with OpenZeppelin smart accounts.

### A1. A payment signed by a key the account does not accept

Mitigated · Spoofing · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** Someone signs a C-account payment with a key that is not one of the smart account's signers.

**What Stellar makes different.** A contract account authorizes through its own \_\_check\_auth; only executing it can tell whether a signature is valid. There is no signature a facilitator could check offline.

**Preconditions.** Knowledge of the smart account's address. **Impact.** None if detected; the account's \_\_check\_auth refuses.

**Mitigations**

* Verify and settle both run an enforcing simulation, which executes \_\_check\_auth; a refusal is named smart\_account\_rejected. ([`packages/stellar/src/diagnostics.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/diagnostics.ts), `classifyTransferFailure`)

**Tests**

* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): rejects a smart-account payment signed by the wrong key
* [`packages/stellar/test/diagnostics.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/diagnostics.test.ts): a smart-account payer rejecting in \_\_check\_auth → smart\_account\_rejected
* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): attributes refusals inside a smart account's \_\_check\_auth to the account

**Conformance runs.** `live-matrix` case `c-payer-wrong-key`, `rejection-audit` case `upto-c-payer-wrong-key`.

**Residual risk.** None.

### A2. \_\_check\_auth that passes simulation and fails at apply

Accepted risk · Denial of service · boundary TB7 (Contracts) · asset: Sponsor XLM

**Threat.** A smart account whose \_\_check\_auth depends on state it can change (a nonce, a time, a policy budget) passes simulation and fails at inclusion, deliberately.

**What Stellar makes different.** \_\_check\_auth is arbitrary contract code. Its result at apply time can differ from simulation, and the included, failed transaction's fee is the sponsor's.

**Preconditions.** A smart account the attacker controls. **Impact.** One wasted sponsor fee per payment.

**Mitigations**

* The simulation is repeated at settle, so state changed before settle is caught. ([`packages/facilitator/src/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/scheme.ts), `settle`)
* Wasted fees are metered; the hourly budget bounds the total. ([`packages/facilitator/src/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/guard.ts), `SponsorBudget`)

**Tests**

* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): names a smart account whose \_\_check\_auth passes the settle-time simulation and fails at inclusion; the fee is counted as wasted
* [`packages/facilitator/test/fee-exposure.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/fee-exposure.integration.test.ts): stops settling once the hourly fee budget is committed, before any RPC work

**Residual risk.** Like S1, the race within one ledger cannot be closed. A per-payer limit on on-chain failures is not built; the hourly budget is the bound.

### A3. Policy events refuse exact payments from policy accounts

Accepted risk · Denial of service · boundary TB7 (Contracts) · asset: Availability

**Threat.** A smart account with a policy that emits its own events (such as a spending limit recording a spend) pays with exact.

**What Stellar makes different.** The upstream @x402/stellar exact verifier accepts only the token's transfer event in the simulation and refuses any other contract event as invalid\_exact\_stellar\_payload\_event\_not\_transfer. The upto verifier accepts events other than balance changes, so policy events do not refuse upto payments.

**Preconditions.** A payer whose \_\_check\_auth publishes events. **Impact.** Such accounts cannot pay with exact through any facilitator built on the upstream verifier; nothing moves and the sponsor pays nothing.

**Mitigations**

* The refusal is passed through with its registered code and reason. ([`packages/stellar/src/codes.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/codes.ts), `invalid_exact_stellar_payload_event_not_transfer`)
* upto accepts non-balance events such as a policy's own. ([`packages/upto/src/facilitator/simulation.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/simulation.ts), `checkSimulationEvents`)

**Tests.** None: this is a documented limit, not a behaviour Rail402 implements.

**Residual risk.** An upstream limit of the exact scheme; it fails closed. Policy accounts pay with upto.

### A4. A spending limit that does not cover upto

Mitigated · Elevation of privilege · boundary TB7 (Contracts) · asset: Agent budgets

**Threat.** An agent key constrained by a spending-limit policy pays with upto, whose settlement reaches the account as an approve rather than a transfer, and slips past a limit that only counts transfers.

**What Stellar makes different.** The upto contract spends through approve(payer, contract, cap, expiration) and transfer\_from, so the account's \_\_check\_auth sees an approve context for the full cap.

**Preconditions.** A smart account whose policy budgets transfer only. **Impact.** Spending beyond the account owner's budget.

**Mitigations**

* The approve-spending-limit policy counts approve and transfer against one rolling budget and reserves the full cap of each upto payment. ([`contracts/testing/approve-spending-limit/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/approve-spending-limit/src/lib.rs), `SpendingLimitExceeded`)

**Tests**

* [`contracts/testing/approve-spending-limit/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/approve-spending-limit/src/test.rs): transfers\_and\_approvals\_share\_one\_budget
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): payments\_within\_the\_budget\_settle
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): a\_fourth\_payment\_after\_the\_budget\_is\_used\_fails\_in\_the\_policy
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): a\_cap\_above\_the\_remaining\_budget\_fails\_even\_for\_a\_small\_amount

**Conformance runs.** `live-matrix` case `upto-c-over-limit`.

**Residual risk.** OpenZeppelin's stock spending-limit policy refuses the approve context outright; accounts that use it cannot pay with upto.

### A5. Bypassing the policy through the Default rule

Mitigated · Elevation of privilege · boundary TB7 (Contracts) · asset: Agent budgets

**Threat.** An agent key picks a context rule without the spending policy (the Default rule, or the policy-free upto rule for the approve), or a relayer moves a signed payment to another rule.

**What Stellar makes different.** In OpenZeppelin smart accounts the signer chooses the rule per context, so a key that appears on a rule without the policy can spend without it.

**Preconditions.** An agent key listed on a rule without the policy. **Impact.** Spending beyond the budget.

**Mitigations**

* The documented rule configuration keeps the agent key off every rule without the policy, and the signature covers the rule ids. ([`contracts/testing/README.md`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/README.md), `Rule configuration for an agent key`)

**Tests**

* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): the\_agent\_key\_cannot\_use\_the\_default\_rule
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): the\_agent\_key\_cannot\_route\_the\_approval\_around\_the\_policy
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): the\_rule\_selection\_is\_bound\_by\_the\_signature

**Residual risk.** Depends on the account owner configuring rules as documented.

### A6. Concurrent payments beyond the account's own budget

Accepted risk · Denial of service · boundary TB7 (Contracts) · asset: Sponsor XLM

**Threat.** Several payments from one policy account are verified at once; together they exceed the account's budget, and the later ones fail at apply.

**What Stellar makes different.** The policy's budget is on-chain state updated at apply time; simulations of concurrent payments each see the budget before the others.

**Preconditions.** A policy account paying several sellers at once. **Impact.** Wasted sponsor fees for the payments that fail.

**Mitigations**

* The policy refuses on chain; the failure is named and counted as wasted, within the hourly budget. ([`packages/facilitator/src/explain.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/explain.ts), `explainFailedTransaction`)

**Tests**

* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): a\_fourth\_payment\_after\_the\_budget\_is\_used\_fails\_in\_the\_policy
* [`contracts/testing/approve-spending-limit/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/approve-spending-limit/src/test.rs): spends\_leave\_the\_window\_after\_period\_ledgers

**Residual risk.** As S1.

## MCP paid calls

`rail402-mcp`, the SDK and the CLI hold a signing key on the agent's side. The threats are about the agent's
money: paying more than allowed, paying the wrong party, leaking the key, paying twice, and following URLs it
should not.

### M1. An agent spends beyond its caps

Mitigated · Elevation of privilege · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** A model, a prompt injection or a bug makes an agent pay more per call or per session than its owner allowed, including through many concurrent calls or several processes.

**What Stellar makes different.** A signed authorization is spendable by whoever holds it until it expires, so the cap must be enforced before signing, not after.

**Preconditions.** An agent with a signing key and a budget. **Impact.** Loss of the agent owner's funds up to the key's balance.

**Mitigations**

* A guarded signer checks the per-call cap and reserves the session budget immediately before signing, and signs only the payment it approved. ([`packages/sdk/src/policy/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/guard.ts), `createGuardedSigner`)
* The budget ledger reserves atomically, also across processes sharing one SQLite file. ([`packages/sdk/src/policy/ledger.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/ledger.ts), `checkReservation`)

**Tests**

* [`packages/sdk/test/guard.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/guard.test.ts): never lets 20 concurrent payments exceed the session budget
* [`packages/sdk/test/sqlite-ledger.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/sqlite-ledger.test.ts): never lets four processes sharing one window exceed its budget
* [`packages/sdk/test/guard.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/guard.integration.test.ts): refuses to sign a payment above the per-call cap
* [`packages/mcp/test/pay.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/pay.integration.test.ts): refuses a price above the caller's cap before signing
* [`packages/sdk/test/guard.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/guard.test.ts): refuses a second signature within one call and keeps the first one's budget pending

**Conformance runs.** `rejection-audit` case `mcp-per-call-cap`, `rejection-audit` case `mcp-over-ceiling`.

**Residual risk.** The caps bound the loss; anything within them the agent may spend.

### M2. Payments on networks, assets or to recipients not allowed

Mitigated · Elevation of privilege · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** A seller's 402 asks for another network, another asset, or a recipient outside the allow list.

**What Stellar makes different.** Assets are contracts; the same symbol can be issued by anyone. The policy therefore compares contract ids and network passphrases, not symbols.

**Preconditions.** A seller offering unexpected terms. **Impact.** Payment in the wrong asset or to the wrong party.

**Mitigations**

* Spend decisions check the network in the signed preimage, the asset contract and the recipient against the policy. ([`packages/sdk/src/policy/decide.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/decide.ts), `decideSpend`)

**Tests**

* [`packages/sdk/test/decide.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/decide.test.ts): refuses another network, whether in the preimage or in the requested passphrase
* [`packages/sdk/test/policy.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/policy.test.ts): compares muxed recipients by base account and refuses self-payment
* [`packages/sdk/test/policy.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/policy.test.ts): refuses an option priced in a different asset than the cap

**Conformance runs.** `rejection-audit` case `mcp-network-not-permitted`, `rejection-audit` case `mcp-asset-not-permitted`, `rejection-audit` case `mcp-recipient-not-permitted`.

**Residual risk.** None.

### M3. Secrets in tool schemas, results, errors or logs

Mitigated · Information disclosure · boundary TB6 (Agent, MCP server and paid URLs) · asset: Secrets

**Threat.** The signing seed, its raw bytes, a signature or a payment header leaks into an MCP tool schema, a tool result, a protocol error, a log line, the budget ledger or the CLI's output.

**What Stellar makes different.** A Stellar seed (S…) or its 32 raw bytes is the whole key; a leaked signed authorization is spendable until it expires.

**Preconditions.** Any error or log path that echoes inputs, or a seller that echoes the payment into its answer. **Impact.** Loss of the agent's funds.

**Mitigations**

* Errors and results are redacted and sanitised; loggers redact secret-like fields. ([`packages/sdk/src/redact.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/redact.ts), `redactDeep`)
* Tool schemas are published without secret-like properties or header mirroring. ([`packages/mcp/src/schemas.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/src/schemas.ts), `publishedSchema`)
* A seller that echoes its request cannot hand the payment back: a paid answer is returned and stored with the call's payment header, payload, signed entries and signatures replaced. ([`packages/sdk/src/flow/scrub.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/flow/scrub.ts), `scrubOutcome`)

**Tests**

* [`packages/mcp/test/secrets.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/secrets.integration.test.ts): leaks nothing into frames, logs or the ledger
* [`packages/sdk/test/scrub.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/scrub.test.ts): removes the payment from an HTTP answer and its stored replay, keeping the rest
* [`packages/sdk/test/scrub.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/scrub.test.ts): removes the payment from an MCP tool's content and structured content
* [`packages/sdk/test/secrets.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/secrets.integration.test.ts): leaks nothing, and the payment reached only the paid origin
* [`packages/cli/test/secrets.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/cli/test/secrets.integration.test.ts): leaks nothing on stdout, stderr or in the ledger
* [`packages/mcp/test/schemas.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/schemas.test.ts): publishes self-contained object schemas without header mirroring or secret-like fields
* [`packages/mcp/test/logger.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/logger.test.ts): writes JSON lines at or above its level, redacting secrets in the message and nested fields

**Residual risk.** The key lives in the agent's process; a compromised host is out of scope.

### M4. A retry pays twice

Mitigated · Tampering · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** An agent retries a paid call after a timeout or crash and pays again for the same work.

**What Stellar makes different.** Each new authorization carries a fresh nonce, so a retry signed anew is a second payment.

**Preconditions.** A retried tool call. **Impact.** Double payment.

**Mitigations**

* Calls carry an idempotency key; a repeated key replays the stored outcome without signing, and the payment identifier travels to the seller. ([`packages/sdk/src/flow/common.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/flow/common.ts), `replayStored`)
* A repeated key first reconciles its own payment on chain: it reports a consumed authorization as paid, and frees the key for a new payment only once the chain shows the authorization expired unused. ([`packages/sdk/src/flow/common.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/flow/common.ts), `replayStored`)

**Tests**

* [`packages/mcp/test/tools.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/tools.test.ts): replays a stored result for a repeated idempotency key
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): replays a finished call for a repeated idempotency key without signing again
* [`packages/mcp/test/pay.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/pay.integration.test.ts): pays an HTTP resource by URL without custody, and replays a repeated key without paying
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): frees an idempotency key on the retry itself, once the chain shows its authorization unused
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): reports a keyed retry as paid once the chain shows its authorization consumed

**Residual risk.** A retry without the same key is a new payment by design.

### M5. Prompt-injected listings and bait-and-switch sellers

Accepted risk · Spoofing, Tampering · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** A listing's text steers the agent, or the seller's live 402 asks for a different recipient or a higher price than its listing.

**What Stellar makes different.** The listing's payTo is the account the facilitator saw settled; the live 402 is not bound to it unless the buyer checks.

**Preconditions.** A malicious or changed seller. **Impact.** Payment to an unexpected party or at an unexpected price.

**Mitigations**

* Paying through a listing binds its recipient and its listed price (HTTP resources and MCP tools at an http(s) endpoint) before anything is signed. ([`packages/sdk/src/policy/decide.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/decide.ts), `checkOption`)
* Tool results bound and strip seller content. ([`packages/mcp/src/results.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/src/results.ts), `sellerJson`)

**Tests**

* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): binds a listing's recipient and its price
* [`packages/sdk/test/mcp-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/mcp-flow.test.ts): binds a listing's price: a live price above the listed one is refused before signing
* [`packages/sdk/test/policy.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/policy.test.ts): binds the recipient of every listing and the price a listing carries
* [`packages/sdk/test/discovery.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/discovery.test.ts): binds the listed prices of HTTP listings and of MCP tools at an http(s) endpoint
* [`packages/sdk/test/discovery.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/discovery.test.ts): refuses listings that cannot be paid before anything is fetched from the seller
* [`packages/mcp/test/tools.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/tools.test.ts): strips control characters and drops oversized seller JSON

**Residual risk.** An agent still reads seller-controlled text; the caps (M1) bound what it can spend.

### M6. A tampered payment or a false settlement report

Mitigated · Tampering, Repudiation · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** A scheme client changes the payload after the guard approved it, or a seller reports a settlement that did not happen, reports another payment's transaction hash, or settles through its own contract so the transaction does not look like a direct settlement.

**What Stellar makes different.** The host records every authorization nonce it consumes in a temporary ledger entry keyed by the payer and the nonce, and only the entry the payer signed can consume it. A transaction used a payment's authorization exactly when it carries the payer's entry with that payment's nonce and its meta creates that nonce entry, whatever its call path: Soroban matches an entry below the top-level call, so a wrapper, router or batch contract can settle it too, and one transaction can consume several nonces. A transfer event alone says nothing about which authorization moved the money.

**Preconditions.** A malicious scheme client or seller. **Impact.** Paying for something else, believing a payment failed or succeeded wrongly, or a session budget that under-counts what moved.

**Mitigations**

* The guard compares the payload with what it signed. ([`packages/sdk/src/policy/guard.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/guard.ts), `checkGuardedPayload`)
* A payment is proven only by its own nonce: a transaction that consumed it settles the reservation whatever its shape, for the exact price, or for upto the amount the signed settlement contract recorded for the signed terms (the whole cap when that cannot be read). A consumed authorization is never released. ([`packages/sdk/src/policy/proof.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/proof.ts), `reservationSettlement`)
* A reported hash is only a claim against the payment it was reported for and counts as spent; it never removes a transaction from another payment's proof. Budget returns only when the authorization has expired and no transaction in its validity range consumed it. ([`packages/sdk/src/policy/reconcile.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/reconcile.ts), `reconcile`)
* An expired payment the RPC can no longer prove either way counts as spent at its whole amount and is no longer reconciled. ([`packages/sdk/src/sqlite/ledger.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/sqlite/ledger.ts), `markUnprovable`)

**Tests**

* [`packages/sdk/test/guard.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/guard.test.ts): refuses a payload whose transaction differs from the signed authorization; the budget stays pending
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): refuses a tampering scheme client after the probe, with nothing sent to the seller
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): verifies a reported settlement on chain
* [`packages/sdk/test/http-flow.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/http-flow.test.ts): counts a reused transaction hash against each payment that reports it
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): never lets a claim of another payment's hash release that payment (%s ledger)
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): settles every payment a batch transaction consumed (%s ledger)
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): settles each payer's payment from one batch transaction in a shared ledger file
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): counts an upto settlement made through another contract (%s ledger)
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): settles a payment only from a transfer made with its own authorization
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): never lets another payment's transfer release an upto cap early (%s ledger)
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): does not count an authorization carried in a transaction that never consumed it
* [`packages/sdk/test/reconcile.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/test/reconcile.test.ts): counts expired rows the RPC can no longer prove as spent, and stops checking them (%s ledger)

**Residual risk.** A payment that is not reconciled before the RPC's retention ends counts as spent in full, even if it never moved money.

### M7. Another local process drives the MCP server over HTTP

Mitigated · Spoofing, Elevation of privilege · boundary TB6 (Agent, MCP server and paid URLs) · asset: Agent budgets

**Threat.** A web page (DNS rebinding) or another local process reaches the MCP server's HTTP transport and spends through its tools.

**What Stellar makes different.** The MCP server holds the agent's Stellar signing key. Any caller that reaches its tools can make it sign Soroban authorization entries, bounded only by its per-call and session caps (M1) and its network, asset and recipient allow lists (M2); keeping other callers out is what keeps that key the agent's.

**Preconditions.** The HTTP transport enabled. **Impact.** Spending by an unauthorised caller, within the caps.

**Mitigations**

* The HTTP transport listens on loopback, requires a bearer token and refuses foreign Host and Origin headers. ([`packages/mcp/src/http.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/src/http.ts), `hostHeaderValidationResponse`)

**Tests**

* [`packages/mcp/test/http.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/http.test.ts): refuses a Host outside the allowlist (DNS rebinding) with 403
* [`packages/mcp/test/http.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/http.test.ts): refuses a browser Origin outside the allowlist with 403
* [`packages/mcp/test/http.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/mcp/test/http.test.ts): listens on loopback and serves a client that presents the token

**Residual risk.** None; stdio is the default transport.

## The upto contract

With `upto` the payer signs a cap, and the settlement authority named in the terms chooses the amount at
settlement. The contract `x402-upto-stellar` has no admin, no storage and no upgrade path; the payer's
signature covers the terms and an `approve` of the cap to the contract, and the authority's authorization
covers the amount.

### U1. Settling more than the cap

Mitigated · Tampering, Elevation of privilege · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** The settlement authority, a bug or a hostile seller settles an amount above the cap the payer signed.

**What Stellar makes different.** The payer's signature covers approve(payer, contract, max\_amount, expiration\_ledger); the token would refuse a transfer\_from above the allowance, and the contract refuses first.

**Preconditions.** Control of the settle arguments. **Impact.** None: refused.

**Mitigations**

* The contract refuses amount > max\_amount with error #3 and moves nothing. ([`contracts/stellar/upto/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/lib.rs), `AmountExceedsMax`)
* The facilitator refuses before simulating. ([`packages/upto/src/facilitator/rules.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/rules.ts), `checkUptoPayment`)

**Tests**

* [`contracts/stellar/upto/src/test/errors.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/errors.rs): rejects\_an\_amount\_over\_the\_cap
* [`contracts/stellar/upto/src/test/errors.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/errors.rs): a\_rejected\_settlement\_moves\_nothing
* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): refuses an amount above the cap before simulating
* [`contracts/testing/composition/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/composition/src/test.rs): an\_amount\_over\_the\_cap\_is\_contract\_error\_3

**Conformance runs.** `live-matrix` case `upto-over-cap`, `rejection-audit` case `upto-settle-over-cap`.

**Residual risk.** None.

### U2. Changing the recipient, token, expiry, nonce or tree

Mitigated · Tampering · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** The terms or the authorization tree are changed after signing: another recipient, token, facilitator, expiration, nonce or an extra sub-invocation.

**What Stellar makes different.** Every term is an argument of the signed settle(terms) root invocation and of its approve child; the host checks the signature over the whole tree, and the network id and nonce are in the preimage.

**Preconditions.** A copy of the payload. **Impact.** None: signature verification fails.

**Mitigations**

* The facilitator checks every bound field and the exact tree shape before simulating; the host rejects any change the facilitator missed. ([`packages/upto/src/authorization.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/authorization.ts), `inspectUptoAuthorization`)

**Tests**

* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): mutated\_terms\_are\_rejected
* [`contracts/stellar/upto/src/test/signed.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/signed.rs): a\_signature\_over\_other\_terms\_fails
* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses terms changed consistently in the entry and the requirements
* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses a changed nonce
* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses a signature expiration moved later than signed
* [`packages/upto/test/vectors.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/vectors.test.ts): cover every bound field and tree shape

**Conformance runs.** `rejection-audit` case `upto-wrong-recipient`, `rejection-audit` case `upto-wrong-asset`, `rejection-audit` case `upto-authorization-tree`, `rejection-audit` case `upto-forged-terms`.

**Residual risk.** None.

### U3. Settling one authorization twice

Mitigated · Tampering · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** The same upto authorization is settled twice, for the same or different amounts.

**What Stellar makes different.** The payer's entry nonce is consumed by the host on first use and kept until the signature expires; a failed settlement rolls the nonce back.

**Preconditions.** A copy of the payload and the authority. **Impact.** None: the second use fails with Error(Auth, ExistingValue); Rail402 answers repeats from its ledger.

**Mitigations**

* Host nonce consumption; Rail402's settlement ledger and request fingerprint including the settle amount. ([`packages/facilitator/src/fingerprint.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/fingerprint.ts), `uptoFingerprintOf`)

**Tests**

* [`contracts/stellar/upto/src/test/signed.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/signed.rs): a\_second\_use\_of\_the\_same\_entry\_fails
* [`contracts/stellar/upto/src/test/signed.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/signed.rs): a\_failed\_settlement\_does\_not\_consume\_the\_nonce
* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses a replay of a settled authorization
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): settles 5 concurrent requests for different amounts once: one success, four conflicts

**Conformance runs.** `live-matrix` case `upto-repeat-different-amount`, `rejection-audit` case `upto-replay`, `live-matrix` case `upto-concurrent-same-authorization`.

**Residual risk.** None.

### U4. Settling with an authority the payer did not name

Mitigated · Spoofing, Elevation of privilege · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** A facilitator, or anyone, settles an upto payment that named another settlement authority.

**What Stellar makes different.** terms.facilitator is part of the signed terms and must authorize the settle call itself.

**Preconditions.** A copy of the payload. **Impact.** None: refused.

**Mitigations**

* Requirements whose facilitatorAddress is not this facilitator's authority are refused; the contract requires terms.facilitator's authorization. ([`packages/upto/src/errors.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/errors.ts), `invalid_upto_stellar_requirements_facilitator_mismatch`)

**Tests**

* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses an authority other than the one the payer named
* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): missing\_facilitator\_authorization\_is\_rejected
* [`contracts/stellar/upto/src/test/auth.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/auth.rs): facilitator\_authorization\_for\_another\_amount\_is\_rejected

**Conformance runs.** `rejection-audit` case `upto-terms-authority`, `rejection-audit` case `upto-authority-not-ours`.

**Residual risk.** None.

### U5. The allowance left by a partial settlement

Mitigated · Elevation of privilege · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** After a partial settlement, the remaining allowance (cap minus amount) is spent by someone else.

**What Stellar makes different.** The signed approve grants the settlement contract an allowance of the full cap until the expiration ledger; transfer\_from spends only the settled amount.

**Preconditions.** A partial upto settlement. **Impact.** None: only the contract can spend the allowance, and it spends only inside a settlement.

**Mitigations**

* The allowance's spender is the settlement contract; it expires with the terms and the next settlement's approve overwrites it. ([`contracts/stellar/upto/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/lib.rs), `transfer_from`)

**Tests**

* [`contracts/stellar/upto/src/test/allowance.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/allowance.rs): nobody\_can\_spend\_the\_residual\_directly
* [`contracts/stellar/upto/src/test/allowance.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/allowance.rs): another\_contract\_cannot\_spend\_the\_residual
* [`contracts/stellar/upto/src/test/allowance.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/allowance.rs): the\_next\_settlement\_overwrites\_the\_residual
* [`contracts/stellar/upto/src/test/allowance.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/allowance.rs): the\_residual\_expires\_with\_the\_terms
* [`contracts/stellar/upto/src/test/settle.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/settle.rs): zero\_leaves\_an\_existing\_allowance\_untouched

**Residual risk.** Wallets show an allowance to the settlement contract until the expiration ledger.

### U6. Zero settlements paid by the sponsor

Accepted risk · Denial of service · boundary TB2 (Seller and facilitator) · asset: Sponsor XLM

**Threat.** Anyone settles many upto payments for zero; each on-chain zero settlement costs the sponsor a fee and moves nothing. A zero needs no token balance or trustline, so the caller needs only an account that can sign.

**What Stellar makes different.** An on-chain zero consumes the nonce (so the authorization cannot be reused) but costs a Soroban transaction.

**Preconditions.** Sellers or buyers that trigger zero settlements. **Impact.** Sponsor fees for payments that earn nothing.

**Mitigations**

* UPTO\_ZERO\_SETTLEMENT=offchain records a zero as a terminal success without a transaction, in the durable settlement ledger (STORE=postgres is required for it). Pubnet defaults to offchain; testnet stays on-chain for a public on-chain record. ([`apps/rail402/src/config.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/config.ts), `zeroSettlement`)
* Pubnet refuses to start without an explicit hourly sponsor budget, which bounds on-chain zeros with every other settlement. ([`apps/rail402/src/config.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/config.ts), `PUBNET_MAX_SPONSOR_SPEND_XLM_PER_HOUR`)
* Zero settlements are never cataloged, on-chain or off-chain. Content planted with a settlement of one base unit is not published either: listings with an origin publish only what the origin answers (P1, P9). ([`packages/bazaar/src/codes.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/codes.ts), `bazaar_settlement_zero`)

**Tests**

* [`packages/facilitator/test/upto-scheme.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto-scheme.test.ts): settles zero off-chain when configured: no transaction, the authorization consumed in the ledger
* [`packages/facilitator/test/upto-scheme.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto-scheme.test.ts): drops the claim when an off-chain zero cannot settle, so it may be retried
* [`apps/rail402/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto.integration.test.ts): never catalogs an off-chain zero settlement
* [`packages/bazaar/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/bazaar/src/testing.ts): does not catalog a settlement that moved nothing, however high its cap
* [`apps/rail402/test/config.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/config.test.ts): requires explicit, safe mainnet settings

**Conformance runs.** `live-matrix` case `upto-zero`, `live-bazaar` case `upto-offchain-zero`.

**Residual risk.** On the keyless testnet service, zeros settle on-chain and each costs one fee, bounded by the per-client rate limit (IPv6 clients are grouped by /64) and, where configured, the hourly budget. A client with many addresses passes the rate limit, so the budget is the real bound, after which every settlement is refused for the rest of the hour. On pubnet zeros settle off-chain by default and all spend is bounded by the required hourly budget.

### U7. The settlement contract archived

Mitigated · Denial of service · boundary TB7 (Contracts) · asset: Availability

**Threat.** The upto contract's instance or code entry reaches the end of its TTL and is archived, so every upto payment fails.

**What Stellar makes different.** Persistent entries are archived at liveUntilLedgerSeq; any invocation needs them restored. The contract keeps no storage of its own, so only the instance and code entries pay rent.

**Preconditions.** Nobody extends the entries for months. **Impact.** upto payments unavailable; restoring inside a payment would bill the buyer's sponsor (S7).

**Mitigations**

* The TTL monitor warns at 30 days and turns critical at 7; the keeper extends both entries before they run low. ([`apps/rail402/src/upto-keeper.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/upto-keeper.ts), `UptoTtlKeeper`)
* An operator command restores and extends where automatic restoration is off. ([`packages/facilitator/src/contract-ttl.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/contract-ttl.ts), `restoreContract`)

**Tests**

* [`apps/rail402/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto.integration.test.ts): extends the contract from the keeper and the operator command, and reports its TTL
* [`apps/rail402/test/upto-archival.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto-archival.integration.test.ts): is restored by the TTL keeper where automatic restoration is allowed
* [`apps/rail402/test/upto-archival.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto-archival.integration.test.ts): refuses payments while the contract is archived, and settles once an operator restores it
* [`packages/facilitator/test/contract-ttl.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/contract-ttl.test.ts): never extends past the network's maximum TTL
* [`contracts/stellar/upto/src/test/settle.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/test/settle.rs): an\_archived\_instance\_is\_restored\_by\_the\_invocation\_that\_uses\_it

**Residual risk.** None while the keeper runs; extension and restoration are permissionless, so anyone can also keep the contract live.

### U8. A different or changed settlement contract

Mitigated · Tampering, Elevation of privilege · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** Requirements name a hostile contract as the settlement contract, or the canonical contract's code is replaced.

**What Stellar makes different.** Contract addresses derive from the deployer and a salt; the deployed WASM hash is readable from the instance. A contract without an admin or upgrade function cannot change its code.

**Preconditions.** A seller or configuration pointing at another contract. **Impact.** A contract that spends the approved allowance differently.

**Mitigations**

* The contract has no admin, no storage and no upgrade path. ([`contracts/stellar/upto/src/lib.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/stellar/upto/src/lib.rs), `cannot be upgraded`)
* Payments naming another contract than the network's configured one are refused; the service refuses to start when the configured contract runs other code. ([`packages/upto/src/errors.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/errors.ts), `invalid_upto_stellar_payload_wrong_contract`)

**Tests**

* [`packages/upto/test/deployment.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/deployment.test.ts): runs the committed wasm
* [`packages/upto/test/deployment.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/deployment.test.ts): sits at the address derived from the deployer and the canonical salt
* [`apps/rail402/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/upto.integration.test.ts): refuses to start with a contract running other code, and starts without its authority
* [`packages/upto/test/codec.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/codec.test.ts): resolves the settlement contract from overrides and canonical deployments

**Residual risk.** An operator can configure a contract of their own (UPTO\_TESTNET\_DEPLOYMENT); buyers see it in /supported.

### U9. Token differences between SAC and SEP-41 contracts

Mitigated · Tampering · boundary TB7 (Contracts) · asset: Payer funds

**Threat.** A token whose events or allowance semantics differ from a Stellar Asset Contract is mis-read, so a wrong transfer passes the checks or a right one is refused.

**What Stellar makes different.** Stellar Asset Contracts publish \[name, from, to, asset] topics; OpenZeppelin SEP-41 tokens publish \[name, from, to] with a bare amount, and their approve data is a map with live\_until\_ledger. Decimals are read from the token.

**Preconditions.** A custom SEP-41 asset. **Impact.** False acceptance or refusal of upto settlements.

**Mitigations**

* Both event shapes are accepted and checked field by field; exactly one transfer of the amount from the payer to payTo, and no balance event involving a facilitator account. ([`packages/upto/src/facilitator/simulation.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/simulation.ts), `checkSimulationEvents`)

**Tests**

* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): accepts SEP-41 token events (three topics, map data)
* [`packages/upto/test/facilitator.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/facilitator.test.ts): requires exactly one transfer of the cap from the payer to payTo
* [`contracts/testing/test-token/src/test.rs`](https://github.com/tolgayayci/rail402/blob/main/contracts/testing/test-token/src/test.rs): approve\_event\_has\_three\_topics\_and\_map\_data
* [`tools/conformance/test/assets.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/tools/conformance/test/assets.integration.test.ts): refuses a payer holding less than the amount, or none of the token, as insufficient\_funds

**Conformance runs.** `live-bazaar` case `second-asset`.

**Residual risk.** Tokens with fees on transfer or rebasing balances are not supported; their settlements fail the event check.

### U10. The settlement authority's key is compromised

Accepted risk · Elevation of privilege · boundary TB4 (Facilitator and its state) · asset: Payer funds

**Threat.** The authority's key, which is derived from the sponsor secret, leaks.

**What Stellar makes different.** The authority chooses the amount of any upto payment that names it, up to each payment's cap, within the payment's validity window; it holds no funds and cannot change recipient or token.

**Preconditions.** Leak of the sponsor secret. **Impact.** Payloads the attacker obtains could be settled for up to their caps, to their named recipients, until they expire; the sponsor's XLM is at risk.

**Mitigations**

* The authority is derived per network with HKDF from the sponsor secret, distinct from the sponsor and the channels, and holds a zero balance. ([`packages/facilitator/src/authority.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/authority.ts), `deriveAuthorityKeypair`)
* Rotation with the sponsor: payments naming the old authority are refused by the new one and expire within the maximum timeout (300 s by default). ([`docs/operations.md`](https://github.com/tolgayayci/rail402/blob/main/docs/operations.md), `Rotating the sponsor key`)

**Tests**

* [`packages/facilitator/test/authority.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/authority.test.ts): is stable per sponsor and network, and distinct from the sponsor and every channel
* [`packages/facilitator/test/authority.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/authority.test.ts): reports whether the account exists and what it holds
* [`packages/upto/test/upto.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/test/upto.integration.test.ts): refuses an authority other than the one the payer named

**Residual risk.** Within the maximum timeout after a leak, captured payloads can be charged up to their caps. Keep caps near expected prices and timeouts short.

### U11. A served buyer defaults before settlement

Accepted risk · Repudiation · boundary TB2 (Seller and facilitator) · asset: Seller revenue

**Threat.** An upto seller serves first and settles afterwards. Between /verify and the seller's /settle the buyer moves its funds, uses its authorization nonce in another transaction, or settles the payload itself for zero, and the seller's settlement fails for work already done.

**What Stellar makes different.** /verify simulates the settlement for the full cap, so the signature, balance and trustline hold at that moment, but a Soroban authorization is not an escrow: nothing is locked until the settlement transaction applies.

**Preconditions.** A buyer willing to default, and a seller that releases output before its own settlement succeeds. **Impact.** One request served unpaid. A streaming or metered seller cannot withhold what it already sent.

**Mitigations**

* Short maxTimeoutSeconds, settling immediately after serving, withholding buffered output until settlement succeeds (the stock Express middleware does), and refusing payers whose settlements fail. ([`docs/facilitator/upto.mdx`](https://github.com/tolgayayci/rail402/blob/main/docs/facilitator/upto.mdx), `Payment is not guaranteed after verification`)

**Tests.** None: this is a documented limit, not a behaviour Rail402 implements.

**Residual risk.** Accepted: upto is post-paid by design; the loss is bounded by one request's cap.

## `exact` payments

The `exact` scheme is verified by the upstream `@x402/stellar` verifier, which Rail402 wraps with its own
checks before any RPC work and with its settlement engine after.

### E1. Tampering with amount, recipient or asset

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Payer funds

**Threat.** A seller or intermediary changes the amount, payTo or asset of an exact payment after the payer signed it.

**What Stellar makes different.** The payer signs transfer(from, to, amount) on the asset contract; any change alters the invocation the host checks the signature against.

**Preconditions.** A copy of the payload. **Impact.** None: refused at verify, or fails signature verification.

**Mitigations**

* The transfer must match the requirements; the payer's entry must authorize exactly the operation's invocation. ([`packages/facilitator/src/preflight.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/preflight.ts), `invalid_exact_stellar_payload_auth_invocation_mismatch`)

**Tests**

* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): rejects tampering: a changed amount breaks the signed authorization
* [`packages/facilitator/test/preflight.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/preflight.test.ts): rejects a transfer that does not match the requirements
* [`packages/facilitator/test/preflight.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/preflight.test.ts): rejects an authorization signed for a different call than the operation

**Conformance runs.** `live-matrix` case `tamper-amount`, `live-matrix` case `tamper-recipient`, `rejection-audit` case `tampered-amount`.

**Residual risk.** None.

### E2. The facilitator made a party to the payment

Mitigated · Elevation of privilege · boundary TB2 (Seller and facilitator) · asset: Sponsor XLM

**Threat.** A payload names a facilitator account (sponsor, channel or upto authority) as payer, transaction or operation source, or co-signer, so the facilitator's signature would move its own funds.

**What Stellar makes different.** The facilitator signs the envelope and the fee bump; if one of its accounts were the source of a transfer or the subject of an auth entry, its signature would authorize it.

**Preconditions.** Knowledge of the facilitator's public addresses (they are in /supported). **Impact.** Loss of the facilitator's funds; custody.

**Mitigations**

* Any facilitator account as payer, source or authorization party is refused. ([`packages/facilitator/src/preflight.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/preflight.ts), `invalid_exact_stellar_payload_facilitator_in_auth`)

**Tests**

* [`packages/facilitator/test/preflight.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/preflight.test.ts): protects facilitator accounts
* [`packages/facilitator/test/preflight.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/preflight.test.ts): rejects a facilitator account as payer or co-signer
* [`packages/facilitator/test/upto-scheme.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto-scheme.test.ts): treats the authority as a facilitator account for exact payments too
* [`test/agents.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/test/agents.integration.test.ts): paid the seller exactly what was settled, from the payer, never from the sponsor

**Conformance runs.** `rejection-audit` case `facilitator-as-cosigner`, `rejection-audit` case `facilitator-as-payer`, `rejection-audit` case `facilitator-as-source`.

**Residual risk.** None.

### E3. Extra invocations riding on the payment

Mitigated · Tampering, Elevation of privilege · boundary TB2 (Seller and facilitator) · asset: Payer funds

**Threat.** The authorization tree carries sub-invocations, or the transaction carries more operations or preconditions, so that settling the payment does more than one transfer.

**What Stellar makes different.** An authorization entry authorizes a tree of calls; a transaction may carry preconditions and several operations.

**Preconditions.** A crafted payload. **Impact.** Side effects signed by the payer and paid for by the sponsor.

**Mitigations**

* Exactly one invokeHostFunction transfer, no sub-invocations, no unsupported preconditions, bounded size. ([`packages/stellar/src/payload.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/payload.ts), `inspectExactTransaction`)

**Tests**

* [`packages/stellar/test/payload.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/payload.test.ts): reports sub-invocations and preconditions
* [`packages/stellar/test/payload.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/payload.test.ts): rejects more than one operation
* [`packages/stellar/test/payload.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/payload.test.ts): rejects a fee-bump envelope
* [`packages/stellar/test/payload.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/payload.test.ts): rejects an oversized payload before decoding it

**Conformance runs.** `rejection-audit` case `sub-invocations`, `rejection-audit` case `multiple-operations`, `rejection-audit` case `fee-bump-envelope`.

**Residual risk.** None.

### E4. An authorization valid for too long

Mitigated · Tampering · boundary TB2 (Seller and facilitator) · asset: Payer funds

**Threat.** A payload signed with a far-future expiration stays spendable long after the purchase.

**What Stellar makes different.** The expiration ledger is chosen by the signer; x402 derives it from maxTimeoutSeconds as ceil(T / ledger seconds) past the latest ledger.

**Preconditions.** A client that signs long-lived authorizations. **Impact.** A payment that can be settled much later.

**Mitigations**

* Expirations beyond maxLedger plus 2 are refused; maxTimeoutSeconds outside 10 to 300 is refused. ([`packages/stellar/src/expiration.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/expiration.ts), `maxAcceptableExpiration`)

**Tests**

* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): holds the upper edge at maxLedger plus 2, from upstream's ledger-time estimate
* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): derives the expiration ledger from maxTimeoutSeconds like the stock client
* [`packages/facilitator/test/expiration.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/expiration.integration.test.ts): settles before the expiration ledger and refuses the same window after it

**Conformance runs.** `rejection-audit` case `expires-too-far`, `rejection-audit` case `timeout-out-of-range`, `live-matrix` case `expiry-window`.

**Residual risk.** None.

### E5. Misreading a custom token's failures and decimals

Mitigated · Tampering · boundary TB3 (Facilitator and Stellar) · asset: Payer funds

**Threat.** A token whose error numbers or decimals differ from a Stellar Asset Contract is priced or explained as if it were one.

**What Stellar makes different.** Classic assets are always 7 decimals through their SAC; SEP-41 contract tokens choose their decimals and their own error numbers.

**Preconditions.** A custom SEP-41 asset configured. **Impact.** Wrong prices by orders of magnitude, or wrong refusal reasons.

**Mitigations**

* The service reads decimals() at startup and refuses to start on a mismatch. ([`apps/rail402/src/asset-check.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/asset-check.ts), `checkAssets`)
* SAC error numbers are applied only to SACs; other tokens are named by number. ([`packages/stellar/src/diagnostics.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/src/diagnostics.ts), `classifyTransferFailure`)

**Tests**

* [`apps/rail402/test/asset-check.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/asset-check.test.ts): fails for good, with a coded ConfigError naming the variable, when the decimals differ
* [`packages/stellar/test/diagnostics.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/stellar/test/diagnostics.test.ts): names a custom token's own error by number, never by the Stellar Asset Contract's meaning
* [`tools/conformance/test/assets.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/tools/conformance/test/assets.integration.test.ts): refuses to start when a configured asset's decimals differ from the chain's

**Residual risk.** None.

## Operations

Crashes, outages, sequence numbers, retention windows and keys: what happens to a payment when the service
itself fails.

### O1. A crash after submission

Mitigated · Denial of service, Repudiation · boundary TB4 (Facilitator and its state) · asset: Payer funds

**Threat.** The process dies after signing or broadcasting a settlement and before recording its outcome; a restart submits a replacement and the payer is charged twice, or the seller is never told.

**What Stellar makes different.** A signed envelope stays valid until its time bound; the network may include it after the process died.

**Preconditions.** A crash, deploy or kill at the wrong moment. **Impact.** Double charge (without protection) or a lost success.

**Mitigations**

* The signed envelope is recorded before broadcast; recovery re-sends the same bytes and never builds a replacement. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `recordingSponsor`)
* A recorded settlement is expired only when the RPC's retained history proves it absent. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `historyCovers`)

**Tests**

* [`apps/rail402/test/crash-recovery.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/crash-recovery.integration.test.ts): K0: finds the transaction the network accepted before the process died
* [`apps/rail402/test/crash-recovery.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/crash-recovery.integration.test.ts): K1: rebroadcasts the identical recorded bytes when the broadcast never left
* [`apps/rail402/test/crash-recovery.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/crash-recovery.integration.test.ts): K2: finds a transaction that was included before the process saw it confirmed
* [`apps/rail402/test/crash-recovery.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/crash-recovery.integration.test.ts): K3: expires a transaction whose time bound passed while the service was down, and nothing moves
* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryable

**Conformance runs.** `fault-recovery` case `k0-exact`, `fault-recovery` case `k1-exact`, `fault-recovery` case `k3-exact`, `fault-recovery` case `k0-upto`, `fault-recovery` case `k1-upto`.

**Residual risk.** A settlement whose ledgers the RPC no longer retains stays unresolvable and is reported (O4).

### O2. Channel sequence collisions

Mitigated · Denial of service · boundary TB4 (Facilitator and its state) · asset: Availability

**Threat.** Two settlements use the same channel account at once, or something else uses a channel's key, and transactions fail with a bad sequence number.

**What Stellar makes different.** An account's transactions are ordered by sequence number; one in-flight transaction per source is the rule.

**Preconditions.** Concurrency, several replicas, or external use of a channel key. **Impact.** Failed settlements and wasted fees.

**Mitigations**

* Channels are leased exclusively through Postgres across replicas. ([`packages/store-postgres/src/channel-pool.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/src/channel-pool.ts), `PostgresChannelPool`)
* Sequence conflicts are counted so external use is visible. ([`apps/rail402/src/metrics.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/metrics.ts), `rail402_sequence_conflicts_total`)

**Tests**

* [`packages/facilitator/test/concurrency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/concurrency.integration.test.ts): with injected duplicates, without a collision
* [`apps/rail402/test/replicas.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/replicas.integration.test.ts): with every duplicate sent to the other replica
* [`apps/rail402/test/observability.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/observability.integration.test.ts): counts a sequence conflict when a channel's sequence is used behind its back
* [`packages/facilitator/src/testing.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/testing.ts): never leases one channel twice, even under concurrency

**Conformance runs.** `concurrency` case `distinct-sequences`.

**Residual risk.** Reusing the sponsor seed in another deployment derives the same channels; do not.

### O3. The RPC or the settlement store is unavailable

Mitigated · Denial of service · boundary TB3 (Facilitator and Stellar) · asset: Availability

**Threat.** The Stellar RPC or Postgres stops answering while payments arrive.

**What Stellar makes different.** Without the RPC nothing can be simulated, submitted or confirmed.

**Preconditions.** An outage. **Impact.** Payments refused, or worse, settled without a record.

**Mitigations**

* /ready fails while the RPC is unreachable; refusals carry retryable codes. ([`apps/rail402/src/health.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/health.ts), `HealthMonitor`)
* Nothing is broadcast unless its envelope was recorded; verify stays available when the ledger cannot be read. ([`packages/facilitator/src/scheme.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/scheme.ts), `alreadySubmitted`)

**Tests**

* [`apps/rail402/test/runtime.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/runtime.integration.test.ts): fails while the RPC is unreachable
* [`apps/rail402/test/observability.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/observability.integration.test.ts): counts transport and request errors while the RPC fails, and reports it in /health
* [`apps/rail402/test/idempotency.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/idempotency.integration.test.ts): keeps verify available when the ledger cannot be read
* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryable
* [`packages/store-postgres/test/stores.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/store-postgres/test/stores.integration.test.ts): survives the server closing its idle connections, as in a restart or failover

**Residual risk.** None beyond downtime.

### O4. Settlements past the RPC's retention

Mitigated · Repudiation · boundary TB3 (Facilitator and Stellar) · asset: Seller revenue

**Threat.** A recorded settlement is examined after the RPC no longer keeps the ledgers that could prove it included or absent.

**What Stellar makes different.** An RPC keeps only a limited window of recent ledgers and transactions; older ones cannot be read from it.

**Preconditions.** A long outage or a restored backup. **Impact.** A payment wrongly declared expired while it did settle.

**Mitigations**

* Such settlements stay unresolved and are reported, never expired. ([`packages/facilitator/src/engine.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/engine.ts), `historyCovers`)

**Tests**

* [`apps/rail402/test/crash-recovery.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/crash-recovery.integration.test.ts): keeps a settlement the RPC can no longer prove absent unresolved, and reports it
* [`packages/facilitator/test/recovery.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/recovery.test.ts): never declares one expired once the RPC no longer retains the ledgers that could hold it

**Residual risk.** An operator resolves them from an archive or an indexer.

### O5. All channels busy

Mitigated · Denial of service · boundary TB4 (Facilitator and its state) · asset: Availability

**Threat.** More settlements arrive than there are channel accounts.

**What Stellar makes different.** Each channel carries one in-flight transaction.

**Preconditions.** A burst of payments. **Impact.** Slow or refused settlements.

**Mitigations**

* A request waits at most SETTLE\_CHANNEL\_WAIT\_MS for a channel and is refused with a retryable code, leaving no record. ([`packages/facilitator/src/channels.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/src/channels.ts), `MemoryChannelPool`)

**Tests**

* [`packages/facilitator/test/settlement.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/settlement.integration.test.ts): refuses when no channel frees up in time, leaving the payment retryable
* [`packages/facilitator/test/upto-scheme.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/upto-scheme.test.ts): answers busy channels with the upto code and keeps nothing claimed
* [`apps/rail402/test/observability.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/observability.integration.test.ts): measures the wait for a channel when settlements outnumber channels

**Residual risk.** Throughput is bounded by the channel count; raise CHANNEL\_COUNT.

### O6. The sponsor secret is compromised

Accepted risk · Elevation of privilege, Information disclosure · boundary TB4 (Facilitator and its state) · asset: Secrets

**Threat.** The sponsor seed leaks from the platform, a log or a backup.

**What Stellar makes different.** The sponsor seed derives the channels and the upto authority; none of them can move a payer's funds.

**Preconditions.** A leak. **Impact.** Loss of the sponsor's XLM; U10 for upto payloads in flight.

**Mitigations**

* The seed is never logged or echoed; configuration errors name the variable, not the value. ([`apps/rail402/src/logger.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/src/logger.ts), `REDACTED_PATHS`)
* Documented rotation: retire channels, move funds, provision with the new seed. ([`docs/operations.md`](https://github.com/tolgayayci/rail402/blob/main/docs/operations.md), `Rotating the sponsor key`)

**Tests**

* [`apps/rail402/test/config.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/config.test.ts): names the variable and never echoes a secret
* [`packages/facilitator/test/state.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/facilitator/test/state.test.ts): is deterministic per sponsor, network and index, and never collides
* [`apps/rail402/test/channels.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/rail402/test/channels.integration.test.ts): reports durable state and refuses to retire while a channel is leased

**Residual risk.** Keep only a working balance on the sponsor.

### O7. Degraded search indexing

Mitigated · Denial of service · boundary TB2 (Seller and facilitator) · asset: Catalog and search integrity

**Threat.** The embedding model fails or is slow, or its files are tampered with.

**What Stellar makes different.** None.

**Preconditions.** A model failure or a changed model file. **Impact.** Wrong or missing search results.

**Mitigations**

* Model files are pinned by SHA-256; search falls back to lexical results and says so with partialResults. ([`packages/search/src/embedder.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/search/src/embedder.ts), `sha256`)

**Tests**

* [`packages/search/test/search.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/search/test/search.test.ts): answers lexical-only, as partial results, when the embedder fails or runs out of time
* [`packages/search/test/embedder.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/search/test/embedder.test.ts): refuses to start when a model file does not match its pinned hash

**Residual risk.** None.

## The Playground

A testnet key in a browser page, and a server that relays the page's seller requests: what a visitor, another
site or a seller could do with either.

### PG1. The relay used to reach private networks or as an open proxy

Mitigated · Spoofing, Information disclosure, Denial of service · boundary TB8 (Playground page and server) · asset: Operator infrastructure

**Threat.** A visitor, or a script calling the Playground's API, asks the relay to fetch a loopback, private or cloud-metadata address, a name that resolves to one, or a seller that redirects to one; or uses the relay to send volume to third parties from the Playground's address.

**What Stellar makes different.** Nothing Stellar-specific: the relay exists because Rail402 and most x402 sellers answer no CORS headers, so a page cannot call them itself. A paid request through it carries a PAYMENT-SIGNATURE the visitor's own key made.

**Preconditions.** Network access to the Playground server. **Impact.** Without protection: requests into the server's network, or the Playground's address used for abuse.

**Mitigations**

* The relay sends with safe-fetch under a fixed policy: https on port 443 to public addresses only, DNS answers checked and pinned, redirects refused except same-origin hops for unpaid probes, request and answer size limits. ([`apps/playground/src/relay/egress.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/relay/egress.ts), `RelayEgress`)
* By host name, it never contacts the Playground itself (its public host and the platform's generated domain), the facilitator, the Bazaar or an operator-denied host. ([`apps/playground/src/relay/egress.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/relay/egress.ts), `playground_relay_loop`)
* Per-client, global, per-host, in-flight and daily byte limits, with Retry-After, and a switch to turn the relay off. ([`apps/playground/src/limits.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/limits.ts), `DailyBudget`)
* Global limits are fair-shared: one network (an IPv4 address or an IPv6 /48) holds at most an eighth of a pool, or its per-client limit when that is larger; clients are keyed by IPv4 address or IPv6 /56. ([`apps/playground/src/limits.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/limits.ts), `SharedRate`)
* Only a payment that decodes as an x402 payment counts against the paid limits, whether it travels in PAYMENT-SIGNATURE or in an MCP tool call's \_meta; anything else is refused before a token is taken. ([`apps/playground/src/payment-header.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/payment-header.ts), `mcpPaymentOf`)
* The demo seller has a paid pool of its own, a paid request that reached no seller gives the shared pool its token back, and the lab sellers return it when the facilitator was never asked, so payments that only have the right shape cannot use up the pools the demo flows need. ([`apps/playground/src/relay/relay.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/relay/relay.ts), `trustedPaidGlobal`)
* Unpaid requests wait no longer than the SDK's probe timeouts; bodies pass only as JSON, text or octet-stream; slots of an abandoned upload or an unread answer are released; connections to hosts other than the demo seller are closed after each answer. ([`apps/playground/src/relay/relay.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/relay/relay.ts), `timeoutsFor`)
* Unpaid requests and answer bytes are also limited per network, bytes for all clients per day, requests in flight per destination host, and each network gets at most its share of a host's budget. ([`apps/playground/src/limits.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/limits.ts), `KeyedSharedRate`)

**Tests**

* [`apps/playground/test/ssrf.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/ssrf.test.ts): refuses every private, local, reserved or off-policy literal before any lookup
* [`apps/playground/test/ssrf.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/ssrf.test.ts): refuses a public name whose DNS answers include any private address
* [`apps/playground/test/ssrf.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/ssrf.test.ts): does not let the relay reach the Playground, the facilitator, the Bazaar or a denied host by any spelling
* [`apps/playground/test/ssrf.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/ssrf.test.ts): never follows a redirect off the seller's origin, even for unpaid requests that allow redirects
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): limits requests per destination host across clients
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): stops a client past its daily transfer budget
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): refuses a payment header that holds no payment before any limit counts it
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): gives one network at most its share of the global paid budget
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): gives back the slots of an upload the client abandons
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): counts an MCP tool call carrying a payment in its body as paid
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): keeps a paid pool of its own for the trusted hosts
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): gives one network at most its share of a destination host's budget
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): forwards a body only as JSON, text or octet-stream
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): treats the platform's generated domain as the Playground itself
* [`apps/playground/test/relay-wire.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay-wire.test.ts): ends a body nobody reads at its deadline and cancels the seller's stream
* [`packages/safe-fetch/test/fetch.test.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/safe-fetch/test/fetch.test.ts): closes kept connections that sit unused and caps connections across hosts
* [`apps/playground/e2e/playground.spec.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/e2e/playground.spec.ts): never follows a redirect to another origin, and signs nothing

**Conformance runs.** `live-playground` case `ssrf-literal`, `live-playground` case `ssrf-metadata`, `live-playground` case `redirect`.

**Residual risk.** For a client that is not a browser, the relay is an open https egress to public hosts at the Playground's address, bounded only by the limits; IP-keyed limits cannot stop a client with many addresses or networks. The payment check stops only malformed payments: a payment that merely has the right shape is counted as paid, so enough addresses can still use up a shared paid pool for a minute at a time. Loop and deny lists match host names: another name of a denied host is caught only when the operator lists it. Its user agent names the Playground, and the operator can deny hosts or switch the relay off.

### PG2. Another site drives the relay from a visitor's browser

Mitigated · Spoofing, Information disclosure · boundary TB8 (Playground page and server) · asset: Operator infrastructure

**Threat.** A page on another origin makes a visitor's browser send requests through the Playground's relay, seller check or proxies, or reads their answers.

**What Stellar makes different.** The relay holds no key and no session, so a forged request carries no authority the attacker lacks; a payment needs a signature from the key in the Playground's own page.

**Preconditions.** A visitor with the Playground open, or not, visits the attacker's page. **Impact.** Without protection: the Playground's relay used through visitors' browsers.

**Mitigations**

* The relay needs its own request headers, which a cross-site form cannot set; a browser's Sec-Fetch-Site must be same-origin and its Origin the Playground's public origin. ([`apps/playground/src/security.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/security.ts), `sameOrigin`)
* The relay answers no CORS headers, so no other origin can read an answer. ([`apps/playground/src/shared/relay-protocol.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/shared/relay-protocol.ts), `x-relay-target`)
* The Bazaar and /supported proxies refuse another site's requests the same way, so no page can spend their shared upstream budget through visitors' browsers. ([`apps/playground/src/proxy/bazaar.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/proxy/bazaar.ts), `proxyOrigin`)

**Tests**

* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): answers only same-origin requests from the page
* [`apps/playground/test/seller-check.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/seller-check.test.ts): answers only same-origin requests from the page
* [`apps/playground/test/proxy.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/proxy.test.ts): refuses requests another site's page makes, before any upstream fetch

**Residual risk.** Clients that are not browsers send neither header and pass. They get nothing a browser visitor lacks, but they can use the relay as an https egress at the Playground's address, bounded only by the limits (PG1).

### PG3. Script injected into the page uses the testnet key

Mitigated · Tampering, Elevation of privilege · boundary TB8 (Playground page and server) · asset: Secrets

**Threat.** Seller-supplied text (a description, an error, a response body) or a third-party script runs in the page and signs with, or reads, the wallet's key.

**What Stellar makes different.** The key signs Soroban authorization entries through the SDK's signing guard; a script in the page could ask it to sign within the visitor's caps.

**Preconditions.** A seller or listing the visitor's page displays. **Impact.** Without protection: testnet funds spent within the caps, or the testnet key read out.

**Mitigations**

* A content security policy without inline or third-party script, styles or frames from the Playground's origin only, and connections only to the configured Stellar endpoints. ([`apps/playground/src/security.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/security.ts), `contentSecurityPolicy`)
* Seller text is rendered as text, and an untrusted URL becomes a link only when it is https and parses; no view renders raw markup. ([`apps/playground/web/src/ui/links.tsx`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/src/ui/links.tsx), `safeHttpsUrl`)
* The stored seed is encrypted under a key WebCrypto generated as non-extractable; only the wallet modules touch the key. ([`apps/playground/web/src/wallet/keystore.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/src/wallet/keystore.ts), `non-extractable`)

**Tests**

* [`apps/playground/test/app.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/app.test.ts): sends the security headers and a strict content security policy on every response
* [`apps/playground/web/test/engine/dependency.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/engine/dependency.test.ts): the page never renders raw markup
* [`apps/playground/web/test/engine/dependency.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/engine/dependency.test.ts): only the wallet modules touch the key itself
* [`apps/playground/web/test/ui/lab.test.tsx`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/ui/lab.test.tsx): does not render HTML from seller data
* [`apps/playground/e2e/playground.spec.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/e2e/playground.spec.ts): serves the built page with a strict CSP and starts the real engine

**Conformance runs.** `live-playground` case `secret-scan`.

**Residual risk.** A script that did run in the page would not be bound by the caps: it could open the page's IndexedDB, decrypt the stored seed with the stored key (non-extractable protects the key, not what it decrypts) and send the seed out. The CSP and the absence of markup sinks make that unlikely. The key is meant for testnet only, and the import field says never to paste a key that holds real funds.

### PG4. The seller check used to scan ports or private hosts

Mitigated · Information disclosure, Denial of service · boundary TB8 (Playground page and server) · asset: Operator infrastructure

**Threat.** A visitor submits private addresses, other ports or many URLs to the seller check to map the server's network or to load third parties.

**What Stellar makes different.** The check runs the SDK's seller checks: the URL's 402, its discovery metadata, whether payTo can receive the asset, and the seller's stellar.toml and home domain.

**Preconditions.** Network access to the Playground server. **Impact.** Without protection: a port scanner inside the server's network.

**Mitigations**

* The check fetches with https on port 443 to public addresses only, through the Playground's own facilitator, in the hosted configuration. ([`apps/playground/src/seller-check.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/seller-check.ts), `sellerCheckEgress`)
* Ten checks per minute per client and sixty for all clients, and a switch to turn it off. ([`apps/playground/src/seller-check.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/seller-check.ts), `mountSellerCheck`)

**Tests**

* [`apps/playground/test/seller-check.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/seller-check.test.ts): checks the URL with an https:443, public-only policy and the Playground's own facilitator
* [`apps/playground/test/seller-check.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/seller-check.test.ts): refuses the same loopback seller in hosted mode before contacting it
* [`apps/playground/test/seller-check.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/seller-check.test.ts): limits checks per client and across clients

**Residual risk.** The check reaches public https hosts on port 443 at its rate.

### PG5. The lab sellers take real payments or pollute the catalog

Mitigated · Tampering, Denial of service · boundary TB8 (Playground page and server) · asset: Payer funds

**Threat.** The Playground's misbehaving sellers (the offers, the greedy upto route) take a payment that sticks, or every visitor's sandbox sale adds a listing to the public Bazaar.

**What Stellar makes different.** An upto authorization caps what any settlement can move; Rail402 refuses a settlement above it, and the settlement contract enforces the cap on chain too.

**Preconditions.** A visitor running the lab. **Impact.** Without protection: visitors charged more than they authorized, or a catalog flooded with sample listings.

**Mitigations**

* The offers answer a 402 only and never verify or settle. ([`apps/playground/src/lab/offers.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/lab/offers.ts), `offerHandler`)
* The greedy route asks to settle twice its cap, which Rail402's upto rules refuse; it declares no discovery metadata. ([`packages/upto/src/facilitator/rules.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/upto/src/facilitator/rules.ts), `invalid_upto_stellar_payload_settlement_exceeds_amount`)
* The sandbox route answers the same 402 for every city and visitor, so it is one listing paid to one operator account. ([`apps/playground/src/lab/sell.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/lab/sell.ts), `declareSellRoute`)
* Its discovery metadata always carries the declared example city, never the one a request names, and a path that is not a city name gets no 402, so no payer writes text into the shared listing. ([`apps/playground/src/lab/sell.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/lab/sell.ts), `sellBazaarExtension`)

**Tests**

* [`apps/playground/test/lab-sellers.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/lab-sellers.test.ts): answer a valid x402 version 2 402 only, never contacting the facilitator
* [`apps/playground/test/lab-sellers.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/lab-sellers.test.ts): asks the facilitator to settle twice the cap and reports the refusal
* [`apps/playground/test/lab-sellers.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/lab-sellers.test.ts): answers the same 402 every time, for any city, so the origin check always agrees
* [`apps/playground/test/lab-sellers.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/lab-sellers.test.ts): keeps the declared example in every 402, whatever city the request names
* [`apps/playground/test/lab-sellers.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/lab-sellers.test.ts): answers anything but a city name with a coded 404, before any 402
* [`apps/playground/web/test/integration/engine.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/integration/engine.integration.test.ts): blocks a seller that charges over the cap: nothing moves, the hold returns after expiry
* [`apps/playground/web/test/integration/engine.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/integration/engine.integration.test.ts): sells through the sandbox route: settled, cataloged, origin-checked and found by search

**Conformance runs.** `live-playground` case `overcharge`, `live-playground` case `overcharge-release`, `live-playground` case `refusal-network`, `live-playground` case `refusal-asset`, `live-playground` case `refusal-amount`, `live-playground` case `sell-listed`.

**Residual risk.** None for funds. The sandbox listing is public and permanent, as one listing; its resource URL names the city of the payment that created it.

### PG6. The stored wallet read or overspent

Mitigated · Information disclosure, Elevation of privilege · boundary TB8 (Playground page and server) · asset: Agent budgets

**Threat.** The wallet's stored record is swapped or tampered with, or two tabs spend one session budget at the same time.

**What Stellar makes different.** The budget ledger reserves each payment before it is signed; two writers of one stored ledger could each see room the other already used.

**Preconditions.** Access to the browser profile, or two open tabs. **Impact.** Without protection: a different key loaded, or the session budget exceeded.

**Mitigations**

* The stored record is bound to its address and network; a tampered or rebound record is refused. ([`apps/playground/web/src/wallet/keystore.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/src/wallet/keystore.ts), `additionalData`)
* One tab holds the wallet through a Web Lock; the others are read-only. ([`apps/playground/web/src/wallet/tab-lock.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/src/wallet/tab-lock.ts), `acquireTabLock`)
* A stored ledger that cannot be read is refused until it is reset, never silently emptied. ([`packages/sdk/src/policy/storage-ledger.ts`](https://github.com/tolgayayci/rail402/blob/main/packages/sdk/src/policy/storage-ledger.ts), `payment_ledger_unavailable`)

**Tests**

* [`apps/playground/web/test/wallet/keystore.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/wallet/keystore.test.ts): stores the seed encrypted under a non-extractable key and loads it back
* [`apps/playground/web/test/wallet/keystore.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/wallet/keystore.test.ts): refuses a tampered or rebound record instead of loading another key
* [`apps/playground/web/test/wallet/tab-lock.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/wallet/tab-lock.test.ts): lets one tab hold the wallet; another waits until it lets go
* [`apps/playground/e2e/playground.spec.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/e2e/playground.spec.ts): keeps the wallet in one tab: a second tab finds it locked

**Conformance runs.** `live-playground` case `refusal-session`.

**Residual risk.** The record is encrypted under a key kept in the same browser profile, not under a passphrase: anyone using that profile can use the wallet through the page. It is a testnet key with testnet funds.

### PG7. Payments or the key leak into logs, traces or storage

Mitigated · Information disclosure · boundary TB8 (Playground page and server) · asset: Secrets

**Threat.** A PAYMENT-SIGNATURE, a signed authorization or the key ends up in the server's logs or traces, an error, the page's storage, a run report or a request to a third party.

**What Stellar makes different.** A signed authorization is spendable by whoever holds it until it expires.

**Preconditions.** Any run. **Impact.** Without protection: an authorization replayable by whoever reads the channel, or the key exposed.

**Mitigations**

* The relay logs the host, status and outcome only, never the target path, query, headers or body. ([`apps/playground/src/relay/relay.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/relay/relay.ts), `upstreamStatus`)
* The lab sellers' trace header carries verify and settle outcomes, never payloads. ([`apps/playground/src/lab/trace.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/src/lab/trace.ts), `TraceRecorder`)
* Run reports are built from the run's events, which hold no key, signature, preimage or payment header. ([`apps/playground/web/src/engine/run-report.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/src/engine/run-report.ts), `runReport`)

**Tests**

* [`apps/playground/test/secret-scan.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/secret-scan.test.ts): keeps the payment and the key out of every channel but the seller and the facilitator
* [`apps/playground/test/relay.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/test/relay.test.ts): logs the host and outcome, never the target path, query, headers or body
* [`apps/playground/web/test/integration/engine.integration.test.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/web/test/integration/engine.integration.test.ts): keeps the key and the payments out of every other channel
* [`apps/playground/e2e/playground.spec.ts`](https://github.com/tolgayayci/rail402/blob/main/apps/playground/e2e/playground.spec.ts): keeps the key and the payments out of every other channel

**Conformance runs.** `live-playground` case `secret-scan`.

**Residual risk.** The relay holds a payment header in memory for the one request that carries it, as any seller does.

## Out of scope

* Whether a seller delivers what it sells, or charges a fair `upto` amount within the cap the buyer signed.
* The security of buyers' wallets and of the machines agents run on.
* The integrity of the Stellar RPC provider beyond what simulation and the ledger prove; the RPC URL is
  operator configuration.
* Attacks on the hosted testnet instance's infrastructure; see [SECURITY.md](https://github.com/tolgayayci/rail402/blob/main/SECURITY.md)
  for how to report a vulnerability.


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