x402-safe-fetch 0.4.0 → 0.5.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 +9 -0
- package/index.d.ts +2 -0
- package/index.js +33 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ Solana works the same way: `network: "solana"` and register an `ExactSvmScheme`
|
|
|
59
59
|
| `verifyReceipts` | `"require"` | Check Doctor's signed receipt on every preflight; `"off"` skips it |
|
|
60
60
|
| `doctorSigners` | Doctor's published signer | Accepted signer addresses (for a self-hosted Doctor) |
|
|
61
61
|
| `diagnoseOnFailure` | `false` | When a payment still fails, buy a $0.01 Doctor diagnosis of why (see below) |
|
|
62
|
+
| `shareOutcomes` | `false` | After paying, tell Doctor whether the payment worked, so its later preflights learn from it (see below) |
|
|
62
63
|
| `onDiagnosis` | | `(report, { url, method, status, error }) => void`, e.g. for logging or alerting |
|
|
63
64
|
| `authority` | the Fizzl payout wallet | Wallet whose certificates also make a signer trusted (key rotation); `null` accepts only `doctorSigners` |
|
|
64
65
|
|
|
@@ -88,6 +89,14 @@ if (res.status === 402) {
|
|
|
88
89
|
|
|
89
90
|
Doctor's requests carry `user-agent: x402-safe-fetch/<version>` so the service can count how often the package is used; nothing about your agent or wallet is sent beyond the payment itself.
|
|
90
91
|
|
|
92
|
+
## Helping the preflight learn
|
|
93
|
+
|
|
94
|
+
With `shareOutcomes: true`, safe-fetch tells x402 Doctor what happened after it paid an endpoint that a preflight approved: `paid_ok` (2xx), `paid_failed` (402 again: the payment was not accepted) or `paid_error` (another error). Doctor counts these reports per endpoint over 30 days, from every agent that shares them. When most payments to an endpoint failed, reported by at least three different wallets, later preflights for it answer `caution` (`payments_fail_after_preflight`), so the next agent does not pay into the same wall.
|
|
95
|
+
|
|
96
|
+
- What is sent: the outcome, the HTTP status, the signed preflight you already received and its query (endpoint, method, budget, network). No keys, no response content.
|
|
97
|
+
- A report only counts with a real, paid preflight for that endpoint, once per preflight, so reports cannot be made up for free.
|
|
98
|
+
- It is sent after the answer and never delays or changes it. Off by default.
|
|
99
|
+
|
|
91
100
|
## Signed verdicts
|
|
92
101
|
|
|
93
102
|
x402 Doctor signs every paid preflight (EIP-191 over canonical JSON, with the request inside the signed body; see [Signed verdicts](https://github.com/Fizzl13/x402-doctor#signed-verdicts)). safe-fetch checks that signature before it acts on a verdict:
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface SafeFetchOptions {
|
|
|
55
55
|
/** When a payment still fails (402 again, or paying throws), buy a $0.01 Doctor diagnosis of why. Default false. */
|
|
56
56
|
diagnoseOnFailure?: boolean;
|
|
57
57
|
onDiagnosis?: (report: Diagnosis, info: { url: string; method: string; status: number | null; error: Error | null }) => void;
|
|
58
|
+
/** After paying, tell Doctor whether the payment worked (with the signed preflight as proof), so later preflights learn from it. Default false. */
|
|
59
|
+
shareOutcomes?: boolean;
|
|
58
60
|
fetch?: typeof globalThis.fetch;
|
|
59
61
|
/** Advanced/testing: a paying fetch capped at `cap` (e.g. "$0.05"). */
|
|
60
62
|
createPayingFetch?: (cap: string) => typeof globalThis.fetch;
|
package/index.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// With diagnoseOnFailure, a payment that still fails (the endpoint answers 402
|
|
13
13
|
// again, or paying throws) gets a $0.01 Doctor diagnosis that says why and how
|
|
14
14
|
// to fix it: onDiagnosis(report), diagnosisOf(response) or error.diagnosis.
|
|
15
|
+
// With shareOutcomes, after paying it tells Doctor what happened (paid_ok,
|
|
16
|
+
// paid_failed, paid_error), with the signed preflight as proof, so later
|
|
17
|
+
// preflights for that endpoint learn from what agents ran into. Off by
|
|
18
|
+
// default; no keys, amounts or response content are sent.
|
|
15
19
|
// Every preflight must carry Doctor's signed receipt for exactly this request
|
|
16
20
|
// (endpoint, method, budget, network), checked against Doctor's pinned signer:
|
|
17
21
|
// a missing, changed or forged verdict is never a payment (receipt.js).
|
|
@@ -27,7 +31,7 @@ export { verifyReceipt, recoverSigner, canonicalJson, inputHash, certMessage, DO
|
|
|
27
31
|
export const DOCTOR_URL = "https://x402-doctor.fizzl.eu";
|
|
28
32
|
export const PREFLIGHT_CAP = "$0.002"; // the preflight costs $0.001; never more than twice that
|
|
29
33
|
export const DIAGNOSE_CAP = "$0.02"; // the diagnosis costs $0.01; never more than twice that
|
|
30
|
-
export const VERSION = "0.
|
|
34
|
+
export const VERSION = "0.5.0";
|
|
31
35
|
// Doctor sees which calls come from this package (in its usage counts), nothing more.
|
|
32
36
|
const DOCTOR_HEADERS = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION}` };
|
|
33
37
|
export const BASE = "eip155:8453";
|
|
@@ -116,6 +120,7 @@ function requestOf(input, init) {
|
|
|
116
120
|
* @param {typeof fetch} [options.fetch] the underlying fetch (default globalThis.fetch)
|
|
117
121
|
* @param {boolean} [options.diagnoseOnFailure] when a payment still fails, buy a $0.01 Doctor diagnosis of why (default false)
|
|
118
122
|
* @param {(report: object, info: {url: string, method: string, status: number|null, error: Error|null}) => void} [options.onDiagnosis]
|
|
123
|
+
* @param {boolean} [options.shareOutcomes] after paying, tell Doctor whether the payment worked (default false)
|
|
119
124
|
* @param {(cap: string) => typeof fetch} [options.createPayingFetch] advanced/testing: a paying fetch capped at `cap`
|
|
120
125
|
* @param {() => number} [options.now]
|
|
121
126
|
*/
|
|
@@ -133,6 +138,7 @@ export function createSafeFetch({
|
|
|
133
138
|
authority = AUTHORITY,
|
|
134
139
|
diagnoseOnFailure = false,
|
|
135
140
|
onDiagnosis,
|
|
141
|
+
shareOutcomes = false,
|
|
136
142
|
fetch: baseFetch = globalThis.fetch,
|
|
137
143
|
createPayingFetch,
|
|
138
144
|
now = Date.now,
|
|
@@ -211,6 +217,26 @@ export function createSafeFetch({
|
|
|
211
217
|
return report;
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
// One outcome report per paid preflight: Doctor counts it once anyway.
|
|
221
|
+
const reported = new Set();
|
|
222
|
+
function reportOutcome(url, method, preflight, res) {
|
|
223
|
+
if (!shareOutcomes || !preflight?.receipt?.request_id || reported.has(preflight.receipt.request_id)) return;
|
|
224
|
+
const outcome = res.ok ? "paid_ok" : res.status === 402 ? "paid_failed" : res.status >= 400 ? "paid_error" : null;
|
|
225
|
+
if (!outcome) return;
|
|
226
|
+
reported.add(preflight.receipt.request_id);
|
|
227
|
+
if (reported.size > 1000) reported.delete(reported.values().next().value);
|
|
228
|
+
const query = Object.fromEntries(new URL(preflightUrl(url, { method, maxUsd: budget, network: payNetwork, doctorUrl })).searchParams);
|
|
229
|
+
// Fire and forget: the report never delays or changes the answer.
|
|
230
|
+
Promise.resolve()
|
|
231
|
+
.then(() => baseFetch(`${doctorUrl.replace(/\/$/, "")}/api/v1/outcome`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { ...DOCTOR_HEADERS, "content-type": "application/json" },
|
|
234
|
+
body: JSON.stringify({ outcome, status: res.status, preflight, query }),
|
|
235
|
+
}))
|
|
236
|
+
.then((r) => r?.body?.cancel?.())
|
|
237
|
+
.catch(() => {});
|
|
238
|
+
}
|
|
239
|
+
|
|
214
240
|
async function payAndCheck(url, init, method) {
|
|
215
241
|
if (!diagnoseOnFailure) return payEndpoint(url, init);
|
|
216
242
|
let res;
|
|
@@ -244,6 +270,11 @@ export function createSafeFetch({
|
|
|
244
270
|
if (trustedHosts.has(hostOf(url))) return payAndCheck(url, init, method);
|
|
245
271
|
|
|
246
272
|
const { preflight, cached } = await preflightFor(url, method);
|
|
273
|
+
const payAfterPreflight = async () => {
|
|
274
|
+
const res = await payAndCheck(url, init, method);
|
|
275
|
+
reportOutcome(url, method, preflight, res);
|
|
276
|
+
return res;
|
|
277
|
+
};
|
|
247
278
|
onPreflight?.(preflight, { url, method, cached });
|
|
248
279
|
const why = preflight.summary || preflight.verdict;
|
|
249
280
|
if (preflight.verdict === "no_go") throw new SafePayError(`not paid: ${why}`, { code: "no_go", preflight, url });
|
|
@@ -253,6 +284,6 @@ export function createSafeFetch({
|
|
|
253
284
|
} else if (preflight.verdict !== "go") {
|
|
254
285
|
throw new SafePayError(`not paid: unknown verdict ${preflight.verdict}`, { code: "preflight_failed", preflight, url });
|
|
255
286
|
}
|
|
256
|
-
return
|
|
287
|
+
return payAfterPreflight();
|
|
257
288
|
};
|
|
258
289
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|