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 +17 -8
- package/index.js +38 -18
- package/known-assets.js +135 -20
- package/openclaw.plugin.json +5 -1
- package/package.json +4 -4
- package/proxy.js +6 -3
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:
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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;
|
|
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
|
}
|
|
@@ -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
|
|
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` (
|
|
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
|
|
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",
|
|
@@ -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
|
|
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
|
-
|
|
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":
|
|
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.
|