agent402-openclaw 0.5.8 → 0.5.9
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 +11 -4
- package/index.js +36 -16
- package/known-assets.js +135 -20
- package/openclaw.plugin.json +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,14 +76,21 @@ claude --model claude-sonnet-5
|
|
|
76
76
|
```bash
|
|
77
77
|
npm i @x402/fetch @x402/evm viem
|
|
78
78
|
export AGENT402_WALLET_KEY=0x... # an EVM key holding USDC on Base
|
|
79
|
-
export AGENT402_MAX_PER_CALL_USD=2 # per-call ceiling checked before signing (default 2, the gateway's metered cap
|
|
79
|
+
export AGENT402_MAX_PER_CALL_USD=2 # per-call ceiling checked before signing (default 2, the gateway's metered cap)
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
With no credits key present the proxy signs an x402 payment per call from that
|
|
83
83
|
wallet. The key never leaves the machine. The ceiling is checked on the payment
|
|
84
84
|
option that is signed, priced from the known decimals of its asset (USDC or
|
|
85
|
-
USDG on a known network)
|
|
86
|
-
ceiling is set.
|
|
85
|
+
USDG on a known network) and from the amount field that option's x402 version
|
|
86
|
+
signs; an option in any other asset is not signed while a ceiling is set. Once
|
|
87
|
+
the payment is built, the amount it actually signs is read back and checked
|
|
88
|
+
against the same price and ceiling before it is sent.
|
|
89
|
+
|
|
90
|
+
To run with no ceiling, set `maxPerCallUsd` to `0` or `"off"` together with
|
|
91
|
+
`allowUncappedPayments: true` (or `AGENT402_ALLOW_UNCAPPED_PAYMENTS=true`).
|
|
92
|
+
Without that acknowledgment `off` keeps the default ceiling and the proxy
|
|
93
|
+
logs why.
|
|
87
94
|
|
|
88
95
|
Two ways the wallet can pay a metered call:
|
|
89
96
|
|
|
@@ -136,7 +143,7 @@ MCP or HTTP, wallet-keyed memory, finished reports (dossiers, insider flow, 13F,
|
|
|
136
143
|
domain audits, token risk, deep research) and monitors, plus routing that buys
|
|
137
144
|
from external sellers and resells the result.
|
|
138
145
|
|
|
139
|
-
Why pay here, with every claim linked to its proof: https://agent402.tools
|
|
146
|
+
Why pay here, with every claim linked to its proof: https://agent402.tools/docs#why.
|
|
140
147
|
Usage is priced under a ceiling quoted before you pay, a failed call is not
|
|
141
148
|
charged and the receipt proves it, a keyed retry on a credits key is not
|
|
142
149
|
debited twice, and uptime and transactions are published from outside
|
package/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { buildProvider, setActiveProxy, setCachedRoutes, DEFAULT_PORT, PLUGIN_ID } from "./provider.js";
|
|
9
9
|
import { startProxy, loadRoutes, DEFAULT_UPSTREAM } from "./proxy.js";
|
|
10
10
|
import { stripTrailingSlashes } from "./models.js";
|
|
11
|
-
import { acceptUsd } from "./known-assets.js";
|
|
12
|
-
export { acceptUsd, KNOWN_ASSETS } from "./known-assets.js";
|
|
11
|
+
import { acceptUsd, guardSignedAmount } from "./known-assets.js";
|
|
12
|
+
export { acceptUsd, acceptAmount, signedPaymentProblem, KNOWN_ASSETS } from "./known-assets.js";
|
|
13
13
|
|
|
14
14
|
export const STATE_DIR = () => join(process.env.AGENT402_OPENCLAW_HOME || join(homedir(), ".openclaw"), "agent402");
|
|
15
15
|
export const CREDITS_KEY_FILE = () => join(STATE_DIR(), "credits.key");
|
|
@@ -56,29 +56,46 @@ export const PERMIT2_READY_MIN = 10n ** 12n; // 1,000,000 USDC (6 decimals): a r
|
|
|
56
56
|
* carries its own ceiling under its own name. The default is $2, the gateway's
|
|
57
57
|
* own metered quote cap (GATEWAY_METERED_MAX_QUOTE_USD), so no call the gateway
|
|
58
58
|
* would serve is refused here while a runaway quote still stops at the wallet.
|
|
59
|
-
* `maxPerCallUsd` in the plugin config or AGENT402_MAX_PER_CALL_USD;
|
|
60
|
-
*
|
|
59
|
+
* `maxPerCallUsd` in the plugin config or AGENT402_MAX_PER_CALL_USD; a
|
|
60
|
+
* malformed value reads as the default, never as off. `0`/`off` removes the
|
|
61
|
+
* ceiling only beside an explicit acknowledgment, `allowUncappedPayments:
|
|
62
|
+
* true` in the plugin config or AGENT402_ALLOW_UNCAPPED_PAYMENTS=true; without
|
|
63
|
+
* it `off` reads as the default too (uncappedRefused says so for the log). */
|
|
61
64
|
export const DEFAULT_MAX_PER_CALL_USD = 2;
|
|
65
|
+
const capOffRequested = (pluginConfig) => {
|
|
66
|
+
const raw = String(pluginConfig.maxPerCallUsd ?? process.env.AGENT402_MAX_PER_CALL_USD ?? "").trim().toLowerCase();
|
|
67
|
+
return raw === "0" || raw === "off";
|
|
68
|
+
};
|
|
69
|
+
export function uncappedAcknowledged(pluginConfig = {}) {
|
|
70
|
+
if (pluginConfig.allowUncappedPayments != null) return pluginConfig.allowUncappedPayments === true || String(pluginConfig.allowUncappedPayments).trim().toLowerCase() === "true";
|
|
71
|
+
return String(process.env.AGENT402_ALLOW_UNCAPPED_PAYMENTS ?? "").trim().toLowerCase() === "true";
|
|
72
|
+
}
|
|
73
|
+
/** True when the config asks for no ceiling without the acknowledgment, so
|
|
74
|
+
* the default ceiling applies instead. */
|
|
75
|
+
export const uncappedRefused = (pluginConfig = {}) => capOffRequested(pluginConfig) && !uncappedAcknowledged(pluginConfig);
|
|
62
76
|
export function maxPerCallUsd(pluginConfig = {}) {
|
|
77
|
+
if (capOffRequested(pluginConfig)) return uncappedAcknowledged(pluginConfig) ? Infinity : DEFAULT_MAX_PER_CALL_USD;
|
|
63
78
|
const raw = String(pluginConfig.maxPerCallUsd ?? process.env.AGENT402_MAX_PER_CALL_USD ?? "").trim().toLowerCase();
|
|
64
|
-
if (raw === "0" || raw === "off") return Infinity;
|
|
65
79
|
const n = Number(raw);
|
|
66
80
|
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_PER_CALL_USD;
|
|
67
81
|
}
|
|
68
82
|
// The dollar value of an accept's quote is acceptUsd (./known-assets.js):
|
|
69
|
-
// `amount`
|
|
70
|
-
// decimals of its asset, never a decimals
|
|
71
|
-
// signer signs the raw amount. An accept in
|
|
72
|
-
// with no whole-number amount,
|
|
73
|
-
//
|
|
74
|
-
|
|
83
|
+
// the field the challenge's version signs (`amount` in v2, `maxAmountRequired`
|
|
84
|
+
// in v1) in base units over the KNOWN decimals of its asset, never a decimals
|
|
85
|
+
// value the seller sends, since the signer signs the raw amount. An accept in
|
|
86
|
+
// an asset outside the table, with no whole-number amount, or with both fields
|
|
87
|
+
// at different values, reads as null, and null is REFUSED under a finite
|
|
88
|
+
// ceiling: a quote we cannot price is not one we sign. After the payload is
|
|
89
|
+
// built, guardSignedAmount checks the amount actually signed against the same
|
|
90
|
+
// figure and the ceiling before anything is sent.
|
|
91
|
+
export function selectAccept(accepts, { preferUpto = true, maxUsd = Infinity, x402Version = undefined } = {}) {
|
|
75
92
|
const list = Array.isArray(accepts) ? accepts : [];
|
|
76
93
|
const pick = (preferUpto && list.find((a) => a?.scheme === "upto" && a?.network === BASE_CAIP2))
|
|
77
94
|
|| list.find((a) => a?.scheme === "exact" && a?.network === BASE_CAIP2) || list[0];
|
|
78
95
|
if (pick && Number.isFinite(maxUsd)) {
|
|
79
|
-
const usd = acceptUsd(pick);
|
|
96
|
+
const usd = acceptUsd(pick, x402Version);
|
|
80
97
|
if (usd == null || usd > maxUsd) {
|
|
81
|
-
throw new Error(`agent402-openclaw: quote ${usd == null ? "unreadable" : `$${usd}`} exceeds the per-call ceiling $${maxUsd} (maxPerCallUsd / AGENT402_MAX_PER_CALL_USD; 0
|
|
98
|
+
throw new Error(`agent402-openclaw: quote ${usd == null ? "unreadable" : `$${usd}`} exceeds the per-call ceiling $${maxUsd} (maxPerCallUsd / AGENT402_MAX_PER_CALL_USD; 0 with allowUncappedPayments removes it)`);
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
return pick;
|
|
@@ -88,6 +105,8 @@ export const uptoReady = (allowance) => { try { return BigInt(allowance ?? 0) >=
|
|
|
88
105
|
export async function resolvePayFetch(pluginConfig = {}, log = () => {}) {
|
|
89
106
|
const pk = resolveWalletKey(pluginConfig);
|
|
90
107
|
if (!pk) return null;
|
|
108
|
+
if (uncappedRefused(pluginConfig)) log(`[agent402-openclaw] maxPerCallUsd "off" needs allowUncappedPayments: true (or AGENT402_ALLOW_UNCAPPED_PAYMENTS=true); keeping the $${DEFAULT_MAX_PER_CALL_USD} per-call ceiling`);
|
|
109
|
+
const maxUsd = maxPerCallUsd(pluginConfig);
|
|
91
110
|
try {
|
|
92
111
|
const [{ wrapFetchWithPayment, x402Client }, { privateKeyToAccount }, { toClientEvmSigner }, { registerExactEvmScheme }] = await Promise.all([
|
|
93
112
|
import("@x402/fetch"), import("viem/accounts"), import("@x402/evm"), import("@x402/evm/exact/client"),
|
|
@@ -101,9 +120,9 @@ export async function resolvePayFetch(pluginConfig = {}, log = () => {}) {
|
|
|
101
120
|
const pub = createPublicClient({ chain: base, transport: http((pluginConfig.baseRpc || process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim()) });
|
|
102
121
|
const allowance = await pub.readContract(getPermit2AllowanceReadParams({ tokenAddress: USDC_BASE, ownerAddress: account.address }));
|
|
103
122
|
upto = uptoReady(allowance);
|
|
104
|
-
const
|
|
105
|
-
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: upto, maxUsd }));
|
|
123
|
+
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: upto, maxUsd, x402Version: v }));
|
|
106
124
|
client.setSpendControls?.(false); // the vendor default ($1, its own default assets only) would refuse USDG and any quote over $1; selectAccept holds the accept that is signed to our own maxPerCallUsd instead
|
|
125
|
+
guardSignedAmount(client, { allowance: () => maxUsd });
|
|
107
126
|
registerExactEvmScheme(client, { signer });
|
|
108
127
|
if (upto) client.register(BASE_CAIP2, new UptoEvmScheme(signer));
|
|
109
128
|
log(upto
|
|
@@ -113,8 +132,9 @@ export async function resolvePayFetch(pluginConfig = {}, log = () => {}) {
|
|
|
113
132
|
} catch (e) {
|
|
114
133
|
// The allowance read or the upto scheme failed: exact still works.
|
|
115
134
|
log(`[agent402-openclaw] upto unavailable (${String(e?.message || e).slice(0, 120)}) - paying exact`);
|
|
116
|
-
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: false, maxUsd:
|
|
135
|
+
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: false, maxUsd, x402Version: v }));
|
|
117
136
|
client.setSpendControls?.(false); // the vendor default ($1, its own default assets only) would refuse USDG and any quote over $1; selectAccept holds the accept that is signed to our own maxPerCallUsd instead
|
|
137
|
+
guardSignedAmount(client, { allowance: () => maxUsd });
|
|
118
138
|
registerExactEvmScheme(client, { signer });
|
|
119
139
|
return wrapFetchWithPayment(fetch, client);
|
|
120
140
|
}
|
package/known-assets.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Known payment assets, and the USD value of an x402 accept priced from them.
|
|
2
2
|
//
|
|
3
|
-
// An x402 "exact" signer signs the accept's raw base-unit
|
|
4
|
-
// accept's `asset
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// An x402 "exact" signer signs the accept's raw base-unit amount for the
|
|
4
|
+
// accept's `asset`: `amount` under x402 v2, `maxAmountRequired` under v1. A
|
|
5
|
+
// spending cap therefore has to price the field the signer of that version
|
|
6
|
+
// reads, with the asset's real decimals, and a decimals hint the seller sends
|
|
7
|
+
// beside it (for example `extra.decimals`) is not evidence of anything. So the
|
|
8
|
+
// price of an accept comes from its challenge's version and this table alone:
|
|
9
|
+
// the stablecoins each network settles on the Agent402 rails, with their
|
|
10
|
+
// on-chain decimals. An accept whose (network, asset) pair is not in the table,
|
|
11
|
+
// or that carries both amount fields with different values, cannot be priced,
|
|
12
|
+
// and a buyer with a cap does not sign it. The table is keyed by CAIP-2 ids
|
|
13
|
+
// only: a bare v1 network name (for example "base") is not priced.
|
|
11
14
|
//
|
|
12
15
|
// The same file ships in agent402-client, agent402-mcp and agent402-openclaw;
|
|
13
16
|
// the parent repo's scripts/test-known-assets.js keeps the copies identical and
|
|
@@ -85,13 +88,33 @@ export function knownAsset(network, asset) {
|
|
|
85
88
|
return list.find((e) => sameAsset(e.asset, asset)) || null;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
/** The
|
|
89
|
-
* `maxAmountRequired`
|
|
90
|
-
*
|
|
91
|
-
|
|
91
|
+
/** The base-unit amount the signer of `x402Version` signs for one accept, as
|
|
92
|
+
* a string of digits: `maxAmountRequired` under v1, `amount` under v2. With
|
|
93
|
+
* no version, the one field present (or both, when they agree). null when
|
|
94
|
+
* the field that version signs is missing or not a whole number, when the
|
|
95
|
+
* version is neither 1 nor 2, and whenever the accept carries both fields
|
|
96
|
+
* with different values: such an accept means one price to one reader and
|
|
97
|
+
* another to the next, so it is not priced at all. */
|
|
98
|
+
export function acceptAmount(accept, x402Version = undefined) {
|
|
92
99
|
if (!accept || typeof accept !== "object") return null;
|
|
93
|
-
const
|
|
94
|
-
|
|
100
|
+
const has = (x) => x != null && x !== "";
|
|
101
|
+
const v2 = accept.amount, v1 = accept.maxAmountRequired;
|
|
102
|
+
if (has(v2) && has(v1) && String(v2) !== String(v1)) return null;
|
|
103
|
+
let raw;
|
|
104
|
+
if (x402Version === 1) raw = v1;
|
|
105
|
+
else if (x402Version === 2) raw = v2;
|
|
106
|
+
else if (x402Version == null) raw = has(v2) ? v2 : v1;
|
|
107
|
+
else return null;
|
|
108
|
+
if (!has(raw) || !/^\d+$/.test(String(raw))) return null;
|
|
109
|
+
return String(raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The USD value of one accept: the base-unit amount its version's signer
|
|
113
|
+
* signs (acceptAmount) over the KNOWN decimals of its asset. null when the
|
|
114
|
+
* asset is not known or the amount cannot be read. */
|
|
115
|
+
export function acceptUsd(accept, x402Version = undefined) {
|
|
116
|
+
const raw = acceptAmount(accept, x402Version);
|
|
117
|
+
if (raw == null) return null;
|
|
95
118
|
const entry = knownAsset(accept.network, accept.asset);
|
|
96
119
|
if (!entry) return null;
|
|
97
120
|
return Number(raw) / 10 ** entry.decimals;
|
|
@@ -99,11 +122,11 @@ export function acceptUsd(accept) {
|
|
|
99
122
|
|
|
100
123
|
/** The USD values of a 402's accepts: `max` and `min` over the accepts that can
|
|
101
124
|
* be priced, and `unknown`, the count that cannot. null when there are none. */
|
|
102
|
-
export function quoteOf(accepts) {
|
|
125
|
+
export function quoteOf(accepts, x402Version = undefined) {
|
|
103
126
|
if (!Array.isArray(accepts) || !accepts.length) return null;
|
|
104
127
|
let max = null, min = null, unknown = 0;
|
|
105
128
|
for (const a of accepts) {
|
|
106
|
-
const usd = acceptUsd(a);
|
|
129
|
+
const usd = acceptUsd(a, x402Version);
|
|
107
130
|
if (usd == null) { unknown++; continue; }
|
|
108
131
|
if (max == null || usd > max) max = usd;
|
|
109
132
|
if (min == null || usd < min) min = usd;
|
|
@@ -128,9 +151,9 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
128
151
|
const refuse = (detail) => { try { onRefuse?.(detail); } catch { /* the refusal stands */ } return detail; };
|
|
129
152
|
client.registerPolicy((version, accepts) => {
|
|
130
153
|
const cap = allowance();
|
|
131
|
-
const kept = accepts.filter((a) => { const usd = acceptUsd(a); return usd != null && usd <= cap; });
|
|
154
|
+
const kept = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
|
|
132
155
|
if (!kept.length) {
|
|
133
|
-
const q = quoteOf(accepts);
|
|
156
|
+
const q = quoteOf(accepts, version);
|
|
134
157
|
const d = refuse({ reason: q && q.min != null ? "over-allowance" : "unknown-asset", priceUsd: q ? q.min : null, allowanceUsd: cap });
|
|
135
158
|
throw new Error(d.reason === "unknown-asset"
|
|
136
159
|
? "spending guard: no payment option is in a known asset, so none can be priced; nothing was signed"
|
|
@@ -138,8 +161,8 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
138
161
|
}
|
|
139
162
|
return kept;
|
|
140
163
|
});
|
|
141
|
-
client.onBeforePaymentCreation(async ({ selectedRequirements: r }) => {
|
|
142
|
-
const usd = acceptUsd(r);
|
|
164
|
+
client.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements: r }) => {
|
|
165
|
+
const usd = acceptUsd(r, paymentRequired?.x402Version);
|
|
143
166
|
const cap = allowance();
|
|
144
167
|
if (usd == null) {
|
|
145
168
|
refuse({ reason: "unknown-asset", priceUsd: null, allowanceUsd: cap });
|
|
@@ -152,5 +175,97 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
|
|
|
152
175
|
onSign?.(usd, r);
|
|
153
176
|
return undefined;
|
|
154
177
|
});
|
|
178
|
+
guardSignedAmount(client, { allowance, onRefuse: refuse });
|
|
179
|
+
return client;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The token programs a Solana "exact" payment moves its asset through, and
|
|
183
|
+
// the TransferChecked instruction's tag.
|
|
184
|
+
const SOLANA_TOKEN_PROGRAMS = new Set(["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"]);
|
|
185
|
+
const TRANSFER_CHECKED = 12;
|
|
186
|
+
|
|
187
|
+
/** The amount a Solana payment transaction moves: its one token-program
|
|
188
|
+
* instruction, which must be a TransferChecked, read as base units and
|
|
189
|
+
* decimals. Decoded with @solana/kit, the peer every Solana x402 signer
|
|
190
|
+
* already loads. null when the transaction cannot be read or does not hold
|
|
191
|
+
* exactly that one transfer. */
|
|
192
|
+
async function solanaTransferOf(transaction) {
|
|
193
|
+
if (typeof transaction !== "string" || !transaction) return null;
|
|
194
|
+
try {
|
|
195
|
+
const kit = await import("@solana/kit");
|
|
196
|
+
const tx = kit.getTransactionDecoder().decode(kit.getBase64Encoder().encode(transaction));
|
|
197
|
+
const msg = kit.getCompiledTransactionMessageDecoder().decode(tx.messageBytes);
|
|
198
|
+
const keys = msg.staticAccounts || [];
|
|
199
|
+
const ixs = msg.version === 1
|
|
200
|
+
? (msg.instructionHeaders || []).map((h, i) => ({ program: keys[h.programAccountIndex], data: msg.instructionPayloads?.[i]?.instructionData }))
|
|
201
|
+
: (msg.instructions || []).map((ix) => ({ program: keys[ix.programAddressIndex], data: ix.data }));
|
|
202
|
+
const token = ixs.filter((ix) => SOLANA_TOKEN_PROGRAMS.has(String(ix.program)));
|
|
203
|
+
if (token.length !== 1) return null;
|
|
204
|
+
const d = token[0].data;
|
|
205
|
+
if (!d || d.length !== 10 || d[0] !== TRANSFER_CHECKED) return null;
|
|
206
|
+
let amount = 0n;
|
|
207
|
+
for (let i = 8; i >= 1; i--) amount = (amount << 8n) | BigInt(d[i]);
|
|
208
|
+
return { amount: amount.toString(), decimals: d[9] };
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The base-unit amount a created x402 payment payload commits the buyer to,
|
|
215
|
+
* read from what was signed: an EVM EIP-3009 `authorization.value`, an EVM
|
|
216
|
+
* Permit2 `permitted.amount`, or a Solana transaction's transfer. Returns
|
|
217
|
+
* `{ amount, decimals? }`, `{ unchecked: true }` for a network whose payload
|
|
218
|
+
* this reader does not decode (neither EVM nor Solana), or null when an EVM or
|
|
219
|
+
* Solana payload holds no amount it can read. */
|
|
220
|
+
export async function signedAmountOf(paymentPayload, network) {
|
|
221
|
+
const p = paymentPayload?.payload;
|
|
222
|
+
const net = String(network || "");
|
|
223
|
+
if (net.startsWith("eip155:")) {
|
|
224
|
+
const vals = [p?.authorization?.value, p?.permit2Authorization?.permitted?.amount].filter((x) => x != null);
|
|
225
|
+
if (vals.length !== 1 || !/^\d+$/.test(String(vals[0]))) return null;
|
|
226
|
+
return { amount: BigInt(String(vals[0])).toString() };
|
|
227
|
+
}
|
|
228
|
+
if (net.startsWith("solana:")) return solanaTransferOf(p?.transaction);
|
|
229
|
+
return { unchecked: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Check a created payment payload against the accept it was built for,
|
|
233
|
+
* before it is sent: the amount that was signed must be the amount the
|
|
234
|
+
* allowance was checked against (acceptAmount under the challenge's
|
|
235
|
+
* version), in the known asset's decimals, and within `cap`. Returns null
|
|
236
|
+
* when it holds, else why not. */
|
|
237
|
+
export async function signedPaymentProblem(paymentPayload, requirements, x402Version, cap = Infinity) {
|
|
238
|
+
const expected = acceptAmount(requirements, x402Version);
|
|
239
|
+
const entry = knownAsset(requirements?.network, requirements?.asset);
|
|
240
|
+
if (expected == null) return "the selected payment option's amount cannot be read";
|
|
241
|
+
const signed = await signedAmountOf(paymentPayload, requirements?.network);
|
|
242
|
+
if (signed == null) return "the signed payment's amount cannot be read";
|
|
243
|
+
if (signed.unchecked) return null;
|
|
244
|
+
if (signed.amount !== BigInt(expected).toString()) return `the signed amount (${signed.amount}) differs from the priced amount (${expected})`;
|
|
245
|
+
if (signed.decimals != null && entry && signed.decimals !== entry.decimals) return "the signed transfer's decimals differ from the known asset's";
|
|
246
|
+
if (Number.isFinite(cap)) {
|
|
247
|
+
if (!entry) return "the selected payment option is not in a known asset";
|
|
248
|
+
const usd = Number(signed.amount) / 10 ** entry.decimals;
|
|
249
|
+
if (usd > cap) return `the signed amount asks ${usd}, above the ${cap} this buyer may sign`;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Refuse to send a payment payload whose signed amount is not the one the
|
|
255
|
+
* allowance passed: registered after payload creation, so the check reads
|
|
256
|
+
* what was actually signed, and a throw here stops the payment-aware fetch
|
|
257
|
+
* before the payload leaves the process. guardSigning registers it; a
|
|
258
|
+
* client with its own selector (and no guardSigning) can register it alone. */
|
|
259
|
+
export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse = null } = {}) {
|
|
260
|
+
if (!client || typeof client.onAfterPaymentCreation !== "function") {
|
|
261
|
+
throw new TypeError("guardSignedAmount needs an @x402/core x402Client (onAfterPaymentCreation)");
|
|
262
|
+
}
|
|
263
|
+
client.onAfterPaymentCreation(async ({ paymentRequired, selectedRequirements: r, paymentPayload }) => {
|
|
264
|
+
const cap = allowance();
|
|
265
|
+
const problem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
|
|
266
|
+
if (!problem) return;
|
|
267
|
+
try { onRefuse?.({ reason: "signed-amount", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
|
|
268
|
+
throw new Error(`spending guard: ${problem}; the payment was not sent`);
|
|
269
|
+
});
|
|
155
270
|
return client;
|
|
156
271
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -42,7 +42,11 @@
|
|
|
42
42
|
"number",
|
|
43
43
|
"string"
|
|
44
44
|
],
|
|
45
|
-
"description": "Per-call ceiling in USD for wallet (x402) payments, checked before signing (default 2, the gateway's metered quote cap; 0 or \"off\"
|
|
45
|
+
"description": "Per-call ceiling in USD for wallet (x402) payments, checked before signing and again on the signed amount before sending (default 2, the gateway's metered quote cap; 0 or \"off\" removes it only with allowUncappedPayments: true). Also AGENT402_MAX_PER_CALL_USD."
|
|
46
|
+
},
|
|
47
|
+
"allowUncappedPayments": {
|
|
48
|
+
"type": "boolean",
|
|
49
|
+
"description": "Set true to let maxPerCallUsd 0 or \"off\" remove the per-call ceiling; without it the default ceiling stays. Also AGENT402_ALLOW_UNCAPPED_PAYMENTS=true."
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-openclaw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "OpenClaw model provider for Agent402: an auto-routed tier at a flat per-call price and explicit LLM models priced per request, paid in USDC over x402, or with a prepaid credits key, through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|