x402-safe-fetch 0.7.0 → 0.9.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 +26 -2
- package/index.d.ts +5 -2
- package/index.js +64 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ try {
|
|
|
45
45
|
|
|
46
46
|
Solana works the same way: `network: "solana"` and register an `ExactSvmScheme` from `@x402/svm/exact/client` with a `@solana/kit` signer.
|
|
47
47
|
|
|
48
|
-
Algorand and the XRP Ledger work the same way, and only their dollar stablecoin is ever paid. On Algorand that is USDC (ASA 31566704), never ALGO or another ASA. On the XRP Ledger it is RLUSD from Ripple's issuer, never XRP or a look-alike token. RLUSD prices are decimal dollars, so the budget is checked in dollars. The Doctor preflight is paid on the same network.
|
|
48
|
+
Algorand and the XRP Ledger work the same way, and only their dollar stablecoin is ever paid. On Algorand that is USDC (ASA 31566704), never ALGO or another ASA. Sellers name Algorand mainnet either by the whole genesis hash or by its CAIP-2 id (`algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k`); both are paid, also with `@x402/avm` 2.29.0, which on its own accepts only the short id. On the XRP Ledger it is RLUSD from Ripple's issuer, never XRP or a look-alike token. RLUSD prices are decimal dollars, so the budget is checked in dollars. The Doctor preflight is paid on the same network.
|
|
49
49
|
|
|
50
50
|
```js
|
|
51
51
|
import { toClientAvmSigner } from "@x402/avm";
|
|
@@ -60,14 +60,38 @@ const safeFetch = createSafeFetch({
|
|
|
60
60
|
// Stellar: network: "stellar" and an ExactStellarScheme from @x402/stellar/exact/client
|
|
61
61
|
// (createEd25519Signer(process.env.STELLAR_SECRET, "stellar:pubnet")); needs a USDC trustline.
|
|
62
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.
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
### Escrowed payments (auth-capture)
|
|
69
|
+
|
|
70
|
+
Some sellers on Base take payment through Coinbase's escrow instead of a direct transfer: the x402 scheme
|
|
71
|
+
`auth-capture`. Your wallet signs once, the USDC is held, and the seller captures it only after it has delivered
|
|
72
|
+
(Atom's domain checkout, for instance, captures once the domain is registered, and releases it if registration
|
|
73
|
+
fails). Register that scheme next to the exact one to pay such sellers (`@x402/evm` 2.27 or later):
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { AuthCaptureEvmScheme } from "@x402/evm/auth-capture/client";
|
|
77
|
+
|
|
78
|
+
const safeFetch = createSafeFetch({
|
|
79
|
+
register: (client) => {
|
|
80
|
+
client.register("eip155:8453", new ExactEvmScheme(account));
|
|
81
|
+
client.register("eip155:8453", new AuthCaptureEvmScheme(account));
|
|
82
|
+
},
|
|
83
|
+
maxUsd: 25, // the budget caps the most that can be captured
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When a seller offers both, the exact option is paid. The preflight and the budget apply the same way.
|
|
88
|
+
|
|
65
89
|
## Options
|
|
66
90
|
|
|
67
91
|
| Option | Default | |
|
|
68
92
|
|---|---|---|
|
|
69
93
|
| `register` | required | `(client) => client.register(network, scheme)`: your payment schemes; your keys stay in your code |
|
|
70
|
-
| `network` | `"base"` | `"base"`, `"solana"`, `"algorand"` (USDC), `"xrpl"` (RLUSD), their `-testnet` forms, or a CAIP-2 id; the preflight checks the option on this network |
|
|
94
|
+
| `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 |
|
|
71
95
|
| `maxUsd` | `0.05` | Budget per endpoint call; the payment is capped at it and the preflight says `no_go` above it |
|
|
72
96
|
| `onCaution` | `"stop"` | `"stop"`, `"pay"` or `async (preflight) => boolean` |
|
|
73
97
|
| `trusted` | `[]` | Hosts paid without a preflight |
|
package/index.d.ts
CHANGED
|
@@ -12,9 +12,12 @@ export declare const XRPL: "xrpl:0";
|
|
|
12
12
|
export declare const XRPL_TESTNET: "xrpl:1";
|
|
13
13
|
export declare const STELLAR: "stellar:pubnet";
|
|
14
14
|
export declare const STELLAR_TESTNET: "stellar:testnet";
|
|
15
|
-
|
|
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. */
|
|
16
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. */
|
|
17
18
|
export declare function offerFingerprint(res: Response): Promise<string | null>;
|
|
19
|
+
/** Wraps an Algorand scheme (e.g. @x402/avm's ExactAvmScheme) so it pays sellers that name Algorand by the whole genesis hash as well as by the CAIP-2 id. createSafeFetch does this for you. */
|
|
20
|
+
export declare function anyAlgorandId<S extends object>(scheme: S): S;
|
|
18
21
|
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;
|
|
19
22
|
|
|
20
23
|
/** The x402 Doctor preflight: see https://x402-doctor.fizzl.eu/openapi.json */
|
|
@@ -44,7 +47,7 @@ export declare class SafePayError extends Error {
|
|
|
44
47
|
export interface SafeFetchOptions {
|
|
45
48
|
/** Registers your payment schemes, e.g. (c) => c.register("eip155:8453", new ExactEvmScheme(account)). */
|
|
46
49
|
register: (client: x402Client) => unknown;
|
|
47
|
-
/** The network you pay on: "base", "solana", "algorand" (USDC), "xrpl" (RLUSD), "stellar" (USDC), their "-testnet" forms, or a CAIP-2 id. Default Base. */
|
|
50
|
+
/** 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. */
|
|
48
51
|
network?: string;
|
|
49
52
|
/** Budget per endpoint call in USD. Default 0.05. */
|
|
50
53
|
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), the XRP Ledger (RLUSD) and
|
|
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.9.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";
|
|
@@ -44,15 +45,39 @@ export const XRPL = "xrpl:0";
|
|
|
44
45
|
export const XRPL_TESTNET = "xrpl:1";
|
|
45
46
|
export const STELLAR = "stellar:pubnet";
|
|
46
47
|
export const STELLAR_TESTNET = "stellar:testnet";
|
|
47
|
-
const
|
|
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 };
|
|
48
50
|
|
|
49
51
|
// On Algorand and the XRP Ledger only the dollar stablecoin is paid: USDC (an ASA) on Algorand, RLUSD from
|
|
50
52
|
// Ripple's issuer on the XRP Ledger. Never ALGO, XRP or another token, so the dollar budget always holds.
|
|
51
|
-
const ALGORAND_USDC = { [ALGORAND]: "31566704", [ALGORAND_TESTNET]: "10458941" };
|
|
53
|
+
const ALGORAND_USDC = { [algorandId(ALGORAND)]: "31566704", [algorandId(ALGORAND_TESTNET)]: "10458941" };
|
|
52
54
|
const RLUSD_HEX = "524C555344000000000000000000000000000000";
|
|
53
55
|
const RLUSD_ISSUER = { [XRPL]: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De", [XRPL_TESTNET]: "rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV" };
|
|
54
56
|
// On Stellar only Circle's USDC token contract (7 decimals), never XLM or another token named USDC.
|
|
55
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
|
+
};
|
|
64
|
+
|
|
65
|
+
// Algorand by its CAIP-2 id: the first 32 characters of the genesis hash. Sellers name it either way (the whole hash,
|
|
66
|
+
// or since @x402/avm 2.29.0 only the short id), so networks are compared by this.
|
|
67
|
+
function algorandId(network) {
|
|
68
|
+
return typeof network === "string" && network.startsWith("algorand:") ? network.slice(0, 41) : network;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// @x402/avm 2.29.0's ExactAvmScheme refuses the whole-hash form, which most Algorand sellers still use (2.28.0 took
|
|
72
|
+
// both). This hands the scheme the short id; the payment still names the seller's own network (x402 puts the
|
|
73
|
+
// seller's option in `accepted`), so either kind of seller can be paid. createSafeFetch wraps Algorand schemes in it.
|
|
74
|
+
export function anyAlgorandId(scheme) {
|
|
75
|
+
return Object.assign(Object.create(scheme), {
|
|
76
|
+
createPaymentPayload: (version, requirements, context) =>
|
|
77
|
+
scheme.createPaymentPayload(version, { ...requirements, network: algorandId(requirements.network) }, context),
|
|
78
|
+
...(typeof scheme.findDefaultAsset === "function" && { findDefaultAsset: (asset, network) => scheme.findDefaultAsset(asset, algorandId(network)) }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
56
81
|
|
|
57
82
|
// The option to pay on `network`, or null. RLUSD prices are decimal dollars ("0.001", and "2" is two dollars),
|
|
58
83
|
// which x402's spend controls would read as base units, so the budget is checked here in dollars.
|
|
@@ -77,17 +102,26 @@ const UNREAD_OFFER_MS = 10 * 60 * 1000;
|
|
|
77
102
|
|
|
78
103
|
export function payableOption(accepts, network, maxUsd) {
|
|
79
104
|
const cap = Number(String(maxUsd).replace(/^\$/, ""));
|
|
80
|
-
const on = (accepts ?? []).filter((a) => a?.network === network);
|
|
81
|
-
|
|
105
|
+
const on = (accepts ?? []).filter((a) => algorandId(a?.network) === algorandId(network));
|
|
106
|
+
const usdc = ALGORAND_USDC[algorandId(network)];
|
|
107
|
+
if (usdc) return on.find((a) => String(a.asset) === usdc) ?? null;
|
|
82
108
|
// x402's spend controls cap Stellar USDC too; the budget is checked here as well, in dollars.
|
|
83
109
|
if (STELLAR_USDC[network]) {
|
|
84
110
|
return on.find((a) => a.asset === STELLAR_USDC[network] && /^\d+$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) / 1e7 <= cap) ?? null;
|
|
85
111
|
}
|
|
112
|
+
if (XLAYER_DOLLARS[network]) {
|
|
113
|
+
return on.find((a) => XLAYER_DOLLARS[network].includes(String(a.asset ?? "").toLowerCase())
|
|
114
|
+
&& /^\d+$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) / 1e6 <= cap) ?? null;
|
|
115
|
+
}
|
|
86
116
|
if (RLUSD_ISSUER[network]) {
|
|
87
117
|
return on.find((a) => String(a.asset ?? "").toUpperCase() === RLUSD_HEX && a.extra?.issuer === RLUSD_ISSUER[network]
|
|
88
118
|
&& /^\d+(\.\d+)?$/.test(String(a.amount)) && Number(a.amount) > 0 && Number(a.amount) <= cap) ?? null;
|
|
89
119
|
}
|
|
90
|
-
|
|
120
|
+
// Base and other EVM chains: "exact" first (what most clients register), then "auth-capture" (Coinbase's escrow:
|
|
121
|
+
// the buyer signs once, the USDC is held and only captured when the seller delivers, e.g. Atom's domain checkout).
|
|
122
|
+
// Either is paid only if your register() added its scheme, and both stay within the budget (x402's spend controls
|
|
123
|
+
// read the amount, which for auth-capture is the most that can be captured).
|
|
124
|
+
return on.find((a) => a?.scheme === "exact") ?? on.find((a) => a?.scheme === "auth-capture") ?? on[0] ?? null;
|
|
91
125
|
}
|
|
92
126
|
|
|
93
127
|
export class SafePayError extends Error {
|
|
@@ -108,6 +142,13 @@ export function usdCap(maxUsd) {
|
|
|
108
142
|
return `$${Number(n.toFixed(6))}`;
|
|
109
143
|
}
|
|
110
144
|
|
|
145
|
+
// x402 spend controls for X Layer's dollars: each one allowed up to the budget in atomic units (6 decimals).
|
|
146
|
+
function xlayerAllowance(network, cap) {
|
|
147
|
+
if (!XLAYER_DOLLARS[network]) return {};
|
|
148
|
+
const atomic = String(Math.round(Number(String(cap).replace(/^\$/, "")) * 1e6));
|
|
149
|
+
return { allowedAssets: XLAYER_DOLLARS[network].map((asset) => ({ network, asset, maxAmountPerPayment: atomic })) };
|
|
150
|
+
}
|
|
151
|
+
|
|
111
152
|
export function diagnoseUrl(target, { method = "GET", doctorUrl = DOCTOR_URL } = {}) {
|
|
112
153
|
const q = new URLSearchParams({ url: target, method });
|
|
113
154
|
return `${doctorUrl.replace(/\/$/, "")}/api/v1/diagnose?${q}`;
|
|
@@ -209,10 +250,23 @@ export function createSafeFetch({
|
|
|
209
250
|
const makePayingFetch = createPayingFetch ?? ((cap) => {
|
|
210
251
|
const client = new x402Client((_version, accepts) => {
|
|
211
252
|
const pick = payableOption(accepts, payNetwork, cap);
|
|
212
|
-
if (!pick) throw new SafePayError(`no payment option on ${payNetwork}${ALGORAND_USDC[payNetwork] ? " in USDC" : STELLAR_USDC[payNetwork] ? ` in USDC within ${cap}` : RLUSD_ISSUER[payNetwork] ? ` in RLUSD within ${cap}` : ""}`, { code: "no_option" });
|
|
253
|
+
if (!pick) throw new SafePayError(`no payment option on ${payNetwork}${ALGORAND_USDC[algorandId(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" });
|
|
213
254
|
return pick;
|
|
214
|
-
}).setSpendControls({ maxAmountPerPayment: cap });
|
|
215
|
-
|
|
255
|
+
}).setSpendControls({ maxAmountPerPayment: cap, ...xlayerAllowance(payNetwork, cap) });
|
|
256
|
+
// Algorand schemes get anyAlgorandId, so sellers with either network id can be paid.
|
|
257
|
+
const registering = new Proxy(client, {
|
|
258
|
+
get(target, prop) {
|
|
259
|
+
if (prop === "register") {
|
|
260
|
+
return (network, scheme) => {
|
|
261
|
+
target.register(network, String(network).startsWith("algorand:") ? anyAlgorandId(scheme) : scheme);
|
|
262
|
+
return registering;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const value = Reflect.get(target, prop);
|
|
266
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
register(registering);
|
|
216
270
|
return wrapFetchWithPayment(baseFetch, client);
|
|
217
271
|
});
|
|
218
272
|
let preflightFetch;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-safe-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@algorandfoundation/algokit-utils": "^10.0.0-alpha.46",
|
|
32
|
-
"@x402/avm": "^2.
|
|
32
|
+
"@x402/avm": "^2.29.0",
|
|
33
33
|
"@x402/evm": "^2.27.0",
|
|
34
34
|
"@x402/fetch": "^2.27.0",
|
|
35
35
|
"@x402/stellar": "^2.28.0",
|