@shroud-fi/x402 0.1.3 → 0.1.5

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.
Files changed (50) hide show
  1. package/package.json +9 -6
  2. package/src/client.ts +261 -0
  3. package/src/constants.ts +18 -0
  4. package/src/errors.ts +78 -0
  5. package/src/facilitator.ts +211 -0
  6. package/src/index.ts +59 -0
  7. package/src/protocol.ts +154 -0
  8. package/src/server.ts +313 -0
  9. package/src/signing.ts +176 -0
  10. package/src/types.ts +102 -0
  11. package/tsconfig.json +9 -0
  12. package/dist/cjs/client.d.ts.map +0 -1
  13. package/dist/cjs/client.js.map +0 -1
  14. package/dist/cjs/constants.d.ts.map +0 -1
  15. package/dist/cjs/constants.js.map +0 -1
  16. package/dist/cjs/errors.d.ts.map +0 -1
  17. package/dist/cjs/errors.js.map +0 -1
  18. package/dist/cjs/facilitator.d.ts.map +0 -1
  19. package/dist/cjs/facilitator.js.map +0 -1
  20. package/dist/cjs/index.d.ts.map +0 -1
  21. package/dist/cjs/index.js.map +0 -1
  22. package/dist/cjs/protocol.d.ts.map +0 -1
  23. package/dist/cjs/protocol.js.map +0 -1
  24. package/dist/cjs/server.d.ts.map +0 -1
  25. package/dist/cjs/server.js.map +0 -1
  26. package/dist/cjs/signing.d.ts.map +0 -1
  27. package/dist/cjs/signing.js.map +0 -1
  28. package/dist/cjs/types.d.ts.map +0 -1
  29. package/dist/cjs/types.js.map +0 -1
  30. package/dist/esm/client.d.ts.map +0 -1
  31. package/dist/esm/client.js.map +0 -1
  32. package/dist/esm/constants.d.ts.map +0 -1
  33. package/dist/esm/constants.js.map +0 -1
  34. package/dist/esm/errors.d.ts.map +0 -1
  35. package/dist/esm/errors.js.map +0 -1
  36. package/dist/esm/facilitator.d.ts.map +0 -1
  37. package/dist/esm/facilitator.js.map +0 -1
  38. package/dist/esm/index.d.ts.map +0 -1
  39. package/dist/esm/index.js.map +0 -1
  40. package/dist/esm/protocol.d.ts.map +0 -1
  41. package/dist/esm/protocol.js.map +0 -1
  42. package/dist/esm/server.d.ts.map +0 -1
  43. package/dist/esm/server.js.map +0 -1
  44. package/dist/esm/signing.d.ts.map +0 -1
  45. package/dist/esm/signing.js.map +0 -1
  46. package/dist/esm/types.d.ts.map +0 -1
  47. package/dist/esm/types.js.map +0 -1
  48. package/dist/tsconfig.cjs.tsbuildinfo +0 -1
  49. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  50. package/dist/tsconfig.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shroud-fi/x402",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Stealth-addressed HTTP 402 payments for AI agents. EIP-3009 settlement into one-time stealth addresses.",
5
5
  "keywords": [
6
6
  "shroudfi",
@@ -27,13 +27,16 @@
27
27
  }
28
28
  },
29
29
  "files": [
30
- "dist"
30
+ "dist",
31
+ "src",
32
+ "tsconfig.json",
33
+ "README.md"
31
34
  ],
32
35
  "dependencies": {
33
36
  "@noble/hashes": "^1.5.0",
34
- "@shroud-fi/core": "0.1.1",
35
- "@shroud-fi/payments": "0.1.1",
36
- "@shroud-fi/transport": "0.1.2"
37
+ "@shroud-fi/payments": "0.1.3",
38
+ "@shroud-fi/core": "0.1.3",
39
+ "@shroud-fi/transport": "0.1.4"
37
40
  },
38
41
  "peerDependencies": {
39
42
  "viem": "^2.21.0"
@@ -42,7 +45,7 @@
42
45
  "viem": "^2.21.0",
43
46
  "vitest": "^2.0.0",
44
47
  "typescript": "^5.6.0",
45
- "@shroud-fi/scanning": "0.1.1"
48
+ "@shroud-fi/scanning": "0.1.3"
46
49
  },
47
50
  "publishConfig": {
48
51
  "access": "public"
package/src/client.ts ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * x402 client — fetch wrapper that auto-handles 402 challenges.
3
+ *
4
+ * Flow:
5
+ * 1. Issue initial fetch.
6
+ * 2. If status !== 402, return as-is.
7
+ * 3. Parse X-PAYMENT-REQUIRED, validate scheme + asset + safety cap.
8
+ * 4. Sign EIP-3009 TransferWithAuthorization for USDC.
9
+ * 5. Re-issue request with `X-PAYMENT: base64(JSON(payload))`.
10
+ * 6. On 2xx, surface `x402Settlement` on the returned Response if the
11
+ * server attached X-PAYMENT-RESPONSE.
12
+ *
13
+ * Privacy invariants:
14
+ * - The signed payload is constructed in memory and forwarded directly to
15
+ * the server. We never log or serialize the signature, nonce, or value.
16
+ * - The client wallet address is unavoidably visible (it IS the `from`).
17
+ * This is the agent's stealth-payer EOA, not a persistent identity if
18
+ * used by ShroudAgent.
19
+ * - Errors carry no amount, no signature, no nonce bytes.
20
+ */
21
+
22
+ import type { Hex } from 'viem';
23
+ import { getEip3009Token } from '@shroud-fi/transport';
24
+ import {
25
+ X402_PAYMENT_REQUIRED_HEADER,
26
+ X402_PAYMENT_RESPONSE_HEADER,
27
+ X402_PAYMENT_SIGNATURE_HEADER,
28
+ X402_SCHEME_EXACT,
29
+ type X402PaymentPayload,
30
+ type X402PaymentRequirements,
31
+ } from './protocol.js';
32
+ import { X402_VERSION, DEFAULT_AUTHORIZATION_TTL_SECS } from './constants.js';
33
+ import {
34
+ X402AmountMismatchError,
35
+ X402AssetNotSupportedError,
36
+ X402InvalidChallengeError,
37
+ } from './errors.js';
38
+ import type { X402ClientConfig, X402PaymentResult } from './types.js';
39
+ import {
40
+ generateAuthorizationNonce,
41
+ signTransferWithAuthorization,
42
+ } from './signing.js';
43
+
44
+ /**
45
+ * The augmented Response surface. We can't subclass `Response` in a portable
46
+ * way, so we attach `x402Settlement` as a non-standard own-property.
47
+ */
48
+ export interface X402Response extends Response {
49
+ x402Settlement?: X402PaymentResult;
50
+ }
51
+
52
+ export interface X402Client {
53
+ /**
54
+ * Wrap `fetch` with 402 auto-pay. The returned Response is augmented with
55
+ * `x402Settlement` when a payment was made.
56
+ *
57
+ * @param maxPriceAtomic — safety cap. If the server demands more, the
58
+ * client throws X402AmountMismatchError without signing anything.
59
+ */
60
+ fetch(
61
+ input: string | URL | Request,
62
+ init?: RequestInit & { maxPriceAtomic?: bigint },
63
+ ): Promise<X402Response>;
64
+ }
65
+
66
+ /** Decode a 402 challenge body from the response. Tries header, then body. */
67
+ async function readChallenge(
68
+ res: Response,
69
+ ): Promise<{ accepts: readonly X402PaymentRequirements[] }> {
70
+ // Header path (spec-canonical) first.
71
+ const header = res.headers.get(X402_PAYMENT_REQUIRED_HEADER);
72
+ if (header !== null && header.length > 0) {
73
+ try {
74
+ const parsed = JSON.parse(header) as {
75
+ accepts: readonly X402PaymentRequirements[];
76
+ };
77
+ if (Array.isArray(parsed.accepts) && parsed.accepts.length > 0) {
78
+ return parsed;
79
+ }
80
+ } catch {
81
+ // fall through to body parse
82
+ }
83
+ }
84
+ // Body fallback — some servers put the envelope in the body only.
85
+ try {
86
+ const text = await res.clone().text();
87
+ const parsed = JSON.parse(text) as {
88
+ accepts: readonly X402PaymentRequirements[];
89
+ };
90
+ if (Array.isArray(parsed.accepts) && parsed.accepts.length > 0) {
91
+ return parsed;
92
+ }
93
+ } catch {
94
+ // fall through
95
+ }
96
+ throw new X402InvalidChallengeError();
97
+ }
98
+
99
+ /** Decode the X-PAYMENT-RESPONSE header into a settlement record. */
100
+ function readSettlement(res: Response): X402PaymentResult | undefined {
101
+ const header = res.headers.get(X402_PAYMENT_RESPONSE_HEADER);
102
+ if (header === null || header.length === 0) return undefined;
103
+ try {
104
+ // Header is base64-encoded JSON per the spec.
105
+ const decoded = Buffer.from(header, 'base64').toString('utf-8');
106
+ const parsed = JSON.parse(decoded) as {
107
+ transaction?: string;
108
+ txHash?: string;
109
+ settledAt?: number;
110
+ };
111
+ const txHash =
112
+ (parsed.transaction ?? parsed.txHash) as Hex | undefined;
113
+ if (txHash === undefined) return undefined;
114
+ return {
115
+ txHash,
116
+ settledAt:
117
+ typeof parsed.settledAt === 'number'
118
+ ? parsed.settledAt
119
+ : Math.floor(Date.now() / 1000),
120
+ };
121
+ } catch {
122
+ return undefined;
123
+ }
124
+ }
125
+
126
+ /** Convert a value into a bigint safely. */
127
+ function safeBigInt(v: unknown): bigint | null {
128
+ try {
129
+ if (typeof v === 'bigint') return v;
130
+ if (typeof v === 'string' || typeof v === 'number') return BigInt(v);
131
+ return null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ export function createX402Client(config: X402ClientConfig): X402Client {
138
+ const transport = config.transport;
139
+ const walletClient = transport.walletClient;
140
+ if (walletClient === undefined || walletClient.account === undefined) {
141
+ // We deliberately throw at construction time so a misconfigured client
142
+ // can't ship to production without a wallet.
143
+ throw new X402AssetNotSupportedError();
144
+ }
145
+ const account = walletClient.account;
146
+ const chain = transport.chain;
147
+
148
+ return {
149
+ async fetch(input, init) {
150
+ const fetchImpl = globalThis.fetch;
151
+ const maxPriceAtomic = init?.maxPriceAtomic;
152
+
153
+ // Strip our non-standard option before passing init to fetch.
154
+ const passThroughInit: RequestInit = { ...(init ?? {}) };
155
+ delete (passThroughInit as { maxPriceAtomic?: bigint }).maxPriceAtomic;
156
+
157
+ // 1. First attempt.
158
+ const first = (await fetchImpl(input, passThroughInit)) as X402Response;
159
+ if (first.status !== 402) {
160
+ return first;
161
+ }
162
+
163
+ // 2. Decode challenge.
164
+ const challenge = await readChallenge(first);
165
+ const requirements = challenge.accepts[0];
166
+ if (requirements === undefined) {
167
+ throw new X402InvalidChallengeError();
168
+ }
169
+
170
+ // 3. Scheme + network + asset checks.
171
+ if (requirements.scheme !== X402_SCHEME_EXACT) {
172
+ throw new X402InvalidChallengeError();
173
+ }
174
+ // v0.1.1: accept any EIP-3009 asset registered for this chain (USDC, EURC).
175
+ // The exact asset is challenge-driven — the server picks; we validate
176
+ // membership in the registry rather than pinning to one symbol.
177
+ const challengedAsset = getEip3009Token(chain.id, requirements.asset);
178
+ if (challengedAsset === undefined) {
179
+ throw new X402AssetNotSupportedError();
180
+ }
181
+ const expectedNetwork = `eip155:${chain.id}`;
182
+ if (requirements.network !== expectedNetwork) {
183
+ throw new X402AssetNotSupportedError();
184
+ }
185
+
186
+ // 4. Safety cap.
187
+ const required = safeBigInt(requirements.maxAmountRequired);
188
+ if (required === null) {
189
+ throw new X402InvalidChallengeError();
190
+ }
191
+ if (maxPriceAtomic !== undefined && required > maxPriceAtomic) {
192
+ throw new X402AmountMismatchError();
193
+ }
194
+
195
+ // 5. Build authorization. validAfter=0n, validBefore=now+ttl.
196
+ const nowSecs = BigInt(Math.floor(Date.now() / 1000));
197
+ const ttl = BigInt(
198
+ Math.min(
199
+ requirements.maxTimeoutSeconds || DEFAULT_AUTHORIZATION_TTL_SECS,
200
+ DEFAULT_AUTHORIZATION_TTL_SECS,
201
+ ),
202
+ );
203
+ const validBefore = nowSecs + ttl;
204
+ const nonce = generateAuthorizationNonce();
205
+
206
+ const signed = await signTransferWithAuthorization({
207
+ account,
208
+ chain,
209
+ verifyingContract: requirements.asset,
210
+ input: {
211
+ from: account.address,
212
+ to: requirements.payTo,
213
+ value: required,
214
+ validAfter: 0n,
215
+ validBefore,
216
+ nonce,
217
+ },
218
+ });
219
+
220
+ // 6. Build the PaymentPayload + base64 the header.
221
+ const payload: X402PaymentPayload = {
222
+ x402Version: X402_VERSION,
223
+ scheme: X402_SCHEME_EXACT,
224
+ network: requirements.network,
225
+ payload: {
226
+ signature: signed.signature,
227
+ authorization: {
228
+ from: signed.from,
229
+ to: signed.to,
230
+ value: signed.value.toString(),
231
+ validAfter: signed.validAfter.toString(),
232
+ validBefore: signed.validBefore.toString(),
233
+ nonce: signed.nonce,
234
+ },
235
+ },
236
+ };
237
+ const encoded = Buffer.from(JSON.stringify(payload), 'utf-8').toString(
238
+ 'base64',
239
+ );
240
+
241
+ // 7. Re-issue. Mutating the headers via a new object so the caller's
242
+ // original init stays untouched.
243
+ const retryHeaders = new Headers(passThroughInit.headers ?? undefined);
244
+ retryHeaders.set(X402_PAYMENT_SIGNATURE_HEADER, encoded);
245
+ const retryInit: RequestInit = {
246
+ ...passThroughInit,
247
+ headers: retryHeaders,
248
+ };
249
+
250
+ const second = (await fetchImpl(input, retryInit)) as X402Response;
251
+ const settlement = readSettlement(second);
252
+ if (settlement !== undefined) {
253
+ // Attach as a non-standard own-property.
254
+ second.x402Settlement = settlement;
255
+ }
256
+ return second;
257
+ },
258
+ };
259
+ }
260
+
261
+ export type { X402ClientConfig, X402PaymentResult } from './types.js';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Defaults for the @shroud-fi/x402 package.
3
+ *
4
+ * Tight defaults reduce blast radius if a signed payload leaks:
5
+ * - 5-minute validity window matches Coinbase CDP recommendation and the
6
+ * PayAI free tier facilitator's default maxTimeoutSeconds (300).
7
+ * - x402Version pinned to 2 (current shipping spec). We deliberately do NOT
8
+ * implement v1 backward-compat per LOCKED-DECISIONS (whitepaper field
9
+ * names are deprecated).
10
+ */
11
+
12
+ export const X402_VERSION = 2 as const;
13
+
14
+ /** Default EIP-3009 authorization validity window (seconds). */
15
+ export const DEFAULT_AUTHORIZATION_TTL_SECS = 300;
16
+
17
+ /** Default facilitator `maxTimeoutSeconds` echoed back in PaymentRequired. */
18
+ export const DEFAULT_MAX_TIMEOUT_SECS = 300;
package/src/errors.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * x402 error hierarchy.
3
+ *
4
+ * Privacy invariants:
5
+ * - No amount, value, or balance numbers in any .message string.
6
+ * - No private keys, signature bytes, or nonce bytes in any .message string.
7
+ * - No recipient main wallet addresses in any .message string (stealth
8
+ * addresses are derivable on-chain so leaking them post-payment is fine,
9
+ * but the registrant wallet is identity-bearing — never include it).
10
+ * - All errors extend the same base class so callers can `instanceof X402Error`
11
+ * once at the boundary.
12
+ */
13
+
14
+ export class X402Error extends Error {
15
+ override readonly name: string = 'X402Error';
16
+ constructor(message: string) {
17
+ super(message);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * The 402 challenge body (or its header) could not be parsed / is malformed.
23
+ * Catch-all for "we received something the spec disallows".
24
+ */
25
+ export class X402InvalidChallengeError extends X402Error {
26
+ override readonly name: string = 'X402InvalidChallengeError';
27
+ constructor() {
28
+ super('Invalid x402 challenge payload');
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The signed `PAYMENT-SIGNATURE` payload submitted by the client could not be
34
+ * verified against the challenge (signer mismatch, malformed JSON, expired
35
+ * authorization, etc).
36
+ */
37
+ export class X402SignatureVerificationError extends X402Error {
38
+ override readonly name: string = 'X402SignatureVerificationError';
39
+ constructor() {
40
+ super('x402 signed payment payload failed verification');
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Facilitator HTTP call (verify or settle) failed. Tag identifies the
46
+ * facilitator stage but never carries response body, token amount, or signed
47
+ * payload.
48
+ */
49
+ export class X402FacilitatorError extends X402Error {
50
+ override readonly name: string = 'X402FacilitatorError';
51
+ constructor(stage: 'verify' | 'settle' | 'network') {
52
+ super(`x402 facilitator request failed (stage=${stage})`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * The server's configured asset is not the canonical USDC on the configured
58
+ * chain (or USDC is not deployed on the chain). Hard-fail to avoid signing
59
+ * authorizations against the wrong token.
60
+ */
61
+ export class X402AssetNotSupportedError extends X402Error {
62
+ override readonly name: string = 'X402AssetNotSupportedError';
63
+ constructor() {
64
+ super('x402: requested asset is not supported on the configured chain');
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The server's quoted price exceeded the client's `maxPriceAtomic` safety cap,
70
+ * or the verified payload's value did not equal the challenge requirement.
71
+ * The error carries no amount bytes — caller can read inputs separately.
72
+ */
73
+ export class X402AmountMismatchError extends X402Error {
74
+ override readonly name: string = 'X402AmountMismatchError';
75
+ constructor() {
76
+ super('x402: payment amount exceeds caller cap or does not match challenge');
77
+ }
78
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Facilitator HTTP client.
3
+ *
4
+ * Two endpoints per the x402 spec:
5
+ * - POST /verify — facilitator validates signature + balance + simulation.
6
+ * - POST /settle — facilitator broadcasts the on-chain settlement tx.
7
+ *
8
+ * Privacy invariants:
9
+ * - We forward only the signed PaymentPayload + the PaymentRequirements. The
10
+ * facilitator never receives the recipient's main wallet — only the
11
+ * freshly derived stealth address (the `payTo` field of the requirements).
12
+ * - We never log request/response bodies. Tags are short strings only.
13
+ * - Auth token (if any) is attached as a Bearer header. We never log the
14
+ * full token; tests verify the header is set but redaction is the
15
+ * operator's responsibility upstream.
16
+ */
17
+
18
+ import type { Hex } from 'viem';
19
+ import {
20
+ PAYAI_FACILITATOR_URL,
21
+ facilitatorNetworkName,
22
+ type X402FacilitatorConfig,
23
+ } from './protocol.js';
24
+ import { X402FacilitatorError } from './errors.js';
25
+
26
+ export interface FacilitatorVerifyResult {
27
+ readonly isValid: boolean;
28
+ readonly invalidReason?: string;
29
+ readonly payer?: string;
30
+ }
31
+
32
+ export interface FacilitatorSettleResult {
33
+ readonly success: boolean;
34
+ readonly txHash?: Hex;
35
+ readonly errorReason?: string;
36
+ readonly payer?: string;
37
+ readonly network?: string;
38
+ }
39
+
40
+ /** Resolve the facilitator config, defaulting to PayAI free tier. */
41
+ export function resolveFacilitator(
42
+ config: X402FacilitatorConfig | undefined,
43
+ ): X402FacilitatorConfig {
44
+ if (config === undefined) {
45
+ return { url: PAYAI_FACILITATOR_URL };
46
+ }
47
+ return config;
48
+ }
49
+
50
+ /**
51
+ * Translate our internal x402 v2 / CAIP-2 verify+settle body into the v1 /
52
+ * legacy-network shape that PayAI (and the wider facilitator ecosystem)
53
+ * accepts on the wire. Proven end-to-end on Base mainnet (tx 0x57d84d53…)
54
+ * before this shipped.
55
+ *
56
+ * Transformations:
57
+ * - x402Version 2 → 1 (top-level and on the payload)
58
+ * - network CAIP-2 (`eip155:8453`) → legacy name (`base`) on both the
59
+ * payload and the requirements
60
+ * - requirements.extra trimmed to `{ name, version }` — the facilitator
61
+ * does not consume our `shroudfiAnnouncement` extension and some reject
62
+ * unknown extra keys
63
+ * - requirements gains `mimeType: ''` (present in the canonical v1 shape)
64
+ *
65
+ * Pure — never mutates its inputs. Throws X402FacilitatorError('network') for
66
+ * a chain with no mapped facilitator network name.
67
+ */
68
+ export function toFacilitatorRequest(args: {
69
+ readonly chainId: number;
70
+ readonly paymentRequirements: Record<string, unknown> & {
71
+ readonly extra?: Record<string, unknown>;
72
+ };
73
+ readonly paymentPayload: Record<string, unknown>;
74
+ }): {
75
+ x402Version: 1;
76
+ paymentPayload: Record<string, unknown> & { x402Version: 1; network: string };
77
+ paymentRequirements: Record<string, unknown> & {
78
+ network: string;
79
+ mimeType: string;
80
+ extra: { name: string; version: string };
81
+ };
82
+ } {
83
+ const network = facilitatorNetworkName(args.chainId);
84
+ if (network === undefined) {
85
+ throw new X402FacilitatorError('network');
86
+ }
87
+
88
+ const srcExtra = (args.paymentRequirements.extra ?? {}) as Record<
89
+ string,
90
+ unknown
91
+ >;
92
+ const extra = {
93
+ name: String(srcExtra.name ?? ''),
94
+ version: String(srcExtra.version ?? ''),
95
+ };
96
+
97
+ const paymentRequirements = {
98
+ ...args.paymentRequirements,
99
+ network,
100
+ mimeType:
101
+ typeof args.paymentRequirements.mimeType === 'string'
102
+ ? (args.paymentRequirements.mimeType as string)
103
+ : '',
104
+ extra,
105
+ };
106
+
107
+ const paymentPayload = {
108
+ ...args.paymentPayload,
109
+ x402Version: 1 as const,
110
+ network,
111
+ };
112
+
113
+ return { x402Version: 1, paymentPayload, paymentRequirements };
114
+ }
115
+
116
+ /**
117
+ * Build standard headers for a facilitator request. When an Ed25519 auth
118
+ * token is configured, attach `Authorization: Bearer <token>`. Otherwise
119
+ * the request hits the PayAI free-tier endpoint (no auth required).
120
+ */
121
+ export function buildFacilitatorHeaders(
122
+ config: X402FacilitatorConfig,
123
+ ): Record<string, string> {
124
+ const headers: Record<string, string> = {
125
+ 'content-type': 'application/json',
126
+ accept: 'application/json',
127
+ };
128
+ if (config.ed25519AuthToken !== undefined) {
129
+ headers['authorization'] = `Bearer ${config.ed25519AuthToken}`;
130
+ }
131
+ return headers;
132
+ }
133
+
134
+ /**
135
+ * Call the facilitator's `/verify` endpoint. Returns the parsed verification
136
+ * result. Re-wraps every failure path as a tagged X402FacilitatorError so
137
+ * no upstream response body leaks through.
138
+ */
139
+ export async function facilitatorVerify(
140
+ config: X402FacilitatorConfig,
141
+ body: unknown,
142
+ fetchImpl: typeof fetch = fetch,
143
+ ): Promise<FacilitatorVerifyResult> {
144
+ const url = `${config.url.replace(/\/+$/, '')}/verify`;
145
+ let res: Response;
146
+ try {
147
+ res = await fetchImpl(url, {
148
+ method: 'POST',
149
+ headers: buildFacilitatorHeaders(config),
150
+ body: JSON.stringify(body),
151
+ });
152
+ } catch {
153
+ throw new X402FacilitatorError('network');
154
+ }
155
+ if (!res.ok) {
156
+ throw new X402FacilitatorError('verify');
157
+ }
158
+ let parsed: FacilitatorVerifyResult;
159
+ try {
160
+ parsed = (await res.json()) as FacilitatorVerifyResult;
161
+ } catch {
162
+ throw new X402FacilitatorError('verify');
163
+ }
164
+ return parsed;
165
+ }
166
+
167
+ /**
168
+ * Call the facilitator's `/settle` endpoint. Returns the parsed settle
169
+ * result (including the on-chain tx hash on success).
170
+ */
171
+ export async function facilitatorSettle(
172
+ config: X402FacilitatorConfig,
173
+ body: unknown,
174
+ fetchImpl: typeof fetch = fetch,
175
+ ): Promise<FacilitatorSettleResult> {
176
+ const url = `${config.url.replace(/\/+$/, '')}/settle`;
177
+ let res: Response;
178
+ try {
179
+ res = await fetchImpl(url, {
180
+ method: 'POST',
181
+ headers: buildFacilitatorHeaders(config),
182
+ body: JSON.stringify(body),
183
+ });
184
+ } catch {
185
+ throw new X402FacilitatorError('network');
186
+ }
187
+ if (!res.ok) {
188
+ throw new X402FacilitatorError('settle');
189
+ }
190
+ let raw: Record<string, unknown>;
191
+ try {
192
+ raw = (await res.json()) as Record<string, unknown>;
193
+ } catch {
194
+ throw new X402FacilitatorError('settle');
195
+ }
196
+ // Normalize the tx-hash field. The x402 facilitator spec drifted: PayAI
197
+ // returns `transaction`, some return `transactionHash`, our own type uses
198
+ // `txHash`. Accept all three so callers get a populated txHash on success.
199
+ const txHash = (raw.txHash ?? raw.transaction ?? raw.transactionHash) as
200
+ | Hex
201
+ | undefined;
202
+ return {
203
+ success: raw.success === true,
204
+ ...(txHash !== undefined ? { txHash } : {}),
205
+ ...(typeof raw.errorReason === 'string'
206
+ ? { errorReason: raw.errorReason }
207
+ : {}),
208
+ ...(typeof raw.payer === 'string' ? { payer: raw.payer } : {}),
209
+ ...(typeof raw.network === 'string' ? { network: raw.network } : {}),
210
+ };
211
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ // Public surface — @shroud-fi/x402
2
+ // HTTP 402 + stealth-address payment routing for AI agents on Base.
3
+ // See packages/x402/README and docs/research/competitive/x402-spec-and-ecosystem.md.
4
+
5
+ // Server side
6
+ export { createX402Server } from './server.js';
7
+ export type {
8
+ X402Server,
9
+ X402ServerConfig,
10
+ X402Challenge,
11
+ X402PaymentRequirements,
12
+ X402PaymentVerification,
13
+ } from './server.js';
14
+
15
+ // Client side
16
+ export { createX402Client } from './client.js';
17
+ export type {
18
+ X402Client,
19
+ X402ClientConfig,
20
+ X402PaymentResult,
21
+ } from './client.js';
22
+
23
+ // Protocol constants + types
24
+ export {
25
+ X402_PAYMENT_REQUIRED_HEADER,
26
+ X402_PAYMENT_SIGNATURE_HEADER,
27
+ X402_PAYMENT_RESPONSE_HEADER,
28
+ X402_SCHEME_EXACT,
29
+ PAYAI_FACILITATOR_URL,
30
+ COINBASE_FACILITATOR_URL,
31
+ facilitatorNetworkName,
32
+ } from './protocol.js';
33
+
34
+ // Facilitator client + adapter (v0.1.2)
35
+ export {
36
+ resolveFacilitator,
37
+ toFacilitatorRequest,
38
+ facilitatorVerify,
39
+ facilitatorSettle,
40
+ } from './facilitator.js';
41
+ export type {
42
+ FacilitatorVerifyResult,
43
+ FacilitatorSettleResult,
44
+ } from './facilitator.js';
45
+ export type {
46
+ X402PaymentPayload,
47
+ X402SchemeId,
48
+ X402FacilitatorConfig,
49
+ } from './protocol.js';
50
+
51
+ // Errors
52
+ export {
53
+ X402Error,
54
+ X402InvalidChallengeError,
55
+ X402SignatureVerificationError,
56
+ X402FacilitatorError,
57
+ X402AssetNotSupportedError,
58
+ X402AmountMismatchError,
59
+ } from './errors.js';