docs/security/findings.json.
A unit test fails when a finding has no severity or status, when a fixed finding has no fix commit on
main or no regression test that still exists, when an accepted finding has no written acceptance, or
when a critical or high finding is accepted rather than fixed. The release workflow refuses to publish a
version tag while a critical or high finding is open or accepted.
Tested revision
The release tag, commit, images and packages above are the revision this report applies to: every fix
commit below is an ancestor of that commit. The findings come from internal reviews of the code that shipped
in those packages. Several were found and fixed before the affected component was first released; for
those, the fix commit is the commit that introduced the component.
Scope and method
Each internal review read the code and the tests of the components in its scope, tried to break the invariants of the threat model with probes against a local network and the public testnet, and recorded every finding with a severity:- Critical: funds can be lost or stolen, or the facilitator becomes a source of funds.
- High: a stated security property does not hold, for example an agent can spend beyond its cap.
- Medium: a property holds only partly, or a wrong outcome is reported for a payment.
- Low: a defect with a narrow precondition or a bounded cost.
- Info: a note without a defect in Rail402.
Findings
Threat model
The threat model lists 170 threats: 146 mitigated, 23 accepted with a stated residual risk, 1 open.
The operational test report shows the exercises that run the threat
model’s tests, and the conformance report shows the runs against the testnet.
Accepted risks
Threats the threat model accepts, each with the risk that remains:External audit
No third-party audit has been completed yet. When one is, its report is linked from the review table above and its findings are added to the register with the same rules.Reporting a vulnerability
SeeSECURITY.md for how to report a
vulnerability privately and which versions receive fixes.
Finding details
F-2026-001
The buyer budget could release an upto remainder without proof High · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M1
Budget reconciliation released the unspent part of an upto cap as soon as any transfer from the payer to the recipient at or below the cap appeared on chain, without proving that the transfer came from that authorization. A seller could make a session under-count what it spent and so exceed the session cap. Reconciliation now counts a payment only from a transfer made with the payment’s own authorization, proven by the payer’s consumed nonce.
Fixed in c00699b. Tests
packages/sdk/test/reconcile.test.ts: settles a payment only from a transfer made with its own authorizationpackages/sdk/test/reconcile.test.ts: keeps an upto remainder pending until the chain shows what moved
F-2026-002
Paid calls were cut off after 15 seconds of seller silence High · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M4
The SDK applied its probe’s 15-second idle timeout to the paid request, although paid HTTP and MCP calls are meant to wait up to two minutes. A slow seller settled the payment and the buyer lost the answer. Paid requests now wait for the paid answer under their own timeout.
Fixed in c00699b. Tests
packages/sdk/test/http-flow.test.ts: waits for a paid answer that takes longer than the probe’s idle timeoutpackages/sdk/test/mcp-flow.test.ts: waits for a paid tool that works longer than the probe’s idle timeout
F-2026-003
A landed upto settlement could be reported as failed Medium · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats F2, O1
When a rebroadcast of a recorded settlement was refused as too late, the engine finished the settlement as failed with no funds moved, although the first broadcast could already have landed. The engine now keeps such a settlement pending and finishes it as failed only when the payer’s unconsumed nonce proves that nothing landed.
Fixed in 3a2ee7c. Tests
packages/facilitator/test/upto-engine.test.ts: stay pending when a rebroadcast is refused as too late: the transaction may have landedpackages/facilitator/test/upto-engine.test.ts: fail a permanently refused rebroadcast only once the unconsumed nonce proves nothing landed
F-2026-004
Zero-amount upto settlements were cataloged at the signed cap Medium · bazaar · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats P1, U6
A settlement of zero under an upto authorization created or updated a Bazaar listing at the cap the buyer signed, so a listing could be published for free. Zero settlements are never cataloged.
Fixed in cecab3a. Tests
apps/rail402/test/upto.integration.test.ts: never catalogs an off-chain zero settlement
F-2026-005
The sponsor’s hourly fee budget was optional on mainnet Medium · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats S4, U6
Sponsor spend on a network was bounded only by an hourly fee budget the operator could leave unset. A pubnet configuration now refuses to start without an explicit hourly budget, and the budget stops settlements before any RPC work once it is committed.
Fixed in cecab3a. Tests
apps/rail402/test/config.test.ts: requires explicit, safe mainnet settingspackages/facilitator/test/fee-exposure.integration.test.ts: stops settling once the hourly fee budget is committed, before any RPC work
F-2026-006
Any holder of an upto payload can choose the settled amount through /settle Medium · facilitator · Accepted risk · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats F5
Whoever holds a signed upto payload can call /settle with any amount up to the cap before the seller does. The payer’s signature still bounds the recipient, the token and the cap.
Tests
apps/rail402/test/upto-settlement.integration.test.ts: lets the first settle request fix the amount: a payload holder’s zero turns the seller’s settle into a conflict
- The first settle request fixes the amount; a later request for another amount is refused as a conflict naming the original transaction.
- The cap, recipient and token are bound by the payer’s signature.
- Mainnet facilitators can require API keys on /settle.
F-2026-007
A payment option without extra crashed the buyer flows Medium · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M2
A valid x402 v2 payment option without an extra field, or a seller error with non-string fields, raised a raw TypeError: the MCP tool reported an internal error with an unknown payment state and the CLI exited with an unexpected code. Such an option is now refused as unsponsored before anything is signed.
Fixed in c00699b. Tests
packages/sdk/test/http-flow.test.ts: refuses an option without extra as unsponsored, in pay and in quote, before signing
F-2026-008
An idempotency key stayed blocked after its authorization expired unused Medium · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M4
Once a call with an idempotency key had no stored result, every retry with the same key was refused as in progress for the whole budget window, even after the chain showed that the authorization expired unused. The retry itself now reconciles its row and frees the key when nothing was paid.
Fixed in c00699b. Tests
packages/sdk/test/http-flow.test.ts: frees an idempotency key on the retry itself, once the chain shows its authorization unusedpackages/sdk/test/ledger.test.ts: frees a key once the chain proves its signed authorization expired unused
F-2026-009
Domain verification trusted a one-way claim Medium · bazaar · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats P2
A listing reached domain_verified when the host’s stellar.toml listed the owner account, without the account naming the host as its home domain. An endpoint could claim another seller’s account. Trust is now raised only while the domain and the account name each other.
Fixed in b42266d. Tests
packages/bazaar/src/testing.ts: never raises trust on a one-way claim: a domain listing an account that does not name it
F-2026-010
Service logs could include RPC URLs and request bodies Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats O6
Transport errors from the RPC client were logged with the request URL and JSON-RPC body; a provider URL can embed an API key. The logger now writes such errors without their URL, body or headers.
Fixed in cecab3a. Tests
apps/rail402/test/logger.test.ts: logs an HTTP client error without its URL, body or headers
F-2026-011
Every upto verification called a public Horizon server Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats O3
The upstream upto verifier read the ledger close time from a public Horizon server on every /verify and /settle, before any static check, which made each request depend on a third party. The facilitator now reads it from its own RPC, in the service and in process.
Fixed in 3a2ee7c. Tests
packages/facilitator/test/ledger-seconds.test.ts: reads the ledger close time from its own RPC, never from Horizon
F-2026-012
/health and /ready published raw internal errors Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats O3
Health warnings carried raw error strings that could include internal addresses. Public health output now names the kind of failure only; the details stay in the service log.
Fixed in cecab3a. Tests
apps/rail402/test/health.test.ts: reports an unreachable RPC and an unreadable settlement ledger
F-2026-013
IPv4-translated IPv6 addresses passed the destination check Low · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats X6
safe-fetch did not recognise the IPv4-translated range ::ffff:0:0:0/96, so a URL could reach a loopback or metadata address in that spelling. Every IP literal spelling is now classified before any lookup, and such ranges are refused.
Fixed in 05294d9. Tests
packages/safe-fetch/test/address.test.ts: classifies IP literals in every spelling before any lookuppackages/safe-fetch/test/egress.integration.test.ts: refuses IP-literal URLs to this machine in every spelling, before connecting
F-2026-014
An MCP paid call could exceed the request limit after signing Low · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M4
Adding the payment to an MCP request could push it over the 64 KiB request limit only after the payment was signed, and the call was then reported with an unknown payment state. The size is now checked with the payment before anything is sent.
Fixed in c00699b. Tests
packages/sdk/test/mcp-flow.test.ts: refuses a paid call that would exceed the request limit with its payment, before sending it
F-2026-015
A ledger failure after sending was reported as unpaid Low · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M4
When the local budget ledger failed after a paid request had been sent, the SDK reported the payment as not paid and retryable, which invited a second payment. Such a call is now reported with an unknown payment state.
Fixed in c00699b. Tests
packages/sdk/test/http-flow.test.ts: never reports a payment as unpaid when the ledger fails after it was sent
F-2026-016
Seller structured content was returned unsanitized Low · sdk · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats M3
MCP structured content from a seller was passed to the agent as received, and a __proto__ key could replace the copy’s prototype. Seed redaction also missed a seed glued after other base32 characters. Structured content is now sanitized and bounded, and redaction covers glued seeds.
Fixed in c00699b. Tests
packages/sdk/test/mcp-flow.test.ts: sanitizes structured seller content and bounds a structured payment requestpackages/sdk/test/basics.test.ts: removes a seed glued after other base32 characters
F-2026-017
A null accepted.extra caused an unexpected error Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30
An upto payload whose accepted.extra was null threw inside the static rules and was answered as an unexpected error instead of a coded refusal. Such a payload is now refused with a reason naming the mismatch.
Fixed in 7b10203. Tests
packages/upto/test/facilitator.test.ts: refuses an accepted block or accepted extra that is not an object, naming the mismatch
F-2026-018
An expiration margin of zero ledgers was accepted Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats E4
The upto rules require a margin of at least one ledger between the current ledger and the authorization’s expiration, but the setting accepted 0. The margin now must be at least one ledger.
Fixed in 7b10203. Tests
packages/upto/test/facilitator.test.ts: requires an expiration margin of at least one ledger
F-2026-019
The contract TTL keeper and channel provisioning could use the sponsor at the same time Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats U7
The keeper that extends the upto contract’s TTL spent the sponsor’s sequence number under a lock separate from channel provisioning, so the two could collide. Both now run under one task lock that one replica holds at a time.
Fixed in 3a2ee7c. Tests
packages/store-postgres/test/stores.integration.test.ts: runs a named task on one replica at a time and frees the lock after it, even when it fails
F-2026-020
Settle-time simulation has no resource margin for smart-account payers Low · facilitator · Accepted risk · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats A2
A contract-account payer whose policy state grows between simulation and inclusion can make a settlement fail on chain, and the sponsor pays the fee.
Tests
packages/facilitator/test/fee-exposure.integration.test.ts: names a smart account whose __check_auth passes the settle-time simulation and fails at inclusion; the fee is counted as wasted
- Per-transaction fee ceilings and the hourly sponsor budget bound the cost.
- Failures at inclusion are reported with a stable code and counted in the wasted-fee metric.
F-2026-021
Rotating the upto authority breaks upto for running sellers until they restart Low · facilitator · Accepted risk · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats U10
A seller’s resource server reads the facilitator’s settlement authority once; after a rotation, its upto offers name the old authority and are refused until it restarts.
Tests
packages/facilitator/test/upto-preflight.test.ts: another facilitator’s authority in extra
- The rotation procedure in the operations guide includes the seller restart.
- Refusals name the mismatch with a stable code.
F-2026-022
Delegated signers were never exercised end to end Low · facilitator · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30 · threats A1
signerAuthorizations for smart accounts with delegated signers were specified and shipped but had no end-to-end test. The upto payer suite now settles with a delegate’s entry and refuses a missing or forged one.
Fixed in cecab3a. Tests
apps/rail402/test/upto-payers.integration.test.ts: carries the delegate’s entry in signerAuthorizations and settles
F-2026-023
The evidence runner passed the sponsor seed on a command line Low · operations · Fixed · found byREV-2026-09-internal-1 on 2026-09-30, resolved 2026-09-30
The conformance runbook passed the sponsor seed to env as a command-line argument, where other local users can read it. It now reads the seed from a file.
Fixed in 0ffef31. Tests
tools/conformance/test/run-evidence.test.ts: never passes the sponsor seed on a command line
F-2026-024
A false settlement claim could hide a real payment from the budget High · sdk · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats M1, M6
A seller could report another payment’s transaction hash as the settlement of a call; the claim blocked the real payment’s own proof, and reconciliation then released it, so the session cap under-counted. Claims are now confirmed from the chain, never overwrite a verified settlement, and a false claim is released only after expiry.
Fixed in c00699b. Tests
packages/sdk/test/reconcile.test.ts: confirms a reported settlement from the chain, and releases a false claim after expirypackages/sdk/test/http-flow.test.ts: counts a reused transaction hash against each payment that reports it
F-2026-025
An upto settlement through a wrapper contract was released from the budget High · sdk · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats M1
An upto settlement made through a wrapper or batch contract consumed the payer’s nonce and moved the cap, but the proof did not recognise it and reconciliation released the row. Proofs now follow the consumed nonce, and a cap whose settled amount cannot be read is counted whole.
Fixed in c00699b. Tests
packages/sdk/test/reconcile.test.ts: does not count an authorization carried in a transaction that never consumed itpackages/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 authorization’s whole cap when its settlement amount cannot be read
F-2026-026
Rows the RPC could no longer prove stayed pending forever Medium · sdk · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats M1
Budget rows that the RPC could no longer prove were carried into every renewed window, and the oldest ones starved reconciliation of new rows. Rows are now deferred and bounded per run, and a row is released only when the chain proves its authorization expired unused.
Fixed in c00699b. Tests
packages/sdk/test/reconcile.test.ts: keeps a row it cannot prove pending while its authorization is still validpackages/sdk/test/reconcile.test.ts: defers rows when the RPC fails mid-run and bounds the work per run
F-2026-027
A keyed MCP retry never reconciled its own row Medium · mcp · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats M4
A retry with the same idempotency key replayed a stale error until some unrelated call reconciled the row, and MCP agents had no way to trigger it. The retry now reconciles its own row first.
Fixed in c00699b. Tests
packages/sdk/test/mcp-flow.test.ts: frees an idempotency key on the retry itself once the chain shows its authorization unused
F-2026-028
An MCP tool listing could be planted under another seller for one base unit Medium · bazaar · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats P9, P2
Paying one base unit to a seller’s payTo was enough to publish and freeze an MCP tool listing with any description and price under that seller. MCP tools at an http(s) endpoint are now listed only after the tool’s own server confirms them with an unpaid call, and a planted settlement can neither withdraw nor change a confirmed tool.
Fixed in b42266d. Tests
packages/bazaar/src/testing.ts: lists an MCP tool at an https endpoint only once its unpaid call confirms itpackages/bazaar/src/testing.ts: never lets a planted settlement withdraw or change a tool its origin confirmed
F-2026-029
The upto reference facilitator could lose a settlement’s hash Medium · facilitator · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats F2
In the reference upto mechanism, a failed send answered every repeat with an unexpected error and no transaction hash, and a pending settlement was forgotten once its authorization expired, so a landed transaction could be reported as expired. Pending settlements are now kept and polled past expiry and answered with what landed.
Fixed in 7b10203. Tests
packages/upto/test/facilitator.test.ts: keeps polling a pending settlement after its authorization expired, and answers what landed
F-2026-030
The landing proof read the wrong ledger and misattributed Rail402’s own settlement Low · facilitator · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats F2
The proof that a recorded settlement landed compared a nonce read with an earlier, different RPC response, and recorded Rail402’s own landed fee bump as a third-party settlement without its fee. The engine now finds its own settlement by the inner-hash lookup and attributes it correctly.
Fixed in 3a2ee7c. Tests
packages/facilitator/test/upto-engine.test.ts: succeed as Rail402’s own settlement when the inner-hash lookup finds the recorded fee bump
F-2026-031
The per-client rate limit keyed on full IPv6 addresses Low · facilitator · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats S4
One IPv6 client could rotate addresses within its /64 and escape the per-client limit that bounds keyless sponsor spend. The limiter now keys an IPv6 address by its /64 and an IPv4-mapped address as the IPv4 address.
Fixed in cecab3a. Tests
apps/rail402/test/rate-limit.test.ts: keys an IPv6 address by its /64, however it is writtenapps/rail402/test/rate-limit.test.ts: keys an IPv4-mapped IPv6 address as the IPv4 address
F-2026-032
An unknown transaction meta version read as nothing happened Low · sdk · Fixed · found byREV-2026-09-internal-2 on 2026-09-30, resolved 2026-10-01 · threats M1
Budget proofs treated a transaction whose meta version the SDK could not decode as showing no transfer, so a future protocol upgrade could release paid rows. Such a transaction is now asked about again later.
Fixed in c00699b. Tests
packages/sdk/test/reconcile.test.ts: asks again later about a transaction whose meta this version cannot read
F-2026-033
A seller that echoed the payment header put the signed payload into results Medium · sdk · Fixed · found byREV-2026-09-internal-3 on 2026-10-01, resolved 2026-10-02 · threats M3
A seller that copied the PAYMENT-SIGNATURE header into its paid answer made the SDK return the signed payload in its result and store it in the budget ledger. The call’s own payment material is now removed from seller content before it is returned or stored.
Fixed in c00699b. Tests
packages/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 content
F-2026-034
MCP listing prices were not bound when paying a listing Low · sdk · Fixed · found byREV-2026-09-internal-3 on 2026-10-01, resolved 2026-10-02 · threats M5
When an agent paid a listed MCP tool, the live price was not compared with the listed one, so a seller could switch to a higher price up to the agent’s cap. A live price above the listed one is now refused before signing.
Fixed in c00699b. Tests
packages/sdk/test/mcp-flow.test.ts: binds a listing’s price: a live price above the listed one is refused before signing
F-2026-035
An idempotency key reused for another resource replayed the earlier result Low · sdk · Fixed · found byREV-2026-09-internal-3 on 2026-10-01, resolved 2026-10-02 · threats M4
A repeated idempotency key returned the earlier call’s stored result even when the new call targeted a different resource or tool. Such a reuse is now refused.
Fixed in c00699b. Tests
packages/sdk/test/http-flow.test.ts: refuses an idempotency key reused for another resource instead of replaying its resultpackages/sdk/test/mcp-flow.test.ts: refuses an idempotency key reused for another tool instead of replaying its result
F-2026-036
Exact payments to a muxed payTo could never settle Low · facilitator · Fixed · found byREV-2026-09-internal-3 on 2026-10-01, resolved 2026-10-08 · threats E1
The stock exact verifier compares the transfer event’s recipient with payTo literally, and a muxed M… address never matches the CAP-67 event, so such payments failed after submission. Exact requirements with a muxed payTo are now refused up front with a reason that names it.
Fixed in 3a2ee7c. Tests
packages/facilitator/test/preflight.test.ts: refuses a muxed payTo, which the exact verifier cannot match to the transfer event
F-2026-037
A bad upto signature from a G account was reported as expired terms Medium · facilitator · Fixed · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21 · threats A1
When a G payer’s upto authorization was signed by a key that is not its signer, or by too few multisig keys, the simulation classifier mapped the contract’s authentication error to signature_expired, telling the buyer to sign again rather than that the signature was wrong. Authentication failures now answer invalid_signature for a G payer and smart_account_rejected for a contract account; a real expiry stays signature_expired.
Fixed in 10abb21. Tests
apps/rail402/test/upto-tamper.integration.test.ts: refuses a G… payer signed with a key that is not its signer as a bad signatureapps/rail402/test/upto-tamper.integration.test.ts: refuses a 2-of-2 multisig G… payer signed with one key as a bad signaturepackages/upto/test/facilitator.test.ts: names a contract account’s failed authentication as its smart account’s refusalpackages/upto/test/facilitator.test.ts: keeps the settlement contract’s #5 without an authentication failure as expired terms
F-2026-038
A conflict was reported as final when the original settlement never reached the network Low · facilitator · Fixed · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21 · threats R1
When a request with the same authorization and another amount failed before broadcast, a concurrent request for the right amount was told the authorization was already settled, with no transaction and no retry. A request whose conflicting predecessor failed before anything reached the network is now settled.
Fixed in 10abb21. Tests
packages/upto/test/facilitator.test.ts: settles a request whose conflicting predecessor failed before anything reached the network
F-2026-039
The upto fee ceiling counted the fee bump’s inclusion fee once Low · facilitator · Fixed · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21 · threats S3
With a fee bump, the envelope pays the inclusion fee of the inner transaction and of the fee bump, but the ceiling check counted it once, so the sponsor could pay up to one inclusion fee above its ceiling. Both are now counted.
Fixed in 10abb21. Tests
packages/upto/test/facilitator.test.ts: enforces the fee ceiling, counting the inclusion fee of the transaction and of its fee bump
F-2026-040
The upto reference facilitator could miss a settlement re-wrapped by a third party Low · facilitator · Fixed · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21 · threats F2
Confirmation polled only the outer fee-bump hash, so if a third party wrapped the public inner transaction in its own fee bump and it landed, the reference mechanism could report it as expired. It now also looks the settlement up by its inner hash.
Fixed in 10abb21. Tests
packages/upto/test/facilitator.test.ts: finds a settlement that landed inside another party’s fee bump by its inner hash
F-2026-041
Two different maxTimeoutSeconds ranges in refusal messages Info · facilitator · Fixed · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21
The service named the allowed maxTimeoutSeconds range differently in two refusals. Both now name one range, raised to one ledger more than the expiration margin.
Fixed in 10abb21. Tests
packages/facilitator/test/upto-preflight.test.ts: names one maxTimeoutSeconds range, raised to a ledger more than the expiration margin
F-2026-042
upto events are only as trustworthy as the token contract Info · contract · Accepted risk · found byREV-2026-10-upto on 2026-10-21, resolved 2026-10-21 · threats U9
A malicious token can make transfer_from do nothing and emit false events while the settlement contract still emits upto_settled, so an indexer that trusts the event alone could count a payment that never happened. The payer is not at risk.
Tests
apps/rail402/test/asset-check.test.ts: fails for good, with a coded ConfigError naming the variable, when the decimals differ
- Each network accepts only the assets listed in its configuration.
- Asset decimals are read from the token at startup and a mismatch stops the service.
F-2026-043
Locked dependencies have published advisories High · operations · Fixed · found byREV-2026-10-dependency-scan on 2026-10-22, resolved 2026-10-23 · threats C5
The first osv-scanner and pnpm audit runs over pnpm-lock.yaml report advisories against locked versions, several rated high. Loaded at runtime by the service or the published packages: axios 1.18.0 and follow-redirects 1.16.0 (through @stellar/stellar-sdk, whose 16.x releases pin axios exactly), @modelcontextprotocol/sdk 1.31.0, @modelcontextprotocol/server and client 2.2.0, hono 4.13.8 and ip-address 10.7.2 (through the MCP SDK’s express-rate-limit). Build and test tools only: vite 8.3.0, @vitest/mocker 5.0.1 and source-map-js 1.2.1. Each has a fixed release. Reachability: axios and follow-redirects run in the facilitator process that holds the sponsor seeds, for every RPC and Horizon call; most of the axios advisories need an attacker-chosen URL or configuration, HTTP/2 or a separate prototype-pollution primitive, and the follow-redirects ones leak credential headers when a provider redirects, which matters when an RPC URL or its headers carry a provider key; the RPC URLs are operator configuration, never request input, so exploitation needs a hostile or compromised provider, but the code is reachable and must be fixed before serving pubnet. The MCP SDK’s memory-exhaustion advisory (GHSA-46jr-4wx8-v453) is reachable through the public MCP endpoint of the demo seller and the MCP server’s HTTP transport, which accept unauthenticated requests up to their body limit. The shared-transport advisory (GHSA-jm63-cq47-j5jp) is not reachable: both create a new transport per request. The MCP client’s redirect advisory applies when the Bazaar confirms a seller’s MCP endpoint, which runs through safe-fetch with redirects refused. hono’s serveStatic advisory and ip-address (express-rate-limit is not used by Rail402’s code) are not reachable. The development tools never run in a deployed service. The fix moves axios to 1.20.0, follow-redirects to 1.16.1, source-map-js to 1.2.2, ip-address to 10.7.4 and hono to 4.13.13 with pnpm overrides (the MCP SDK keeps its own hono otherwise), and the catalog’s MCP SDK to 1.32.1, its server and client to 2.3.1, hono to 4.13.13, vitest and @vitest/coverage-v8 to 5.0.5 and vite to 8.3.5 (the catalog pins vite for the browser builds, so vitest alone does not move it). With all of these, osv-scanner over every lockfile reports no advisory. The dependency scan job in CI fails while any of them is locked.
Fixed in ebeea7a. Tests
tools/conformance/test/dependencies.test.ts: lock no version with a published advisory fixed in a later releasetools/conformance/test/workflows.test.ts: runs dependency scanners and pins base images by digest
F-2026-044
Pubnet sponsor key files lacked the network tag the service requires High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats K3
The key ceremony’s tools wrote the pubnet sponsor seed without the `# rail402-key network=stellar:pubnet` tag the service requires on pubnet, so the first pubnet start would fail and invite a hand edit of the key file. The key generator now writes network-tagged key files in the service’s format, with the public key printed apart.
Fixed in 32bb685. Tests
tools/conformance/test/deploy-keys.test.ts: writes network-tagged key files in the format the service reads, with the public key apart
F-2026-045
A dump restore left the split database roles broken High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N8, K5
Restoring a logical dump as the superuser left every table owned by it and took the runtime role’s privileges away, so both replicas would fail their role check after an incident restore. restore-dump.sh now restores as the owner role and re-applies the runtime grants.
Fixed in 32bb685. Tests
tools/conformance/test/roles.integration.test.ts: restores a dump with the owner keeping every table and the runtime role its privileges
F-2026-046
No alert on WAL archiving, backup age or host disk space High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats K5, O11
Nothing alerted when WAL archiving failed or the newest backup aged, although a failing archive fills the database’s disk. Archiving, backup age and disk space are now exported for node_exporter and page the on-call.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: watches WAL archiving, backups and host disk space, and pages on each
F-2026-047
A compromised facilitator could silence alerts High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats O12
The facilitator replicas shared a network with Alertmanager, whose API can create silences without authentication, so code execution in the process that holds the sponsor seed could mute every alert. The monitoring networks are now split: only Prometheus reaches Alertmanager.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: keeps the service off the alerting network: only Prometheus reaches Alertmanager
F-2026-048
Recovery drills passed without checking that recovery worked Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats O11
A drill passed as soon as its alert fired; its recovery checks were recorded as text and never asserted. A drill now fails when any structured recovery check does not hold.
Fixed in 8695731. Tests
tools/conformance/test/drills.test.ts: fail a drill whose recovery check did not hold, and keep the verdict consistent with its checks
F-2026-049
Alerts did not follow each network in the two-network setup Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N4, O11
With neither network gating /ready, an RPC outage on one network no longer raised a readiness alert, and the sponsor thresholds were not per network. Readiness is now probed per network and the thresholds are per network.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: follows each network on its own: per-network readiness, per-network sponsor thresholds
F-2026-050
The deploy gate did not pin the release signer or bind the tag to origin Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats C2, C3
The gate accepted any signature the operator’s own git trust settings knew, resolved the tag locally, and accepted an attestation from any workflow or ref. Release signers are now pinned in deploy/release-signers, origin must have the same tag object, and the attestation must come from the release workflow run for that tag.
Fixed in ebeea7a. Tests
tools/conformance/test/check-deployable.test.ts: refuses a tag that is not on origin, or that origin has as another object
F-2026-051
Alert inhibition and thresholds had the wrong semantics Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats O11
An inhibition rule treated a missing network label as equal, so any page suppressed every unrelated ticket; the budget alert tested a fixed amount instead of the share of the budget; and a replica that disappeared never alerted. Inhibition is limited to alerts with a network label, the budget ratio is used and a missing replica raises a ticket.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: follows each network on its own: per-network readiness, per-network sponsor thresholdstools/conformance/test/alertmanager.integration.test.ts: lets a page silence only the tickets of its own cause on its own network
F-2026-052
The backup documentation claimed more than the restore drill proved Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats K5
The backup section implied encryption and off-host immutability that the drill did not exercise, and pgBackRest’s repository key could reach Postgres through its environment. The drill now proves the backup is encrypted, pgBackRest reads its settings from a file secret, and the documentation states what the drill does not prove.
Fixed in 32bb685. Tests
tools/conformance/test/restore-drill.integration.test.ts: records a restore drill with row counts and checksums
F-2026-053
Forged payloads used up the per-payTo and per-payer limits High · facilitator · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats S4, Q1
Payer and payTo quotas were taken before the payment proved its signatures, so anyone could exhaust a pubnet seller’s or payer’s quota with forged payloads at no cost. Quotas are now taken only after upstream verification and simulation succeeded.
Fixed in 9355cd2. Tests
packages/facilitator/test/party-limits.integration.test.ts: forged payloads naming a payTo or a payer use none of their quota
F-2026-054
TTL keeper spend that failed on chain bypassed the fee budgets Medium · facilitator · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats S13
A contract TTL extension that failed on chain was never recorded, so the keeper’s daily cap and the network’s hourly fee budget did not see its fee. The keeper now reserves its fee ceiling before signing and corrects it to the fee charged, keeping the ceiling when the outcome is unknown.
Fixed in 3549435. Tests
apps/rail402/test/upto-keeper-spend.integration.test.ts: keeps the ceiling counted when an extension’s outcome is unknown, so the daily cap holds
F-2026-055
Public metrics and health revealed pubnet budget state Medium · facilitator · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N6
The public /metrics and /health still showed when pubnet’s fee budget ran out, which tells an attacker when its spend has taken effect. Pubnet budget warnings and refusal reasons are now kept off the public views.
Fixed in 3549435. Tests
apps/rail402/test/metrics-exposure.test.ts: cannot tell pubnet budget exhaustion from any other failure on the public metrics endpoint
F-2026-056
Pubnet settlements still waited on the shared database pool Medium · facilitator · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N3
Pubnet’s own pool covered only the ledger and channel leases; its limiters and metering used the shared pool, so a testnet flood could still stall pubnet settlements, and pool waits had no bound. Pubnet limits and metering now use its pool, and every pool wait is bounded.
Fixed in 3549435. Tests
apps/rail402/test/pubnet-pool.integration.test.ts: settles pubnet through its own pool while testnet holds every shared connection
F-2026-057
JSON-RPC batches bypassed the hosted MCP endpoint’s limits High · mcp · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats H3
One HTTP request carrying a JSON-RPC batch ran many tool calls past the per-client rate limit and the in-flight cap. The hosted endpoint now refuses batches (`rpc_batch_unsupported`) and caps tool calls across all clients with an upstream budget.
Fixed in b08b49f. Tests
packages/mcp/test/hosted.test.ts: refuses a JSON-RPC batch, so one request runs at most one search
F-2026-058
All hosted MCP users shared one discovery rate-limit bucket Medium · mcp · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats H3
Every hosted MCP user reached the Bazaar from the endpoint’s one address, so one heavy user could use up the discovery limit for all of them. The hosted endpoint now reads a discovery-internal access key from a file and sends it with a per-client identifier, so the Bazaar budgets each remote client apart (the Bazaar side is F-2026-097).
Fixed in b08b49f. Tests
packages/mcp/test/hosted.test.ts: sends the key and a per-client identifier upstream, so each client has its own discovery budgetpackages/mcp/test/hosted.test.ts: reads the key from a file, refuses it inline, and never echoes a malformed one
F-2026-059
Hosted MCP mode loaded the key, signer and payment modules Medium · mcp · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats H2
Hosted, discovery-only mode imported the modules that hold keys and sign payments, contrary to its documentation and threat entry. It now imports only the discovery code, and a test checks the module graph.
Fixed in b08b49f. Tests
packages/mcp/test/hosted-graph.test.ts: loads no key, signer, ledger or payment module in hosted mode
F-2026-060
An approved MCP confirmation could be replayed for ten minutes Medium · mcp · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats M9
An approved payment confirmation stayed valid for ten minutes, so it could approve further payments even with `RAIL402_MCP_CONFIRM=every`. Confirmations are now single-use and bound to the tool call.
Fixed in b08b49f. Tests
packages/mcp/test/pubnet.test.ts: accepts a confirmation once: replaying the approved retry signs nothing more
F-2026-061
Origin checks of all networks shared one queue High · bazaar · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N5, P14
The origin-check queue took the oldest checks of any network and ran them one after another, so a testnet backlog of hosts that never answer delayed pubnet listings without bound. Origin checks now run in one bounded lane per network.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: checks each network’s origins in its own lane, so a testnet backlog of unanswering hosts never delays pubnet
F-2026-062
The load test could not measure settlement latency under discovery load Medium · bazaar · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats N1, N2
The load package served no payments, so the isolation claims (pubnet settlement latency under a testnet or search flood) were not measured. A settle probe now settles real payments during a search flood and reports their latency.
Fixed in 8695731. Tests
tools/loadtest/test/settle.integration.test.ts: settles real payments during a search flood and reports their latency percentiles
F-2026-063
Indexed events were not checked against the transaction’s result Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I1, I4
The indexer trusted Soroban events without binding them to the transaction’s result, and did not cross-check an upto settlement’s amount against its signed arguments. Events are now bound to the result and upto settle arguments are checked.
Fixed in 1530c9d. Tests
packages/x402-index/test/integrity.test.ts: stops on an upto settlement whose event and transfer were rewritten together
F-2026-064
Explorer partitions were not anchored to an independent source Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I4, I6
Partitions started unanchored, links between partitions were never checked and rebuild anchors outside the range were ignored. Each partition’s start is now anchored to an independent ledger hash and stored links are checked.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: commits a ledger only after its hash matched the anchor, stops on another hash and waits while it is unavailable
F-2026-065
A bounded indexer run skipped ledgers the source did not hold yet Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I6
When the source answered that a ledger was not available yet, a bounded run skipped the rest of the partition and exited successfully. It now fails at that ledger instead of moving on.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: keeps a route’s checkpoint when discovery fails for now, and fails a bounded run that cannot finish
F-2026-066
The testnet beta was missing from the attribution registry Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I3
Settlements through the earlier testnet beta’s contracts appeared as unattributed traffic. The beta’s contracts are now registered and their settlements labelled as beta history.
Fixed in 1530c9d. Tests
packages/x402-index/test/registry.test.ts: labels settlements through the beta’s testnet contracts as beta history, whoever submitted them
F-2026-067
Stored history was not re-attributed after a registry update Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I3, I8
After a registry version bump the live database kept the old attribution. Stored rows are now re-attributed, or partitions re-indexed when the upto contracts change.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: follows registry changes: routes come and go with their ledgers, and re-attribution equals a fresh index
F-2026-068
Shared-relayer rows were counted in the headline without a flag Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I3
Rows from a facilitator marked as a shared relayer were folded into the headline totals with nothing on the row saying so. They are now flagged and totalled on their own line.
Fixed in 50e7461. Tests
apps/explorer/test/rebuild.test.ts: flags shared-relayer rows on the row and totals them on their own line
F-2026-069
Open redirect through the Explorer’s network aliases Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats W6
The `/public`, `/pubnet` and `/mainnet` aliases redirected to a path taken from the request, which could send a visitor to another site. The aliases now redirect only within the Explorer.
Fixed in 50e7461. Tests
apps/explorer/test/pages.test.ts: redirects aliases and upper-case hashes, and answers unknown paths with explorer_not_found
F-2026-070
Indexed transaction pages were cached as immutable Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats W5
Transaction pages were marked immutable for a year although their attribution can change, and the ETag named the served registry rather than the row’s. Rows under older versions are now cached briefly with an ETag of their own versions.
Fixed in 50e7461. Tests
apps/explorer/test/pages.test.ts: caches a row under an older registry version only briefly, with an ETag of its own versions
F-2026-071
Explorer pool acquisition never timed out and pages were unbounded Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats W3
Waiting for a database connection had no timeout, one account page held several connections and ran uncapped aggregates. Requests beyond the in-flight limit are now answered 503 with Retry-After, and database use is bounded.
Fixed in 50e7461. Tests
apps/explorer/test/api.test.ts: answers 503 with Retry-After instead of queueing beyond the in-flight limit
F-2026-072
Explorer per-client limits were per exact IP and the lookup budget was cheap to exhaust Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats W4
Limits keyed by exact IPv6 address and per process let one client rotate addresses, and the global live-lookup budget could be used up cheaply. IPv6 clients are now grouped by /64 and the lookup budget keeps a reserve and refunds refused lookups.
Fixed in 50e7461. Tests
apps/explorer/test/live-lookup.test.ts: keeps a reserve for first lookups, refunds refused ones and groups IPv6 clients by /64
F-2026-073
The Explorer report rescanned everything every few minutes per replica Medium · explorer · Fixed · found byREV-2026-10-internal-tracks on 2026-10-22, resolved 2026-10-23 · threats I9, W3
Each web replica recomputed the full report every five minutes in one long transaction. The indexer now publishes the report and the web role serves it.
Fixed in 50e7461. Tests
apps/explorer/test/web.integration.test.ts: serves the report the indexer published, equal to computing it again
F-2026-074
Throwaway smart accounts strand funds on pubnet conformance runs High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats M1
Contract-account payer cases funded an account whose owner key was discarded at the end of the run, so its remaining balance could not be swept back to the treasury. Smart accounts are now deployed only through the run’s funder, which derives and keeps their owner keys and returns their USDC before merging the run’s accounts.
Fixed in 8695731. Tests
tools/conformance/test/network.integration.test.ts: returns a smart account’s USDC with its derived owner key and merges the run’s accountstools/conformance/test/network.test.ts: deploy smart accounts only through the funder, which keeps their owner keys
F-2026-075
Third-party suite code runs with the treasury secret path in its environment High · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats C5
The upstream end-to-end suite inherited the treasury and sponsor secret file paths. It now gets only an allow-listed toolchain environment and its own reviewed variables, and no secret-like variable reaches it whatever its prefix.
Fixed in 8695731. Tests
tools/conformance/test/upstream-e2e-env.test.ts: never passes a secret-ish variable of this process, whatever prefix it has
F-2026-076
Spend caps do not see real Soroban fees and miss some deployments Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats M1
The conformance spend caps were debited with estimates and some contract deployments were not debited at all. Every fee the treasury pays is now debited before it is sent and counted as charged, and a fee source outside the run is refused.
Fixed in 8695731. Tests
tools/conformance/test/network.integration.test.ts: counts every fee the treasury pays, as charged, and refuses a step past the captools/conformance/test/network.test.ts: debits every fee the treasury pays before sending, and refuses a fee source outside the run
F-2026-077
Sweep-back is skipped when a harness throws or is interrupted Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats M1
An exception between funding and sweep-back left funds in derived accounts. A run now sweeps once after its body, also when the body throws, before the error is rethrown.
Fixed in 8695731. Tests
tools/conformance/test/network.test.ts: sweeps once after the body, and after a body that throws before rethrowing
F-2026-078
The settlement cap is a static estimate, not checked per settlement Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats M1
The settlement cap was debited once with a hand-counted bound. Every /settle a harness sends is now debited at its scheme’s fee ceiling before it is sent, and none is sent past the cap.
Fixed in 8695731. Tests
tools/conformance/test/facilitator-guard.test.ts: debits every /settle at its scheme’s fee ceiling before sending it, and sends none past a cap
F-2026-079
Spend caps apply per harness process and one confirmation is reusable Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats M1
Spend caps applied per harness process and the real-funds confirmation was reusable. One run ledger now holds the caps for every harness process of a run, and a confirmed run is claimed once.
Fixed in 8695731. Tests
tools/conformance/test/run-ledger.test.ts: never lets concurrent harness processes pass the run’s cap togethertools/conformance/test/network.test.ts: plans a run on the first attempt and spends nothing; a confirmed run claims it once
F-2026-080
Pubnet upto runs ignore the configured contract in several harnesses Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats U8
Several harnesses did not pass the pubnet upto contract on, so pubnet upto cases failed after funding accounts. Every upto client now gets the configured contract on pubnet.
Fixed in 8695731. Tests
tools/conformance/test/network.test.ts: pass the configured upto contract to every upto client on pubnet
F-2026-081
Harness fee ceilings assume defaults the facilitator does not have Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats S3
The harnesses assumed pubnet fee-ceiling defaults that the facilitator requires explicitly, and ignored malformed overrides. Pubnet runs now read the fee ceilings explicitly and refuse a malformed or zero value.
Fixed in 8695731. Tests
tools/conformance/test/network.test.ts: refuses a malformed or zero fee ceiling instead of ignoring it
F-2026-082
Skip categories are never emitted, so the pending-dependency gate cannot fire Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats O11
Skipped cases rendered as not categorised, so a required check skipped for a missing dependency could still be published. Evidence recorded from the requirement’s date must give every skipped case a category.
Fixed in 8695731. Tests
tools/conformance/test/evidence.test.ts: requires a skip category on the skipped cases of a run recorded from the requirement’s date
F-2026-083
A keyed facilitator is not detected before a pubnet run spends Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats Q4
A pubnet run against a facilitator that requires an access key funded accounts before every request was refused. The run now checks the facilitator first and refuses before spending when it wants a key the run does not have.
Fixed in 8695731. Tests
tools/conformance/test/facilitator-guard.test.ts: refuses a run before spending when the facilitator wants a key it does not have or refuses
F-2026-084
A closed upstream tracking issue is recreated every week Medium · operations · Fixed · found byREV-2026-10-internal-tracks on 2026-10-23, resolved 2026-10-23 · threats SW3
The upstream watch recreated a tracking issue a maintainer had closed while the drift was unchanged. A hand-closed issue now stays closed until the drift changes.
Fixed in f8c4e1c. Tests
tools/upstream-watch/test/lifecycle.test.ts: respects an issue a person closed while its drift persists, until the drift changes
F-2026-085
An exact payment with its own operation source held a channel and fee budget without landing High · facilitator · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats O5, S8, S4
The payer’s authorization does not cover the operation source, and upstream settlement keeps it when it rebuilds the transaction from a channel. A payload with any non-facilitator operation source therefore verified, was recorded, and was then refused by the network for the missing signature. Its record stayed open until the authorization expired, holding its channel and fee reservation; with a handful of such payloads every channel could be kept busy at no cost. Such operation sources are now refused before anything is claimed (invalid_exact_stellar_payload_operation_source_set), a first submission the network refuses for good finishes at once as a submission failure with nothing charged and frees its channel, and finished settlements count the fee actually charged against the budgets. Because a copy of an exact authorization can still land while it is valid, a refused first submission now gives its channel back at once but stays open until the authorization expired unused (then a submission failure with nothing charged) or a copy landed (then that payment is answered); an upto settlement, which only the authority can settle, still finishes at once.
Fixed in 9355cd2. Tests
packages/facilitator/test/preflight.test.ts: refuses any operation source, which settlement keeps but cannot sign forpackages/facilitator/test/operation-source.integration.test.ts: is refused at verify and settle, before any claim, channel or feepackages/facilitator/test/first-submission.test.ts: finishes as a submission failure once the authorization expired unused, freeing the channel and fee budgetpackages/facilitator/test/first-submission.test.ts: answers a copy of the authorization that landed before its first submission was refusedpackages/facilitator/test/first-submission.test.ts: keeps a refused first submission open while its authorization is valid, and answers a later copypackages/facilitator/test/first-submission.test.ts: stays open when the resend is refused for a reason that can passpackages/facilitator/test/upto-engine.test.ts: finish an upto settlement refused at its first broadcast without waiting for confirmationpackages/facilitator/src/testing.ts: counts a finished settlement’s charged fee, nothing when it expired or was refused unsent
F-2026-086
One network’s reconciliation failure or hang held up the others Medium · facilitator · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats N3, O3
All networks were reconciled in one sequential round without a per-network catch, so a testnet ledger error or a hung testnet RPC call stopped pubnet’s recorded settlements from being finished and their channels from being freed. Each network now reconciles on its own, one round at a time per network, and RPC calls are bounded.
Fixed in 9355cd2. Tests
packages/facilitator/test/reconcile-isolation.test.ts: still reconciles pubnet when testnet’s ledger failspackages/facilitator/test/reconcile-isolation.test.ts: reconciles pubnet on its own while testnet’s round hangs
F-2026-087
The per-payTo limit was shared by every caller Low · facilitator · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats S8
MAX_SETTLEMENTS_PER_PAYTO_PER_MINUTE counted every caller in one bucket per payTo, so genuine minimum-amount payments sent without a key could use up the quota of a seller settling under its own key. The count is now kept per access key, with one shared count for keyless callers.
Fixed in 9355cd2. Tests
packages/facilitator/test/limits.test.ts: keeps a keyed seller’s payTo quota apart from keyless traffic to the same payTo
F-2026-088
RPC calls had no timeout, and a slow settlement could outlive its channel lease Low · facilitator · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats O2
The Stellar SDK sets no timeout on RPC calls, so a hung provider could keep a settlement before signing past its channel lease, after which a second settlement could build on the same channel sequence. Every RPC call of the service’s clients now fails after 10 seconds, a settlement that has signed nothing by the end of its 30-second claim window signs nothing afterwards, and a channel referenced by an open settlement is never leased again.
Fixed in 9355cd2. Tests
packages/facilitator/test/rpc-pool.test.ts: fails a call to a provider that never answerspackages/facilitator/test/first-submission.test.ts: stops there, signs nothing later and frees its claim and channelpackages/store-postgres/test/stores.integration.test.ts: never lease a channel whose recorded settlement is still open, released or expired
F-2026-089
A pubnet upto settlement could settle less than the asset’s minimum Low · facilitator · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats S8
The asset minimum applied to an upto cap but not to the amount settled, so a non-zero settlement of one base unit cost the sponsor a full fee. On pubnet a non-zero upto settlement below the minimum is now refused with invalid_upto_stellar_requirements_amount_out_of_range; a zero still settles off-chain.
Fixed in 9355cd2. Tests
packages/facilitator/test/upto-preflight.test.ts: holds a non-zero pubnet settlement to the asset’s minimum; zero and testnet amounts pass
F-2026-090
A revoked access key stayed valid behind a single-file mount Medium · operations · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats Q2
A container mount of the single key file keeps showing the file it was created with, so a key file replaced by rename on the host was never seen and a revoked key kept working until a restart, with no reload error. API_KEYS_DIR now reads keys.json from a mounted directory, reloads compare the inode too, and /health/private shows the digest of the file in force.
Fixed in 3549435. Tests
apps/rail402/test/access.test.ts: sees a file replaced by rename inside its directory, even with the same size and timeapps/rail402/test/access.test.ts: reads keys.json from API_KEYS_DIR, and refuses it together with API_KEYS_FILE
F-2026-091
Every /ready request took a database connection Low · operations · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats N3
The public, unlimited /ready ran a database query per request on the pool that also serves settlements, so a flood of readiness requests could make settlements wait for connections. The database check now runs at most once every 2 seconds, shared by concurrent requests.
Fixed in 3549435. Tests
apps/rail402/test/cached-probe.test.ts: runs once for any number of concurrent requests and answers from its result for the TTL
F-2026-092
Public /health showed access-key counts Info · operations · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats N6
The public /health listed active, expired and revoked key counts and the code of a failed key reload. They are now shown in /health/private only.
Fixed in 3549435. Tests
apps/rail402/test/health.test.ts: keeps access keys out of the public view: no counts, no reload state, no error code
F-2026-093
Pubnet secret settings were looser than intended Info · operations · Fixed · found byREV-2026-10-internal on 2026-10-23, resolved 2026-10-23 · threats K1, N8
With pubnet, the service accepted the schema owner’s connection (DATABASE_MIGRATION_URL), database URL and cursor secret files readable by others, and RPC URLs with credentials in the environment. Each is now refused at startup with a coded error.
Fixed in 3549435. Tests
apps/rail402/test/secrets.test.ts: refuses the schema owner’s connection in the service process when pubnet is enabledapps/rail402/test/secrets.test.ts: refuses database and cursor secret files readable by others when pubnet is enabledapps/rail402/test/secrets.test.ts: refuses pubnet RPC URLs with credentials in the environment, and takes them from the file
F-2026-094
Search snapshots could outgrow the process’s memory High · search · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats D2, N2
Each search index snapshot kept every published listing with its full content, the snapshot size estimate counted only the indexed text, nothing bounded the catalog’s size, and up to eight superseded snapshots stayed alive while their cursors were valid. Free testnet mcp:// listings of maximum size, plus a cursor after each catalog change, could grow the service’s memory until it stopped, together with pubnet settlement in the same process. Snapshots now keep only what ranking and filters read and pages read listings from the catalog; the catalog is read in batches with a statement timeout; each field indexes a bounded number of terms; each network’s listings take a bounded share of a snapshot (unverified listings at most half); the size estimates are calibrated against the measured heap of thousands of maximum-size listings; and each network holds a bounded number of published listings, with unverified and idle listings expiring.
Fixed in 2fd3600. Tests
packages/search/test/memory.test.ts: estimates a snapshot at no less than the heap it holds, for thousands of maximum-size listingspackages/search/test/memory.test.ts: stays within its memory caps under a flood of free testnet listings, and keeps every pubnet listing searchablepackages/bazaar/src/testing.ts: refuses a new listing while its network holds its maximum of published listings, and publishes none past itpackages/bazaar/src/testing.ts: withdraws an unverified tool after its retention and lists it again on its owner’s next settlement
F-2026-095
Unconfirmed listings counted toward the network-wide quota Medium · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats N5, P13, P5
The network-wide hourly quota of new listings counted pending and withdrawn listings, which any payer can create for any payTo. Dust payments to junk URLs could fill pubnet’s quota and refuse every honest seller’s first listing. The network-wide quota now counts only listings published now; unconfirmed listings count toward their payer’s quota only.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: never lets unconfirmed listings use up the network-wide quota, so dust payments cannot block a sellerpackages/bazaar/src/testing.ts: bounds the new listings the whole catalog publishes per hour
F-2026-096
A shared schema validator let previews and testnet starve pubnet cataloging Medium · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats P3, N5
One validator worker with one queue served both networks and the /verify previews, and a settlement refused because the queue was full lost its cataloging for good. Repeated previews with slow schemas could keep pubnet settlements from being cataloged. Each network now validates settlements and previews in sandboxes of its own, a busy preview reports processing, and a busy settlement validation stays queued and is retried.
Fixed in 2fd3600. Tests
packages/bazaar/test/security.test.ts: validates each network’s settlements and its previews in their own lanes, so a flood of slow schemas starves no one elsepackages/bazaar/src/testing.ts: retries a settlement whose schema validation found the validator busy, instead of dropping it
F-2026-097
Hosted discovery users shared one cost budget Medium · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats Q6, H3
The discovery cost bucket was keyed by the caller’s address only, even for an internal discovery identity, so every user of a hosted front end shared one budget and one user’s searches could refuse everyone else’s. Requests of a discovery-internal key are now charged to their forwarded client’s bucket and to the identity’s own, never to the address.
Fixed in 3549435. Tests
apps/rail402/test/access.test.ts: charges discovery costs to each forwarded client and to the identity, never to the identity’s address
F-2026-098
Origin and stellar.toml fetches could be held open by a drip-fed body Medium · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats X4, X5
Origin checks stopped their timer once headers arrived and then drained the body without a deadline; a non-200 stellar.toml was drained the same way. A host dripping bytes could hold one socket per check open for days. Origin checks now close the connection as soon as the headers arrive, and stellar.toml reads keep one deadline through the whole body.
Fixed in 2fd3600. Tests
packages/bazaar/test/origin.test.ts: closes the connection once the headers arrive, so a drip-fed body never holds the socketpackages/bazaar/test/stellar-toml.test.ts: keeps one deadline through the body, so a drip-fed file is cut off and its socket closed
F-2026-099
A pending ownership claim could be replaced by another payTo Low · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats P2, P13
A listing kept one claimant: a later settlement by another payTo replaced the claim and reset the check, and a check asked for by another payTo could quarantine a published listing on one passing error of its origin. Claims are now kept as a set, a claim never resets a check, and a check only claims asked for never withdraws a published listing.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: keeps every payTo’s claim to a listing, so one claimant cannot replace another’spackages/bazaar/src/testing.ts: never withdraws a published listing on a check only another payTo’s claim asked for
F-2026-100
One facts queue served every network Low · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats N5
Stellar facts and domain trust were refreshed in one queue across networks, so testnet listings whose hosts stall could delay pubnet trust indefinitely. Each network now refreshes in its own lane, and a busy lane is skipped.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: refreshes each network’s facts in its own lane, so slow testnet hosts never delay pubnet trust
F-2026-101
Route-template listings could change owner on every settlement Low · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats P2
A route-template listing was checked at the claimant’s own path, so on a host serving one tenant per path each tenant could take the template over in turn. A template listing now moves only to a payTo that its example path names too.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: moves a route-template listing only to a payTo its example path names too
F-2026-102
Listing detail showed metadata no origin confirmed Info · bazaar · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23 · threats P13
The detail and history routes returned the content of pending and withdrawn listings, which can be metadata a payer echoed under the payTo it paid. Funds were never at risk (the SDK pays published listings only). Those routes now withhold that metadata and say so with contentWithheld.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery.test.ts: show their state and settled payment, never the metadata a payer echoed
F-2026-103
The SDK read only the first page of a listing’s history Info · sdk · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-23
DiscoveryClient.versions() returned the first page of at most 100 versions; the Bazaar pages longer histories. It now follows pagination.next to the end, with a bound on the pages read.
Fixed in b08b49f. Tests
packages/sdk/test/discovery.test.ts: reads every page of a long version history
F-2026-104
One pubnet approval covered later payments to any recipient in the window Medium · sdk · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M9, M1
Under the first-per-session confirmation policy (the rail402-mcp pubnet default), approving the first payment of a budget window let every later payment of that window be signed without asking, to any recipient, resource or listing, up to the caps, and the prompt did not say so. An agent misled by seller or listing text could then pay other sellers unasked. An approval now covers only the window, recipient base account, listing (or resource without one), asset, scheme and settlement authority it was shown for; anything else is asked about again. Every prompt states what approving covers, with the per-call cap, the budget left and the window end, and SignIntent carries approvalScope and those session fields.
Fixed in b08b49f. Tests
packages/sdk/test/pubnet-safety.test.ts: asks again for every new recipient or listing in a window, and says what an approval coverspackages/mcp/test/pubnet.test.ts: states the approval’s scope: the recipient and listing it covers, the caps and the window end
F-2026-105
A pubnet buyer built in code accepted a plain-http or private Bazaar URL Low · sdk · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M11
createBuyer and createBrowserBuyer checked the RPC and seller egress on pubnet but passed bazaarUrl and facilitatorUrl through, while the environment path refused non-https. An on-path attacker could then rewrite listings, which decide whom a pubnet buyer pays. On pubnet both URLs must now be https on a public host name: no credentials, IP literal, localhost, single label or private-network suffix (payment_policy_invalid).
Fixed in b08b49f. Tests
packages/sdk/test/pubnet-safety.test.ts: refuses a plain-http, IP-literal or private-network Bazaar or facilitator built in code
F-2026-106
An MCP resource reached confirmation prompts as raw text Low · mcp · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M9
The MCP payment flow used the endpoint string as given, so line breaks or bidirectional overrides that the URL parser drops or encodes could add lines to, or reorder, the MCP and CLI confirmation prompts. Such an endpoint is now refused with egress_url_invalid, the normalized URL is what is confirmed and reserved, and both prompts show untrusted fields on one line without control, line-break or bidirectional characters.
Fixed in b08b49f. Tests
packages/sdk/test/mcp-flow.test.ts: confirms and records the normalized endpoint URL, never the raw textpackages/mcp/test/pubnet.test.ts: shows untrusted fields on one line, without line breaks, controls or bidi overridespackages/cli/test/pay-confirm.test.ts: shows the upto authority and keeps untrusted fields on one line
F-2026-107
A pubnet upto seller could name its own settlement authority Low · sdk · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M13
With an upto contract pinned on pubnet, any settlement authority other than the payer and the recipient was accepted, so a seller could name a key it controls and settle the full cap; the prompts did not show the authority and the sealed MCP intent did not bind it. A signing pubnet upto buyer now pins its authorities from uptoFacilitators or from its facilitator’s /supported, and refuses to open without either; the authority is shown in both prompts and bound in the sealed MCP intent.
Fixed in b08b49f. Tests
packages/sdk/test/pubnet-safety.test.ts: pins the authorities the facilitator advertises for upto on pubnetpackages/sdk/test/pubnet-safety.test.ts: refuses to open a signing pubnet upto buyer with no pinned authoritypackages/mcp/test/pubnet.test.ts: shows the upto settlement authority and binds it, with the listing, to the approval
F-2026-108
The memory budget store gave pubnet a full budget on every start Info · sdk · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M8
With RAIL402_BUDGET_STORE=memory, every rail402 run and every restart of rail402-mcp opened a new budget window with the full session total. A pubnet configuration with a key now refuses the memory store and keeps its budget in the SQLite ledger.
Fixed in b08b49f. Tests
packages/sdk/test/pubnet-safety.test.ts: keeps a keyed pubnet budget in the persistent store
F-2026-109
The CLI confirmation was described as an agent control Low · docs · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M9
The threat model listed the CLI’s terminal confirmation beside MCP elicitation as a pubnet control, but an agent that runs the CLI can pass —yes or answer its own terminal. The CLI guide and the threat model now say the question protects a person at the terminal, point agents to rail402-mcp’s elicitation, and name the controls that hold against an agent: caps, the tagged pubnet key file and the spending policy.
Fixed in b08b49f. Tests
packages/cli/test/pay-confirm.test.ts: asks for confirmation before the first pubnet payment unless —yes
F-2026-110
The Playground key download was untagged Info · cli · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats M10
The Playground downloaded its testnet key as a bare seed, which rail402 keys import could tag for pubnet without warning. The download is now a key file tagged for testnet, and keys import reads tagged files and refuses a tag for another network with key_network_mismatch.
Fixed in 9bee50a. Tests
apps/playground/web/test/wallet/keystore.test.ts: writes a key file the CLI and SDK read, and never shows the key otherwisepackages/cli/test/local.test.ts: takes a key file the Playground exported, after the documented step
F-2026-111
The Playground relay could reach the facilitator’s other network hosts Low · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats PG1
The relay refused the facilitator and Bazaar host names it was configured with, but one facilitator process also answers for pubnet and the Explorer under sibling host names, so Playground users could drive those through the relay as one shared keyless client. When the facilitator or Bazaar host starts with testnet., mainnet. or pubnet., the relay now also denies the same domain under testnet., mainnet., pubnet. and explorer.; RELAY_DENY_HOSTS stays for any other name.
Fixed in 9bee50a. Tests
apps/playground/test/ssrf.test.ts: does not let the relay reach the other network and Explorer hosts of the same facilitator
F-2026-112
One hostile transaction could stop the Explorer indexer Medium · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I5
A settlement through a concluded beta contract took the seller’s muxed id from the transfer event without comparing it with anything, and a CAP-67 to_muxed_id in the text form was kept byte for byte. A token contract chosen by the caller could emit an id containing a NUL character, which Postgres refuses in text, so the commit failed, the partition was marked failed and every restart failed at the same ledger. A to_muxed_id is now taken only as a u64 in every rule (the text and hash memo forms never come from a contract call and count as transfer_event_mismatch), and every row field must have its expected shape and be storable text, or the transaction is counted as a malformed exclusion. Indexing continues past such a transaction.
Fixed in 1530c9d. Tests
packages/x402-index/test/hostile.test.ts: never takes a text or hash memo form of to_muxed_id into a row, in any rulepackages/x402-index/test/hostile.test.ts: refuses any row field that could not be stored as it isapps/explorer/test/indexer.integration.test.ts: counts a hostile token’s NUL muxed id as an exclusion and keeps indexing
F-2026-113
Indexers sharing a database collided on the totals tables Medium · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I6
Each commit refreshed the daily and per-account totals by deleting and re-inserting the rows it touched. Two workers committing partitions with a shared seller or day, or a reset racing a worker, both inserted the same key; one commit failed with a unique-key error and its partition was marked failed. The refresh now runs under a transaction-scoped advisory lock per network and epoch, so such transactions take turns and the second recomputes from every committed row, and a commit that still loses a race is retried instead of failing the partition.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: refresh shared sellers and days concurrently without colliding on the totals
F-2026-114
Rows between anchors were served before they were anchored, and failed partitions stayed in the report Medium · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I4, I8
The indexer checked an independent Horizon anchor only at a partition’s start and when a partition completed, about 3.7 days of ledgers apart. Rows of a self-consistent fork from the RPC fallback or a tampered data lake object were committed, served and reported in between, and the report kept a partition the indexer had marked failed, or one that did not link to its neighbour, in its coverage. A batch is now committed only after the hash of its last ledger matched the anchor (once per batch in the backfill, every 16 ledgers at the head), the anchors checked are stored and listed in the published manifest, and the report stops before a failed or unlinked partition and is not marked reproducible while one exists. The rebuild command takes a published manifest’s anchors with —anchors-from.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: commits a ledger only after its hash matched the anchor, stops on another hash and waits while it is unavailableapps/explorer/test/indexer.integration.test.ts: stops at a ledger whose bytes are not the ledger asked for, marks it failed and holds the report before itapps/explorer/test/indexer-units.test.ts: stops before the first candidate that is pending or failed
F-2026-115
A live lookup did not check that the RPC answered for the requested hash Low · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I4
A transaction page for a hash not indexed yet showed whatever transaction the lookup RPC returned, under the requested hash, and cached it for a minute. A faulty or hostile RPC could show one crafted settlement under any link. The answer is now used only when its envelope’s network hash, or its inner hash for a fee bump, equals the requested hash; otherwise the lookup answers explorer_lookup_unavailable.
Fixed in 50e7461. Tests
apps/explorer/test/live-lookup.test.ts: refuses an RPC answer for another transaction than the hash asked for
F-2026-116
Account totals were cut at 200 rows without notice, in an order anyone could game Low · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats W2
Account totals were ordered by side and contract id and cut at 200 rows, and received assets sorted before paid ones, so payments in many throwaway tokens could push an account’s paid totals and verified assets off its page and its API answer without any sign. Each side now lists at most 100 assets, verified Stellar assets first and then by payments, and the page and the API say how many more each side has.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: list each side apart, verified assets first, and count what they leave outapps/explorer/test/pages.test.ts: says how many assets the account totals leave out, on the page and in the API
F-2026-117
Every Explorer /health request took a database connection Low · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats W3
The Explorer’s /health is exempt from rate limits and the in-flight cap and pinged the database on every request, so a flood of health checks queued pool acquisitions ahead of page queries. All health requests in any two seconds now share one database check.
Fixed in 50e7461. Tests
apps/explorer/test/api.test.ts: asks the database at most once per interval for /health, however many clients ask
F-2026-118
The concluded testnet beta’s contracts kept counting new calls as beta history Low · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I3
The registry listed the testnet beta’s settlement contracts with open ranges. The contracts cannot be changed and anyone can call them, so every later call was labelled concluded beta history and anyone could inflate it. Each beta contract’s range now ends at testnet ledger 5,326,244, the last ledger closed before the beta’s pubnet account was merged, with archived evidence of both; calls after it are not beta history.
Fixed in 1530c9d. Tests
packages/x402-index/test/registry.test.ts: closes every concluded beta contract’s range, so later calls are not beta history
F-2026-119
No launch or rotation step registered the hosted service’s accounts with the Explorer Low · explorer · Fixed · found byREV-2026-10-explorer on 2026-10-23, resolved 2026-10-23 · threats I3
Neither the mainnet key ceremony nor the sponsor rotation said to add the hosted service’s new accounts to the Explorer’s registry, so its own settlements would have shown as unattributed until someone noticed. Both procedures and the runbook now include the registry change, and registry-check —hosted-<network> fails while any signer or settlement authority the service advertises in /supported lacks an open registry entry.
Fixed in 50e7461. Tests
apps/explorer/test/registry-check.test.ts: names an advertised account the registry does not list, or lists only in a closed rangeapps/explorer/test/registry-check.test.ts: passes when every advertised signer and the upto authority have open rail402 entries
F-2026-120
Pre-flight configuration checks refused every release that adds a migration on pubnet High · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats N8
With pubnet, the configuration check a deploy runs before touching the database refused any pending migration, so a release with a migration could not be deployed in the documented order. `CHECK_CONFIG_PENDING_OK=true` now skips only the pending-migration refusals while still checking the database role, and the migrations then run as the schema owner.
Fixed in 50e7461. Tests
apps/rail402/test/database-roles.integration.test.ts: lets the preflight expect pending migrations, but never skips the role check
F-2026-121
The Explorer and hosted MCP images were outside the supply-chain controls Medium · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats C1, C5
The Explorer and hosted MCP Dockerfiles used a base image by tag, were not built in CI and were not released or attested, so the deploy gate could not cover them. Both are now pinned by digest, built in CI and released as attested images, and the gate can require them.
Fixed in 643f084. Tests
tools/conformance/test/workflows.test.ts: builds every deployable image in CI and publishes each with its own attestation
F-2026-122
Monitoring never scraped the Explorer or the hosted MCP endpoint Medium · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats O11, I6
No scrape job covered the Explorer or the hosted MCP endpoint, so the Explorer lag alert could never fire and a stopped indexer went unnoticed. Scrape jobs and alerts for indexers, the web role and the endpoint were added.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: scrapes the Explorer and the hosted MCP endpoint, and alerts when they stop
F-2026-123
The leak response did not use the cold key to lock out the leaked key Medium · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats K2, O6
The rotation steps had the operator race an attacker for the hot balance and moved funds last, while the cold key can lock the hot key out in one transaction. A cold-key freeze (master weight 0) is now the first step of the leak response. Its source and fee payer is the cold account and only the operation names the sponsor, so the holder of the leaked seed cannot make it fail by using or bumping the sponsor’s sequence (a first version took the sponsor as the source and could be blocked that way); the sweep is a separate cold-signed transaction after it. The testnet rehearsal and the recovery drill bump the sponsor’s sequence with the leaked key before the freeze is submitted, and the freeze still applies.
Fixed in ebeea7a. Tests
tools/conformance/test/ceremony.test.ts: freezes from the cold account, so the leaked key cannot block it through the sponsor’s sequencetools/conformance/test/ceremony.test.ts: sweeps in a separate cold-sourced transaction after the freeze
F-2026-124
The deploy gate accepted an OpenPGP signature the operator’s keyring trusted Low · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats C2
git picks the verifier from the signature block, so an OpenPGP-signed tag was verified against the operator’s keyring, where a key of unknown trust passes. The gate now verifies only SSH signatures, with ssh-keygen and the committed signers file.
Fixed in 643f084. Tests
tools/conformance/test/check-deployable.test.ts: refuses an OpenPGP-signed tag even when the operator’s keyring trusts its key
F-2026-125
No alert fired when no backup existed at all Medium · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats K5
The stale-backup alert compared the newest backup’s age and returned nothing when no backup was listed, so a missing base backup or an unreadable repository never paged. It now also pages when archiving has been on for six hours and no completed backup is listed.
Fixed in 32bb685. Tests
tools/conformance/test/monitoring.test.ts: watches WAL archiving, backups and host disk space, and pages on each
F-2026-126
The sponsor signer setup needed the hot seed on a networked machine Low · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats K7
The one-time signer setup was signed by a tool that needs the full dependency tree and network access, with the hot seed on that machine. The setup can now be built unsigned and signed offline by the zero-dependency key tool.
Fixed in 32bb685. Tests
tools/conformance/test/ceremony.test.ts: signs an unsigned envelope without the SDK exactly as the SDK does
F-2026-127
The release gate did not name the deploy files kept outside the release Low · operations · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats C3
Files a deployment keeps outside the release, including a pubnet network’s fee and budget settings, were not identified by the gate. It now prints their digest for the host to check and record with the release; reviewing those files remains the operator’s step.
Fixed in 643f084. Tests
tools/conformance/test/check-deployable.test.ts: digests a deploy kit the same way a shell on the host does
F-2026-128
A startup RPC failure that threw a non-error stopped the service Low · facilitator · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats N4
With one network, an RPC that answered a startup check with a JSON-RPC error made the process exit with an unreadable message instead of holding that network until the RPC recovers. Any failure that is not a contradiction with the configuration now holds only that network with a code, and every startup failure prints one coded line without URLs or secret seeds.
Fixed in 3549435. Tests
apps/rail402/test/main.integration.test.ts: starts with its network held, not exiting, when the RPC makes startup throw %sapps/rail402/test/startup-failures.integration.test.ts: holds pubnet only when an RPC call itself throws %sapps/rail402/test/failure.test.ts: never carries a URL or a secret seed, and stays one bounded line
F-2026-129
An RPC’s own simulation failure refused a valid payment as invalid Low · facilitator · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats O3
When the RPC failed a simulation without running the host, for example with an incomplete ledger snapshot under load, verify and settle refused the payment with a non-retryable simulation code although nothing about it had been checked, so buyers treated a valid signed payment as refused. Such failures now answer a retryable code saying the payment was not judged; nothing is signed or submitted, and the same payment settles once the RPC recovers.
Fixed in 9355cd2. Tests
packages/facilitator/test/settlement.integration.test.ts: answers a retryable code when the RPC fails a simulation without running it, and settles on retrypackages/facilitator/test/upto.integration.test.ts: answers a retryable code when the RPC fails a simulation without running it, and settles on retrypackages/facilitator/test/explain.test.ts: tells a host error from an RPC’s own failure
F-2026-130
Verification could wait minutes for an RPC that stopped answering Low · facilitator · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats O2
The upstream exact verifier builds its own RPC client from the URL alone, without the 10-second call timeout of the service’s own clients, so /verify waited for the platform’s fetch default (minutes) when a provider accepted the connection and never answered; settlement was already bounded by its claim window. Upstream verification is now cut off after the RPC timeout with a retryable unexpected_verify_error, the channel and authority commands use bounded clients, and the threat model no longer claims that every RPC call fails after 10 seconds.
Fixed in ebeea7a. Tests
packages/facilitator/test/verify-timeout.integration.test.ts: answers unexpected_verify_error within the RPC timeout, and signs nothing
F-2026-131
The rebuild guide claimed an anchored hash no test reproduced Low · explorer · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats I4
The Explorer rebuild guide said that —anchors-from with a published manifest gives the published manifest hash, but no test rebuilt an anchored report. A test now indexes the committed slice with anchors, reads the anchors back from the report’s manifest the way —anchors-from does, and rebuilds to the report’s manifest hash; the parser is shared with the command.
Fixed in 50e7461. Tests
apps/explorer/test/indexer.integration.test.ts: commits a ledger only after its hash matched the anchor, stops on another hash and waits while it is unavailable
F-2026-132
Startup failures that were not RPC failures were reported as an unreachable RPC Low · facilitator · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23 · threats N4
Every plain error from a network’s startup checks became the hold reason rpc_unreachable, so a sponsor that does not exist or another replica still provisioning accounts was refused as “its RPC cannot be reached”. A missing sponsor now holds the network as sponsor_missing, a refused channel creation as channels_missing and provisioning contention as the new provisioning_busy; only RPC failures stay rpc_unreachable.
Fixed in 3549435. Tests
apps/rail402/test/startup-hold.test.ts: names another replica’s provisioning as its own reason, not an unreachable RPCapps/rail402/test/startup-hold.test.ts: calls only an RPC failure unreachable: an RPC error, a hang, or something that is not an Errorapps/rail402/test/startup-failures.integration.test.ts: holds a network whose sponsor does not exist as sponsor_missing, not as an unreachable RPC
F-2026-133
The docs pinned a package release without the pubnet buyer guards they describe Info · sdk · Fixed · found byREV-2026-10-internal-pubnet on 2026-10-23, resolved 2026-10-23
Every package was still at 0.4.2 while the agent, seller and in-process pages, the Playground snippets and the buyer example installed @0.4.2 and described pubnet guards (tagged keys, per-recipient approvals, pinned upto authorities, the persistent budget store) that 0.4.2 does not have. Every package and the workspace are now 0.5.0 and those install pins name 0.5.0, and a test keeps every @rail402.dev install pin in the docs at the workspace version. The release is pending: until 0.5.0 is published to npm these pins do not install, so publish before the docs go live; the examples’ package.json files keep the published 0.4.2 so their registry install check passes, and move to 0.5.0 after the publish.
Fixed in ebeea7a. Tests
tools/conformance/test/release-pins.test.ts: pins every @rail402.dev install in the docs, READMEs and Playground snippets to the workspace version
F-2026-134
Discovery beyond capacity made searches fail with 500 and stalled health checks in the shared process Medium · search · Fixed · found byREV-2026-10-discovery on 2026-10-23, resolved 2026-10-24 · threats D5
In the load test, uncached searches at 300 a second over a 5,000-listing catalog (above the 234 a second the service completes) piled up inside the process: 6,245 searches answered 500 after waiting more than DATABASE_ACQUIRE_TIMEOUT_MS for a connection of the pool settlements also use (every result page read its listings from the database), 7,972 timed out at 30 s, and 150 of 600 /health probes timed out. Discovery now sheds load before any limit or database read (at most DISCOVERY_MAX_IN_FLIGHT requests run and DISCOVERY_QUEUE_LENGTH wait, each at most DISCOVERY_QUEUE_TIMEOUT_MS; the rest are refused with 503 discovery_overloaded and Retry-After), reads the catalog and keeps its limits through a database pool of its own, answers a busy or unreachable catalog database with a coded 503 instead of a 500, and reuses a page’s listings read within the one-second revision read at the same revision; /ready answers from its last database probe while a new one runs. Measured again on the release image at 300 a second (tools/loadtest/published/2026-10-26): 33,802 searches answered and 2,198 refused with 503 beside a /health probe (run B-uncached-open-300), 33,956 answered and 2,044 refused beside the settle probe (run B-settle-under-uncached-open-300), with no 500 or timeout; every /health probe and every settlement succeeded.
Fixed in 3549435. Tests
apps/rail402/test/discovery-overload.test.ts: refuses discovery beyond its slots and queue with 503 while health and settlement routes answerapps/rail402/test/discovery-overload.test.ts: refuse with a coded 503 and Retry-After, never a 500apps/rail402/test/discovery-overload.integration.test.ts: sheds a discovery flood with coded 503s while settlement, /health and /ready answerapps/rail402/test/discovery-overload.integration.test.ts: refuses discovery with a code while its pool is held, and settles through the other poolspackages/search/test/search.test.ts: reads a page’s listings once per revisionMaxAgeMs at one revision, never past a withdrawal
F-2026-135
Any page silenced every ticket of its network, hiding an independent failure Medium · operations · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats O11
The Alertmanager inhibition let any page that named a network suppress every ticket of that network. In the channel-exhaustion drill, Rail402SponsorGuardRefusing was paging for testnet, so the Rail402ChannelLeaseTimeouts ticket raised by the exhausted channels was never sent. A page now silences only the tickets of its own cause on its own network: alerts carry a cause label (deploy/monitoring/alerts.ts), the rule matches equal: [network, cause], Rail402ChannelsExhausted covers Rail402ChannelLeaseTimeouts and Rail402RpcStale covers Rail402RpcFailover; every other alert is always sent.
Fixed in 32bb685. Tests
tools/conformance/test/alertmanager.integration.test.ts: lets a page silence only the tickets of its own cause on its own networktools/conformance/test/monitoring.test.ts: lets a page silence only the tickets of its own cause, on its own networkdeploy/monitoring/alerts.test.yml: Rail402ChannelLeaseTimeouts
F-2026-136
A sequence conflict held its channel until the payment’s authorization expired Medium · facilitator · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats O2
When another transaction took a channel account’s sequence number, the settlement’s txBadSeq refusal was treated as transient (the same bytes might have applied), so the settlement and its channel lease waited for the transaction’s time bound and the payer’s authorization expiry: about 4.5 minutes in the drill, with Rail402SequenceConflict firing correctly. The engine now proves the conflict final, from the channel’s sequence number on the network (at least the transaction’s) and the absence of the transaction’s hash and inner hash at an RPC that has ingested that ledger and retains every ledger since the claim, and then finishes the settlement at once as a submission failure with nothing charged, unless the payer’s nonce shows the authorization was carried elsewhere. The channel is quarantined (SETTLE_CHANNEL_QUARANTINE_MS, doubled per conflict in a row, up to 16 times) instead of being reused at once. The settlement is never re-signed on another channel, so the recorded-bytes idempotency stays as it was. An exact settlement whose bytes the network had accepted before the conflict still waits for the authorization to expire, since another transaction may carry it. For an exact settlement the proven conflict now quarantines and gives back the channel at once, while the record stays open until the payer’s authorization expired unused or a copy of it landed, so a payment carried by another transaction is still answered; upto settlements still finish at once.
Fixed in 9355cd2. Tests
packages/facilitator/test/sequence-conflict.integration.test.ts: gives its channel back within seconds, moves nothing, never settles twice and fails once the authorization expiredpackages/facilitator/test/first-submission.test.ts: quarantines the channel at once instead of holding it, and finishes with nothing charged once the authorization expiredpackages/facilitator/test/first-submission.test.ts: gives its channel back at a proven sequence conflict, and answers a later copypackages/facilitator/test/first-submission.test.ts: finishes a sequence conflict as failed once the authorization expired unused, releasing nothing twicepackages/store-postgres/test/stores.integration.test.ts: lease a channel again once its open settlement gave it backpackages/facilitator/src/testing.ts: lets a submitted settlement give its channel back once, and keeps that when it finishespackages/facilitator/test/first-submission.test.ts: stays open while the channel’s sequence number has not reached the transaction’spackages/facilitator/test/first-submission.test.ts: never finishes as failed when the transaction landed inside another fee bumppackages/facilitator/test/upto-engine.test.ts: finish an upto settlement at once and quarantine the channel, since only the authority could settle itpackages/facilitator/src/testing.ts: doubles the cooldown of a channel quarantined again, until a plain release
F-2026-137
The sponsor balance alerts used fixed thresholds instead of each network’s floor Low · operations · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats S5
Rail402SponsorBalanceLow watched only pubnet, and the testnet sponsor had a separate ticket, both against fixed thresholds in the alert file rather than the configured readiness floors, so a testnet sponsor drill could not fire the sponsor alert and a changed floor silently left the alerts behind. The service now exports each network’s floor (rail402_sponsor_balance_floor_stroops) and one alert fires on every network below twice its own floor: a page for the key custodian, and on testnet, whose XLM has no value, a ticket for the on-call.
Fixed in 3549435. Tests
tools/conformance/test/monitoring.test.ts: follows each network on its own: per-network readiness, per-network sponsor thresholdsdeploy/monitoring/alerts.test.yml: Rail402SponsorBalanceLow
F-2026-138
The Explorer lag alert could not fire without a lag series Low · operations · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats O11, I6
Rail402ExplorerLag compared rail402_explorer_lag_ledgers with a threshold, so an indexer that exported no lag, or whose walked-through ledger stopped moving, never raised it. It now also fires when an indexer answers its scrape without rail402_explorer_last_ledger or when that ledger has not moved for an hour; a stopped indexer stays reported by Rail402ExplorerIndexerDown alone, not twice.
Fixed in 32bb685. Tests
deploy/monitoring/alerts.test.yml: Rail402ExplorerLag
F-2026-139
The stale-RPC alert stayed silent on a replica that never read a ledger, or after its polls failed Medium · operations · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats O11
rail402_rpc_ledger_age_seconds was set only by a successful poll: a replica that never read a ledger had no series, and one whose polls started failing kept its last, fresh value, so Rail402RpcStale could not fire for either. The age is now computed at each scrape from the last ledger read, and from when the replica started watching the network while none was read, so both cases are reported (a never-read replica 3 minutes after start).
Fixed in 3549435. Tests
apps/rail402/test/metrics-signals.test.ts: grows at every scrape, from the start of watching while no ledger was ever readdeploy/monitoring/alerts.test.yml: Rail402RpcStale
F-2026-140
No alert covered discovery load shedding Low · operations · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats D5
Discovery refuses requests beyond its capacity with 503 discovery_overloaded and counts them in rail402_discovery_shed_total, but no alert read the counter, so sustained refusals went unnoticed. Rail402DiscoveryShedding raises a ticket when one reason passes 20 refusals in 10 minutes, with a runbook section.
Fixed in ebeea7a. Tests
deploy/monitoring/alerts.test.yml: Rail402DiscoveryShedding
F-2026-141
The CLI reported retryable facilitator refusals as final, and a pending settlement as not settled Medium · cli · Fixed · found byREV-2026-10-recovery-drills on 2026-10-24, resolved 2026-10-24 · threats M4
A payment the facilitator refused was always printed as payment_rejected with retryable false, even for settle_exact_stellar_channel_unavailable, rate limits or a spent budget, which a new payment can pass; and a refusal with settlement_pending reported paid: not_settled although that transaction may still land, inviting a second payment. The CLI now takes retryable from the facilitator’s code table (exact and upto) for the code the seller passed on, and reports settlement_pending as paid: unknown with exit 5; an unknown code changes nothing.
Fixed in b08b49f. Tests
packages/cli/test/basics.test.ts: says whether running the command again, with a new payment, can succeed
F-2026-142
A sequence conflict first answered TRY_AGAIN_LATER still held its channel until the authorization expired Medium · facilitator · Fixed · found byREV-2026-10-recovery-drills on 2026-10-26, resolved 2026-10-26 · threats O2
The re-run of the sequence-conflict drill on the hosted testnet showed the fix of F-2026-136 did not trigger in the realistic order: the other transaction held the channel’s queue slot, so the scheme’s send and the engine’s immediate resend answered TRY_AGAIN_LATER rather than txBadSeq; the settlement then counted its bytes as possibly public, and the 23 later txBadSeq rebroadcasts all waited for the exact authorization’s expiry (317 s, channel released at expiry, not quarantined; nothing charged). The answer to every send of the recorded bytes is now persisted on the settlement (broadcast_accepted and sends_in_flight, migration 0024): a send is marked in flight before it is made, PENDING or DUPLICATE marks the bytes accepted, ERROR or TRY_AGAIN_LATER ends it as refused, and a lost answer (timeout, transport error, crash) stays in flight and counts as accepted. When the sequence conflict is proven and no send was ever accepted, the settlement finishes at once with nothing charged and its channel quarantined, on whichever replica or reconciler sees the txBadSeq; records written before the migration count as accepted. A declined early finish is logged with its reason. For exact settlements “finishes” now means the channel is quarantined and given back at once, with the record left open until the authorization expired unused or a copy landed; bytes whose channel was given back are never sent again.
Fixed in 9355cd2. Tests
packages/facilitator/test/sequence-conflict.integration.test.ts: gives its channel back the same way when the channel’s queue slot was taken first (TRY_AGAIN_LATER)packages/facilitator/test/first-submission.test.ts: quarantines the channel at the first txBadSeq when no send was ever acceptedpackages/facilitator/test/first-submission.test.ts: sends nothing more once it gave its channel back, while the same request still waitspackages/facilitator/test/first-submission.test.ts: keeps waiting when a send’s answer was lost: it may have been acceptedpackages/facilitator/test/first-submission.test.ts: keeps waiting when the scheme’s own send was lostpackages/facilitator/test/first-submission.test.ts: keeps waiting when a send was answered DUPLICATE: the network held the bytespackages/facilitator/test/first-submission.test.ts: is finished by another replica’s reconciler from the recorded answerspackages/facilitator/test/first-submission.test.ts: is left waiting by another replica’s reconciler when a send’s answer was lostpackages/facilitator/src/testing.ts: tracks the sends of recorded bytes: public once one is accepted or its answer is lost
F-2026-143
A waiting sequence conflict logged its decline on every rebroadcast Low · facilitator · Fixed · found byREV-2026-10-recovery-drills on 2026-10-26, resolved 2026-10-26 · threats O2
While a settlement whose channel sequence number was taken kept waiting (its bytes may have been public), every rebroadcast logged the same decline, on each replica: about 60 lines per replica in a 5-minute wait, which buried other log lines and gave no count to alert or graph on. A decline is now logged once per settlement and replica, and again only when its reason changes, and each decision is counted in rail402_sequence_conflict_outcomes_total{network, outcome, reason} with a fixed set of reasons (private on pubnet, like the neighbouring sequence-conflict counter).
Fixed in 9355cd2. Tests
packages/facilitator/test/first-submission.test.ts: reports a decline once per settlement, however many rebroadcasts meet itpackages/facilitator/test/first-submission.test.ts: reports a decline again when its reason changespackages/facilitator/test/first-submission.test.ts: reports a settlement finished at the conflict, with its reasonapps/rail402/test/metrics-signals.test.ts: exports every outcome and reason at zero, and counts what the engine reports
F-2026-144
The CLI’s retryable flag used the facilitator’s meaning, not the command’s Medium · cli · Fixed · found byREV-2026-10-recovery-drills on 2026-10-26, resolved 2026-10-26 · threats M4
The CLI copied the facilitator’s retryable flag for a refused payment, but the facilitator’s flag means that resending the same signed payment can succeed, while running a CLI command again signs a new payment. So settle_*_settlement_in_progress was reported as retryable with paid: not_settled although another request was still settling the same authorization, which a second run could have paid twice; and settle_*_transaction_submission_failed was reported as not retryable although its reason says to sign a new payment and retry. The CLI now says whether running the command again can succeed: settlement_pending, settlement_in_progress and idempotency_conflict report paid: unknown (exit 5), submission failures and expiries with no funds moved are retryable, and the error-code reference and the CLI page define retryable for client codes this way.
Fixed in ebeea7a. Tests
packages/cli/test/basics.test.ts: says whether running the command again, with a new payment, can succeed
F-2026-145
A quarantined channel returned to rotation while an outside sender still used it Low · facilitator · Fixed · found byREV-2026-10-recovery-drills on 2026-10-26, resolved 2026-10-26 · threats O2
In the hosted re-run the third sequence conflict on one channel came one second after its quarantine ended: the quarantine only waited, so a channel someone kept using was handed back on time and one more payment failed. The account’s sequence number seen at the conflict is now kept with the quarantine (memory and Postgres pools, migration 0025), and the next lease compares it with the account’s current one; when it moved, the channel is quarantined again for twice as long and another channel is leased instead. A failed read leases the channel, where another conflict would quarantine it again.
Fixed in ebeea7a. Tests
packages/facilitator/test/first-submission.test.ts: is quarantined again, for twice as long, when its sequence number moved sincepackages/facilitator/test/first-submission.test.ts: serves again once its sequence number stayed where the conflict left itpackages/facilitator/src/testing.ts: hands a quarantined channel’s sequence number to its next lease, once
F-2026-146
The SDK and the MCP server reported a refused payment’s retryability apart from the CLI Medium · sdk · Fixed · found byREV-2026-10-recovery-drills on 2026-10-26, resolved 2026-10-26 · threats M4
Only the CLI read the facilitator’s code a seller passed on with a refused payment. The SDK’s PaymentError kept retryable false and paid: not_settled for every payment_rejected, so an SDK caller saw settlement_pending as nothing paid, and the rail402-mcp tool error took retryable from the code set instead of the error, so an agent never saw a busy channel as retryable. The CLI also called a seller-relayed transaction_submission_failed retryable with nothing moved, although upstream facilitators send that code for a DUPLICATE submission whose bytes can still land. One table in the SDK (refusalOutcomes) now sets retryable and paid on the PaymentError itself, and rail402-mcp and the CLI report the error’s values: settlement_pending, settlement_in_progress, idempotency_conflict, transaction_submission_failed, settle_exact_stellar_transaction_failed and settle_upto_stellar_settled_elsewhere report paid: unknown and are not retryable, and the seller’s code never makes a refusal more retryable than the facilitator’s table. rail402-mcp’s JSON-RPC errors now carry retryable and paid: none as well.
Fixed in b08b49f. Tests
packages/sdk/test/refusal.test.ts: applies the table to every code it holds, and never invites a second paymentpackages/sdk/test/refusal.test.ts: never reads the seller’s code as more retryable than the facilitator’s tablepackages/mcp/test/errors.test.ts: follows the SDK’s table for every facilitator codepackages/cli/test/basics.test.ts: never tells a seller-relayed submission failure that nothing movedtools/conformance/test/retry-agreement.test.ts: agrees on a refused payment for every registered code, and none
F-2026-147
The Explorer’s Bazaar calls shared one rate bucket for every visitor High · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats Q6, W3
Every Bazaar call the Explorer made for its visitors was charged to the Explorer’s own address, so one visitor paging quickly spent the discovery budget of all of them and the pages of everyone else lost their listings and paid-for links. The Explorer now calls with a discovery-internal access key read from a file and sends a per-visitor HMAC identifier (X-Rail402-Client), never the visitor’s address; the facilitator charges each forwarded visitor its own bucket and the key its own.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: sends the access key and a per-visitor identifier, never the visitor’s addressapps/explorer/test/explorer-fixes.test.ts: forwards the page’s visitor from the app, and sends no key without oneapps/explorer/test/explorer-fixes.test.ts: reads the key from a file only, refuses an inline key and a key of another shape
F-2026-148
A payment page was cached as final before its Bazaar link was known Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats W5
A payment page was cached for an hour as soon as the payment was indexed, even when the Bazaar had not linked it yet because cataloging was still running, so its paid-for link stayed missing for an hour. A payment page is now cached as final only once its link is known, or once the Bazaar answered for a payment older than 10 minutes; otherwise for 15 seconds.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: caches a payment page as final only once its link is known or the grace period is over
F-2026-149
Another issuer’s token named USDC was shown and counted as USDC Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats W2
Asset codes were shown by their code alone, so a token any issuer named USDC was labelled and summed as USDC. Only allow-listed contracts are shown by a bare code; any other token shows its code with its issuer and is never counted as USDC.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: shows a bare code only for an allow-listed contractapps/explorer/test/explorer-fixes.test.ts: never counts or names another issuer’s USDC as USDC
F-2026-150
Pending and withdrawn listings had full resource pages with a way to pay them Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats P9, W2
A listing the Bazaar had not published (pending its origin check, or withdrawn) was rendered like a published one, with a price, a seller and a call snippet, although its metadata may have been echoed by a payer and never confirmed. Such a listing now shows only its state and history: no price, no seller and no way to call it.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: shows a pending listing’s state and history only: no price, no seller, no way to call it
F-2026-151
A payment’s paid-for link was not checked against the account it paid Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats W2
The Explorer showed the Bazaar’s paid-for link on any payment the Bazaar named, even when the listing’s payTo was not the account the payment paid on chain. A link is now shown only when the listing’s payTo, or the base account of a muxed payTo, is the account the payment paid.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: shows a link only when the listing’s payTo is the account the payment paidapps/explorer/test/explorer-fixes.test.ts: treats a muxed payTo as its base account
F-2026-152
Listing pages paged by what the Explorer kept, skipping listings Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats W3
The services pages computed their next offset from the listings the Explorer kept after filtering, not from what the Bazaar sent, so listings were skipped or repeated across pages, and version history was read in pages too small for long histories. Pages now follow the Bazaar’s own pagination, version history is read 25 at a time, and crawlers are kept off paginated pages and the CSV.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: pages listings by what the Bazaar sent, not by what it keptapps/explorer/test/explorer-fixes.test.ts: asks for 25 versions and keeps crawlers off paginated pages and the CSV
F-2026-153
A slow Bazaar held Explorer pages for several sequential timeouts Medium · explorer · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats W3, W4
Each Bazaar call of a page had its own timeout and the calls ran one after another, so a hanging Bazaar held an account page or the CSV for many seconds. All Bazaar calls of one page now share one 3-second deadline; when it runs out the page renders from the ledger alone and says so, and a call cut short is never kept for the next visitor.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: gives up at the page’s deadline, and never keeps that failure for the next visitorapps/explorer/test/explorer-fixes.test.ts: renders an account page and the CSV within the deadline while the Bazaar hangs
F-2026-154
A listing with private usage answered as if it did not exist Low · bazaar · Fixed · found byREV-2026-10-explorer-v2 on 2026-10-26, resolved 2026-10-26 · threats P16
The listing usage route answered a listing whose seller keeps its usage private with discovery_listing_not_found, so a client could not tell a private listing from a missing one. It now answers 404 with its own code, discovery_usage_private, documented with the other discovery codes.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery-usage.test.ts: answers 404 with its own code for a listing whose seller keeps its usage private
F-2026-155
The first payer of an mcp:// tool decided its private-usage opt-out for good Medium · bazaar · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats P15
The private-usage opt-out was read from the payload a buyer echoes. A tool under an mcp:// URL has no origin to confirm it, so the first payer’s echo decided it for good: leaving the key out published every later payer of a seller that opted out, and adding it hid a seller’s usage. Usage is now never published for a listing no origin confirmed (trust settled), applied at read time so the listings cataloged earlier are covered without changing their data, and the opt-out ratchets one way: a settlement that declares it makes the usage private at once and asks the origin to confirm; only the origin’s own 402 can remove it.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery-usage.test.ts: stays private for an mcp:// tool whatever its first payer echoed, and links no settlementapps/rail402/test/discovery-usage.test.ts: keeps a declared opt-out once any settlement declared it, whatever later payers echopackages/bazaar/src/testing.ts: makes a listing’s usage private when a payload declares it, until its origin says otherwise
F-2026-156
Testnet discovery load could shed pubnet discovery Medium · bazaar · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats N11, D5
One admission gate, one discovery database pool and one limiter served the discovery routes of both networks, ahead of the per-address limit, so enough parallel slow testnet requests made every pubnet discovery request fail with discovery_overloaded. Discovery now admits in two lanes, pubnet-scoped requests and everything else, each with its own gate; with pubnet served next to testnet the pubnet lane reads the catalog and keeps its limits through a discovery pool of its own.
Fixed in 3549435. Tests
apps/rail402/test/discovery-overload.test.ts: keeps admitting pubnet discovery while testnet discovery fills every slot of its laneapps/rail402/test/discovery-overload.test.ts: reads a pubnet request through the pubnet lane’s store while the shared one is busyapps/rail402/test/pubnet-pool.integration.test.ts: serves pubnet-scoped discovery through its own pool while the shared discovery pool is saturated
F-2026-157
The usage routes published each pubnet payer’s purchases by resource Low · bazaar · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats P16
The usage routes linked settlements, and with them payer addresses, to the listings they paid for on every network, and the Explorer labelled each payer’s payments with what they bought. On chain a payment names only payer, payTo and amount. Each network now publishes either settlement links or counts only (<NET>_BAZAAR_USAGE_DETAIL); pubnet defaults to counts, so no route links a pubnet payer or transaction to a listing, and the Explorer labels no pubnet payment.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery-usage.test.ts: with counts, links no settlement to a listing and lists no payer, but still countsapps/rail402/test/discovery-config.test.ts: publishes usage counts only on pubnet by default, each network settable on its ownapps/explorer/test/explorer-fixes.test.ts: says a network publishes payment counts only, and links no payment to the listing then
F-2026-158
A private-usage listing still named its settlements in its history and detail Low · bazaar · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats P16
rail402-private-usage hid usage on the usage routes only: the listing’s version history still named the transaction that created or re-listed it, and its detail showed when it was last paid. While a listing’s usage is private, or on a network that publishes counts only, its versions name no transaction, and a private listing shows no lastSettled. Its settlement count stays public: published clients require it.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery-usage.test.ts: links no version to a settlement, and dates no settlement, while the usage is privateapps/rail402/test/discovery-usage.test.ts: links no version to a settlement on a network that publishes usage counts only
F-2026-159
One visitor’s refused Bazaar call blanked Bazaar data for every Explorer visitor Low · explorer · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats W7
Bazaar calls are charged to each visitor’s forwarded budget but their answers are cached for everyone, and any answer but 200 or 404 was cached for 5 seconds as the Bazaar being down. A visitor over its own budget could keep every other visitor’s pages without listings. A 429, or a 503 with Retry-After, is now an answer for that visitor only and is never cached.
Fixed in 50e7461. Tests
apps/explorer/test/explorer-fixes.test.ts: never keeps one visitor’s refused budget as the Bazaar being down for the next visitor
F-2026-160
The sponsor balance page fired late on pubnet and a floor of 0 switched it off Low · operations · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats O11
Rail402SponsorBalanceLow fired below twice each network’s readiness floor, so pubnet’s default page threshold fell from 100 to 50 XLM, and a pubnet MIN_SPONSOR_BALANCE_XLM of 0 silenced it. The alert now fires below each network’s floor and, on every network but testnet, below 100 XLM whatever the floor; pubnet refuses a floor below 1 XLM at startup (config_pubnet_sponsor_floor_invalid).
Fixed in ebeea7a. Tests
apps/rail402/test/config.test.ts: refuses a pubnet sponsor floor of 0, which would switch readiness and the balance alert offdeploy/monitoring/alerts.test.yml: Rail402SponsorBalanceLow
F-2026-161
The unused-nonce check of a sequence conflict was not bound to the proof’s ledger Low · facilitator · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats O2
When no send of a settlement’s bytes was ever accepted, a sequence conflict finished the settlement as nothing moved once the payer’s nonce read as unused. That read came from whichever RPC was active, with no check that it had reached the ledger the sequence proof was read at, so a lagging provider or one switched to after a failover could show a nonce another transaction already consumed as unused. The read must now come from an RPC at or past the proof’s ledger; otherwise the settlement keeps waiting (rpc_behind).
Fixed in ebeea7a. Tests
packages/facilitator/test/first-submission.test.ts: keeps waiting when the payer’s nonce is read from an RPC behind the sequence proof
F-2026-162
The status page built HTML from its check history without escaping all of it Low · operations · Fixed · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats O15
The status page wrote a day’s downtime from history/summary.json into a title attribute unescaped, and had no Content-Security-Policy of its own, so whoever could write the check history could run script on the page. Every value is now escaped and a day’s downtime is read as a whole number of minutes; the page carries a policy that allows only its one inline script by SHA-256, the same policy the served header builds from that script.
Fixed in ebeea7a. Tests
tools/conformance/test/status-page.test.ts: writes the check history into the page as text onlytools/conformance/test/status-page.test.ts: allows its one inline script by hash and nothing else
F-2026-163
A quarantine does not check that the lease it clears is still the caller’s Info · facilitator · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats O2
The Postgres channel pool’s quarantine clears any lease on the channel. If the settlement’s own lease expired and another settlement leased the channel between the settlement’s final transition and the quarantine, the newer lease is cleared.
Tests
packages/store-postgres/test/stores.integration.test.ts: never lease a channel whose recorded settlement is still open, released or expired
- A channel whose recorded settlement is still open is never leased again, released or expired.
- A cleared lease leaves the channel quarantined, so no other settlement leases it until the quarantine ends.
F-2026-164
A seller’s usage aggregates are heavy for their cost Info · bazaar · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats D5, N11
GET /discovery/sellers/{payTo}/usage costs 2 units and runs up to two count(DISTINCT) aggregates over all of a seller’s settlements, each up to the 5-second statement timeout, on the small discovery pool.
Tests
apps/rail402/test/discovery-usage.test.ts: charges a settlements lookup by its size and a seller’s usage double, reporting refusals by route
- Per-client cost budget, the seller route charged double.
- 5-second statement timeout on discovery reads.
- Admission lanes and a pubnet discovery pool.
F-2026-165
The payee backfill of migration 0026 runs as one update and can name a later owner Info · bazaar · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats P16
Migration 0026 fills the payee of earlier settlement observations in one UPDATE inside the migration’s transaction, and a backfilled settlement another payTo made while claiming a listing is attributed to the listing’s owner of the time.
Tests
packages/bazaar/src/testing.ts: counts only the settlements that paid a listing’s owner, never another payTo’s claim
- The Explorer shows a paid-for link only when the listing’s payTo is the account the payment paid on chain.
- The migration states the limit of the backfill.
F-2026-166
The Explorer’s live holdings budget is shared by every visitor Info · explorer · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats W4
Live holdings reads are bounded by one budget of 30 calls a minute for all visitors, so one visitor can use it up and the card then shows no live balances for everyone until it refills.
Tests
apps/explorer/test/explorer-pages.test.ts: shows them labelled live and fails soft, within a call budget
- A global call budget per network that fails soft.
- Indexed pages never depend on live holdings.
F-2026-167
The status page shows sample figures off its own host Info · operations · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26 · threats O13
Served from any host but status.rail402.dev, such as a repository preview URL, the status page renders generated sample uptime under a preview banner instead of the recorded history.
Tests
tools/conformance/test/status-page.test.ts: writes the check history into the page as text only
- The published host never renders the sample.
- A preview banner is shown whenever the sample is.
F-2026-168
New licences after the audit candidate Info · docs · Accepted risk · found byREV-2026-10-postfreeze on 2026-10-26, resolved 2026-10-26
The changes after the audit candidate add no third-party npm dependency. The Explorer bundles three fonts under the SIL Open Font License 1.1 with their licence texts, and the status checks run on Upptime (MIT).
Tests
tools/license-gate/test/policy.test.ts: rejects copyleft everywhere
- The licence gate refuses copyleft in every dependency path.
- Font licence texts ship with the fonts.
F-2026-169
A tool listed from a payload without example arguments could never be confirmed again Low · bazaar · Fixed · found byREV-2026-10-rehearsal on 2026-10-27, resolved 2026-10-27 · threats P9
An MCP tool’s origin check called the tool with the example arguments stored on its listing. A listing first cataloged from a payload without an example, by its seller before it declared one or by any payer echoing the tool, was called without arguments; a tool that validates arguments before asking for payment answered with a result, and the listing was quarantined. Every later check, including those its owner’s genuine settlements requested, made the same call, so the seller’s tool stayed withdrawn and its later quarantines recorded nothing. A check the owner’s settlement requests now also tries the call that settlement’s payload declares, and uses only a PaymentRequired to it; a quarantined listing whose check fails for another reason records the new reason, and the store refuses a quarantine version without a reason.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: lists a quarantined tool again when its owner’s settlement proposes the call its server answerspackages/bazaar/src/testing.ts: never lets a payload’s proposed call fail the check of a tool that answers its listed callpackages/bazaar/src/testing.ts: records a new reason when a quarantined listing’s check fails for another reasonpackages/store-postgres/test/stores.integration.test.ts: keeps quarantines recorded before reasons were and refuses a new one without a reason
F-2026-170
The RPC failover alert never fired for the first switch after a start Low · operations · Fixed · found byREV-2026-10-local-drills on 2026-10-27, resolved 2026-10-27 · threats O3, O11
rail402_rpc_failovers_total got its series for a pair of providers only when the service first switched between them, so the series appeared at 1. Prometheus sees no increase in a series whose first sample is 1, so Rail402RpcFailover (increase over 15 minutes above 0) stayed silent for the first failover of every replica and for its first fail-back; only a second switch between the same providers in the same process fired it. The service now exports the counter at zero for every ordered pair of a network’s providers when it starts, so the first switch is an increase.
Fixed in 3549435. Tests
apps/rail402/test/metrics-signals.test.ts: exports every switch between a network’s RPC providers at zero, so the first failover is an increaseapps/rail402/test/runtime.integration.test.ts: fails over to the next RPC provider and reports the active one
F-2026-171
The facilitator’s HTTP server had no deadline for writing a response Medium · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats N1, D5
The service’s Node server gave a response no time limit to reach its client. A client that stopped reading kept its socket and the whole response, a full discovery page included, for as long as it liked, after the request had already given up its admission slot, in the one process that settles both networks; pipelining let one connection queue many such answers. Every response, pipelined ones and those the server gives without the app (an invalid Host, an unmet Expect) included, must now be handed to the network within RESPONSE_WRITE_TIMEOUT_MS (30 s) of being ready, or the connection is reset and counted. One connection may have at most MAX_PIPELINED_REQUESTS (4) requests waiting for their answers, and a discovery request keeps its slot until its page is sent, at most DISCOVERY_MAX_IN_FLIGHT_PER_CLIENT per client and lane. At shutdown, idle connections and requests still arriving are dropped, and a client gets at most 1 s more to take an answer.
Fixed in ebeea7a. Tests
apps/rail402/test/server.test.ts: resets the connection of a client that stops reading, and releases the response it heldapps/rail402/test/server.test.ts: caps the requests one connection may have waiting for their answers, on every routeapps/rail402/test/server.test.ts: are timed: a connection that does not read them is reset within the deadlineapps/rail402/test/discovery-overload.test.ts: hold their slot until they are sent, so the pages one process keeps for its clients are boundedapps/rail402/test/server.test.ts: drops a request whose body is still arriving when shutdown begins
F-2026-172
Keyless testnet traffic could fail readiness and delay pubnet admission through the shared database pool Medium · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats N3, N4
/ready probed the database through the shared pool, and on the mainnet host every caller’s per-address limit was written through it too. Keyless testnet traffic that held every shared connection therefore failed the aggregate /ready the mainnet host is routed by, and delayed every pubnet /verify and /settle until the pool’s timeout. /ready now probes through one connection of its own, with a timeout. On the pubnet host the key is decided in memory first: a pubnet key holder’s limit is kept through pubnet’s own pool and every other caller’s in the process, so no anonymous request takes a pubnet connection. A client already over a limit is refused from memory until its window ends, /metrics is answered from memory, and at startup the service checks that its database role’s connection limit holds every pool of a process, refusing to serve pubnet below it (config_database_connections_low).
Fixed in 3549435. Tests
apps/rail402/test/pubnet-pool.integration.test.ts: stays ready and admits a pubnet key holder at once while keyless testnet traffic holds the shared poolapps/rail402/test/pubnet-pool.integration.test.ts: gives keyless and refused mainnet-host callers no pubnet connection and no per-address writeapps/rail402/test/public-hosts.test.ts: limits a pubnet key holder through pubnet’s limiter and nobody else there through a shared oneapps/rail402/test/runtime.integration.test.ts: fails the database check within the acquire timeout once its connection stops answeringapps/rail402/test/database-roles.integration.test.ts: refuses pubnet a role whose connection limit cannot hold one process’s pools
F-2026-173
Any party could use up a seller’s new-listing quota with listings its own host confirmed for the seller’s payTo Medium · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats P5, P13
A payTo’s hourly quota of new listings counted every confirmed listing owned by that payTo, on any host. Any host can answer a 402 naming any payTo, so a party paying its own server for listings that named a seller, or claims that handed listings over to the seller, could fill the seller’s quota and keep its real listings out of the catalog. The quota is now kept per origin (scheme, host and port), and a listing counts toward it only once its origin confirmed a settlement that paid the payTo its price there (for upto, the whole cap). An ownership transfer clears the charge, and concurrent checks hold the owner’s quota until their transaction ends, so none charges past it.
Fixed in 2fd3600. Tests
packages/bazaar/src/testing.ts: keeps a payTo’s quota per origin, so listings another host confirms for it never block its ownpackages/bazaar/src/testing.ts: never lets listings moved to a payTo on another host use up its quota on its own hostpackages/bazaar/src/testing.ts: charges a payTo’s quota only for listings a settlement paid its origin’s price forpackages/bazaar/src/testing.ts: never charges a payTo’s quota for listings handed over to it at its own origin
F-2026-174
safe-fetch read the bodies of answers it never returned without a limit Medium · sdk · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats X4, PG1
safe-fetch drained the body of a redirect it refused or followed, and of a 205 answer, with no byte cap and outside the request’s total timeout. A seller answering the Playground relay with a redirect and an endless body kept the socket, and its slot in the relay’s shared pool, for as long as it kept sending, so a few such requests starved every other relay client. A body that is never handed to the caller is now read only when it arrived whole; otherwise the connection is closed and its slot freed at once. A connection is also closed when an answer ends before the upload was sent, or goes on after a compressed body ends, so no server holds a connection past the request’s limits.
Fixed in ebeea7a. Tests
packages/safe-fetch/test/fetch.test.ts: closes a refused redirect instead of reading its body, and frees its connectionpackages/safe-fetch/test/fetch.test.ts: closes a followed redirect’s endless body before the next hoppackages/safe-fetch/test/fetch.test.ts: closes a 205 answer that sends a body anyway, and frees its connectionpackages/safe-fetch/test/fetch.test.ts: closes a refused redirect’s connection and frees its slotpackages/safe-fetch/test/fetch.test.ts: closes the connection when a compressed body ends before the answer
F-2026-175
The pubnet conformance run executed freshly installed npm packages in the process holding the treasury secret Medium · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats CF2
The pubnet live run of the published packages installed them, with every dependency npm resolved, and imported them in the process that read the treasury secret, with the secret file’s path in its environment and install scripts enabled. A compromised dependency could read the treasury seed. The installed packages now run only in a separate process with an allow-listed environment, npm runs with install scripts off, and the payer is a fresh account per run that the treasury process funds on request. On pubnet that process starts in its own user, mount and PID namespaces with the treasury and key paths masked, and the run refuses to start, before anything is spent, where that isolation is unavailable.
Fixed in 8695731. Tests
tools/conformance/test/pubnet-live.test.ts: on pubnet: npm without install scripts or secrets, and the SDK away from the treasurytools/conformance/test/upstream-e2e-env.test.ts: keep only the toolchain variables of this process, never a secret’stools/conformance/test/upstream-e2e-env.test.ts: run no dependency’s install scripttools/pack-check/test/consumer-install.test.ts: runs npm with install scripts off and the allow-listed toolchain environment only
F-2026-176
The demo seller served concurrent copies of one payment Low · sdk · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats R4
In the authorization flow /verify refuses a payment already settled, but copies of one payment that arrive together all pass /verify before any of them settles. The demo seller, the Playground lab sellers and the seller-http example used the facilitator client as it was, so each concurrent copy was served while only one payment moved. Each now wraps its facilitator client in withSettleDedupe, covering the exact routes, the upto /history server and the MCP tools: the first copy is served and the others get seller_settlement_repeat. The seller helpers page and quickstart now wrap the client in both payment flows.
Fixed in ebeea7a. Tests
apps/demo-seller/test/seller.test.ts: serves concurrent copies of one payment once, whether or not the facilitator marks the repeatsapps/demo-seller/test/seller.test.ts: serves concurrent copies of one metered payment onceapps/demo-seller/test/seller.test.ts: serves concurrent copies of one paid tool call once, metered or notapps/playground/test/lab-sellers.test.ts: serves a payment once when copies of it arrive together, and traces the refused copypackages/sdk/test/example-seller-http.test.ts: serves concurrent copies of one payment once
F-2026-177
Anonymous demo seller traffic could use up the facilitator limit its buyers shared Low · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29
Every request with a payment header made the demo seller call the facilitator, and those calls all come from the seller’s one address, which the facilitator limits per address. Anyone could send the demo seller payment headers it would never be paid for until that budget was spent and every real buyer’s payment was refused; with default settings the Playground shared the same address and budget. The demo seller now counts, per client and in total, only the payments it actually asks the facilitator about, below what the facilitator allows one address, and refuses the rest with a 402 rate_limited and Retry-After. It keeps at most 10,000 clients in memory, counts IPv6 addresses per /56, and counts IPv4 written as IPv6 as that IPv4.
Fixed in 3549435. Tests
apps/demo-seller/test/seller.test.ts: counts only the payments it asks the facilitator about, never a request that carries noneapps/demo-seller/test/seller.test.ts: limits each client’s paid requests before they reach the facilitatorapps/demo-seller/test/seller.test.ts: limits paid requests across all clients, however many addresses send themapps/demo-seller/test/seller.test.ts: counts the IPv6 addresses of one /56 as one client, and IPv4 written as IPv6 as that IPv4apps/demo-seller/test/seller.test.ts: keeps a bounded number of clients in memory, forgetting the least recently seen
F-2026-178
A key file replaced while the service started was marked as loaded but never applied Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats Q2
The access keys were built from the records the configuration had parsed, and the key file was then read a second time for its stat and digest. A keys.json replaced between those two reads, for example to revoke a key, was recorded as seen but never parsed, so the revoked key stayed valid until the file changed again, while /health/private reported the new file’s digest. The records, the file’s stat and its digest now come from one read, at start and on every reload. A file that cannot be read or parsed leaves the previous records in force, reports no digest, and is read again on the next reload.
Fixed in 3549435. Tests
apps/rail402/test/access.test.ts: applies a file renamed into place between loading the configuration and startingapps/rail402/test/access.test.ts: reports no digest for a file it could not apply at start, and reports the file on the first reloadapps/rail402/test/access-reload-race.test.ts: applies the good file on the next reload instead of remembering it as seen
F-2026-179
Anonymous traffic from a key holder’s address spent the key holder’s /verify and /settle limit Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats N1, N3
The service-wide limit counted requests per client address. Keyless requests of either network, unknown paths and refused requests from a pubnet key holder’s address or IPv6 /64, such as other clients behind the same NAT, used up the same bucket and got the key holder’s /verify and /settle refused. A /verify or /settle with a usable key is now held to a limit of its own key, sized to what the key may send on all its networks and never below RATE_LIMIT_PER_MINUTE. Every other request is held to its address’s limit, under a prefix no client address can spell as a key’s, and a request for a network that does not admit the key pays its address’s limit as well.
Fixed in ebeea7a. Tests
apps/rail402/test/public-hosts.test.ts: never lets anonymous or other-network traffic from its address or /64 spend a key holder’s limitapps/rail402/test/public-hosts.test.ts: never lets a key carry payments past its address’s limit on a network that does not admit itapps/rail402/test/public-hosts.test.ts: sizes a key’s own limit to every network it may use, never below RATE_LIMIT_PER_MINUTEapps/rail402/test/pubnet-pool.integration.test.ts: admits a pubnet key holder on every host after anonymous testnet traffic spent its address’s limit
F-2026-180
Any payer could withdraw a seller’s confirmed route-template listing by paying at another path Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P1, P2, P9
The origin check a settlement requested asked the path that settlement paid. For a published route-template listing, any payer could pay the seller at another path of the route that answered without a 402, or with other metadata, and the check withdrew or rewrote the seller’s listing. A published listing is now decided by one origin answer only, at its resource or its route template’s example path, and a published MCP tool by its listed call: a settlement at another path, or a call a payer proposed, can neither change nor withdraw it. While a listing is not yet listed, its check keeps the paths the settlements paying its owner proposed, asks them largest payments first within the host’s budget, and lists the listing from an answer that lists it for its owner.
Fixed in 2fd3600. Tests
packages/bazaar/src/testing.ts: asks only a route-template listing’s example path whether it changed or still sellspackages/bazaar/src/testing.ts: never lets a settlement withdraw a route-template listing whose example path is unknownpackages/bazaar/src/testing.ts: never lets a claim’s path change what a published route-template listing sayspackages/bazaar/src/testing.ts: never lets a payment to the seller redirect a claim’s check to a path its payer chosepackages/bazaar/src/testing.ts: change or withdraw a tool whose listed call fails for now
F-2026-181
One-unit payments for mcp:// tools could use up testnet’s hourly listing quota and published room Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P5, P10
mcp:// tools are listed from the payload alone, since no origin can confirm them, and any payer can list one for any payTo with a one-unit payment. They counted toward the network’s hourly new-listing quota and its room for published listings like listings an origin confirmed, so one payer could fill both and keep every seller with an origin out of the catalog. Listings no origin confirms are now counted apart: on testnet they take at most half of the hourly quota and half of the published room, and never use up those of listings an origin confirmed.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: never lets tools no origin can confirm take the room listings an origin confirmed needpackages/bazaar/src/testing.ts: never lets tools no origin can confirm use up the hourly quota of listings an origin confirms
F-2026-182
Hourly new-listing quotas were checked only when a listing was created Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P5, P13
The network’s and a payTo’s hourly quotas of new listings counted listings when they were first cataloged. Listings created at once, or kept pending for hours while their origin did not answer, were all published together once their origin confirmed them, far past the quotas. The quotas are now spent when a listing is first published: a listing confirmed while a quota is spent waits, without spending a retry or asking its origin, until the earliest listing counted leaves the hour (publication_deferred). The network’s count holds a lock until its transaction ends, so concurrent publications stay within the quota.
Fixed in 2fd3600. Tests
packages/bazaar/src/testing.ts: publishes no more new listings per hour than its network’s quota, however many wait for their originpackages/bazaar/src/testing.ts: counts a listing in the hour it is first published, however long ago it was createdpackages/bazaar/src/testing.ts: publishes no more of a payTo’s new listings at an origin per hour than its quota, however many are confirmed at oncepackages/bazaar/test/memory-catalog.test.ts: publishes listings staged pending past the hour only as the hourly quotas have room
F-2026-183
A route-template listing’s example path was taken from what its first payer echoed Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P2
A route-template listing’s example (info.input.pathParams) decides which path is asked when someone claims the listing, but it was the pathParams the creating payer echoed, which nothing checked. A payer could set an example that never answers, or answers for the payer, and so block or decide every later transfer of the seller’s listing. The example is now the path the creating settlement paid, and then the path at which the check that listed the listing confirmed it. A payer’s echo no longer decides who may claim a listing no origin has confirmed, and a route template whose paid path does not percent-decode is not used.
Fixed in ebeea7a. Tests
packages/bazaar/src/testing.ts: takes a route-template listing’s example from the path its origin confirmed, never from an echopackages/bazaar/src/testing.ts: takes a route-template listing’s example from the settlement whose path its origin first confirmedpackages/bazaar/src/testing.ts: never lets an echoed example keep a seller from claiming a route template no origin confirmed
F-2026-184
The proxy example logged API keys and payment headers for requests it could not proxy Low · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats Q2
The proxy example deleted X-API-Key and the payment headers from each site’s access log only. Caddy logs a request with its headers outside that log when it cannot proxy it (no ready replica, a failed round trip), when a response is cut off mid-body, for a Host that matches a site only without case, and with debugging on, so those entries kept the credentials in clear. The example’s default logger and a logger for handler errors now delete them too, X-Relay-H-Payment-Signature included, under which the Playground relay forwards a payment, and host names no site claims get a 404.
Fixed in ebeea7a. Tests
tools/conformance/test/proxy.test.ts: keeps payment headers and API keys out of every log besides the sites’ access logstools/conformance/test/proxy.integration.test.ts: keeps them out of the log of a request no replica could answertools/conformance/test/proxy.integration.test.ts: keeps them out of the log of a response a replica cut offtools/conformance/test/proxy.test.ts: answers host names no site claims with a 404 of its own
F-2026-185
A database restore could let an upto authorization settled at zero off-chain be settled again Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats R5
Where upto zeros settle off-chain, the settlement row is the only record that an authorization was settled for zero. Restoring a dump taken before that row lost it, and the seller could then settle the same authorization on chain for up to its signed cap. Every restore is now recorded in settlement_rewinds, and from then on /verify and upto settlements above zero answer settle_upto_stellar_authority_unavailable (retryable) until the chain has passed the longest window rule 7 accepts, about 25 minutes by default. The ledger the hold counts from is recorded only by the service’s network poll, from a provider that answers with the network’s passphrase and whose newest ledger closed within the last minute, counting its age in; requests never move it.
Fixed in 3549435. Tests
packages/facilitator/test/upto-scheme.test.ts: holds on-chain settlement where zeros settle off-chain, so a zero the restore lost is not charged againpackages/facilitator/test/upto-scheme.test.ts: settles on chain again once the chain has passed the longest window rule 7 can acceptpackages/facilitator/test/upto-scheme.test.ts: holds until authorizations accepted before a minute-old first reading have expiredpackages/facilitator/test/restore-hold.test.ts: records the first ledger only from a provider of this network whose newest ledger closed just nowpackages/store-postgres/test/stores.integration.test.ts: count a database that held settlements before restores were recorded as just restored
F-2026-186
A withdrawn listing whose usage is private showed its last settlement Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P15
GET /discovery/resources/{id} built a pending or withdrawn listing’s summary from the copy whose content it withholds, which no longer carried the private-usage extension. The detail of such a listing therefore showed lastSettled although its seller had made its usage private. The summary is now built from the stored listing, so the usage stays private while the content is withheld.
Fixed in 3549435. Tests
apps/rail402/test/discovery-usage.test.ts: dates no settlement of a withdrawn listing whose usage is private, while it is paid again
F-2026-187
rail402 quote and seller check printed a seller’s 402 fields with terminal controls Low · cli · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats P7
The CLI’s human output printed an option’s scheme, raw amount and payTo, and the seller check’s payTo and asset, as the seller’s 402 sent them. Escape sequences in them could rewrite the terminal, fake a line such as the chosen option, plant links or write to the clipboard. Every value from a seller, the Bazaar or the network is now shown without control, bidirectional or invisible format characters, one-line values without line breaks, and a response body without carriage returns; —json output is unchanged. SDK reasons that quote a seller are cleaned the same way, before secrets are redacted from them.
Fixed in b08b49f. Tests
packages/cli/test/commands.test.ts: prints a seller’s payment options without terminal controls, and JSON unchangedpackages/cli/test/commands.test.ts: keeps the lines of a paid response but not its carriage returns or controlspackages/cli/test/local.test.ts: prints the 402’s payTo and asset without terminal controls, and JSON unchangedpackages/sdk/test/basics.test.ts: strips controls before redacting, so one inside a seed cannot hide it
F-2026-188
An upto settlement landed through another fee bump could be finished as expired on lagging RPC reads Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats F2, O2, O4
Once its own fee-bump hash read not found past its bounds, a settlement was finished as expired, with no funds moved, on an inner-hash lookup and a nonce read from whichever RPC answered. A lagging provider, a backend behind a balancer or another provider after a failover could answer from before the ledger at which the bounds were seen to pass, so a payment someone else’s fee bump had landed was reported as expired. Reads that would prove a settlement absent, and the search for a copy after a failure on a consumed nonce, now count only from an RPC at or past the ledger their proof holds at, and with the history they need; otherwise the settlement waits. One left undecided for a minute counts in rail402_settlements_unresolvable.
Fixed in ebeea7a. Tests
packages/facilitator/test/upto-engine.test.ts: keep waiting while the inner-hash lookup and the nonce read lag, then succeed through the other fee bumppackages/facilitator/test/upto-engine.test.ts: keep waiting when only the payer’s nonce is read from an RPC behind the ledger boundpackages/facilitator/test/upto-engine.test.ts: keep a failure on the consumed nonce open while the nonce read lags, then succeed through the other transactionpackages/facilitator/test/upto-engine.test.ts: keep a failure open while the consuming ledger is older than the history of the RPC that scans itpackages/facilitator/test/upto-engine.test.ts: report a settlement whose search stays behind for a minute as unresolvable
F-2026-189
A pubnet approval shown for one budget window covered the next Low · mcp · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats M9
rail402-mcp’s first-per-session pubnet confirmation recorded the approved call without the budget window it was shown for. When the window renewed, or the session restarted, before the approved call was retried, the approval covered payments in a window the user had never been shown. The confirmation now carries the window and covers only payments in it; otherwise the user is asked again with the new window’s end and budget.
Fixed in ebeea7a. Tests
packages/mcp/test/pubnet.test.ts: binds an approval to the budget window it was shown forpackages/mcp/test/pubnet.test.ts: asks again when the budget window changes before the approved call is retried
F-2026-190
A keyed retry paid again after the budget window renewed or a new session started Low · sdk · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats M4
Idempotency keys were looked up within the current budget window only. A retry with the same key after the window renewed or a new session started found no earlier call and signed a second payment, although the chain had proven the first one settled. A key now belongs to the payer on its network: every ledger checks it across the payer’s windows inside its atomic reserve, replays the earlier outcome or reports paid: settled, and pays again only once the chain shows the earlier authorization expired unused. The payment identifier is chosen at random and kept with the key.
Fixed in b08b49f. Tests
packages/sdk/test/ledger.test.ts: keeps an idempotency key across budget windows and sessions while its payment may have moved moneypackages/sdk/test/ledger.test.ts: frees a key held in an earlier window once the chain proves its authorization expired unusedpackages/sdk/test/http-flow.test.ts: never pays twice for a keyed retry in a later budget windowpackages/sdk/test/mcp-flow.test.ts: never pays twice for a keyed retry after a new budget session starts
F-2026-191
withSettleDedupe refused a paid request whose first /settle answer was settlement_pending Low · sdk · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats R4
When /settle answers settlement_pending, the stock resource server settles again at once, and Rail402 answers that retry from its record with the repeat marker. withSettleDedupe took the marker for a copy of a payment already served and refused the buyer, who had paid, with seller_settlement_repeat. Each /settle the wrapper sends now carries an attempt id, which the facilitator records and echoes in every repeat’s marker. The wrapper serves, once, a marked success that echoes one of its own attempts that was answered pending or brought no answer, and still refuses every other marked success.
Fixed in 9355cd2. Tests
packages/sdk/test/seller.test.ts: serves the first success after the facilitator answered settlement_pendingpackages/sdk/test/seller.test.ts: serves a payment the stock resource server settles again after settlement_pendingpackages/sdk/test/seller.test.ts: serves a pending payment once, whichever of its copies or retries succeeds firstpackages/facilitator/test/idempotency.test.ts: records the id of the request that starts a settlement, and echoes it in every repeat
F-2026-192
Pubnet searches could wait on a search-index build queued on the shared discovery pool Low · search · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats N11, D5
A search-index build is shared by every search waiting for the same revision, and it read the catalog through the pool of the search that started it. A pubnet search could therefore join a build waiting for a connection of the shared discovery pool, which testnet or unscoped load had filled, and be shed with it. Builds and the background index check now read the catalog through a pool of their own (2 connections) that no request takes, and a search whose build cannot read the catalog answers from the previous index.
Fixed in 3549435. Tests
packages/search/test/search.test.ts: answers from the previous snapshot while an index build cannot read the catalogpackages/search/test/search.test.ts: builds the index, and reads a refresh’s revision, through the index store onlyapps/rail402/test/discovery-overload.test.ts: answers a pubnet search while the shared lane’s store has no connection for an index buildapps/rail402/test/discovery-overload.integration.test.ts: keeps the search index current while every discovery connection is held
F-2026-193
Anyone could take over the fixture smart accounts a pubnet conformance run funds Low · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats CF1
The test kit’s smart-account payer ran a contract whose init set its owner in a separate call that needed no authorization and that anyone could repeat. Another account could make itself the owner of a fixture account a pubnet run had funded with USDC and drain it. The fixture is now a contract built in this repository whose constructor binds the owner key in the transaction that creates the account, and nothing can change the key afterwards. Before a pubnet run sends USDC to a contract account, it reads the account on chain and refuses one that does not run a known account’s code, or a fixture account whose owner is not the run’s key.
Fixed in f8c4e1c. Tests
packages/testkit/test/smart-account.test.ts: takes its owner in the constructor and exports no function that could replace ittools/conformance/test/network.integration.test.ts: refuses to fund a contract account whose owner on chain is not the key it trackstools/conformance/test/network.integration.test.ts: funds a smart account whose owner no other account can replace, and gets all of it back
F-2026-194
The Explorer could name an account as the payer of a custom-token payment it never signed Low · explorer · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats I13, I3
The exact indexing rule named the transfer’s from address as the payer whenever the authorization entry named it. A token that is not a Stellar Asset Contract can skip require_auth, so a transfer from any account could succeed with an entry nobody signed, and the Explorer recorded that account as having paid. Indexing rules v2 add check E7: an exact row names its payer only when the token is a Stellar Asset Contract, checked against its asset, or the payer’s own key signed the entry over the payload the network signs; other rows are excluded as unproven_authorization. The indexer reads ledgers indexed under v1 again, and until it has, the pages, API and report leave out rows of other rules.
Fixed in 50e7461. Tests
packages/x402-index/test/hostile.test.ts: never names an account as the payer of a custom-token transfer it did not signpackages/x402-index/test/hostile.test.ts: counts a custom-token transfer the payer’s own key signed, with Address or AddressV2 credentialsapps/explorer/test/pages.test.ts: never shows an account as the payer of a custom-token transfer it did not signapps/explorer/test/indexer.integration.test.ts: reads the ledgers again when the rules version changes, so no row of older rules stayspackages/sdk/test/inspect.test.ts: says a payer authorized a transfer only when the chain proves it
F-2026-195
A refused upto rebroadcast finished as failed while its transaction could still land Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats F2
When the network refused an upto settlement’s rebroadcast for good before its bounds passed, for example once the sponsor key was frozen, the settlement was finished as failed with no funds moved. An earlier send may already have made its inner transaction public, and anyone can wrap it in a fee bump of their own until its bounds pass, so the payment could still land while the record and every repeat said failed. Such a settlement now keeps waiting until no later ledger can include its transaction, and is answered as settled when another fee bump lands it; one whose sends were all refused still finishes at once.
Fixed in ebeea7a. Tests
packages/facilitator/test/upto-engine.test.ts: keep waiting while a send may have been accepted, then succeed through someone else’s fee bumppackages/facilitator/test/upto-engine.test.ts: expire once the ledger bound passed and nothing carried the authorizationpackages/facilitator/test/upto-engine.test.ts: keep waiting when a send the look’s copy of the settlement predates was acceptedpackages/facilitator/test/upto-engine.test.ts: fail at once when no send of the bytes was ever accepted
F-2026-196
An exact payment included after its authorization expired hid a copy that had landed first Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats F1, F2
An exact transaction’s time bound can outlast the payer’s authorization. Included after the expiration, it fails with invalid_exact_stellar_signature_expired, since the host checks the expiration before the nonce, and that cause did not start the search for a copy of the authorization someone else had landed earlier. The settlement was answered as expired although the payment had settled. That cause now starts the same search as a consumed nonce, and a copy found is answered as settled with its hash.
Fixed in ebeea7a. Tests
packages/facilitator/test/upto-engine.test.ts: succeed through a copy of the authorization that landed before the expirationpackages/facilitator/test/upto-engine.test.ts: keep the failure open while the nonce read lags behind the ledger it failed in, then succeedpackages/facilitator/test/front-run.integration.test.ts: answers success with the attacker’s hash when ours lands after the authorization expired
F-2026-197
A refused exact submission finished as failed while a copy of the payment could still land Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats F2, O2
When the network refused an exact settlement’s first submission for good, or another transaction took its channel’s sequence number before any send was accepted, the settlement was finished at once as failed, with no funds moved. The payer’s authorization stays valid until it expires, so the payer, the seller or anyone the payment passed through could still land a copy, and the reason asked the buyer to sign a new payment. Such a settlement now stays open until the authorization expired unused, and is answered as settled if a copy lands. Once the channel’s sequence number proves its own transaction can never apply, it gives the channel back at once and sends nothing more.
Fixed in 9355cd2. Tests
packages/facilitator/test/first-submission.test.ts: answers a copy of the authorization that landed before its first submission was refusedpackages/facilitator/test/first-submission.test.ts: keeps a refused first submission open while its authorization is valid, and answers a later copypackages/facilitator/test/first-submission.test.ts: gives its channel back at a proven sequence conflict, and answers a later copypackages/facilitator/test/first-submission.test.ts: sends nothing more once it gave its channel back, while the same request still waitspackages/facilitator/test/sequence-conflict.integration.test.ts: gives its channel back within seconds, moves nothing, never settles twice and fails once the authorization expired
F-2026-198
An exact transaction that failed on chain was answered as failed while its authorization stayed usable Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats F2, S1
An exact transaction included and failed on chain for a cause that leaves the authorization usable (a balance spent after simulation, a trustline removed, a smart account or token that refused it for now) was finished as failed at once. The failure rolls back, nonce included, so anyone holding the payment could still land it until it expired, while the record said failed and the SDK told the buyer nothing was paid, inviting a second payment. Such a settlement now answers settlement_pending, gives its channel back, is answered as settled if a copy lands, and with the on-chain cause once the authorization expired unused; only a consumed nonce, or an expiry past the payer’s own expiration ledger, finishes at once. The SDK, rail402-mcp and the CLI report those causes as paid: unknown and never retryable.
Fixed in ebeea7a. Tests
packages/facilitator/test/first-submission.test.ts: keeps a failure on the payer’s balance open while the authorization is valid, and answers a later copypackages/facilitator/test/first-submission.test.ts: keeps a failure on a removed trustline until the authorization expired unused, then names it with the fee chargedpackages/facilitator/test/first-submission.test.ts: keeps a smart account’s failed signature open while the authorization is valid, and answers a later copypackages/facilitator/test/first-submission.test.ts: keeps an expired-authorization error raised by a token contract before the payer’s expiration open, and answers a later copypackages/facilitator/test/first-submission.test.ts: sends nothing after its own failed transaction gave the channel back, when a lagging RPC then misses it
F-2026-199
An Explorer account page aggregated its seller’s whole settlement history on every view Low · explorer · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats W3, I9
Every anonymous view of a seller’s account page counted the seller’s exact and upto payments and its distinct payers over the settlements table, with no precomputed total, so the work of one view grew with the seller’s history and ran on the Postgres the facilitator shares; only the reader role’s statement timeout bounded it, and revalidations ran it too. The indexer now keeps those counts in seller_summaries with the account totals (Explorer migration 0006, filled from the stored settlements), a view reads one row, and a seller’s newest upto payments are read through an index of their own.
Fixed in 50e7461. Tests
apps/explorer/test/web.integration.test.ts: lists, filters and pages payments, and reads what a seller received, from the database
F-2026-200
The network window published its anchor Horizon URL, including any provider key in its path Low · explorer · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats I11, I8
The network window job published the whole EXPLORER_ANCHOR_HORIZON_URL in its rebuild command and document, which the Ecosystem page, the report API and snapshot exports serve to anyone. The same variable is the indexer’s private anchor, and a paid provider’s key can only sit in the URL path. A window now names only a bare-origin Horizon: the anchor itself when its path is /, otherwise EXPLORER_NETWORK_WINDOW_PUBLIC_ANCHOR_URL, without which the job refuses to start (config_anchor_not_public); the window refuses to publish any anchor URL with a path, query or credentials.
Fixed in 50e7461. Tests
apps/explorer/test/network-window.test.ts: refuses an anchor with a path, which may carry a provider key, unless a public anchor is namedapps/explorer/test/network-window.test.ts: refuses a public anchor that is not a bare originapps/explorer/test/network-window.test.ts: never publishes an anchor URL with a path, even when called directly
F-2026-201
An address over its discovery limit still took places in the pubnet discovery lane Low · facilitator · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-28 · threats N1, D5
Per-client discovery limits ran only after a request held a lane slot, and each refused request still wrote one serialized upsert to the shared rate-limit row, so one address sending faster than that write drained could take the pubnet lane’s slots from other clients. One client now holds at most its share of a lane (DISCOVERY_MAX_IN_FLIGHT_PER_CLIENT, 8 by default) before the lane’s gate, and an address seen over its limit is refused from memory until its window ends, with no further write.
Fixed in ebeea7a. Tests
apps/rail402/test/discovery-overload.test.ts: limit how many one client holds at once, so one client cannot take every slotpackages/store-postgres/test/stores.integration.test.ts: refuses a key over its limit from memory, without another write, until its window ends
F-2026-202
Listings their origin never confirmed were kept forever, with the payer’s content stored three times Low · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats N3, P5
A free testnet settlement with Bazaar metadata for any https resource creates a pending listing; when its origin does not confirm it, it is withdrawn. Such listings and their versions, each holding the payer-echoed content, were never deleted, and only the hourly per-payer quota bounded how many were created, so free settlements grew the database pubnet shares without bound. A listing never published that has stayed withdrawn for 7 days is now deleted with its versions, options, observations and origin check (listing_pruned, migration 0036); its content was never public, and a later settlement stages it again.
Fixed in 2fd3600. Tests
packages/bazaar/src/testing.ts: deletes a listing never published once it stayed withdrawn for a week, and keeps every listing ever published
F-2026-203
One party could fill pubnet’s published-listing capacity and keep every new seller out Medium · bazaar · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats P5, P13
Pubnet’s capacity of published listings was shared first come, first served, with no share per seller, and under pubnet’s defaults no published listing ever left: the idle retention was off and nothing re-checked a published listing’s origin. A party publishing from its own origins could fill the network and keep it full after abandoning them, refusing every new seller with bazaar_catalog_full. One payTo may now hold at most PUBNET_BAZAAR_MAX_PUBLISHED_PER_OWNER (250) published listings, and pubnet withdraws a listing after 90 days without a settlement (PUBNET_BAZAAR_IDLE_TTL_DAYS, was 0); a withdrawn listing comes back only when its origin confirms it again.
Fixed in 2fd3600. Tests
packages/bazaar/src/testing.ts: holds each payTo to its share of published listings, so no party fills its networkapps/rail402/test/discovery-config.test.ts: reads each network’s own budget variables, which win over the legacy ones
F-2026-204
A contract release did not prove which revision of the third-party build workflow ran Low · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats C1, C5
The contract release calls stellar.expert’s reusable workflow at its main branch, which stellar.expert requires, with this repository’s contents, OIDC and attestation permissions. Its only check compared that branch with the reviewed revision before the call, a second read of a mutable name, and the release’s attestation was verified by signer repository alone, so a run of another revision would have gone unnoticed. The release now waits for a reviewer in the contract-release environment, and its attestation must name the reviewed workflow revision and the tag’s commit (—signer-digest, —source-digest); a pubnet deploy of the upto contract checks the same revision.
Fixed in 32bb685. Tests
tools/conformance/test/workflows.test.ts: binds a contract release to the reviewed revision of stellar.expert’s workflow and the tag’s commit
F-2026-205
The deploy gate accepted an image built for the tag name from another commit Medium · operations · Fixed · found byREV-2026-10-prelaunch on 2026-10-28, resolved 2026-10-29 · threats C2, C3
The deploy gate checked that the release tag was signed and that each image’s attestation came from the release workflow run for that tag name, but not that the build ran from the signed commit. Someone able to push a tag but without a release-signing key could point the tag at a commit whose own release.yml built and attested an image, then let the signed tag replace it; the gate would accept that image for the signed commit. The gate now requires each attestation to name the signed commit both as the build’s source and as the workflow file that ran (—source-digest, —signer-digest).
Fixed in 32bb685. Tests
tools/conformance/test/check-deployable.test.ts: gathers its facts from git and the GitHub API
F-2026-206
A protocol upgrade the SDK could not read stopped every verification while /health and /ready said healthy Medium · operations · Fixed · found byREV-2026-11-protocol-30 on 2026-11-04, resolved 2026-11-04 · threats O3
The Stellar testnet moved to protocol 30, whose ledger headers carry a StellarValueType that @stellar/stellar-sdk 16.3.0 cannot decode. Every /verify then answered unexpected_verify_error, so no testnet payment settled, while the network poll (getHealth, getTransaction and balance reads, none of which decode a ledger header) kept /health and /ready healthy and no alert fired. 0.5.2 moves to stellar-sdk 16.4.1, which reads protocol 30, and the network poll now decodes the newest ledger header each time: an SDK that cannot read the network’s ledgers fails the RPC readiness check, so /ready answers 503 and the alerts on it fire before payments fail one by one.
Fixed in 6079ae2. Tests
apps/rail402/test/protocol-readiness.integration.test.ts: fails readiness while the newest ledger header cannot be decoded, and passes again after