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",