x402-safe-fetch 0.6.0 → 0.8.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 +8 -2
- package/index.d.ts +5 -2
- package/index.js +33 -6
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A free endpoint is answered as is. For a paid one (HTTP 402), your agent first p
|
|
|
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
|
|
|
13
|
-
The preflight reads the endpoint's 402 challenge and checks, among others, the price against your budget and against what the service advertises, whether the option is payable on your network (USDC, a valid payout address, a Solana payout account that exists, an Algorand payout opted in to USDC), HTTPS, and whether the endpoint is listed in the CDP Bazaar. A verdict is reused while the endpoint's 402 offers the same thing (up to an hour); a changed price, payout address, token or network is checked again right away. Hosts you already trust skip it.
|
|
13
|
+
The preflight reads the endpoint's 402 challenge and checks, among others, the price against your budget and against what the service advertises, whether the option is payable on your network (USDC, a valid payout address, a Solana payout account that exists, an Algorand payout opted in to USDC), whether the payout address is on the OFAC SDN sanctions list (then `no_go`: never paid), HTTPS, and whether the endpoint is listed in the CDP Bazaar. A verdict is reused while the endpoint's 402 offers the same thing (up to an hour); a changed price, payout address, token or network is checked again right away. Hosts you already trust skip it.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -57,6 +57,12 @@ const safeFetch = createSafeFetch({
|
|
|
57
57
|
});
|
|
58
58
|
// XRP Ledger: network: "xrpl" and an ExactXrplScheme from @x402/xrpl/exact/client
|
|
59
59
|
// (createXrplWalletSigner(Wallet.fromSeed(process.env.XRPL_SEED))); needs an RLUSD trust line.
|
|
60
|
+
// Stellar: network: "stellar" and an ExactStellarScheme from @x402/stellar/exact/client
|
|
61
|
+
// (createEd25519Signer(process.env.STELLAR_SECRET, "stellar:pubnet")); needs a USDC trustline.
|
|
62
|
+
// Only Circle's USDC contract is paid, never XLM; the facilitator pays the network fee.
|
|
63
|
+
// X Layer (OKX): network: "xlayer" and an ExactEvmScheme from @x402/evm/exact/client with your viem account.
|
|
64
|
+
// Only USD₮0 (0x779d…3736) or USDC (0x74b7…6d22) is paid, never OKB or another token named USDC; the
|
|
65
|
+
// facilitator (PayAI) pays the gas.
|
|
60
66
|
```
|
|
61
67
|
|
|
62
68
|
## Options
|
|
@@ -64,7 +70,7 @@ const safeFetch = createSafeFetch({
|
|
|
64
70
|
| Option | Default | |
|
|
65
71
|
|---|---|---|
|
|
66
72
|
| `register` | required | `(client) => client.register(network, scheme)`: your payment schemes; your keys stay in your code |
|
|
67
|
-
| `network` | `"base"` | `"base"`, `"solana"`, `"algorand"` (USDC), `"xrpl"` (RLUSD), their `-testnet` forms, or a CAIP-2 id; the preflight checks the option on this network |
|
|
73
|
+
| `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 |
|
|
68
74
|
| `maxUsd` | `0.05` | Budget per endpoint call; the payment is capped at it and the preflight says `no_go` above it |
|
|
69
75
|
| `onCaution` | `"stop"` | `"stop"`, `"pay"` or `async (preflight) => boolean` |
|
|
70
76
|
| `trusted` | `[]` | Hosts paid without a preflight |
|
package/index.d.ts
CHANGED
|
@@ -10,7 +10,10 @@ export declare const ALGORAND: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzk
|
|
|
10
10
|
export declare const ALGORAND_TESTNET: "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
|
|
11
11
|
export declare const XRPL: "xrpl:0";
|
|
12
12
|
export declare const XRPL_TESTNET: "xrpl:1";
|
|
13
|
-
|
|
13
|
+
export declare const STELLAR: "stellar:pubnet";
|
|
14
|
+
export declare const STELLAR_TESTNET: "stellar:testnet";
|
|
15
|
+
export declare const XLAYER: "eip155:196";
|
|
16
|
+
/** The option paid on `network`: on Algorand only USDC, on Stellar only Circle's USDC contract within maxUsd, on X Layer only USD₮0 or USDC within maxUsd, on the XRP Ledger only RLUSD from Ripple's issuer within maxUsd (in dollars); null when there is none. */
|
|
14
17
|
/** What a 402 offers (per x402 option: scheme, network, asset, amount, payTo) as a stable string; null without a readable x402 challenge. Consumes the body. */
|
|
15
18
|
export declare function offerFingerprint(res: Response): Promise<string | null>;
|
|
16
19
|
export declare function payableOption<A extends { network: string; asset?: unknown; amount?: unknown; extra?: Record<string, unknown> }>(accepts: A[], network: string, maxUsd: number | string): A | null;
|
|
@@ -42,7 +45,7 @@ export declare class SafePayError extends Error {
|
|
|
42
45
|
export interface SafeFetchOptions {
|
|
43
46
|
/** Registers your payment schemes, e.g. (c) => c.register("eip155:8453", new ExactEvmScheme(account)). */
|
|
44
47
|
register: (client: x402Client) => unknown;
|
|
45
|
-
/** The network you pay on: "base", "solana", "algorand" (USDC), "xrpl" (RLUSD), their "-testnet" forms, or a CAIP-2 id. Default Base. */
|
|
48
|
+
/** The network you pay on: "base", "solana", "algorand" (USDC), "xrpl" (RLUSD), "stellar" (USDC), "xlayer" (USD₮0 or USDC), their "-testnet" forms, or a CAIP-2 id. Default Base. */
|
|
46
49
|
network?: string;
|
|
47
50
|
/** Budget per endpoint call in USD. Default 0.05. */
|
|
48
51
|
maxUsd?: number | string;
|
package/index.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Payments use @x402/fetch with the schemes you register (your keys stay in
|
|
25
25
|
// your code): register: (client) => client.register("eip155:8453", new ExactEvmScheme(account))
|
|
26
|
-
// On Algorand (USDC)
|
|
26
|
+
// On Algorand (USDC), the XRP Ledger (RLUSD), Stellar (USDC) and X Layer (USD₮0 or USDC) only the dollar
|
|
27
|
+
// stablecoin is paid.
|
|
27
28
|
|
|
28
29
|
import { wrapFetchWithPayment, x402Client, decodePaymentResponseHeader } from "@x402/fetch";
|
|
29
30
|
import { verifyReceipt, DOCTOR_SIGNERS, AUTHORITY } from "./receipt.js";
|
|
@@ -33,7 +34,7 @@ export { verifyReceipt, recoverSigner, canonicalJson, inputHash, certMessage, DO
|
|
|
33
34
|
export const DOCTOR_URL = "https://x402-doctor.fizzl.eu";
|
|
34
35
|
export const PREFLIGHT_CAP = "$0.002"; // the preflight costs $0.001; never more than twice that
|
|
35
36
|
export const DIAGNOSE_CAP = "$0.02"; // the diagnosis costs $0.01; never more than twice that
|
|
36
|
-
export const VERSION = "0.
|
|
37
|
+
export const VERSION = "0.8.0";
|
|
37
38
|
// Doctor sees which calls come from this package (in its usage counts), nothing more.
|
|
38
39
|
const DOCTOR_HEADERS = { accept: "application/json", "user-agent": `x402-safe-fetch/${VERSION}` };
|
|
39
40
|
export const BASE = "eip155:8453";
|
|
@@ -42,13 +43,24 @@ export const ALGORAND = "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=";
|
|
|
42
43
|
export const ALGORAND_TESTNET = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=";
|
|
43
44
|
export const XRPL = "xrpl:0";
|
|
44
45
|
export const XRPL_TESTNET = "xrpl:1";
|
|
45
|
-
const
|
|
46
|
+
export const STELLAR = "stellar:pubnet";
|
|
47
|
+
export const STELLAR_TESTNET = "stellar:testnet";
|
|
48
|
+
export const XLAYER = "eip155:196";
|
|
49
|
+
const NETWORK_ALIASES = { base: BASE, solana: SOLANA, algorand: ALGORAND, "algorand-testnet": ALGORAND_TESTNET, xrpl: XRPL, "xrpl-testnet": XRPL_TESTNET, stellar: STELLAR, "stellar-testnet": STELLAR_TESTNET, xlayer: XLAYER };
|
|
46
50
|
|
|
47
51
|
// On Algorand and the XRP Ledger only the dollar stablecoin is paid: USDC (an ASA) on Algorand, RLUSD from
|
|
48
52
|
// Ripple's issuer on the XRP Ledger. Never ALGO, XRP or another token, so the dollar budget always holds.
|
|
49
53
|
const ALGORAND_USDC = { [ALGORAND]: "31566704", [ALGORAND_TESTNET]: "10458941" };
|
|
50
54
|
const RLUSD_HEX = "524C555344000000000000000000000000000000";
|
|
51
55
|
const RLUSD_ISSUER = { [XRPL]: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", [XRPL_TESTNET]: "rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV" };
|
|
56
|
+
// On Stellar only Circle's USDC token contract (7 decimals), never XLM or another token named USDC.
|
|
57
|
+
const STELLAR_USDC = { [STELLAR]: "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", [STELLAR_TESTNET]: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA" };
|
|
58
|
+
// On X Layer only USD₮0 (what most sellers there ask for) and the USDC x402 Doctor checks against (both 6 decimals,
|
|
59
|
+
// EIP-3009), never OKB or another token that calls itself USDC. @x402/evm has no default asset there, so x402's
|
|
60
|
+
// spend controls only let these through with an allowedAssets entry, capped at the budget (see safeFetch).
|
|
61
|
+
const XLAYER_DOLLARS = {
|
|
62
|
+
[XLAYER]: ["0x779ded0c9e1022225f8e0630b35a9b54be713736", "0x74b7f16337b8972027f6196a17a631ac6de26d22"],
|
|
63
|
+
};
|
|
52
64
|
|
|
53
65
|
// The option to pay on `network`, or null. RLUSD prices are decimal dollars ("0.001", and "2" is two dollars),
|
|
54
66
|
// which x402's spend controls would read as base units, so the budget is checked here in dollars.
|
|
@@ -75,6 +87,14 @@ export function payableOption(accepts, network, maxUsd) {
|
|
|
75
87
|
const cap = Number(String(maxUsd).replace(/^\$/, ""));
|
|
76
88
|
const on = (accepts ?? []).filter((a) => a?.network === network);
|
|
77
89
|
if (ALGORAND_USDC[network]) return on.find((a) => String(a.asset) === ALGORAND_USDC[network]) ?? null;
|
|
90
|
+
// x402's spend controls cap Stellar USDC too; the budget is checked here as well, in dollars.
|
|
91
|
+
if (STELLAR_USDC[network]) {
|
|
92
|
+
return on.find((a) => a.asset === STELLAR_USDC[network] && /^\d+$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) / 1e7 <= cap) ?? null;
|
|
93
|
+
}
|
|
94
|
+
if (XLAYER_DOLLARS[network]) {
|
|
95
|
+
return on.find((a) => XLAYER_DOLLARS[network].includes(String(a.asset ?? "").toLowerCase())
|
|
96
|
+
&& /^\d+$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) / 1e6 <= cap) ?? null;
|
|
97
|
+
}
|
|
78
98
|
if (RLUSD_ISSUER[network]) {
|
|
79
99
|
return on.find((a) => String(a.asset ?? "").toUpperCase() === RLUSD_HEX && a.extra?.issuer === RLUSD_ISSUER[network]
|
|
80
100
|
&& /^\d+(\.\d+)?$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) <= cap) ?? null;
|
|
@@ -100,6 +120,13 @@ export function usdCap(maxUsd) {
|
|
|
100
120
|
return `$${Number(n.toFixed(6))}`;
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
// x402 spend controls for X Layer's dollars: each one allowed up to the budget in atomic units (6 decimals).
|
|
124
|
+
function xlayerAllowance(network, cap) {
|
|
125
|
+
if (!XLAYER_DOLLARS[network]) return {};
|
|
126
|
+
const atomic = String(Math.round(Number(String(cap).replace(/^\$/, "")) * 1e6));
|
|
127
|
+
return { allowedAssets: XLAYER_DOLLARS[network].map((asset) => ({ network, asset, maxAmountPerPayment: atomic })) };
|
|
128
|
+
}
|
|
129
|
+
|
|
103
130
|
export function diagnoseUrl(target, { method = "GET", doctorUrl = DOCTOR_URL } = {}) {
|
|
104
131
|
const q = new URLSearchParams({ url: target, method });
|
|
105
132
|
return `${doctorUrl.replace(/\/$/, "")}/api/v1/diagnose?${q}`;
|
|
@@ -151,7 +178,7 @@ function requestOf(input, init) {
|
|
|
151
178
|
/**
|
|
152
179
|
* @param {object} options
|
|
153
180
|
* @param {(client: x402Client) => unknown} options.register registers your payment schemes on a client
|
|
154
|
-
* @param {string} [options.network] the network you pay on: "base", "solana", "algorand", "xrpl" or a CAIP-2 id (default Base)
|
|
181
|
+
* @param {string} [options.network] the network you pay on: "base", "solana", "algorand", "xrpl", "stellar" or a CAIP-2 id (default Base)
|
|
155
182
|
* @param {number|string} [options.maxUsd] budget per endpoint call (default 0.05)
|
|
156
183
|
* @param {"stop"|"pay"|((preflight: object) => boolean|Promise<boolean>)} [options.onCaution]
|
|
157
184
|
* @param {string[]} [options.trusted] hosts you already trust: paid without a preflight
|
|
@@ -201,9 +228,9 @@ export function createSafeFetch({
|
|
|
201
228
|
const makePayingFetch = createPayingFetch ?? ((cap) => {
|
|
202
229
|
const client = new x402Client((_version, accepts) => {
|
|
203
230
|
const pick = payableOption(accepts, payNetwork, cap);
|
|
204
|
-
if (!pick) throw new SafePayError(`no payment option on ${payNetwork}${ALGORAND_USDC[payNetwork] ? " in USDC" : RLUSD_ISSUER[payNetwork] ? ` in RLUSD within ${cap}` : ""}`, { code: "no_option" });
|
|
231
|
+
if (!pick) throw new SafePayError(`no payment option on ${payNetwork}${ALGORAND_USDC[payNetwork] ? " in USDC" : STELLAR_USDC[payNetwork] ? ` in USDC within ${cap}` : XLAYER_DOLLARS[payNetwork] ? ` in USD₮0 or USDC within ${cap}` : RLUSD_ISSUER[payNetwork] ? ` in RLUSD within ${cap}` : ""}`, { code: "no_option" });
|
|
205
232
|
return pick;
|
|
206
|
-
}).setSpendControls({ maxAmountPerPayment: cap });
|
|
233
|
+
}).setSpendControls({ maxAmountPerPayment: cap, ...xlayerAllowance(payNetwork, cap) });
|
|
207
234
|
register(client);
|
|
208
235
|
return wrapFetchWithPayment(baseFetch, client);
|
|
209
236
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@x402/avm": "^2.28.0",
|
|
33
33
|
"@x402/evm": "^2.27.0",
|
|
34
34
|
"@x402/fetch": "^2.27.0",
|
|
35
|
+
"@x402/stellar": "^2.28.0",
|
|
35
36
|
"@x402/xrpl": "^2.28.0",
|
|
36
37
|
"viem": "^2.56.9"
|
|
37
38
|
},
|
|
@@ -47,7 +48,8 @@
|
|
|
47
48
|
"algorand",
|
|
48
49
|
"xrpl",
|
|
49
50
|
"rlusd",
|
|
50
|
-
"signed-receipts"
|
|
51
|
+
"signed-receipts",
|
|
52
|
+
"stellar"
|
|
51
53
|
],
|
|
52
54
|
"repository": {
|
|
53
55
|
"type": "git",
|