s402 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-03-02
9
+
10
+ ### Added
11
+
12
+ - **Conformance test vectors ship in npm package** — 133 machine-readable JSON test vectors across 12 files now included via `test/conformance/vectors`. Cross-language implementors (Go, Python, Rust) can `npm pack s402` to get the vectors without cloning the repo.
13
+ - **API stability declaration** — `API-STABILITY.md` classifies all 83 exports as stable, experimental, or internal.
14
+
15
+ ### Fixed
16
+
17
+ - Barrel export JSDoc updated to chain-agnostic wording (was "Sui-native").
18
+
8
19
  ## [0.2.0] - 2026-03-01
9
20
 
10
21
  ### Added
@@ -27,7 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
38
 
28
39
  - **BREAKING**: `s402RouteConfig.asset` is now required (was optional).
29
40
  - JSDoc on `s402PaymentRequirements` updated to chain-agnostic wording (network, asset, amount fields).
30
- - 258 tests across 11 suites (was 207 at v0.1.0).
41
+ - **Conformance test suite** — 133 machine-readable JSON test vectors across 12 files for cross-language implementation verification. Covers encode/decode, body transport, compat normalization, receipt format/parse, validation rejection, key-stripping, and roundtrip identity. Vectors ship in the npm package.
42
+ - **API stability declaration** — `API-STABILITY.md` classifies all 83 exports as stable/experimental/internal.
43
+ - 405 tests across 12 suites (was 207 at v0.1.0).
31
44
 
32
45
  ## [0.1.8] - 2026-02-27
33
46
 
@@ -116,6 +129,7 @@ _Version bump for npm publish after license change._
116
129
  - Property-based fuzz testing via fast-check
117
130
  - 207 tests, zero runtime dependencies
118
131
 
132
+ [0.2.1]: https://github.com/s402-protocol/core/compare/v0.2.0...v0.2.1
119
133
  [0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
120
134
  [0.1.8]: https://github.com/s402-protocol/core/compare/v0.1.7...v0.1.8
121
135
  [0.1.7]: https://github.com/s402-protocol/core/compare/v0.1.6...v0.1.7
package/README.md CHANGED
@@ -36,6 +36,18 @@ HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a pay
36
36
 
37
37
  **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.
38
38
 
39
+ ## Which Scheme Should I Use?
40
+
41
+ | Your situation | Scheme | Gas per 1K calls | Latency |
42
+ |---|---|---|---|
43
+ | One-time API call, simplest path | **Exact** | $7.00 | ~400ms |
44
+ | High-frequency API (10+ calls) | **Prepaid** | $0.014 | ~0ms per call |
45
+ | Buyer needs dispute protection | **Escrow** | $7.00 | ~400ms |
46
+ | Selling encrypted content | **Unlock** | $7.00 | ~400ms |
47
+ | Real-time billing (per-second) | **Stream** | variable | ~400ms setup |
48
+
49
+ **Quick decision:** Use **Prepaid** for AI agents making repeated API calls. Use **Exact** for everything else (it's the x402-compatible default). See the [full guide](https://s402-protocol.org/guide/which-scheme) for details.
50
+
39
51
  ## Architecture
40
52
 
41
53
  ```
@@ -325,6 +337,20 @@ const requirements: s402PaymentRequirements = {
325
337
 
326
338
  5. **Errors tell you what to do.** Every error code includes `retryable` (can the client try again?) and `suggestedAction` (what should it do?). Agents can self-recover.
327
339
 
340
+ ## Conformance Testing
341
+
342
+ s402 ships 133 machine-readable JSON test vectors for cross-language conformance. If you're implementing s402 in Go, Python, Rust, or any other language, use these vectors to verify your implementation matches the spec.
343
+
344
+ ```bash
345
+ # Vectors are in the npm package
346
+ ls node_modules/s402/test/conformance/vectors/
347
+
348
+ # Or clone the repo
349
+ ls test/conformance/vectors/
350
+ ```
351
+
352
+ See [`test/conformance/README.md`](./test/conformance/README.md) for the vector format, encoding scheme, and implementation guide.
353
+
328
354
  ## Related
329
355
 
330
356
  - **[SweeFi](https://github.com/sweeinc/sweefi)** — Open-source payment SDK built on s402. 10 packages including PTB builders, MCP tools, CLI, and UI components.
package/dist/compat.d.mts CHANGED
@@ -108,6 +108,20 @@ declare function fromX402Envelope(envelope: x402PaymentRequiredEnvelope): s402Pa
108
108
  * Validates required fields to catch malformed/malicious payloads at the trust boundary.
109
109
  *
110
110
  * Returns a clean object with only known s402 fields — unknown top-level keys are stripped.
111
+ *
112
+ * @param obj - Raw decoded JSON (could be s402, x402 V1, or x402 V2 envelope)
113
+ * @returns Validated s402PaymentRequirements
114
+ * @throws {s402Error} `INVALID_PAYLOAD` if the format is unrecognized or malformed
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import { normalizeRequirements } from 's402/compat';
119
+ *
120
+ * // Works with any format — auto-detects s402 vs x402
121
+ * const rawJson = JSON.parse(atob(header));
122
+ * const requirements = normalizeRequirements(rawJson);
123
+ * // Always returns s402PaymentRequirements regardless of input format
124
+ * ```
111
125
  */
112
126
  declare function normalizeRequirements(obj: Record<string, unknown>): s402PaymentRequirements;
113
127
  //#endregion
package/dist/compat.mjs CHANGED
@@ -127,8 +127,23 @@ function fromX402Envelope(envelope) {
127
127
  * Validates required fields to catch malformed/malicious payloads at the trust boundary.
128
128
  *
129
129
  * Returns a clean object with only known s402 fields — unknown top-level keys are stripped.
130
+ *
131
+ * @param obj - Raw decoded JSON (could be s402, x402 V1, or x402 V2 envelope)
132
+ * @returns Validated s402PaymentRequirements
133
+ * @throws {s402Error} `INVALID_PAYLOAD` if the format is unrecognized or malformed
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { normalizeRequirements } from 's402/compat';
138
+ *
139
+ * // Works with any format — auto-detects s402 vs x402
140
+ * const rawJson = JSON.parse(atob(header));
141
+ * const requirements = normalizeRequirements(rawJson);
142
+ * // Always returns s402PaymentRequirements regardless of input format
143
+ * ```
130
144
  */
131
145
  function normalizeRequirements(obj) {
146
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) throw new s402Error("INVALID_PAYLOAD", `Payment requirements must be a plain object, got ${obj === null ? "null" : Array.isArray(obj) ? "array" : typeof obj}`);
132
147
  if (isS402(obj)) {
133
148
  validateRequirementsShape(obj);
134
149
  return pickRequirementsFields(obj);
package/dist/errors.d.mts CHANGED
@@ -33,10 +33,36 @@ interface s402ErrorInfo {
33
33
  }
34
34
  /**
35
35
  * Create a typed s402 error with recovery hints.
36
+ *
37
+ * @param code - One of the 15 s402 error codes (e.g. 'SETTLEMENT_FAILED')
38
+ * @param message - Optional human-readable message (defaults to the code)
39
+ * @returns Error info object with code, message, retryable flag, and suggestedAction
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const info = createS402Error('INSUFFICIENT_BALANCE');
44
+ * // { code: 'INSUFFICIENT_BALANCE', message: 'INSUFFICIENT_BALANCE',
45
+ * // retryable: false, suggestedAction: 'Top up wallet balance...' }
46
+ * ```
36
47
  */
37
48
  declare function createS402Error(code: s402ErrorCodeType, message?: string): s402ErrorInfo;
38
49
  /**
39
50
  * s402 error class for throwing in code.
51
+ * Every instance includes a machine-readable `code`, `retryable` flag, and `suggestedAction`.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * try {
56
+ * await facilitator.process(payload, requirements);
57
+ * } catch (e) {
58
+ * if (e instanceof s402Error) {
59
+ * console.log(e.code); // 'SETTLEMENT_FAILED'
60
+ * console.log(e.retryable); // true
61
+ * console.log(e.suggestedAction); // 'Transient RPC failure...'
62
+ * if (e.retryable) { } // retry with exponential backoff
63
+ * }
64
+ * }
65
+ * ```
40
66
  */
41
67
  declare class s402Error extends Error {
42
68
  readonly code: s402ErrorCodeType;
package/dist/errors.mjs CHANGED
@@ -89,6 +89,17 @@ const ERROR_HINTS = {
89
89
  };
90
90
  /**
91
91
  * Create a typed s402 error with recovery hints.
92
+ *
93
+ * @param code - One of the 15 s402 error codes (e.g. 'SETTLEMENT_FAILED')
94
+ * @param message - Optional human-readable message (defaults to the code)
95
+ * @returns Error info object with code, message, retryable flag, and suggestedAction
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const info = createS402Error('INSUFFICIENT_BALANCE');
100
+ * // { code: 'INSUFFICIENT_BALANCE', message: 'INSUFFICIENT_BALANCE',
101
+ * // retryable: false, suggestedAction: 'Top up wallet balance...' }
102
+ * ```
92
103
  */
93
104
  function createS402Error(code, message) {
94
105
  const hints = ERROR_HINTS[code];
@@ -101,6 +112,21 @@ function createS402Error(code, message) {
101
112
  }
102
113
  /**
103
114
  * s402 error class for throwing in code.
115
+ * Every instance includes a machine-readable `code`, `retryable` flag, and `suggestedAction`.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * try {
120
+ * await facilitator.process(payload, requirements);
121
+ * } catch (e) {
122
+ * if (e instanceof s402Error) {
123
+ * console.log(e.code); // 'SETTLEMENT_FAILED'
124
+ * console.log(e.retryable); // true
125
+ * console.log(e.suggestedAction); // 'Transient RPC failure...'
126
+ * if (e.retryable) { } // retry with exponential backoff
127
+ * }
128
+ * }
129
+ * ```
104
130
  */
105
131
  var s402Error = class extends Error {
106
132
  code;
package/dist/http.d.mts CHANGED
@@ -1,19 +1,85 @@
1
1
  import { s402PaymentPayload, s402PaymentRequirements, s402SettleResponse } from "./types.mjs";
2
2
 
3
3
  //#region src/http.d.ts
4
- /** Encode payment requirements for the `payment-required` header */
4
+ /**
5
+ * Encode payment requirements for the `payment-required` header.
6
+ *
7
+ * @param requirements - Typed s402 payment requirements
8
+ * @returns Base64-encoded JSON string for the HTTP header
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { encodePaymentRequired } from 's402/http';
13
+ *
14
+ * const header = encodePaymentRequired({
15
+ * s402Version: '1',
16
+ * accepts: ['exact'],
17
+ * network: 'sui:mainnet',
18
+ * asset: '0x2::sui::SUI',
19
+ * amount: '1000000',
20
+ * payTo: '0xYOUR_ADDRESS',
21
+ * });
22
+ * response.headers.set('payment-required', header);
23
+ * ```
24
+ */
5
25
  declare function encodePaymentRequired(requirements: s402PaymentRequirements): string;
6
- /** Encode payment payload for the `x-payment` header */
26
+ /**
27
+ * Encode payment payload for the `x-payment` header.
28
+ *
29
+ * @param payload - Typed s402 payment payload (any scheme)
30
+ * @returns Base64-encoded JSON string for the HTTP header
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { encodePaymentPayload, S402_HEADERS } from 's402/http';
35
+ *
36
+ * const header = encodePaymentPayload(payload);
37
+ * fetch(url, { headers: { [S402_HEADERS.PAYMENT]: header } });
38
+ * ```
39
+ */
7
40
  declare function encodePaymentPayload(payload: s402PaymentPayload): string;
8
41
  /** Encode settlement response for the `payment-response` header */
9
42
  declare function encodeSettleResponse(response: s402SettleResponse): string;
10
43
  /** Return a clean object with only known s402 requirement fields. */
11
44
  declare function pickRequirementsFields(obj: Record<string, unknown>): s402PaymentRequirements;
12
- /** Decode payment requirements from the `payment-required` header */
45
+ /**
46
+ * Decode payment requirements from the `payment-required` header.
47
+ * Validates shape, strips unknown keys, enforces size limit (64KB).
48
+ *
49
+ * @param header - Base64-encoded JSON string from the HTTP header
50
+ * @returns Validated s402 payment requirements
51
+ * @throws {s402Error} `INVALID_PAYLOAD` on oversized header, invalid base64/JSON, or malformed shape
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { decodePaymentRequired } from 's402/http';
56
+ *
57
+ * const header = response.headers.get('payment-required')!;
58
+ * const requirements = decodePaymentRequired(header);
59
+ * console.log(requirements.accepts); // ['exact', 'prepaid']
60
+ * console.log(requirements.amount); // '1000000'
61
+ * ```
62
+ */
13
63
  declare function decodePaymentRequired(header: string): s402PaymentRequirements;
14
64
  /** Return a clean payload object with only known s402 payload fields. */
15
65
  declare function pickPayloadFields(obj: Record<string, unknown>): s402PaymentPayload;
16
- /** Decode payment payload from the `x-payment` header */
66
+ /**
67
+ * Decode payment payload from the `x-payment` header.
68
+ * Validates shape, strips unknown keys, enforces size limit (64KB).
69
+ *
70
+ * @param header - Base64-encoded JSON string from the HTTP header
71
+ * @returns Validated s402 payment payload
72
+ * @throws {s402Error} `INVALID_PAYLOAD` on oversized header, invalid base64/JSON, or malformed shape
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { decodePaymentPayload, S402_HEADERS } from 's402/http';
77
+ *
78
+ * const header = request.headers.get(S402_HEADERS.PAYMENT)!;
79
+ * const payload = decodePaymentPayload(header);
80
+ * console.log(payload.scheme); // 'exact'
81
+ * ```
82
+ */
17
83
  declare function decodePaymentPayload(header: string): s402PaymentPayload;
18
84
  /** Return a clean settle response with only known s402 fields. */
19
85
  declare function pickSettleResponseFields(obj: Record<string, unknown>): s402SettleResponse;
@@ -110,6 +176,22 @@ declare function detectProtocol(headers: Headers): 's402' | 'x402' | 'unknown';
110
176
  * Extract s402 payment requirements from a 402 Response.
111
177
  * Returns null if the header is missing, malformed, or not s402 format.
112
178
  * For x402 responses, decode the header manually and use normalizeRequirements().
179
+ *
180
+ * @param response - Fetch API Response object (status should be 402)
181
+ * @returns Parsed requirements, or null if not an s402 response
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * import { extractRequirementsFromResponse } from 's402/http';
186
+ *
187
+ * const res = await fetch(url);
188
+ * if (res.status === 402) {
189
+ * const requirements = extractRequirementsFromResponse(res);
190
+ * if (requirements) {
191
+ * // Build and send payment
192
+ * }
193
+ * }
194
+ * ```
113
195
  */
114
196
  declare function extractRequirementsFromResponse(response: Response): s402PaymentRequirements | null;
115
197
  //#endregion
package/dist/http.mjs CHANGED
@@ -13,11 +13,44 @@ function fromBase64(b64) {
13
13
  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
14
14
  return new TextDecoder().decode(bytes);
15
15
  }
16
- /** Encode payment requirements for the `payment-required` header */
16
+ /**
17
+ * Encode payment requirements for the `payment-required` header.
18
+ *
19
+ * @param requirements - Typed s402 payment requirements
20
+ * @returns Base64-encoded JSON string for the HTTP header
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { encodePaymentRequired } from 's402/http';
25
+ *
26
+ * const header = encodePaymentRequired({
27
+ * s402Version: '1',
28
+ * accepts: ['exact'],
29
+ * network: 'sui:mainnet',
30
+ * asset: '0x2::sui::SUI',
31
+ * amount: '1000000',
32
+ * payTo: '0xYOUR_ADDRESS',
33
+ * });
34
+ * response.headers.set('payment-required', header);
35
+ * ```
36
+ */
17
37
  function encodePaymentRequired(requirements) {
18
38
  return toBase64(JSON.stringify(requirements));
19
39
  }
20
- /** Encode payment payload for the `x-payment` header */
40
+ /**
41
+ * Encode payment payload for the `x-payment` header.
42
+ *
43
+ * @param payload - Typed s402 payment payload (any scheme)
44
+ * @returns Base64-encoded JSON string for the HTTP header
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { encodePaymentPayload, S402_HEADERS } from 's402/http';
49
+ *
50
+ * const header = encodePaymentPayload(payload);
51
+ * fetch(url, { headers: { [S402_HEADERS.PAYMENT]: header } });
52
+ * ```
53
+ */
21
54
  function encodePaymentPayload(payload) {
22
55
  return toBase64(JSON.stringify(payload));
23
56
  }
@@ -104,7 +137,24 @@ function pickRequirementsFields(obj) {
104
137
  else result[key] = obj[key];
105
138
  return result;
106
139
  }
107
- /** Decode payment requirements from the `payment-required` header */
140
+ /**
141
+ * Decode payment requirements from the `payment-required` header.
142
+ * Validates shape, strips unknown keys, enforces size limit (64KB).
143
+ *
144
+ * @param header - Base64-encoded JSON string from the HTTP header
145
+ * @returns Validated s402 payment requirements
146
+ * @throws {s402Error} `INVALID_PAYLOAD` on oversized header, invalid base64/JSON, or malformed shape
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { decodePaymentRequired } from 's402/http';
151
+ *
152
+ * const header = response.headers.get('payment-required')!;
153
+ * const requirements = decodePaymentRequired(header);
154
+ * console.log(requirements.accepts); // ['exact', 'prepaid']
155
+ * console.log(requirements.amount); // '1000000'
156
+ * ```
157
+ */
108
158
  function decodePaymentRequired(header) {
109
159
  if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `payment-required header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
110
160
  let parsed;
@@ -160,7 +210,23 @@ function pickPayloadFields(obj) {
160
210
  }
161
211
  return result;
162
212
  }
163
- /** Decode payment payload from the `x-payment` header */
213
+ /**
214
+ * Decode payment payload from the `x-payment` header.
215
+ * Validates shape, strips unknown keys, enforces size limit (64KB).
216
+ *
217
+ * @param header - Base64-encoded JSON string from the HTTP header
218
+ * @returns Validated s402 payment payload
219
+ * @throws {s402Error} `INVALID_PAYLOAD` on oversized header, invalid base64/JSON, or malformed shape
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * import { decodePaymentPayload, S402_HEADERS } from 's402/http';
224
+ *
225
+ * const header = request.headers.get(S402_HEADERS.PAYMENT)!;
226
+ * const payload = decodePaymentPayload(header);
227
+ * console.log(payload.scheme); // 'exact'
228
+ * ```
229
+ */
164
230
  function decodePaymentPayload(header) {
165
231
  if (header.length > MAX_HEADER_BYTES) throw new s402Error("INVALID_PAYLOAD", `x-payment header exceeds maximum size (${header.length} > ${MAX_HEADER_BYTES})`);
166
232
  let parsed;
@@ -374,6 +440,19 @@ function validateRequirementsShape(obj) {
374
440
  if (record.facilitatorUrl !== void 0) {
375
441
  if (typeof record.facilitatorUrl !== "string") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must be a string, got ${typeof record.facilitatorUrl}`);
376
442
  if (/[\x00-\x1f\x7f]/.test(record.facilitatorUrl)) throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl contains control characters (potential header injection)");
443
+ try {
444
+ const url = new URL(record.facilitatorUrl);
445
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new s402Error("INVALID_PAYLOAD", `facilitatorUrl must use https:// or http://, got "${url.protocol}"`);
446
+ } catch (e) {
447
+ if (e instanceof s402Error) throw e;
448
+ throw new s402Error("INVALID_PAYLOAD", "facilitatorUrl is not a valid URL");
449
+ }
450
+ }
451
+ if (record.settlementMode !== void 0) {
452
+ if (record.settlementMode !== "facilitator" && record.settlementMode !== "direct") throw new s402Error("INVALID_PAYLOAD", `settlementMode must be "facilitator" or "direct", got ${JSON.stringify(record.settlementMode)}`);
453
+ }
454
+ if (record.receiptRequired !== void 0) {
455
+ if (typeof record.receiptRequired !== "boolean") throw new s402Error("INVALID_PAYLOAD", `receiptRequired must be a boolean, got ${typeof record.receiptRequired}`);
377
456
  }
378
457
  validateSubObjects(record);
379
458
  }
@@ -392,11 +471,21 @@ function validatePayloadShape(obj) {
392
471
  if (typeof inner.signature !== "string") throw new s402Error("INVALID_PAYLOAD", `payload.signature must be a string, got ${typeof inner.signature}`);
393
472
  if (record.scheme === "unlock" && typeof inner.encryptionId !== "string") throw new s402Error("INVALID_PAYLOAD", `unlock payload requires encryptionId (string), got ${typeof inner.encryptionId}`);
394
473
  if (record.scheme === "prepaid" && typeof inner.ratePerCall !== "string") throw new s402Error("INVALID_PAYLOAD", `prepaid payload requires ratePerCall (string), got ${typeof inner.ratePerCall}`);
474
+ if (record.scheme === "prepaid" && inner.maxCalls !== void 0 && typeof inner.maxCalls !== "string") throw new s402Error("INVALID_PAYLOAD", `prepaid payload maxCalls must be a string if provided, got ${typeof inner.maxCalls}`);
395
475
  }
396
476
  /** Validate that a decoded settle response has the required shape. */
397
477
  function validateSettleShape(obj) {
398
478
  if (obj == null || typeof obj !== "object") throw new s402Error("INVALID_PAYLOAD", "Settle response is not an object");
399
- if (typeof obj.success !== "boolean") throw new s402Error("INVALID_PAYLOAD", "Malformed settle response: missing or invalid \"success\" (boolean)");
479
+ const record = obj;
480
+ if (typeof record.success !== "boolean") throw new s402Error("INVALID_PAYLOAD", "Malformed settle response: missing or invalid \"success\" (boolean)");
481
+ if (record.txDigest !== void 0 && typeof record.txDigest !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: txDigest must be a string, got ${typeof record.txDigest}`);
482
+ if (record.receiptId !== void 0 && typeof record.receiptId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: receiptId must be a string, got ${typeof record.receiptId}`);
483
+ if (record.finalityMs !== void 0 && (typeof record.finalityMs !== "number" || !Number.isFinite(record.finalityMs))) throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: finalityMs must be a finite number, got ${typeof record.finalityMs}`);
484
+ if (record.streamId !== void 0 && typeof record.streamId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: streamId must be a string, got ${typeof record.streamId}`);
485
+ if (record.escrowId !== void 0 && typeof record.escrowId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: escrowId must be a string, got ${typeof record.escrowId}`);
486
+ if (record.balanceId !== void 0 && typeof record.balanceId !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: balanceId must be a string, got ${typeof record.balanceId}`);
487
+ if (record.error !== void 0 && typeof record.error !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: error must be a string, got ${typeof record.error}`);
488
+ if (record.errorCode !== void 0 && typeof record.errorCode !== "string") throw new s402Error("INVALID_PAYLOAD", `Malformed settle response: errorCode must be a string, got ${typeof record.errorCode}`);
400
489
  }
401
490
  /** Content type for s402 JSON body transport */
402
491
  const S402_CONTENT_TYPE = "application/s402+json";
@@ -481,6 +570,22 @@ function detectProtocol(headers) {
481
570
  * Extract s402 payment requirements from a 402 Response.
482
571
  * Returns null if the header is missing, malformed, or not s402 format.
483
572
  * For x402 responses, decode the header manually and use normalizeRequirements().
573
+ *
574
+ * @param response - Fetch API Response object (status should be 402)
575
+ * @returns Parsed requirements, or null if not an s402 response
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * import { extractRequirementsFromResponse } from 's402/http';
580
+ *
581
+ * const res = await fetch(url);
582
+ * if (res.status === 402) {
583
+ * const requirements = extractRequirementsFromResponse(res);
584
+ * if (requirements) {
585
+ * // Build and send payment
586
+ * }
587
+ * }
588
+ * ```
484
589
  */
485
590
  function extractRequirementsFromResponse(response) {
486
591
  const header = response.headers.get(S402_HEADERS.PAYMENT_REQUIRED);
package/dist/index.d.mts CHANGED
@@ -102,8 +102,19 @@ declare class s402Client {
102
102
  /**
103
103
  * Register a scheme implementation for a network.
104
104
  *
105
- * @param network - Sui network (e.g., "sui:testnet")
106
- * @param scheme - Scheme implementation
105
+ * @param network - Network identifier (e.g., "sui:testnet", "sui:mainnet")
106
+ * @param scheme - Scheme implementation (from @sweefi/sui or your own)
107
+ * @returns `this` for chaining
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { s402Client } from 's402';
112
+ *
113
+ * const client = new s402Client();
114
+ * client
115
+ * .register('sui:mainnet', exactScheme)
116
+ * .register('sui:mainnet', prepaidScheme);
117
+ * ```
107
118
  */
108
119
  register(network: string, scheme: s402ClientScheme): this;
109
120
  /**
@@ -114,6 +125,23 @@ declare class s402Client {
114
125
  *
115
126
  * Accepts typed s402PaymentRequirements only. For x402 input, normalize
116
127
  * first via `normalizeRequirements()` from 's402/compat'.
128
+ *
129
+ * @param requirements - Server's payment requirements (from a 402 response)
130
+ * @returns Payment payload ready to send in the `x-payment` header
131
+ * @throws {s402Error} `NETWORK_MISMATCH` if no schemes registered for the network
132
+ * @throws {s402Error} `SCHEME_NOT_SUPPORTED` if no registered scheme matches server's accepts
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * import { s402Client, decodePaymentRequired, encodePaymentPayload, S402_HEADERS } from 's402';
137
+ *
138
+ * const client = new s402Client();
139
+ * client.register('sui:mainnet', exactScheme);
140
+ *
141
+ * const requirements = decodePaymentRequired(res.headers.get('payment-required')!);
142
+ * const payload = await client.createPayment(requirements);
143
+ * fetch(url, { headers: { [S402_HEADERS.PAYMENT]: encodePaymentPayload(payload) } });
144
+ * ```
117
145
  */
118
146
  createPayment(requirements: s402PaymentRequirements): Promise<s402PaymentPayload>;
119
147
  /**
@@ -141,12 +169,33 @@ declare class s402Facilitator {
141
169
  */
142
170
  settle(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
143
171
  /**
144
- * Expiration-guarded verify + settle in one call.
172
+ * Expiration-guarded verify + settle in one call. **This is the recommended path.**
145
173
  * Rejects expired requirements, verifies the payload, then settles.
174
+ * Includes deduplication (prevents concurrent identical requests) and timeouts
175
+ * (5s verify, 15s settle).
146
176
  *
147
177
  * Note: True atomicity comes from Sui's PTBs in the scheme implementation,
148
178
  * not from this method. This method provides the expiration guard and
149
- * sequential verifysettle orchestration.
179
+ * sequential verify-then-settle orchestration.
180
+ *
181
+ * @param payload - Client's payment payload
182
+ * @param requirements - Server's payment requirements
183
+ * @returns Settlement result (check `result.success` and `result.errorCode`)
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { s402Facilitator } from 's402';
188
+ *
189
+ * const facilitator = new s402Facilitator();
190
+ * facilitator.register('sui:mainnet', exactFacilitatorScheme);
191
+ *
192
+ * const result = await facilitator.process(payload, requirements);
193
+ * if (result.success) {
194
+ * console.log(result.txDigest); // Sui transaction digest
195
+ * } else {
196
+ * console.log(result.errorCode); // e.g. 'VERIFICATION_FAILED'
197
+ * }
198
+ * ```
150
199
  */
151
200
  process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
152
201
  /**
@@ -176,6 +225,25 @@ declare class s402ResourceServer {
176
225
  * Build payment requirements for a route.
177
226
  * Uses the first scheme in the config's schemes array.
178
227
  * Always includes "exact" in accepts for x402 compat.
228
+ *
229
+ * @param config - Per-route payment configuration (schemes, price, network, payTo, asset)
230
+ * @returns Payment requirements ready to encode for the 402 response
231
+ * @throws {s402Error} `INVALID_PAYLOAD` if price is not a valid non-negative integer string
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * import { s402ResourceServer, encodePaymentRequired } from 's402';
236
+ *
237
+ * const server = new s402ResourceServer();
238
+ * const requirements = server.buildRequirements({
239
+ * schemes: ['exact'],
240
+ * price: '1000000',
241
+ * network: 'sui:mainnet',
242
+ * payTo: '0xYOUR_ADDRESS',
243
+ * asset: '0x2::sui::SUI',
244
+ * });
245
+ * res.status(402).setHeader('payment-required', encodePaymentRequired(requirements));
246
+ * ```
179
247
  */
180
248
  buildRequirements(config: s402RouteConfig): s402PaymentRequirements;
181
249
  /**
@@ -191,6 +259,20 @@ declare class s402ResourceServer {
191
259
  * Expiration-guarded verify + settle. This is the recommended path.
192
260
  * Rejects expired requirements, verifies the payload, then settles.
193
261
  * True atomicity comes from Sui PTBs in the scheme implementation.
262
+ *
263
+ * @param payload - Client's payment payload (from the `x-payment` header)
264
+ * @param requirements - The requirements this server originally sent
265
+ * @returns Settlement result with txDigest on success
266
+ * @throws {s402Error} `FACILITATOR_UNAVAILABLE` if no facilitator is configured
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * const result = await server.process(payload, requirements);
271
+ * if (result.success) {
272
+ * // Serve the protected resource
273
+ * res.status(200).json({ data: 'paid content' });
274
+ * }
275
+ * ```
194
276
  */
195
277
  process(payload: s402PaymentPayload, requirements: s402PaymentRequirements): Promise<s402SettleResponse>;
196
278
  }