/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, 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.
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 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.- 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.
- 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.
- The payer’s signature fixes the recipient and the amount, or the cap. Changing either after signing fails signature verification on chain.
- The sponsor’s exposure is bounded per settlement by the fee ceiling and per hour by the fee budget.
- A listing is owned by the
payTothat was settled, and is published only after its own origin confirms it. - 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.
- An agent cannot spend beyond the caps its owner set, which are checked immediately before signing.
Assets
Trust boundaries
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.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,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,answerRepeat) - A request whose terms differ from the recorded request is refused, never given the stored success. (
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,alreadySubmitted)
packages/facilitator/test/settlement.integration.test.ts: answers a repeated settle with the original result and moves funds oncepackages/facilitator/test/settlement.integration.test.ts: settles concurrent duplicates exactly once, all callers getting the same resultapps/rail402/test/idempotency.integration.test.ts: settles 5 concurrent repeats once: one transfer, one original, identical bodiesapps/rail402/test/idempotency.integration.test.ts: never answers a request for different terms with the stored successapps/rail402/test/idempotency.integration.test.ts: reports a settlement in progress to verify, then the used authorizationpackages/facilitator/src/testing.ts: grants exactly one claim per authorization, even under concurrency
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,keyOf) - A different envelope for a recorded authorization is refused with the idempotency conflict, naming the recorded transaction. (
packages/facilitator/src/engine.ts,conflict)
apps/rail402/test/idempotency.integration.test.ts: refuses the same authorization in another envelope, naming the recorded transactionpackages/facilitator/test/settlement.integration.test.ts: refuses a settled authorization in a different envelope, and verify names the replaypackages/facilitator/test/state.test.ts: reports a conflict when the same authorization arrives in a different envelope
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,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,invalid_network)
packages/stellar/test/auth.test.ts: is SHA-256 of the passphrasepackages/sdk/test/decide.test.ts: refuses another network, whether in the preimage or in the requested passphraseapps/rail402/test/app.test.ts: refuses networks and schemes it does not serve without calling the core
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,markRepeat) - The seller helpers refuse a second request whose settlement is a repeat or a transaction already served. (
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,alreadySubmitted)
apps/rail402/test/app.test.ts: sends the recorded body and moves the repeat marker into EXTENSION-RESPONSESpackages/sdk/test/seller.test.ts: trusts Rail402’s repeat marker, even for a settlement it never sawpackages/sdk/test/seller.integration.test.ts: does not serve a replayed payment signature againpackages/sdk/test/seller.integration.test.ts: serves an upfront settlement once when the facilitator answers its repeatapps/rail402/test/idempotency.integration.test.ts: marks the repeat in extension responses, and counts and meters it once
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,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,offchainZero)
contracts/stellar/upto/src/test/signed.rs: a_zero_settlement_also_consumes_the_noncepackages/facilitator/test/upto.integration.test.ts: settles zero on-chain: the nonce is consumed and no funds movepackages/facilitator/test/upto.integration.test.ts: settles zero off-chain when configured: no transaction, the authorization used up in the ledgerpackages/facilitator/src/testing.ts: finishes an upto zero-amount success without an envelope, only for the claim’s owner
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,execute) - Signed envelopes and authorizations are redacted from logs. (
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,wasted)
packages/facilitator/test/front-run.integration.test.ts: answers success with the attacker’s hash; the payment moves once and our fee is wastedapps/rail402/test/logger.test.ts: redacts secret fields at the top level and one level down
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,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,signedAuthorizationOf)
packages/facilitator/test/front-run.integration.test.ts: answers success with the attacker’s hash; the payment moves once and our fee is wastedpackages/facilitator/test/front-run.integration.test.ts: refuses an authorization consumed before the request was claimedpackages/facilitator/test/front-run.integration.test.ts: refuses when the consuming transaction carried a different invocation for the same noncepackages/facilitator/test/front-run.integration.test.ts: refuses an authorization whose nonce was never consumed
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,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,authorityEntry)
contracts/stellar/upto/src/test/auth.rs: a_different_submitter_cannot_settlecontracts/stellar/upto/src/test/auth.rs: missing_facilitator_authorization_is_rejectedcontracts/stellar/upto/src/test/auth.rs: facilitator_authorization_for_another_amount_is_rejectedcontracts/stellar/upto/src/test/signed.rs: another_source_account_cannot_act_as_the_authoritypackages/facilitator/test/front-run.integration.test.ts: cannot be settled by a payload holder for another amount: the host refuses without the authoritypackages/facilitator/test/front-run.integration.test.ts: cannot be landed by an attacker without the settlement authority; Rail402’s settlement proceeds
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,settle_upto_stellar_settled_elsewhere)
packages/facilitator/test/upto.integration.test.ts: succeeds with that transaction when it settled the same amountpackages/facilitator/test/upto.integration.test.ts: answers settled_elsewhere when it settled a different amountpackages/facilitator/test/front-run.integration.test.ts: answers with the authority’s own earlier transaction for the same amount; our fee is wasted
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,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,requireApiKey)
apps/rail402/test/upto-settlement.integration.test.ts: lands one transaction for 20 concurrent settlements of different amounts and refuses the restapps/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
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,settle) - The failure is named from the transaction’s diagnostic events and counted as wasted fee. (
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,SponsorBudget)
packages/facilitator/test/fee-exposure.integration.test.ts: refuses at settle, with nothing submitted, a payment whose balance moved after verifypackages/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 wastedpackages/facilitator/test/fee-exposure.integration.test.ts: stops settling once the hourly fee budget is committed, before any RPC work
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,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,buildTransaction)
packages/facilitator/test/expiration.integration.test.ts: holds the lower edge at the current ledger plus a one-ledger marginpackages/facilitator/test/expiration.integration.test.ts: names an authorization that expires between submission and inclusion; the fee is spentpackages/facilitator/test/expiration.integration.test.ts: refuses at the network an upto settlement held past expiration_ledger; no fee is spent
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,DEFAULT_MAX_TRANSACTION_FEE_STROOPS) - The inclusion-fee bid is capped. (
packages/facilitator/src/fees.ts,InclusionFeeOracle)
packages/facilitator/test/fee-exposure.integration.test.ts: refuses a payment whose settlement would cost more than the fee ceiling, before signing anythingpackages/upto/test/facilitator.test.ts: enforces the fee ceilingpackages/facilitator/test/fees.test.ts: bids the configured percentile, clamped between floor and cap
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,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,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,parseAssets)
packages/facilitator/test/fee-exposure.integration.test.ts: stops settling once the hourly fee budget is committed, before any RPC workapps/rail402/test/app.test.ts: rate-limits per client with Retry-After and a coded bodyapps/rail402/test/app.test.ts: rate-limits an IPv6 client by its /64 and a mapped IPv4 client as IPv4apps/rail402/test/app.test.ts: enforces API keys when the network requires themapps/rail402/test/config.test.ts: requires explicit, safe mainnet settings
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,minBalanceStroops) - /ready fails below MIN_SPONSOR_BALANCE_XLM and /health reports it. (
apps/rail402/src/health.ts,sponsor_below_floor)
packages/facilitator/test/fees.test.ts: refuses while the sponsor is below its reserve, admits otherwiseapps/rail402/test/observability.integration.test.ts: counts refusals below the reserve and reports the sponsor below its floorapps/rail402/test/runtime.integration.test.ts: fails while the sponsor holds less than MIN_SPONSOR_BALANCE_XLMpackages/facilitator/test/settlement.integration.test.ts: refuses while the sponsor guard is closed, then settles once it reopens
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,claim) - A crashed settlement is resumed from its recorded envelope bytes, never rebuilt. (
packages/facilitator/src/engine.ts,recordingSponsor)
packages/facilitator/test/concurrency.integration.test.ts: with injected duplicates, without a collisionapps/rail402/test/replicas.integration.test.ts: with every duplicate sent to the other replicapackages/upto/test/facilitator.test.ts: submits one transaction for 20 concurrent identical settlements
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,archivedStateRejection) - Exact payments name archived state from the simulation’s restore preamble. (
packages/facilitator/src/explain.ts,explainSimulation)
packages/upto/test/facilitator.test.ts: refuses when the settlement contract must be restored, and names other archived statepackages/facilitator/test/upto-scheme.test.ts: refuses to pay for restoring an archived settlement contractpackages/facilitator/test/explain.test.ts: names archived ledger state when simulation asks for a restoreapps/rail402/test/upto-archival.integration.test.ts: refuses payments while the contract is archived, and settles once an operator restores it
Catalog poisoning
The Bazaar catalogs resources automatically after a successful settlement. The settlement proves one thing, that a payment reachedpayTo; 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,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,probeMcpTool)
packages/bazaar/src/testing.ts: does not let echoed metadata change a listing until the origin confirms itpackages/bazaar/src/testing.ts: never lists an HTTP resource whose own response is not a 402packages/bazaar/src/testing.ts: quarantines a listing whose origin does not name its ownerpackages/bazaar/src/testing.ts: lists an MCP tool at an https endpoint only once its unpaid call confirms itpackages/bazaar/src/testing.ts: quarantines a tool its server does not sell to the listed payTo, with a coded reasonpackages/bazaar/test/mcp-origin.test.ts: reads a tool’s PaymentRequired from its unpaid call, sending the example arguments
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,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,unprovenSettlement)
packages/bazaar/src/testing.ts: never lets a different payTo modify a listingpackages/bazaar/src/testing.ts: transfers ownership only when the origin names the new payToapps/rail402/test/bazaar.integration.test.ts: refuses a different payTo for the same resource, with a coded reasonpackages/bazaar/src/testing.ts: does not catalog a settlement that moved nothing, however high its cap
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,SchemaSandbox)
packages/bazaar/test/security.test.ts: stops a catastrophic-backtracking pattern within its budget and keeps servingpackages/bazaar/test/security.test.ts: stops a combinatorial uniqueItems check within its budgetpackages/bazaar/test/security.test.ts: refuses oversized and deeply nested documents before compiling thempackages/bazaar/test/security.test.ts: finds external references in every schema position, and only therepackages/bazaar/test/security.test.ts: refuses validations beyond its queue at once instead of delaying everyone
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,isSafeTemplate) - Buyers refuse traversal when filling route parameters. (
packages/sdk/src/listing.ts,expandRoute)
packages/bazaar/test/security.test.ts: refuses route templates that decode to NUL, CR, LF, a backslash or an empty segmentpackages/bazaar/src/testing.ts: uses a routeTemplate only when it is safe and matches the paid pathpackages/sdk/test/discovery.test.ts: fills route parameters and refuses traversal
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,maxNewListingsPerPayerPerHour) - Search never ranks by settlement count: ties break by origin verification, then listing age. (
packages/bazaar/src/wire.ts,trustRank) - Self-payments are not cataloged. (
packages/bazaar/src/codes.ts,bazaar_self_payment)
packages/bazaar/src/testing.ts: bounds how many listings one payTo may create per hourpackages/bazaar/src/testing.ts: bounds the new listings one payer’s settlements may create per hourpackages/bazaar/src/testing.ts: rejects self-paymentspackages/search/test/search.test.ts: serves filter-only queries by trust, then listing age, never by settlements bought
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,listing_observations_transaction)
packages/bazaar/src/testing.ts: catalogs at most one resource per settlementpackages/bazaar/src/testing.ts: records a repeated settlement once and never duplicates the listingpackages/bazaar/src/testing.ts: does not catalog a payment settled without a transaction
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,boundedReason)
packages/bazaar/src/testing.ts: bounds a rejection reason that quotes the buyer’s inputpackages/bazaar/test/memory-catalog.test.ts: stays within the header budget whatever the outcome carries
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,canonicalizeUrl)
packages/bazaar/test/security.test.ts: canonicalises case, default ports, dot segments, query and fragmentpackages/bazaar/test/security.test.ts: normalizes percent-encoding so equivalent paths are one resourcepackages/bazaar/src/testing.ts: treats equivalent spellings of one URL as one listing
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,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,checkOrigins) - Domain trust is granted only to origin-verified listings, and needs the two-way SEP-1 binding. (
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,countCreatedBy)
packages/bazaar/src/testing.ts: never publishes a tool description or price planted withpackages/bazaar/src/testing.ts: never lets a planted settlement withdraw or change a tool its origin confirmedpackages/bazaar/src/testing.ts: updates an MCP tool’s metadata once its origin confirms the changepackages/bazaar/src/testing.ts: lets a domain vouch for an MCP tool listed under its hostpackages/bazaar/src/testing.ts: does not charge an owner’s quota for listings other payers created and its origin never confirmed
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,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,resolvePolicy)
packages/bazaar/src/testing.ts: publishes an MCP tool under an mcp:// URL at once, at trust settled, keeping its first content
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,safeLookup) - Private and loopback addresses are refused unless DISCOVERY_ALLOW_LOOPBACK is set, which is refused together with pubnet. (
apps/rail402/src/config.ts,DISCOVERY_ALLOW_LOOPBACK)
packages/bazaar/test/origin.test.ts: refuses a host whose DNS answers are not public, before connectingpackages/bazaar/test/egress.test.ts: refuses a private IP literal for origin checks unless loopback is allowedpackages/bazaar/test/security.test.ts: classifies public and non-public addressesapps/rail402/test/config.test.ts: refuses loopback discovery fetches on a service that serves pubnet
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,pinnedLookup)
packages/safe-fetch/test/egress.integration.test.ts: checks every DNS answer, so a rebinding name cannot reach a private addresspackages/safe-fetch/test/address.test.ts: refuses an empty answer and a mixed one, without naming the address
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,nextHop)
packages/bazaar/test/origin.test.ts: does not follow redirectspackages/safe-fetch/test/egress.integration.test.ts: never follows a redirect towards a private or other destinationpackages/sdk/test/http-flow.test.ts: never follows a redirect after paying
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,DEFAULT_LIMITS)
packages/safe-fetch/test/egress.integration.test.ts: stops oversized and compressed-bomb answers at the decoded limitpackages/safe-fetch/test/fetch.test.ts: times out waiting for headers, on an idle body, and in totalpackages/bazaar/test/stellar-toml.test.ts: refuses a file larger than SEP-1 allows
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,fetchStellarTomlAccounts)
packages/bazaar/test/stellar-toml.test.ts: never connects to a private address unless loopback is explicitly allowedpackages/bazaar/test/egress.test.ts: refuses a private IP literal for stellar.toml
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,createSafeFetch)
packages/sdk/test/http-flow.test.ts: refuses private destinations before sending anythingpackages/safe-fetch/test/egress.integration.test.ts: refuses IP-literal URLs to this machine in every spelling, before connectingpackages/safe-fetch/test/egress.integration.test.ts: refuses a name that resolves to a private address, without naming the address
rejection-audit case mcp-private-address.
Residual risk. None.
Smart accounts
AC… 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,classifyTransferFailure)
packages/facilitator/test/settlement.integration.test.ts: rejects a smart-account payment signed by the wrong keypackages/stellar/test/diagnostics.test.ts: a smart-account payer rejecting in __check_auth → smart_account_rejectedpackages/upto/test/facilitator.test.ts: attributes refusals inside a smart account’s __check_auth to the account
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,settle) - Wasted fees are metered; the hourly budget bounds the total. (
packages/facilitator/src/guard.ts,SponsorBudget)
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 wastedpackages/facilitator/test/fee-exposure.integration.test.ts: stops settling once the hourly fee budget is committed, before any RPC work
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,invalid_exact_stellar_payload_event_not_transfer) - upto accepts non-balance events such as a policy’s own. (
packages/upto/src/facilitator/simulation.ts,checkSimulationEvents)
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,SpendingLimitExceeded)
contracts/testing/approve-spending-limit/src/test.rs: transfers_and_approvals_share_one_budgetcontracts/testing/composition/src/test.rs: payments_within_the_budget_settlecontracts/testing/composition/src/test.rs: a_fourth_payment_after_the_budget_is_used_fails_in_the_policycontracts/testing/composition/src/test.rs: a_cap_above_the_remaining_budget_fails_even_for_a_small_amount
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,Rule configuration for an agent key)
contracts/testing/composition/src/test.rs: the_agent_key_cannot_use_the_default_rulecontracts/testing/composition/src/test.rs: the_agent_key_cannot_route_the_approval_around_the_policycontracts/testing/composition/src/test.rs: the_rule_selection_is_bound_by_the_signature
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,explainFailedTransaction)
contracts/testing/composition/src/test.rs: a_fourth_payment_after_the_budget_is_used_fails_in_the_policycontracts/testing/approve-spending-limit/src/test.rs: spends_leave_the_window_after_period_ledgers
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,createGuardedSigner) - The budget ledger reserves atomically, also across processes sharing one SQLite file. (
packages/sdk/src/policy/ledger.ts,checkReservation)
packages/sdk/test/guard.test.ts: never lets 20 concurrent payments exceed the session budgetpackages/sdk/test/sqlite-ledger.test.ts: never lets four processes sharing one window exceed its budgetpackages/sdk/test/guard.integration.test.ts: refuses to sign a payment above the per-call cappackages/mcp/test/pay.integration.test.ts: refuses a price above the caller’s cap before signingpackages/sdk/test/guard.test.ts: refuses a second signature within one call and keeps the first one’s budget pending
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,decideSpend)
packages/sdk/test/decide.test.ts: refuses another network, whether in the preimage or in the requested passphrasepackages/sdk/test/policy.test.ts: compares muxed recipients by base account and refuses self-paymentpackages/sdk/test/policy.test.ts: refuses an option priced in a different asset than the cap
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,redactDeep) - Tool schemas are published without secret-like properties or header mirroring. (
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,scrubOutcome)
packages/mcp/test/secrets.integration.test.ts: leaks nothing into frames, logs or the ledgerpackages/sdk/test/scrub.test.ts: removes the payment from an HTTP answer and its stored replay, keeping the restpackages/sdk/test/scrub.test.ts: removes the payment from an MCP tool’s content and structured contentpackages/sdk/test/secrets.integration.test.ts: leaks nothing, and the payment reached only the paid originpackages/cli/test/secrets.integration.test.ts: leaks nothing on stdout, stderr or in the ledgerpackages/mcp/test/schemas.test.ts: publishes self-contained object schemas without header mirroring or secret-like fieldspackages/mcp/test/logger.test.ts: writes JSON lines at or above its level, redacting secrets in the message and nested fields
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,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,replayStored)
packages/mcp/test/tools.test.ts: replays a stored result for a repeated idempotency keypackages/sdk/test/http-flow.test.ts: replays a finished call for a repeated idempotency key without signing againpackages/mcp/test/pay.integration.test.ts: pays an HTTP resource by URL without custody, and replays a repeated key without payingpackages/sdk/test/http-flow.test.ts: frees an idempotency key on the retry itself, once the chain shows its authorization unusedpackages/sdk/test/http-flow.test.ts: reports a keyed retry as paid once the chain shows its authorization consumed
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,checkOption) - Tool results bound and strip seller content. (
packages/mcp/src/results.ts,sellerJson)
packages/sdk/test/http-flow.test.ts: binds a listing’s recipient and its pricepackages/sdk/test/mcp-flow.test.ts: binds a listing’s price: a live price above the listed one is refused before signingpackages/sdk/test/policy.test.ts: binds the recipient of every listing and the price a listing carriespackages/sdk/test/discovery.test.ts: binds the listed prices of HTTP listings and of MCP tools at an http(s) endpointpackages/sdk/test/discovery.test.ts: refuses listings that cannot be paid before anything is fetched from the sellerpackages/mcp/test/tools.test.ts: strips control characters and drops oversized seller JSON
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,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,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,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,markUnprovable)
packages/sdk/test/guard.test.ts: refuses a payload whose transaction differs from the signed authorization; the budget stays pendingpackages/sdk/test/http-flow.test.ts: refuses a tampering scheme client after the probe, with nothing sent to the sellerpackages/sdk/test/http-flow.test.ts: verifies a reported settlement on chainpackages/sdk/test/http-flow.test.ts: counts a reused transaction hash against each payment that reports itpackages/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: settles every payment a batch transaction consumed (%s ledger)packages/sdk/test/reconcile.test.ts: settles each payer’s payment from one batch transaction in a shared ledger filepackages/sdk/test/reconcile.test.ts: counts an upto settlement made through another contract (%s ledger)packages/sdk/test/reconcile.test.ts: settles a payment only from a transfer made with its own authorizationpackages/sdk/test/reconcile.test.ts: never lets another payment’s transfer release an upto cap early (%s ledger)packages/sdk/test/reconcile.test.ts: does not count an authorization carried in a transaction that never consumed itpackages/sdk/test/reconcile.test.ts: counts expired rows the RPC can no longer prove as spent, and stops checking them (%s ledger)
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,hostHeaderValidationResponse)
packages/mcp/test/http.test.ts: refuses a Host outside the allowlist (DNS rebinding) with 403packages/mcp/test/http.test.ts: refuses a browser Origin outside the allowlist with 403packages/mcp/test/http.test.ts: listens on loopback and serves a client that presents the token
The upto contract
Withupto 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,AmountExceedsMax) - The facilitator refuses before simulating. (
packages/upto/src/facilitator/rules.ts,checkUptoPayment)
contracts/stellar/upto/src/test/errors.rs: rejects_an_amount_over_the_capcontracts/stellar/upto/src/test/errors.rs: a_rejected_settlement_moves_nothingpackages/upto/test/facilitator.test.ts: refuses an amount above the cap before simulatingcontracts/testing/composition/src/test.rs: an_amount_over_the_cap_is_contract_error_3
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,inspectUptoAuthorization)
contracts/stellar/upto/src/test/auth.rs: mutated_terms_are_rejectedcontracts/stellar/upto/src/test/signed.rs: a_signature_over_other_terms_failspackages/upto/test/upto.integration.test.ts: refuses terms changed consistently in the entry and the requirementspackages/upto/test/upto.integration.test.ts: refuses a changed noncepackages/upto/test/upto.integration.test.ts: refuses a signature expiration moved later than signedpackages/upto/test/vectors.test.ts: cover every bound field and tree shape
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,uptoFingerprintOf)
contracts/stellar/upto/src/test/signed.rs: a_second_use_of_the_same_entry_failscontracts/stellar/upto/src/test/signed.rs: a_failed_settlement_does_not_consume_the_noncepackages/upto/test/upto.integration.test.ts: refuses a replay of a settled authorizationapps/rail402/test/idempotency.integration.test.ts: settles 5 concurrent requests for different amounts once: one success, four conflicts
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,invalid_upto_stellar_requirements_facilitator_mismatch)
packages/upto/test/upto.integration.test.ts: refuses an authority other than the one the payer namedcontracts/stellar/upto/src/test/auth.rs: missing_facilitator_authorization_is_rejectedcontracts/stellar/upto/src/test/auth.rs: facilitator_authorization_for_another_amount_is_rejected
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,transfer_from)
contracts/stellar/upto/src/test/allowance.rs: nobody_can_spend_the_residual_directlycontracts/stellar/upto/src/test/allowance.rs: another_contract_cannot_spend_the_residualcontracts/stellar/upto/src/test/allowance.rs: the_next_settlement_overwrites_the_residualcontracts/stellar/upto/src/test/allowance.rs: the_residual_expires_with_the_termscontracts/stellar/upto/src/test/settle.rs: zero_leaves_an_existing_allowance_untouched
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,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,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,bazaar_settlement_zero)
packages/facilitator/test/upto-scheme.test.ts: settles zero off-chain when configured: no transaction, the authorization consumed in the ledgerpackages/facilitator/test/upto-scheme.test.ts: drops the claim when an off-chain zero cannot settle, so it may be retriedapps/rail402/test/upto.integration.test.ts: never catalogs an off-chain zero settlementpackages/bazaar/src/testing.ts: does not catalog a settlement that moved nothing, however high its capapps/rail402/test/config.test.ts: requires explicit, safe mainnet settings
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,UptoTtlKeeper) - An operator command restores and extends where automatic restoration is off. (
packages/facilitator/src/contract-ttl.ts,restoreContract)
apps/rail402/test/upto.integration.test.ts: extends the contract from the keeper and the operator command, and reports its TTLapps/rail402/test/upto-archival.integration.test.ts: is restored by the TTL keeper where automatic restoration is allowedapps/rail402/test/upto-archival.integration.test.ts: refuses payments while the contract is archived, and settles once an operator restores itpackages/facilitator/test/contract-ttl.test.ts: never extends past the network’s maximum TTLcontracts/stellar/upto/src/test/settle.rs: an_archived_instance_is_restored_by_the_invocation_that_uses_it
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,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,invalid_upto_stellar_payload_wrong_contract)
packages/upto/test/deployment.test.ts: runs the committed wasmpackages/upto/test/deployment.test.ts: sits at the address derived from the deployer and the canonical saltapps/rail402/test/upto.integration.test.ts: refuses to start with a contract running other code, and starts without its authoritypackages/upto/test/codec.test.ts: resolves the settlement contract from overrides and canonical deployments
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,checkSimulationEvents)
packages/upto/test/facilitator.test.ts: accepts SEP-41 token events (three topics, map data)packages/upto/test/facilitator.test.ts: requires exactly one transfer of the cap from the payer to payTocontracts/testing/test-token/src/test.rs: approve_event_has_three_topics_and_map_datatools/conformance/test/assets.integration.test.ts: refuses a payer holding less than the amount, or none of the token, as insufficient_funds
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,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,Rotating the sponsor key)
packages/facilitator/test/authority.test.ts: is stable per sponsor and network, and distinct from the sponsor and every channelpackages/facilitator/test/authority.test.ts: reports whether the account exists and what it holdspackages/upto/test/upto.integration.test.ts: refuses an authority other than the one the payer named
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,Payment is not guaranteed after verification)
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,invalid_exact_stellar_payload_auth_invocation_mismatch)
packages/facilitator/test/settlement.integration.test.ts: rejects tampering: a changed amount breaks the signed authorizationpackages/facilitator/test/preflight.test.ts: rejects a transfer that does not match the requirementspackages/facilitator/test/preflight.test.ts: rejects an authorization signed for a different call than the operation
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,invalid_exact_stellar_payload_facilitator_in_auth)
packages/facilitator/test/preflight.test.ts: protects facilitator accountspackages/facilitator/test/preflight.test.ts: rejects a facilitator account as payer or co-signerpackages/facilitator/test/upto-scheme.test.ts: treats the authority as a facilitator account for exact payments tootest/agents.integration.test.ts: paid the seller exactly what was settled, from the payer, never from the sponsor
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,inspectExactTransaction)
packages/stellar/test/payload.test.ts: reports sub-invocations and preconditionspackages/stellar/test/payload.test.ts: rejects more than one operationpackages/stellar/test/payload.test.ts: rejects a fee-bump envelopepackages/stellar/test/payload.test.ts: rejects an oversized payload before decoding it
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,maxAcceptableExpiration)
packages/facilitator/test/expiration.integration.test.ts: holds the upper edge at maxLedger plus 2, from upstream’s ledger-time estimatepackages/facilitator/test/expiration.integration.test.ts: derives the expiration ledger from maxTimeoutSeconds like the stock clientpackages/facilitator/test/expiration.integration.test.ts: settles before the expiration ledger and refuses the same window after it
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,checkAssets) - SAC error numbers are applied only to SACs; other tokens are named by number. (
packages/stellar/src/diagnostics.ts,classifyTransferFailure)
apps/rail402/test/asset-check.test.ts: fails for good, with a coded ConfigError naming the variable, when the decimals differpackages/stellar/test/diagnostics.test.ts: names a custom token’s own error by number, never by the Stellar Asset Contract’s meaningtools/conformance/test/assets.integration.test.ts: refuses to start when a configured asset’s decimals differ from the chain’s
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,recordingSponsor) - A recorded settlement is expired only when the RPC’s retained history proves it absent. (
packages/facilitator/src/engine.ts,historyCovers)
apps/rail402/test/crash-recovery.integration.test.ts: K0: finds the transaction the network accepted before the process diedapps/rail402/test/crash-recovery.integration.test.ts: K1: rebroadcasts the identical recorded bytes when the broadcast never leftapps/rail402/test/crash-recovery.integration.test.ts: K2: finds a transaction that was included before the process saw it confirmedapps/rail402/test/crash-recovery.integration.test.ts: K3: expires a transaction whose time bound passed while the service was down, and nothing movespackages/facilitator/test/settlement.integration.test.ts: broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryable
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,PostgresChannelPool) - Sequence conflicts are counted so external use is visible. (
apps/rail402/src/metrics.ts,rail402_sequence_conflicts_total)
packages/facilitator/test/concurrency.integration.test.ts: with injected duplicates, without a collisionapps/rail402/test/replicas.integration.test.ts: with every duplicate sent to the other replicaapps/rail402/test/observability.integration.test.ts: counts a sequence conflict when a channel’s sequence is used behind its backpackages/facilitator/src/testing.ts: never leases one channel twice, even under concurrency
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,HealthMonitor) - Nothing is broadcast unless its envelope was recorded; verify stays available when the ledger cannot be read. (
packages/facilitator/src/scheme.ts,alreadySubmitted)
apps/rail402/test/runtime.integration.test.ts: fails while the RPC is unreachableapps/rail402/test/observability.integration.test.ts: counts transport and request errors while the RPC fails, and reports it in /healthapps/rail402/test/idempotency.integration.test.ts: keeps verify available when the ledger cannot be readpackages/facilitator/test/settlement.integration.test.ts: broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryablepackages/store-postgres/test/stores.integration.test.ts: survives the server closing its idle connections, as in a restart or failover
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,historyCovers)
apps/rail402/test/crash-recovery.integration.test.ts: keeps a settlement the RPC can no longer prove absent unresolved, and reports itpackages/facilitator/test/recovery.test.ts: never declares one expired once the RPC no longer retains the ledgers that could hold it
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,MemoryChannelPool)
packages/facilitator/test/settlement.integration.test.ts: refuses when no channel frees up in time, leaving the payment retryablepackages/facilitator/test/upto-scheme.test.ts: answers busy channels with the upto code and keeps nothing claimedapps/rail402/test/observability.integration.test.ts: measures the wait for a channel when settlements outnumber channels
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,REDACTED_PATHS) - Documented rotation: retire channels, move funds, provision with the new seed. (
docs/operations.md,Rotating the sponsor key)
apps/rail402/test/config.test.ts: names the variable and never echoes a secretpackages/facilitator/test/state.test.ts: is deterministic per sponsor, network and index, and never collidesapps/rail402/test/channels.integration.test.ts: reports durable state and refuses to retire while a channel is leased
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,sha256)
packages/search/test/search.test.ts: answers lexical-only, as partial results, when the embedder fails or runs out of timepackages/search/test/embedder.test.ts: refuses to start when a model file does not match its pinned hash
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,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,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,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,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,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,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,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,KeyedSharedRate)
apps/playground/test/ssrf.test.ts: refuses every private, local, reserved or off-policy literal before any lookupapps/playground/test/ssrf.test.ts: refuses a public name whose DNS answers include any private addressapps/playground/test/ssrf.test.ts: does not let the relay reach the Playground, the facilitator, the Bazaar or a denied host by any spellingapps/playground/test/ssrf.test.ts: never follows a redirect off the seller’s origin, even for unpaid requests that allow redirectsapps/playground/test/relay.test.ts: limits requests per destination host across clientsapps/playground/test/relay.test.ts: stops a client past its daily transfer budgetapps/playground/test/relay.test.ts: refuses a payment header that holds no payment before any limit counts itapps/playground/test/relay.test.ts: gives one network at most its share of the global paid budgetapps/playground/test/relay.test.ts: gives back the slots of an upload the client abandonsapps/playground/test/relay.test.ts: counts an MCP tool call carrying a payment in its body as paidapps/playground/test/relay.test.ts: keeps a paid pool of its own for the trusted hostsapps/playground/test/relay.test.ts: gives one network at most its share of a destination host’s budgetapps/playground/test/relay.test.ts: forwards a body only as JSON, text or octet-streamapps/playground/test/relay.test.ts: treats the platform’s generated domain as the Playground itselfapps/playground/test/relay-wire.test.ts: ends a body nobody reads at its deadline and cancels the seller’s streampackages/safe-fetch/test/fetch.test.ts: closes kept connections that sit unused and caps connections across hostsapps/playground/e2e/playground.spec.ts: never follows a redirect to another origin, and signs nothing
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,sameOrigin) - The relay answers no CORS headers, so no other origin can read an answer. (
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,proxyOrigin)
apps/playground/test/relay.test.ts: answers only same-origin requests from the pageapps/playground/test/seller-check.test.ts: answers only same-origin requests from the pageapps/playground/test/proxy.test.ts: refuses requests another site’s page makes, before any upstream fetch
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,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,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,non-extractable)
apps/playground/test/app.test.ts: sends the security headers and a strict content security policy on every responseapps/playground/web/test/engine/dependency.test.ts: the page never renders raw markupapps/playground/web/test/engine/dependency.test.ts: only the wallet modules touch the key itselfapps/playground/web/test/ui/lab.test.tsx: does not render HTML from seller dataapps/playground/e2e/playground.spec.ts: serves the built page with a strict CSP and starts the real engine
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,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,mountSellerCheck)
apps/playground/test/seller-check.test.ts: checks the URL with an https:443, public-only policy and the Playground’s own facilitatorapps/playground/test/seller-check.test.ts: refuses the same loopback seller in hosted mode before contacting itapps/playground/test/seller-check.test.ts: limits checks per client and across clients
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,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,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,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,sellBazaarExtension)
apps/playground/test/lab-sellers.test.ts: answer a valid x402 version 2 402 only, never contacting the facilitatorapps/playground/test/lab-sellers.test.ts: asks the facilitator to settle twice the cap and reports the refusalapps/playground/test/lab-sellers.test.ts: answers the same 402 every time, for any city, so the origin check always agreesapps/playground/test/lab-sellers.test.ts: keeps the declared example in every 402, whatever city the request namesapps/playground/test/lab-sellers.test.ts: answers anything but a city name with a coded 404, before any 402apps/playground/web/test/integration/engine.integration.test.ts: blocks a seller that charges over the cap: nothing moves, the hold returns after expiryapps/playground/web/test/integration/engine.integration.test.ts: sells through the sandbox route: settled, cataloged, origin-checked and found by search
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,additionalData) - One tab holds the wallet through a Web Lock; the others are read-only. (
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,payment_ledger_unavailable)
apps/playground/web/test/wallet/keystore.test.ts: stores the seed encrypted under a non-extractable key and loads it backapps/playground/web/test/wallet/keystore.test.ts: refuses a tampered or rebound record instead of loading another keyapps/playground/web/test/wallet/tab-lock.test.ts: lets one tab hold the wallet; another waits until it lets goapps/playground/e2e/playground.spec.ts: keeps the wallet in one tab: a second tab finds it locked
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,upstreamStatus) - The lab sellers’ trace header carries verify and settle outcomes, never payloads. (
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,runReport)
apps/playground/test/secret-scan.test.ts: keeps the payment and the key out of every channel but the seller and the facilitatorapps/playground/test/relay.test.ts: logs the host and outcome, never the target path, query, headers or bodyapps/playground/web/test/integration/engine.integration.test.ts: keeps the key and the payments out of every other channelapps/playground/e2e/playground.spec.ts: keeps the key and the payments out of every other channel
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
uptoamount 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 for how to report a vulnerability.