From 10d98e2422c061c782de1924a556649a655f92ae Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Tue, 6 Oct 2026 00:51:14 +0800 Subject: [PATCH 1/9] docs(evidence): one file per date for the 0.7.0 runs The unreleased 0.7.0 cycle had left five evidence files dated 2026-10-05 and one dated 2026-10-06. Each date now has one file, 2026-10-05-0.7.0-verification-runs.md and 2026-10-06-0.7.0-verification-runs.md, opening with a table of its parts (what each checks, and its outcome) and then the parts in the order they were run, each naming its own commits and targets: Base Sepolia, the three negative checks, Solana devnet, the payer balance read, and the wallet and serve additions on the 5th; Ethereum Sepolia, the client's spend cap and named settlement failures on the 6th. The date, environment and authorization statement are said once per file. Nothing in the runs themselves changed. Every link to the old files (CHANGELOG, CHECKS.md, the CLI guide, the core README, the evidence index) now points at the part it meant, and each link and anchor was checked to resolve. Files already published on main keep their names, since links to them have been sent. A public test comment that named a local, unpublished run record now names the evidence instead. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +- docs/CHECKS.md | 4 +- .../2026-10-05-0.7.0-verification-runs.md | 569 ++++++++++++++++++ ...026-10-05-base-sepolia-verification-run.md | 129 ---- .../2026-10-05-payer-balance-check-run.md | 97 --- ...26-10-05-solana-devnet-verification-run.md | 149 ----- ...0-05-wallet-network-and-serve-modes-run.md | 81 --- .../2026-10-05-x402-negative-checks-run.md | 112 ---- ... => 2026-10-06-0.7.0-verification-runs.md} | 33 +- docs/evidence/README.md | 8 +- docs/guides/cli.md | 2 +- packages/core/README.md | 2 +- packages/core/test/unit/payer-balance.test.ts | 2 +- 13 files changed, 606 insertions(+), 595 deletions(-) create mode 100644 docs/evidence/2026-10-05-0.7.0-verification-runs.md delete mode 100644 docs/evidence/2026-10-05-base-sepolia-verification-run.md delete mode 100644 docs/evidence/2026-10-05-payer-balance-check-run.md delete mode 100644 docs/evidence/2026-10-05-solana-devnet-verification-run.md delete mode 100644 docs/evidence/2026-10-05-wallet-network-and-serve-modes-run.md delete mode 100644 docs/evidence/2026-10-05-x402-negative-checks-run.md rename docs/evidence/{2026-10-06-ethereum-sepolia-verification-run.md => 2026-10-06-0.7.0-verification-runs.md} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b37d2..11b9733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ EIP-3009, and Permit2 with its approval signed as a gas-sponsored EIP-2612 permi `ERROR (setup)`, not a failure). Against Wasit's own Base Sepolia fixtures, both methods pass with `X402-06` settled on-chain and the payer holding no ETH; against `wasit serve` posing on Base Sepolia, the lying modes fail both checks -([evidence](docs/evidence/2026-10-05-base-sepolia-verification-run.md)). Stellar stays +([evidence](docs/evidence/2026-10-05-0.7.0-verification-runs.md#part-1-x402-payment-checks-on-base-sepolia-eip-3009-and-permit2)). Stellar stays the default; MPP is Stellar only. Under the hood, the payment checks now go through a per-chain adapter, and the x402 SDK moves from 2.19 to 2.28. @@ -72,7 +72,7 @@ for a settled transaction is now per network: 10 blocks on Sepolia, whose blocks seconds apart. No public facilitator settles Ethereum Sepolia, so Wasit's own fixture runs the SDK's facilitator itself. Against it, all ten pass with the settlement read back independently; against `wasit serve` there, the lying modes fail -([evidence](docs/evidence/2026-10-06-ethereum-sepolia-verification-run.md)). BNB Smart +([evidence](docs/evidence/2026-10-06-0.7.0-verification-runs.md#part-1-x402-payment-checks-on-ethereum-sepolia)). BNB Smart Chain testnet stays read-only. **Added — the x402 payment checks pay on Solana devnet.** `--network @@ -87,7 +87,7 @@ the RPC confirms has expired, since on Solana the blockhash, not `maxTimeoutSeco sets a payment's lifetime. Against Wasit's own Solana fixture, all ten pass with the settlement read back independently and the payer holding no SOL; against `wasit serve` on devnet, the lying modes fail -([evidence](docs/evidence/2026-10-05-solana-devnet-verification-run.md)). The payee's +([evidence](docs/evidence/2026-10-05-0.7.0-verification-runs.md#part-3-x402-payment-checks-on-solana-devnet)). The payee's token account must exist before the payment. Library users with TypeScript 6 or 7 see npm peer warnings from `@solana/kit` 5 at install; the install succeeds and core runs. @@ -101,7 +101,8 @@ target's stated reason, when it gives one. Against Wasit's own fixtures all thre all three chains, refused for the reason each is about (`invalid_exact_stellar_payload_wrong_amount`, `invalid_exact_evm_payload_authorization_value_mismatch`, `invalid_exact_evm_payload_authorization_valid_before`, `invalid_exact_svm_payload_amount_mismatch`, -...); against `wasit serve --mode no-settle` all three fail. `X402-08` is skipped when the challenge advertises the +...); against `wasit serve --mode no-settle` all three fail +([evidence](docs/evidence/2026-10-05-0.7.0-verification-runs.md#part-2-three-new-negative-checks-replay-underpayment-expired-authorization)). `X402-08` is skipped when the challenge advertises the `payment-identifier` extension, whose cached replies are legitimate. The catalogue grows from 13 to 16 checks. `X402-10` waits about 20 seconds on Stellar for the authorization to expire. @@ -131,7 +132,7 @@ payer's balance of the advertised asset before it signs; below the price, it rep unfunded payer used to fail `X402-06` on Solana devnet, for a reason real defects give too, and to stop on Stellar with only the token's error code. A balance that cannot be read pays as before. Measured on all three chains with fresh payers -([evidence](docs/evidence/2026-10-05-payer-balance-check-run.md)). +([evidence](docs/evidence/2026-10-05-0.7.0-verification-runs.md#part-4-x402-06-reads-the-payers-balance-before-paying)). **Changed — a payer key for the wrong chain, or a malformed one, stops the run at preflight.** It used to surface mid-payment as a harness error. The message never @@ -152,7 +153,7 @@ challenge (Base Sepolia and Solana devnet; v1 names no Stellar network), and `malformed-header` a `PAYMENT-REQUIRED` header that does not decode; both answer a payment with 402 and log which header it came in. The official SDK client pays the first as v1 and refuses the second without paying -([evidence](docs/evidence/2026-10-05-wallet-network-and-serve-modes-run.md)). +([evidence](docs/evidence/2026-10-05-0.7.0-verification-runs.md#part-5-payer-keys-for-base-sepolia-and-solana-and-two-new-serve-modes)). **Added — `wasit wallet --network` for the Base Sepolia and Solana devnet payers.** `wasit wallet create --role x402 --network eip155:84532` (or Solana devnet) generates the diff --git a/docs/CHECKS.md b/docs/CHECKS.md index 91a8f36..14ac544 100644 --- a/docs/CHECKS.md +++ b/docs/CHECKS.md @@ -382,14 +382,14 @@ paying through. `X402-06` reads the payer's balance before it signs. When the chain reports less than the advertised amount it stops with `ERROR (setup)`: "This run's payer holds less of … than the advertised … base units", naming the key to fund. Nothing is sent, and -`X402-07`–`10` are skipped ([evidence](evidence/2026-10-05-payer-balance-check-run.md)). +`X402-07`–`10` are skipped ([evidence](evidence/2026-10-05-0.7.0-verification-runs.md#part-4-x402-06-reads-the-payers-balance-before-paying)). The official x402 client, which Wasit pays through, refuses a price above $1 and an asset outside the SDK's defaults (Circle's USDC on Ethereum Sepolia is allowed too). Such a target gets `ERROR (setup)` saying which, and nothing is sent: the cap keeps a paywall from charging a payer more than it meant to, and Wasit pays automatically, on `stellar:pubnet` with real money -([evidence](evidence/2026-10-06-ethereum-sepolia-verification-run.md#the-clients-spend-controls)). +([evidence](evidence/2026-10-06-0.7.0-verification-runs.md#part-2-the-clients-spend-controls)). On Stellar, the payment checks need a testnet payer holding testnet USDC. `wasit wallet create --role x402 --fund` generates the key and funds it with diff --git a/docs/evidence/2026-10-05-0.7.0-verification-runs.md b/docs/evidence/2026-10-05-0.7.0-verification-runs.md new file mode 100644 index 0000000..51fc456 --- /dev/null +++ b/docs/evidence/2026-10-05-0.7.0-verification-runs.md @@ -0,0 +1,569 @@ +# Wasit 0.7.0 Verification, 2026-10-05: Base Sepolia, Solana Devnet and Three New Payment Checks + +**Date:** 2026-10-05 +**Wasit:** builds of the local `release/0.7.0` branch, not yet published; each part names its +commits. +**Environment:** macOS, Node `v26.8.1`. + +**Authorization.** None was needed or sought. Every target in every part was Wasit's own +code on our machine: `wasit serve`, or Wasit's own fixtures settling through a +facilitator. No service operated by anyone else was tested. + +Everything checked on this date, in five parts, in the order they were run. Each part says what it is +for, what was run against what, the output as printed, and its limits. + +| Part | What it checks | Outcome | +|---|---|---| +| [1. x402 payment checks on Base Sepolia (EIP-3009 and Permit2)](#part-1-x402-payment-checks-on-base-sepolia-eip-3009-and-permit2) | `X402-06` and `X402-07` pay on Base Sepolia, by EIP-3009 and by Permit2 | Lying servers fail; Wasit's fixtures pass, the settlement read back on-chain, the payer holding no ETH | +| [2. Three new negative checks (replay, underpayment, expired authorization)](#part-2-three-new-negative-checks-replay-underpayment-expired-authorization) | `X402-08`, `X402-09`, `X402-10`, on Stellar and Base Sepolia | 10/10 on both chains, each refusal for the reason the check is about; a server that takes anything fails all three | +| [3. x402 payment checks on Solana devnet](#part-3-x402-payment-checks-on-solana-devnet) | `X402-06` to `X402-10` pay on Solana devnet | Lying servers fail; Wasit's fixture passes 10/10, the settlement read back, the payer holding no SOL; installs alongside TypeScript 5.8 to 7 | +| [4. X402-06 reads the payer's balance before paying](#part-4-x402-06-reads-the-payers-balance-before-paying) | An unfunded payer is the payer's problem, not the target's | Fresh unfunded payers get `ERROR (setup)` with nothing sent, on all three chains; funded payers pass 10/10 | +| [5. Payer keys for Base Sepolia and Solana, and two new serve modes](#part-5-payer-keys-for-base-sepolia-and-solana-and-two-new-serve-modes) | `wasit wallet --network`; `wasit serve --mode v1-challenge` and `malformed-header` | Keys made and read on both chains; the official SDK client pays the v1 challenge as v1 and refuses the malformed one | + +## Part 1: x402 payment checks on Base Sepolia (EIP-3009 and Permit2) + +**Build:** builds of the local `release/0.7.0` branch (`282004c` for the EVM adapter, +`fb21760` for `wasit serve` on Base Sepolia, `7d27551` for the fixture, `384f0b8` for +the Permit2 fixture), not yet published. +**Targets:** `wasit serve --network eip155:84532`, the paywall that misbehaves on +purpose, in three modes; and Wasit's own Base Sepolia x402 fixture +(`packages/core/test/fixtures/x402-evm-server.ts`), settling through the public +`x402.org` facilitator. Both ran on our machine; the chain is Base Sepolia. + +**What this is for.** Until 0.7.0, `X402-06` and `X402-07` paid on Stellar only. They +now also pay on Base Sepolia (`eip155:84532`), with the `exact` scheme's EIP-3009 +method: the payer signs a `transferWithAuthorization` off-chain and the facilitator +submits it and pays the gas. The same two things have to hold as when the checks were +made stricter in 0.6.0: they fail servers that lie, and they pass an honest one. + +### Servers that lie + +Payer: a throwaway key generated for the run, holding nothing. That is enough here: +EIP-3009 signing needs no balance, and `wasit serve` never settles, so nothing moved. + +| `wasit serve` mode | What it does | `X402-06` | `X402-07` | +|---|---|---|---| +| `no-settle` | Serves any payment with 200, without settling and without `PAYMENT-RESPONSE` | **FAIL** | **FAIL** | +| `wrong-settlement` | Serves with a `PAYMENT-RESPONSE` naming a transaction that does not exist | **FAIL** | **FAIL** | +| `wrong-network` | Asks to be paid on Base mainnet (`eip155:8453`) | SKIP, nothing sent | SKIP | + +Output, quoted as printed: + +``` +FAIL X402-06 Signature Resubmit Accepted + Valid payment accepted (HTTP 200), but the response carried no + PAYMENT-RESPONSE header, which the x402 v2 HTTP transport uses to report + settlement. Whether the payment settled cannot be verified. +FAIL X402-07 Invalid Signature Rejected + A payment with a corrupted authorization signature was accepted with + HTTP 200 — security-relevant failure. +``` + +``` +FAIL X402-06 Signature Resubmit Accepted + Target reported settlement as tx 0x18bb3379…2393, but the chain produced + 32 more blocks without it. Either it was never broadcast, or the target + referenced a transaction that does not exist. +``` + +The second was decided against Base Sepolia itself, through its public RPC +(`https://sepolia.base.org`): the run took 65 seconds, nearly all of it waiting until +the chain had produced 32 blocks without the cited transaction. + +``` +SKIP X402-06 Signature Resubmit Accepted + Skipped: the target offers no payment option on eip155:84532 (it offers + eip155:8453), so no payment was attempted (see X402-05). +``` + +### An honest server + +Payer: a Base Sepolia account holding 20 USDC, from Circle's faucet, and **no ETH**. + +`wasit test --target http://localhost:3005/protected --network eip155:84532`: **7/7.** + +``` +PASS X402-06 Signature Resubmit Accepted + Valid payment accepted (HTTP 200) and settled on-chain for exactly the + advertised 10000 base units of 0x036CbD53842c5426634e7929541eC2318f3dCF7e + to 0xb57bFdA962aAa607D5260747A175cda8f4bDDad7, verified from the Transfer + log (tx 0xb182df33…7e91). +PASS X402-07 Invalid Signature Rejected + Payment with a corrupted authorization signature correctly rejected (HTTP 402). +``` + +[`0xb182df33…7e91`](https://base-sepolia.blockscout.com/tx/0xb182df33ee676ac15ec9137717027a61432fc24af4761d9fa89fddb23cbf7e91) + +Read back independently afterwards, outside Wasit, with `viem` against the same RPC: + +| | | +|---|---| +| Receipt | `success`, block 47709831 | +| Sent by (gas paid by) | `0xd407…f1bf`, the facilitator's account, calling the USDC contract | +| Transfers logged | exactly one: payer `0xC171…0428` to payee `0xb57b…Dad7`, 10000 units | +| Payer after | 19.99 USDC, **still 0 ETH** | +| Payee after | 20.01 USDC | + +So the settlement Wasit verified is the one that happened, and a Base Sepolia payer +needs the token and nothing else. `X402-07`'s forged signature was refused, and nothing +settled for it. + +### The Permit2 method + +A second fixture (`x402-evm-permit2-server.ts`, port 3006) advertises +`assetTransferMethod: "permit2"` and the `eip2612GasSponsoring` extension, which the +public facilitator supports (its `/supported` list). The same payer, still with no ETH, +ran all ten x402 checks against it: **10/10**, each refusal for its own reason: + +``` +PASS X402-07 ... rejected (HTTP 402: invalid_permit2_signature). +PASS X402-08 ... refused (HTTP 412: permit2_allowance_required). +PASS X402-09 ... refused (HTTP 402: permit2_amount_mismatch). +PASS X402-10 ... refused (HTTP 402: permit2_deadline_expired). +``` + +Read back for the second run's settlement, +[`0x860ca8cc…dcf7`](https://base-sepolia.blockscout.com/tx/0x860ca8cc7563e31fb9d95090369e527461bd3f614b15779f4124f30d71fbdcf7): +`success` in block 47710896, sent by the facilitator's account to the x402 Permit2 +proxy, logging an `Approval` from the payer to the canonical Permit2 contract for 10000 +(the EIP-2612 permit) and exactly one `Transfer` from the payer to the payee for 10000. +The payer was still at 0 ETH, and its Permit2 allowance back at 0. + +### What did not change + +The same day, on the same build, Wasit's Stellar fixtures were unchanged: x402 7/7 with +`X402-06` settled on-chain, `MPP-01` PASS, channel 3 passed and 2 skipped. 199 core and +54 CLI offline tests pass. + +### Limits + +- One EVM network: Base Sepolia. Permit2 was run only with a gas-sponsored approval. + Ethereum Sepolia and BNB Smart Chain testnet get the read-only checks only; no public + facilitator settles them and the official SDK ships no default token for either. +- The honest target is Wasit's own fixture on the official SDK. No third-party Base + Sepolia service was tested. +- Unreleased: this ran from the branch, not from npm. Registry parity follows the 0.7.0 + release. + +## Part 2: Three new negative checks (replay, underpayment, expired authorization) + +**Build:** the local `release/0.7.0` branch, `25a9d9d` (the checks) and `5a70778` (their docs), +not yet published. +**Targets:** Wasit's own x402 fixtures, on Stellar testnet (port 3001) and Base Sepolia +(port 3005), both settling through the public `x402.org` facilitator; and `wasit serve +--mode no-settle` on each chain, a paywall that takes any payment. All on our machine. + +**What this is for.** `X402-07` already checked that a corrupted signature is refused. +Three more ways a payment can be wrong while its signature is fine are now checked, each +grounded in the spec: the same payment sent twice (each authorization is single-use, +x402 v2 §10.1), a payment for less than the price (Stellar: the amount must equal the +requirement exactly; EVM: verification step 3), and a payment whose authorization has +expired (§10.1 time constraints; verification of the validity window). As in 0.6.0, a +check is only worth having if it fails the servers that get this wrong and passes one +that gets it right. + +### An honest server: 10/10 on both chains + +Each refusal below is quoted as printed, with the reason the target gave. + +**Stellar testnet** (`wasit test --target http://localhost:3001/protected`), 42 seconds, +most of it `X402-10` waiting out the authorization: + +``` +PASS X402-07 Invalid Signature Rejected + Payment with a corrupted authorization signature correctly rejected + (HTTP 402: invalid_exact_stellar_payload_simulation_failed). +PASS X402-08 Payment Replay Rejected + The same payment, sent again, was refused + (HTTP 402: invalid_exact_stellar_payload_simulation_failed). +PASS X402-09 Underpayment Rejected + A validly signed payment for 50000 of the advertised 100000 base units was + refused (HTTP 402: invalid_exact_stellar_payload_wrong_amount). +PASS X402-10 Expired Authorization Rejected + A payment whose authorization had expired was refused + (HTTP 402: invalid_exact_stellar_payload_simulation_failed). +``` + +On Stellar the facilitator re-simulates every payment against the current ledger; a +spent nonce, an expired auth entry and a bad signature all fail that simulation, which is +the reason it names for three of the four. The underpayment is caught earlier, by the +amount rule. + +**Base Sepolia** (`--network eip155:84532`, target port 3005), 9.5 seconds: + +``` +PASS X402-07 Invalid Signature Rejected + Payment with a corrupted authorization signature correctly rejected + (HTTP 402: invalid_exact_evm_signature). +PASS X402-08 Payment Replay Rejected + The same payment, sent again, was refused (HTTP 402). +PASS X402-09 Underpayment Rejected + A validly signed payment for 5000 of the advertised 10000 base units was + refused (HTTP 402: invalid_exact_evm_payload_authorization_value_mismatch). +PASS X402-10 Expired Authorization Rejected + A payment whose authorization had expired was refused + (HTTP 402: invalid_exact_evm_payload_authorization_valid_before). +``` + +The Base Sepolia replay was refused without a stated reason. A separate probe paid the +fixture once and replayed the same header: the replay drew 402 with no +`PAYMENT-REQUIRED` header and an empty `{}` body, a different response path from the +verify refusals above. What matters to the check holds either way: the resource was not +served a second time. + +### A server that takes anything + +`wasit serve --mode no-settle` answers every payment with 200 and settles nothing. + +| | Stellar | Base Sepolia | +|---|---|---| +| `X402-06` | FAIL | FAIL | +| `X402-07` | FAIL | FAIL | +| `X402-08` | FAIL | FAIL | +| `X402-09` | FAIL | FAIL | +| `X402-10` | FAIL | FAIL | + +### How each payment was built + +- **Replay:** the headers `X402-06`'s accepted payment was sent with, byte for byte. + Nothing can settle twice, so this costs nothing. +- **Underpayment:** signed by the official SDK client for half the advertised amount, + then sent with `accepted` set back to the advertised terms, so the target's own + matching passes and only the signed amount is wrong. +- **Expired:** signed with `maxTimeoutSeconds: 1`, from which both SDK clients derive + the lifetime (EVM `validBefore = now + 1`; Stellar one ledger), held 5 seconds on Base + Sepolia and 20 on Stellar, and sent claiming the advertised terms. + +An offline test confirms each payload says what its check claims: the replay equals the +honest header, the underpayment signs 5000 while `accepted` says 10000, and the expired +authorization's `validBefore` is already past when it arrives. + +### Not a check: settling before serving + +The default x402 v2 flow is verify, run the resource, settle, respond (§6.1), so a +server that runs the resource before settling is conformant. Only `upfront` and `escrow` +flows settle first, and they must say so in `extra.paymentFlow`. What a client can +observe, a response that arrives with a real settlement, `X402-06` already requires. + +### Limits + +- Wasit's own fixtures only, on the official SDK and the public facilitator. No + third-party service was tested. +- `X402-08` cannot tell a server that refuses a replay at verify from one that refuses + at settlement; both keep the resource from being served twice. +- Unreleased: this ran from the branch. Registry parity follows the 0.7.0 release. + +## Part 3: x402 payment checks on Solana devnet + +**Build:** builds of the local `release/0.7.0` branch (`165e23c` for the Solana adapter +and fixture, `7ea229f` for `wasit serve` on Solana devnet), not yet published. +**Targets:** `wasit serve --network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`, the +paywall that misbehaves on purpose, in three modes; and Wasit's own Solana devnet x402 +fixture (`packages/core/test/fixtures/x402-svm-server.ts`), settling through the public +`x402.org` facilitator. Both ran on our machine; the chain is Solana devnet. + +**What this is for.** `X402-06` to `X402-10` now also pay on Solana devnet, with the +`exact` scheme: the payer signs a transaction carrying one `TransferChecked` to the +payee's token account, and the sponsor named in `extra.feePayer`, here the facilitator, +signs as fee payer and submits it (`scheme_exact_svm.md`). As on Stellar and Base +Sepolia, the checks are only worth having if they fail servers that get this wrong and +pass one that gets it right. + +### Servers that lie + +Payer and payee: throwaway keypairs generated for the run, holding nothing. That is +enough here: signing needs no balance, and `wasit serve` never settles, so nothing moved. + +| `wasit serve` mode | What it does | `X402-06` | `X402-07`–`10` | +|---|---|---|---| +| `no-settle` | Serves any payment with 200, without settling and without `PAYMENT-RESPONSE` | **FAIL** | **FAIL** | +| `wrong-settlement` | Serves with a `PAYMENT-RESPONSE` naming a random signature that exists nowhere | **FAIL** | **FAIL** | +| `wrong-network` | Asks to be paid on Solana mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) | SKIP, nothing sent | SKIP | + +Output, quoted as printed: + +``` +FAIL X402-06 Signature Resubmit Accepted + Valid payment accepted (HTTP 200), but the response carried no + PAYMENT-RESPONSE header, which the x402 v2 HTTP transport uses to report + settlement. Whether the payment settled cannot be verified. +``` + +``` +FAIL X402-06 Signature Resubmit Accepted + Target reported settlement as tx 5BjTpgSQ…bLVRq, but the chain advanced 152 + more slots without it. Either it was never broadcast, or the target + referenced a transaction that does not exist. +``` + +The second was decided against Solana devnet itself, through its public RPC +(`https://api.devnet.solana.com`): the whole run took 38 seconds, most of it waiting for +150 slots to pass without the cited signature. + +### An honest server + +Payer: a devnet account holding 20 USDC of the SDK's default devnet mint +(`4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`), from Circle's faucet +(https://faucet.circle.com, Solana Devnet), and **no SOL**. Payee: a second account +whose USDC token account already existed (the payment's transaction does not create +one). + +`wasit test --target http://localhost:3007/protected --network +solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`: **10/10**, in 4 seconds. + +``` +PASS X402-06 Signature Resubmit Accepted + Valid payment accepted (HTTP 200) and settled on-chain for exactly the + advertised 10000 base units of 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU + to HzrAfDD9vQ7jjywai5dgiZbEzFumwNhWw3LLbXViHdBS, verified from the + transaction's token balances (tx 3QJCb8Z8…M7B1w). +PASS X402-07 Invalid Signature Rejected + Payment with a corrupted authorization signature correctly rejected + (HTTP 402: invalid_exact_svm_payload_signature_invalid). +PASS X402-08 Payment Replay Rejected + The same payment, sent again, was refused + (HTTP 402: invalid_exact_svm_transaction_simulation_failed). +PASS X402-09 Underpayment Rejected + A validly signed payment for 5000 of the advertised 10000 base units was + refused (HTTP 402: invalid_exact_svm_payload_amount_mismatch). +PASS X402-10 Expired Authorization Rejected + A payment whose authorization had expired was refused + (HTTP 402: invalid_exact_svm_transaction_simulation_failed). +``` + +[`3QJCb8Z8…M7B1w`](https://explorer.solana.com/tx/3QJCb8Z88QFRByCjCqb7JXvyTFZEGsSDBYqsrBmaqtZg1N3semLEm6R6McCwSNaNph33UUDttFvs3H8Ye5fM7B1w?cluster=devnet) + +Read back independently afterwards, outside Wasit, with raw JSON-RPC against the same +endpoint: + +| | | +|---|---| +| Status | succeeded (`err` null), slot 507717163 | +| Fee payer (account 0) | `CKPKJWNd…WYp5`, the facilitator's account, which paid the 10001-lamport fee | +| Signers | the facilitator and the payer `Dya3U1bb…oQ9m`, nobody else | +| Instructions | two ComputeBudget, one `spl-token` `transferChecked` of 10000 (6 decimals) from the payer's token account to the payee's, authority the payer; one memo carrying a random nonce; no inner instructions | +| Token balances | payer 20000000 → 19990000, payee 20000000 → 20010000 | +| Payer afterwards | 19.99 USDC, **still 0 SOL** | + +The payer's history holds two transactions: the deposit that funded it, and this +settlement. So the settlement Wasit verified is the one that happened, the four +payments the target had to refuse moved nothing, and a Solana devnet payer needs the +token and nothing else. + +A second run on the same build, after the dependency checks below, passed 10/10 again +([`3FomjFsz…WG8B`](https://explorer.solana.com/tx/3FomjFszTTivLg876SJopURcZTGJPaSnAsdKu2sJVULe53Qt4K6J9vYc3S29fyXmcMDSmDMBe8E9MC9Tka6XWG8B?cluster=devnet)). + +### How each payment was built + +- **Corrupted signature (`X402-07`):** before the sponsor signs, the transaction carries + one signature, the payer's; its first byte is flipped, and the message, transfer and + blockhash are untouched. +- **Replay (`X402-08`):** the headers `X402-06`'s accepted payment was sent with, byte for + byte. Solana executes a transaction once, so this costs nothing. +- **Underpayment (`X402-09`):** signed by the official SDK client for half the advertised + amount, then sent with `accepted` set back to the advertised terms. +- **Expired (`X402-10`):** on Solana a payment's lifetime is its recent blockhash, which + the SDK client does not derive from `maxTimeoutSeconds`. The payment is built on a + blockhash from 300 slots back, through the client's `extra.recentBlockhash` hint, after + the RPC confirms that blockhash is no longer valid. So it had expired before it was + sent, without holding a fresh payment for the minute or more it takes to expire. + +### What did not change + +On the same build, Wasit's other x402 fixtures each passed 10/10: Stellar testnet +(port 3001), Base Sepolia with EIP-3009 (3005) and with Permit2 (3006). `X402-10`'s +expiry moved into each chain's adapter in this change, and was refused there for the +same reasons as before (`invalid_exact_stellar_payload_simulation_failed`, +`invalid_exact_evm_payload_authorization_valid_before`, `permit2_deadline_expired`). 221 +core and 63 CLI offline tests pass. + +### Installing alongside TypeScript + +`@x402/svm` 2.28 requires `@solana/kit` 5, whose packages declare an optional peer +`typescript ^5`. The packed `@wasit-dev/core` was installed into fresh projects with +TypeScript 5.8.3, 6.0.3, 7.0.2 and none: every install succeeded. Under TypeScript 7, npm +printed 110 `ERESOLVE overriding peer dependency` warnings, and core then ran in that +project: Solana's settlement check verified `3QJCb8Z8…M7B1w` against devnet and the read +checks passed against the fixture. The CLI and MCP server installed globally or through +npx carry no TypeScript, so the warnings do not arise there. + +### Limits + +- One Solana network: devnet. The payee's token account must exist before the payment. +- The honest target is Wasit's own fixture on the official SDK. No third-party Solana + service was tested. +- `X402-08`'s and `X402-10`'s refusals both read `transaction_simulation_failed`: the + facilitator's simulation fails for a transaction already executed and for an expired + blockhash alike. Both keep the resource from being served. +- Unreleased: this ran from the branch, not from npm. Registry parity follows the 0.7.0 + release. + +## Part 4: X402-06 reads the payer's balance before paying + +**Build:** a build of the local `release/0.7.0` branch (`21f7bdc`), not yet published. +**Targets:** Wasit's own x402 fixtures on Stellar testnet (port 3001), Base Sepolia +(3005, EIP-3009; 3006, Permit2) and Solana devnet (3007), each settling through the +public `x402.org` facilitator. All on our machine. + +**What this is for.** A conformance tester must not blame a target for its own setup. A payer holding none of the asset cannot fund a payment, and before this +change that showed up as a verdict about the target, or as an unreadable error: + +- On Solana devnet, measured earlier the same day with a throwaway payer: + `FAIL X402-06 Expected 2xx after a valid payment, got 402 + (invalid_exact_svm_transaction_simulation_failed)`. The facilitator gives that reason + for real defects too, so it cannot be told apart afterwards. +- On Stellar testnet, the SDK client simulates the transfer while building the payment, + and stopped there: `ERROR X402-06 Check could not run (harness): Stellar simulation + failed with error message: HostError: Error(Contract, #13)`. No verdict, but nothing + that says what to do. +- On Base Sepolia this was not measured before the change. + +X402-06 now reads the payer's balance of the advertised asset before anything is +signed. When the chain reports less than the price, the result is `ERROR (setup)` and +nothing is sent. When the balance cannot be read, the run pays as before, so a failed +read never becomes a verdict. + +### Fresh, unfunded payers + +Each payer was a keypair generated for the run, holding nothing, never printed. + +``` +ERROR X402-06 Signature Resubmit Accepted + Check could not run (setup): This run's payer holds less of + CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA than the advertised + 100000 base units, so its payment cannot settle, and a refusal would say + nothing about the target. Fund the payer (STELLAR_PRIVATE_KEY), then re-run. +``` + +| Chain | `X402-06` | `X402-07`–`10` | +|---|---|---| +| Stellar testnet | `ERROR (setup)`, names `STELLAR_PRIVATE_KEY` | SKIP | +| Base Sepolia | `ERROR (setup)`, names `EVM_PRIVATE_KEY` | SKIP | +| Solana devnet | `ERROR (setup)`, names `SVM_PRIVATE_KEY` | SKIP | + +How each balance is read: + +- **Base Sepolia:** the token's ERC-20 `balanceOf`. +- **Solana devnet:** every token account the payer holds for the mint, summed. The client + pays from the associated one only, so the sum is never less than what it can spend. +- **Stellar:** the token's `balance`, simulated. A Stellar Asset Contract answers an + account without a trustline with an error rather than zero, which is the usual state of + an unfunded payer. So when the token is that asset's SAC, recognised because the asset + named by its `name` (`USDC:GBBD47IF…FLA5`) derives this very contract id, the trustline + is looked up, and none means the payer holds nothing. + +The same reads, made directly against each chain's public RPC: the funded payers read +196000000 (Stellar), 19910000 (Base Sepolia) and 19970000 (Solana) base units; fresh +addresses read 0 on all three. + +### Funded payers + +With the funded payers, on the same build, every x402 fixture passed 10/10: Stellar +(port 3001), Base Sepolia EIP-3009 (3005) and Permit2 (3006), Solana devnet (3007), with +`X402-06` settled on-chain each time. + +### An observation on the way + +One Permit2 run (port 3006), started a few seconds after an EIP-3009 run from the same +payer, drew `FAIL X402-06 Expected 2xx after a valid payment, got 402.`, with no reason +given. The payer's USDC transfers on Base Sepolia show no settlement at that time, so the +payment was refused, not taken. Five Permit2 runs followed, three of them right after an +EIP-3009 run, as the failing one was: all passed 10/10. The cause was not found that day. + +*Added 2026-10-06:* found. With the Permit2 fixture logging its facilitator's answers, +the same failure recurred on both Base Sepolia fixtures (five of sixteen runs on +2026-10-06), and each time its cause could be seen (the Permit2 fixture's log twice, +Wasit's own report once) it was the public facilitator's settlement failing as it +broadcast its own transaction: `settle failed: invalid_exact_evm_transaction_failed: Missing or invalid +parameters ... "method":"eth_sendRawTransaction"`. The payment verified; the +facilitator's submission failed; the payer was not charged. The official SDK server +says so in a `PAYMENT-RESPONSE` with `success: false`, which Wasit did not read; since +`178b8a6` it does, and the failure reads `got 402 (settlement failed: +invalid_exact_evm_transaction_failed)` +([Ethereum Sepolia run](2026-10-06-0.7.0-verification-runs.md#part-3-settlement-failures-are-named-now)). + +### Limits + +- Wasit's own fixtures only. No third-party service was tested. +- A balance that cannot be read gives no protection: the run pays, as before 0.7.0. +- On Stellar, only a Stellar Asset Contract's missing trustline is read as zero; for + other SEP-41 tokens a failed `balance` read pays as before. The asset's issuer, which + needs no trustline, is never read as zero. +- Unreleased: this ran from the branch, not from npm. + +## Part 5: Payer keys for Base Sepolia and Solana, and two new serve modes + +**Build:** builds of the local `release/0.7.0` branch (`4c3ca55` for `wallet --network`, +and `d82555a` for the two modes), not yet published. +**Targets:** none operated by anyone else. `wasit wallet` read balances from each +chain's public RPC; `wasit serve` ran on our machine, and the official x402 SDK client +was pointed at it as the agent. + +### `wasit wallet --network` + +Output as printed, with the generated secrets masked here: + +``` +$ wasit wallet create --role x402 --network eip155:84532 +Generated a new Base Sepolia key for x402: + +Address: 0x44170cBa1F242367Ce56BC8Df93531c39E40a05c +Secret: 0x<64-hex masked> + +Paste into .env: + +EVM_PRIVATE_KEY=0x<64-hex masked> + +Fund it with USDC at https://faucet.circle.com (network Base Sepolia), +pasting 0x44170cBa1F242367Ce56BC8Df93531c39E40a05c. +It needs no ETH: the facilitator pays the fee. +``` + +`--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` printed the same shape with an +`SVM_PRIVATE_KEY=` line. Against the configured payers: + +``` +$ wasit wallet status --network eip155:84532 +x402 0xC1716C740469a8E3dc68EC420d196164666c0428 (Base Sepolia) + USDC 19.8 + +$ wasit wallet status --network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 --json +[ { "role": "x402", "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "address": "Dya3U1bbDA7N9SSqptqgCZ4RuEeDQr1RriF9WLiYoQ9m", "usdc": "19.95" } ] +``` + +Refused, each with exit 2: `--role mpp-charge --network eip155:84532` ("Stellar only: +MPP runs on Stellar"), `--network eip155:8453` ("Expected a testnet"), and `--fund` on +Base Sepolia ("--fund is Stellar only (Friendbot)"). `wasit wallet status --role x402` +without `--network` printed the Stellar balances as before. + +### Two `wasit serve` modes about the challenge + +The agent was the official x402 SDK client (`@x402/fetch`'s `wrapFetchWithPayment`, +2.28.0, with `@x402/evm` and `@x402/svm` registered), signing with throwaway keys. + +| Mode | What the paywall sends | What the SDK client did | `wasit test --read-only` | +|---|---|---|---| +| `v1-challenge`, Base Sepolia | an x402 v1 challenge in the body (`base-sepolia`, `maxAmountRequired`), no `PAYMENT-REQUIRED` header | paid it as v1, in `X-PAYMENT` | `X402-02` FAIL (v1 body challenge), `X402-04` "All required v1 fields present", `X402-05` FAIL (`base-sepolia` is not CAIP-2) | +| `v1-challenge`, Solana devnet | the same, on `solana-devnet` | paid it as v1, in `X-PAYMENT` | as above | +| `malformed-header`, Base Sepolia | a `PAYMENT-REQUIRED` header that is base64 of JSON cut short | threw `Failed to parse payment requirements: Unterminated string in JSON`, and paid nothing | `X402-02` PASS, `X402-03` FAIL, `X402-04`–`05` SKIP | + +The server's own lines, quoted: + +``` +GET /paid: 402 x402 v1 challenge in the body, 10000 base units on base-sepolia, no PAYMENT-REQUIRED header +GET /paid: payment received in X-PAYMENT for ? base units on base-sepolia. Your agent paid the v1 challenge as v1, in X-PAYMENT. Answered 402: wasit serve never settles. +GET /paid: 402 with a PAYMENT-REQUIRED header that does not decode +``` + +`v1-challenge` refuses Stellar (`x402 v1 names no Stellar testnet network`): the `exact` +scheme on Stellar is defined for v2 only. Both modes answer any payment with 402, since +they are about the challenge, and neither settles anything. + +### Limits + +- The agent here was the official SDK client only. How other agents behave is what the + modes are for, and was not run. +- `wasit wallet` cannot fund Base Sepolia or Solana devnet: Circle's faucet takes no + scripted requests, so `fund --network` prints the faucet step. +- Unreleased: this ran from the branch, not from npm. diff --git a/docs/evidence/2026-10-05-base-sepolia-verification-run.md b/docs/evidence/2026-10-05-base-sepolia-verification-run.md deleted file mode 100644 index 70b1234..0000000 --- a/docs/evidence/2026-10-05-base-sepolia-verification-run.md +++ /dev/null @@ -1,129 +0,0 @@ -# x402 Payment Checks on Base Sepolia — A/B Against Lying Servers, and a Real Settlement - -**Date:** 2026-10-05 -**Wasit:** builds of the local `release/0.7.0` branch (`282004c` for the EVM adapter, -`fb21760` for `wasit serve` on Base Sepolia, `7d27551` for the fixture, `384f0b8` for -the Permit2 fixture), not yet published. -**Targets:** `wasit serve --network eip155:84532`, the paywall that misbehaves on -purpose, in three modes; and Wasit's own Base Sepolia x402 fixture -(`packages/core/test/fixtures/x402-evm-server.ts`), settling through the public -`x402.org` facilitator. Both ran on our machine; the chain is Base Sepolia. -**Environment:** macOS, Node `v26.8.1`. - -**What this is for.** Until 0.7.0, `X402-06` and `X402-07` paid on Stellar only. They -now also pay on Base Sepolia (`eip155:84532`), with the `exact` scheme's EIP-3009 -method: the payer signs a `transferWithAuthorization` off-chain and the facilitator -submits it and pays the gas. The same two things have to hold as when the checks were -made stricter in 0.6.0: they fail servers that lie, and they pass an honest one. - -**Authorization.** None was needed or sought. Every target was Wasit's own code on our -machine. No service operated by anyone else was tested. - -## Servers that lie - -Payer: a throwaway key generated for the run, holding nothing. That is enough here: -EIP-3009 signing needs no balance, and `wasit serve` never settles, so nothing moved. - -| `wasit serve` mode | What it does | `X402-06` | `X402-07` | -|---|---|---|---| -| `no-settle` | Serves any payment with 200, without settling and without `PAYMENT-RESPONSE` | **FAIL** | **FAIL** | -| `wrong-settlement` | Serves with a `PAYMENT-RESPONSE` naming a transaction that does not exist | **FAIL** | **FAIL** | -| `wrong-network` | Asks to be paid on Base mainnet (`eip155:8453`) | SKIP, nothing sent | SKIP | - -Output, quoted as printed: - -``` -FAIL X402-06 Signature Resubmit Accepted - Valid payment accepted (HTTP 200), but the response carried no - PAYMENT-RESPONSE header, which the x402 v2 HTTP transport uses to report - settlement. Whether the payment settled cannot be verified. -FAIL X402-07 Invalid Signature Rejected - A payment with a corrupted authorization signature was accepted with - HTTP 200 — security-relevant failure. -``` - -``` -FAIL X402-06 Signature Resubmit Accepted - Target reported settlement as tx 0x18bb3379…2393, but the chain produced - 32 more blocks without it. Either it was never broadcast, or the target - referenced a transaction that does not exist. -``` - -The second was decided against Base Sepolia itself, through its public RPC -(`https://sepolia.base.org`): the run took 65 seconds, nearly all of it waiting until -the chain had produced 32 blocks without the cited transaction. - -``` -SKIP X402-06 Signature Resubmit Accepted - Skipped: the target offers no payment option on eip155:84532 (it offers - eip155:8453), so no payment was attempted (see X402-05). -``` - -## An honest server - -Payer: a Base Sepolia account holding 20 USDC, from Circle's faucet, and **no ETH**. - -`wasit test --target http://localhost:3005/protected --network eip155:84532`: **7/7.** - -``` -PASS X402-06 Signature Resubmit Accepted - Valid payment accepted (HTTP 200) and settled on-chain for exactly the - advertised 10000 base units of 0x036CbD53842c5426634e7929541eC2318f3dCF7e - to 0xb57bFdA962aAa607D5260747A175cda8f4bDDad7, verified from the Transfer - log (tx 0xb182df33…7e91). -PASS X402-07 Invalid Signature Rejected - Payment with a corrupted authorization signature correctly rejected (HTTP 402). -``` - -[`0xb182df33…7e91`](https://base-sepolia.blockscout.com/tx/0xb182df33ee676ac15ec9137717027a61432fc24af4761d9fa89fddb23cbf7e91) - -Read back independently afterwards, outside Wasit, with `viem` against the same RPC: - -| | | -|---|---| -| Receipt | `success`, block 47709831 | -| Sent by (gas paid by) | `0xd407…f1bf`, the facilitator's account, calling the USDC contract | -| Transfers logged | exactly one: payer `0xC171…0428` to payee `0xb57b…Dad7`, 10000 units | -| Payer after | 19.99 USDC, **still 0 ETH** | -| Payee after | 20.01 USDC | - -So the settlement Wasit verified is the one that happened, and a Base Sepolia payer -needs the token and nothing else. `X402-07`'s forged signature was refused, and nothing -settled for it. - -## The Permit2 method - -A second fixture (`x402-evm-permit2-server.ts`, port 3006) advertises -`assetTransferMethod: "permit2"` and the `eip2612GasSponsoring` extension, which the -public facilitator supports (its `/supported` list). The same payer, still with no ETH, -ran all ten x402 checks against it: **10/10**, each refusal for its own reason: - -``` -PASS X402-07 ... rejected (HTTP 402: invalid_permit2_signature). -PASS X402-08 ... refused (HTTP 412: permit2_allowance_required). -PASS X402-09 ... refused (HTTP 402: permit2_amount_mismatch). -PASS X402-10 ... refused (HTTP 402: permit2_deadline_expired). -``` - -Read back for the second run's settlement, -[`0x860ca8cc…dcf7`](https://base-sepolia.blockscout.com/tx/0x860ca8cc7563e31fb9d95090369e527461bd3f614b15779f4124f30d71fbdcf7): -`success` in block 47710896, sent by the facilitator's account to the x402 Permit2 -proxy, logging an `Approval` from the payer to the canonical Permit2 contract for 10000 -(the EIP-2612 permit) and exactly one `Transfer` from the payer to the payee for 10000. -The payer was still at 0 ETH, and its Permit2 allowance back at 0. - -## What did not change - -The same day, on the same build, Wasit's Stellar fixtures were unchanged: x402 7/7 with -`X402-06` settled on-chain, `MPP-01` PASS, channel 3 passed and 2 skipped. 199 core and -54 CLI offline tests pass. - -## Limits - -- One EVM network: Base Sepolia. Permit2 was run only with a gas-sponsored approval. - Ethereum Sepolia and BNB Smart Chain testnet get the read-only checks only; no public - facilitator settles them and the official SDK ships no default token for either. -- The honest target is Wasit's own fixture on the official SDK. No third-party Base - Sepolia service was tested. -- Unreleased: this ran from the branch, not from npm. Registry parity follows the 0.7.0 - release. diff --git a/docs/evidence/2026-10-05-payer-balance-check-run.md b/docs/evidence/2026-10-05-payer-balance-check-run.md deleted file mode 100644 index eb9d31c..0000000 --- a/docs/evidence/2026-10-05-payer-balance-check-run.md +++ /dev/null @@ -1,97 +0,0 @@ -# A Payer That Cannot Pay Is the Payer's Problem — X402-06 Reads the Balance First - -**Date:** 2026-10-05 -**Wasit:** a build of the local `release/0.7.0` branch (`21f7bdc`), not yet published. -**Targets:** Wasit's own x402 fixtures on Stellar testnet (port 3001), Base Sepolia -(3005, EIP-3009; 3006, Permit2) and Solana devnet (3007), each settling through the -public `x402.org` facilitator. All on our machine. -**Environment:** macOS, Node `v26.8.1`. - -**What this is for.** A conformance tester must not blame a target for its own setup. A payer holding none of the asset cannot fund a payment, and before this -change that showed up as a verdict about the target, or as an unreadable error: - -- On Solana devnet, measured earlier the same day with a throwaway payer: - `FAIL X402-06 Expected 2xx after a valid payment, got 402 - (invalid_exact_svm_transaction_simulation_failed)`. The facilitator gives that reason - for real defects too, so it cannot be told apart afterwards. -- On Stellar testnet, the SDK client simulates the transfer while building the payment, - and stopped there: `ERROR X402-06 Check could not run (harness): Stellar simulation - failed with error message: HostError: Error(Contract, #13)`. No verdict, but nothing - that says what to do. -- On Base Sepolia this was not measured before the change. - -X402-06 now reads the payer's balance of the advertised asset before anything is -signed. When the chain reports less than the price, the result is `ERROR (setup)` and -nothing is sent. When the balance cannot be read, the run pays as before, so a failed -read never becomes a verdict. - -**Authorization.** None was needed or sought. Every target was Wasit's own code on our -machine. No service operated by anyone else was tested. - -## Fresh, unfunded payers - -Each payer was a keypair generated for the run, holding nothing, never printed. - -``` -ERROR X402-06 Signature Resubmit Accepted - Check could not run (setup): This run's payer holds less of - CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA than the advertised - 100000 base units, so its payment cannot settle, and a refusal would say - nothing about the target. Fund the payer (STELLAR_PRIVATE_KEY), then re-run. -``` - -| Chain | `X402-06` | `X402-07`–`10` | -|---|---|---| -| Stellar testnet | `ERROR (setup)`, names `STELLAR_PRIVATE_KEY` | SKIP | -| Base Sepolia | `ERROR (setup)`, names `EVM_PRIVATE_KEY` | SKIP | -| Solana devnet | `ERROR (setup)`, names `SVM_PRIVATE_KEY` | SKIP | - -How each balance is read: - -- **Base Sepolia:** the token's ERC-20 `balanceOf`. -- **Solana devnet:** every token account the payer holds for the mint, summed. The client - pays from the associated one only, so the sum is never less than what it can spend. -- **Stellar:** the token's `balance`, simulated. A Stellar Asset Contract answers an - account without a trustline with an error rather than zero, which is the usual state of - an unfunded payer. So when the token is that asset's SAC, recognised because the asset - named by its `name` (`USDC:GBBD47IF…FLA5`) derives this very contract id, the trustline - is looked up, and none means the payer holds nothing. - -The same reads, made directly against each chain's public RPC: the funded payers read -196000000 (Stellar), 19910000 (Base Sepolia) and 19970000 (Solana) base units; fresh -addresses read 0 on all three. - -## Funded payers - -With the funded payers, on the same build, every x402 fixture passed 10/10: Stellar -(port 3001), Base Sepolia EIP-3009 (3005) and Permit2 (3006), Solana devnet (3007), with -`X402-06` settled on-chain each time. - -## An observation on the way - -One Permit2 run (port 3006), started a few seconds after an EIP-3009 run from the same -payer, drew `FAIL X402-06 Expected 2xx after a valid payment, got 402.`, with no reason -given. The payer's USDC transfers on Base Sepolia show no settlement at that time, so the -payment was refused, not taken. Five Permit2 runs followed, three of them right after an -EIP-3009 run, as the failing one was: all passed 10/10. The cause was not found that day. - -*Added 2026-10-06:* found. With the Permit2 fixture logging its facilitator's answers, -the same failure recurred on both Base Sepolia fixtures (five of sixteen runs on -2026-10-06), and each time its cause could be seen (the Permit2 fixture's log twice, -Wasit's own report once) it was the public facilitator's settlement failing as it -broadcast its own transaction: `settle failed: invalid_exact_evm_transaction_failed: Missing or invalid -parameters ... "method":"eth_sendRawTransaction"`. The payment verified; the -facilitator's submission failed; the payer was not charged. The official SDK server -says so in a `PAYMENT-RESPONSE` with `success: false`, which Wasit did not read; since -`178b8a6` it does, and the failure reads `got 402 (settlement failed: -invalid_exact_evm_transaction_failed)` -([Ethereum Sepolia run](2026-10-06-ethereum-sepolia-verification-run.md#settlement-failures-are-named-now)). - -## Limits - -- Wasit's own fixtures only. No third-party service was tested. -- A balance that cannot be read gives no protection: the run pays, as before 0.7.0. -- On Stellar, only a Stellar Asset Contract's missing trustline is read as zero; for - other SEP-41 tokens a failed `balance` read pays as before. The asset's issuer, which - needs no trustline, is never read as zero. -- Unreleased: this ran from the branch, not from npm. diff --git a/docs/evidence/2026-10-05-solana-devnet-verification-run.md b/docs/evidence/2026-10-05-solana-devnet-verification-run.md deleted file mode 100644 index c5bf05d..0000000 --- a/docs/evidence/2026-10-05-solana-devnet-verification-run.md +++ /dev/null @@ -1,149 +0,0 @@ -# x402 Payment Checks on Solana Devnet — A/B Against Lying Servers, and a Real Settlement - -**Date:** 2026-10-05 -**Wasit:** builds of the local `release/0.7.0` branch (`165e23c` for the Solana adapter -and fixture, `7ea229f` for `wasit serve` on Solana devnet), not yet published. -**Targets:** `wasit serve --network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`, the -paywall that misbehaves on purpose, in three modes; and Wasit's own Solana devnet x402 -fixture (`packages/core/test/fixtures/x402-svm-server.ts`), settling through the public -`x402.org` facilitator. Both ran on our machine; the chain is Solana devnet. -**Environment:** macOS, Node `v26.8.1`. - -**What this is for.** `X402-06` to `X402-10` now also pay on Solana devnet, with the -`exact` scheme: the payer signs a transaction carrying one `TransferChecked` to the -payee's token account, and the sponsor named in `extra.feePayer`, here the facilitator, -signs as fee payer and submits it (`scheme_exact_svm.md`). As on Stellar and Base -Sepolia, the checks are only worth having if they fail servers that get this wrong and -pass one that gets it right. - -**Authorization.** None was needed or sought. Every target was Wasit's own code on our -machine. No service operated by anyone else was tested. - -## Servers that lie - -Payer and payee: throwaway keypairs generated for the run, holding nothing. That is -enough here: signing needs no balance, and `wasit serve` never settles, so nothing moved. - -| `wasit serve` mode | What it does | `X402-06` | `X402-07`–`10` | -|---|---|---|---| -| `no-settle` | Serves any payment with 200, without settling and without `PAYMENT-RESPONSE` | **FAIL** | **FAIL** | -| `wrong-settlement` | Serves with a `PAYMENT-RESPONSE` naming a random signature that exists nowhere | **FAIL** | **FAIL** | -| `wrong-network` | Asks to be paid on Solana mainnet (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) | SKIP, nothing sent | SKIP | - -Output, quoted as printed: - -``` -FAIL X402-06 Signature Resubmit Accepted - Valid payment accepted (HTTP 200), but the response carried no - PAYMENT-RESPONSE header, which the x402 v2 HTTP transport uses to report - settlement. Whether the payment settled cannot be verified. -``` - -``` -FAIL X402-06 Signature Resubmit Accepted - Target reported settlement as tx 5BjTpgSQ…bLVRq, but the chain advanced 152 - more slots without it. Either it was never broadcast, or the target - referenced a transaction that does not exist. -``` - -The second was decided against Solana devnet itself, through its public RPC -(`https://api.devnet.solana.com`): the whole run took 38 seconds, most of it waiting for -150 slots to pass without the cited signature. - -## An honest server - -Payer: a devnet account holding 20 USDC of the SDK's default devnet mint -(`4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`), from Circle's faucet -(https://faucet.circle.com, Solana Devnet), and **no SOL**. Payee: a second account -whose USDC token account already existed (the payment's transaction does not create -one). - -`wasit test --target http://localhost:3007/protected --network -solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`: **10/10**, in 4 seconds. - -``` -PASS X402-06 Signature Resubmit Accepted - Valid payment accepted (HTTP 200) and settled on-chain for exactly the - advertised 10000 base units of 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU - to HzrAfDD9vQ7jjywai5dgiZbEzFumwNhWw3LLbXViHdBS, verified from the - transaction's token balances (tx 3QJCb8Z8…M7B1w). -PASS X402-07 Invalid Signature Rejected - Payment with a corrupted authorization signature correctly rejected - (HTTP 402: invalid_exact_svm_payload_signature_invalid). -PASS X402-08 Payment Replay Rejected - The same payment, sent again, was refused - (HTTP 402: invalid_exact_svm_transaction_simulation_failed). -PASS X402-09 Underpayment Rejected - A validly signed payment for 5000 of the advertised 10000 base units was - refused (HTTP 402: invalid_exact_svm_payload_amount_mismatch). -PASS X402-10 Expired Authorization Rejected - A payment whose authorization had expired was refused - (HTTP 402: invalid_exact_svm_transaction_simulation_failed). -``` - -[`3QJCb8Z8…M7B1w`](https://explorer.solana.com/tx/3QJCb8Z88QFRByCjCqb7JXvyTFZEGsSDBYqsrBmaqtZg1N3semLEm6R6McCwSNaNph33UUDttFvs3H8Ye5fM7B1w?cluster=devnet) - -Read back independently afterwards, outside Wasit, with raw JSON-RPC against the same -endpoint: - -| | | -|---|---| -| Status | succeeded (`err` null), slot 507717163 | -| Fee payer (account 0) | `CKPKJWNd…WYp5`, the facilitator's account, which paid the 10001-lamport fee | -| Signers | the facilitator and the payer `Dya3U1bb…oQ9m`, nobody else | -| Instructions | two ComputeBudget, one `spl-token` `transferChecked` of 10000 (6 decimals) from the payer's token account to the payee's, authority the payer; one memo carrying a random nonce; no inner instructions | -| Token balances | payer 20000000 → 19990000, payee 20000000 → 20010000 | -| Payer afterwards | 19.99 USDC, **still 0 SOL** | - -The payer's history holds two transactions: the deposit that funded it, and this -settlement. So the settlement Wasit verified is the one that happened, the four -payments the target had to refuse moved nothing, and a Solana devnet payer needs the -token and nothing else. - -A second run on the same build, after the dependency checks below, passed 10/10 again -([`3FomjFsz…WG8B`](https://explorer.solana.com/tx/3FomjFszTTivLg876SJopURcZTGJPaSnAsdKu2sJVULe53Qt4K6J9vYc3S29fyXmcMDSmDMBe8E9MC9Tka6XWG8B?cluster=devnet)). - -## How each payment was built - -- **Corrupted signature (`X402-07`):** before the sponsor signs, the transaction carries - one signature, the payer's; its first byte is flipped, and the message, transfer and - blockhash are untouched. -- **Replay (`X402-08`):** the headers `X402-06`'s accepted payment was sent with, byte for - byte. Solana executes a transaction once, so this costs nothing. -- **Underpayment (`X402-09`):** signed by the official SDK client for half the advertised - amount, then sent with `accepted` set back to the advertised terms. -- **Expired (`X402-10`):** on Solana a payment's lifetime is its recent blockhash, which - the SDK client does not derive from `maxTimeoutSeconds`. The payment is built on a - blockhash from 300 slots back, through the client's `extra.recentBlockhash` hint, after - the RPC confirms that blockhash is no longer valid. So it had expired before it was - sent, without holding a fresh payment for the minute or more it takes to expire. - -## What did not change - -On the same build, Wasit's other x402 fixtures each passed 10/10: Stellar testnet -(port 3001), Base Sepolia with EIP-3009 (3005) and with Permit2 (3006). `X402-10`'s -expiry moved into each chain's adapter in this change, and was refused there for the -same reasons as before (`invalid_exact_stellar_payload_simulation_failed`, -`invalid_exact_evm_payload_authorization_valid_before`, `permit2_deadline_expired`). 221 -core and 63 CLI offline tests pass. - -## Installing alongside TypeScript - -`@x402/svm` 2.28 requires `@solana/kit` 5, whose packages declare an optional peer -`typescript ^5`. The packed `@wasit-dev/core` was installed into fresh projects with -TypeScript 5.8.3, 6.0.3, 7.0.2 and none: every install succeeded. Under TypeScript 7, npm -printed 110 `ERESOLVE overriding peer dependency` warnings, and core then ran in that -project: Solana's settlement check verified `3QJCb8Z8…M7B1w` against devnet and the read -checks passed against the fixture. The CLI and MCP server installed globally or through -npx carry no TypeScript, so the warnings do not arise there. - -## Limits - -- One Solana network: devnet. The payee's token account must exist before the payment. -- The honest target is Wasit's own fixture on the official SDK. No third-party Solana - service was tested. -- `X402-08`'s and `X402-10`'s refusals both read `transaction_simulation_failed`: the - facilitator's simulation fails for a transaction already executed and for an expired - blockhash alike. Both keep the resource from being served. -- Unreleased: this ran from the branch, not from npm. Registry parity follows the 0.7.0 - release. diff --git a/docs/evidence/2026-10-05-wallet-network-and-serve-modes-run.md b/docs/evidence/2026-10-05-wallet-network-and-serve-modes-run.md deleted file mode 100644 index ecd02e0..0000000 --- a/docs/evidence/2026-10-05-wallet-network-and-serve-modes-run.md +++ /dev/null @@ -1,81 +0,0 @@ -# `wasit wallet --network` and Two New `wasit serve` Modes — What They Do, as Run - -**Date:** 2026-10-05 -**Wasit:** builds of the local `release/0.7.0` branch (`4c3ca55` for `wallet --network`, -and `d82555a` for the two modes), not yet published. -**Targets:** none operated by anyone else. `wasit wallet` read balances from each -chain's public RPC; `wasit serve` ran on our machine, and the official x402 SDK client -was pointed at it as the agent. -**Environment:** macOS, Node `v26.8.1`. - -**Authorization.** None was needed or sought. No service operated by anyone else was -tested. - -## `wasit wallet --network` - -Output as printed, with the generated secrets masked here: - -``` -$ wasit wallet create --role x402 --network eip155:84532 -Generated a new Base Sepolia key for x402: - -Address: 0x44170cBa1F242367Ce56BC8Df93531c39E40a05c -Secret: 0x<64-hex masked> - -Paste into .env: - -EVM_PRIVATE_KEY=0x<64-hex masked> - -Fund it with USDC at https://faucet.circle.com (network Base Sepolia), -pasting 0x44170cBa1F242367Ce56BC8Df93531c39E40a05c. -It needs no ETH: the facilitator pays the fee. -``` - -`--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` printed the same shape with an -`SVM_PRIVATE_KEY=` line. Against the configured payers: - -``` -$ wasit wallet status --network eip155:84532 -x402 0xC1716C740469a8E3dc68EC420d196164666c0428 (Base Sepolia) - USDC 19.8 - -$ wasit wallet status --network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 --json -[ { "role": "x402", "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", - "address": "Dya3U1bbDA7N9SSqptqgCZ4RuEeDQr1RriF9WLiYoQ9m", "usdc": "19.95" } ] -``` - -Refused, each with exit 2: `--role mpp-charge --network eip155:84532` ("Stellar only: -MPP runs on Stellar"), `--network eip155:8453` ("Expected a testnet"), and `--fund` on -Base Sepolia ("--fund is Stellar only (Friendbot)"). `wasit wallet status --role x402` -without `--network` printed the Stellar balances as before. - -## Two `wasit serve` modes about the challenge - -The agent was the official x402 SDK client (`@x402/fetch`'s `wrapFetchWithPayment`, -2.28.0, with `@x402/evm` and `@x402/svm` registered), signing with throwaway keys. - -| Mode | What the paywall sends | What the SDK client did | `wasit test --read-only` | -|---|---|---|---| -| `v1-challenge`, Base Sepolia | an x402 v1 challenge in the body (`base-sepolia`, `maxAmountRequired`), no `PAYMENT-REQUIRED` header | paid it as v1, in `X-PAYMENT` | `X402-02` FAIL (v1 body challenge), `X402-04` "All required v1 fields present", `X402-05` FAIL (`base-sepolia` is not CAIP-2) | -| `v1-challenge`, Solana devnet | the same, on `solana-devnet` | paid it as v1, in `X-PAYMENT` | as above | -| `malformed-header`, Base Sepolia | a `PAYMENT-REQUIRED` header that is base64 of JSON cut short | threw `Failed to parse payment requirements: Unterminated string in JSON`, and paid nothing | `X402-02` PASS, `X402-03` FAIL, `X402-04`–`05` SKIP | - -The server's own lines, quoted: - -``` -GET /paid: 402 x402 v1 challenge in the body, 10000 base units on base-sepolia, no PAYMENT-REQUIRED header -GET /paid: payment received in X-PAYMENT for ? base units on base-sepolia. Your agent paid the v1 challenge as v1, in X-PAYMENT. Answered 402: wasit serve never settles. -GET /paid: 402 with a PAYMENT-REQUIRED header that does not decode -``` - -`v1-challenge` refuses Stellar (`x402 v1 names no Stellar testnet network`): the `exact` -scheme on Stellar is defined for v2 only. Both modes answer any payment with 402, since -they are about the challenge, and neither settles anything. - -## Limits - -- The agent here was the official SDK client only. How other agents behave is what the - modes are for, and was not run. -- `wasit wallet` cannot fund Base Sepolia or Solana devnet: Circle's faucet takes no - scripted requests, so `fund --network` prints the faucet step. -- Unreleased: this ran from the branch, not from npm. diff --git a/docs/evidence/2026-10-05-x402-negative-checks-run.md b/docs/evidence/2026-10-05-x402-negative-checks-run.md deleted file mode 100644 index e497fa8..0000000 --- a/docs/evidence/2026-10-05-x402-negative-checks-run.md +++ /dev/null @@ -1,112 +0,0 @@ -# Replay, Underpayment and Expired Authorization — Three New x402 Checks, on Stellar and Base Sepolia - -**Date:** 2026-10-05 -**Wasit:** a build of the local `release/0.7.0` branch with `X402-08`, `X402-09` and -`X402-10`, not yet published. -**Targets:** Wasit's own x402 fixtures, on Stellar testnet (port 3001) and Base Sepolia -(port 3005), both settling through the public `x402.org` facilitator; and `wasit serve ---mode no-settle` on each chain, a paywall that takes any payment. All on our machine. -**Environment:** macOS, Node `v26.8.1`. - -**What this is for.** `X402-07` already checked that a corrupted signature is refused. -Three more ways a payment can be wrong while its signature is fine are now checked, each -grounded in the spec: the same payment sent twice (each authorization is single-use, -x402 v2 §10.1), a payment for less than the price (Stellar: the amount must equal the -requirement exactly; EVM: verification step 3), and a payment whose authorization has -expired (§10.1 time constraints; verification of the validity window). As in 0.6.0, a -check is only worth having if it fails the servers that get this wrong and passes one -that gets it right. - -**Authorization.** None was needed or sought. Every target was Wasit's own code on our -machine. No service operated by anyone else was tested. - -## An honest server: 10/10 on both chains - -Each refusal below is quoted as printed, with the reason the target gave. - -**Stellar testnet** (`wasit test --target http://localhost:3001/protected`), 42 seconds, -most of it `X402-10` waiting out the authorization: - -``` -PASS X402-07 Invalid Signature Rejected - Payment with a corrupted authorization signature correctly rejected - (HTTP 402: invalid_exact_stellar_payload_simulation_failed). -PASS X402-08 Payment Replay Rejected - The same payment, sent again, was refused - (HTTP 402: invalid_exact_stellar_payload_simulation_failed). -PASS X402-09 Underpayment Rejected - A validly signed payment for 50000 of the advertised 100000 base units was - refused (HTTP 402: invalid_exact_stellar_payload_wrong_amount). -PASS X402-10 Expired Authorization Rejected - A payment whose authorization had expired was refused - (HTTP 402: invalid_exact_stellar_payload_simulation_failed). -``` - -On Stellar the facilitator re-simulates every payment against the current ledger; a -spent nonce, an expired auth entry and a bad signature all fail that simulation, which is -the reason it names for three of the four. The underpayment is caught earlier, by the -amount rule. - -**Base Sepolia** (`--network eip155:84532`, target port 3005), 9.5 seconds: - -``` -PASS X402-07 Invalid Signature Rejected - Payment with a corrupted authorization signature correctly rejected - (HTTP 402: invalid_exact_evm_signature). -PASS X402-08 Payment Replay Rejected - The same payment, sent again, was refused (HTTP 402). -PASS X402-09 Underpayment Rejected - A validly signed payment for 5000 of the advertised 10000 base units was - refused (HTTP 402: invalid_exact_evm_payload_authorization_value_mismatch). -PASS X402-10 Expired Authorization Rejected - A payment whose authorization had expired was refused - (HTTP 402: invalid_exact_evm_payload_authorization_valid_before). -``` - -The Base Sepolia replay was refused without a stated reason. A separate probe paid the -fixture once and replayed the same header: the replay drew 402 with no -`PAYMENT-REQUIRED` header and an empty `{}` body, a different response path from the -verify refusals above. What matters to the check holds either way: the resource was not -served a second time. - -## A server that takes anything - -`wasit serve --mode no-settle` answers every payment with 200 and settles nothing. - -| | Stellar | Base Sepolia | -|---|---|---| -| `X402-06` | FAIL | FAIL | -| `X402-07` | FAIL | FAIL | -| `X402-08` | FAIL | FAIL | -| `X402-09` | FAIL | FAIL | -| `X402-10` | FAIL | FAIL | - -## How each payment was built - -- **Replay:** the headers `X402-06`'s accepted payment was sent with, byte for byte. - Nothing can settle twice, so this costs nothing. -- **Underpayment:** signed by the official SDK client for half the advertised amount, - then sent with `accepted` set back to the advertised terms, so the target's own - matching passes and only the signed amount is wrong. -- **Expired:** signed with `maxTimeoutSeconds: 1`, from which both SDK clients derive - the lifetime (EVM `validBefore = now + 1`; Stellar one ledger), held 5 seconds on Base - Sepolia and 20 on Stellar, and sent claiming the advertised terms. - -An offline test confirms each payload says what its check claims: the replay equals the -honest header, the underpayment signs 5000 while `accepted` says 10000, and the expired -authorization's `validBefore` is already past when it arrives. - -## Not a check: settling before serving - -The default x402 v2 flow is verify, run the resource, settle, respond (§6.1), so a -server that runs the resource before settling is conformant. Only `upfront` and `escrow` -flows settle first, and they must say so in `extra.paymentFlow`. What a client can -observe, a response that arrives with a real settlement, `X402-06` already requires. - -## Limits - -- Wasit's own fixtures only, on the official SDK and the public facilitator. No - third-party service was tested. -- `X402-08` cannot tell a server that refuses a replay at verify from one that refuses - at settlement; both keep the resource from being served twice. -- Unreleased: this ran from the branch. Registry parity follows the 0.7.0 release. diff --git a/docs/evidence/2026-10-06-ethereum-sepolia-verification-run.md b/docs/evidence/2026-10-06-0.7.0-verification-runs.md similarity index 79% rename from docs/evidence/2026-10-06-ethereum-sepolia-verification-run.md rename to docs/evidence/2026-10-06-0.7.0-verification-runs.md index 97f25d2..3b2d7b8 100644 --- a/docs/evidence/2026-10-06-ethereum-sepolia-verification-run.md +++ b/docs/evidence/2026-10-06-0.7.0-verification-runs.md @@ -1,16 +1,32 @@ -# x402 Payment Checks on Ethereum Sepolia — A/B Against Lying Servers, and a Real Settlement +# Wasit 0.7.0 Verification, 2026-10-06: Ethereum Sepolia, the $1 Spend Cap and Named Settlement Failures -**Date:** 2026-10-05 and 2026-10-06 +**Date:** 2026-10-06 (the `wasit serve` runs in part 1 ran late on 2026-10-05) **Wasit:** builds of the local `release/0.7.0` branch (`35b12a6` and `11672d4` for Ethereum Sepolia, `93e04c0` for the spend-control result, `178b8a6` for the settlement failure reason), not yet published. +**Environment:** macOS, Node `v26.8.1`. + +**Authorization.** None was needed or sought. Every target in every part was Wasit's own +code on our machine: `wasit serve`, or Wasit's own fixtures settling through a +facilitator. No service operated by anyone else was tested. + +Everything checked on this date, in three parts. Each part says what it is for, what was run +against what, and the output as printed; the limits close the file. + +| Part | What it checks | Outcome | +|---|---|---| +| [1. x402 payment checks on Ethereum Sepolia](#part-1-x402-payment-checks-on-ethereum-sepolia) | `X402-06` to `X402-10` pay on Ethereum Sepolia, in Circle's USDC | Lying servers fail; Wasit's fixture, running the SDK's facilitator, passes 10/10, the settlement read back, the payer holding no ETH | +| [2. The client's spend controls](#part-2-the-clients-spend-controls) | What the official x402 client will not pay, and what Wasit reports | A price above $1, or an asset outside the client's list, gets `ERROR (setup)` with nothing sent | +| [3. Settlement failures are named now](#part-3-settlement-failures-are-named-now) | A payment that verified but did not settle | Its reason is read from `PAYMENT-RESPONSE`; the intermittent Base Sepolia failure is the public facilitator's broadcast | + +## Part 1: x402 payment checks on Ethereum Sepolia + **Targets:** `wasit serve --network eip155:11155111`, the paywall that misbehaves on purpose; and Wasit's own Ethereum Sepolia x402 fixture (`packages/core/test/fixtures/x402-evm-sepolia-server.ts`). No public facilitator settles Ethereum Sepolia, so that fixture runs the official SDK's facilitator (`x402Facilitator` with the EVM `exact` scheme, `@x402/evm` 2.28.0) in its own process, paying the gas from a key of ours. All on our machine; the chain is Ethereum Sepolia. -**Environment:** macOS, Node `v26.8.1`. **What this is for.** `X402-06` to `X402-10` now also pay on Ethereum Sepolia, through the same EVM adapter as Base Sepolia. Circle's USDC there @@ -19,10 +35,7 @@ answers `authorizationState` and `nonces`, with EIP-712 name "USDC", version "2" decimals. The same two things have to hold as on the other chains: the checks fail servers that lie, and pass an honest one. -**Authorization.** None was needed or sought. Every target was Wasit's own code on our -machine. No service operated by anyone else was tested. - -## Servers that lie +### Servers that lie Payer: Najmi's Base Sepolia payer, which also held 20 Sepolia USDC and no ETH. Nothing moved: `wasit serve` never settles. @@ -36,7 +49,7 @@ moved: `wasit serve` never settles. Ethereum Sepolia makes a block about every 12 seconds, so the wait for a cited transaction is 10 blocks there (30 on Base Sepolia): 131 seconds here. -## An honest server +### An honest server `wasit test --target http://localhost:3008/protected --network eip155:11155111`: **10/10**, in 20 seconds. @@ -69,7 +82,7 @@ Sepolia RPC: A second run, after the spend-control change (`93e04c0`), passed 10/10 again ([`0x5b351060…67bb`](https://sepolia.etherscan.io/tx/0x5b351060f5597a8240b3f9f4a193a3124cb7c8e0eec238a0af616664341f67bb)). -## The client's spend controls +## Part 2: The client's spend controls The official x402 client pays only the SDK's default assets, at most $1 a payment, unless told otherwise. Circle's Sepolia USDC is not a default asset, so the first @@ -91,7 +104,7 @@ ERROR X402-06 Signature Resubmit Accepted and the paywall's log shows no payment received. -## Settlement failures are named now +## Part 3: Settlement failures are named now A payment that verifies but does not settle used to read `got 402.` with no reason. The official SDK server answers it with a `PAYMENT-RESPONSE` whose `success` is false and diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 1db7200..01a3ce6 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -92,12 +92,8 @@ and what it does and does not establish. Newest first. | Date | File | What it shows | Deliverable | |---|---|---|---| -| 2026-10-06 | [ethereum-sepolia-verification-run](2026-10-06-ethereum-sepolia-verification-run.md) | The x402 payment checks on Ethereum Sepolia (unreleased 0.7.0 branch): `wasit serve`'s lying modes fail and a mainnet request gets nothing sent; Wasit's own fixture, running the SDK's facilitator, passes 10/10 with the settlement read back independently and the payer holding no ETH; a price above the client's $1 cap gets no verdict; a failed settlement now names its reason | Beyond the SOW | -| 2026-10-05 | [wallet-network-and-serve-modes-run](2026-10-05-wallet-network-and-serve-modes-run.md) | `wasit wallet --network` makes and reads the Base Sepolia and Solana devnet payers; `wasit serve`'s `v1-challenge` and `malformed-header` modes, with the official SDK client as the agent: it pays the first as v1 and refuses the second (unreleased 0.7.0 branch) | Beyond the SOW | -| 2026-10-05 | [payer-balance-check-run](2026-10-05-payer-balance-check-run.md) | `X402-06` reads the payer's balance before signing (unreleased 0.7.0 branch): fresh, unfunded payers get `ERROR (setup)` with nothing sent on Stellar, Base Sepolia and Solana devnet, where they used to read as a target's failure or an unexplained error; funded payers pass 10/10 on every fixture | Beyond the SOW | -| 2026-10-05 | [solana-devnet-verification-run](2026-10-05-solana-devnet-verification-run.md) | The x402 payment checks on Solana devnet (unreleased 0.7.0 branch): `wasit serve`'s lying modes fail `X402-06`–`10` and a mainnet request gets nothing sent; Wasit's own fixture passes 10/10 with `X402-06` settled on-chain and read back independently, the payer holding no SOL; the packed core installs and runs alongside TypeScript 5.8, 6 and 7 | Beyond the SOW | -| 2026-10-05 | [x402-negative-checks-run](2026-10-05-x402-negative-checks-run.md) | `X402-08` replay, `X402-09` underpayment and `X402-10` expired authorization (unreleased 0.7.0 branch): Wasit's fixtures pass 10/10 on Stellar and Base Sepolia, each refusal for the reason the check is about; `wasit serve --mode no-settle` fails all of them on both chains | Beyond the SOW | -| 2026-10-05 | [base-sepolia-verification-run](2026-10-05-base-sepolia-verification-run.md) | The x402 payment checks on Base Sepolia (unreleased 0.7.0 branch): servers that skip settlement or cite a missing transaction fail `X402-06` and `X402-07`; a mainnet request gets nothing sent; Wasit's own fixture passes 7/7 with `X402-06` settled on-chain and read back independently, the payer holding no ETH | Beyond the SOW | +| 2026-10-06 | [0.7.0-verification-runs](2026-10-06-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, three parts: the x402 payment checks on Ethereum Sepolia (lying servers fail; Wasit's fixture, running the SDK's facilitator, passes 10/10 with the settlement read back and the payer holding no ETH); a price above the official client's $1 cap gets no verdict; a failed settlement now names its reason | Beyond the SOW | +| 2026-10-05 | [0.7.0-verification-runs](2026-10-05-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, five parts: the x402 payment checks on Base Sepolia (EIP-3009 and Permit2) and Solana devnet (lying servers fail; Wasit's fixtures pass with settlements read back); `X402-08` replay, `X402-09` underpayment and `X402-10` expired authorization (10/10 on Stellar and Base Sepolia); an unfunded payer gets no verdict; `wasit wallet --network` and two new `wasit serve` modes | Beyond the SOW | | 2026-09-30 | [x402-0.6.0-verification-run](2026-09-30-x402-0.6.0-verification-run.md) | 0.6.0's stricter `X402-06` and `X402-07`, A/B against 0.5.0: servers built to skip settlement, misreport it or skip signature checks pass 0.5.0 and fail 0.6.0. `stellar/x402-stellar`'s reference still passes 7/7, with `X402-06` verified on-chain. Registry parity for the published 0.6.0 | D1 | | 2026-09-28 | [authorized-third-party-run](2026-09-28-authorized-third-party-run.md) | Published 0.5.0 against defi-copilot, a third-party service, with its operator's written authorization, as a local instance: 3 pass, 2 diverge from the Stellar spec (x402 v1, non-CAIP-2 network), 2 no verdict, nothing paid. Also 0.5.0 registry parity against Wasit's own fixtures | D2 | | 2026-09-24 | [official-sdk-reference-run](2026-09-24-official-sdk-reference-run.md) | Published 0.4.0 against `stellar/stellar-mpp-sdk`'s own example servers. Charge settles on-chain; an A/B across the SDK's `mppx` 0.10.1 bump shows rejected channel vouchers moving from 402 to 500 ([#82](https://github.com/stellar/stellar-mpp-sdk/issues/82)). Closes 0.4.0 registry parity | D2 | diff --git a/docs/guides/cli.md b/docs/guides/cli.md index cba391f..87fbe37 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -293,7 +293,7 @@ about the challenge itself: `v1-challenge` is a complete v1 challenge `X402-03`. They answer any payment with 402 and log which header it came in. The official x402 SDK client pays `v1-challenge` as v1 and refuses `malformed-header` without paying -([evidence](../evidence/2026-10-05-wallet-network-and-serve-modes-run.md)). +([evidence](../evidence/2026-10-05-0.7.0-verification-runs.md#part-5-payer-keys-for-base-sepolia-and-solana-and-two-new-serve-modes)). ## Reading output diff --git a/packages/core/README.md b/packages/core/README.md index 1473ada..82dd6f6 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -20,7 +20,7 @@ In a project on TypeScript 6 or 7, npm prints `ERESOLVE overriding peer dependen warnings while installing: they come from `@solana/kit` 5, which the Solana support depends on through `@x402/svm`, and which declares an optional peer `typescript ^5`. The install succeeds and core runs; measured with TypeScript 5.8, 6.0 and 7.0 -([evidence](https://github.com/wasit-dev/wasit/blob/main/docs/evidence/2026-10-05-solana-devnet-verification-run.md#installing-alongside-typescript)). +([evidence](https://github.com/wasit-dev/wasit/blob/main/docs/evidence/2026-10-05-0.7.0-verification-runs.md#installing-alongside-typescript)). ## Usage diff --git a/packages/core/test/unit/payer-balance.test.ts b/packages/core/test/unit/payer-balance.test.ts index 73007c1..b1f402c 100644 --- a/packages/core/test/unit/payer-balance.test.ts +++ b/packages/core/test/unit/payer-balance.test.ts @@ -7,7 +7,7 @@ * own contract id, and a Solana payer's balance is every token account it * holds for the mint, or nothing when RPC answers in a shape it does not * know. The Stellar `balance` read and the trustline lookup were run live - * against testnet (runs/2026-10-05-m4-solana.md). + * against testnet (docs/evidence/2026-10-05-0.7.0-verification-runs.md, part 4). */ import assert from "node:assert/strict"; From de8df7ac9a85fea3282027beb2789d6a4df0cd10 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Tue, 6 Oct 2026 01:14:55 +0800 Subject: [PATCH 2/9] docs(site): the site and the security policy match 0.7.0 The CLI demo on the homepage played a 0.6.0 run: seven checks, X402-07 refused with a bare 402. It now plays a real 10/10 run of the 0.7.0 branch against Wasit's own Stellar fixture (2026-10-06), copied line by line: X402-06's settlement and transaction, and X402-07..10 each refused with its reason. Texts that said Stellar alone now say what 0.7.0 does, Stellar first: the homepage's settlement row, the "never trusts the receipt" note and the FAQ on safety; the docs overview; the Requirements page, whose note also claimed there was no mainnet mode at all although stellar:pubnet pays when given an explicit --rpc-url, and whose table now covers X402-06..10 on each chain; the Quick setup page, which said `wasit wallet` had no --network and now shows it for Base Sepolia, Ethereum Sepolia and Solana devnet; and SECURITY.md's testnet section, rendered on the site. The hero's one-line description is unchanged. Co-Authored-By: Claude Opus 5.5 --- SECURITY.md | 6 +++-- frontend/app/page.tsx | 8 +++--- frontend/components/CliDemo.tsx | 37 +++++++++++++++++++++++---- frontend/lib/content.ts | 4 ++- frontend/lib/docs-pages.ts | 44 +++++++++++++++++++++++++-------- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 5a2c850..32195a4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,8 +8,10 @@ keys, what it will and will not do on its own, and how to report a problem. ## Testnet only -Wasit is built and tested for Stellar testnet. Several checks settle real -transactions, and one permanently closes a payment channel. +Wasit is built and tested for testnets: Stellar testnet, and for the x402 +payment checks Base Sepolia, Ethereum Sepolia and Solana devnet, whose mainnets +it does not pay on at all. Several checks settle real transactions, and one +permanently closes a payment channel. Nothing in the tool prevents a `stellar:pubnet` network identifier from parsing, but pubnet deliberately has no default RPC endpoint, so a pubnet run diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 90f4e45..294fb6f 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -46,7 +46,7 @@ type CompareRow = { without: string; with: string }; const COMPARE_ROWS: CompareRow[] = [ { without: "A 200 OK is the whole signal — nothing confirms the payment actually settled.", - with: "Settlement is read from the token contract's own transfer event via Stellar RPC.", + with: "Settlement is read from the token contract's own transfer event via Stellar RPC, and from each chain's own record on Base, Ethereum and Solana testnets.", }, { without: "Payment and channel bugs surface in production, the first time a real payer hits them.", @@ -109,7 +109,7 @@ const FAQ_ITEMS: FaqEntry[] = [ }, { q: "Is this safe to point at a service I don't control?", - a: "Destructive checks require an explicit flag and only run against a channel you name as disposable. Wasit is built for Stellar testnet.", + a: "Destructive checks require an explicit flag and only run against a channel you name as disposable. Wasit is built for testnets: Stellar testnet first, and Base Sepolia, Ethereum Sepolia and Solana devnet for the x402 payment checks.", }, { q: "What's the difference between the CLI and the MCP server?", @@ -331,7 +331,9 @@ export default function Home() {

Steps 5 and 6 are the point of the tool: Wasit calls Stellar RPC directly and checks the transfer event itself, instead of - trusting your service's receipt about what happened on chain.{" "} + trusting your service's receipt about what happened on chain. + On Base, Ethereum and Solana testnets the x402 checks read that + chain's RPC the same way.{" "} Full flow in the docs →

diff --git a/frontend/components/CliDemo.tsx b/frontend/components/CliDemo.tsx index 551c6cd..673c83d 100644 --- a/frontend/components/CliDemo.tsx +++ b/frontend/components/CliDemo.tsx @@ -35,13 +35,15 @@ interface Script { // a target that conforms. Illustrative numbers (channel balance, ledger) // stand in for values that are only known at run time, the same way // "https://api.example.com/paid-endpoint" stands in for a real target URL. -// X402-06's amount, addresses and transaction come from a real testnet run -// of 0.6.0 against Stellar's reference paywall. +// Every x402 line, including X402-06's amount, addresses and transaction and +// the refusal reasons of X402-07..10, is copied from a real testnet run of +// 0.7.0 against Wasit's own Stellar fixture (2026-10-06, +// docs/evidence/2026-10-06-0.7.0-verification-runs.md). const X402_SCRIPT: Script = { command: "wasit test --target https://api.example.com/paid-endpoint", ariaLabel: - "Example wasit test run against a paid endpoint: all seven x402 checks pass, including a payment whose settlement is verified on-chain and a check that a payment with a corrupted authorization signature is refused with HTTP 402.", + "Example wasit test run against a paid endpoint: all ten x402 checks pass, including a payment whose settlement is verified on-chain and four payments the endpoint must refuse (a corrupted signature, a replay, an underpayment and an expired authorization), each refused with HTTP 402 and its reason.", results: [ { status: "PASS", @@ -78,13 +80,38 @@ const X402_SCRIPT: Script = { id: "X402-06", name: "Signature Resubmit Accepted", detail: - "Valid payment accepted (HTTP 200) and settled on-chain for exactly the advertised 100000 base units of CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA to GALVZ57VAY6BE33WYPJMUJ27PFAUDRQ6ATTVMIIF4STTGQTXDBEIBXUI, verified from the transfer event (tx 741c155b620641a7f8845c14e7857201aed67763ea7ca087a7adf921b550a474).", + "Valid payment accepted (HTTP 200) and settled on-chain for exactly the advertised 100000 base units of CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA to GDPAPDZWAKBXUPCNMI4YHAZ7DS7UOUTPGXAFDSWZG4URRMWHFSQTDQBM, verified from the transfer event (tx 19a00c3f0f8ad80d6245cf4431c968b4b5b2f91c78ad037538ed3cce7a53b56f).", }, { status: "PASS", id: "X402-07", name: "Invalid Signature Rejected", - detail: "Payment with a corrupted authorization signature correctly rejected (HTTP 402).", + detail: + "Payment with a corrupted authorization signature correctly rejected (HTTP 402: invalid_exact_stellar_payload_simulation_failed).", + negative: true, + }, + { + status: "PASS", + id: "X402-08", + name: "Payment Replay Rejected", + detail: + "The same payment, sent again, was refused (HTTP 402: invalid_exact_stellar_payload_simulation_failed).", + negative: true, + }, + { + status: "PASS", + id: "X402-09", + name: "Underpayment Rejected", + detail: + "A validly signed payment for 50000 of the advertised 100000 base units was refused (HTTP 402: invalid_exact_stellar_payload_wrong_amount).", + negative: true, + }, + { + status: "PASS", + id: "X402-10", + name: "Expired Authorization Rejected", + detail: + "A payment whose authorization had expired was refused (HTTP 402: invalid_exact_stellar_payload_simulation_failed).", negative: true, }, ], diff --git a/frontend/lib/content.ts b/frontend/lib/content.ts index 92375fd..8c963e4 100644 --- a/frontend/lib/content.ts +++ b/frontend/lib/content.ts @@ -465,7 +465,9 @@ Wasit is an independent protocol-compliance tester for two agentic-payment protocols on Stellar: **x402** and **MPP**. It runs the real payment flow against a live service and verifies the settlement itself — by reading Stellar RPC and the token contract's own transfer event — rather than -trusting whatever the service's response claims happened. +trusting whatever the service's response claims happened. The x402 payment +checks also pay on Base Sepolia, Ethereum Sepolia and Solana devnet, where +the settlement is read from that chain's own record of the transfer. It is not a schema validator. A response can have every field in the right place and still take money without settling it; that gap is exactly what diff --git a/frontend/lib/docs-pages.ts b/frontend/lib/docs-pages.ts index e02b188..85d76a2 100644 --- a/frontend/lib/docs-pages.ts +++ b/frontend/lib/docs-pages.ts @@ -37,11 +37,14 @@ compile, no native dependency, and no service to run. Continuous integration builds and tests on Node 22 and Node 24. Node 26 works too — the full build, typecheck and test suite has been run on it. -> **Note** — Wasit is testnet-only. Every check, and every key it reads, -> targets Stellar testnet. There is no mainnet mode and no flag that -> enables one. +> **Note** — Wasit is built for testnets: Stellar testnet by default, and +> Base Sepolia, Ethereum Sepolia and Solana devnet for the x402 payment +> checks. The one mainnet it can pay on is Stellar's, and only when you +> name \`stellar:pubnet\` and pass your own \`--rpc-url\`: there is no +> default endpoint for it, so it cannot be reached by accident. Do not do +> this; see the [Security Policy](/docs/security/overview). -## A Stellar testnet account +## A testnet account Only for the checks that move money. The read-only x402 checks (\`X402-01\`–\`05\`) need no key, no funds, and no account — that is the @@ -55,12 +58,15 @@ list of which environment variable each check reads — including | You want to run | You need to have | | --- | --- | | \`X402-01\`–\`05\` | Nothing at all | -| \`X402-06\`, \`07\` | A testnet account funded with XLM **and** the asset the target charges in, usually USDC | +| \`X402-06\`–\`10\` on Stellar | A testnet account funded with XLM **and** the asset the target charges in, usually USDC | +| \`X402-06\`–\`10\` on Base Sepolia, Ethereum Sepolia or Solana devnet | A key holding that network's USDC, and nothing else: the facilitator pays the fee | | \`MPP-01\` | A testnet account funded with XLM **and** USDC | | \`MPP-10\`–\`14\` | A commitment signing key — no funds, no account | \`wasit wallet create\` generates any of these and \`wasit wallet fund\` -funds them from Friendbot — see [Quick setup](/docs/start/quick-setup). +funds the Stellar ones from Friendbot; \`--network\` makes the key for +another testnet, whose USDC comes from Circle's faucet — see +[Quick setup](/docs/start/quick-setup). ## AI clients, for the MCP server @@ -186,14 +192,15 @@ Set-Location wasit-testnet New-Item -ItemType File .env \`\`\` -> **Note** — \`.env\` holds Stellar secret keys. On macOS and Linux, +> **Note** — \`.env\` holds testnet secret keys. On macOS and Linux, > \`chmod 600\` makes it readable only by you; Windows files are already > restricted to your user profile by default. Never commit it. ## Generate and fund a key -\`wasit wallet\` does both. It is testnet-only and has no \`--network\` -flag, so there is nothing here that can be pointed at mainnet by mistake. +\`wasit wallet\` does both. It is testnet-only: its \`--network\` takes +testnets and nothing else, so there is nothing here that can be pointed +at mainnet by mistake. \`\`\`bash wasit wallet create --role mpp-charge --fund @@ -232,6 +239,22 @@ faucet exists for Stellar: paste the printed public key into \`WASIT_USDC_DISTRIBUTOR_SECRET\` to an account you already funded that way and every later run sends from it automatically. +### Paying on Base Sepolia, Ethereum Sepolia or Solana devnet + +The x402 payment checks also pay on these testnets, with a key of that +chain's own kind: + +\`\`\`bash +wasit wallet create --role x402 --network eip155:84532 +\`\`\` + +That prints an \`EVM_PRIVATE_KEY=\` line to paste and the address to +fund. The same key pays on Ethereum Sepolia (\`eip155:11155111\`); for +Solana devnet use \`--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1\`, +which prints an \`SVM_PRIVATE_KEY=\` line. The only funding step is USDC +from [faucet.circle.com](https://faucet.circle.com), on that network: the +payer needs no ETH or SOL, since the facilitator pays the fee. + ## Check what you have \`\`\`bash @@ -239,7 +262,8 @@ wasit wallet status \`\`\` Prints each configured role's address and balances, or tells you which -variable is still unset. +variable is still unset. \`wasit wallet status --network eip155:84532\` +shows the x402 payer's USDC on that network instead. ## Run a settling check From 5f0d8b2c7c9ae2a250e44e2eeecbfc02603b18dc Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 09:15:47 +0800 Subject: [PATCH 3/9] fix(core): MPP-01 signs only for the run's network, and pays once MPP-01 read the amount, currency and recipient from the target's challenge, never its network. The SDK's charge client signs for the network the challenge names (resolveNetworkId in @stellar/mpp 0.7.1), through its own default RPC endpoint for that network, pubnet included. A target asking for stellar:pubnet could therefore have a mainnet transfer signed by the payer key. The network is now read the way the SDK reads it (none means testnet), and a challenge for any network but the run's is refused before anything is signed, as ERROR (configuration). The check runs twice: on the unpaid read, and on the exact challenge about to be signed, since mppx requests the target again and pays whatever that answer carries. A network that is not stellar:testnet or stellar:pubnet fails. The payment also goes through the run's RPC endpoint instead of the SDK's default, and the SDK's client cannot reach an http endpoint, so an http one is refused before anything is sent rather than reported as the target's failure. And it is made once: mppx paid up to three times (maxPaymentRetries, default 3) when a target kept answering 402. Tested offline against the official charge server in process; each rule was broken on purpose and a test failed. Co-Authored-By: Claude Opus 5.5 --- packages/core/src/mpp/charge.ts | 107 +++++++- .../core/test/unit/challenge-parsing.test.ts | 23 ++ .../core/test/unit/charge-network.test.ts | 231 ++++++++++++++++++ 3 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 packages/core/test/unit/charge-network.test.ts diff --git a/packages/core/src/mpp/charge.ts b/packages/core/src/mpp/charge.ts index f781bcb..7aec488 100644 --- a/packages/core/src/mpp/charge.ts +++ b/packages/core/src/mpp/charge.ts @@ -4,7 +4,10 @@ * The target's own 402 challenge is the source of truth for what should be * paid. Verifying against it rather than against this run's configuration is * what makes the check meaningful against a third-party service: it asks - * whether the service settled what it advertised, not what we assumed. + * whether the service settled what it advertised, not what we assumed. The + * one term it does not get to choose is the network: Wasit signs only for the + * network the run names, so a target cannot redirect the payer's key to + * mainnet by asking for it. * * Settlement is verified from the CAP-46 `transfer` contract event rather than * the transaction envelope, because those can disagree. The envelope records @@ -26,7 +29,7 @@ import { assertHttpUrl, fetchTarget, } from "../errors.js"; -import { assertMppNetwork, resolveRpcUrl } from "./network.js"; +import { type MppNetwork, assertMppNetwork, isMppNetwork, resolveRpcUrl } from "./network.js"; import { verifySettlement } from "../settlement.js"; const CHECK_ID = "MPP-01"; @@ -44,8 +47,13 @@ export interface ChargeChallenge { readonly amount: bigint; readonly currency: string; readonly recipient: string; + /** The network the SDK's charge client would sign this challenge for. */ + readonly network: MppNetwork; } +/** The SDK's charge client, as `charge()` from @stellar/mpp builds it. */ +type ChargeClient = ReturnType; + function asRecord(value: unknown): Record | undefined { return typeof value === "object" && value !== null ? (value as Record) @@ -133,7 +141,76 @@ export function parseChargeChallenge( ); } - return { amount, currency, recipient }; + return { amount, currency, recipient, network: chargeNetwork(request) }; +} + +/** + * The network @stellar/mpp's charge client signs a challenge for. + * + * The client takes it from the challenge, `methodDetails.network`, and signs a + * challenge that names none for testnet (`resolveNetworkId`, + * dist/shared/validation.js in 0.7.1). This reads it the same way, because + * what matters is the network the signature will be valid on. + */ +function chargeNetwork(request: Record): MppNetwork { + const network = asRecord(request["methodDetails"])?.["network"]; + if (network === undefined || network === null) return "stellar:testnet"; + if (typeof network === "string" && isMppNetwork(network)) return network; + throw new MalformedResponseError( + `Charge challenge names network ${JSON.stringify(network)}. A Stellar ` + + `network is "stellar:testnet" or "stellar:pubnet" (CAIP-2).`, + ); +} + +/** Refuses a challenge for any network but the run's, before anything is signed. */ +function assertRunNetwork(asked: MppNetwork, network: MppNetwork): void { + if (asked !== network) { + throw new ConfigurationError( + `The target asks to be paid on ${asked}, and this run pays on ${network} ` + + `only, so nothing was signed.`, + ); + } +} + +/** + * `method`, refusing to sign a challenge for any network but `network`. + * + * Checking the unpaid challenge is not enough on its own: mppx requests the + * target again and pays the challenge in that answer, which the target is + * free to change. So the check also sits here, on the exact challenge about to + * be signed. Without it a target asking for `stellar:pubnet` gets a mainnet + * transfer signed, sent through the SDK's own default pubnet RPC endpoint. + */ +function onNetwork(method: ChargeClient, network: MppNetwork): ChargeClient { + return { + ...method, + createCredential: async (parameters) => { + assertRunNetwork(chargeNetwork(asRecord(parameters.challenge.request) ?? {}), network); + return method.createCredential(parameters); + }, + }; +} + +/** + * Pays `target` through `method` once at most, and only a challenge for + * `network`. + * + * mppx answers a 402 that follows a payment by paying the new challenge, up to + * `maxPaymentRetries` attempts (3 by default, dist/client/internal/Fetch.js in + * 0.8.14). MPP-01 judges one payment: a target that answers 402 after being + * paid has that 402 reported, rather than being paid twice more. + */ +export async function payCharge( + target: string, + network: MppNetwork, + method: ChargeClient, +): Promise { + const mppx = Mppx.create({ + methods: [onNetwork(method, network)], + polyfill: false, + maxPaymentRetries: 1, + }); + return mppx.fetch(target); } function fail(detail: string): CheckResult[] { @@ -150,7 +227,8 @@ function pass(detail: string): CheckResult[] { * Runs a real payment against the target every time it is called. See * docs/CHECKS.md — this check spends funds by design. * - * @throws {ConfigurationError} Invalid target URL, network, or missing key. + * @throws {ConfigurationError} Invalid target URL, network, or missing key, or + * a challenge for another network than the run's (nothing is signed). * @throws {TargetUnreachableError} The target never answered. * @throws {MalformedResponseError} The target answered, non-conformantly. * @throws {Error} RPC failure — reported as `harness`, never as a target defect. @@ -161,7 +239,7 @@ export async function runMppChargeChecks( const { target, network, payerSecretKey, rpcUrl } = options; assertHttpUrl(target); - assertMppNetwork(network); + const runNetwork = assertMppNetwork(network); if (!payerSecretKey) { throw new ConfigurationError( @@ -181,15 +259,28 @@ export async function runMppChargeChecks( // Resolve the endpoint before spending anything: a bad RPC URL discovered // after settlement would leave money moved and no verdict to show for it. const endpoint = resolveRpcUrl(network, rpcUrl); + // The SDK's charge client builds its RPC client without `allowHttp`, so it + // cannot pay through an http endpoint, though settlement could be read from + // one. Say so before paying, instead of reporting the SDK's refusal as the + // target's failure. + if (endpoint.startsWith("http://")) { + throw new ConfigurationError( + `MPP-01 pays through @stellar/mpp's charge client, which accepts only an ` + + `https RPC endpoint, and ${endpoint} is http. Nothing was sent.`, + ); + } const advertised = await fetchChargeChallenge(target); - - const mppx = Mppx.create({ methods: [charge({ secretKey: payerSecretKey })], polyfill: false }); + assertRunNetwork(advertised.network, runNetwork); let paid: Response; try { - paid = await mppx.fetch(target); + // The payment goes through the run's endpoint too. Left to itself the SDK + // picks its own per network, so `--rpc-url` would cover only the + // settlement read, and pubnet would get a default endpoint after all. + paid = await payCharge(target, runNetwork, charge({ secretKey: payerSecretKey, rpcUrl: endpoint })); } catch (error) { + if (error instanceof ConfigurationError) throw error; // The target was reachable moments ago, so this is the payment path // failing, not the network. Report it as a conformance failure. throw new MalformedResponseError( diff --git a/packages/core/test/unit/challenge-parsing.test.ts b/packages/core/test/unit/challenge-parsing.test.ts index 79ed0b7..8fc5ee6 100644 --- a/packages/core/test/unit/challenge-parsing.test.ts +++ b/packages/core/test/unit/challenge-parsing.test.ts @@ -159,6 +159,29 @@ describe("parseChargeChallenge", () => { assert.throws(() => parseChargeChallenge(challengeWith(undefined)), /no request object/); }); + // The network the SDK's client would sign for: the challenge's own, or + // testnet when it names none (resolveNetworkId in @stellar/mpp 0.7.1). + it("reads the network the SDK would sign for", () => { + const terms = { amount: "1", currency: "C1", recipient: "G1" }; + assert.equal(parseChargeChallenge(challengeWith(terms)).network, "stellar:testnet"); + assert.equal(parseChargeChallenge(challengeWith({ ...terms, methodDetails: {} })).network, "stellar:testnet"); + assert.equal( + parseChargeChallenge(challengeWith({ ...terms, methodDetails: { network: "stellar:pubnet" } })).network, + "stellar:pubnet", + ); + }); + + it("rejects a network that is not a Stellar CAIP-2 identifier", () => { + const terms = { amount: "1", currency: "C1", recipient: "G1" }; + for (const network of ["stellar:mainnet", "eip155:8453", 7]) { + assert.throws( + () => parseChargeChallenge(challengeWith({ ...terms, methodDetails: { network } })), + /names network .*"stellar:testnet" or "stellar:pubnet"/, + String(network), + ); + } + }); + // An operator fixing one field at a time needs the whole list, not the first // thing that happened to be checked. it("names every missing field, not just the first", () => { diff --git a/packages/core/test/unit/charge-network.test.ts b/packages/core/test/unit/charge-network.test.ts new file mode 100644 index 0000000..e45d010 --- /dev/null +++ b/packages/core/test/unit/charge-network.test.ts @@ -0,0 +1,231 @@ +/** + * MPP-01 signs only for the run's network, through the run's RPC endpoint, + * and pays once at most, offline. + * + * The SDK's charge client signs for whatever network the challenge names and + * reaches pubnet through its own default endpoint, so a target asking for + * `stellar:pubnet` would otherwise get a mainnet transfer signed from a key the + * run meant for testnet. The targets here are the official @stellar/mpp charge + * server in process, issuing its own challenges; the RPC endpoint is a local + * stand-in that counts the connections reaching it. Nothing leaves 127.0.0.1. + */ + +import assert from "node:assert/strict"; +import http from "node:http"; +import net from "node:net"; +import { describe, it } from "node:test"; + +import { Keypair } from "@stellar/stellar-sdk"; +import { charge as chargeClient } from "@stellar/mpp/charge/client"; +import { Mppx, Store, charge } from "@stellar/mpp/charge/server"; + +import { classifyCheckError } from "../../src/errors.js"; +import { payCharge, runMppChargeChecks } from "../../src/mpp/charge.js"; +import type { MppNetwork } from "../../src/mpp/network.js"; + +const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + +interface Listening { + readonly url: string; + readonly close: () => Promise; +} + +async function listen(handler: http.RequestListener): Promise { + const server = http.createServer(handler); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as { port: number }; + return { + url: `http://127.0.0.1:${port}`, + close: () => + new Promise((resolve) => { + server.closeAllConnections(); + server.close(() => resolve()); + }), + }; +} + +/** The official charge server's listener for a 0.001 USDC resource on `network`. */ +function officialCharge(network: MppNetwork): http.RequestListener { + const recipient = Keypair.random().publicKey(); + const mppx = Mppx.create({ + secretKey: "wasit-unit-test-challenge-secret", + methods: [charge({ recipient, currency: USDC_SAC, network, store: Store.memory() })], + }); + return async (req, res) => { + const result = await Mppx.toNodeListener(mppx.charge({ amount: "0.001", currency: USDC_SAC, recipient }))(req, res); + if (result.status !== 402) res.end("paid"); + }; +} + +/** + * A charge target answering with `pick(n)`'s official challenge on its n-th + * request, counting requests and those that carried a credential. + */ +async function chargeTarget(pick: (request: number) => MppNetwork) { + const listeners = new Map(); + let requests = 0; + let credentials = 0; + const target = await listen((req, res) => { + requests += 1; + if (req.headers.authorization) credentials += 1; + const network = pick(requests); + if (!listeners.has(network)) listeners.set(network, officialCharge(network)); + return listeners.get(network)!(req, res); + }); + return { ...target, requests: () => requests, credentials: () => credentials }; +} + +/** + * An https RPC address on 127.0.0.1 that counts connections and drops each one. + * + * The SDK's client accepts only https, and the test cannot hold a certificate + * it would trust, so nothing answers: a connection arriving is the evidence + * that the SDK used this endpoint, and dropping it fails the payment before + * anything is signed. + */ +async function countingRpc() { + let calls = 0; + const server = net.createServer((socket) => { + calls += 1; + socket.destroy(); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as net.AddressInfo; + return { + url: `https://127.0.0.1:${port}`, + calls: () => calls, + close: () => new Promise((resolve) => server.close(() => resolve())), + }; +} + +async function rejection(promise: Promise): Promise { + try { + await promise; + } catch (error) { + return error; + } + assert.fail("expected the run to throw"); +} + +describe("MPP-01 and the challenge's network", () => { + for (const [asked, run] of [ + ["stellar:pubnet", "stellar:testnet"], + ["stellar:testnet", "stellar:pubnet"], + ] as const) { + it(`refuses a ${asked} challenge on a ${run} run before anything is signed`, async () => { + const target = await chargeTarget(() => asked); + const rpc = await countingRpc(); + try { + const error = await rejection( + runMppChargeChecks({ target: target.url, network: run, payerSecretKey: Keypair.random().secret(), rpcUrl: rpc.url }), + ); + assert.match(String((error as Error).message), new RegExp(`asks to be paid on ${asked}, and this run pays on ${run} only, so nothing was signed`)); + assert.equal(classifyCheckError(error).kind, "configuration"); + // Stopped at the unpaid read: the SDK never asked the target for a + // challenge to pay, nor the RPC for the payer's account. + assert.equal(target.requests(), 1); + assert.equal(target.credentials(), 0); + assert.equal(rpc.calls(), 0); + } finally { + await target.close(); + await rpc.close(); + } + }); + } + + it("refuses when the challenge it is about to pay switches to another network", async () => { + // Testnet for the unpaid read, pubnet for the request mppx pays. + const target = await chargeTarget((request) => (request === 1 ? "stellar:testnet" : "stellar:pubnet")); + const rpc = await countingRpc(); + try { + const error = await rejection( + runMppChargeChecks({ + target: target.url, + network: "stellar:testnet", + payerSecretKey: Keypair.random().secret(), + rpcUrl: rpc.url, + }), + ); + assert.match(String((error as Error).message), /asks to be paid on stellar:pubnet, and this run pays on stellar:testnet only/); + assert.equal(classifyCheckError(error).kind, "configuration"); + assert.equal(target.requests(), 2); + assert.equal(target.credentials(), 0); + assert.equal(rpc.calls(), 0); + } finally { + await target.close(); + await rpc.close(); + } + }); + + it("builds the payment through the run's RPC endpoint, not the SDK's default", async () => { + const target = await chargeTarget(() => "stellar:testnet"); + const rpc = await countingRpc(); + try { + // The stand-in drops the payer's account lookup, so the payment fails + // before anything is signed; what matters is where the lookup went. + const error = await rejection( + runMppChargeChecks({ + target: target.url, + network: "stellar:testnet", + payerSecretKey: Keypair.random().secret(), + rpcUrl: rpc.url, + }), + ); + assert.ok(rpc.calls() > 0, `the SDK never reached the run's RPC endpoint: ${(error as Error).message}`); + assert.equal(target.credentials(), 0); + } finally { + await target.close(); + await rpc.close(); + } + }); + + it("refuses an http RPC endpoint, which the SDK cannot pay through, before sending anything", async () => { + const target = await chargeTarget(() => "stellar:testnet"); + try { + const error = await rejection( + runMppChargeChecks({ + target: target.url, + network: "stellar:testnet", + payerSecretKey: Keypair.random().secret(), + rpcUrl: "http://127.0.0.1:1", + }), + ); + assert.match(String((error as Error).message), /accepts only an https RPC endpoint, and http:\/\/127\.0\.0\.1:1 is http/); + assert.equal(classifyCheckError(error).kind, "configuration"); + assert.equal(target.requests(), 0); + } finally { + await target.close(); + } + }); +}); + +describe("payCharge", () => { + it("pays once when the target answers 402 again after being paid", async () => { + // A target that ignores every credential, so each paid request is met + // with a fresh challenge, as a target fishing for more payments would. + const official = officialCharge("stellar:testnet"); + let credentials = 0; + const target = await listen((req, res) => { + if (req.headers.authorization) credentials += 1; + delete req.headers.authorization; + return official(req, res); + }); + let signed = 0; + const sdk = chargeClient({ secretKey: Keypair.random().secret() }); + const method = { + ...sdk, + createCredential: async () => { + signed += 1; + return "wasit-unit-test-credential"; + }, + } as typeof sdk; + try { + const response = await payCharge(target.url, "stellar:testnet", method); + assert.equal(response.status, 402); + assert.equal(signed, 1); + assert.equal(credentials, 1); + } finally { + await target.close(); + } + }); +}); From 15e45bfd52296cfeb8317bb56e9dcf7e8a348ef7 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 09:18:23 +0800 Subject: [PATCH 4/9] docs: MPP-01 pays only on the run's network, through its RPC, once The check catalogue, the security policy, the scope boundary, the agent skill and the MCP tool description now say that a target cannot choose the network: MPP-01 refuses a challenge for any network but the run's before anything is signed. The --rpc-url rows say the payment uses the endpoint too, https only. The changelog records the fix and what the previous behaviour was, read from the SDK's source. Evidence for 2026-10-08: an honest testnet target passes with the settlement read back, a target asking for stellar:pubnet gets no verdict and no credential, and each rule broken on purpose fails a test. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ++ SECURITY.md | 11 ++ docs/CHECKS.md | 2 +- docs/design/scope-boundary.md | 3 + .../2026-10-08-0.7.0-verification-runs.md | 151 ++++++++++++++++++ docs/evidence/README.md | 1 + docs/guides/cli.md | 4 +- packages/cli/README.md | 4 +- packages/server/src/index.ts | 8 +- skills/wasit/SKILL.md | 4 +- 10 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 docs/evidence/2026-10-08-0.7.0-verification-runs.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b9733..9ddca69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to Wasit are recorded here. Versions follow [Semantic Versio ## [Unreleased] +**Fixed — `MPP-01` signs only for the network the run names, and pays once.** +`MPP-01` read the amount, currency and recipient from the target's challenge +but not its network, and the SDK's charge client signs for whichever network +the challenge names, through its own default RPC endpoint for it. A target +asking for `stellar:pubnet` could therefore have a mainnet transfer, for the +amount it chose, signed by the payer key if that account holds mainnet funds. +This is read from the SDK's source (`@stellar/mpp` 0.7.1), not run: it would +need mainnet funds. A challenge for any network but the run's is now refused before anything +is signed, on the unpaid read and on the request actually paid, with no verdict +(`ERROR (configuration)`); a network that is not `stellar:testnet` or +`stellar:pubnet` fails. The payment now goes through the run's RPC endpoint +(`--rpc-url`, MCP: `rpcUrl`) rather than the SDK's default, so an http endpoint, +which the SDK's client refuses, now stops the run before anything is sent. And +it is made once: the SDK's client paid up to three times when a target kept +answering 402 after being paid. + **Changed — `X402-04` and `X402-05` read every payment option, on any chain.** Both read only `accepts[0]`, and `X402-05` accepted only `stellar:testnet` and `stellar:pubnet`, so a conformant x402 service on another chain failed it, and diff --git a/SECURITY.md b/SECURITY.md index 32195a4..e046c39 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,6 +19,17 @@ has to be configured on purpose rather than reached by accident. **Do not do this.** No check in the catalogue has been validated against mainnet conditions, and the destructive check is irreversible wherever it runs. +Nor can a target choose the network. The payment checks sign only for the +network the run names: `X402-06` to `X402-10` pay only an option on +`--network`, and `MPP-01` refuses a challenge for any other network before +anything is signed, both on its unpaid read and on the request it pays. `MPP-01` +pays through the run's RPC endpoint, not the SDK's default for the network, and +pays once per run. Before 0.7.0, `MPP-01` let the SDK sign for whichever +network the challenge named, through the SDK's default endpoint for it, so a +target asking for `stellar:pubnet` could have a mainnet transfer signed by the +payer key if that account holds mainnet funds (read from the SDK's source, not +run). Use a payer key that has never held mainnet funds. + ## Authorization **Only run Wasit against a service you own, or one whose operator has given you diff --git a/docs/CHECKS.md b/docs/CHECKS.md index 14ac544..fc9c578 100644 --- a/docs/CHECKS.md +++ b/docs/CHECKS.md @@ -89,7 +89,7 @@ settlement could be verified. | ID | Check Name | Spec Reference | What It Checks | Pass Criteria | |---|---|---|---|---| -| `MPP-01` | Charge Settlement On-Chain | MPP Charge Guide; CAP-46 transfer events | The charge settles on-chain for exactly what the target advertised | The target's own 402 challenge is read first, unpaid, capturing the advertised `amount` (base units), `currency` and `recipient`. After payment, Stellar RPC `getTransaction` confirms the transaction referenced by the `Payment-Receipt` header succeeded, and its CAP-46 `transfer` contract event shows exactly one balance change: the advertised amount, to the advertised recipient, emitted by the advertised token contract, sent from this run's own payer. Verifying the **event** rather than the transaction envelope means a token contract whose `transfer` moves a different amount than its arguments claim is still caught. Verifying the **sender** means a target cannot satisfy the check by referencing some pre-existing transaction it did not cause. Verified against `@stellar/mpp@0.7.1` — mode `"pull"` (default): `onProgress` only fires through `"signed"`, so the settled tx reference comes from `Payment-Receipt`, not `onProgress`. The transfer event's data is read in both forms the protocol defines: a bare `i128` amount (CAP-46), and the map `{ amount, to_muxed_id }` that CAP-67 emits for a muxed destination, whose base address is in the topic. An advertised muxed recipient (`M...`) therefore matches only when both the base account and the muxed id match. A transaction RPC cannot find is a FAIL only after RPC has closed ten more ledgers without it; an RPC that stops advancing before then gives no verdict (`ERROR (harness)`), since its silence says nothing about the target. | +| `MPP-01` | Charge Settlement On-Chain | MPP Charge Guide; CAP-46 transfer events | The charge settles on-chain for exactly what the target advertised | The target's own 402 challenge is read first, unpaid, capturing the advertised `amount` (base units), `currency` and `recipient`, and the network it names in `methodDetails.network` (one naming none is signed for testnet, as the SDK's client does). Wasit signs only for the run's network: a challenge for another, whether on the unpaid read or on the request actually paid, gets no verdict (`ERROR (configuration)`) and nothing is signed, and a network that is not `stellar:testnet` or `stellar:pubnet` fails. The payment goes through the run's RPC endpoint, which must be https since the SDK's client accepts no other, and is made once: a target that answers 402 after being paid fails with that status rather than being paid again. After payment, Stellar RPC `getTransaction` confirms the transaction referenced by the `Payment-Receipt` header succeeded, and its CAP-46 `transfer` contract event shows exactly one balance change: the advertised amount, to the advertised recipient, emitted by the advertised token contract, sent from this run's own payer. Verifying the **event** rather than the transaction envelope means a token contract whose `transfer` moves a different amount than its arguments claim is still caught. Verifying the **sender** means a target cannot satisfy the check by referencing some pre-existing transaction it did not cause. Verified against `@stellar/mpp@0.7.1` — mode `"pull"` (default): `onProgress` only fires through `"signed"`, so the settled tx reference comes from `Payment-Receipt`, not `onProgress`. The transfer event's data is read in both forms the protocol defines: a bare `i128` amount (CAP-46), and the map `{ amount, to_muxed_id }` that CAP-67 emits for a muxed destination, whose base address is in the topic. An advertised muxed recipient (`M...`) therefore matches only when both the base account and the muxed id match. A transaction RPC cannot find is a FAIL only after RPC has closed ten more ledgers without it; an RPC that stops advancing before then gives no verdict (`ERROR (harness)`), since its silence says nothing about the target. | **Note on MPP-01's cost (Week 2).** MPP-01 is **not** destructive: nothing is permanently ended and the check can be run again. But it is not free and not diff --git a/docs/design/scope-boundary.md b/docs/design/scope-boundary.md index e548a23..d719664 100644 --- a/docs/design/scope-boundary.md +++ b/docs/design/scope-boundary.md @@ -33,6 +33,9 @@ Every check assumes testnet. `MPP-01`, `X402-06`, and `MPP-13` move real value, and `MPP-13` destroys a channel. Nothing about the tool prevents a pubnet network identifier from parsing, but pubnet deliberately has no default RPC endpoint, so such a run must be configured deliberately rather than by accident. +A target cannot choose the network for the run either: the payment checks sign +only for the network the run names, and `MPP-01` refuses a challenge for any +other before anything is signed. Do not run destructive or costly checks against infrastructure you do not own. diff --git a/docs/evidence/2026-10-08-0.7.0-verification-runs.md b/docs/evidence/2026-10-08-0.7.0-verification-runs.md new file mode 100644 index 0000000..2442995 --- /dev/null +++ b/docs/evidence/2026-10-08-0.7.0-verification-runs.md @@ -0,0 +1,151 @@ +# Wasit 0.7.0 Verification, 2026-10-08: MPP-01 Pays Only on the Run's Network + +**Date:** 2026-10-08 +**Wasit:** a build of the local `release/0.7.0` branch at `5f0d8b2`, not yet published. +**Environment:** macOS, Node `v26.8.1`, `@stellar/mpp` 0.7.1, `mppx` 0.8.14. + +**Authorization.** None was needed or sought. Both targets were run by us on our machine: +Wasit's own charge fixture, and the official `@stellar/mpp` charge server in a process of +its own. No service operated by anyone else was tested. + +Everything checked on this date, in one part. It says what it is for, what was run against +what, and the output as printed; the limits close the file. + +| Part | What it checks | Outcome | +|---|---|---| +| [1. MPP-01 and the challenge's network](#part-1-mpp01-and-the-challenges-network) | `MPP-01` signs only for the network the run names, pays through the run's RPC endpoint, and pays once | An honest testnet target passes, the settlement read back; a target asking for `stellar:pubnet` gets `ERROR (configuration)` and no credential | + +## Part 1: MPP01 and the challenge's network + +**Build:** `5f0d8b2`. The CLI was built from this code (`packages/core/src` and +`packages/cli/src`), four of whose source comments have since been shortened; nothing +else differs. + +**Targets:** Wasit's charge fixture (`packages/core/test/fixtures/mpp-charge-server.ts`, +port 3002), which is the official `@stellar/mpp` charge server on `stellar:testnet`; and +the official `@stellar/mpp` charge server configured with `network: "stellar:pubnet"`, on +`127.0.0.1:3099`, which was never paid. Both on our machine; the only chain touched is +Stellar testnet. + +**What this is for.** Up to this build, `MPP-01` read the `amount`, `currency` and +`recipient` from the target's challenge, but not its network. The SDK's charge client +takes the network from the challenge and builds its RPC client from it +(`dist/charge/client/Charge.js` in `@stellar/mpp` 0.7.1): + +```js +const network = resolveNetworkId(request.methodDetails?.network); +const resolvedRpcUrl = rpcUrl ?? SOROBAN_RPC_URLS[network]; +const networkPassphrase = NETWORK_PASSPHRASE[network]; +``` + +`SOROBAN_RPC_URLS` (`dist/constants.js`) has a default pubnet endpoint, and Wasit passed +no `rpcUrl`. So a target asking for `stellar:pubnet` could have a mainnet transfer, for the +amount it chose, signed by the payer key if that account holds mainnet funds. That follows +from the source; it was not run (see Limits). Separately, mppx pays a 402 that follows a +payment again, up to `maxPaymentRetries` attempts, 3 by default +(`dist/client/internal/Fetch.js` in `mppx` 0.8.14). + +In `5f0d8b2`, `MPP-01` reads the network the SDK would sign for (one naming none is +testnet, as `resolveNetworkId` does) and refuses any network but the run's. It does so +before anything is signed, both on its unpaid read and on the exact challenge about to be +signed, since mppx requests the target again and pays whatever that answer carries. The +payment goes through the run's RPC endpoint, which must be https, and is made once. + +### An honest target pays once + +``` +$ node packages/cli/dist/index.js mpp-charge --target http://localhost:3002/data +MPP-01 settles a real payment. If the target is reachable, testnet funds will move. + +PASS MPP-01 Charge Settlement On-Chain + Settled on-chain for exactly the advertised 10000 base units of CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA to GALVZ57VAY6BE33WYPJMUJ27PFAUDRQ6ATTVMIIF4STTGQTXDBEIBXUI, verified from the transfer event (tx c7a758b3e210ea5775a2004ce7cb3915a65ff1e8a2520c8b7f0fe3bcc60c0c40). + +1 passed. +``` + +Exit code 0. Read back independently from Horizon testnet +([`c7a758b3…0c40`](https://stellar.expert/explorer/testnet/tx/c7a758b3e210ea5775a2004ce7cb3915a65ff1e8a2520c8b7f0fe3bcc60c0c40)): +successful, ledger 5079715, 2026-10-08T01:16:02Z, one operation, and these effects: + +``` +account_debited GDHQTUA2P5OO7EXSZ5MAUID4HUFM3H7XP6B5GJNQSRX3N675KCFNAFVK 0.0010000 USDC GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 +account_credited GALVZ57VAY6BE33WYPJMUJ27PFAUDRQ6ATTVMIIF4STTGQTXDBEIBXUI 0.0010000 USDC GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 +``` + +### A target asking for pubnet gets nothing signed + +The challenge the official server issued, its `request` decoded (the random recipient +elided): + +```json +{"amount":"10000","currency":"CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA","methodDetails":{"credentialTypes":["transaction","signedHash"],"network":"stellar:pubnet"},"recipient":"G…(random)"} +``` + +The run, on the default `stellar:testnet`: + +``` +$ node packages/cli/dist/index.js mpp-charge --target http://127.0.0.1:3099/data +MPP-01 settles a real payment. If the target is reachable, testnet funds will move. + +ERROR MPP-01 Charge Settlement On-Chain + Check could not run (configuration): The target asks to be paid on stellar:pubnet, and this run pays on stellar:testnet only, so nothing was signed. + +0 passed, 1 could not run. + +No verdict: some checks never reached the target or the run is misconfigured. This is not a statement about the target's conformance. +``` + +Exit code 2. What the target saw, logged by the target itself: + +``` +target: request 1, credentials 0 +target: request 2, credentials 0 +``` + +Request 1 was a `curl` reading the challenge header for the decode above; request 2 was +`MPP-01`'s unpaid read. No request carried a credential. + +### Offline tests, and each rule broken on purpose + +`packages/core/test/unit/charge-network.test.ts` runs `MPP-01` against the official +charge server in process, with an https RPC address on `127.0.0.1` that counts the +connections reaching it. Nothing leaves the machine. Two more tests in +`challenge-parsing.test.ts` cover the parsing. Core: 246 tests, all passing. + +``` +✔ refuses a stellar:pubnet challenge on a stellar:testnet run before anything is signed +✔ refuses a stellar:testnet challenge on a stellar:pubnet run before anything is signed +✔ refuses when the challenge it is about to pay switches to another network +✔ builds the payment through the run's RPC endpoint, not the SDK's default +✔ refuses an http RPC endpoint, which the SDK cannot pay through, before sending anything +✔ pays once when the target answers 402 again after being paid +✔ reads the network the SDK would sign for +✔ rejects a network that is not a Stellar CAIP-2 identifier +``` + +Each rule was then broken in the source, one at a time, and the tests rerun: + +| Rule broken | Test that failed | +|---|---| +| No check on the challenge about to be signed | refuses when the challenge it is about to pay switches to another network | +| No check on the unpaid read | both "refuses a … challenge on a … run" tests (the target got a second request) | +| mppx's default of 3 payment attempts | pays once when the target answers 402 again after being paid | +| The SDK picks its own RPC endpoint | builds the payment through the run's RPC endpoint, not the SDK's default | +| No refusal of an http endpoint | refuses an http RPC endpoint, which the SDK cannot pay through, before sending anything | +| A challenge naming no network read as pubnet | reads the network the SDK would sign for | +| The refusal reported as the target's failure | refuses when the challenge it is about to pay switches to another network | +| Any string accepted as a network | rejects a network that is not a Stellar CAIP-2 identifier | + +## Limits + +- The behaviour before `5f0d8b2` was not reproduced live. A run would have built a payment + through the SDK's default pubnet RPC endpoint, which someone else operates, and signing + needs a payer account on mainnet. It is read from the SDK's source, and the two + mutations that remove the network checks show the SDK going on to build the payment. +- A challenge that switches network between the unpaid read and the payment, a 402 after + payment, and an http RPC endpoint were exercised offline only, against the official + server in process, not through a CLI run. +- The mutation that lets the SDK pick its own endpoint sent one account lookup, for a + random key, to the SDK's default testnet endpoint (`soroban-testnet.stellar.org`, also + Wasit's own default). +- Unreleased: this ran from the branch, not from npm. diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 01a3ce6..0d91a64 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -92,6 +92,7 @@ and what it does and does not establish. Newest first. | Date | File | What it shows | Deliverable | |---|---|---|---| +| 2026-10-08 | [0.7.0-verification-runs](2026-10-08-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, one part: `MPP-01` signs only for the network the run names, pays through the run's RPC endpoint, and pays once (an honest testnet target passes, the settlement read back; a target asking for `stellar:pubnet` gets no verdict and no credential) | Beyond the SOW | | 2026-10-06 | [0.7.0-verification-runs](2026-10-06-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, three parts: the x402 payment checks on Ethereum Sepolia (lying servers fail; Wasit's fixture, running the SDK's facilitator, passes 10/10 with the settlement read back and the payer holding no ETH); a price above the official client's $1 cap gets no verdict; a failed settlement now names its reason | Beyond the SOW | | 2026-10-05 | [0.7.0-verification-runs](2026-10-05-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, five parts: the x402 payment checks on Base Sepolia (EIP-3009 and Permit2) and Solana devnet (lying servers fail; Wasit's fixtures pass with settlements read back); `X402-08` replay, `X402-09` underpayment and `X402-10` expired authorization (10/10 on Stellar and Base Sepolia); an unfunded payer gets no verdict; `wasit wallet --network` and two new `wasit serve` modes | Beyond the SOW | | 2026-09-30 | [x402-0.6.0-verification-run](2026-09-30-x402-0.6.0-verification-run.md) | 0.6.0's stricter `X402-06` and `X402-07`, A/B against 0.5.0: servers built to skip settlement, misreport it or skip signature checks pass 0.5.0 and fail 0.6.0. `stellar/x402-stellar`'s reference still passes 7/7, with `X402-06` verified on-chain. Registry parity for the published 0.6.0 | D1 | diff --git a/docs/guides/cli.md b/docs/guides/cli.md index 87fbe37..583c70a 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -192,8 +192,8 @@ wasit mpp-charge --target [options] |---|---|---| | `--target ` | required | The paid resource | | `--payer-key ` | `MPP_PAYER_SECRET` | Secret key, `S...` | -| `--network ` | `MPP_STELLAR_NETWORK` | CAIP-2 | -| `--rpc-url ` | testnet default | Required for pubnet | +| `--network ` | `MPP_STELLAR_NETWORK` | CAIP-2. Only a challenge on this network is paid | +| `--rpc-url ` | testnet default | Used for the payment and the settlement read; https only. Required for pubnet | | `--json` | off | Machine-readable output (see below) instead of formatted text | **Every run settles a real payment.** There is no read-only mode: charge mode diff --git a/packages/cli/README.md b/packages/cli/README.md index aeec585..079a612 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -68,8 +68,8 @@ wasit mpp-charge --target [options] |---|---|---| | `--target ` | required | The paid resource | | `--payer-key ` | `MPP_PAYER_SECRET` | Secret key, `S...` | -| `--network ` | `MPP_STELLAR_NETWORK` | CAIP-2 | -| `--rpc-url ` | testnet default | Required for pubnet | +| `--network ` | `MPP_STELLAR_NETWORK` | CAIP-2. Only a challenge on this network is paid | +| `--rpc-url ` | testnet default | Used for the payment and the settlement read; https only. Required for pubnet | **Every run settles a real payment.** Charge mode has no dry run — a settlement that never happened cannot be verified on-chain. diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2000571..215ab98 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -271,11 +271,15 @@ server.registerTool( "NOT IDEMPOTENT - every call settles a real payment and spends testnet " + "funds from the payer key. Repeated calls repeatedly spend. The payer " + "key is read from MPP_PAYER_SECRET in this server's environment. " + - "Testnet only.", + "Pays only a challenge on `network`: one for any other network is " + + "refused before anything is signed. Testnet only.", inputSchema: { target: z.string().describe("Full URL of the paid resource, including scheme"), network: z.string().optional().describe('CAIP-2 network id, default "stellar:testnet"'), - rpcUrl: z.string().optional().describe("Override the Soroban RPC endpoint"), + rpcUrl: z + .string() + .optional() + .describe("Override the Soroban RPC endpoint (https), used for the payment and the settlement read"), }, outputSchema: runOutputShape, annotations: { diff --git a/skills/wasit/SKILL.md b/skills/wasit/SKILL.md index d17dda2..d74a484 100644 --- a/skills/wasit/SKILL.md +++ b/skills/wasit/SKILL.md @@ -17,7 +17,9 @@ that gap the way `stellar-anchor-tests` fills it for SEP-24/31 anchors, and in doing so has already surfaced three reproducible defects in the official SDKs (`stellar-mpp-sdk#66`, `#67`, `#70`), not hypothetical bugs. -Testnet only. Mainnet is explicitly out of scope. Not a security audit: no +Testnet only. Mainnet is explicitly out of scope, and a target cannot opt a run +into it: Wasit signs only for the network the run names, and a challenge for +any other gets nothing signed. Not a security audit: no source or bytecode is read. Complementary to Scout, the Certora Sunbeam Prover, Komet, and OpenZeppelin's Soroban detectors, not a replacement for any of them. From d529603f2b412bb9481ea32af1a208513c2e5d7d Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 09:37:48 +0800 Subject: [PATCH 5/9] fix(cli): wasit wallet's help names every testnet it covers The command's one-line description still said Stellar testnet wallets, while its subcommands take --network for Base Sepolia, Ethereum Sepolia and Solana devnet, where they manage the x402 payer. Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/wallet-command.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/wallet-command.ts b/packages/cli/src/wallet-command.ts index 525e31a..c59d2de 100644 --- a/packages/cli/src/wallet-command.ts +++ b/packages/cli/src/wallet-command.ts @@ -286,7 +286,9 @@ async function printPayerStatus(network: string, jsonMode: boolean): Promise<0 | export function registerWalletCommand(program: Command): void { const wallet = program .command("wallet") - .description("Generate, fund, and inspect disposable Stellar testnet wallets"); + .description( + "Generate, fund, and inspect disposable testnet wallets: Stellar's payers, and the x402 payer on Base Sepolia, Ethereum Sepolia and Solana devnet", + ); wallet .command("status") From cb067d1d3076e8eccbe03059391e3f5ca6886895 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 14:04:47 +0800 Subject: [PATCH 6/9] docs(readme): 0.7.0, without the 0.6.0 captures The README described 0.6.0: Stellar only, thirteen checks, four fixtures, five subcommands, and a passing run of seven checks. It now says what 0.7.0 does: x402 on Stellar testnet, Base Sepolia, Ethereum Sepolia and Solana devnet, MPP on Stellar, sixteen checks, the eight fixtures and what each needs in .env, wasit serve and wallet --network, and example output from a 0.7.0 run, a pass and a failure with its Fix line. The CLI and MCP captures of 0.6.0 at the top are gone, and with them the paragraph about them: the release video covers that, and the files stay in docs/media for the evidence that links them. The link to the channel-mode note pointed at an anchor that never existed; it now opens the section the note is in. The roadmap item about a third-party run was already done, as the Status table says; it gives way to BNB Smart Chain testnet payments. Co-Authored-By: Claude Opus 5.5 --- README.md | 128 +++++++++++++++++++++++++----------------------------- 1 file changed, 59 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 49b194e..30f5c98 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,18 @@ # Wasit -**Protocol-compliance testing for x402 and MPP on Stellar.** +**Protocol-compliance testing for x402 and MPP.** Wasit runs the real payment flow against a live service, not a mock of it, and it is not a schema validator: a response can have every field in the right place -and still take money without settling it. Settlement is verified on-chain — from -the token contract's own transfer event rather than the service's own response — -for both MPP charge payments and x402 payments. +and still take money without settling it. Settlement is verified on-chain, from +the chain's own record of the transfer rather than the service's response: the +token contract's transfer event on Stellar, the token's `Transfer` log on Base +and Ethereum, the token balances on Solana. x402 is tested on Stellar testnet, +Base Sepolia, Ethereum Sepolia and Solana devnet; MPP on Stellar testnet. [![CI](https://github.com/wasit-dev/wasit/actions/workflows/ci.yml/badge.svg)](https://github.com/wasit-dev/wasit/actions/workflows/ci.yml) -[![Stellar](https://img.shields.io/badge/Stellar-Testnet-7c3aed)](https://stellar.org) +[![Testnets](https://img.shields.io/badge/Testnets-Stellar%20%C2%B7%20Base%20%C2%B7%20Ethereum%20%C2%B7%20Solana-7c3aed)](docs/guides/cli.md) [![x402](https://img.shields.io/badge/Protocol-x402%20v2-0891b2)](https://x402.org) [![MPP](https://img.shields.io/badge/Protocol-MPP-111827)](https://paymentauth.org) [![MCP](https://img.shields.io/badge/Interface-CLI%20%2B%20MCP-16a34a)](https://modelcontextprotocol.io) @@ -32,24 +34,6 @@ for both MPP charge payments and x402 payments. - - - - - -
- The wasit CLI running the x402 checks against a local fixture: five read-only, then all seven, with the payment's settlement verified on-chain -
- CLI · npx @wasit-dev/cli@0.6.0
catalogue, read-only 5/5, then 7/7 with the settlement verified on-chain
-
- The same checks run as MCP tool calls from Claude Code: x402 7/7 with the settlement verified on-chain, then MPP charge and channel conformant -
- MCP · the same checks from Claude Code
x402, then MPP charge and channel in one session
-
- -

Both ran the published 0.6.0 packages against the local fixtures in -Trying It. Click either one for full size.

- --- ## Table of Contents @@ -69,8 +53,9 @@ for both MPP charge payments and x402 payments. ## How It Works -Wasit talks to two things: the service under test, over HTTP, and the Stellar -network, over RPC. It never trusts the first about what happened on the second. +Wasit talks to two things: the service under test, over HTTP, and the chain the +payment settles on, over RPC. It never trusts the first about what happened on +the second. ```mermaid flowchart TB @@ -85,7 +70,7 @@ flowchart TB SVC["Your running service"] end - CHAIN["Stellar RPC
settlement, contract events"] + CHAIN["Chain RPC
Stellar, Base, Ethereum, Solana testnets"] CLI --> CORE MCP --> CORE @@ -100,8 +85,8 @@ flowchart TB Step 5 is the point of the tool. The challenge in step 2 states what the service wants paid; the receipt in step 4 states what it claims happened. Wasit compares -both against what the chain actually recorded — including the token contract's -own transfer event, not just the transaction it was asked to make. +both against what the chain actually recorded — including the token's own +record of the transfer, not just the transaction it was asked to make. The CLI and the MCP server are thin adapters over the same suite functions. They cannot disagree about the same target, because there is only one implementation @@ -175,7 +160,7 @@ surface. returns an identical HTTP 402 body. The SDK defines precise error types for each of these and none are reachable, because of a class-hierarchy mismatch between two packages. An operator debugging a rejected payment cannot tell which rule -they broke. See [docs/CHECKS.md](docs/CHECKS.md#note-on-error-granularity-week-2) +they broke. See [docs/CHECKS.md](docs/CHECKS.md#mpp--channel-mode) and the full write-up in [docs/findings/upstream-sdk.md](docs/findings/upstream-sdk.md). Filed upstream as [stellar-mpp-sdk#66](https://github.com/stellar/stellar-mpp-sdk/issues/66); independently confirmed by [RouteDock's fix](https://github.com/winsznx/routedock/pull/241) @@ -196,12 +181,12 @@ Wasit exists so these are found by a tool, before they are found by a user. | Area | Status | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| x402 read-only checks (`X402-01`–`05`) | Done, verified against a real facilitator | -| x402 payment checks (`X402-06`–`10`) | Done, settlement verified from contract events | -| MPP charge mode (`MPP-01`) | Done, settlement verified from contract events | +| x402 read-only checks (`X402-01`–`05`) | Done, on a challenge from any chain; verified against a real facilitator | +| x402 payment checks (`X402-06`–`10`) | Done on Stellar testnet, Base Sepolia, Ethereum Sepolia and Solana devnet, settlement verified from each chain's own record | +| MPP charge mode (`MPP-01`) | Done, settlement verified from contract events; pays only on the run's network | | MPP channel mode (`MPP-10`–`14`) | Done, including destructive close | -| CLI | Done — five subcommands plus an interactive dashboard | -| Testnet wallet tooling | Done — `wasit wallet` create/fund/status, testnet only | +| CLI | Done — six subcommands, `wasit serve` among them, plus an interactive dashboard | +| Testnet wallet tooling | Done — `wasit wallet` create/fund/status on Stellar testnet, and for the x402 payer on Base Sepolia, Ethereum Sepolia and Solana devnet | | MCP server | Done, three tools + one behind an opt-in | | Error taxonomy and exit codes | Done | | Wasit's own test suite | Done — offline (no keys, no target, no network); type-check and tests both run in CI | @@ -210,7 +195,7 @@ Wasit exists so these are found by a tool, before they are found by a user. | Published to npm | Yes — `@wasit-dev/core`, `@wasit-dev/cli`, `@wasit-dev/server` | | Mainnet | Out of scope — see [Design Notes](#design-notes) | -Thirteen checks are implemented and reachable from both front ends. Every one is +Sixteen checks are implemented and reachable from both front ends. Every one is traced to a written spec clause in [docs/CHECKS.md](docs/CHECKS.md); a check that cannot be traced is out of scope by construction. @@ -237,12 +222,15 @@ Running `wasit` with no arguments in a terminal opens an interactive dashboard — the same checks, plus a check catalogue browser and a testnet wallet screen, driven by arrow keys. Piped or in CI it prints help instead, so nothing that scripts Wasit today changes behaviour. `wasit wallet create|fund|status` -is the same wallet tooling as plain subcommands: it generates testnet keys, -funds them through Friendbot, and opens a USDC trustline. Both are covered in +is the same wallet tooling as plain subcommands: on Stellar it generates +testnet keys, funds them through Friendbot, and opens a USDC trustline; with +`--network` it generates the x402 payer for Base Sepolia, Ethereum Sepolia or +Solana devnet and shows its USDC. `wasit serve` runs a local x402 paywall that +misbehaves on purpose, to test an agent that pays. All of it is covered in [docs/guides/cli.md](docs/guides/cli.md). -The checks that settle a real testnet payment — `X402-06`, `X402-07`, `MPP-01`, -and the channel suite — need a funded testnet account. +The checks that settle a real testnet payment — `X402-06` to `X402-10`, +`MPP-01`, and the channel suite — need a funded testnet account. [docs/guides/configuration.md](docs/guides/configuration.md) explains each value and where to get it. For the MCP server, see [docs/guides/mcp.md](docs/guides/mcp.md). @@ -250,7 +238,7 @@ and where to get it. For the MCP server, see ## Building from source Only needed to change Wasit itself, or to run the bundled fixture servers below. -Developed and verified on Node **v24.18.0**. +Developed and verified on Node **v26.8.1**; CI runs Node 22 and 24. ```bash git clone https://github.com/wasit-dev/wasit.git @@ -279,10 +267,14 @@ would: the CLI's own binary and the MCP server over stdio. It runs in CI too. ## Trying It -Four fixture servers are bundled. Three are real servers built on the official +Eight fixture servers are bundled. Seven are real servers built on the official SDKs, not mocks — testing against a mock would mean testing against our own assumptions, and one of the defects listed above was found precisely because the -fixtures are real. The fourth is a deliberate misbehaver, described below. +fixtures are real. The other is a deliberate misbehaver, described below. The +Base Sepolia, Ethereum Sepolia and Solana devnet fixtures start only when `.env` +names their payee (`EVM_PAYEE_ADDRESS`, `SVM_PAYEE_ADDRESS`). The Ethereum Sepolia +one also needs `EVM_FACILITATOR_PRIVATE_KEY`: it runs the official SDK's +facilitator itself, which pays the gas from that key. Start them all at once: @@ -309,9 +301,13 @@ npx tsx packages/core/test/fixtures/x402-real-server.ts # :3001/prot npx tsx packages/core/test/fixtures/mpp-charge-server.ts # :3002/data npx tsx packages/core/test/fixtures/mpp-channel-server.ts # :3003/data npx tsx packages/core/test/fixtures/mpp-channel-refusing-server.ts # :3004/data +npx tsx packages/core/test/fixtures/x402-evm-server.ts # :3005/protected, Base Sepolia, EIP-3009 +npx tsx packages/core/test/fixtures/x402-evm-permit2-server.ts # :3006/protected, Base Sepolia, Permit2 +npx tsx packages/core/test/fixtures/x402-svm-server.ts # :3007/protected, Solana devnet +npx tsx packages/core/test/fixtures/x402-evm-sepolia-server.ts # :3008/protected, Ethereum Sepolia ``` -The fourth issues valid channel challenges and then refuses every credential. It +The refusing one (`:3004`) issues valid channel challenges and then refuses every credential. It is not a conformance target and must never be used as one. It exists to reproduce, on demand, a run where the precondition a channel check needs cannot be established, so the reporting path for that case can be exercised without @@ -327,6 +323,9 @@ node packages/cli/dist/index.js test --target http://localhost:3001/protected -- # x402 — full flow, settles a real testnet payment node packages/cli/dist/index.js test --target http://localhost:3001/protected +# x402 on Base Sepolia — the same checks, paid in Base Sepolia USDC +node packages/cli/dist/index.js test --target http://localhost:3005/protected --network eip155:84532 + # MPP charge — settles a real testnet payment node packages/cli/dist/index.js mpp-charge --target http://localhost:3002/data @@ -335,45 +334,37 @@ node packages/cli/dist/index.js mpp-channel --target http://localhost:3003/data ``` -The two captures at the top of this README ran against these fixtures, from the -published packages rather than a local build: the CLI capture opens with -`--version` printing `0.6.0`, and the MCP session started the server through -`npx -y @wasit-dev/server@0.6.0`. The -raw asciinema captures are [`docs/media/x402-0.6.0.cast`](docs/media/x402-0.6.0.cast) -and [`docs/media/mcp-0.6.0.cast`](docs/media/mcp-0.6.0.cast), replayable with -`asciinema play`. The 0.4.0 captures submitted as SOW evidence stay available: -the CLI at [`docs/media/d1-x402.gif`](docs/media/d1-x402.gif), the MCP session in -the [v0.4.0 release](https://github.com/wasit-dev/wasit/releases/tag/v0.4.0). - -The MCP session is worth a second look. It calls `wasit_mpp_charge_test` -and then `wasit_mpp_channel_test` back to back in a single server process. Until -0.4.0 that sequence failed every time, for a reason that had nothing to do with -the target being tested: -[the write-up](docs/evidence/2026-09-17-cross-check-isolation-run.md). - -A passing run looks like this: +A passing run looks like this, from the Stellar fixture on 0.7.0: ``` PASS X402-01 402 Response Status Server responded with 402 as required. ... PASS X402-06 Signature Resubmit Accepted - Valid payment accepted (HTTP 200) and settled on-chain for exactly the advertised 100000 base units of CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA to GALVZ57VAY6BE33WYPJMUJ27PFAUDRQ6ATTVMIIF4STTGQTXDBEIBXUI, verified from the transfer event (tx 741c155b620641a7f8845c14e7857201aed67763ea7ca087a7adf921b550a474). + Valid payment accepted (HTTP 200) and settled on-chain for exactly the advertised 100000 base units of CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA to GDPAPDZWAKBXUPCNMI4YHAZ7DS7UOUTPGXAFDSWZG4URRMWHFSQTDQBM, verified from the transfer event (tx 1b569989e64253c8fd4714400e7aa08b126db4b049573e88fa7febe0e9f1792d). PASS X402-07 Invalid Signature Rejected - Payment with a corrupted authorization signature correctly rejected (HTTP 402). - -7 passed. + Payment with a corrupted authorization signature correctly rejected (HTTP 402: invalid_exact_stellar_payload_simulation_failed). +PASS X402-08 Payment Replay Rejected + The same payment, sent again, was refused (HTTP 402: invalid_exact_stellar_payload_simulation_failed). +PASS X402-09 Underpayment Rejected + A validly signed payment for 50000 of the advertised 100000 base units was refused (HTTP 402: invalid_exact_stellar_payload_wrong_amount). +PASS X402-10 Expired Authorization Rejected + A payment whose authorization had expired was refused (HTTP 402: invalid_exact_stellar_payload_simulation_failed). + +10 passed. ``` -A failing one distinguishes what broke from what was never tested: +A failing one distinguishes what broke from what was never tested, and says what +to change: ``` FAIL X402-01 402 Response Status Expected status 402, got 404. + Fix: A 404 usually means the wrong path or method. Check the URL, and pass the method the endpoint uses (--method POST, MCP `method`). + Docs: https://usewasit.dev/docs/checks/x402 SKIP X402-02 Payment Header Present - Skipped: the target answered 404 rather than 402, so it issued no - payment challenge to inspect. + Skipped: the target answered 404 rather than 402, so it issued no payment challenge to inspect. ... 0 passed, 1 failed, 4 skipped. ``` @@ -412,8 +403,7 @@ users actually experience. The revision notes in ## Roadmap -- Run the suite against a third-party service with the operator's explicit - authorization — the existing evidence runs don't qualify, see Status above +- x402 payments on BNB Smart Chain testnet (its read checks already apply) - Expand the catalogue as the x402 and MPP specs stabilise - Evidence documents under `docs/evidence/` for each verified run From fa9ebc592b2d539b2f7bffe3abbb6202a8363db2 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 14:04:47 +0800 Subject: [PATCH 7/9] feat(site): say where each protocol is tested, not "on Stellar" alone The hero, the page description, the docs overview and the legal pages called Wasit a tester for x402 and MPP on Stellar, and the stack strip said it trusts one chain. Since 0.7.0 the x402 payment checks also pay on Base Sepolia, Ethereum Sepolia and Solana devnet. The hero now says x402 on Stellar, Base, Ethereum and Solana testnets and MPP on Stellar, settlement read from the chain's own record of the transfer; the ticker lists the four chains; the stack strip trusts only the chain, with the x402 chains named in Stellar's cell. The terms page's description of the service changes, so its date does too. Co-Authored-By: Claude Opus 5.5 --- frontend/app/layout.tsx | 2 +- frontend/app/legal/privacy/page.tsx | 2 +- frontend/app/legal/terms/page.tsx | 7 ++++--- frontend/app/page.tsx | 9 +++++---- frontend/components/StackStrip.tsx | 10 ++++++---- frontend/lib/content.ts | 2 +- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 65cd97f..346999b 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -23,7 +23,7 @@ const jetbrainsMono = JetBrains_Mono({ export const metadata: Metadata = { title: "Wasit", description: - "Independent protocol-compliance tester for x402 and MPP on Stellar. Runs the real payment flow and verifies on-chain settlement, not just response shape.", + "Independent protocol-compliance tester for x402 and MPP. Runs the real payment flow and verifies on-chain settlement, not just response shape: x402 on Stellar, Base, Ethereum and Solana testnets, MPP on Stellar.", }; export default function RootLayout({ diff --git a/frontend/app/legal/privacy/page.tsx b/frontend/app/legal/privacy/page.tsx index 24ae39d..9945024 100644 --- a/frontend/app/legal/privacy/page.tsx +++ b/frontend/app/legal/privacy/page.tsx @@ -5,7 +5,7 @@ import { DocsToc } from "@/components/docs-toc" export const metadata: Metadata = { title: "Privacy Policy — Wasit", - description: "Privacy Policy for Wasit, an open-source protocol-compliance tester for the x402 and MPP protocols on Stellar.", + description: "Privacy Policy for Wasit, an open-source protocol-compliance tester for the x402 and MPP payment protocols.", } export default function PrivacyPage() { diff --git a/frontend/app/legal/terms/page.tsx b/frontend/app/legal/terms/page.tsx index 148e034..8e805ad 100644 --- a/frontend/app/legal/terms/page.tsx +++ b/frontend/app/legal/terms/page.tsx @@ -5,7 +5,7 @@ import { DocsToc } from "@/components/docs-toc" export const metadata: Metadata = { title: "Terms of Service — Wasit", - description: "Terms of Service for Wasit, an open-source protocol-compliance tester for the x402 and MPP protocols on Stellar.", + description: "Terms of Service for Wasit, an open-source protocol-compliance tester for the x402 and MPP payment protocols.", } /** @@ -24,12 +24,13 @@ export default function TermsPage() {

Terms of Service

-

Last updated: September 2026.

+

Last updated: October 2026.

1. What Wasit is

Wasit is an open-source protocol-compliance tester for the x402 and MPP - (Machine Payments Protocol) payment protocols on Stellar. It + (Machine Payments Protocol) payment protocols, on public testnets: + Stellar for both, and Base, Ethereum and Solana for x402. It ships as a CLI, an MCP server, and a shared core library (@wasit-dev/cli, @wasit-dev/server, @wasit-dev/core), published to npm under the diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 294fb6f..41625d8 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -31,7 +31,7 @@ const TICKER_ITEMS = [ "VERIFY THE SETTLEMENT", "x402", "MPP", - "STELLAR TESTNET", + "STELLAR · BASE · ETHEREUM · SOLANA", `${CHECK_COUNT} CHECKS`, "CLI · MCP · CORE", "APACHE-2.0", @@ -209,13 +209,14 @@ export default function Home() {

Independent protocol-compliance testing for x402 and{" "} - MPP on Stellar. + MPP.

Wasit runs the real payment flow against your service, not a schema check against its response — and verifies settlement{" "} - on-chain, from the token - contract’s own transfer event. + on-chain, from the chain’s own + record of the transfer. x402 on Stellar, Base, Ethereum and Solana + testnets; MPP on Stellar.

diff --git a/frontend/components/StackStrip.tsx b/frontend/components/StackStrip.tsx index 2254328..2961388 100644 --- a/frontend/components/StackStrip.tsx +++ b/frontend/components/StackStrip.tsx @@ -38,15 +38,17 @@ const CELLS: Cell[] = [ href: "https://stellar.org", name: "Stellar", role: "trusts", - meta: "settlement read from Stellar RPC", + meta: "settlement read from Stellar RPC; for x402 also Base, Ethereum and Solana", logo: Stellar, }, ] /** * The stack Wasit works on, as a logo strip under the hero: the two - * payment protocols it checks and the one chain it trusts. Each cell links - * to that project's own site. + * payment protocols it checks and the chain it trusts for the settlement. + * Stellar has the cell because both protocols run there; the x402 chains + * beyond it are named in its line rather than given logos of their own. + * Each cell links to that project's own site. */ export function StackStrip() { return ( @@ -55,7 +57,7 @@ export function StackStrip() {
The stack

- Two protocols it checks. One chain it trusts. + Two protocols it checks. Only the chain it trusts.

{CELLS.map((cell, i) => ( diff --git a/frontend/lib/content.ts b/frontend/lib/content.ts index 8c963e4..6ee2d92 100644 --- a/frontend/lib/content.ts +++ b/frontend/lib/content.ts @@ -462,7 +462,7 @@ export function getAboutMarkdown(): string { # Wasit Wasit is an independent protocol-compliance tester for two agentic-payment -protocols on Stellar: **x402** and **MPP**. It runs the real payment flow +protocols, **x402** and **MPP**, on Stellar testnet. It runs the real payment flow against a live service and verifies the settlement itself — by reading Stellar RPC and the token contract's own transfer event — rather than trusting whatever the service's response claims happened. The x402 payment From d18a9dd734d08785a44d040c5c94a65fb11e1b96 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 14:05:17 +0800 Subject: [PATCH 8/9] chore(release): 0.7.0 All three packages move to 0.7.0, the CLI and the MCP server depending on core ^0.7.0, and the CHANGELOG's Unreleased section becomes the dated 0.7.0 entry, opening with a summary and what can change on upgrade. SECURITY.md's count of advisories in a clean install was out of date. On the 0.7.0 tarballs it is nine packages, eight high and one moderate (the CLI alone eight), because axios advisories published since 0.6.0 now match axios 1.18.0, which @stellar/stellar-sdk 16.3.1 pins; no 16.x release moves past it, and @x402/stellar 2.28.0 requires ^16.3.0. It also says that 0.7.0 has no advisory of its own, while earlier versions have MPP-01's network issue. The evidence index names 0.7.0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 ++++++++++++++------- SECURITY.md | 52 ++++++++++++++++++++---------------- docs/evidence/README.md | 2 +- package-lock.json | 10 +++---- packages/cli/package.json | 4 +-- packages/core/package.json | 2 +- packages/server/package.json | 4 +-- 7 files changed, 62 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddca69..34eba18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,28 @@ All notable changes to Wasit are recorded here. Versions follow [Semantic Versioning](https://semver.org/): patch releases are fixes, minor releases add checks or features without breaking existing usage, major releases break something. -## [Unreleased] +## [0.7.0] — 2026-10-08 + +All three packages, versioned together as usual. The x402 payment checks now +pay on Base Sepolia, Ethereum Sepolia and Solana devnet as well as Stellar +testnet, and three negative payment checks join them (`X402-08` to `X402-10`), +for sixteen checks in all. `X402-04` and `X402-05` read every payment option, on +any chain, and every FAIL says what to change. `MPP-01` now signs only for the +network the run names. New in the CLI: `wasit serve`, a paywall that misbehaves +on purpose, and `wasit wallet --network`. + +**Results can change on upgrade.** A service on a chain other than Stellar now +passes `X402-05` where it failed. A challenge with a broken second option now +fails `X402-04` or `X402-05` where it passed. A challenge without `scheme`, +`asset` or `maxTimeoutSeconds`, or with a wrongly typed field, now fails +`X402-04`. A target that accepts replayed, underpaid or expired payments now fails +`X402-08`, `X402-09` or `X402-10`; one that refuses even a valid payment now gets +`X402-07` skipped instead of passed. A payer whose balance is below the price now +gets no verdict from `X402-06`, and nothing is sent. A target that asks `MPP-01` +to pay on a network other than the run's now gets no verdict and is not paid, +an http `--rpc-url` now stops `MPP-01` before anything is sent, and a target +that answers 402 after being paid has that 402 reported instead of being paid +again. **Fixed — `MPP-01` signs only for the network the run names, and pays once.** `MPP-01` read the amount, currency and recipient from the target's challenge @@ -183,14 +204,6 @@ lists `EVM_PRIVATE_KEY` and `SVM_PRIVATE_KEY`. would move before every payment run, including runs where the target offered no option on the run's network and nothing was paid. -**Results can change on upgrade.** A service on a chain other than Stellar now -passes `X402-05` where it failed. A challenge with a broken second option now -fails `X402-04` or `X402-05` where it passed. A challenge without `scheme`, -`asset` or `maxTimeoutSeconds`, or with a wrongly typed field, now fails -`X402-04`. A target that accepts replayed, underpaid or expired payments now fails -`X402-08`, `X402-09` or `X402-10`; one that refuses even a valid payment now gets -`X402-07` skipped instead of passed. - ## [0.6.0] — 2026-09-30 All three packages, versioned together as usual. The two x402 payment checks diff --git a/SECURITY.md b/SECURITY.md index e046c39..cee743a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -180,30 +180,36 @@ would not appear in that file until it was resolved. ## Known advisories in a clean install -`npm audit` on a fresh install of all three published packages (0.6.0, -measured 2026-09-30) reports eight packages: four high, four moderate. -Installing `@wasit-dev/cli` alone reports seven (four high, three moderate), -since it pulls a smaller slice of the same graph. None originate in Wasit's own -code or declared dependencies. All of them trace to `@stellar/mpp@0.7.1`, which -brings older copies of two packages alongside the ones Wasit declares: - -- `@stellar/stellar-sdk@15.1.0`, because `@stellar/mpp` peers on `^15.1.0` - while this project declares `^16.1.0`. That copy brings `axios@1.15.0` and - `toml@3.0.0` (high). The SDK Wasit itself declares resolves `axios@1.18.0`, - which no current advisory matches. -- `mppx@0.6.31`, because `@stellar/mpp` peers on `^0.6.29`. The "gas draining" - advisories (moderate) affect `mppx` before 0.8.1; the `mppx` Wasit declares - resolves 0.8.19. - -The three `@wasit-dev/*` packages appear in that count only because npm marks -a package that depends on an affected one; there is no advisory against Wasit. - -There is no downstream fix — the only lever is those two peer ranges, which we -do not control. They are reported upstream as +`npm audit` on a fresh install of all three packages (0.7.0, measured +2026-10-08) reports nine packages: eight high, one moderate. Installing +`@wasit-dev/cli` alone reports eight (seven high, one moderate), since it pulls +a smaller slice of the same graph. None originate in Wasit's own code. They +arrive by two routes: + +- `@stellar/mpp@0.7.1` brings older copies of two packages alongside the ones + Wasit declares. `@stellar/stellar-sdk@15.1.0`, because `@stellar/mpp` peers on + `^15.1.0`, brings `axios@1.15.0` and `toml@3.0.0` (high). `mppx@0.6.31`, + because `@stellar/mpp` peers on `^0.6.29`, is matched by the "gas draining" + advisories (moderate), which affect `mppx` before 0.8.2; the `mppx` Wasit + declares resolves 0.8.19. +- `@stellar/stellar-sdk@16.3.1`, the version Wasit and `@x402/stellar@2.28.0` + both resolve, pins `axios@1.18.0`, which axios advisories published since + 0.6.0 now match (fixed in axios 1.20.0). No 16.x release of the SDK moves past + axios 1.18.0. 17.2.1 pins axios 1.20.0, but `@x402/stellar` 2.28.0, its latest, + requires `^16.3.0`, so moving Wasit to 17 would install two copies of the SDK. + +`@stellar/mpp`, `@x402/stellar` and the three `@wasit-dev/*` packages appear in +that count only because npm marks a package that depends on an affected one. +There is no advisory against Wasit 0.7.0's own code; versions up to 0.6.0 have +the `MPP-01` network issue described under [Testnet only](#testnet-only). + +There is no downstream fix. The levers are `@stellar/mpp`'s two peer ranges, +reported upstream as [stellar-mpp-sdk#70](https://github.com/stellar/stellar-mpp-sdk/issues/70) and -written up in [findings/upstream-sdk.md](docs/findings/upstream-sdk.md); the -fix has merged upstream, but `@stellar/mpp@0.7.1` is still the latest on npm -(checked 2026-09-30). +written up in [findings/upstream-sdk.md](docs/findings/upstream-sdk.md), whose +fix has merged upstream while `@stellar/mpp@0.7.1` is still the latest on npm; +and a release of the Stellar SDK's 16 line on a fixed axios, or `@x402/stellar` +accepting 17 (all checked 2026-10-08). `npm run verify:clean-install` installs all three packages and prints the audit on every CI run, so the count is measured rather than remembered. diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 0d91a64..77a813b 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -7,7 +7,7 @@ narrative, read the [completion summary](2026-09-20-completion-summary.md). **Builder:** Muhammad Dzakwan Najmi · **Program:** Stellar Chapter Ambassador Indonesia Instaward · **Repo:** [wasit-dev/wasit](https://github.com/wasit-dev/wasit) · **Site:** [usewasit.dev](https://usewasit.dev) · **Packages:** `@wasit-dev/core`, -`@wasit-dev/cli`, `@wasit-dev/server` at 0.6.0 · **As of:** 2026-10-01 +`@wasit-dev/cli`, `@wasit-dev/server` at 0.7.0 · **As of:** 2026-10-08 | Deliverable | Status | |---|---| diff --git a/package-lock.json b/package-lock.json index 9cb5982..97226f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5613,10 +5613,10 @@ }, "packages/cli": { "name": "@wasit-dev/cli", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { - "@wasit-dev/core": "^0.6.0", + "@wasit-dev/core": "^0.7.0", "cfonts": "^3.3.1", "commander": "^15.0.0", "dotenv": "^17.4.2", @@ -5640,7 +5640,7 @@ }, "packages/core": { "name": "@wasit-dev/core", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "@solana/kit": "^5.5.1", @@ -6646,11 +6646,11 @@ }, "packages/server": { "name": "@wasit-dev/server", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "@wasit-dev/core": "^0.6.0", + "@wasit-dev/core": "^0.7.0", "zod": "^4.4.3" }, "bin": { diff --git a/packages/cli/package.json b/packages/cli/package.json index b7c7843..0ab6a04 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wasit-dev/cli", - "version": "0.6.0", + "version": "0.7.0", "description": "CLI for x402/MPP protocol-conformance testing on Stellar — verifies a service's payment flow settles on-chain per spec.", "type": "module", "bin": { @@ -41,7 +41,7 @@ "typecheck": "tsc -p test/tsconfig.json" }, "dependencies": { - "@wasit-dev/core": "^0.6.0", + "@wasit-dev/core": "^0.7.0", "cfonts": "^3.3.1", "commander": "^15.0.0", "dotenv": "^17.4.2", diff --git a/packages/core/package.json b/packages/core/package.json index 0715896..6bedb92 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wasit-dev/core", - "version": "0.6.0", + "version": "0.7.0", "description": "Check suite for x402/MPP protocol-conformance testing on Stellar. Powers the wasit CLI and wasit-mcp server.", "type": "module", "main": "dist/index.js", diff --git a/packages/server/package.json b/packages/server/package.json index bf85940..6b09d0e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@wasit-dev/server", - "version": "0.6.0", + "version": "0.7.0", "description": "MCP server exposing Wasit's x402/MPP conformance checks as tools for Claude Code and other MCP-compatible agents.", "type": "module", "bin": { @@ -42,7 +42,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "@wasit-dev/core": "^0.6.0", + "@wasit-dev/core": "^0.7.0", "zod": "^4.4.3" } } From 0cf2310d56f1c9af9bc6a63d09f0449e0fc10677 Mon Sep 17 00:00:00 2001 From: Muhammad Dzakwan Najmi Date: Thu, 8 Oct 2026 14:07:14 +0800 Subject: [PATCH 9/9] docs(evidence): the 0.7.0 release build on every chain Part 2 of the 2026-10-08 file: the release commit's packages installed as a user gets them, every suite against every fixture with each settlement read back from its chain by hand, and wasit serve's lying modes on Stellar testnet and Base Sepolia. The Permit2 suite's first run failed at the public facilitator's broadcast, the intermittent failure recorded on 2026-10-06; its second run passed and is the settlement read back. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-08-0.7.0-verification-runs.md | 116 ++++++++++++++++-- docs/evidence/README.md | 2 +- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/docs/evidence/2026-10-08-0.7.0-verification-runs.md b/docs/evidence/2026-10-08-0.7.0-verification-runs.md index 2442995..4c13e12 100644 --- a/docs/evidence/2026-10-08-0.7.0-verification-runs.md +++ b/docs/evidence/2026-10-08-0.7.0-verification-runs.md @@ -1,19 +1,24 @@ -# Wasit 0.7.0 Verification, 2026-10-08: MPP-01 Pays Only on the Run's Network +# Wasit 0.7.0 Verification, 2026-10-08: MPP-01's Network, and the Release Build on Every Chain **Date:** 2026-10-08 -**Wasit:** a build of the local `release/0.7.0` branch at `5f0d8b2`, not yet published. +**Wasit:** builds of the local `release/0.7.0` branch: `5f0d8b2` for part 1, the release +commit `d18a9dd` for part 2. Not yet published. **Environment:** macOS, Node `v26.8.1`, `@stellar/mpp` 0.7.1, `mppx` 0.8.14. -**Authorization.** None was needed or sought. Both targets were run by us on our machine: -Wasit's own charge fixture, and the official `@stellar/mpp` charge server in a process of -its own. No service operated by anyone else was tested. +**Authorization.** None was needed or sought. Every target was run by us on our machine: +Wasit's own fixtures, `wasit serve`, and the official `@stellar/mpp` charge server in a +process of its own. The x402 fixtures on Stellar, Base Sepolia and Solana devnet settle +through the public x402 facilitator (`https://www.x402.org/facilitator`), as in the +earlier 0.7.0 runs; the Ethereum Sepolia one runs the SDK's facilitator in its own +process. No service operated by anyone else was tested. -Everything checked on this date, in one part. It says what it is for, what was run against -what, and the output as printed; the limits close the file. +Everything checked on this date, in two parts. Each says what it is for, what was run +against what, and the output as printed; the limits close the file. | Part | What it checks | Outcome | |---|---|---| | [1. MPP-01 and the challenge's network](#part-1-mpp01-and-the-challenges-network) | `MPP-01` signs only for the network the run names, pays through the run's RPC endpoint, and pays once | An honest testnet target passes, the settlement read back; a target asking for `stellar:pubnet` gets `ERROR (configuration)` and no credential | +| [2. The release build on every chain](#part-2-the-release-build-on-every-chain) | Every suite against every fixture on the release commit, each settlement read back, and the paywalls that lie | 10/10 on Stellar, Base Sepolia (EIP-3009, and Permit2 on its second run), Ethereum Sepolia and Solana devnet; `MPP-01` and the channel suite pass; every lying mode fails or skips as it should | ## Part 1: MPP01 and the challenge's network @@ -136,6 +141,97 @@ Each rule was then broken in the source, one at a time, and the tests rerun: | The refusal reported as the target's failure | refuses when the challenge it is about to pay switches to another network | | Any string accepted as a network | rejects a network that is not a Stellar CAIP-2 identifier | +## Part 2: The release build on every chain + +**Build:** `d18a9dd`, the release commit. The run used `d529603` with the version bumps +that commit makes, which are its only change in `packages/`. + +**Targets:** Wasit's eight fixtures (`./scripts/fixtures.sh start`), and `wasit serve` in +each of its modes on Stellar testnet and Base Sepolia. All on our machine; the chains are +Stellar testnet, Base Sepolia, Ethereum Sepolia and Solana devnet. + +**What this is for.** The last run before the release: the packages as a user installs +them, every suite against every honest fixture with each settlement read back from the +chain by hand, and the paywalls that lie on purpose, so that the build that ships both +passes the honest targets and fails the dishonest ones. + +### The packages as a user installs them + +`npm run verify:clean-install` packed the three packages, installed the tarballs into an +empty directory and drove them: + +``` +PASS wasit --version reports the version being published + got 0.7.0, expected 0.7.0 +PASS wasit checks runs from the tarball and lists the full catalogue + 16 checks +PASS the tarball's catalogue matches the repo's + tarball 16, repo 16 +... +Advisories in the clean install: + 8 high, 1 moderate +... +PASS wasit-mcp completes an MCP handshake and answers tools/list + wasit_x402_test, wasit_mpp_charge_test, wasit_mpp_channel_test +PASS wasit-mcp reports the version being published + got 0.7.0, expected 0.7.0 +Clean install verified. +``` + +The advisories are described in SECURITY.md; none is against Wasit's own code. + +### Honest fixtures, each settlement read back + +`./scripts/run-all.sh --keep --full`, then the readbacks: Stellar from Horizon testnet, +EVM receipts and their ERC-20 logs with viem, Solana's token balance changes from +`getTransaction` on devnet. + +| Suite | Result | Settlement | Read back | +|---|---|---|---| +| x402, Stellar testnet | 10 passed | [`1b569989…792d`](https://stellar.expert/explorer/testnet/tx/1b569989e64253c8fd4714400e7aa08b126db4b049573e88fa7febe0e9f1792d) | successful, ledger 5080022; payer `GCWG…KPB2` debited 0.0100000 USDC, payee `GDPA…DQBM` credited 0.0100000 USDC | +| MPP charge | 1 passed | [`c5ad9e39…42b8`](https://stellar.expert/explorer/testnet/tx/c5ad9e3981bb7ff16c7f6a80960d65334f96c436712e7e866a527037763842b8) | successful, ledger 5080029; payer `GDHQ…AFVK` debited 0.0010000 USDC, recipient `GALV…BXUI` credited 0.0010000 USDC | +| x402, Base Sepolia, EIP-3009 | 10 passed | [`0x6d7c0537…b13f`](https://base-sepolia.blockscout.com/tx/0x6d7c053731b45278d0d34e7bc268c160e5e5f74c78f617fc54aca52029e4b13f) | success, block 47827725, sent by the facilitator `0xd407…f1bf`; one USDC `Transfer`, payer `0xC171…0428` to payee `0xb57b…Dad7`, 10000 | +| x402, Base Sepolia, Permit2 | first run: `X402-06` failed, `07`–`10` skipped; second run: 10 passed | [`0x107ce27e…ccd6`](https://base-sepolia.blockscout.com/tx/0x107ce27e46fc39e22fb9ea641c36007ece88c7e33930a8a15d7fa160a20cccd6) (second run) | success, block 47827767, sent by the facilitator; the permit's `Approval` to Permit2, and one USDC `Transfer`, payer to payee, 10000 | +| x402, Ethereum Sepolia | 10 passed | [`0xb9b039ca…d277`](https://sepolia.etherscan.io/tx/0xb9b039ca465ae8c4a503b8ee64256ef70868de05ae1afb942f6a61cfb799d277) | success, block 11866940, sent by the fixture's own facilitator `0x5d58…c579`; one USDC `Transfer`, payer to payee, 10000 | +| x402, Solana devnet | 10 passed | [`3B74Ypix…P9bf`](https://explorer.solana.com/tx/3B74YpixY7PgoD54ryah8wtrTTq5Ewodw1BQnfzTjWDAhWS1b3cbFGo57fom6cBSL6eWo8vg3D81LVgjMmLDP9bf?cluster=devnet) | slot 508654432, no error, fee paid by the sponsor `CKPK…WYp5`; devnet USDC: payer `Dya3…oQ9m` -10000, payee `HzrA…HdBS` +10000 | +| MPP channel | 3 passed, 2 skipped | — | `MPP-10` skipped (no deploy parameters given), `MPP-13` skipped (destructive, never run) | + +The Permit2 failure, as printed: + +``` +FAIL X402-06 Signature Resubmit Accepted + Expected 2xx after a valid payment, got 402 (settlement failed: invalid_exact_evm_transaction_failed). +``` + +The fixture's log of its facilitator's answer shows where it failed: + +``` +settle failed: invalid_exact_evm_transaction_failed: Missing or invalid parameters. +Double check you have provided the correct parameters. + +URL: https://sepolia.base.org +Request body: {"method":"eth_sendRawTransaction", ... +``` + +This is the public facilitator's broadcast of the settlement being refused by the Base +Sepolia RPC, the intermittent failure already recorded on 2026-10-06 (part 3 of that +day's file). The payment had verified. Run again minutes later, the same suite passed +10/10 with the settlement above. + +### Paywalls that lie + +`wasit serve --mode --network `, then `wasit test` against it. The server +never settles and never forwards a payment. + +| Mode | Stellar testnet | Base Sepolia | +|---|---|---| +| `no-settle` | 5 passed, `X402-06`–`10` failed | 5 passed, `X402-06`–`10` failed | +| `wrong-settlement` | 5 passed, `X402-06`–`10` failed | 5 passed, `X402-06`–`10` failed | +| `wrong-network` | 5 passed, `X402-06`–`10` skipped, nothing sent | the same | +| `overprice` | `X402-06` `ERROR (setup)`, the payer holding less than the price; `07`–`10` skipped | the same | +| `v1-challenge` | not offered: x402 v1 names no Stellar testnet, and `wasit serve` says so | `X402-02` and `X402-05` failed, `06`–`10` skipped | +| `malformed-header` | `X402-03` failed, `04`–`10` skipped | the same | + ## Limits - The behaviour before `5f0d8b2` was not reproduced live. A run would have built a payment @@ -148,4 +244,10 @@ Each rule was then broken in the source, one at a time, and the tests rerun: - The mutation that lets the SDK pick its own endpoint sent one account lookup, for a random key, to the SDK's default testnet endpoint (`soroban-testnet.stellar.org`, also Wasit's own default). +- Part 2's Permit2 suite passed on its second run only. The first failed at the public + facilitator's broadcast, outside Wasit and outside the fixture; a single run cannot tell + a transient failure from a lasting one. +- The lying paywalls ran on Stellar testnet and Base Sepolia only this time. `wasit serve` + was run on Solana devnet on 2026-10-05 and on Ethereum Sepolia on 2026-10-06 (those + days' files say which modes). - Unreleased: this ran from the branch, not from npm. diff --git a/docs/evidence/README.md b/docs/evidence/README.md index 77a813b..3c18b42 100644 --- a/docs/evidence/README.md +++ b/docs/evidence/README.md @@ -92,7 +92,7 @@ and what it does and does not establish. Newest first. | Date | File | What it shows | Deliverable | |---|---|---|---| -| 2026-10-08 | [0.7.0-verification-runs](2026-10-08-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, one part: `MPP-01` signs only for the network the run names, pays through the run's RPC endpoint, and pays once (an honest testnet target passes, the settlement read back; a target asking for `stellar:pubnet` gets no verdict and no credential) | Beyond the SOW | +| 2026-10-08 | [0.7.0-verification-runs](2026-10-08-0.7.0-verification-runs.md) | 0.7.0 release commit, two parts: `MPP-01` signs only for the network the run names, pays through the run's RPC endpoint, and pays once (a target asking for `stellar:pubnet` gets no verdict and no credential); the release build on every chain, 10/10 on Stellar, Base Sepolia, Ethereum Sepolia and Solana devnet with each settlement read back, the clean install, and the paywalls that lie | Beyond the SOW | | 2026-10-06 | [0.7.0-verification-runs](2026-10-06-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, three parts: the x402 payment checks on Ethereum Sepolia (lying servers fail; Wasit's fixture, running the SDK's facilitator, passes 10/10 with the settlement read back and the payer holding no ETH); a price above the official client's $1 cap gets no verdict; a failed settlement now names its reason | Beyond the SOW | | 2026-10-05 | [0.7.0-verification-runs](2026-10-05-0.7.0-verification-runs.md) | Unreleased 0.7.0 branch, five parts: the x402 payment checks on Base Sepolia (EIP-3009 and Permit2) and Solana devnet (lying servers fail; Wasit's fixtures pass with settlements read back); `X402-08` replay, `X402-09` underpayment and `X402-10` expired authorization (10/10 on Stellar and Base Sepolia); an unfunded payer gets no verdict; `wasit wallet --network` and two new `wasit serve` modes | Beyond the SOW | | 2026-09-30 | [x402-0.6.0-verification-run](2026-09-30-x402-0.6.0-verification-run.md) | 0.6.0's stricter `X402-06` and `X402-07`, A/B against 0.5.0: servers built to skip settlement, misreport it or skip signature checks pass 0.5.0 and fail 0.6.0. `stellar/x402-stellar`'s reference still passes 7/7, with `X402-06` verified on-chain. Registry parity for the published 0.6.0 | D1 |