From 0361bc1dc36acd1fcba465af6dc0a58a2d2f0a30 Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Fri, 21 Aug 2026 23:29:03 -0700 Subject: [PATCH] =?UTF-8?q?DAN-855:=20the=20front=20door=20=E2=80=94=20a?= =?UTF-8?q?=20demo=20you=20can=20run,=20and=20four=20claims=20that=20were?= =?UTF-8?q?=20not=20true?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engineering here was already sound and already verified. The front door was not. A grant reviewer opens this repo first, and until now the entire runnable surface of a 389-line README was `npm install` and two `ls` commands. `pnpm demo` now exists. Sixty seconds, no wallet, no keys, no network: a server encodes a 402 into one HTTP header, a client decodes it with no shared code, an x402 V1 body is absorbed through the compat layer, and the 167 published conformance vectors run against the code in your clone. It reports how many malformed headers were refused AND how many leaked through. The second number is the one that means anything — a suite that only ever watches things pass cannot tell a working validator from one that returns true unconditionally. That counter was verified the only way it can be: by poisoning a vector with a valid header falsely marked shouldReject, and confirming the run names it, counts it, and exits non-zero. Four claims were false, and the first would stop a reader cold: `s402/compat` is not an exported subpath. The README used it four times. Copying the compat example produced ERR_PACKAGE_PATH_NOT_EXPORTED. The real path is `s402/compat/x402`, and it exports all five documented functions. The sub-path table was also missing six real entries. `@sweefi/sui` was described as "coming soon" while it is published on npm — and the same README described that implementation in the present tense two paragraphs earlier, so one fact appeared in two tenses. This is the shape DAN-588 already cost 33 days to: a hedge nobody re-read after its condition lifted. An under-claim is not the safe direction. `GET /api/catalog` reported the protocol version as "0.3" while S402_VERSION is "1" — a reviewer curling the catalog to see what the protocol advertises got a number contradicting every payload the same server emits. The gas table stated modelled estimates as flat facts and dropped the case the project's own whitepaper is careful to name: x402 on Solana is cheaper than s402 Exact on Sui for one-shot calls. The README now marks the numbers as modelled, links the method, and says where a competitor wins. The whitepaper was more honest than the front page, which is backwards. Added the two sections the README convention requires and this repo had neither of: What Is True Today (shipped / published / partial / not-runnable, per component) and Receipts (a command for every claim the README makes about itself). Those were missing structurally, which is why the four false claims had nowhere to be correct. demo-api/ is reachable again. It was absent from pnpm-workspace.yaml, had no README, and pinned a published s402@^0.6.0 while this repo ships 0.9.0 — so it was neither wired to the workspace nor documented. It is now a workspace package on workspace:*, typechecks against current source, and documents the verified route: /api/joke, not /api/data, which is what the first draft of its README claimed until curl said 404. Verified on this branch: pnpm typecheck clean, 1108/1108 tests across 29 files, build 23 files, pnpm demo exit 0, demo-api's documented curl pipeline run against a freshly started process. Findings: DAN-855, and the workspace knowledge file cited there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N7ufynvh6FC65iyC9YHNzb --- README.md | 92 +++++++++++++++++--- demo-api/README.md | 82 ++++++++++++++++++ demo-api/package.json | 4 +- demo-api/src/server.ts | 2 +- package.json | 3 +- pnpm-lock.yaml | 16 ++++ pnpm-workspace.yaml | 1 + typescript/CHANGELOG.md | 42 +++++++++ typescript/examples/quickstart.mjs | 134 +++++++++++++++++++++++++++++ typescript/package.json | 3 +- 10 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 demo-api/README.md create mode 100644 typescript/examples/quickstart.mjs diff --git a/README.md b/README.md index d38af1c..f14e398 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ [![CI](https://github.com/s402-protocol/core/actions/workflows/ci.yml/badge.svg)](https://github.com/s402-protocol/core/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/s402.svg)](https://www.npmjs.com/package/s402) -**Chain-agnostic HTTP 402 protocol.** Six payment schemes for AI agent commerce. Wire-compatible with x402. Zero runtime dependencies. Includes an optional compat layer (`s402/compat`) for normalizing x402 input. +**Chain-agnostic HTTP 402 protocol.** Six payment schemes for AI agent commerce. Wire-compatible with x402. Zero runtime dependencies. Includes an optional compat layer (`s402/compat/x402`) for normalizing x402 input. -s402 is a chain-agnostic HTTP 402 wire format — types, HTTP encoding, scheme registry, and error handling for six payment schemes. The protocol layer contains no chain-specific logic (see [S7 invariant](./AGENTS.md)). The reference implementation on Sui uses Programmable Transaction Blocks to reduce 1,000 payments to just 2 on-chain transactions via the Prepaid scheme, cutting per-call effective gas from $0.007 to $0.000014 and making micropayments economically viable for AI agents for the first time. +s402 is a chain-agnostic HTTP 402 wire format — types, HTTP encoding, scheme registry, and error handling for six payment schemes. The protocol layer contains no chain-specific logic (see [S7 invariant](./AGENTS.md)). + +The Sui reference implementation ships separately as [`@sweefi/sui`](https://www.npmjs.com/package/@sweefi/sui). Its Prepaid scheme uses Programmable Transaction Blocks to settle 1,000 API calls in **2 on-chain transactions instead of 1,000** — a *modelled* effective gas cost of ~$0.000014 per call against ~$0.007 for one-shot Exact. Those are estimates under stated price and congestion assumptions, not measurements from production traffic: the model, its inputs, and the cases where a competitor is cheaper are all in [the whitepaper's gas section](./docs/whitepaper.md). ```bash npm install s402 @@ -16,6 +18,31 @@ deno add npm:s402 > **ESM-only.** This package ships ES modules only (`"type": "module"`). Requires Node.js >= 20. CommonJS `require()` is not supported. +## The one demo — 60 seconds, no wallet + +```bash +git clone https://github.com/s402-protocol/core.git && cd core +pnpm install +pnpm demo +``` + +**What you will see:** a server encoding a 402 into a single HTTP header, a client +decoding it with no shared code, an x402 payment body being absorbed through the compat +layer, and the 167 published conformance vectors run against the code you just cloned — +including 45 malformed headers that **must** be refused. + +**What it proves:** the wire format works, on this build, in your terminal. **What it does +not prove:** anything about settlement on Sui — that needs a chain, and lives in +[`@sweefi/sui`](https://www.npmjs.com/package/@sweefi/sui). + +Nothing in the demo touches a network, a key, or a testnet faucet. Source: +[`typescript/examples/quickstart.mjs`](./typescript/examples/quickstart.mjs). + +> Two larger demos ship in this repo and need more than 60 seconds: +> [`mcp-demo/`](./mcp-demo) (three payment protocols in one MCP envelope — **read its README +> first, it cannot reach the network today**) and [`demo-api/`](./demo-api) (a paid HTTP +> endpoint). + ## Governing Principle > **We interop when possible. We superset when wise.** @@ -37,13 +64,20 @@ HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a pay | **Settlement** | Two-step: verify then settle (temporal gap) | Atomic: verify + settle in one PTB | | **Finality** | 12+ second blocks (EVM L1) | ~400ms (Sui) | | **Payment models** | Exact (one-shot) only | Six schemes: Exact, Prepaid, Escrow, Unlock, Stream, Upto | -| **Micro-payments** | ~$1.60 gas per 1K calls on Base (broken) | $0.014 gas per 1K calls (prepaid) | +| **Micro-payments** | ~$1.60 per 1K calls on Base* | ~$0.014 per 1K calls (prepaid)* | | **Coin handling** | approve + transferFrom | Native `coinWithBalance` + `splitCoins` | | **Agent auth** | None | AP2 mandate delegation | | **Direct mode** | No | Yes (no facilitator needed) | | **Receipts** | Off-chain | On-chain NFT proofs | | **Compatibility** | n/a | Optional x402 compat layer (`s402/compat`) | +\* **Modelled, not measured.** Gas figures are estimates under stated ETH-price and congestion +assumptions; the model and its inputs are in [the whitepaper](./docs/whitepaper.md#the-gas-comparison). +Read that table before quoting these — it is more honest than a two-column summary can be, and it +names the case we lose: **x402 on Solana (~$0.25 per 1K calls) is cheaper than s402 Exact on Sui +(~$7.00) for one-shot calls.** s402 Prepaid wins on *fixed overhead regardless of volume*, not on +per-transaction cost. + **s402 is Sui-native by design.** These advantages come from Sui's object model, PTBs, and sub-second finality. They can't be replicated on EVM — and they don't need to be. x402 already handles EVM well. s402 handles Sui better. ## Who This Is For @@ -59,6 +93,38 @@ say so. Also skip it if you want a payments *product*: s402 is a wire format and with zero runtime dependencies. It does not move money, custody funds, or run a facilitator for you. Fiat and card rails are out of scope and will stay that way. +## What Is True Today + +The honest tense, as of the current release. Nothing below is described in the present tense +before it ships. + +| | State | +|---|---| +| **`s402` wire format** (this repo) | **Shipped** — `0.9.0` on npm. Types, HTTP encoding, scheme registry, error taxonomy, six schemes defined. Zero runtime dependencies. | +| **Conformance vectors** | **Shipped** — 167 across 14 files, in `spec/vectors/`, run by `pnpm demo`. | +| **`@sweefi/sui`** (Sui adapter) | **Published** — PTB builders and the payment adapter. Settlement lives here, not in this repo. | +| **`@sweefi/server`** (HTTP middleware) | **Published.** | +| **Unlock scheme** | **Partial** — depends on encryption key-server infrastructure; under active development. | +| **`mcp-demo/`** | **Built, not currently runnable end-to-end** — Sui deprecated JSON-RPC on public fullnodes. Its README says so first. | +| **Architecture decisions** | 12 ADRs: **7 shipped**, 1 upheld, 2 in-progress, 2 not-started. Each carries an `Implementation:` field, so "ratified" and "built" are distinguishable. | + +## Receipts + +Claims in this README are checkable. These are the checks. + +| Claim | How you check it | +|---|---| +| The wire format works | `pnpm demo` — encode, decode, x402 compat, and 167 vectors, offline | +| The test suite is green | `cd typescript && pnpm vitest run` → **1108 tests across 29 files** | +| It typechecks and builds | `cd typescript && pnpm typecheck && pnpm build` | +| Malformed input is refused | `pnpm demo` reports how many rejected **and how many leaked through**. The second number is the real one | +| 167 vectors, 14 files | `ls spec/vectors/ \| wc -l`, and the demo runs them | +| Zero runtime dependencies | `cat typescript/package.json` — there is no `dependencies` key | +| Which decisions were built | `grep -h -o '\*\*Implementation:\*\* *[a-z-]*' docs/adr/*.md \| sort \| uniq -c` | + +Last observed on a clean clone of `main`: typecheck clean, **1108/1108 tests across 29 files**, +build 23 files. If any of the above does not run for you, that is a bug — please open an issue. + ## Which Scheme Should I Use? | Your situation | Scheme | Gas per 1K calls | Latency | @@ -220,7 +286,7 @@ import { isX402, toX402Requirements, fromX402Requirements, -} from 's402/compat'; +} from 's402/compat/x402'; // Normalize x402 JSON (V1 or V2) to s402 format const requirements = normalizeRequirements(rawJsonObject); @@ -278,11 +344,17 @@ if (result.success) { ## Sub-path Exports ```typescript -import { ... } from 's402'; // Everything +import { ... } from 's402'; // Everything import type { ... } from 's402/types'; // Types + constants only -import { ... } from 's402/http'; // HTTP encode/decode -import { ... } from 's402/compat'; // x402 interop -import { ... } from 's402/errors'; // Error types +import { ... } from 's402/http'; // HTTP encode/decode +import { ... } from 's402/server'; // s402Gate server helpers +import { ... } from 's402/errors'; // Error types +import { ... } from 's402/receipts'; // On-chain receipt parsing +import { ... } from 's402/extensions'; // Extension registry +import { ... } from 's402/compat/x402'; // x402 interop +import { ... } from 's402/compat/mpp'; // Stripe MPP interop +import { ... } from 's402/compat/l402'; // L402 interop +import { ... } from 's402/test-utils'; // Fixtures for your own tests ``` ## Implementing a Scheme @@ -350,9 +422,9 @@ const requirements: s402PaymentRequirements = { ## Design Principles -1. **Protocol-agnostic core, Sui-native reference.** `s402` defines chain-agnostic protocol types and HTTP encoding. The reference implementation (`@sweefi/sui`, coming soon) will exploit Sui's unique properties — PTBs, object model, sub-second finality. Other chains can implement s402 schemes using their own primitives. +1. **Protocol-agnostic core, Sui-native reference.** `s402` defines chain-agnostic protocol types and HTTP encoding. The reference implementation, [`@sweefi/sui`](https://www.npmjs.com/package/@sweefi/sui), is **published** and exploits Sui's properties — PTBs, object model, sub-second finality. Other chains can implement s402 schemes using their own primitives. -2. **Optional x402 compat.** The `s402/compat` subpath provides a migration aid for codebases with x402-formatted JSON. It normalizes x402 V1 (`maxAmountRequired`) and V2 (`amount`) to s402 format. This is opt-in — the core protocol has no x402 dependency. +2. **Optional x402 compat.** The `s402/compat/x402` subpath provides a migration aid for codebases with x402-formatted JSON. It normalizes x402 V1 (`maxAmountRequired`) and V2 (`amount`) to s402 format. This is opt-in — the core protocol has no x402 dependency. 3. **Scheme-specific verification.** Each scheme has its own verify logic. Exact verify (signature recovery + dry-run) is fundamentally different from stream verify (deposit check + rate validation). The facilitator dispatches — it doesn't share logic. diff --git a/demo-api/README.md b/demo-api/README.md new file mode 100644 index 0000000..0a4df4e --- /dev/null +++ b/demo-api/README.md @@ -0,0 +1,82 @@ +# s402 demo API + +A paid HTTP endpoint, running on your machine, with **no wallet, no keys and no network**. + +It answers the one question protocol types cannot: *what does a 402 exchange actually look like +over the wire?* Payment verification is stubbed with in-file mock schemes — this demonstrates the +**protocol handshake**, not settlement. + +## Run it + +```bash +pnpm install # from the repo root +pnpm --filter s402-demo-api dev +``` + +Then, in another terminal: + +```bash +curl -i http://localhost:3402/api/joke +``` + +**Expected — verified 2026-08-22:** + +``` +HTTP/1.1 402 Payment Required +payment-required: eyJzNDAyVmVyc2lvbiI6IjEiLCJhY2NlcHRzIjpbImV4YWN0Il0s… +``` + +That header is the entire protocol surface a client needs. Decode it: + +```bash +curl -sD- -o/dev/null http://localhost:3402/api/joke \ + | grep -i '^payment-required:' | sed 's/^[^:]*: *//' | tr -d '\r' \ + | base64 -d | python3 -m json.tool +``` + +```json +{ + "s402Version": "1", + "accepts": ["exact"], + "network": "sui:testnet", + "asset": "0x2::sui::SUI", + "amount": "1000000", + "payTo": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +## What is here + +| Route | Price | Paid? | +|---|---|---| +| `/` | — | Browser UI | +| `/api/catalog` | — | Free — lists every endpoint and its price | +| `/api/stats` | — | Free | +| `/api/joke` | 0.001 SUI | **402** | +| `/api/wisdom` | 0.005 SUI | **402** | +| `/api/alpha` | see catalog | **402** | + +## What this proves, and what it does not + +**Proves:** the server helpers build a well-formed 402, and the header encoding survives a real +HTTP round trip to a real client. + +**Does not prove:** that anyone got paid. The facilitator here is a mock that approves +everything, and `payTo` is 64 a's — deliberately not a real address. Settlement needs +[`@sweefi/sui`](https://www.npmjs.com/package/@sweefi/sui) and a chain. + +## Configuration + +All optional; the defaults run standalone. + +| Env var | Default | +|---|---| +| `PORT` | `3402` | +| `S402_NETWORK` | `sui:testnet` | +| `S402_PAY_TO` | `0x` + 64 × `a` | + +## Smaller demo + +To see the wire format and the conformance vectors without starting a server, run `pnpm demo` +from the repo root instead. It is faster and covers the encoder, the x402 compat layer, and all +167 vectors. diff --git a/demo-api/package.json b/demo-api/package.json index 50a41f1..8e21dfb 100644 --- a/demo-api/package.json +++ b/demo-api/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "type": "module", "private": true, - "description": "Demo API showcasing the s402 protocol — pay for endpoints with SUI", + "description": "Demo API showcasing the s402 protocol \u2014 pay for endpoints with SUI", "scripts": { "dev": "tsx src/server.ts", "build": "tsc", @@ -11,7 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "s402": "^0.6.0" + "s402": "workspace:*" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/demo-api/src/server.ts b/demo-api/src/server.ts index 763bbf6..9f81268 100644 --- a/demo-api/src/server.ts +++ b/demo-api/src/server.ts @@ -295,7 +295,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise if (url === '/api/catalog') { json(res, 200, { protocol: 's402', - version: '0.3', + version: S402_VERSION, network: NETWORK, endpoints: routes.map((r) => ({ path: r.path, diff --git a/package.json b/package.json index b3c27fc..53e14ad 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "private": true, "name": "s402-protocol", "type": "module", - "description": "s402 protocol monorepo — spec, TypeScript, Python", + "description": "s402 protocol monorepo \u2014 spec, TypeScript, Python", "scripts": { + "demo": "pnpm --filter s402 demo", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 241fb39..68410f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,22 @@ importers: specifier: ^1.6.4 version: 1.6.4(@algolia/client-search@5.49.2)(@types/node@25.6.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@6.0.3) + demo-api: + dependencies: + s402: + specifier: workspace:* + version: link:../typescript + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + mcp-demo: dependencies: '@hono/node-server': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06c4c94..0d42e0e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'typescript' - 'server-ts' - 'mcp-demo' + - 'demo-api' diff --git a/typescript/CHANGELOG.md b/typescript/CHANGELOG.md index 83ec37c..9c455d3 100644 --- a/typescript/CHANGELOG.md +++ b/typescript/CHANGELOG.md @@ -9,6 +9,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Every `s402/compat` import in the README failed at module resolution.** The README told you + to `import { normalizeRequirements, … } from 's402/compat'` in four places — the opening + paragraph, the compat example, the sub-path export table, and Design Principle 2. That subpath + is not exported; copying the example produced + `ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './compat' is not defined by "exports"`. The + real path is **`s402/compat/x402`**, which exports all five documented functions. The sub-path + table was also missing six real entries (`server`, `receipts`, `extensions`, `test-utils`, + `compat/mpp`, `compat/l402`). +- **The README described a published package as unreleased.** Design Principle 1 called the Sui + reference implementation `@sweefi/sui`, *"coming soon"* — while `@sweefi/sui` is on npm and + `@sweefi/server` alongside it. The same document simultaneously described that implementation + in the **present tense** two paragraphs earlier, so one fact appeared in two tenses. Both now + say what is true: it is published, and it lives in a separate package. +- **The demo API misreported the protocol version it speaks.** `GET /api/catalog` returned a + hardcoded `"version": "0.3"` while `S402_VERSION` is `"1"`. Anyone curling the catalog to see + what the protocol advertises got a number that contradicts every payload the same server emits. +- **Gas figures were stated without their conditions.** The comparison table presented modelled + estimates as flat facts and omitted the case the project's own whitepaper is careful to name: + **x402 on Solana (~$0.25 per 1K calls) is cheaper than s402 Exact on Sui (~$7.00) for one-shot + calls.** The table now marks the numbers as modelled, links the method, and states where a + competitor wins. + +### Added + +- **One demo you can run in sixty seconds, with no wallet, no keys and no network** — `pnpm demo`. + It encodes a 402 into a single HTTP header, decodes it from the client side, absorbs an x402 V1 + payment body through the compat layer, and runs the 167 published conformance vectors against + the code in your clone. Source: `typescript/examples/quickstart.mjs`. + The demo reports **how many malformed headers were refused *and how many leaked through***, + because a suite that only ever watches things pass cannot distinguish a working validator from + one that returns `true` unconditionally. That counter was itself verified by poisoning a vector + and confirming the run turns red and exits non-zero. +- **A README that says what is true today, and how to check it.** Two new sections: *What Is True + Today* (shipped / published / partial / not-runnable, per component) and *Receipts* (a command + for every claim the README makes about itself). +- **`demo-api/` is reachable again.** It was absent from `pnpm-workspace.yaml`, had no README, and + depended on a published `s402@^0.6.0` while this repo ships `0.9.0` — so it was neither wired to + the workspace nor documented anywhere. It is now a workspace package on `workspace:*`, it + typechecks against the current source, and it has a README with the verified route (`/api/joke`, + not `/api/data`), the decoded 402 body, and an explicit statement that its facilitator is a mock + that settles nothing. + - **The documented size of the conformance suite was wrong everywhere it appeared.** The repo stated it three different ways — `README.md` said 133 vectors, `docs/specification.md` said 161 in two places — while `spec/vectors/` holds **167 vectors across 14 files**. All three now diff --git a/typescript/examples/quickstart.mjs b/typescript/examples/quickstart.mjs new file mode 100644 index 0000000..fd9ae3d --- /dev/null +++ b/typescript/examples/quickstart.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * s402 quickstart — the one demo. + * + * Runs offline. No keys, no wallet, no network, no Sui node. That is the point: + * s402 is a wire format, so the thing worth showing a stranger is the wire. + * + * pnpm demo (from the repo root) + * + * Four things get proven, in order: + * 1. a server builds a 402 and encodes it to a header + * 2. a client decodes that header and can read what it is being asked to pay + * 3. an x402 V1 body normalizes into s402 through the compat layer + * 4. the published conformance vectors run against this build — including + * malformed input that MUST be rejected + * + * Step 4's second half is the one that matters. A suite that only ever watches + * things pass cannot tell a working validator from a validator that returns + * true unconditionally. + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { encodePaymentRequired, decodePaymentRequired } from '../dist/index.mjs'; +import { normalizeRequirements, isX402 } from '../dist/compat/x402.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const VECTORS = join(HERE, '..', '..', 'spec', 'vectors'); + +const h1 = (s) => console.log(`\n\x1b[1m${s}\x1b[0m\n${'─'.repeat(s.length)}`); +const ok = (s) => console.log(` \x1b[32m✓\x1b[0m ${s}`); +const bad = (s) => console.log(` \x1b[31m✗\x1b[0m ${s}`); + +let failures = 0; + +// ── 1. Server: charge for an endpoint ──────────────────────────────────────── +h1('1. A server asks to be paid'); + +const requirements = { + s402Version: '1', + accepts: ['exact', 'stream'], + network: 'sui:mainnet', + asset: '0x2::sui::SUI', + amount: '1000000', // 0.001 SUI, denominated in MIST + payTo: '0x0000000000000000000000000000000000000000000000000000000000000001', +}; + +const header = encodePaymentRequired(requirements); +console.log(' HTTP/1.1 402 Payment Required'); +console.log(` payment-required: ${header.slice(0, 64)}…`); +console.log(` (${header.length} chars of base64 JSON — one header, no body needed)`); + +// ── 2. Client: read the 402 ────────────────────────────────────────────────── +h1('2. A client reads it, with no shared code'); + +const decoded = decodePaymentRequired(header); +console.log(` wants : ${decoded.amount} MIST of ${decoded.asset}`); +console.log(` on : ${decoded.network}`); +console.log(` pay to : ${decoded.payTo.slice(0, 18)}…`); +console.log(` will take : ${decoded.accepts.join(' or ')}`); + +const roundTripped = JSON.stringify(decoded) === JSON.stringify(requirements); +roundTripped ? ok('round-trip is byte-exact') : (failures++, bad('round-trip LOST data')); + +// ── 3. Compat: accept an x402 payer ────────────────────────────────────────── +h1('3. An x402 client turns up instead'); + +const x402Body = { + x402Version: 1, + scheme: 'exact', + network: 'base-mainnet', + maxAmountRequired: '10000', + asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + payTo: '0x0000000000000000000000000000000000000001', + resource: 'https://api.example.com/data', +}; + +console.log(` detected as x402 : ${isX402(x402Body)}`); +const normalized = normalizeRequirements(x402Body); +console.log(` normalized : amount=${normalized.amount} network=${normalized.network}`); +isX402(x402Body) && normalized.amount === '10000' + ? ok('x402 V1 absorbed without the caller knowing which protocol arrived') + : (failures++, bad('compat normalization did not behave as documented')); + +// ── 4. Conformance, both directions ────────────────────────────────────────── +h1('4. The published conformance vectors, run against this build'); + +const load = (f) => JSON.parse(readFileSync(join(VECTORS, f), 'utf8')); + +// 4a. positive — encoding must match the published header byte for byte +const enc = load('requirements-encode.json'); +let encPass = 0; +for (const v of enc) { + if (v.shouldReject) continue; + if (encodePaymentRequired(v.input) === v.expected.header) encPass++; + else { failures++; bad(`encode mismatch: ${v.description}`); } +} +ok(`${encPass}/${enc.filter((v) => !v.shouldReject).length} encode vectors match the published bytes`); + +// 4b. NEGATIVE CONTROL — malformed input must be refused +const rej = load('validation-reject.json'); +let rejected = 0; +let leaked = 0; +for (const v of rej) { + if (!v.shouldReject || !v.input?.header) continue; + try { + decodePaymentRequired(v.input.header); + leaked++; + bad(`ACCEPTED malformed input it should have rejected: ${v.description}`); + } catch { + rejected++; + } +} +failures += leaked; +ok(`${rejected} malformed headers refused, ${leaked} leaked through`); +console.log( + '\n \x1b[2mThat second number is the one to read. A validator that never\n' + + ' rejects anything would score full marks on the first.\x1b[0m' +); + +// ── verdict ────────────────────────────────────────────────────────────────── +h1('Result'); +if (failures === 0) { + console.log(' \x1b[32mAll checks passed.\x1b[0m Nothing above touched a network or a key.'); + console.log('\n What this proves: the wire format encodes, decodes, absorbs x402,'); + console.log(' and refuses malformed input — on the code in this clone.'); + console.log(' What it does NOT prove: anything about settlement on Sui.'); + console.log(' That lives in @sweefi/sui and needs a chain. See the README.\n'); +} else { + console.log(` \x1b[31m${failures} check(s) FAILED.\x1b[0m This build does not match its own published vectors.\n`); + process.exitCode = 1; +} diff --git a/typescript/package.json b/typescript/package.json index b96062e..37819dc 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -2,7 +2,7 @@ "name": "s402", "version": "0.9.0", "type": "module", - "description": "s402 — Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for six payment schemes. Wire-compatible with x402. Zero runtime dependencies.", + "description": "s402 \u2014 Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for six payment schemes. Wire-compatible with x402. Zero runtime dependencies.", "license": "Apache-2.0", "author": "SweeInc (https://s402-protocol.org)", "repository": { @@ -127,6 +127,7 @@ }, "scripts": { "build": "tsdown", + "demo": "npm run build && node examples/quickstart.mjs", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest",