x402-safe-fetch 0.11.1 → 0.12.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 +3 -2
- package/index.d.ts +7 -1
- package/index.js +17 -3
- package/package.json +1 -1
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
|
|
|
@@ -94,6 +94,7 @@ When a seller offers both, the exact option is paid. The preflight and the budge
|
|
|
94
94
|
| `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
95
|
| `maxUsd` | `0.05` | Budget per endpoint call; the payment is capped at it and the preflight says `no_go` above it |
|
|
96
96
|
| `onCaution` | `"stop"` | `"stop"`, `"pay"` or `async (preflight) => boolean` |
|
|
97
|
+
| `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
98
|
| `trusted` | `[]` | Hosts paid without a preflight |
|
|
98
99
|
| `onPreflight` | | `(preflight, { url, method, cached }) => void`, e.g. for logging |
|
|
99
100
|
| `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 +147,7 @@ With `tellSellers: true`, a refusal also goes straight to the seller, when its 4
|
|
|
146
147
|
- the sha256 of the refused offer;
|
|
147
148
|
- the endpoint without its query.
|
|
148
149
|
|
|
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
|
|
150
|
+
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
151
|
|
|
151
152
|
## Signed verdicts
|
|
152
153
|
|
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.
|
|
44
|
+
export const VERSION = "0.12.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
|
-
|
|
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
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|