agent402-openclaw 0.5.7 → 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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Part of [Agent402](https://agent402.tools), the open-source paid door for AI agents.
4
4
 
5
- Agent402 as an [OpenClaw](https://openclaw.ai) model provider: the auto-routed
6
- tier at a **flat per-call price** and explicit frontier models **priced per
7
- request** from the body, paid in **USDC over x402** from a wallet or with a
5
+ Agent402 as an [OpenClaw](https://openclaw.ai) model provider: explicit models
6
+ **priced per request** from the body by default (metered), and the auto-routed
7
+ tier at a **flat per-call price**, paid in **USDC over x402** from a wallet or with a
8
8
  prepaid credits key (agent402.tools/credits says whether packs are on sale). The
9
9
  same key and gateway reach Agent402's 500+ pay-per-call tools; credits pay
10
10
  Agent402's own services, so the routes that buy from an outside seller
@@ -57,7 +57,9 @@ install from a packed archive and from the npm registry, doctor, setup, model
57
57
  listing, gateway boot and one agent turn (`test-real-install.js`).
58
58
 
59
59
  No OpenClaw? `agent402-openclaw proxy` runs the proxy alone; point any OpenAI
60
- client at `http://127.0.0.1:8412/v1` with model `auto`.
60
+ client at `http://127.0.0.1:8412/v1`. An explicit model rides the metered route
61
+ by default (each request quoted from its body, and settlement never exceeds the
62
+ quote); model `auto` takes the flat routed tier.
61
63
 
62
64
  **Claude Code and other Anthropic-wire clients** (0.5.0): the proxy also answers
63
65
  `POST /v1/messages` and forwards it to the gateway's metered Messages route,
@@ -74,14 +76,21 @@ claude --model claude-sonnet-5
74
76
  ```bash
75
77
  npm i @x402/fetch @x402/evm viem
76
78
  export AGENT402_WALLET_KEY=0x... # an EVM key holding USDC on Base
77
- export AGENT402_MAX_PER_CALL_USD=2 # per-call ceiling checked before signing (default 2, the gateway's metered cap; 0 disables)
79
+ export AGENT402_MAX_PER_CALL_USD=2 # per-call ceiling checked before signing (default 2, the gateway's metered cap)
78
80
  ```
79
81
 
80
82
  With no credits key present the proxy signs an x402 payment per call from that
81
83
  wallet. The key never leaves the machine. The ceiling is checked on the payment
82
84
  option that is signed, priced from the known decimals of its asset (USDC or
83
- USDG on a known network); an option in any other asset is not signed while a
84
- 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.
85
94
 
86
95
  Two ways the wallet can pay a metered call:
87
96
 
@@ -134,7 +143,7 @@ MCP or HTTP, wallet-keyed memory, finished reports (dossiers, insider flow, 13F,
134
143
  domain audits, token risk, deep research) and monitors, plus routing that buys
135
144
  from external sellers and resells the result.
136
145
 
137
- Why pay here, with every claim linked to its proof: https://agent402.tools/#why.
146
+ Why pay here, with every claim linked to its proof: https://agent402.tools/docs#why.
138
147
  Usage is priced under a ceiling quoted before you pay, a failed call is not
139
148
  charged and the receipt proves it, a keyed retry on a credits key is not
140
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; `0`/`off`
60
- * disables; a malformed value reads as the default, never as off. */
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` (v2) or `maxAmountRequired` (v1) in base units over the KNOWN
70
- // decimals of its asset, never a decimals value the seller sends, since the
71
- // signer signs the raw amount. An accept in an asset outside the table, or
72
- // with no whole-number amount, reads as null, and null is REFUSED under a
73
- // finite ceiling: a quote we cannot price is not one we sign.
74
- export function selectAccept(accepts, { preferUpto = true, maxUsd = Infinity } = {}) {
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 disables)`);
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 maxUsd = maxPerCallUsd(pluginConfig);
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: maxPerCallUsd(pluginConfig) }));
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
  }
@@ -130,7 +150,7 @@ let registeredOnce = false;
130
150
  const plugin = {
131
151
  id: PLUGIN_ID,
132
152
  name: "Agent402",
133
- description: "Agent402 model provider: routed + explicit models at a flat per-call price, paid by card or USDC.",
153
+ description: "Agent402 model provider: routed + explicit models at a flat per-call price, paid in USDC from a wallet, or with a credits key already issued.",
134
154
  register(api) {
135
155
  const cfg = api.pluginConfig || {};
136
156
  const port = Number(cfg.port) || DEFAULT_PORT;
@@ -149,7 +169,7 @@ const plugin = {
149
169
  try { setCachedRoutes(await loadRoutes(upstream, fetch, { pricing })); } catch (e) { log(`[agent402-openclaw] could not read ${upstream}/v1/models yet: ${e?.message || e}`); }
150
170
  started = await startProxy({ upstream, creditsKey, payFetch, port, pricing, log });
151
171
  setActiveProxy(started);
152
- if (started.mode === "unpaid") log("[agent402-openclaw] no payment configured: run `agent402-openclaw setup` (card credits key) - paid calls will answer 402 until then");
172
+ if (started.mode === "unpaid") log("[agent402-openclaw] no payment configured: run `agent402-openclaw setup` (a wallet key, or a credits key already issued) - paid calls will answer 402 until then");
153
173
  },
154
174
  stop: async () => { if (started) { await started.close(); started = null; setActiveProxy(null); } },
155
175
  });
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 `amount` for the
4
- // accept's `asset`. A spending cap therefore has to price that amount with the
5
- // asset's real decimals, and a decimals hint the seller sends beside it (for
6
- // example `extra.decimals`) is not evidence of anything. So the price of an
7
- // accept comes from this table alone: the stablecoins each network settles on
8
- // the Agent402 rails, with their on-chain decimals. An accept whose
9
- // (network, asset) pair is not in the table cannot be priced, and a buyer with
10
- // a cap does not sign it.
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 USD value of one accept: its base-unit amount (`amount` in x402 v2,
89
- * `maxAmountRequired` in v1) over the KNOWN decimals of its asset. null when
90
- * the asset is not known or the amount is not a whole number of base units. */
91
- export function acceptUsd(accept) {
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 raw = accept.amount ?? accept.maxAmountRequired;
94
- if (raw == null || !/^\d+$/.test(String(raw))) return null;
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
  }
@@ -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\" disables). Also AGENT402_MAX_PER_CALL_USD."
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.7",
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",
@@ -44,9 +44,9 @@
44
44
  "test": "node test.js"
45
45
  },
46
46
  "peerDependencies": {
47
- "@x402/evm": ">=2.20.0",
48
- "@x402/fetch": ">=2.20.0",
49
- "viem": ">=2.48.11"
47
+ "@x402/evm": ">=2.20.0 <3",
48
+ "@x402/fetch": ">=2.20.0 <3",
49
+ "viem": ">=2.48.11 <3"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@x402/evm": {
package/proxy.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // SDK pay from a wallet too (ANTHROPIC_BASE_URL=http://127.0.0.1:<port>).
7
7
  //
8
8
  // Two ways to pay, chosen at start:
9
- // creditsKey - a prepaid card-credits key (a402_...) sent as a Bearer; the
9
+ // creditsKey - a prepaid credits key (a402_...) sent as a Bearer; the
10
10
  // gateway authorizes against the balance before the handler and
11
11
  // debits only on a final 200. No wallet, no chain.
12
12
  // payFetch - an x402-paying fetch (e.g. @x402/fetch wrapped around a
@@ -45,6 +45,8 @@ import { AUTO_ID, routesFromCatalog, stripTrailingSlashes } from "./models.js";
45
45
 
46
46
  export const DEFAULT_UPSTREAM = "https://agent402.tools";
47
47
  export const PKG_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
48
+ // The User-Agent product token that names this package on every request.
49
+ const USER_AGENT = `agent402-openclaw/${PKG_VERSION}`;
48
50
  const MAX_BODY = 2 * 1024 * 1024;
49
51
 
50
52
  // The model catalog read (GET /v1/models) settles nothing, so a 503 the
@@ -78,7 +80,8 @@ async function discoveryFetch(fetchImpl, label, url, init) {
78
80
  let attempts = 0;
79
81
  for (;;) {
80
82
  attempts++;
81
- r = await fetchImpl(url, typeof init === "function" ? init() : init);
83
+ const i = (typeof init === "function" ? init() : init) || {};
84
+ r = await fetchImpl(url, { ...i, headers: { "User-Agent": USER_AGENT, ...(i.headers || {}) } });
82
85
  if (r.ok) return r;
83
86
  if (attempts >= DISCOVERY_ATTEMPTS) break;
84
87
  const wait = await shedRetryWaitMs(r);
@@ -177,7 +180,7 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
177
180
  // error shape (wire.mapRefusal); every other upstream body is relayed as sent.
178
181
  async function forward(req, res, endpoint, outbound, extraHeaders, wire) {
179
182
  const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
180
- const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}`, ...extraHeaders };
183
+ const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": USER_AGENT, ...extraHeaders };
181
184
  if (key) headers.authorization = `Bearer ${key}`;
182
185
  // A paid request never follows a redirect: followed, it would pay another
183
186
  // origin's 402 or carry a signature (or the credits key) to another host.