x402-safe-fetch 0.18.0 → 0.20.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 +2 -1
- package/cli.js +28 -3
- package/index.js +1 -1
- package/package.json +1 -1
- package/wallet.js +10 -1
package/README.md
CHANGED
|
@@ -28,9 +28,10 @@ npx x402-safe-fetch find "bitcoin price in usd" # free: who sells it, the pr
|
|
|
28
28
|
npx x402-safe-fetch check <url> # free: the price per network and x402 Doctor's free check; pays nothing
|
|
29
29
|
npx x402-safe-fetch pay <url> --max 0.05 --daily 1
|
|
30
30
|
npx x402-safe-fetch spent # what was paid today, and what is left
|
|
31
|
+
npx x402-safe-fetch fund # no crypto yet: buy USDC with a card (Coinbase checkout)
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
**No key to copy.** `connect` makes a wallet for this computer (its key stays in `~/.x402-safe-fetch/wallet.json`, readable by you only) and shows a link and a code: open it on your phone, sign in to [wallet.fizzl.eu](https://wallet.fizzl.eu) and approve. From then on every payment is first reserved on your Fizzl wallet: your limits there apply, and above them you get Approve / Deny on your phone (Telegram or the dashboard) before anything is signed; the key never leaves this computer.
|
|
34
|
+
**No key to copy.** `connect` makes a wallet for this computer (its key stays in `~/.x402-safe-fetch/wallet.json`, readable by you only) and shows a link and a code: open it on your phone, sign in to [wallet.fizzl.eu](https://wallet.fizzl.eu) and approve. From then on every payment is first reserved on your Fizzl wallet: your limits there apply, and above them you get Approve / Deny on your phone (Telegram or the dashboard) before anything is signed; the key never leaves this computer. Then add a few dollars of USDC on Base to the address it shows (`wallet` shows it again, with the balance). No crypto yet? `npx x402-safe-fetch fund` (`--amount 10`) prints a Coinbase checkout to buy USDC with a card, sent straight to that address: your Fizzl wallet checks this computer's link first and asks Coinbase Onramp for it; you pay Coinbase, Fizzl never holds the money (outside the US you sign in to Coinbase). Or send USDC on Base from Coinbase, a wallet app or an exchange. `init` makes the wallet without linking it, and `EVM_PRIVATE_KEY` still overrides both.
|
|
34
35
|
|
|
35
36
|
`pay` pays USDC on Base, only when x402 Doctor's preflight says go: the cheapest option within `--max`, all calls of the UTC day together within `--daily` (preflights included), and the paid answer checked for instructions aimed at your agent (`--no-answer-check` turns that off). An endpoint that took a payment without delivering isn't paid again for 30 minutes, also by the next command and whatever is added to its URL. `--method POST --data '{"q":1}'` sends a body, `--dry-run` is `check`, and `--json` prints one line of JSON (with the answer, the transaction hash and the day's spend). Exit code 0 is done, 1 is not paid (a check said no, or a limit), 2 is a usage error. The day's spend is kept in `~/.x402-safe-fetch/state.json` (`X402_SAFE_FETCH_HOME` moves it). Use a dedicated wallet with a few dollars.
|
|
36
37
|
|
package/cli.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// npx x402-safe-fetch connect a wallet for this computer, linked to your Fizzl wallet
|
|
10
10
|
// npx x402-safe-fetch pay <url> --max 0.05 --daily 1
|
|
11
11
|
// npx x402-safe-fetch spent what was paid today, and what is left
|
|
12
|
+
// npx x402-safe-fetch fund buy USDC with a card (needs connect)
|
|
12
13
|
//
|
|
13
14
|
// Pays USDC on Base from this computer's own wallet (init or connect make it; EVM_PRIVATE_KEY overrides it). Linked
|
|
14
15
|
// to a Fizzl Agent Wallet (connect), the owner's limits and approvals apply to every payment (wallet.js). The day's spend and the
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
import fs from "node:fs";
|
|
19
20
|
import os from "node:os";
|
|
20
21
|
import path from "node:path";
|
|
21
|
-
import { ensureWallet, readWallet, writeWallet, pair, serverSigner, usdcBalance, WALLET_URL } from "./wallet.js";
|
|
22
|
+
import { ensureWallet, readWallet, writeWallet, pair, serverSigner, usdcBalance, onrampLink, WALLET_URL } from "./wallet.js";
|
|
22
23
|
import {
|
|
23
24
|
createSafeFetch, SafePayError, receiptOf, readOffer, usdOfOption, answerCheckOf, purchaseKey, paidTwiceMessage,
|
|
24
25
|
PAID_ERROR_MS, DOCTOR_URL, VERSION,
|
|
@@ -33,6 +34,7 @@ const HELP = `x402-safe-fetch ${VERSION}: pay for an API call from the command l
|
|
|
33
34
|
(wallet.fizzl.eu): approve the code on your phone. Your limits and approvals then apply.
|
|
34
35
|
init Only the wallet for this computer, not linked: shows the address to send USDC on Base to.
|
|
35
36
|
wallet This computer's wallet: address, USDC balance, and whether it is linked.
|
|
37
|
+
fund Buy USDC with a card for this wallet (Coinbase checkout, $5; --amount <usd>). Needs connect.
|
|
36
38
|
find <need> Free. Who sells it (Fizzl Buy's pick): URL, price, and how often it delivered.
|
|
37
39
|
check <url> Free, pays nothing. The price per network and x402 Doctor's free check of the endpoint.
|
|
38
40
|
pay <url> Pays one call in USDC on Base, only if x402 Doctor's preflight says go.
|
|
@@ -261,6 +263,20 @@ async function walletInfo(args, { store, fetch }) {
|
|
|
261
263
|
return { ok: true, address: wallet.address, usdc_base: usdc, linked: wallet.server ? { url: wallet.server.url, agent: wallet.server.agent } : null };
|
|
262
264
|
}
|
|
263
265
|
|
|
266
|
+
// A Coinbase checkout for this wallet, asked through the owner's Fizzl wallet (it checks the agent key first).
|
|
267
|
+
async function fund(args, { store, fetch }) {
|
|
268
|
+
const wallet = readWallet(store.home);
|
|
269
|
+
if (!wallet) return { ok: false, code: "no_wallet", message: "no wallet yet: run npx x402-safe-fetch connect first" };
|
|
270
|
+
if (!wallet.server) return { ok: false, code: "not_linked", message: `Buying with a card goes through your Fizzl wallet: run npx x402-safe-fetch connect first (free), then fund. Or send USDC on Base to ${wallet.address}.` };
|
|
271
|
+
const amount = money(args.flags.amount, "amount") ?? 5;
|
|
272
|
+
try {
|
|
273
|
+
const r = await onrampLink({ server: wallet.server, amount, fetch });
|
|
274
|
+
return { ok: true, url: r.url, address: r.address ?? wallet.address, amount_usd: r.amount_usd ?? amount, expires_in_s: r.expires_in_s ?? 300 };
|
|
275
|
+
} catch (err) {
|
|
276
|
+
return { ok: false, code: "fund_unavailable", message: String(err.message).replace(/^wallet server \/v1\/onramp[^:]*: /, "") };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
264
280
|
function spent(args, { store }) {
|
|
265
281
|
const s = store.read();
|
|
266
282
|
const daily = money(args.flags.daily, "daily") ?? 1;
|
|
@@ -302,8 +318,17 @@ function text(cmd, r) {
|
|
|
302
318
|
if (cmd === "connect") lines.push(`Linked to your Fizzl wallet${r.linked?.agent ? ` as "${r.linked.agent}"` : ""}: your limits and approvals now apply.`);
|
|
303
319
|
lines.push(`Address: ${r.address}`, `USDC on Base: ${r.usdc_base === null ? "?" : `$${r.usdc_base}`}`);
|
|
304
320
|
if (cmd !== "connect") lines.push(r.linked ? `Linked to ${r.linked.url}${r.linked.agent ? ` as "${r.linked.agent}"` : ""}` : "Not linked to a Fizzl wallet (run connect for limits and phone approvals).");
|
|
305
|
-
if (!r.usdc_base)
|
|
321
|
+
if (!r.usdc_base) {
|
|
322
|
+
lines.push(
|
|
323
|
+
"To pay, add a few dollars of USDC on Base to the address above:",
|
|
324
|
+
r.linked ? " with a card: npx x402-safe-fetch fund (Coinbase checkout, $5)" : " with a card: run connect first, then npx x402-safe-fetch fund",
|
|
325
|
+
" or send USDC from Coinbase, a wallet app or an exchange (choose the Base network).",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
306
328
|
lines.push('Then: npx x402-safe-fetch find "what you need"');
|
|
329
|
+
} else if (cmd === "fund") {
|
|
330
|
+
if (!r.ok) return r.message;
|
|
331
|
+
lines.push(`Open this in your browser to buy $${r.amount_usd} of USDC with a card (works once, for ${Math.round(r.expires_in_s / 60)} minutes):`, "", ` ${r.url}`, "", `Coinbase sends the USDC to ${r.address} on Base, usually within minutes. Fizzl never holds the money.`, "Outside the US you sign in to (or create) a Coinbase account.");
|
|
307
332
|
} else if (cmd === "spent") {
|
|
308
333
|
lines.push(`${r.day}: $${r.spent_today_usd} paid in ${r.calls} call(s), $${r.left_usd} left of $${r.daily_limit_usd}`);
|
|
309
334
|
if (r.blocked_endpoints.length) lines.push("Not paid again for now (took money without delivering):", ...r.blocked_endpoints.map((b) => ` ${b}`));
|
|
@@ -315,7 +340,7 @@ export async function main(argv, { fetch = globalThis.fetch, env = process.env,
|
|
|
315
340
|
const args = parseArgs(argv);
|
|
316
341
|
const cmd = args._[0];
|
|
317
342
|
if (!cmd || args.flags.help || cmd === "help") { out(HELP); return cmd || args.flags.help ? 0 : 2; }
|
|
318
|
-
const commands = { connect, init, wallet: walletInfo, find, check, pay, spent };
|
|
343
|
+
const commands = { connect, init, wallet: walletInfo, fund, find, check, pay, spent };
|
|
319
344
|
if (!commands[cmd]) { err(`unknown command "${cmd}"\n\n${HELP}`); return 2; }
|
|
320
345
|
try {
|
|
321
346
|
const say = args.flags.json ? () => {} : (s) => err(s);
|
package/index.js
CHANGED
|
@@ -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.
|
|
54
|
+
export const VERSION = "0.20.0";
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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
|
"bin": {
|
package/wallet.js
CHANGED
|
@@ -13,7 +13,6 @@ import { SafePayError } from "./index.js";
|
|
|
13
13
|
export const WALLET_URL = "https://wallet.fizzl.eu";
|
|
14
14
|
export const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
15
15
|
const BASE_RPC = "https://mainnet.base.org";
|
|
16
|
-
|
|
17
16
|
export function walletFile(home) {
|
|
18
17
|
return path.join(home, "wallet.json");
|
|
19
18
|
}
|
|
@@ -88,6 +87,16 @@ export async function pair({ address, label = "x402-safe-fetch", url = WALLET_UR
|
|
|
88
87
|
throw new Error("the code expired before it was approved (10 minutes); run connect again");
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
/**
|
|
91
|
+
* A Coinbase checkout (single use, 5 minutes) to buy USDC on Base with a card, sent to this wallet's own address: the
|
|
92
|
+
* owner's wallet server checks the agent key and asks for it (x402 Doctor holds the CDP key). Coinbase takes the payment
|
|
93
|
+
* and sends the USDC; Fizzl never holds the money. Returns { url, address, amount_usd, expires_in_s }.
|
|
94
|
+
*/
|
|
95
|
+
export async function onrampLink({ server, amount = 5, fetch = globalThis.fetch }) {
|
|
96
|
+
const base = server.url.replace(/\/$/, "");
|
|
97
|
+
return call(fetch, base, server.key, "GET", `/v1/onramp?amount=${encodeURIComponent(String(amount))}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
/**
|
|
92
101
|
* A signer that asks the owner's wallet server before each signature (reserve), waits for the owner's approval when
|
|
93
102
|
* a limit is reached, and reports what was signed afterwards (finish). Throws SafePayError wallet_denied or
|