@nullclear/x402-arbiter 0.0.0-stage → 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nullclear-Protocol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # @nullclear/x402-arbiter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Autonomous Agent SDK for zero-risk HTTP 402 calls via NullClear Deferred Non-Custodial Schema Escrow.
4
+
5
+ ## Key Features
6
+ - **Automatic 402 Interception:** Intercepts standard `402 Payment Required` headers and signs EIP-3009 authorizations.
7
+ - **Zod & JSON Schema Enforcement:** Define expected response structures with standard Zod or JSON schemas.
8
+ - **$0 Spend Protection:** If the seller crashes, returns 500, or hallucinates bad types, settlement is voided and $0 is spent.
9
+
10
+ ## Quickstart
11
+
12
+ ```typescript
13
+ import { createNullClearFetch } from "@nullclear/x402-arbiter";
14
+ import { z } from "zod";
15
+
16
+ const nullclearFetch = createNullClearFetch({
17
+ accountOrPrivateKey: "0x...", // Buyer private key or viem account
18
+ chainId: 8453, // Base Mainnet (or 84532 for Base Sepolia)
19
+ arbiterUrl: "https://arbiter.nullclear.dev"
20
+ });
21
+
22
+ // Make requests normally - 402 will be intercepted and escrow-routed
23
+ const response = await nullclearFetch("https://seller-agent.com/v1/alpha", {
24
+ nullclear: {
25
+ sellerAgentId: "42",
26
+ minTrustScore: 60,
27
+ schema: z.object({
28
+ ticker: z.string(),
29
+ price: z.number(),
30
+ confidence: z.number()
31
+ })
32
+ }
33
+ });
34
+
35
+ const data = await response.json();
36
+ console.log("Verified data:", data);
37
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NullClearClient: () => NullClearClient,
24
+ NullClearEscrowAbortedError: () => NullClearEscrowAbortedError,
25
+ createNullClearFetch: () => createNullClearFetch,
26
+ signX402Payment: () => signX402Payment,
27
+ zodToJsonSchema: () => zodToJsonSchema
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/signer.ts
32
+ var import_accounts = require("viem/accounts");
33
+ var import_shared = require("@nullclear/shared");
34
+ async function signX402Payment(signer, option, resourceUrl) {
35
+ let accountAddress;
36
+ let signTypedDataFn;
37
+ if (typeof signer === "string" && signer.startsWith("0x")) {
38
+ const account = (0, import_accounts.privateKeyToAccount)(signer);
39
+ accountAddress = account.address;
40
+ signTypedDataFn = (params) => account.signTypedData(params);
41
+ } else if (typeof signer === "object" && signer !== null && "signTypedData" in signer && "account" in signer && signer.account) {
42
+ const rawAccount = signer.account;
43
+ accountAddress = typeof rawAccount === "string" ? rawAccount : rawAccount.address;
44
+ signTypedDataFn = (params) => signer.signTypedData(params);
45
+ } else {
46
+ throw new Error("Invalid signer provided to signX402Payment: must be private key or viem account/wallet client");
47
+ }
48
+ const randomBytes = new Uint8Array(32);
49
+ crypto.getRandomValues(randomBytes);
50
+ const nonce = `0x` + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
51
+ const validAfter = "0";
52
+ const timeoutSeconds = option.maxTimeoutSeconds && option.maxTimeoutSeconds > 0 ? option.maxTimeoutSeconds : 300;
53
+ const validBefore = String(Math.floor(Date.now() / 1e3) + timeoutSeconds);
54
+ const chainConfig = (0, import_shared.getChainConfig)(option.network);
55
+ const domain = {
56
+ name: chainConfig.usdcDomain.name,
57
+ version: chainConfig.usdcDomain.version,
58
+ chainId: chainConfig.chainId,
59
+ verifyingContract: chainConfig.usdc
60
+ };
61
+ const authorization = {
62
+ from: accountAddress,
63
+ to: option.payTo,
64
+ value: option.amount,
65
+ validAfter,
66
+ validBefore,
67
+ nonce
68
+ };
69
+ const signature = await signTypedDataFn({
70
+ domain,
71
+ types: import_shared.EIP3009_TYPES,
72
+ primaryType: "TransferWithAuthorization",
73
+ message: {
74
+ from: authorization.from,
75
+ to: authorization.to,
76
+ value: BigInt(authorization.value),
77
+ validAfter: BigInt(authorization.validAfter),
78
+ validBefore: BigInt(authorization.validBefore),
79
+ nonce: authorization.nonce
80
+ }
81
+ });
82
+ const payload = {
83
+ x402Version: 2,
84
+ resource: resourceUrl ? { url: resourceUrl } : void 0,
85
+ accepted: option,
86
+ payload: {
87
+ signature,
88
+ authorization
89
+ }
90
+ };
91
+ return (0, import_shared.encodeBase64Json)(payload);
92
+ }
93
+
94
+ // src/client.ts
95
+ var NullClearClient = class {
96
+ arbiterUrl;
97
+ defaultChainId;
98
+ customFetch;
99
+ constructor(config = {}) {
100
+ this.arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
101
+ this.defaultChainId = config.defaultChainId || 8453;
102
+ this.customFetch = config.fetch || fetch;
103
+ }
104
+ async getScore(agentId, chainId) {
105
+ const targetChainId = chainId || this.defaultChainId;
106
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/score/${targetChainId}/${agentId}`);
107
+ if (!res.ok) {
108
+ const err = await res.text();
109
+ throw new Error(`Failed to fetch score for agent ${agentId}: ${res.status} ${err}`);
110
+ }
111
+ const data = await res.json();
112
+ return data.data;
113
+ }
114
+ async clear(request) {
115
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/clear`, {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify(request)
119
+ });
120
+ const data = await res.json();
121
+ return data;
122
+ }
123
+ async getKya(agentId, chainId) {
124
+ const targetChainId = chainId || this.defaultChainId;
125
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/${targetChainId}/${agentId}`);
126
+ if (!res.ok) {
127
+ if (res.status === 404) return null;
128
+ const err = await res.text();
129
+ throw new Error(`Failed to fetch KYA mandate for agent ${agentId}: ${res.status} ${err}`);
130
+ }
131
+ const data = await res.json();
132
+ return data.data;
133
+ }
134
+ async attestKya(mandate, chainId, signature, signerAddress) {
135
+ const targetChainId = chainId || this.defaultChainId;
136
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/attest`, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({
140
+ mandate,
141
+ chainId: targetChainId,
142
+ signature,
143
+ signerAddress
144
+ })
145
+ });
146
+ if (!res.ok) {
147
+ const err = await res.text();
148
+ throw new Error(`Failed to attest KYA mandate: ${res.status} ${err}`);
149
+ }
150
+ const data = await res.json();
151
+ return data.data;
152
+ }
153
+ };
154
+
155
+ // src/fetch-wrapper.ts
156
+ var import_zod = require("zod");
157
+ var import_shared2 = require("@nullclear/shared");
158
+ var NullClearEscrowAbortedError = class extends Error {
159
+ stage;
160
+ buyerCharged;
161
+ validationErrors;
162
+ score;
163
+ upstreamStatus;
164
+ reason;
165
+ constructor(message, details) {
166
+ super(message);
167
+ this.name = "NullClearEscrowAbortedError";
168
+ this.stage = details.stage;
169
+ this.buyerCharged = details.buyerCharged || "0";
170
+ this.validationErrors = details.validationErrors || [];
171
+ this.score = details.score;
172
+ this.upstreamStatus = details.upstreamStatus;
173
+ this.reason = details.reason;
174
+ }
175
+ };
176
+ function zodToJsonSchema(schema) {
177
+ const def = schema._def;
178
+ if (!def) return {};
179
+ const typeName = def.typeName;
180
+ switch (typeName) {
181
+ case "ZodString":
182
+ return { type: "string" };
183
+ case "ZodNumber":
184
+ return { type: "number" };
185
+ case "ZodBoolean":
186
+ return { type: "boolean" };
187
+ case "ZodArray":
188
+ return {
189
+ type: "array",
190
+ items: def.type ? zodToJsonSchema(def.type) : {}
191
+ };
192
+ case "ZodObject": {
193
+ const shape = def.shape();
194
+ const properties = {};
195
+ const required = [];
196
+ for (const [key, value] of Object.entries(shape)) {
197
+ properties[key] = zodToJsonSchema(value);
198
+ const subDef = value._def;
199
+ if (subDef.typeName !== "ZodOptional" && subDef.typeName !== "ZodNullable") {
200
+ required.push(key);
201
+ }
202
+ }
203
+ return {
204
+ type: "object",
205
+ properties,
206
+ required: required.length > 0 ? required : void 0
207
+ };
208
+ }
209
+ case "ZodEnum":
210
+ return {
211
+ type: "string",
212
+ enum: def.values
213
+ };
214
+ case "ZodOptional":
215
+ case "ZodNullable":
216
+ return def.innerType ? zodToJsonSchema(def.innerType) : {};
217
+ default:
218
+ return {};
219
+ }
220
+ }
221
+ function createNullClearFetch(config) {
222
+ const chainId = config.chainId || 8453;
223
+ const arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
224
+ const defaultMinTrustScore = config.defaultMinTrustScore || 50;
225
+ const customFetch = config.customFetch || fetch;
226
+ return async function nullclearFetch(input, init) {
227
+ const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
228
+ const fetchInit = { ...init };
229
+ delete fetchInit.nullclear;
230
+ const initialRes = await customFetch(input, fetchInit);
231
+ if (initialRes.status !== 402) {
232
+ return initialRes;
233
+ }
234
+ const paymentRequiredHeader = initialRes.headers.get("PAYMENT-REQUIRED") || initialRes.headers.get("payment-required");
235
+ if (!paymentRequiredHeader) {
236
+ return initialRes;
237
+ }
238
+ let paymentRequired;
239
+ try {
240
+ paymentRequired = (0, import_shared2.decodeBase64Json)(paymentRequiredHeader);
241
+ } catch {
242
+ return initialRes;
243
+ }
244
+ const targetNetwork = `eip155:${chainId}`;
245
+ const selectedOption = paymentRequired.accepts.find(
246
+ (opt) => opt.network === targetNetwork && opt.scheme === "exact"
247
+ );
248
+ if (!selectedOption) {
249
+ throw new Error(
250
+ `Seller does not accept payments on network ${targetNetwork}. Accepted: ${paymentRequired.accepts.map((a) => a.network).join(", ")}`
251
+ );
252
+ }
253
+ const paymentSignatureHeader = await signX402Payment(
254
+ config.accountOrPrivateKey,
255
+ selectedOption,
256
+ targetUrl
257
+ );
258
+ let jsonSchema;
259
+ if (init?.nullclear?.schema) {
260
+ if (init.nullclear.schema instanceof import_zod.z.ZodType) {
261
+ jsonSchema = zodToJsonSchema(init.nullclear.schema);
262
+ } else if (typeof init.nullclear.schema === "object") {
263
+ jsonSchema = init.nullclear.schema;
264
+ }
265
+ }
266
+ const clearPayload = {
267
+ targetUrl,
268
+ method: init?.method || "GET",
269
+ headers: init?.headers || {},
270
+ body: init?.body,
271
+ paymentSignatureHeader,
272
+ sellerAgentId: init?.nullclear?.sellerAgentId,
273
+ minTrustScore: init?.nullclear?.minTrustScore ?? defaultMinTrustScore,
274
+ validation: {
275
+ expectedStatus: [200],
276
+ jsonSchema,
277
+ requiredFields: init?.nullclear?.requiredFields,
278
+ sha256Match: init?.nullclear?.sha256Match
279
+ }
280
+ };
281
+ const arbiterRes = await customFetch(`${arbiterUrl}/v1/clear`, {
282
+ method: "POST",
283
+ headers: { "Content-Type": "application/json" },
284
+ body: JSON.stringify(clearPayload)
285
+ });
286
+ const arbiterData = await arbiterRes.json();
287
+ if (arbiterRes.status === 200 && arbiterData.cleared) {
288
+ const resBody = typeof arbiterData.data === "string" ? arbiterData.data : JSON.stringify(arbiterData.data);
289
+ const resHeaders = new Headers();
290
+ resHeaders.set("Content-Type", "application/json");
291
+ const paymentResponseHdr = arbiterRes.headers.get("PAYMENT-RESPONSE");
292
+ if (paymentResponseHdr) resHeaders.set("PAYMENT-RESPONSE", paymentResponseHdr);
293
+ const attestationHdr = arbiterRes.headers.get("X-NullClear-Attestation");
294
+ if (attestationHdr) resHeaders.set("X-NullClear-Attestation", attestationHdr);
295
+ return new Response(resBody, {
296
+ status: 200,
297
+ headers: resHeaders
298
+ });
299
+ }
300
+ throw new NullClearEscrowAbortedError(
301
+ `NullClear Escrow Aborted: ${arbiterData.stage || "FAILED"} - ${arbiterData.reason || arbiterData.error || (arbiterData.validationErrors || []).join("; ")}`,
302
+ {
303
+ stage: arbiterData.stage || "UNKNOWN_FAILURE",
304
+ buyerCharged: arbiterData.buyerCharged || "0",
305
+ validationErrors: arbiterData.validationErrors || [],
306
+ score: arbiterData.score,
307
+ upstreamStatus: arbiterData.upstreamStatus,
308
+ reason: arbiterData.reason || arbiterData.error
309
+ }
310
+ );
311
+ };
312
+ }
313
+ // Annotate the CommonJS export names for ESM import in node:
314
+ 0 && (module.exports = {
315
+ NullClearClient,
316
+ NullClearEscrowAbortedError,
317
+ createNullClearFetch,
318
+ signX402Payment,
319
+ zodToJsonSchema
320
+ });
@@ -0,0 +1,104 @@
1
+ import { PrivateKeyAccount } from 'viem/accounts';
2
+ import { WalletClient } from 'viem';
3
+ import { X402PaymentOption, KYAMandate } from '@nullclear/shared';
4
+ import { z } from 'zod';
5
+
6
+ type SignerInput = `0x${string}` | PrivateKeyAccount | {
7
+ account: {
8
+ address: `0x${string}`;
9
+ };
10
+ signTypedData: (params: any) => Promise<`0x${string}`>;
11
+ } | WalletClient;
12
+ /**
13
+ * Signs an EIP-3009 TransferWithAuthorization and returns a Base64 encoded X402PaymentSignaturePayload
14
+ */
15
+ declare function signX402Payment(signer: SignerInput, option: X402PaymentOption, resourceUrl?: string): Promise<string>;
16
+
17
+ interface NullClearClientConfig {
18
+ arbiterUrl?: string;
19
+ defaultChainId?: number;
20
+ fetch?: typeof fetch;
21
+ }
22
+ interface ClearRequest {
23
+ targetUrl: string;
24
+ method?: "GET" | "POST" | "PUT" | "DELETE";
25
+ headers?: Record<string, string>;
26
+ body?: unknown;
27
+ paymentSignatureHeader: string;
28
+ sellerAgentId?: string;
29
+ minTrustScore?: number;
30
+ validation?: {
31
+ expectedStatus?: number[];
32
+ jsonSchema?: Record<string, unknown>;
33
+ requiredFields?: string[];
34
+ sha256Match?: string;
35
+ };
36
+ }
37
+ interface ClearResult<T = unknown> {
38
+ cleared: boolean;
39
+ settled: boolean;
40
+ clearingId?: string;
41
+ buyerCharged: string;
42
+ clearingFee?: string;
43
+ transaction?: `0x${string}`;
44
+ attestation?: unknown;
45
+ data?: T;
46
+ stage?: string;
47
+ error?: string;
48
+ validationErrors?: string[];
49
+ }
50
+ declare class NullClearClient {
51
+ private arbiterUrl;
52
+ private defaultChainId;
53
+ private customFetch;
54
+ constructor(config?: NullClearClientConfig);
55
+ getScore(agentId: string, chainId?: number): Promise<any>;
56
+ clear<T = unknown>(request: ClearRequest): Promise<ClearResult<T>>;
57
+ getKya(agentId: string, chainId?: number): Promise<any>;
58
+ attestKya(mandate: KYAMandate, chainId?: number, signature?: string, signerAddress?: `0x${string}`): Promise<any>;
59
+ }
60
+
61
+ declare class NullClearEscrowAbortedError extends Error {
62
+ stage: string;
63
+ buyerCharged: string;
64
+ validationErrors: string[];
65
+ score?: number;
66
+ upstreamStatus?: number;
67
+ reason?: string;
68
+ constructor(message: string, details: {
69
+ stage: string;
70
+ buyerCharged?: string;
71
+ validationErrors?: string[];
72
+ score?: number;
73
+ upstreamStatus?: number;
74
+ reason?: string;
75
+ });
76
+ }
77
+ /**
78
+ * Lightweight deterministic Zod-to-JSON-Schema converter
79
+ */
80
+ declare function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown>;
81
+ interface NullClearOptions {
82
+ sellerAgentId?: string;
83
+ minTrustScore?: number;
84
+ schema?: z.ZodTypeAny | Record<string, unknown>;
85
+ requiredFields?: string[];
86
+ sha256Match?: string;
87
+ }
88
+ interface NullClearFetchConfig {
89
+ accountOrPrivateKey: SignerInput;
90
+ chainId?: number;
91
+ arbiterUrl?: string;
92
+ defaultMinTrustScore?: number;
93
+ customFetch?: typeof fetch;
94
+ }
95
+ type NullClearRequestInit = RequestInit & {
96
+ nullclear?: NullClearOptions;
97
+ };
98
+ /**
99
+ * Creates an enhanced fetch client that autonomously intercepts HTTP 402 Payment Required
100
+ * and safely wraps the call in NullClear Deferred Non-Custodial Schema Escrow
101
+ */
102
+ declare function createNullClearFetch(config: NullClearFetchConfig): (input: RequestInfo | URL, init?: NullClearRequestInit) => Promise<Response>;
103
+
104
+ export { type ClearRequest, type ClearResult, NullClearClient, type NullClearClientConfig, NullClearEscrowAbortedError, type NullClearFetchConfig, type NullClearOptions, type NullClearRequestInit, type SignerInput, createNullClearFetch, signX402Payment, zodToJsonSchema };
@@ -0,0 +1,104 @@
1
+ import { PrivateKeyAccount } from 'viem/accounts';
2
+ import { WalletClient } from 'viem';
3
+ import { X402PaymentOption, KYAMandate } from '@nullclear/shared';
4
+ import { z } from 'zod';
5
+
6
+ type SignerInput = `0x${string}` | PrivateKeyAccount | {
7
+ account: {
8
+ address: `0x${string}`;
9
+ };
10
+ signTypedData: (params: any) => Promise<`0x${string}`>;
11
+ } | WalletClient;
12
+ /**
13
+ * Signs an EIP-3009 TransferWithAuthorization and returns a Base64 encoded X402PaymentSignaturePayload
14
+ */
15
+ declare function signX402Payment(signer: SignerInput, option: X402PaymentOption, resourceUrl?: string): Promise<string>;
16
+
17
+ interface NullClearClientConfig {
18
+ arbiterUrl?: string;
19
+ defaultChainId?: number;
20
+ fetch?: typeof fetch;
21
+ }
22
+ interface ClearRequest {
23
+ targetUrl: string;
24
+ method?: "GET" | "POST" | "PUT" | "DELETE";
25
+ headers?: Record<string, string>;
26
+ body?: unknown;
27
+ paymentSignatureHeader: string;
28
+ sellerAgentId?: string;
29
+ minTrustScore?: number;
30
+ validation?: {
31
+ expectedStatus?: number[];
32
+ jsonSchema?: Record<string, unknown>;
33
+ requiredFields?: string[];
34
+ sha256Match?: string;
35
+ };
36
+ }
37
+ interface ClearResult<T = unknown> {
38
+ cleared: boolean;
39
+ settled: boolean;
40
+ clearingId?: string;
41
+ buyerCharged: string;
42
+ clearingFee?: string;
43
+ transaction?: `0x${string}`;
44
+ attestation?: unknown;
45
+ data?: T;
46
+ stage?: string;
47
+ error?: string;
48
+ validationErrors?: string[];
49
+ }
50
+ declare class NullClearClient {
51
+ private arbiterUrl;
52
+ private defaultChainId;
53
+ private customFetch;
54
+ constructor(config?: NullClearClientConfig);
55
+ getScore(agentId: string, chainId?: number): Promise<any>;
56
+ clear<T = unknown>(request: ClearRequest): Promise<ClearResult<T>>;
57
+ getKya(agentId: string, chainId?: number): Promise<any>;
58
+ attestKya(mandate: KYAMandate, chainId?: number, signature?: string, signerAddress?: `0x${string}`): Promise<any>;
59
+ }
60
+
61
+ declare class NullClearEscrowAbortedError extends Error {
62
+ stage: string;
63
+ buyerCharged: string;
64
+ validationErrors: string[];
65
+ score?: number;
66
+ upstreamStatus?: number;
67
+ reason?: string;
68
+ constructor(message: string, details: {
69
+ stage: string;
70
+ buyerCharged?: string;
71
+ validationErrors?: string[];
72
+ score?: number;
73
+ upstreamStatus?: number;
74
+ reason?: string;
75
+ });
76
+ }
77
+ /**
78
+ * Lightweight deterministic Zod-to-JSON-Schema converter
79
+ */
80
+ declare function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown>;
81
+ interface NullClearOptions {
82
+ sellerAgentId?: string;
83
+ minTrustScore?: number;
84
+ schema?: z.ZodTypeAny | Record<string, unknown>;
85
+ requiredFields?: string[];
86
+ sha256Match?: string;
87
+ }
88
+ interface NullClearFetchConfig {
89
+ accountOrPrivateKey: SignerInput;
90
+ chainId?: number;
91
+ arbiterUrl?: string;
92
+ defaultMinTrustScore?: number;
93
+ customFetch?: typeof fetch;
94
+ }
95
+ type NullClearRequestInit = RequestInit & {
96
+ nullclear?: NullClearOptions;
97
+ };
98
+ /**
99
+ * Creates an enhanced fetch client that autonomously intercepts HTTP 402 Payment Required
100
+ * and safely wraps the call in NullClear Deferred Non-Custodial Schema Escrow
101
+ */
102
+ declare function createNullClearFetch(config: NullClearFetchConfig): (input: RequestInfo | URL, init?: NullClearRequestInit) => Promise<Response>;
103
+
104
+ export { type ClearRequest, type ClearResult, NullClearClient, type NullClearClientConfig, NullClearEscrowAbortedError, type NullClearFetchConfig, type NullClearOptions, type NullClearRequestInit, type SignerInput, createNullClearFetch, signX402Payment, zodToJsonSchema };
@@ -0,0 +1,104 @@
1
+ import { PrivateKeyAccount } from 'viem/accounts';
2
+ import { WalletClient } from 'viem';
3
+ import { X402PaymentOption, KYAMandate } from '@nullclear/shared';
4
+ import { z } from 'zod';
5
+
6
+ type SignerInput = `0x${string}` | PrivateKeyAccount | {
7
+ account: {
8
+ address: `0x${string}`;
9
+ };
10
+ signTypedData: (params: any) => Promise<`0x${string}`>;
11
+ } | WalletClient;
12
+ /**
13
+ * Signs an EIP-3009 TransferWithAuthorization and returns a Base64 encoded X402PaymentSignaturePayload
14
+ */
15
+ declare function signX402Payment(signer: SignerInput, option: X402PaymentOption, resourceUrl?: string): Promise<string>;
16
+
17
+ interface NullClearClientConfig {
18
+ arbiterUrl?: string;
19
+ defaultChainId?: number;
20
+ fetch?: typeof fetch;
21
+ }
22
+ interface ClearRequest {
23
+ targetUrl: string;
24
+ method?: "GET" | "POST" | "PUT" | "DELETE";
25
+ headers?: Record<string, string>;
26
+ body?: unknown;
27
+ paymentSignatureHeader: string;
28
+ sellerAgentId?: string;
29
+ minTrustScore?: number;
30
+ validation?: {
31
+ expectedStatus?: number[];
32
+ jsonSchema?: Record<string, unknown>;
33
+ requiredFields?: string[];
34
+ sha256Match?: string;
35
+ };
36
+ }
37
+ interface ClearResult<T = unknown> {
38
+ cleared: boolean;
39
+ settled: boolean;
40
+ clearingId?: string;
41
+ buyerCharged: string;
42
+ clearingFee?: string;
43
+ transaction?: `0x${string}`;
44
+ attestation?: unknown;
45
+ data?: T;
46
+ stage?: string;
47
+ error?: string;
48
+ validationErrors?: string[];
49
+ }
50
+ declare class NullClearClient {
51
+ private arbiterUrl;
52
+ private defaultChainId;
53
+ private customFetch;
54
+ constructor(config?: NullClearClientConfig);
55
+ getScore(agentId: string, chainId?: number): Promise<any>;
56
+ clear<T = unknown>(request: ClearRequest): Promise<ClearResult<T>>;
57
+ getKya(agentId: string, chainId?: number): Promise<any>;
58
+ attestKya(mandate: KYAMandate, chainId?: number, signature?: string, signerAddress?: `0x${string}`): Promise<any>;
59
+ }
60
+
61
+ declare class NullClearEscrowAbortedError extends Error {
62
+ stage: string;
63
+ buyerCharged: string;
64
+ validationErrors: string[];
65
+ score?: number;
66
+ upstreamStatus?: number;
67
+ reason?: string;
68
+ constructor(message: string, details: {
69
+ stage: string;
70
+ buyerCharged?: string;
71
+ validationErrors?: string[];
72
+ score?: number;
73
+ upstreamStatus?: number;
74
+ reason?: string;
75
+ });
76
+ }
77
+ /**
78
+ * Lightweight deterministic Zod-to-JSON-Schema converter
79
+ */
80
+ declare function zodToJsonSchema(schema: z.ZodTypeAny): Record<string, unknown>;
81
+ interface NullClearOptions {
82
+ sellerAgentId?: string;
83
+ minTrustScore?: number;
84
+ schema?: z.ZodTypeAny | Record<string, unknown>;
85
+ requiredFields?: string[];
86
+ sha256Match?: string;
87
+ }
88
+ interface NullClearFetchConfig {
89
+ accountOrPrivateKey: SignerInput;
90
+ chainId?: number;
91
+ arbiterUrl?: string;
92
+ defaultMinTrustScore?: number;
93
+ customFetch?: typeof fetch;
94
+ }
95
+ type NullClearRequestInit = RequestInit & {
96
+ nullclear?: NullClearOptions;
97
+ };
98
+ /**
99
+ * Creates an enhanced fetch client that autonomously intercepts HTTP 402 Payment Required
100
+ * and safely wraps the call in NullClear Deferred Non-Custodial Schema Escrow
101
+ */
102
+ declare function createNullClearFetch(config: NullClearFetchConfig): (input: RequestInfo | URL, init?: NullClearRequestInit) => Promise<Response>;
103
+
104
+ export { type ClearRequest, type ClearResult, NullClearClient, type NullClearClientConfig, NullClearEscrowAbortedError, type NullClearFetchConfig, type NullClearOptions, type NullClearRequestInit, type SignerInput, createNullClearFetch, signX402Payment, zodToJsonSchema };
package/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ // src/signer.ts
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import {
4
+ EIP3009_TYPES,
5
+ getChainConfig,
6
+ encodeBase64Json
7
+ } from "@nullclear/shared";
8
+ async function signX402Payment(signer, option, resourceUrl) {
9
+ let accountAddress;
10
+ let signTypedDataFn;
11
+ if (typeof signer === "string" && signer.startsWith("0x")) {
12
+ const account = privateKeyToAccount(signer);
13
+ accountAddress = account.address;
14
+ signTypedDataFn = (params) => account.signTypedData(params);
15
+ } else if (typeof signer === "object" && signer !== null && "signTypedData" in signer && "account" in signer && signer.account) {
16
+ const rawAccount = signer.account;
17
+ accountAddress = typeof rawAccount === "string" ? rawAccount : rawAccount.address;
18
+ signTypedDataFn = (params) => signer.signTypedData(params);
19
+ } else {
20
+ throw new Error("Invalid signer provided to signX402Payment: must be private key or viem account/wallet client");
21
+ }
22
+ const randomBytes = new Uint8Array(32);
23
+ crypto.getRandomValues(randomBytes);
24
+ const nonce = `0x` + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
25
+ const validAfter = "0";
26
+ const timeoutSeconds = option.maxTimeoutSeconds && option.maxTimeoutSeconds > 0 ? option.maxTimeoutSeconds : 300;
27
+ const validBefore = String(Math.floor(Date.now() / 1e3) + timeoutSeconds);
28
+ const chainConfig = getChainConfig(option.network);
29
+ const domain = {
30
+ name: chainConfig.usdcDomain.name,
31
+ version: chainConfig.usdcDomain.version,
32
+ chainId: chainConfig.chainId,
33
+ verifyingContract: chainConfig.usdc
34
+ };
35
+ const authorization = {
36
+ from: accountAddress,
37
+ to: option.payTo,
38
+ value: option.amount,
39
+ validAfter,
40
+ validBefore,
41
+ nonce
42
+ };
43
+ const signature = await signTypedDataFn({
44
+ domain,
45
+ types: EIP3009_TYPES,
46
+ primaryType: "TransferWithAuthorization",
47
+ message: {
48
+ from: authorization.from,
49
+ to: authorization.to,
50
+ value: BigInt(authorization.value),
51
+ validAfter: BigInt(authorization.validAfter),
52
+ validBefore: BigInt(authorization.validBefore),
53
+ nonce: authorization.nonce
54
+ }
55
+ });
56
+ const payload = {
57
+ x402Version: 2,
58
+ resource: resourceUrl ? { url: resourceUrl } : void 0,
59
+ accepted: option,
60
+ payload: {
61
+ signature,
62
+ authorization
63
+ }
64
+ };
65
+ return encodeBase64Json(payload);
66
+ }
67
+
68
+ // src/client.ts
69
+ var NullClearClient = class {
70
+ arbiterUrl;
71
+ defaultChainId;
72
+ customFetch;
73
+ constructor(config = {}) {
74
+ this.arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
75
+ this.defaultChainId = config.defaultChainId || 8453;
76
+ this.customFetch = config.fetch || fetch;
77
+ }
78
+ async getScore(agentId, chainId) {
79
+ const targetChainId = chainId || this.defaultChainId;
80
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/score/${targetChainId}/${agentId}`);
81
+ if (!res.ok) {
82
+ const err = await res.text();
83
+ throw new Error(`Failed to fetch score for agent ${agentId}: ${res.status} ${err}`);
84
+ }
85
+ const data = await res.json();
86
+ return data.data;
87
+ }
88
+ async clear(request) {
89
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/clear`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify(request)
93
+ });
94
+ const data = await res.json();
95
+ return data;
96
+ }
97
+ async getKya(agentId, chainId) {
98
+ const targetChainId = chainId || this.defaultChainId;
99
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/${targetChainId}/${agentId}`);
100
+ if (!res.ok) {
101
+ if (res.status === 404) return null;
102
+ const err = await res.text();
103
+ throw new Error(`Failed to fetch KYA mandate for agent ${agentId}: ${res.status} ${err}`);
104
+ }
105
+ const data = await res.json();
106
+ return data.data;
107
+ }
108
+ async attestKya(mandate, chainId, signature, signerAddress) {
109
+ const targetChainId = chainId || this.defaultChainId;
110
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/attest`, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({
114
+ mandate,
115
+ chainId: targetChainId,
116
+ signature,
117
+ signerAddress
118
+ })
119
+ });
120
+ if (!res.ok) {
121
+ const err = await res.text();
122
+ throw new Error(`Failed to attest KYA mandate: ${res.status} ${err}`);
123
+ }
124
+ const data = await res.json();
125
+ return data.data;
126
+ }
127
+ };
128
+
129
+ // src/fetch-wrapper.ts
130
+ import { z } from "zod";
131
+ import {
132
+ decodeBase64Json
133
+ } from "@nullclear/shared";
134
+ var NullClearEscrowAbortedError = class extends Error {
135
+ stage;
136
+ buyerCharged;
137
+ validationErrors;
138
+ score;
139
+ upstreamStatus;
140
+ reason;
141
+ constructor(message, details) {
142
+ super(message);
143
+ this.name = "NullClearEscrowAbortedError";
144
+ this.stage = details.stage;
145
+ this.buyerCharged = details.buyerCharged || "0";
146
+ this.validationErrors = details.validationErrors || [];
147
+ this.score = details.score;
148
+ this.upstreamStatus = details.upstreamStatus;
149
+ this.reason = details.reason;
150
+ }
151
+ };
152
+ function zodToJsonSchema(schema) {
153
+ const def = schema._def;
154
+ if (!def) return {};
155
+ const typeName = def.typeName;
156
+ switch (typeName) {
157
+ case "ZodString":
158
+ return { type: "string" };
159
+ case "ZodNumber":
160
+ return { type: "number" };
161
+ case "ZodBoolean":
162
+ return { type: "boolean" };
163
+ case "ZodArray":
164
+ return {
165
+ type: "array",
166
+ items: def.type ? zodToJsonSchema(def.type) : {}
167
+ };
168
+ case "ZodObject": {
169
+ const shape = def.shape();
170
+ const properties = {};
171
+ const required = [];
172
+ for (const [key, value] of Object.entries(shape)) {
173
+ properties[key] = zodToJsonSchema(value);
174
+ const subDef = value._def;
175
+ if (subDef.typeName !== "ZodOptional" && subDef.typeName !== "ZodNullable") {
176
+ required.push(key);
177
+ }
178
+ }
179
+ return {
180
+ type: "object",
181
+ properties,
182
+ required: required.length > 0 ? required : void 0
183
+ };
184
+ }
185
+ case "ZodEnum":
186
+ return {
187
+ type: "string",
188
+ enum: def.values
189
+ };
190
+ case "ZodOptional":
191
+ case "ZodNullable":
192
+ return def.innerType ? zodToJsonSchema(def.innerType) : {};
193
+ default:
194
+ return {};
195
+ }
196
+ }
197
+ function createNullClearFetch(config) {
198
+ const chainId = config.chainId || 8453;
199
+ const arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
200
+ const defaultMinTrustScore = config.defaultMinTrustScore || 50;
201
+ const customFetch = config.customFetch || fetch;
202
+ return async function nullclearFetch(input, init) {
203
+ const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
204
+ const fetchInit = { ...init };
205
+ delete fetchInit.nullclear;
206
+ const initialRes = await customFetch(input, fetchInit);
207
+ if (initialRes.status !== 402) {
208
+ return initialRes;
209
+ }
210
+ const paymentRequiredHeader = initialRes.headers.get("PAYMENT-REQUIRED") || initialRes.headers.get("payment-required");
211
+ if (!paymentRequiredHeader) {
212
+ return initialRes;
213
+ }
214
+ let paymentRequired;
215
+ try {
216
+ paymentRequired = decodeBase64Json(paymentRequiredHeader);
217
+ } catch {
218
+ return initialRes;
219
+ }
220
+ const targetNetwork = `eip155:${chainId}`;
221
+ const selectedOption = paymentRequired.accepts.find(
222
+ (opt) => opt.network === targetNetwork && opt.scheme === "exact"
223
+ );
224
+ if (!selectedOption) {
225
+ throw new Error(
226
+ `Seller does not accept payments on network ${targetNetwork}. Accepted: ${paymentRequired.accepts.map((a) => a.network).join(", ")}`
227
+ );
228
+ }
229
+ const paymentSignatureHeader = await signX402Payment(
230
+ config.accountOrPrivateKey,
231
+ selectedOption,
232
+ targetUrl
233
+ );
234
+ let jsonSchema;
235
+ if (init?.nullclear?.schema) {
236
+ if (init.nullclear.schema instanceof z.ZodType) {
237
+ jsonSchema = zodToJsonSchema(init.nullclear.schema);
238
+ } else if (typeof init.nullclear.schema === "object") {
239
+ jsonSchema = init.nullclear.schema;
240
+ }
241
+ }
242
+ const clearPayload = {
243
+ targetUrl,
244
+ method: init?.method || "GET",
245
+ headers: init?.headers || {},
246
+ body: init?.body,
247
+ paymentSignatureHeader,
248
+ sellerAgentId: init?.nullclear?.sellerAgentId,
249
+ minTrustScore: init?.nullclear?.minTrustScore ?? defaultMinTrustScore,
250
+ validation: {
251
+ expectedStatus: [200],
252
+ jsonSchema,
253
+ requiredFields: init?.nullclear?.requiredFields,
254
+ sha256Match: init?.nullclear?.sha256Match
255
+ }
256
+ };
257
+ const arbiterRes = await customFetch(`${arbiterUrl}/v1/clear`, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify(clearPayload)
261
+ });
262
+ const arbiterData = await arbiterRes.json();
263
+ if (arbiterRes.status === 200 && arbiterData.cleared) {
264
+ const resBody = typeof arbiterData.data === "string" ? arbiterData.data : JSON.stringify(arbiterData.data);
265
+ const resHeaders = new Headers();
266
+ resHeaders.set("Content-Type", "application/json");
267
+ const paymentResponseHdr = arbiterRes.headers.get("PAYMENT-RESPONSE");
268
+ if (paymentResponseHdr) resHeaders.set("PAYMENT-RESPONSE", paymentResponseHdr);
269
+ const attestationHdr = arbiterRes.headers.get("X-NullClear-Attestation");
270
+ if (attestationHdr) resHeaders.set("X-NullClear-Attestation", attestationHdr);
271
+ return new Response(resBody, {
272
+ status: 200,
273
+ headers: resHeaders
274
+ });
275
+ }
276
+ throw new NullClearEscrowAbortedError(
277
+ `NullClear Escrow Aborted: ${arbiterData.stage || "FAILED"} - ${arbiterData.reason || arbiterData.error || (arbiterData.validationErrors || []).join("; ")}`,
278
+ {
279
+ stage: arbiterData.stage || "UNKNOWN_FAILURE",
280
+ buyerCharged: arbiterData.buyerCharged || "0",
281
+ validationErrors: arbiterData.validationErrors || [],
282
+ score: arbiterData.score,
283
+ upstreamStatus: arbiterData.upstreamStatus,
284
+ reason: arbiterData.reason || arbiterData.error
285
+ }
286
+ );
287
+ };
288
+ }
289
+ export {
290
+ NullClearClient,
291
+ NullClearEscrowAbortedError,
292
+ createNullClearFetch,
293
+ signX402Payment,
294
+ zodToJsonSchema
295
+ };
package/dist/index.mjs ADDED
@@ -0,0 +1,295 @@
1
+ // src/signer.ts
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import {
4
+ EIP3009_TYPES,
5
+ getChainConfig,
6
+ encodeBase64Json
7
+ } from "@nullclear/shared";
8
+ async function signX402Payment(signer, option, resourceUrl) {
9
+ let accountAddress;
10
+ let signTypedDataFn;
11
+ if (typeof signer === "string" && signer.startsWith("0x")) {
12
+ const account = privateKeyToAccount(signer);
13
+ accountAddress = account.address;
14
+ signTypedDataFn = (params) => account.signTypedData(params);
15
+ } else if (typeof signer === "object" && signer !== null && "signTypedData" in signer && "account" in signer && signer.account) {
16
+ const rawAccount = signer.account;
17
+ accountAddress = typeof rawAccount === "string" ? rawAccount : rawAccount.address;
18
+ signTypedDataFn = (params) => signer.signTypedData(params);
19
+ } else {
20
+ throw new Error("Invalid signer provided to signX402Payment: must be private key or viem account/wallet client");
21
+ }
22
+ const randomBytes = new Uint8Array(32);
23
+ crypto.getRandomValues(randomBytes);
24
+ const nonce = `0x` + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
25
+ const validAfter = "0";
26
+ const timeoutSeconds = option.maxTimeoutSeconds && option.maxTimeoutSeconds > 0 ? option.maxTimeoutSeconds : 300;
27
+ const validBefore = String(Math.floor(Date.now() / 1e3) + timeoutSeconds);
28
+ const chainConfig = getChainConfig(option.network);
29
+ const domain = {
30
+ name: chainConfig.usdcDomain.name,
31
+ version: chainConfig.usdcDomain.version,
32
+ chainId: chainConfig.chainId,
33
+ verifyingContract: chainConfig.usdc
34
+ };
35
+ const authorization = {
36
+ from: accountAddress,
37
+ to: option.payTo,
38
+ value: option.amount,
39
+ validAfter,
40
+ validBefore,
41
+ nonce
42
+ };
43
+ const signature = await signTypedDataFn({
44
+ domain,
45
+ types: EIP3009_TYPES,
46
+ primaryType: "TransferWithAuthorization",
47
+ message: {
48
+ from: authorization.from,
49
+ to: authorization.to,
50
+ value: BigInt(authorization.value),
51
+ validAfter: BigInt(authorization.validAfter),
52
+ validBefore: BigInt(authorization.validBefore),
53
+ nonce: authorization.nonce
54
+ }
55
+ });
56
+ const payload = {
57
+ x402Version: 2,
58
+ resource: resourceUrl ? { url: resourceUrl } : void 0,
59
+ accepted: option,
60
+ payload: {
61
+ signature,
62
+ authorization
63
+ }
64
+ };
65
+ return encodeBase64Json(payload);
66
+ }
67
+
68
+ // src/client.ts
69
+ var NullClearClient = class {
70
+ arbiterUrl;
71
+ defaultChainId;
72
+ customFetch;
73
+ constructor(config = {}) {
74
+ this.arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
75
+ this.defaultChainId = config.defaultChainId || 8453;
76
+ this.customFetch = config.fetch || fetch;
77
+ }
78
+ async getScore(agentId, chainId) {
79
+ const targetChainId = chainId || this.defaultChainId;
80
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/score/${targetChainId}/${agentId}`);
81
+ if (!res.ok) {
82
+ const err = await res.text();
83
+ throw new Error(`Failed to fetch score for agent ${agentId}: ${res.status} ${err}`);
84
+ }
85
+ const data = await res.json();
86
+ return data.data;
87
+ }
88
+ async clear(request) {
89
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/clear`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify(request)
93
+ });
94
+ const data = await res.json();
95
+ return data;
96
+ }
97
+ async getKya(agentId, chainId) {
98
+ const targetChainId = chainId || this.defaultChainId;
99
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/${targetChainId}/${agentId}`);
100
+ if (!res.ok) {
101
+ if (res.status === 404) return null;
102
+ const err = await res.text();
103
+ throw new Error(`Failed to fetch KYA mandate for agent ${agentId}: ${res.status} ${err}`);
104
+ }
105
+ const data = await res.json();
106
+ return data.data;
107
+ }
108
+ async attestKya(mandate, chainId, signature, signerAddress) {
109
+ const targetChainId = chainId || this.defaultChainId;
110
+ const res = await this.customFetch(`${this.arbiterUrl}/v1/kya/attest`, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({
114
+ mandate,
115
+ chainId: targetChainId,
116
+ signature,
117
+ signerAddress
118
+ })
119
+ });
120
+ if (!res.ok) {
121
+ const err = await res.text();
122
+ throw new Error(`Failed to attest KYA mandate: ${res.status} ${err}`);
123
+ }
124
+ const data = await res.json();
125
+ return data.data;
126
+ }
127
+ };
128
+
129
+ // src/fetch-wrapper.ts
130
+ import { z } from "zod";
131
+ import {
132
+ decodeBase64Json
133
+ } from "@nullclear/shared";
134
+ var NullClearEscrowAbortedError = class extends Error {
135
+ stage;
136
+ buyerCharged;
137
+ validationErrors;
138
+ score;
139
+ upstreamStatus;
140
+ reason;
141
+ constructor(message, details) {
142
+ super(message);
143
+ this.name = "NullClearEscrowAbortedError";
144
+ this.stage = details.stage;
145
+ this.buyerCharged = details.buyerCharged || "0";
146
+ this.validationErrors = details.validationErrors || [];
147
+ this.score = details.score;
148
+ this.upstreamStatus = details.upstreamStatus;
149
+ this.reason = details.reason;
150
+ }
151
+ };
152
+ function zodToJsonSchema(schema) {
153
+ const def = schema._def;
154
+ if (!def) return {};
155
+ const typeName = def.typeName;
156
+ switch (typeName) {
157
+ case "ZodString":
158
+ return { type: "string" };
159
+ case "ZodNumber":
160
+ return { type: "number" };
161
+ case "ZodBoolean":
162
+ return { type: "boolean" };
163
+ case "ZodArray":
164
+ return {
165
+ type: "array",
166
+ items: def.type ? zodToJsonSchema(def.type) : {}
167
+ };
168
+ case "ZodObject": {
169
+ const shape = def.shape();
170
+ const properties = {};
171
+ const required = [];
172
+ for (const [key, value] of Object.entries(shape)) {
173
+ properties[key] = zodToJsonSchema(value);
174
+ const subDef = value._def;
175
+ if (subDef.typeName !== "ZodOptional" && subDef.typeName !== "ZodNullable") {
176
+ required.push(key);
177
+ }
178
+ }
179
+ return {
180
+ type: "object",
181
+ properties,
182
+ required: required.length > 0 ? required : void 0
183
+ };
184
+ }
185
+ case "ZodEnum":
186
+ return {
187
+ type: "string",
188
+ enum: def.values
189
+ };
190
+ case "ZodOptional":
191
+ case "ZodNullable":
192
+ return def.innerType ? zodToJsonSchema(def.innerType) : {};
193
+ default:
194
+ return {};
195
+ }
196
+ }
197
+ function createNullClearFetch(config) {
198
+ const chainId = config.chainId || 8453;
199
+ const arbiterUrl = (config.arbiterUrl || "https://arbiter.nullclear.dev").replace(/\/$/, "");
200
+ const defaultMinTrustScore = config.defaultMinTrustScore || 50;
201
+ const customFetch = config.customFetch || fetch;
202
+ return async function nullclearFetch(input, init) {
203
+ const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
204
+ const fetchInit = { ...init };
205
+ delete fetchInit.nullclear;
206
+ const initialRes = await customFetch(input, fetchInit);
207
+ if (initialRes.status !== 402) {
208
+ return initialRes;
209
+ }
210
+ const paymentRequiredHeader = initialRes.headers.get("PAYMENT-REQUIRED") || initialRes.headers.get("payment-required");
211
+ if (!paymentRequiredHeader) {
212
+ return initialRes;
213
+ }
214
+ let paymentRequired;
215
+ try {
216
+ paymentRequired = decodeBase64Json(paymentRequiredHeader);
217
+ } catch {
218
+ return initialRes;
219
+ }
220
+ const targetNetwork = `eip155:${chainId}`;
221
+ const selectedOption = paymentRequired.accepts.find(
222
+ (opt) => opt.network === targetNetwork && opt.scheme === "exact"
223
+ );
224
+ if (!selectedOption) {
225
+ throw new Error(
226
+ `Seller does not accept payments on network ${targetNetwork}. Accepted: ${paymentRequired.accepts.map((a) => a.network).join(", ")}`
227
+ );
228
+ }
229
+ const paymentSignatureHeader = await signX402Payment(
230
+ config.accountOrPrivateKey,
231
+ selectedOption,
232
+ targetUrl
233
+ );
234
+ let jsonSchema;
235
+ if (init?.nullclear?.schema) {
236
+ if (init.nullclear.schema instanceof z.ZodType) {
237
+ jsonSchema = zodToJsonSchema(init.nullclear.schema);
238
+ } else if (typeof init.nullclear.schema === "object") {
239
+ jsonSchema = init.nullclear.schema;
240
+ }
241
+ }
242
+ const clearPayload = {
243
+ targetUrl,
244
+ method: init?.method || "GET",
245
+ headers: init?.headers || {},
246
+ body: init?.body,
247
+ paymentSignatureHeader,
248
+ sellerAgentId: init?.nullclear?.sellerAgentId,
249
+ minTrustScore: init?.nullclear?.minTrustScore ?? defaultMinTrustScore,
250
+ validation: {
251
+ expectedStatus: [200],
252
+ jsonSchema,
253
+ requiredFields: init?.nullclear?.requiredFields,
254
+ sha256Match: init?.nullclear?.sha256Match
255
+ }
256
+ };
257
+ const arbiterRes = await customFetch(`${arbiterUrl}/v1/clear`, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify(clearPayload)
261
+ });
262
+ const arbiterData = await arbiterRes.json();
263
+ if (arbiterRes.status === 200 && arbiterData.cleared) {
264
+ const resBody = typeof arbiterData.data === "string" ? arbiterData.data : JSON.stringify(arbiterData.data);
265
+ const resHeaders = new Headers();
266
+ resHeaders.set("Content-Type", "application/json");
267
+ const paymentResponseHdr = arbiterRes.headers.get("PAYMENT-RESPONSE");
268
+ if (paymentResponseHdr) resHeaders.set("PAYMENT-RESPONSE", paymentResponseHdr);
269
+ const attestationHdr = arbiterRes.headers.get("X-NullClear-Attestation");
270
+ if (attestationHdr) resHeaders.set("X-NullClear-Attestation", attestationHdr);
271
+ return new Response(resBody, {
272
+ status: 200,
273
+ headers: resHeaders
274
+ });
275
+ }
276
+ throw new NullClearEscrowAbortedError(
277
+ `NullClear Escrow Aborted: ${arbiterData.stage || "FAILED"} - ${arbiterData.reason || arbiterData.error || (arbiterData.validationErrors || []).join("; ")}`,
278
+ {
279
+ stage: arbiterData.stage || "UNKNOWN_FAILURE",
280
+ buyerCharged: arbiterData.buyerCharged || "0",
281
+ validationErrors: arbiterData.validationErrors || [],
282
+ score: arbiterData.score,
283
+ upstreamStatus: arbiterData.upstreamStatus,
284
+ reason: arbiterData.reason || arbiterData.error
285
+ }
286
+ );
287
+ };
288
+ }
289
+ export {
290
+ NullClearClient,
291
+ NullClearEscrowAbortedError,
292
+ createNullClearFetch,
293
+ signX402Payment,
294
+ zodToJsonSchema
295
+ };
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@nullclear/x402-arbiter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "NullClear Developer and Agent SDK for safe x402 calls with deferred schema escrow",
5
+ "license": "MIT",
6
+ "homepage": "https://nullclear.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Nullclear-Protocol/nullclear.git"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "@nullclear/shared": "1.0.0",
31
+ "viem": "^2.23.0",
32
+ "zod": "^3.24.2"
33
+ },
34
+ "devDependencies": {
35
+ "@nullclear/worker-arbiter": "1.0.0",
36
+ "tsup": "^8.3.6",
37
+ "typescript": "^5.7.3",
38
+ "vitest": "^3.0.5"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm,cjs --dts",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run",
44
+ "lint": "echo \"@nullclear/x402-arbiter clean\""
45
+ }
6
46
  }