x402-safe-fetch 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/index.d.ts +1 -1
- package/index.js +12 -5
- package/package.json +1 -1
- package/paid-guard.js +16 -19
package/README.md
CHANGED
|
@@ -144,7 +144,9 @@ The preflight itself is capped at $0.002 and paid with the same schemes. `receip
|
|
|
144
144
|
|
|
145
145
|
A seller can take the payment and still answer with an error: a 405, a 500, or another 402 asking to pay again. Paying the same request again then usually costs a second payment for nothing. In the agent exam at [exam.fizzl.eu](https://exam.fizzl.eu) this was the most common loss: every model we examined did it at least once, with or without a preflight, because a preflight only sees the 402 before paying.
|
|
146
146
|
|
|
147
|
-
So safe-fetch remembers each paid
|
|
147
|
+
So safe-fetch remembers each paid endpoint (method, origin and path) whose paid answer was not a success. Calling that endpoint again within 30 minutes, with any query string or body, throws `SafePayError` with code `paid_no_delivery` (and `error.earlier`: when it was paid and what it was answered with), without paying and without buying another preflight. Another endpoint is not affected, and a success clears it.
|
|
148
|
+
|
|
149
|
+
The query and the body are left out on purpose. Since 0.16.2: in the exam, an open model that was refused added `&nonce=1`, `&nonce=2`, … to the URL, and each "new" request was paid (eight times). A seller that took your money without delivering doesn't get paid again on that endpoint, whatever the agent changes in the request.
|
|
148
150
|
|
|
149
151
|
```js
|
|
150
152
|
const safeFetch = createSafeFetch({
|
package/index.d.ts
CHANGED
|
@@ -152,7 +152,7 @@ export declare function verifyReceipt(body: unknown, options?: { signers?: strin
|
|
|
152
152
|
/** The reason code reported with a refused outcome: the preflight's first reason at its verdict's level. */
|
|
153
153
|
export function refusalReason(preflight: { verdict?: string; reasons?: { level?: string; code?: string }[] }): string;
|
|
154
154
|
|
|
155
|
-
/** Remembers paid
|
|
155
|
+
/** Remembers paid endpoints (method, origin and path) that took a payment without delivering, so they aren't paid again with any query or body (used by createSafeFetch; usable on its own). */
|
|
156
156
|
export declare function createPaidGuard(options?: { windowMs?: number; now?: () => number }): {
|
|
157
157
|
blocked(url: string, init?: { method?: string; body?: unknown }): { at: number; status: number } | null;
|
|
158
158
|
record(url: string, init: { method?: string; body?: unknown } | undefined, status: number): void;
|
package/index.js
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
// hash of the refused offer; no query, no keys, nothing about the buyer or this client), once a day per endpoint and
|
|
26
26
|
// reason. A screening_hit isn't sent: the draft takes it only signed. Off by default.
|
|
27
27
|
// A paid request answered without delivery (an error, or another 402) is never paid again on its own: the same
|
|
28
|
-
// method,
|
|
29
|
-
// default). The most common loss in the agent exam (exam.fizzl.eu) was paying again after a seller took the payment
|
|
28
|
+
// endpoint (method, origin and path; any query or body) within paidErrorMs throws SafePayError code
|
|
29
|
+
// "paid_no_delivery" (onPaidError "stop", the default). The most common loss in the agent exam (exam.fizzl.eu) was paying again after a seller took the payment
|
|
30
30
|
// and answered with an error; a preflight can't see that, it only reads the 402 before paying (paid-guard.js).
|
|
31
31
|
// Every preflight must carry Doctor's signed receipt for exactly this request
|
|
32
32
|
// (endpoint, method, budget, network), checked against Doctor's pinned signer:
|
|
@@ -51,7 +51,7 @@ export const PRESIGN_GUARD_URL = "https://presign-guard.fizzl.eu";
|
|
|
51
51
|
export const INJECTION_CAP = "$0.02"; // presign-guard's injection check costs $0.01; never more than twice that
|
|
52
52
|
// The header a checked answer carries: green, orange, red (only with onInjection "flag" or a function) or unchecked.
|
|
53
53
|
export const ANSWER_CHECK_HEADER = "x-answer-check";
|
|
54
|
-
export const VERSION = "0.16.
|
|
54
|
+
export const VERSION = "0.16.1";
|
|
55
55
|
// Doctor sees which calls come from this package (in its usage counts), nothing more.
|
|
56
56
|
const DOCTOR_HEADERS = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION}` };
|
|
57
57
|
export const BASE = "eip155:8453";
|
|
@@ -159,8 +159,15 @@ export function payableOption(accepts, network, maxUsd) {
|
|
|
159
159
|
// Base and other EVM chains: "exact" first (what most clients register), then "auth-capture" (Coinbase's escrow:
|
|
160
160
|
// the buyer signs once, the USDC is held and only captured when the seller delivers, e.g. Atom's domain checkout).
|
|
161
161
|
// Either is paid only if your register() added its scheme, and both stay within the budget (x402's spend controls
|
|
162
|
-
// read the amount, which for auth-capture is the most that can be captured).
|
|
163
|
-
|
|
162
|
+
// read the amount, which for auth-capture is the most that can be captured). Of several options of a scheme, the
|
|
163
|
+
// cheapest within the budget: a 402 that lists $0.50 before $0.01 to the same seller is paid a cent, not refused
|
|
164
|
+
// (or overpaid) because the first one costs more.
|
|
165
|
+
const cheapest = (scheme) => on
|
|
166
|
+
.filter((a) => a?.scheme === scheme)
|
|
167
|
+
.map((a) => ({ a, usd: usdOfOption(a, network) }))
|
|
168
|
+
.filter((x) => x.usd !== null && x.usd > 0 && !(x.usd > cap + 1e-9))
|
|
169
|
+
.sort((x, y) => x.usd - y.usd)[0]?.a;
|
|
170
|
+
return cheapest("exact") ?? cheapest("auth-capture") ?? on.find((a) => a?.scheme === "exact") ?? on.find((a) => a?.scheme === "auth-capture") ?? on[0] ?? null;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// The code that stopped the payment: the preflight's first reason at its verdict's level (no_go, then caution), as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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": {
|
package/paid-guard.js
CHANGED
|
@@ -4,32 +4,29 @@
|
|
|
4
4
|
// (a 405, a 500, or another 402 asking to pay again), and the agent pays the same request again, and again. Every
|
|
5
5
|
// model we examined did it at least once, with or without a preflight: the preflight only sees the 402 before paying.
|
|
6
6
|
//
|
|
7
|
-
// The guard remembers each paid
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The guard remembers each paid endpoint (method, origin and path) whose paid answer was not a success. Paying that
|
|
8
|
+
// endpoint again within the window is refused, so the loss stops at one payment. The query string and the body are
|
|
9
|
+
// left out on purpose: in the exam, a model that was refused added "&nonce=1", "&nonce=2", ... to the URL and paid
|
|
10
|
+
// eight more times. A success clears it. Another endpoint (another path, method or host) is not affected. No
|
|
11
|
+
// dependencies: it runs anywhere fetch does.
|
|
10
12
|
|
|
11
13
|
export const PAID_ERROR_MS = 30 * 60 * 1000;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
/** The endpoint as the guard sees it: method, origin and path (no query, no body, no fragment). */
|
|
16
|
+
export function purchaseKey(url, { method = "GET" } = {}) {
|
|
17
|
+
let endpoint = String(url);
|
|
18
|
+
try {
|
|
19
|
+
const u = new URL(endpoint);
|
|
20
|
+
endpoint = `${u.origin}${u.pathname.replace(/\/+$/, "") || "/"}`;
|
|
21
|
+
} catch {
|
|
22
|
+
endpoint = endpoint.split(/[?#]/)[0];
|
|
21
23
|
}
|
|
22
|
-
return `${
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The request as the guard sees it: method, URL and a fingerprint of the body. */
|
|
26
|
-
export function purchaseKey(url, { method = "GET", body } = {}) {
|
|
27
|
-
return `${String(method).toUpperCase()} ${String(url)} ${bodyKey(body)}`;
|
|
24
|
+
return `${String(method).toUpperCase()} ${endpoint}`;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
/**
|
|
31
28
|
* @param {object} [options]
|
|
32
|
-
* @param {number} [options.windowMs] how long a paid
|
|
29
|
+
* @param {number} [options.windowMs] how long a paid endpoint that didn't deliver stays blocked (default 30 minutes)
|
|
33
30
|
* @param {() => number} [options.now]
|
|
34
31
|
*/
|
|
35
32
|
export function createPaidGuard({ windowMs = PAID_ERROR_MS, now = Date.now } = {}) {
|
|
@@ -60,5 +57,5 @@ export function createPaidGuard({ windowMs = PAID_ERROR_MS, now = Date.now } = {
|
|
|
60
57
|
|
|
61
58
|
/** The refusal text, for an error or a tool answer. */
|
|
62
59
|
export function paidTwiceMessage(url, earlier) {
|
|
63
|
-
return `not paid again: ${url} was already paid at ${new Date(earlier.at).toISOString()} and answered HTTP ${earlier.status} without delivering. Paying
|
|
60
|
+
return `not paid again: ${purchaseKey(url).replace(/^GET /, "")} was already paid at ${new Date(earlier.at).toISOString()} and answered HTTP ${earlier.status} without delivering. Paying this endpoint again (with any query or body) would most likely be charged again for nothing; ask the seller, or use another seller.`;
|
|
64
61
|
}
|