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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ EVM_PRIVATE_KEY=
# Only needed to run the bundled fixture.
EVM_PAYEE_ADDRESS=

# --- 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
# facilitator signs as fee payer.
SVM_PRIVATE_KEY=

# Recipient address (base58) the Solana fixture server charges to. Its devnet
# USDC account must already exist. Only needed to run the bundled fixture.
SVM_PAYEE_ADDRESS=

# --- MPP charge mode (MPP-01) ------------------------------------------------
# Payer secret key (S...) for the charge check. Settles a real payment on every
# run; repeated runs spend repeatedly.
Expand Down
30 changes: 24 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,33 @@ 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 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
wallets export it). The payer signs a transaction with one `TransferChecked`, and the
facilitator signs as fee payer, so a payer needs devnet USDC and no SOL. `X402-06` holds
the settlement to the confirmed transaction's token balances: exactly one transfer, of
the advertised amount and mint, to an account `payTo` owns, from this run's payer.
`X402-07` forges only the payer's signature; `X402-10` builds the payment on a blockhash
the RPC confirms has expired, since on Solana the blockhash, not `maxTimeoutSeconds`,
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
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.

**Added — three negative payment checks: `X402-08` Payment Replay Rejected, `X402-09`
Underpayment Rejected, `X402-10` Expired Authorization Rejected.** Each sends a payment the
target must refuse: the exact payment `X402-06` was accepted for, sent again; a validly
signed payment for half the price that still claims the full price; and a payment signed
with a one-second lifetime, sent once it has expired. They run on Stellar and Base
Sepolia. A refusal now shows the target's stated reason, when it gives one. Against
Wasit's own fixtures all three pass on both chains, refused for the reason each is about
with a one-second lifetime, sent once it has expired (on Solana, one built on an expired
blockhash). They run on Stellar, Base Sepolia and Solana devnet. A refusal now shows the
target's stated reason, when it gives one. Against Wasit's own fixtures all three pass on
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`, ...); against `wasit serve
--mode no-settle` all three fail. `X402-08` is skipped when the challenge advertises the
`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
`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.
Expand All @@ -95,7 +112,8 @@ what the agent did. It never settles or forwards anything, so no funds move.
Every mode's challenge is well-formed (`wasit test --read-only` passes it), and
the two settlement modes reproduce the servers built for the 0.6.0 A/B:
`X402-06` and `X402-07` fail against them. `--network eip155:84532` poses the same
modes on Base Sepolia.
modes on Base Sepolia, and `--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` on Solana
devnet.

**Changed — the CLI's payment warning says when it applies.** It said funds
would move before every payment run, including runs where the target offered
Expand Down
30 changes: 19 additions & 11 deletions docs/CHECKS.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions docs/evidence/2026-10-05-solana-devnet-verification-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 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`) 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.
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-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-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 |
Expand Down
10 changes: 5 additions & 5 deletions docs/guides/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,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` or `eip155:84532` (Base Sepolia). `X402-01`–`05` apply to a challenge on any chain |
| `--payer-key <key>` | `STELLAR_PRIVATE_KEY`, or `EVM_PRIVATE_KEY` on Base Sepolia | A Stellar secret (`S...`), or an EVM private key (`0x` + 64 hex) |
| `--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`) |
| `--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`) |
| `--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 @@ -249,8 +249,8 @@ wasit serve --mode no-settle
| `--mode <mode>` | required | One of the four 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` or `eip155:84532` (Base Sepolia). On Base Sepolia the paywall uses the SDK's USDC, `wrong-network` asks for Base mainnet (`eip155:8453`), and `overprice` one million USDC in its 6 decimals |
| `--pay-to <address>` | `STELLAR_PAYEE_ADDRESS`, or `EVM_PAYEE_ADDRESS` on Base Sepolia | A testnet account (`G...`) with a trustline for the asset, or an EVM address (`0x...`) |
| `--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) |
| `--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
14 changes: 13 additions & 1 deletion docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ keys in this file.
| `X402-01`–`05` | nothing |
| `X402-06`–`10` on Stellar | `STELLAR_PRIVATE_KEY` |
| `X402-06`–`10` on Base Sepolia (`--network eip155:84532`) | `EVM_PRIVATE_KEY` |
| `X402-06`–`10` on Solana devnet (`--network solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) | `SVM_PRIVATE_KEY` |
| `MPP-01` | `MPP_PAYER_SECRET`, `MPP_STELLAR_NETWORK` |
| `MPP-10`–`12`, `14` | `COMMITMENT_SECRET_HEX`, `MPP_STELLAR_NETWORK` |
| `MPP-13` | the above, plus `CHANNEL_CONTRACT_DISPOSABLE` and an explicit opt-in |
Expand All @@ -33,7 +34,10 @@ third-party node.
The x402 payment checks also pay on Base Sepolia (`eip155:84532`), through
`--network` (MCP: `network`), with the payer key in `EVM_PRIVATE_KEY`. Its
default RPC endpoint is Base's public `https://sepolia.base.org`, overridable
with `--rpc-url`. Any other network gets the read-only checks only.
with `--rpc-url`. They pay on Solana devnet too
(`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`), with the payer key in `SVM_PRIVATE_KEY`
and the public `https://api.devnet.solana.com` as the default RPC endpoint. Any other
network gets the read-only checks only.

## Getting testnet keys

Expand Down Expand Up @@ -62,6 +66,14 @@ once on-chain, which needs gas, and a run without that approval reports
`ERROR (setup)`. A wrong or malformed key is reported at `PREFLIGHT` before
anything is sent, without echoing it.

For Solana devnet, `SVM_PRIVATE_KEY` is the base58 encoding of the keypair's 64
bytes (seed, then public key), the form wallets export. The payer needs devnet USDC
of the SDK's default mint, `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`, and **no
SOL**: the facilitator signs as fee payer and pays the fee. The payee's USDC token
account must exist before the first payment, since the payment's transaction does
not create it. A keypair whose public half does not belong to its seed, or any
malformed key, is reported at `PREFLIGHT` without echoing it.

## The disposable channel

`MPP-13` closes a channel permanently. `CHANNEL_CONTRACT_DISPOSABLE` should name
Expand Down
Loading
Loading