x402-safe-fetch 0.16.1 → 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 +2 -2
- 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:
|
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
|
}
|