x402-safe-fetch 0.11.1 → 0.13.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/README.md CHANGED
@@ -6,7 +6,7 @@ A free endpoint is answered as is. For a paid one (HTTP 402), your agent first p
6
6
 
7
7
  | Preflight | What `safeFetch` does |
8
8
  |---|---|
9
- | `go` | Pays the endpoint, never more than `maxUsd` |
9
+ | `go` | Pays the endpoint, never more than `maxUsd`; with `onIncomplete: "stop"`, not when Doctor says a check couldn't run (`checks_complete: false`) |
10
10
  | `caution` | `onCaution`: `"stop"` (default, throws), `"pay"`, or your own function (e.g. ask the user) |
11
11
  | `no_go` | Throws `SafePayError`; the endpoint is not paid |
12
12
 
@@ -86,6 +86,32 @@ const safeFetch = createSafeFetch({
86
86
 
87
87
  When a seller offers both, the exact option is paid. The preflight and the budget apply the same way.
88
88
 
89
+ ## Already have an x402 client? One line
90
+
91
+ If your agent already pays with the official x402 client (`x402Client` from `@x402/core`, used by `@x402/fetch`, `@x402/axios` and the other transports), you don't have to switch to `safeFetch`. Add the preflight as a hook, and every payment that client makes is checked first:
92
+
93
+ ```js
94
+ import { x402Client } from "@x402/core/client";
95
+ import { wrapFetchWithPayment } from "@x402/fetch";
96
+ import { doctorPreflight } from "x402-safe-fetch/hook";
97
+
98
+ const client = new x402Client();
99
+ // … register your schemes as before (registerExactEvmScheme(client, { signer }), …)
100
+ client.onBeforePaymentCreation(doctorPreflight({ client, maxUsd: 0.05 }));
101
+
102
+ const fetchWithPayment = wrapFetchWithPayment(fetch, client); // unchanged
103
+ ```
104
+
105
+ Before the client signs a payment, the hook asks x402 Doctor ($0.001, paid by the same client, or 1 prepaid credit with `creditKey`). Then:
106
+
107
+ - `no_go`: the payment is aborted (`Payment creation aborted: x402 Doctor: …`). Nothing is signed.
108
+ - `caution`: aborted, unless `onCaution` is `"pay"` or your function says yes.
109
+ - `go`: the payment goes ahead. With `onIncomplete: "stop"`, a `go` whose `checks_complete` is false is aborted too.
110
+
111
+ The verdict is signed by Doctor and checked, and reused while the endpoint asks the same thing (same payTo, amount, token and network). Doctor's own 402 for the preflight is never checked again and is paid only up to $0.002. Options: `client`, `pay` or `creditKey` (how the preflight is paid), `maxUsd`, `method`, `onCaution`, `onIncomplete`, `onError` (`"stop"` by default), `trusted`, `onPreflight`, `cacheMs`.
112
+
113
+ The hook can't see your request, only the 402, so Doctor checks the endpoint with `method` (GET by default; it tries the other method too).
114
+
89
115
  ## Options
90
116
 
91
117
  | Option | Default | |
@@ -94,6 +120,7 @@ When a seller offers both, the exact option is paid. The preflight and the budge
94
120
  | `network` | `"base"` | `"base"`, `"solana"`, `"algorand"` (USDC), `"xrpl"` (RLUSD), `"stellar"` (USDC), `"xlayer"` (USD₮0 or USDC), their `-testnet` forms, or a CAIP-2 id; the preflight checks the option on this network |
95
121
  | `maxUsd` | `0.05` | Budget per endpoint call; the payment is capped at it and the preflight says `no_go` above it |
96
122
  | `onCaution` | `"stop"` | `"stop"`, `"pay"` or `async (preflight) => boolean` |
123
+ | `onIncomplete` | `"pay"` | A `go` whose `checks_complete` is `false` (a check that applies couldn't run this time, named in `not_checked`): `"pay"`, `"stop"` (throws code `incomplete`; not reported to the seller as a refusal) or `async (preflight) => boolean`. Such a verdict is reused for at most 10 minutes |
97
124
  | `trusted` | `[]` | Hosts paid without a preflight |
98
125
  | `onPreflight` | | `(preflight, { url, method, cached }) => void`, e.g. for logging |
99
126
  | `cacheMs` | 1 hour | How long a verdict is reused per method and URL while the 402 offers the same thing (at most 10 minutes when the offer can't be read); any change in price, payout address, token or network means a new preflight |
@@ -146,7 +173,7 @@ With `tellSellers: true`, a refusal also goes straight to the seller, when its 4
146
173
  - the sha256 of the refused offer;
147
174
  - the endpoint without its query.
148
175
 
149
- No keys, no amounts from your wallet, nothing about you, and no `by` naming safe-fetch, so buyers aren't grouped by the client they use. A `screening_hit` (sanctioned payout address) isn't sent: the draft (0.2) takes it only signed, and safe-fetch holds no key to sign with. It goes only to the endpoint's own site, with redirects refused, at most once a day per endpoint and reason, and never delays the error you get. It works with or without `shareOutcomes`.
176
+ No keys, no amounts from your wallet, nothing about you, and no `by` naming safe-fetch, so buyers aren't grouped by the client they use. A `screening_hit` (sanctioned payout address) isn't sent: the draft takes it only signed, and safe-fetch holds no key to sign with. It goes only to the endpoint's own site, with redirects refused, at most once a day per endpoint and reason, and never delays the error you get. It works with or without `shareOutcomes`.
150
177
 
151
178
  ## Signed verdicts
152
179
 
package/hook.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { Preflight } from "./index.js";
2
+
3
+ export interface DoctorPreflightOptions {
4
+ /** The x402Client the hook is added to: it pays the $0.001 preflight too (Doctor's own 402 is skipped by the hook). */
5
+ client?: unknown;
6
+ /** Or a fetch that pays x402, for the preflight. */
7
+ pay?: typeof fetch;
8
+ /** Or prepaid x402 Doctor credits (xdc_…): tried first, then pay or client. */
9
+ creditKey?: string;
10
+ /** Your budget per call in USD: Doctor says no_go above it. */
11
+ maxUsd?: number | string;
12
+ /** How your calls reach the endpoints. Default "GET"; Doctor tries the other method too. */
13
+ method?: "GET" | "POST";
14
+ /** On a caution verdict. Default "stop". */
15
+ onCaution?: "stop" | "pay" | ((preflight: Preflight) => boolean | Promise<boolean>);
16
+ /** On a go whose checks_complete is false. Default "pay". */
17
+ onIncomplete?: "pay" | "stop" | ((preflight: Preflight) => boolean | Promise<boolean>);
18
+ /** When Doctor can't be asked. Default "stop". */
19
+ onError?: "stop" | "pay";
20
+ /** Hosts paid without a preflight. */
21
+ trusted?: string[];
22
+ onPreflight?: (preflight: Preflight, info: { url: string; cached: boolean }) => void;
23
+ /** How long a verdict is reused while the endpoint asks the same thing, in ms. Default 1 hour (10 minutes for an incomplete one). */
24
+ cacheMs?: number;
25
+ doctorUrl?: string;
26
+ verifyReceipts?: "require" | "off";
27
+ doctorSigners?: string[];
28
+ authority?: string | null;
29
+ fetch?: typeof fetch;
30
+ now?: () => number;
31
+ }
32
+
33
+ /** A BeforePaymentCreationHook for any @x402/core x402Client: client.onBeforePaymentCreation(doctorPreflight({ client })). */
34
+ export function doctorPreflight(options?: DoctorPreflightOptions): (context: { paymentRequired: unknown; selectedRequirements: unknown }) => Promise<void | { abort: true; reason: string }>;
package/hook.js ADDED
@@ -0,0 +1,145 @@
1
+ // x402 Doctor's pre-payment check as a hook for any x402 client (@x402/core x402Client, which @x402/fetch,
2
+ // @x402/axios and the other x402 transports use): one line, and every payment that client makes is checked first.
3
+ //
4
+ // import { doctorPreflight } from "x402-safe-fetch/hook";
5
+ // client.onBeforePaymentCreation(doctorPreflight({ client }));
6
+ //
7
+ // Before the client signs a payment, the hook asks Doctor (GET /api/v1/preflight, $0.001, or 1 prepaid credit)
8
+ // whether the endpoint is safe to pay: no_go stops the payment, caution stops it unless onCaution says otherwise,
9
+ // go lets it through. The verdict is signed by Doctor and checked; it is reused while the endpoint asks the same
10
+ // thing. Doctor's own 402 (for the preflight itself) is never checked again, and is paid only up to $0.002.
11
+ import { wrapFetchWithPayment } from "@x402/fetch";
12
+ import { DOCTOR_URL, preflightUrl, VERSION } from "./index.js";
13
+ import { verifyReceipt, DOCTOR_SIGNERS, AUTHORITY } from "./receipt.js";
14
+
15
+ const CREDIT_HEADER = "x-credit-key";
16
+ const CREDIT_KEY = /^xdc_[A-Za-z0-9_-]{43}$/;
17
+ // The preflight costs $0.001; its own payment is refused above twice that (USDC and the other dollar tokens Doctor
18
+ // takes have 6 decimals; RLUSD on the XRP Ledger is a decimal amount).
19
+ const PREFLIGHT_MAX_ATOMIC = 2000n;
20
+ const PREFLIGHT_MAX_DECIMAL = 0.002;
21
+
22
+ const hostOf = (u) => { try { return new URL(u).host; } catch { return null; } };
23
+ const abort = (reason) => ({ abort: true, reason: String(reason).slice(0, 300) });
24
+
25
+ // Within the preflight's price: atomic units (EVM, Solana, Algorand, Stellar) or a decimal amount (XRPL RLUSD).
26
+ function withinPreflightPrice(amount) {
27
+ const s = String(amount ?? "");
28
+ if (/^\d+$/.test(s)) return BigInt(s) <= PREFLIGHT_MAX_ATOMIC;
29
+ const n = Number(s);
30
+ return Number.isFinite(n) && n > 0 && n <= PREFLIGHT_MAX_DECIMAL;
31
+ }
32
+
33
+ /**
34
+ * @param {object} options
35
+ * @param {object} [options.client] the x402Client the hook is added to: it pays the preflight too (the hook skips Doctor's own 402)
36
+ * @param {typeof fetch} [options.pay] or a fetch that pays x402, for the preflight
37
+ * @param {string} [options.creditKey] or prepaid Doctor credits (xdc_…): tried first, then pay or client
38
+ * @param {number|string} [options.maxUsd] your budget per call: Doctor says no_go above it
39
+ * @param {"GET"|"POST"} [options.method] how your calls reach the endpoints (Doctor tries the other method too)
40
+ * @param {"stop"|"pay"|((preflight: object) => boolean|Promise<boolean>)} [options.onCaution] default "stop"
41
+ * @param {"pay"|"stop"|((preflight: object) => boolean|Promise<boolean>)} [options.onIncomplete] a go whose checks_complete is false; default "pay"
42
+ * @param {"stop"|"pay"} [options.onError] when Doctor can't be asked; default "stop"
43
+ * @param {string[]} [options.trusted] hosts paid without a preflight
44
+ * @param {(preflight: object, info: {url: string, cached: boolean}) => void} [options.onPreflight]
45
+ * @param {number} [options.cacheMs] how long a verdict is reused while the endpoint asks the same thing (default 1 hour)
46
+ * @param {"require"|"off"} [options.verifyReceipts] check Doctor's signature (default "require")
47
+ * @returns {(context: {paymentRequired: object, selectedRequirements: object}) => Promise<void|{abort: true, reason: string}>}
48
+ */
49
+ export function doctorPreflight({
50
+ client,
51
+ pay,
52
+ creditKey,
53
+ maxUsd,
54
+ method = "GET",
55
+ onCaution = "stop",
56
+ onIncomplete = "pay",
57
+ onError = "stop",
58
+ trusted = [],
59
+ onPreflight,
60
+ cacheMs = 60 * 60 * 1000,
61
+ doctorUrl = DOCTOR_URL,
62
+ verifyReceipts = "require",
63
+ doctorSigners = DOCTOR_SIGNERS,
64
+ authority = AUTHORITY,
65
+ fetch: plainFetch = globalThis.fetch,
66
+ now = Date.now,
67
+ } = {}) {
68
+ if (!client && typeof pay !== "function" && !creditKey) throw new TypeError("doctorPreflight needs a way to pay the $0.001 preflight: client, pay or creditKey");
69
+ if (creditKey !== undefined && !CREDIT_KEY.test(String(creditKey))) throw new TypeError("creditKey must be an x402 Doctor credit key (xdc_…)");
70
+ if (!["stop", "pay"].includes(onCaution) && typeof onCaution !== "function") throw new TypeError('onCaution must be "stop", "pay" or a function');
71
+ if (!["pay", "stop"].includes(onIncomplete) && typeof onIncomplete !== "function") throw new TypeError('onIncomplete must be "pay", "stop" or a function');
72
+ if (!["stop", "pay"].includes(onError)) throw new TypeError('onError must be "stop" or "pay"');
73
+ if (!["require", "off"].includes(verifyReceipts)) throw new TypeError('verifyReceipts must be "require" or "off"');
74
+ const doctorHost = hostOf(doctorUrl);
75
+ const trustedHosts = new Set(trusted.map((t) => hostOf(/^https?:/.test(t) ? t : `https://${t}`)).filter(Boolean));
76
+ let payingFetch = typeof pay === "function" ? pay : null;
77
+ const payer = () => (payingFetch ??= client ? wrapFetchWithPayment(plainFetch, client) : null);
78
+ const headers = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION} (hook)` };
79
+ const verdicts = new Map();
80
+
81
+ async function ask(url, network) {
82
+ const pfUrl = preflightUrl(url, { method, maxUsd, network, doctorUrl });
83
+ let res = null;
84
+ if (creditKey) {
85
+ res = await plainFetch(pfUrl, { headers: { ...headers, [CREDIT_HEADER]: creditKey } });
86
+ if (res.status === 402) res = null; // credits used up or unknown: pay instead, if we can
87
+ }
88
+ if (!res) {
89
+ const f = payer();
90
+ if (!f) throw new Error("no prepaid credits left and nothing to pay the preflight with");
91
+ res = await f(pfUrl, { headers });
92
+ }
93
+ const preflight = await res.json().catch(() => null);
94
+ if (!res.ok || !preflight?.verdict) throw new Error(`preflight answered HTTP ${res.status}`);
95
+ if (verifyReceipts === "require") {
96
+ const input = Object.fromEntries(new URL(pfUrl).searchParams);
97
+ const check = verifyReceipt(preflight, { signers: doctorSigners, route: "GET /api/v1/preflight", input, authority, service: "x402-doctor" });
98
+ if (!check.valid) throw new Error(`preflight not trusted (${check.reason})`);
99
+ }
100
+ return preflight;
101
+ }
102
+
103
+ return async function doctorPreflightHook({ paymentRequired, selectedRequirements } = {}) {
104
+ const url = paymentRequired?.resource?.url;
105
+ const host = hostOf(url);
106
+ // Doctor's own 402, for the preflight we are paying: never checked again, and never more than its price.
107
+ if (host && host === doctorHost) {
108
+ return withinPreflightPrice(selectedRequirements?.amount ?? selectedRequirements?.maxAmountRequired) ? undefined : abort("x402 Doctor's preflight asked more than $0.002");
109
+ }
110
+ if (!url || (host && trustedHosts.has(host))) return undefined;
111
+
112
+ const r = selectedRequirements || {};
113
+ const key = JSON.stringify([url, r.network, r.asset, String(r.amount ?? r.maxAmountRequired ?? ""), r.payTo]);
114
+ const hit = verdicts.get(key);
115
+ const ttl = hit?.preflight?.checks_complete === false ? Math.min(cacheMs, 10 * 60 * 1000) : cacheMs;
116
+ let preflight;
117
+ let cached = false;
118
+ if (hit && hit.at + ttl > now()) {
119
+ preflight = hit.preflight;
120
+ cached = true;
121
+ } else {
122
+ try {
123
+ preflight = await ask(url, r.network);
124
+ } catch (err) {
125
+ return onError === "pay" ? undefined : abort(`x402 Doctor's preflight failed (${err.message}); not paid`);
126
+ }
127
+ verdicts.set(key, { preflight, at: now() });
128
+ if (verdicts.size > 1000) verdicts.delete(verdicts.keys().next().value);
129
+ }
130
+ onPreflight?.(preflight, { url, cached });
131
+
132
+ const why = preflight.summary || preflight.verdict;
133
+ if (preflight.verdict === "no_go") return abort(`x402 Doctor: ${why}`);
134
+ if (preflight.verdict === "caution") {
135
+ const ok = onCaution === "pay" || (typeof onCaution === "function" && (await onCaution(preflight)) === true);
136
+ return ok ? undefined : abort(`x402 Doctor (caution): ${why}`);
137
+ }
138
+ if (preflight.verdict !== "go") return abort(`x402 Doctor: unknown verdict ${preflight.verdict}`);
139
+ if (preflight.checks_complete === false && onIncomplete !== "pay") {
140
+ const ok = typeof onIncomplete === "function" && (await onIncomplete(preflight)) === true;
141
+ if (!ok) return abort("x402 Doctor: go, but not every check ran; try again in a few minutes");
142
+ }
143
+ return undefined;
144
+ };
145
+ }
package/index.d.ts CHANGED
@@ -26,6 +26,10 @@ export interface Preflight {
26
26
  summary?: string;
27
27
  reasons?: Array<{ level: string; code?: string; message: string }>;
28
28
  signals?: Record<string, unknown>;
29
+ checked?: Array<{ check: string; what: string }>;
30
+ not_checked?: Array<{ check: string; because: "unavailable" | "no_data" | "not_sent" | "out_of_scope"; why: string; send?: string }>;
31
+ /** false when a check that applies couldn't run this time */
32
+ checks_complete?: boolean;
29
33
  [key: string]: unknown;
30
34
  }
31
35
 
@@ -37,7 +41,7 @@ export interface Diagnosis {
37
41
  }
38
42
 
39
43
  export declare class SafePayError extends Error {
40
- code: "no_go" | "caution" | "preflight_failed" | "bad_receipt" | "no_option";
44
+ code: "no_go" | "caution" | "incomplete" | "preflight_failed" | "bad_receipt" | "no_option";
41
45
  preflight: Preflight | null;
42
46
  url: string | null;
43
47
  /** Set when diagnoseOnFailure diagnosed a payment that failed. */
@@ -53,6 +57,8 @@ export interface SafeFetchOptions {
53
57
  maxUsd?: number | string;
54
58
  /** What to do on a caution verdict. Default "stop". */
55
59
  onCaution?: "stop" | "pay" | ((preflight: Preflight) => boolean | Promise<boolean>);
60
+ /** What to do on a "go" whose checks_complete is false (a check that applies couldn't run). Default "pay"; "stop" throws code "incomplete". */
61
+ onIncomplete?: "pay" | "stop" | ((preflight: Preflight) => boolean | Promise<boolean>);
56
62
  /** Hosts you already trust: paid without a preflight. */
57
63
  trusted?: string[];
58
64
  onPreflight?: (preflight: Preflight, info: { url: string; method: string; cached: boolean }) => void;
package/index.js CHANGED
@@ -41,7 +41,7 @@ export { verifyReceipt, recoverSigner, canonicalJson, inputHash, certMessage, DO
41
41
  export const DOCTOR_URL = "https://x402-doctor.fizzl.eu";
42
42
  export const PREFLIGHT_CAP = "$0.002"; // the preflight costs $0.001; never more than twice that
43
43
  export const DIAGNOSE_CAP = "$0.02"; // the diagnosis costs $0.01; never more than twice that
44
- export const VERSION = "0.11.1";
44
+ export const VERSION = "0.13.0";
45
45
  // Doctor sees which calls come from this package (in its usage counts), nothing more.
46
46
  const DOCTOR_HEADERS = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION}` };
47
47
  export const BASE = "eip155:8453";
@@ -230,6 +230,8 @@ function sameSite(a, b) {
230
230
  * @param {string} [options.network] the network you pay on: "base", "solana", "algorand", "xrpl", "stellar" or a CAIP-2 id (default Base)
231
231
  * @param {number|string} [options.maxUsd] budget per endpoint call (default 0.05)
232
232
  * @param {"stop"|"pay"|((preflight: object) => boolean|Promise<boolean>)} [options.onCaution]
233
+ * @param {"pay"|"stop"|((preflight: object) => boolean|Promise<boolean>)} [options.onIncomplete] a "go" whose checks_complete
234
+ * is false (a check that applies couldn't run): "pay" (default), "stop" throws code "incomplete", a function decides
233
235
  * @param {string[]} [options.trusted] hosts you already trust: paid without a preflight
234
236
  * @param {(preflight: object, info: {url: string, method: string, cached: boolean}) => void} [options.onPreflight]
235
237
  * @param {number} [options.cacheMs] how long a verdict is reused while the 402 offers the same thing (default 1 hour; at most 10 minutes when the offer can't be read)
@@ -250,6 +252,7 @@ export function createSafeFetch({
250
252
  network = BASE,
251
253
  maxUsd = 0.05,
252
254
  onCaution = "stop",
255
+ onIncomplete = "pay",
253
256
  trusted = [],
254
257
  onPreflight,
255
258
  cacheMs = 60 * 60 * 1000,
@@ -273,6 +276,9 @@ export function createSafeFetch({
273
276
  if (!["stop", "pay"].includes(onCaution) && typeof onCaution !== "function") {
274
277
  throw new TypeError('onCaution must be "stop", "pay" or a function');
275
278
  }
279
+ if (!["pay", "stop"].includes(onIncomplete) && typeof onIncomplete !== "function") {
280
+ throw new TypeError('onIncomplete must be "pay", "stop" or a function');
281
+ }
276
282
  if (!["require", "off"].includes(verifyReceipts)) throw new TypeError('verifyReceipts must be "require" or "off"');
277
283
  const trustedHosts = new Set(trusted.map(hostOf).filter(Boolean));
278
284
 
@@ -309,7 +315,9 @@ export function createSafeFetch({
309
315
  async function preflightFor(url, method, offer) {
310
316
  const key = `${method} ${url}`;
311
317
  const hit = verdicts.get(key);
312
- const ttl = offer === null ? Math.min(cacheMs, UNREAD_OFFER_MS) : cacheMs;
318
+ // An unreadable offer, or a verdict on less than usual (checks_complete false), is reused for at most 10 minutes.
319
+ const short = offer === null || hit?.preflight?.checks_complete === false;
320
+ const ttl = short ? Math.min(cacheMs, UNREAD_OFFER_MS) : cacheMs;
313
321
  if (hit && hit.offer === offer && hit.at + ttl > now()) return { preflight: hit.preflight, cached: true };
314
322
  const pfUrl = preflightUrl(url, { method, maxUsd: budget, network: payNetwork, doctorUrl });
315
323
  const res = await payPreflight(pfUrl, { headers: DOCTOR_HEADERS });
@@ -391,7 +399,7 @@ export function createSafeFetch({
391
399
  if (target.protocol !== "https:" && !["localhost", "127.0.0.1"].includes(target.hostname)) return;
392
400
  if (!sameSite(target.hostname, endpoint.hostname)) return;
393
401
  if (refusal.report.resource !== `${endpoint.origin}${endpoint.pathname}`) return;
394
- // The draft (0.2) takes a screening_hit only signed, and this client holds no key to sign with.
402
+ // The draft takes a screening_hit only signed, and this client holds no key to sign with.
395
403
  if (refusal.signature_required || (refusal.report.reason === "screening_hit" && !refusal.report.signature)) return;
396
404
  const key = `${refusal.report.resource} ${refusal.report.reason}`;
397
405
  if (now() - (toldSellers.get(key) ?? -Infinity) < 24 * 60 * 60 * 1000) return;
@@ -468,6 +476,12 @@ export function createSafeFetch({
468
476
  }
469
477
  } else if (preflight.verdict !== "go") {
470
478
  throw new SafePayError(`not paid: unknown verdict ${preflight.verdict}`, { code: "preflight_failed", preflight, url });
479
+ } else if (preflight.checks_complete === false && onIncomplete !== "pay") {
480
+ // "go" on less than usual: a check that applies couldn't run (Doctor's not_checked says which). Not the seller's
481
+ // doing, so no refusal is reported to it.
482
+ const ok = typeof onIncomplete === "function" && (await onIncomplete(preflight)) === true;
483
+ const missing = (preflight.not_checked || []).filter((n) => n?.because === "unavailable").map((n) => n.check).join(", ") || "unknown";
484
+ if (!ok) throw new SafePayError(`not paid: go, but not every check ran (${missing}); try again in a few minutes`, { code: "incomplete", preflight, url });
471
485
  }
472
486
  return payAfterPreflight();
473
487
  };
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "x402-safe-fetch",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "A fetch for AI agents that checks an unknown x402 endpoint ($0.001 x402 Doctor preflight) before paying it, and never pays above your budget.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./index.d.ts",
9
9
  "default": "./index.js"
10
+ },
11
+ "./hook": {
12
+ "types": "./hook.d.ts",
13
+ "default": "./hook.js"
10
14
  }
11
15
  },
12
16
  "main": "./index.js",
@@ -14,7 +18,9 @@
14
18
  "files": [
15
19
  "index.js",
16
20
  "receipt.js",
21
+ "hook.js",
17
22
  "index.d.ts",
23
+ "hook.d.ts",
18
24
  "README.md",
19
25
  "LICENSE"
20
26
  ],