agent402-openclaw 0.5.9 → 0.5.11

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
@@ -87,6 +87,13 @@ signs; an option in any other asset is not signed while a ceiling is set. Once
87
87
  the payment is built, the amount it actually signs is read back and checked
88
88
  against the same price and ceiling before it is sent.
89
89
 
90
+ A signed payment also has a validity ceiling of 24 hours: a payment option
91
+ that asks to stay redeemable for longer (its `maxTimeoutSeconds`) is not
92
+ signed, and a built payment whose expiry (`validBefore` or the Permit2
93
+ `deadline`) lies more than 24 hours out is refused before it is sent. Any
94
+ refusal happens before the payment leaves the machine, and the call's error
95
+ names the reason.
96
+
90
97
  To run with no ceiling, set `maxPerCallUsd` to `0` or `"off"` together with
91
98
  `allowUncappedPayments: true` (or `AGENT402_ALLOW_UNCAPPED_PAYMENTS=true`).
92
99
  Without that acknowledgment `off` keeps the default ceiling and the proxy
@@ -125,6 +132,11 @@ The proxy answers native clients on loopback only: requests carrying a browser
125
132
  `Origin` header or a non-loopback `Host` are refused, so a web page cannot spend
126
133
  the key.
127
134
 
135
+ The credits key stays out of what the proxy prints: an upstream or network
136
+ error is logged and answered (the 502 body) with the key cut out as
137
+ `[redacted]`, and `agent402-openclaw doctor` reports the key as `present` or
138
+ `none`, never any part of it.
139
+
128
140
  ## Commands
129
141
 
130
142
  - `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--no-wallet] [--write] [--port N] [--flat]`
package/cli.js CHANGED
@@ -187,7 +187,7 @@ async function main() {
187
187
  const key = resolveCreditsKey();
188
188
  const walletPk = resolveWalletKey();
189
189
  out(`upstream: ${upstream}`);
190
- out(`credits key: ${key ? `present (${key.slice(0, 8)}…)` : "none"}`);
190
+ out(`credits key: ${key ? "present" : "none"}`);
191
191
  out(`wallet key: ${walletPk ? "present" : "none"}`);
192
192
  if (walletPk) {
193
193
  try {
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, guardSignedAmount } from "./known-assets.js";
12
- export { acceptUsd, acceptAmount, signedPaymentProblem, KNOWN_ASSETS } from "./known-assets.js";
11
+ import { acceptUsd, guardSignedAmount, acceptValidityTooLong, MAX_VALIDITY_SECONDS } from "./known-assets.js";
12
+ export { acceptUsd, acceptAmount, signedPaymentProblem, signedValidityProblem, KNOWN_ASSETS, MAX_VALIDITY_SECONDS } 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");
@@ -87,9 +87,13 @@ export function maxPerCallUsd(pluginConfig = {}) {
87
87
  // at different values, reads as null, and null is REFUSED under a finite
88
88
  // ceiling: a quote we cannot price is not one we sign. After the payload is
89
89
  // built, guardSignedAmount checks the amount actually signed against the same
90
- // figure and the ceiling before anything is sent.
90
+ // figure and the ceiling before anything is sent. An accept that asks to stay
91
+ // valid past MAX_VALIDITY_SECONDS (a day) is never a candidate, ceiling or no
92
+ // ceiling: the signed authorization would be a bearer claim for that long.
91
93
  export function selectAccept(accepts, { preferUpto = true, maxUsd = Infinity, x402Version = undefined } = {}) {
92
- const list = Array.isArray(accepts) ? accepts : [];
94
+ const offered = Array.isArray(accepts) ? accepts : [];
95
+ const list = offered.filter((a) => !acceptValidityTooLong(a));
96
+ if (offered.length && !list.length) throw new Error(`agent402-openclaw: every payment option asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds; nothing was signed`);
93
97
  const pick = (preferUpto && list.find((a) => a?.scheme === "upto" && a?.network === BASE_CAIP2))
94
98
  || list.find((a) => a?.scheme === "exact" && a?.network === BASE_CAIP2) || list[0];
95
99
  if (pick && Number.isFinite(maxUsd)) {
package/known-assets.js CHANGED
@@ -12,6 +12,12 @@
12
12
  // and a buyer with a cap does not sign it. The table is keyed by CAIP-2 ids
13
13
  // only: a bare v1 network name (for example "base") is not priced.
14
14
  //
15
+ // An entry may also name its token's EIP-712 domain (`name` and `version`,
16
+ // read from the token's own name() and version()). An EVM "exact" signer
17
+ // signs under the domain the accept's `extra` carries, so for such an entry
18
+ // an accept whose `extra.name` or `extra.version` is not exactly the token's
19
+ // cannot be priced either, and a buyer with a cap does not sign it.
20
+ //
15
21
  // The same file ships in agent402-client, agent402-mcp and agent402-openclaw;
16
22
  // the parent repo's scripts/test-known-assets.js keeps the copies identical and
17
23
  // checks the table against the assets the server offers.
@@ -29,6 +35,7 @@ export const KNOWN_ASSETS = Object.freeze({
29
35
  "eip155:1329": [{ asset: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392", decimals: 6, symbol: "USDC" }], // Sei
30
36
  "eip155:10": [{ asset: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", decimals: 6, symbol: "USDC" }], // Optimism
31
37
  "eip155:5042": [{ asset: "0x3600000000000000000000000000000000000000", decimals: 6, symbol: "USDC" }], // Arc
38
+ "eip155:57073": [{ asset: "0x2D270e6886d130D724215A266106e6832161EAEd", decimals: 6, symbol: "USDC", name: "USDC", version: "2" }], // Ink
32
39
  "eip155:4663": [{ asset: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", decimals: 6, symbol: "USDG" }], // Robinhood Chain
33
40
  // Solana: the USDC mint.
34
41
  "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": [{ asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6, symbol: "USDC" }],
@@ -56,6 +63,7 @@ export const NETWORK_NAMES = Object.freeze({
56
63
  sei: ["eip155:1329"],
57
64
  optimism: ["eip155:10"],
58
65
  arc: ["eip155:5042"],
66
+ ink: ["eip155:57073"],
59
67
  robinhood: ["eip155:4663"],
60
68
  solana: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
61
69
  "solana-devnet": ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
@@ -88,6 +96,15 @@ export function knownAsset(network, asset) {
88
96
  return list.find((e) => sameAsset(e.asset, asset)) || null;
89
97
  }
90
98
 
99
+ /** True when the accept carries the EIP-712 domain the table names for its
100
+ * asset (`extra.name` and `extra.version`, exactly), or when the entry names
101
+ * none. */
102
+ export function acceptDomainMatches(entry, accept) {
103
+ if (!entry || (entry.name == null && entry.version == null)) return true;
104
+ const extra = accept && typeof accept.extra === "object" && accept.extra ? accept.extra : {};
105
+ return extra.name === entry.name && extra.version === entry.version;
106
+ }
107
+
91
108
  /** The base-unit amount the signer of `x402Version` signs for one accept, as
92
109
  * a string of digits: `maxAmountRequired` under v1, `amount` under v2. With
93
110
  * no version, the one field present (or both, when they agree). null when
@@ -111,12 +128,13 @@ export function acceptAmount(accept, x402Version = undefined) {
111
128
 
112
129
  /** The USD value of one accept: the base-unit amount its version's signer
113
130
  * signs (acceptAmount) over the KNOWN decimals of its asset. null when the
114
- * asset is not known or the amount cannot be read. */
131
+ * asset is not known, when the accept names a different EIP-712 domain than
132
+ * the table's for that asset, or when the amount cannot be read. */
115
133
  export function acceptUsd(accept, x402Version = undefined) {
116
134
  const raw = acceptAmount(accept, x402Version);
117
135
  if (raw == null) return null;
118
136
  const entry = knownAsset(accept.network, accept.asset);
119
- if (!entry) return null;
137
+ if (!entry || !acceptDomainMatches(entry, accept)) return null;
120
138
  return Number(raw) / 10 ** entry.decimals;
121
139
  }
122
140
 
@@ -134,6 +152,51 @@ export function quoteOf(accepts, x402Version = undefined) {
134
152
  return { max, min, unknown, total: accepts.length };
135
153
  }
136
154
 
155
+ // The longest a signed payment may stay redeemable. An EVM "exact" signer
156
+ // sets validBefore (EIP-3009) or deadline (Permit2) to the moment of signing
157
+ // plus the accept's maxTimeoutSeconds, and the signed payload is a bearer
158
+ // claim until then, so a seller that quotes years gets an authorization it
159
+ // could redeem for years and a held amount that never stops counting. Honest
160
+ // accepts ask for minutes (Agent402's own ask 300 s); a day admits every
161
+ // one of them. An accept that asks for longer is dropped before selection,
162
+ // and a payload signed past it is refused before it is sent.
163
+ export const MAX_VALIDITY_SECONDS = 24 * 3600;
164
+ // Signing reads the clock a moment before the check after it does.
165
+ const VALIDITY_CLOCK_SLACK_SECONDS = 60;
166
+
167
+ /** True when an accept asks for a validity window past MAX_VALIDITY_SECONDS.
168
+ * An accept with no readable maxTimeoutSeconds is left to the check of what
169
+ * was signed (signedValidityProblem). */
170
+ export function acceptValidityTooLong(accept, maxSeconds = MAX_VALIDITY_SECONDS) {
171
+ const raw = accept?.maxTimeoutSeconds;
172
+ if (raw == null || raw === "") return false;
173
+ const n = Number(raw);
174
+ return !Number.isFinite(n) || n > maxSeconds;
175
+ }
176
+
177
+ /** The expiry a signed payload carries, in unix seconds: an EIP-3009
178
+ * `authorization.validBefore` or a Permit2 `deadline`. null when the payload
179
+ * names none (a Solana transaction, an unknown scheme). NaN when it names
180
+ * one that is not a whole number of seconds. */
181
+ export function signedExpirySeconds(paymentPayload) {
182
+ const p = paymentPayload?.payload;
183
+ const v = p?.authorization?.validBefore ?? p?.permit2Authorization?.deadline;
184
+ if (v == null) return null;
185
+ return /^\d{1,15}$/.test(String(v)) ? Number(v) : NaN;
186
+ }
187
+
188
+ /** Why a created payment payload stays redeemable for too long, or null: its
189
+ * expiry is past `nowMs` plus `maxSeconds` (and a minute of clock slack), or
190
+ * it names an expiry that cannot be read. */
191
+ export function signedValidityProblem(paymentPayload, { maxSeconds = MAX_VALIDITY_SECONDS, nowMs = Date.now() } = {}) {
192
+ const exp = signedExpirySeconds(paymentPayload);
193
+ if (exp == null) return null;
194
+ if (!Number.isFinite(exp)) return "the signed authorization's expiry cannot be read";
195
+ const left = exp - Math.floor(nowMs / 1000);
196
+ if (left > maxSeconds + VALIDITY_CLOCK_SLACK_SECONDS) return `the signed authorization stays valid for ${left} seconds, past the ${maxSeconds}-second ceiling`;
197
+ return null;
198
+ }
199
+
137
200
  /** Enforce a USD allowance at signing on an @x402/core x402Client.
138
201
  *
139
202
  * `allowance()` is the most this buyer may sign for the payment being built
@@ -141,8 +204,10 @@ export function quoteOf(accepts, x402Version = undefined) {
141
204
  * for none). A registered policy drops every accept that cannot be priced or
142
205
  * is over the allowance, so the client selects among the ones that fit, and a
143
206
  * hook before payload creation checks the selected accept again and passes
144
- * its USD value to `onSign`. A refusal throws (from the policy) or aborts
145
- * (from the hook) before any signature exists; `onRefuse` hears why first. */
207
+ * its USD value to `onSign`. An accept asking for a validity window past
208
+ * MAX_VALIDITY_SECONDS is dropped and refused the same way. A refusal throws
209
+ * (from the policy) or aborts (from the hook) before any signature exists;
210
+ * `onRefuse` hears why first. */
146
211
  export function guardSigning(client, { allowance, onSign = null, onRefuse = null }) {
147
212
  if (!client || typeof client.registerPolicy !== "function" || typeof client.onBeforePaymentCreation !== "function") {
148
213
  throw new TypeError("guardSigning needs an @x402/core x402Client (registerPolicy and onBeforePaymentCreation)");
@@ -151,7 +216,12 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
151
216
  const refuse = (detail) => { try { onRefuse?.(detail); } catch { /* the refusal stands */ } return detail; };
152
217
  client.registerPolicy((version, accepts) => {
153
218
  const cap = allowance();
154
- const kept = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
219
+ const priced = accepts.filter((a) => { const usd = acceptUsd(a, version); return usd != null && usd <= cap; });
220
+ const kept = priced.filter((a) => !acceptValidityTooLong(a));
221
+ if (!kept.length && priced.length) {
222
+ const d = refuse({ reason: "validity", priceUsd: null, allowanceUsd: cap, detail: `every payment option it may sign asks to stay valid for more than ${MAX_VALIDITY_SECONDS} seconds` });
223
+ throw new Error(`spending guard: ${d.detail}; nothing was signed`);
224
+ }
155
225
  if (!kept.length) {
156
226
  const q = quoteOf(accepts, version);
157
227
  const d = refuse({ reason: q && q.min != null ? "over-allowance" : "unknown-asset", priceUsd: q ? q.min : null, allowanceUsd: cap });
@@ -172,6 +242,10 @@ export function guardSigning(client, { allowance, onSign = null, onRefuse = null
172
242
  refuse({ reason: "over-allowance", priceUsd: usd, allowanceUsd: cap });
173
243
  return { abort: true, reason: `spending guard: the selected payment option asks $${usd}, above the $${cap} this buyer may sign; nothing was signed` };
174
244
  }
245
+ if (acceptValidityTooLong(r)) {
246
+ const d = refuse({ reason: "validity", priceUsd: usd, allowanceUsd: cap, detail: `the selected payment option asks to stay valid for ${r.maxTimeoutSeconds} seconds, past the ${MAX_VALIDITY_SECONDS}-second ceiling` });
247
+ return { abort: true, reason: `spending guard: ${d.detail}; nothing was signed` };
248
+ }
175
249
  onSign?.(usd, r);
176
250
  return undefined;
177
251
  });
@@ -252,9 +326,10 @@ export async function signedPaymentProblem(paymentPayload, requirements, x402Ver
252
326
  }
253
327
 
254
328
  /** 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
329
+ * allowance passed, or whose expiry is past MAX_VALIDITY_SECONDS from now
330
+ * (signedValidityProblem): registered after payload creation, so the check
331
+ * reads what was actually signed, and a throw here stops the payment-aware
332
+ * fetch before the payload leaves the process. guardSigning registers it; a
258
333
  * client with its own selector (and no guardSigning) can register it alone. */
259
334
  export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse = null } = {}) {
260
335
  if (!client || typeof client.onAfterPaymentCreation !== "function") {
@@ -262,9 +337,10 @@ export function guardSignedAmount(client, { allowance = () => Infinity, onRefuse
262
337
  }
263
338
  client.onAfterPaymentCreation(async ({ paymentRequired, selectedRequirements: r, paymentPayload }) => {
264
339
  const cap = allowance();
265
- const problem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
340
+ const amountProblem = await signedPaymentProblem(paymentPayload, r, paymentRequired?.x402Version, cap);
341
+ const problem = amountProblem || signedValidityProblem(paymentPayload);
266
342
  if (!problem) return;
267
- try { onRefuse?.({ reason: "signed-amount", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
343
+ try { onRefuse?.({ reason: amountProblem ? "signed-amount" : "signed-validity", priceUsd: null, allowanceUsd: cap, detail: problem }); } catch { /* the refusal stands */ }
268
344
  throw new Error(`spending guard: ${problem}; the payment was not sent`);
269
345
  });
270
346
  return client;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-openclaw",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
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",
package/proxy.js CHANGED
@@ -270,9 +270,12 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
270
270
  return json(res, 404, { error: { message: `No route for ${req.method} ${url.pathname}`, type: "invalid_request_error" } });
271
271
  } catch (e) {
272
272
  stats.errors++;
273
- log(`[agent402-openclaw] ${req.method} ${url.pathname}: ${e?.message || e}`);
273
+ // A thrown message is the payment fetch's or the network's own text,
274
+ // so the credits key is cut out of it before it is logged or answered.
275
+ const msg = key ? String(e?.message || e).split(key).join("[redacted]") : String(e?.message || e);
276
+ log(`[agent402-openclaw] ${req.method} ${url.pathname}: ${msg}`);
274
277
  const wire = url.pathname === "/v1/messages" ? ANTHROPIC_WIRE : OPENAI_WIRE;
275
- if (!res.headersSent) return json(res, 502, wire.error("upstream_error", `Upstream error: ${String(e?.message || e).slice(0, 200)}`));
278
+ if (!res.headersSent) return json(res, 502, wire.error("upstream_error", `Upstream error: ${msg.slice(0, 200)}`));
276
279
  try { res.end(); } catch { /* ignore */ }
277
280
  }
278
281
  });