Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ EVM_PRIVATE_KEY=
# Only needed to run the bundled fixture.
EVM_PAYEE_ADDRESS=

# --- x402 payment checks on Ethereum Sepolia (eip155:11155111) --------------
# The payer is EVM_PRIVATE_KEY above; it needs Circle's Sepolia USDC and no ETH.
# No public facilitator settles Ethereum Sepolia, so the bundled fixture runs
# its own, which pays the gas from this key (0x + 64 hex, holding Sepolia ETH).
# Only needed to run the bundled fixture.
EVM_FACILITATOR_PRIVATE_KEY=
# Optional RPC endpoint for that fixture (default: viem's public Sepolia RPC).
SEPOLIA_RPC_URL=

# --- x402 payment checks on Solana devnet -----------------------------------
# Payer keypair for --network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1, base58
# of its 64 bytes as wallets export it. It needs devnet USDC and no SOL: the
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ Sepolia, the lying modes fail both checks
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.

**Added — the x402 payment checks pay on Ethereum Sepolia.** `--network
eip155:11155111` runs `X402-06` to `X402-10` there with the same `EVM_PRIVATE_KEY`,
paying in Circle's Sepolia USDC through EIP-3009, so the payer needs no ETH. The SDK
ships no default asset for this network, and its client's spend controls refuse other
assets: Wasit allows Circle's Sepolia USDC there, with the SDK's own $1 cap. The wait
for a settled transaction is now per network: 10 blocks on Sepolia, whose blocks are 12
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
Chain testnet stays read-only.

**Added — the x402 payment checks pay on Solana devnet.** `--network
solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (MCP: `network`) runs `X402-06` to `X402-10`
there, with the payer key in `SVM_PRIVATE_KEY` (base58 of the 64-byte keypair, as
Expand Down Expand Up @@ -99,6 +111,20 @@ refuses `X402-06`'s valid payment, `X402-07` to `X402-10` are skipped rather tha
a target that refuses everything proves nothing by refusing a bad payment. `X402-07` used
to pass in that case.

**Changed — a price the official client will not pay gets no verdict.** The official x402
client, which Wasit pays through, pays only the SDK's default assets and at most $1 a
payment. A target above that used to get `X402-06` ERROR (harness) with the SDK's own
configuration advice; it now gets `ERROR (setup)` naming the price or the asset, and
nothing is sent. Wasit keeps the cap: it pays automatically, and on `stellar:pubnet`
with real money.

**Fixed — a failed settlement names its reason.** A valid payment that verified but did
not settle read `got 402.`. The official SDK server reports why in a `PAYMENT-RESPONSE`
with `success: false`, which Wasit now reads: `got 402 (settlement failed:
invalid_exact_evm_transaction_failed)`. The same reason now shows on refused negative
payments, which is how the public facilitator was seen refusing a replay at settlement
rather than at verify.

**Changed — a payer that cannot cover the price gets no verdict.** `X402-06` reads the
payer's balance of the advertised asset before it signs; below the price, it reports
`ERROR (setup)` naming the key to fund, sends nothing, and skips `X402-07`–`10`. An
Expand Down
24 changes: 20 additions & 4 deletions docs/CHECKS.md

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions docs/evidence/2026-10-05-payer-balance-check-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,19 @@ One Permit2 run (port 3006), started a few seconds after an EIP-3009 run from th
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; it was
the target's facilitator refusing once, which Wasit reported as it happened.
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Date:** 2026-10-05
**Wasit:** builds of the local `release/0.7.0` branch (`4c3ca55` for `wallet --network`,
and the commit after it for the two modes), not yet published.
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.
Expand Down
130 changes: 130 additions & 0 deletions docs/evidence/2026-10-06-ethereum-sepolia-verification-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# x402 Payment Checks on Ethereum Sepolia — A/B Against Lying Servers, and a Real Settlement

**Date:** 2026-10-05 and 2026-10-06
**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.
**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
(`0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238`) has EIP-3009: read from the contract, it
answers `authorizationState` and `nonces`, with EIP-712 name "USDC", version "2", 6
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

Payer: Najmi's Base Sepolia payer, which also held 20 Sepolia USDC and no ETH. Nothing
moved: `wasit serve` never settles.

| `wasit serve` mode | `X402-06` | `X402-07`–`10` | Run time |
|---|---|---|---|
| `no-settle` | **FAIL**, no `PAYMENT-RESPONSE` | **FAIL** | 6 s |
| `wrong-settlement` | **FAIL**, "the chain produced 10 more blocks without it" | **FAIL** | 131 s |
| `wrong-network` (asks `eip155:1`) | SKIP, nothing sent | SKIP | 1 s |

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

`wasit test --target http://localhost:3008/protected --network eip155:11155111`:
**10/10**, in 20 seconds.

```
PASS X402-06 Signature Resubmit Accepted
Valid payment accepted (HTTP 200) and settled on-chain for exactly the
advertised 10000 base units of 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
to 0xb57bFdA962aAa607D5260747A175cda8f4bDDad7, verified from the Transfer
log (tx 0x0c48fa9d…7849).
PASS X402-07 ... rejected (HTTP 402: invalid_exact_evm_signature).
PASS X402-08 ... refused (HTTP 402: invalid_exact_evm_nonce_already_used).
PASS X402-09 ... 5000 of the advertised 10000 base units was refused
(HTTP 402: invalid_exact_evm_payload_authorization_value_mismatch).
PASS X402-10 ... refused (HTTP 402: invalid_exact_evm_payload_authorization_valid_before).
```

[`0x0c48fa9d…7849`](https://sepolia.etherscan.io/tx/0x0c48fa9def71f95411862beceef7110c50ac72c323c4603feacb40bc314d7849)

Read back independently afterwards, outside Wasit, with `viem` against the public
Sepolia RPC:

| | |
|---|---|
| Receipt | `success`, block 11849904, 85728 gas |
| Sent by | `0x5D58…C579`, the fixture's facilitator, calling the USDC contract |
| Logs | `AuthorizationUsed` for the payer `0xC171…0428`, and exactly one `Transfer` from the payer to the payee `0xb57b…Dad7` for 10000 |
| Payer after | 19.99 USDC, **still 0 ETH** |

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

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
attempt stopped before signing: `ERROR (harness) All payment requirements were rejected
by spendControls: only default assets or entries in spendControls.allowedAssets are
allowed`. The EVM adapter now allows that token on Ethereum Sepolia only, with the same
$1 cap the SDK puts on its defaults (1000000 base units); an offline test pays 10000 and
refuses 1000001.

A target above those limits is not at fault and gets no verdict. Against `wasit serve`
on Base Sepolia priced at 2000000 units (2 USDC), with a payer holding 19.8:

```
ERROR X402-06 Signature Resubmit Accepted
Check could not run (setup): The target asks for 2000000 base units of
0x036CbD53842c5426634e7929541eC2318f3dCF7e, more than the $1 a payment the
official x402 client pays by default, so nothing was sent. ...
```

and the paywall's log shows no payment received.

## 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
whose `errorReason` names the facilitator's failure, and Wasit now reads it. On
2026-10-06, on the Base Sepolia fixtures, which settle through the public facilitator:

```
FAIL X402-06 Signature Resubmit Accepted
Expected 2xx after a valid payment, got 402
(settlement failed: invalid_exact_evm_transaction_failed).
PASS X402-08 Payment Replay Rejected
The same payment, sent again, was refused
(HTTP 402: settlement failed: invalid_exact_evm_nonce_already_used).
```

The first is the public facilitator failing to broadcast its own transaction (the
fixture's log: `eth_sendRawTransaction` answered "Missing or invalid parameters"); five
of sixteen Base Sepolia runs that day failed `X402-06` this way, each passing on re-run,
and the three whose cause could be seen all showed it. The second
shows where the public facilitator stops a replay: the replayed EIP-3009 authorization
passed its verify and was refused at settlement, its nonce spent. The resource was not
served either way. The same day the Stellar fixture passed 10/10, in 198 seconds against
41 the day before; one earlier run had stopped with `ERROR (harness)` when Stellar
testnet RPC was slow enough for the payment's own authorization to expire before the
SDK client simulated it.

## Limits

- One more EVM network: Ethereum Sepolia, with EIP-3009. Permit2 there was not run.
BNB Smart Chain testnet stays read-only.
- The honest target is Wasit's own fixture with the SDK's facilitator run locally. No
third-party Ethereum Sepolia service was tested.
- A facilitator's transient failure is reported as `X402-06` FAIL, with its reason: the
target did refuse a valid payment. Wasit cannot tell a transient failure from a lasting
one in a single run.
- Unreleased: this ran from the branch, not from npm.
1 change: 1 addition & 0 deletions docs/evidence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ 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 |
Expand Down
16 changes: 8 additions & 8 deletions docs/guides/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ wasit wallet fund --role x402|mpp-charge [--network <testnet>] [--asset xlm|usdc

Testnet-only convenience commands for the payer keys the other subcommands
read from `.env`. `--network` takes testnets only, `stellar:testnet` (the
default), `eip155:84532` (Base Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`
(Solana devnet), since Friendbot, the printed USDC issuer, and the whole idea
default), `eip155:84532` (Base Sepolia), `eip155:11155111` (Ethereum Sepolia) or
`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana devnet), since Friendbot, the printed USDC issuer, and the whole idea
of a disposable generated key only make sense on testnet.

`status` shows each configured role's on-chain balances (or "not yet created"
Expand Down Expand Up @@ -109,7 +109,7 @@ public key `wasit wallet fund` prints), or set
`WASIT_USDC_DISTRIBUTOR_SECRET` in `.env` to an account you already funded
that way — every run after that sends automatically from it.

On Base Sepolia and Solana devnet only `--role x402` applies: MPP runs on
On the other testnets only `--role x402` applies: MPP runs on
Stellar. `create --role x402 --network eip155:84532` prints an
`EVM_PRIVATE_KEY=0x...` line, and `--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`
an `SVM_PRIVATE_KEY=...` line (base58 of the 64-byte keypair), each with the
Expand Down Expand Up @@ -169,9 +169,9 @@ wasit test --target <url> [options]
| Option | Default | Notes |
|---|---|---|
| `--target <url>` | required | Must include the scheme |
| `--network <id>` | `stellar:testnet` | Network the payment checks pay on: `stellar:testnet`, `stellar:pubnet`, `eip155:84532` (Base Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana devnet). `X402-01`–`05` apply to a challenge on any chain |
| `--payer-key <key>` | `STELLAR_PRIVATE_KEY`, `EVM_PRIVATE_KEY` on Base Sepolia, or `SVM_PRIVATE_KEY` on Solana devnet | A Stellar secret (`S...`), an EVM private key (`0x` + 64 hex), or a Solana keypair (base58 of its 64 bytes) |
| `--rpc-url <url>` | the network's default | RPC used to verify `X402-06`'s settlement: Soroban RPC on Stellar, required for pubnet; JSON-RPC on Base Sepolia (default `https://sepolia.base.org`) and on Solana devnet (default `https://api.devnet.solana.com`) |
| `--network <id>` | `stellar:testnet` | Network the payment checks pay on: `stellar:testnet`, `stellar:pubnet`, `eip155:84532` (Base Sepolia), `eip155:11155111` (Ethereum Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana devnet). `X402-01`–`05` apply to a challenge on any chain |
| `--payer-key <key>` | `STELLAR_PRIVATE_KEY`, `EVM_PRIVATE_KEY` on Base or Ethereum Sepolia, or `SVM_PRIVATE_KEY` on Solana devnet | A Stellar secret (`S...`), an EVM private key (`0x` + 64 hex), or a Solana keypair (base58 of its 64 bytes) |
| `--rpc-url <url>` | the network's default | RPC used to verify `X402-06`'s settlement: Soroban RPC on Stellar, required for pubnet; JSON-RPC on Base Sepolia (default `https://sepolia.base.org`), Ethereum Sepolia (default `https://11155111.rpc.thirdweb.com`, viem's) and Solana devnet (default `https://api.devnet.solana.com`) |
| `--method <verb>` | `GET` | HTTP method the paid endpoint uses. Endpoints that compute something usually take `POST` |
| `--body <json>` | — | Request body, sent verbatim; implies `Content-Type: application/json` |
| `--header <name:value>` | — | Extra request header the endpoint needs before it will issue a challenge. Repeatable |
Expand Down Expand Up @@ -262,8 +262,8 @@ wasit serve --mode no-settle
| `--mode <mode>` | required | One of the six above |
| `--port <port>` | `4020` | The server answers on every path |
| `--host <host>` | `127.0.0.1` | Local only unless you bind another interface |
| `--network <id>` | `stellar:testnet` | `stellar:testnet`, `eip155:84532` (Base Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana devnet). On Base Sepolia and Solana devnet the paywall uses the SDK's USDC, `wrong-network` asks for that chain's mainnet (`eip155:8453`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`), and `overprice` one million USDC in its 6 decimals. On Solana the challenge names the payee as `extra.feePayer`, which the spec allows; nothing is ever submitted |
| `--pay-to <address>` | `STELLAR_PAYEE_ADDRESS`, `EVM_PAYEE_ADDRESS` on Base Sepolia, or `SVM_PAYEE_ADDRESS` on Solana devnet | A testnet account (`G...`) with a trustline for the asset, an EVM address (`0x...`), or a Solana address (base58) |
| `--network <id>` | `stellar:testnet` | `stellar:testnet`, `eip155:84532` (Base Sepolia), `eip155:11155111` (Ethereum Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana devnet). Elsewhere than Stellar the paywall uses that network's USDC (the SDK's, or Circle's on Ethereum Sepolia), `wrong-network` asks for that chain's mainnet (`eip155:8453`, `eip155:1`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`), and `overprice` one million USDC in its 6 decimals. On Solana the challenge names the payee as `extra.feePayer`, which the spec allows; nothing is ever submitted |
| `--pay-to <address>` | `STELLAR_PAYEE_ADDRESS`, `EVM_PAYEE_ADDRESS` on Base or Ethereum Sepolia, or `SVM_PAYEE_ADDRESS` on Solana devnet | A testnet account (`G...`) with a trustline for the asset, an EVM address (`0x...`), or a Solana address (base58) |
| `--amount <units>` | `10000` | Price in base units, for every mode except `overprice` |
| `--asset <contract>` | testnet USDC | `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` |
| `--settlement-tx <hash>` | a random hash | `wrong-settlement` only. The default cites a transaction that exists nowhere; pass a real, unrelated transaction to test an agent that finds it on-chain but must notice it is not this payment |
Expand Down
Loading
Loading