agent402-openclaw 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/cli.js +8 -5
- package/index.js +3 -2
- package/models.js +17 -6
- package/package.json +1 -1
- package/proxy.js +11 -9
package/README.md
CHANGED
|
@@ -33,11 +33,16 @@ export AGENT402_WALLET_KEY=0x... # an EVM key holding USDC on Base
|
|
|
33
33
|
With no credits key present the proxy signs an x402 payment per call from that
|
|
34
34
|
wallet. The key never leaves the machine.
|
|
35
35
|
|
|
36
|
-
## Models
|
|
37
|
-
|
|
38
|
-
`auto` (routed per prompt) plus every id from
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
## Models and pricing
|
|
37
|
+
|
|
38
|
+
`auto` (routed per prompt, flat $0.01/call) plus every id from
|
|
39
|
+
`GET https://agent402.tools/v1/models`. Explicit models are **metered by default**:
|
|
40
|
+
the proxy sends them to the gateway's metered route, where each request is quoted
|
|
41
|
+
from its body (exact-BPE input plus your `max_tokens` at the model's list price,
|
|
42
|
+
times 1.15, from $0.001, capped at $2 per call), so a short call costs a fraction
|
|
43
|
+
of a cent and a long one pays for what it asks. `--flat` (or
|
|
44
|
+
`AGENT402_PRICING=flat`) keeps every model on its flat per-call tier instead.
|
|
45
|
+
Either way OpenClaw's per-token cost fields stay zero; the price is per call. A model sent to the wrong tier is answered with a
|
|
41
46
|
400 naming the right one; nothing is charged. A client-supplied `Idempotency-Key`
|
|
42
47
|
is passed through (an x402 retry with the same key replays the paid answer);
|
|
43
48
|
without one, each call is its own payment.
|
package/cli.js
CHANGED
|
@@ -37,11 +37,14 @@ function mergeInto(target, block) {
|
|
|
37
37
|
|
|
38
38
|
async function main() {
|
|
39
39
|
if (cmd === "help" || has("--help")) {
|
|
40
|
-
out("agent402-openclaw setup [--credits-key a402_
|
|
40
|
+
out("agent402-openclaw setup [--credits-key a402_...|-] [--write] [--flat] | proxy [--port N] [--upstream URL] [--flat] | doctor");
|
|
41
41
|
return 0;
|
|
42
42
|
}
|
|
43
43
|
const upstream = stripTrailingSlashes(opt("--upstream") || process.env.AGENT402_UPSTREAM || DEFAULT_UPSTREAM);
|
|
44
44
|
const port = Number(opt("--port")) || DEFAULT_PORT;
|
|
45
|
+
// Pricing: metered by default (explicit models pay a per-request quote, from
|
|
46
|
+
// $0.001); --flat (or AGENT402_PRICING=flat) keeps every model on its flat tier.
|
|
47
|
+
const pricing = has("--flat") || String(process.env.AGENT402_PRICING || "").toLowerCase() === "flat" ? "flat" : "metered";
|
|
45
48
|
|
|
46
49
|
if (cmd === "setup") {
|
|
47
50
|
// The key may arrive on argv (lands in shell history), via AGENT402_CREDITS_KEY,
|
|
@@ -59,7 +62,7 @@ async function main() {
|
|
|
59
62
|
out(`no credits key yet: buy a pack by card at ${upstream}/credits, then rerun with --credits-key a402_...`);
|
|
60
63
|
}
|
|
61
64
|
let routes;
|
|
62
|
-
try { routes = await loadRoutes(upstream); } catch (e) { console.error(`could not read ${upstream}/v1/models: ${e?.message || e}`); return 2; }
|
|
65
|
+
try { routes = await loadRoutes(upstream, fetch, { pricing }); } catch (e) { console.error(`could not read ${upstream}/v1/models: ${e?.message || e}`); return 2; }
|
|
63
66
|
const block = configBlock({ port, routes });
|
|
64
67
|
const cfgPath = join(process.env.AGENT402_OPENCLAW_HOME || join(homedir(), ".openclaw"), "openclaw.json");
|
|
65
68
|
if (has("--write")) {
|
|
@@ -88,8 +91,8 @@ async function main() {
|
|
|
88
91
|
if (cmd === "proxy") {
|
|
89
92
|
const creditsKey = resolveCreditsKey();
|
|
90
93
|
const payFetch = creditsKey ? null : await resolvePayFetch({}, (m) => console.error(m));
|
|
91
|
-
const p = await startProxy({ upstream, creditsKey, payFetch, port, log: (m) => console.error(m) });
|
|
92
|
-
out(`${p.baseUrl}/v1 (${p.mode})`);
|
|
94
|
+
const p = await startProxy({ upstream, creditsKey, payFetch, port, pricing, log: (m) => console.error(m) });
|
|
95
|
+
out(`${p.baseUrl}/v1 (${p.mode}, ${p.pricing} pricing)`);
|
|
93
96
|
await new Promise(() => {}); // run until killed
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -98,7 +101,7 @@ async function main() {
|
|
|
98
101
|
out(`upstream: ${upstream}`);
|
|
99
102
|
out(`credits key: ${key ? `present (${key.slice(0, 8)}…)` : "none"}`);
|
|
100
103
|
out(`wallet key: ${/^0x[0-9a-fA-F]{64}$/.test(process.env.AGENT402_WALLET_KEY || "") ? "present" : "none"}`);
|
|
101
|
-
try { const r = await loadRoutes(upstream); out(`gateway: ok, ${r.size} models`); } catch (e) { out(`gateway: unreachable (${e?.message || e})`); return 1; }
|
|
104
|
+
try { const r = await loadRoutes(upstream, fetch, { pricing }); out(`gateway: ok, ${r.size} models (${pricing} pricing: ${[...r.values()].filter((x) => x.metered).length} metered)`); } catch (e) { out(`gateway: unreachable (${e?.message || e})`); return 1; }
|
|
102
105
|
if (key) {
|
|
103
106
|
try {
|
|
104
107
|
const r = await fetch(`${upstream}/api/credits/balance`, { headers: { authorization: `Bearer ${key}` }, signal: AbortSignal.timeout(15_000) });
|
package/index.js
CHANGED
|
@@ -51,6 +51,7 @@ const plugin = {
|
|
|
51
51
|
const cfg = api.pluginConfig || {};
|
|
52
52
|
const port = Number(cfg.port) || DEFAULT_PORT;
|
|
53
53
|
const upstream = stripTrailingSlashes(cfg.upstream || process.env.AGENT402_UPSTREAM || DEFAULT_UPSTREAM);
|
|
54
|
+
const pricing = String(cfg.pricing || process.env.AGENT402_PRICING || "metered").toLowerCase() === "flat" ? "flat" : "metered";
|
|
54
55
|
const log = (m) => api.logger?.info?.(m);
|
|
55
56
|
api.registerProvider(buildProvider({ port }));
|
|
56
57
|
if (!registeredOnce) {
|
|
@@ -61,8 +62,8 @@ const plugin = {
|
|
|
61
62
|
if (started) return;
|
|
62
63
|
const creditsKey = resolveCreditsKey(cfg);
|
|
63
64
|
const payFetch = creditsKey ? null : await resolvePayFetch(cfg, log);
|
|
64
|
-
try { setCachedRoutes(await loadRoutes(upstream)); } catch (e) { log(`[agent402-openclaw] could not read ${upstream}/v1/models yet: ${e?.message || e}`); }
|
|
65
|
-
started = await startProxy({ upstream, creditsKey, payFetch, port, log });
|
|
65
|
+
try { setCachedRoutes(await loadRoutes(upstream, fetch, { pricing })); } catch (e) { log(`[agent402-openclaw] could not read ${upstream}/v1/models yet: ${e?.message || e}`); }
|
|
66
|
+
started = await startProxy({ upstream, creditsKey, payFetch, port, pricing, log });
|
|
66
67
|
setActiveProxy(started);
|
|
67
68
|
if (started.mode === "unpaid") log("[agent402-openclaw] no payment configured: run `agent402-openclaw setup` (card credits key) - paid calls will answer 402 until then");
|
|
68
69
|
},
|
package/models.js
CHANGED
|
@@ -16,19 +16,30 @@ export function stripTrailingSlashes(s) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
/** The raw catalog entry -> { id, endpoint, tier, priceUsd, maxTokens, maxInputChars }.
|
|
20
|
-
|
|
19
|
+
/** The raw catalog entry -> { id, endpoint, tier, priceUsd, maxTokens, maxInputChars }.
|
|
20
|
+
*
|
|
21
|
+
* `pricing`: "metered" (default) routes every explicit model to the gateway's
|
|
22
|
+
* metered endpoint when the catalog advertises one (`x402.meteredEndpoint`):
|
|
23
|
+
* the 402 quotes each request from its body, from `meteredFromUsd`, instead
|
|
24
|
+
* of the tier's flat price - a short call costs a fraction of a cent, a long
|
|
25
|
+
* one pays for what it asks. "flat" keeps every model on its home tier.
|
|
26
|
+
* `auto` always stays on the routed tier (the metered route needs an explicit
|
|
27
|
+
* model). */
|
|
28
|
+
export function routesFromCatalog(catalog, { pricing = "metered" } = {}) {
|
|
21
29
|
const data = Array.isArray(catalog?.data) ? catalog.data : [];
|
|
22
30
|
const routes = new Map();
|
|
31
|
+
const metered = pricing !== "flat";
|
|
23
32
|
for (const m of data) {
|
|
24
33
|
const x = m?.x402;
|
|
25
34
|
if (!m?.id || !x?.endpoint || typeof x.priceUsd !== "number") continue;
|
|
26
35
|
if (String(m.id).endsWith("*")) continue; // family wildcard, not an id
|
|
36
|
+
const useMetered = metered && typeof x.meteredEndpoint === "string" && x.meteredEndpoint.startsWith("/");
|
|
27
37
|
routes.set(m.id, {
|
|
28
38
|
id: m.id,
|
|
29
|
-
endpoint: x.endpoint,
|
|
30
|
-
tier: x.tier || null,
|
|
31
|
-
priceUsd: x.priceUsd,
|
|
39
|
+
endpoint: useMetered ? x.meteredEndpoint : x.endpoint,
|
|
40
|
+
tier: useMetered ? "v1-chat-metered" : (x.tier || null),
|
|
41
|
+
priceUsd: useMetered ? (Number(x.meteredFromUsd) || x.priceUsd) : x.priceUsd,
|
|
42
|
+
metered: useMetered,
|
|
32
43
|
maxTokens: Number(x.maxTokens) || 1024,
|
|
33
44
|
maxInputChars: Number(x.maxInputChars) || 32_000,
|
|
34
45
|
stealth: !!x.stealth,
|
|
@@ -55,7 +66,7 @@ export function openclawModels(routes) {
|
|
|
55
66
|
rows.sort((a, b) => (a.id === AUTO_ID ? -1 : b.id === AUTO_ID ? 1 : a.priceUsd - b.priceUsd || a.id.localeCompare(b.id)));
|
|
56
67
|
return rows.map((r) => ({
|
|
57
68
|
id: r.id,
|
|
58
|
-
name: r.id === AUTO_ID ? `Agent402 auto (routed, ${priceLabel(r.priceUsd)}/call)` : `${r.id} (${priceLabel(r.priceUsd)}/call)`,
|
|
69
|
+
name: r.id === AUTO_ID ? `Agent402 auto (routed, ${priceLabel(r.priceUsd)}/call)` : r.metered ? `${r.id} (metered, from ${priceLabel(r.priceUsd)}/call)` : `${r.id} (${priceLabel(r.priceUsd)}/call)`,
|
|
59
70
|
reasoning: false,
|
|
60
71
|
input: ["text"],
|
|
61
72
|
// Flat per-call billing: OpenClaw's per-token cost display does not apply.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenClaw model provider for Agent402: auto-routed and explicit LLM models at a flat per-call price, paid by card (prepaid credits key) or in USDC over x402 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
|
@@ -34,10 +34,10 @@ export const DEFAULT_UPSTREAM = "https://agent402.tools";
|
|
|
34
34
|
export const PKG_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
35
35
|
const MAX_BODY = 2 * 1024 * 1024;
|
|
36
36
|
|
|
37
|
-
export async function loadRoutes(upstream, fetchImpl = fetch) {
|
|
37
|
+
export async function loadRoutes(upstream, fetchImpl = fetch, { pricing = "metered" } = {}) {
|
|
38
38
|
const r = await fetchImpl(`${upstream}/v1/models`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15_000) });
|
|
39
39
|
if (!r.ok) throw new Error(`GET /v1/models -> HTTP ${r.status}`);
|
|
40
|
-
return routesFromCatalog(await r.json());
|
|
40
|
+
return routesFromCatalog(await r.json(), { pricing });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function readBody(req, limit = MAX_BODY) {
|
|
@@ -63,14 +63,16 @@ const json = (res, status, obj, headers = {}) => {
|
|
|
63
63
|
* @param {number} [o.port] 0 = ephemeral
|
|
64
64
|
* @param {string} [o.host] default 127.0.0.1 (loopback only: anyone who can reach the port spends your key)
|
|
65
65
|
* @param {Map} [o.routes] pre-loaded routes (tests); else loaded from upstream
|
|
66
|
+
* @param {"metered"|"flat"} [o.pricing] metered (default): explicit models pay per-request quotes; flat: home tiers
|
|
66
67
|
* @param {(msg:string)=>void} [o.log]
|
|
67
68
|
*/
|
|
68
|
-
export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = null, payFetch = null, fetch: fetchImpl = fetch, port = 0, host = "127.0.0.1", routes = null, log = () => {} } = {}) {
|
|
69
|
+
export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = null, payFetch = null, fetch: fetchImpl = fetch, port = 0, host = "127.0.0.1", routes = null, pricing = "metered", log = () => {} } = {}) {
|
|
69
70
|
upstream = stripTrailingSlashes(upstream);
|
|
71
|
+
pricing = pricing === "flat" ? "flat" : "metered";
|
|
70
72
|
const key = typeof creditsKey === "string" && /^a402_[A-Za-z0-9_-]{16,80}$/.test(creditsKey) ? creditsKey : null;
|
|
71
73
|
const paid = key ? fetchImpl : payFetch;
|
|
72
74
|
const mode = key ? "credits" : payFetch ? "x402" : "unpaid";
|
|
73
|
-
let table = routes || await loadRoutes(upstream, fetchImpl);
|
|
75
|
+
let table = routes || await loadRoutes(upstream, fetchImpl, { pricing });
|
|
74
76
|
const stats = { requests: 0, forwarded: 0, errors: 0, startedAt: new Date().toISOString() };
|
|
75
77
|
|
|
76
78
|
const server = createServer(async (req, res) => {
|
|
@@ -85,10 +87,10 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
|
|
|
85
87
|
return json(res, 403, { error: { message: `Host "${hostName.slice(0, 64)}" is not loopback; refused.`, type: "forbidden" } });
|
|
86
88
|
}
|
|
87
89
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
88
|
-
return json(res, 200, { ok: true, upstream, mode, models: table.size, stats });
|
|
90
|
+
return json(res, 200, { ok: true, upstream, mode, pricing, models: table.size, stats });
|
|
89
91
|
}
|
|
90
92
|
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
91
|
-
return json(res, 200, { object: "list", data: [...table.values()].filter((r) => !r.stealth).map((r) => ({ id: r.id, object: "model", owned_by: "agent402", agent402: { endpoint: r.endpoint, priceUsd: r.priceUsd, tier: r.tier } })) });
|
|
93
|
+
return json(res, 200, { object: "list", data: [...table.values()].filter((r) => !r.stealth).map((r) => ({ id: r.id, object: "model", owned_by: "agent402", agent402: { endpoint: r.endpoint, priceUsd: r.priceUsd, tier: r.tier, metered: !!r.metered } })) });
|
|
92
94
|
}
|
|
93
95
|
if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
94
96
|
const raw = await readBody(req);
|
|
@@ -130,11 +132,11 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
|
|
|
130
132
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, resolve); });
|
|
131
133
|
const actualPort = server.address().port;
|
|
132
134
|
const baseUrl = `http://${host}:${actualPort}`;
|
|
133
|
-
log(`[agent402-openclaw] proxy on ${baseUrl}/v1 -> ${upstream} (${mode}, ${table.size} models)`);
|
|
135
|
+
log(`[agent402-openclaw] proxy on ${baseUrl}/v1 -> ${upstream} (${mode}, ${pricing} pricing, ${table.size} models)`);
|
|
134
136
|
return {
|
|
135
|
-
baseUrl, port: actualPort, mode, upstream,
|
|
137
|
+
baseUrl, port: actualPort, mode, upstream, pricing,
|
|
136
138
|
stats: () => ({ ...stats }),
|
|
137
|
-
refreshModels: async () => { table = await loadRoutes(upstream, fetchImpl); return table.size; },
|
|
139
|
+
refreshModels: async () => { table = await loadRoutes(upstream, fetchImpl, { pricing }); return table.size; },
|
|
138
140
|
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
139
141
|
};
|
|
140
142
|
}
|