wolverine-ai 5.1.0 → 5.2.1
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/.env.example +3 -3
- package/package.json +1 -1
- package/src/brain/brain.js +2 -2
- package/src/core/ai-client.js +1 -1
- package/src/core/wolverine.js +0 -4
- package/src/dashboard/server.js +210 -0
- package/src/middleware/x402-fastify.js +214 -95
- package/src/templates/server/config/settings.json +17 -7
- package/src/templates/server/routes/api.js +62 -0
package/.env.example
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
OPENAI_API_KEY=
|
|
8
8
|
ANTHROPIC_API_KEY=
|
|
9
9
|
|
|
10
|
-
# ── Wolverine
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# ── Wolverine Platform (optional) ────────────────────────────────
|
|
11
|
+
# API key for wolverine-hosted models (wolverine-test-1, wolverine-embedding-1)
|
|
12
|
+
# Get your key at wolverinenode.xyz
|
|
13
13
|
WOLVERINE_API_KEY=
|
|
14
14
|
# ── Dashboard Admin Key (make your own) ──────────────────────────────────────────
|
|
15
15
|
# Required for the agent command interface on the dashboard.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/brain/brain.js
CHANGED
|
@@ -190,11 +190,11 @@ const SEED_DOCS = [
|
|
|
190
190
|
metadata: { topic: "notifications" },
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
|
-
text: "x402 paid APIs — turn any route into a paid API with one flag. Register the plugin once: fastify.register(require('wolverine-ai/src/middleware/x402-fastify')). Then add { config: { x402: { price: '$0.10' } } } to any route definition. That's it — the route now requires USDC payment on Base. Routes WITHOUT x402 config are completely unaffected (zero overhead). Two modes: (1) Fixed price: { x402: { price: '$0.10' } } — every call costs exactly $0.10. (2) Variable price: { x402: { variable: true, min: '$1', max: '$10000', priceField: 'dollars' } } — caller specifies amount in request body.
|
|
193
|
+
text: "x402 paid APIs — turn any route into a paid API with one flag. Register the plugin once: fastify.register(require('wolverine-ai/src/middleware/x402-fastify')). Then add { config: { x402: { price: '$0.10' } } } to any route definition. That's it — the route now requires USDC payment on Base. Routes WITHOUT x402 config are completely unaffected (zero overhead). Two modes: (1) Fixed price: { x402: { price: '$0.10' } } — every call costs exactly $0.10. (2) Variable price: { x402: { variable: true, min: '$1', max: '$10000', priceField: 'dollars' } } — caller specifies amount in request body. Settlement flow: no Payment-Signature header → 402 + Payment-Required header with price/network/payTo. Client SDK (@x402/fetch or wallet signing) handles payment → retries with Payment-Signature → middleware sends to x402 facilitator (CDP production: api.cdp.coinbase.com/platform/v2/x402, testnet: x402.org/facilitator) → facilitator verifies signature AND executes on-chain transferWithAuthorization() → USDC moves on Base → req.x402 = { paid: true, amount: '$5.00', from: '0x...', txHash } → handler runs. Fallback: if facilitator is down, server self-settles using vault wallet to call transferWithAuthorization() directly (server pays gas). Facilitator auto-selected by network: eip155:8453 (Base mainnet) → production, eip155:84532 (Base Sepolia) → testnet. Bazaar compatible — services auto-cataloged on first facilitator-settled payment. Vault wallet auto-detected as payTo address. Dashboard tracks x402 payments, revenue by route, USDC/ETH balances. CLI: wolverine --x402-info.",
|
|
194
194
|
metadata: { topic: "x402-paid-apis" },
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
|
-
text: "x402 examples — making a paid API is as simple as adding a route. FREE route (normal): fastify.get('/api/data', async (req) => { return getData(); }). PAID route (just add x402 flag): fastify.get('/api/premium-data', { config: { x402: { price: '$0.01' } } }, async (req) => { return getPremiumData(); }). CREDIT PURCHASE route (variable): fastify.post('/buy-credits', { config: { x402: { variable: true, min: '$1', max: '$10000', priceField: 'dollars' } } }, async (req) => { const credits = parseFloat(req.x402.amount.replace('$','')) * 100; await addCredits(req.body.accountId, credits); return { credits }; }). Payment
|
|
197
|
+
text: "x402 examples — making a paid API is as simple as adding a route. FREE route (normal): fastify.get('/api/data', async (req) => { return getData(); }). PAID route (just add x402 flag): fastify.get('/api/premium-data', { config: { x402: { price: '$0.01' } } }, async (req) => { return getPremiumData(); }). CREDIT PURCHASE route (variable): fastify.post('/buy-credits', { config: { x402: { variable: true, min: '$1', max: '$10000', priceField: 'dollars' } } }, async (req) => { const credits = parseFloat(req.x402.amount.replace('$','')) * 100; await addCredits(req.body.accountId, credits); return { credits }; }). Payment settlement is automatic — the facilitator verifies and settles on-chain before the handler runs. No blockchain code needed in route handlers. Compatible with @x402/fetch, @x402/axios client SDKs and x402 Bazaar API discovery.",
|
|
198
198
|
metadata: { topic: "x402-examples" },
|
|
199
199
|
},
|
|
200
200
|
{
|
package/src/core/ai-client.js
CHANGED
|
@@ -656,7 +656,7 @@ ${backupSourceCode ? `## Last Known Working Version\n\`\`\`javascript\n${backupS
|
|
|
656
656
|
"changes" is for code edits (optional, use for actual code fixes).
|
|
657
657
|
Include both if needed, or just one.`;
|
|
658
658
|
|
|
659
|
-
const result = await aiCall({ model, systemPrompt, userPrompt, maxTokens: 2048, category: "
|
|
659
|
+
const result = await aiCall({ model, systemPrompt, userPrompt, maxTokens: 2048, category: "coding" });
|
|
660
660
|
const content = (result.content || "").trim();
|
|
661
661
|
|
|
662
662
|
// Guard: cap length before regex extraction to prevent catastrophic backtracking
|
package/src/core/wolverine.js
CHANGED
|
@@ -380,10 +380,6 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
380
380
|
const patchResults = applyPatch(repair.changes, cwd, sandbox);
|
|
381
381
|
if (!patchResults.every(r => r.success)) throw new Error("Patch failed");
|
|
382
382
|
|
|
383
|
-
// Track code generation as "coding" — the AI produced code changes
|
|
384
|
-
const codeTokens = repair.changes.reduce((s, c) => s + ((c.new || "").length / 4), 0);
|
|
385
|
-
_trackOp(getModel("coding"), "coding", 0, Math.round(codeTokens), "patch_apply", 0, true);
|
|
386
|
-
|
|
387
383
|
for (const r of patchResults) console.log(chalk.green(` ✅ Patched: ${r.file}`));
|
|
388
384
|
}
|
|
389
385
|
|
package/src/dashboard/server.js
CHANGED
|
@@ -67,6 +67,8 @@ class DashboardServer {
|
|
|
67
67
|
if (req.url === "/api/process") return this._handleProcess(req, res);
|
|
68
68
|
if (req.url === "/api/routes") return this._handleRoutes(req, res);
|
|
69
69
|
if (req.url === "/api/usage/history") return this._handleUsageHistory(req, res);
|
|
70
|
+
if (req.url === "/api/vault") return this._handleVault(req, res);
|
|
71
|
+
if (req.url === "/api/x402") return this._handleX402(req, res);
|
|
70
72
|
if (req.url === "/api/auth/verify" && req.method === "POST") return this._handleAuthVerify(req, res);
|
|
71
73
|
if (req.url === "/api/command" && req.method === "POST") return this._handleCommand(req, res);
|
|
72
74
|
if (req.url.startsWith("/api/backups/") && req.url.endsWith("/rollback") && req.method === "POST") return this._handleRollback(req, res);
|
|
@@ -1048,6 +1050,82 @@ ${context ? "\nBrain:\n" + context : ""}`,
|
|
|
1048
1050
|
res.end(JSON.stringify(this.brain ? this.brain.getStats() : {}));
|
|
1049
1051
|
}
|
|
1050
1052
|
|
|
1053
|
+
async _handleVault(req, res) {
|
|
1054
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1055
|
+
try {
|
|
1056
|
+
const { getVaultStatus, getWalletAddress } = require("../vault/wallet-ops");
|
|
1057
|
+
const status = getVaultStatus();
|
|
1058
|
+
let address = null;
|
|
1059
|
+
let balances = { usdc: null, eth: null };
|
|
1060
|
+
|
|
1061
|
+
if (status.initialized) {
|
|
1062
|
+
try { address = await getWalletAddress(); } catch {}
|
|
1063
|
+
// Fetch on-chain balances from Base network
|
|
1064
|
+
if (address) {
|
|
1065
|
+
try {
|
|
1066
|
+
const https = require("https");
|
|
1067
|
+
const rpc = "https://mainnet.base.org";
|
|
1068
|
+
const fetchRpc = (method, params) => new Promise((resolve) => {
|
|
1069
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
|
|
1070
|
+
const req = https.request(rpc, { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }, timeout: 5000 }, (res) => {
|
|
1071
|
+
let data = ""; res.on("data", c => data += c);
|
|
1072
|
+
res.on("end", () => { try { resolve(JSON.parse(data).result); } catch { resolve(null); } });
|
|
1073
|
+
});
|
|
1074
|
+
req.on("error", () => resolve(null));
|
|
1075
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
1076
|
+
req.write(body); req.end();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// ETH balance
|
|
1080
|
+
const ethHex = await fetchRpc("eth_getBalance", [address, "latest"]);
|
|
1081
|
+
if (ethHex) balances.eth = (parseInt(ethHex, 16) / 1e18).toFixed(6);
|
|
1082
|
+
|
|
1083
|
+
// USDC balance (ERC-20 balanceOf)
|
|
1084
|
+
const usdcContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
1085
|
+
const balanceOfSig = "0x70a08231000000000000000000000000" + address.slice(2).toLowerCase();
|
|
1086
|
+
const usdcHex = await fetchRpc("eth_call", [{ to: usdcContract, data: balanceOfSig }, "latest"]);
|
|
1087
|
+
if (usdcHex) balances.usdc = (parseInt(usdcHex, 16) / 1e6).toFixed(2);
|
|
1088
|
+
} catch {}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
res.end(JSON.stringify({ ...status, address, balances }));
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
res.end(JSON.stringify({ initialized: false, error: err.message }));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
_handleX402(req, res) {
|
|
1099
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1100
|
+
// Read x402 payment log from .wolverine/x402-payments.json
|
|
1101
|
+
const fs = require("fs");
|
|
1102
|
+
const path = require("path");
|
|
1103
|
+
const logPath = path.join(process.cwd(), ".wolverine", "x402-payments.json");
|
|
1104
|
+
let payments = [];
|
|
1105
|
+
try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
|
|
1106
|
+
|
|
1107
|
+
const totalRevenue = payments.reduce((s, p) => s + (parseFloat(p.amount?.replace("$", "")) || 0), 0);
|
|
1108
|
+
const successfulPayments = payments.filter(p => p.verified);
|
|
1109
|
+
const recentPayments = payments.slice(-50).reverse();
|
|
1110
|
+
|
|
1111
|
+
// Group by route
|
|
1112
|
+
const byRoute = {};
|
|
1113
|
+
for (const p of payments) {
|
|
1114
|
+
const route = p.route || "unknown";
|
|
1115
|
+
if (!byRoute[route]) byRoute[route] = { calls: 0, revenue: 0 };
|
|
1116
|
+
byRoute[route].calls++;
|
|
1117
|
+
byRoute[route].revenue += parseFloat(p.amount?.replace("$", "")) || 0;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
res.end(JSON.stringify({
|
|
1121
|
+
totalPayments: payments.length,
|
|
1122
|
+
successfulPayments: successfulPayments.length,
|
|
1123
|
+
totalRevenue: totalRevenue.toFixed(2),
|
|
1124
|
+
recentPayments,
|
|
1125
|
+
byRoute,
|
|
1126
|
+
}));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1051
1129
|
_handleDashboard(req, res) {
|
|
1052
1130
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1053
1131
|
res.end(DASHBOARD_HTML.replace(/__PORT__/g, String(this.port)));
|
|
@@ -1164,6 +1242,10 @@ main{overflow-y:auto;padding:24px}
|
|
|
1164
1242
|
<a data-panel="analytics">📊 Analytics</a>
|
|
1165
1243
|
<a data-panel="processes">⚙️ Processes</a>
|
|
1166
1244
|
<div class="sep"></div>
|
|
1245
|
+
<div class="label">Wallet</div>
|
|
1246
|
+
<a data-panel="vault">🔐 Vault</a>
|
|
1247
|
+
<a data-panel="x402">💳 x402 Payments</a>
|
|
1248
|
+
<div class="sep"></div>
|
|
1167
1249
|
<div class="label">Agent</div>
|
|
1168
1250
|
<a data-panel="command">💬 Command</a>
|
|
1169
1251
|
<div class="sep"></div>
|
|
@@ -1183,6 +1265,14 @@ main{overflow-y:auto;padding:24px}
|
|
|
1183
1265
|
<div class="stat-card brain"><div class="stat-val" id="s-memories">0</div><div class="stat-lbl">Memories</div></div>
|
|
1184
1266
|
<div class="stat-card up"><div class="stat-val" id="s-uptime">0s</div><div class="stat-lbl">Uptime</div></div>
|
|
1185
1267
|
</div>
|
|
1268
|
+
<div class="stats" style="grid-template-columns:repeat(6,1fr);margin-bottom:24px">
|
|
1269
|
+
<div class="stat-card" style="border-top:3px solid var(--green)"><div class="stat-val" id="ov-mem">—</div><div class="stat-lbl">Memory</div></div>
|
|
1270
|
+
<div class="stat-card" style="border-top:3px solid var(--yellow)"><div class="stat-val" id="ov-cpu">—</div><div class="stat-lbl">CPU</div></div>
|
|
1271
|
+
<div class="stat-card" style="border-top:3px solid var(--blue)"><div class="stat-val" id="ov-routes">—</div><div class="stat-lbl">Routes</div></div>
|
|
1272
|
+
<div class="stat-card" style="border-top:3px solid var(--purple)"><div class="stat-val" id="ov-cost">$0</div><div class="stat-lbl">AI Cost</div></div>
|
|
1273
|
+
<div class="stat-card" style="border-top:3px solid #50fa7b"><div class="stat-val" id="ov-usdc">—</div><div class="stat-lbl">USDC</div></div>
|
|
1274
|
+
<div class="stat-card" style="border-top:3px solid var(--accent)"><div class="stat-val" id="ov-x402">$0</div><div class="stat-lbl">x402 Revenue</div></div>
|
|
1275
|
+
</div>
|
|
1186
1276
|
<div class="row2">
|
|
1187
1277
|
<div class="card"><h3>Recent Events</h3><div class="ev-list" id="ov-events" style="max-height:400px"></div></div>
|
|
1188
1278
|
<div>
|
|
@@ -1232,6 +1322,56 @@ main{overflow-y:auto;padding:24px}
|
|
|
1232
1322
|
</div>
|
|
1233
1323
|
<div class="card"><h3>Route Response Times</h3><div id="a-route-list"><div class="empty">Waiting for first probe cycle...</div></div></div>
|
|
1234
1324
|
</div>
|
|
1325
|
+
<div class="panel" id="p-vault">
|
|
1326
|
+
<div class="stats" style="grid-template-columns:repeat(4,1fr)">
|
|
1327
|
+
<div class="stat-card heal"><div class="stat-val" id="v-status">—</div><div class="stat-lbl">Vault Status</div></div>
|
|
1328
|
+
<div class="stat-card up"><div class="stat-val" id="v-usdc">—</div><div class="stat-lbl">USDC Balance</div></div>
|
|
1329
|
+
<div class="stat-card brain"><div class="stat-val" id="v-eth">—</div><div class="stat-lbl">ETH Balance</div></div>
|
|
1330
|
+
<div class="stat-card roll"><div class="stat-val" id="v-x402-rev">$0</div><div class="stat-lbl">x402 Revenue</div></div>
|
|
1331
|
+
</div>
|
|
1332
|
+
<div class="row2">
|
|
1333
|
+
<div class="card">
|
|
1334
|
+
<h3>Wallet Details</h3>
|
|
1335
|
+
<div id="v-details"><div class="empty">Loading vault...</div></div>
|
|
1336
|
+
</div>
|
|
1337
|
+
<div class="card">
|
|
1338
|
+
<h3>Vault Security</h3>
|
|
1339
|
+
<div id="v-security"><div class="empty">Loading...</div></div>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
<div class="card">
|
|
1343
|
+
<h3>Setup Guide</h3>
|
|
1344
|
+
<div style="font-size:.82rem;color:var(--text2);line-height:1.7;padding:8px 0">
|
|
1345
|
+
<div class="mrow"><span style="color:var(--accent);font-family:monospace">wolverine --init-vault</span><span class="vals">Initialize encrypted wallet (AES-256-GCM)</span></div>
|
|
1346
|
+
<div class="mrow"><span style="color:var(--accent);font-family:monospace">wolverine --x402-info</span><span class="vals">Show wallet address & x402 configuration</span></div>
|
|
1347
|
+
<div class="mrow"><span style="color:var(--text2)">Vault path</span><span class="vals">.wolverine/vault/ (auto-backed up in every snapshot)</span></div>
|
|
1348
|
+
<div class="mrow"><span style="color:var(--text2)">Network</span><span class="vals">Base (eip155:8453) — USDC payments</span></div>
|
|
1349
|
+
<div class="mrow"><span style="color:var(--text2)">Protocol</span><span class="vals"><a href="https://docs.cdp.coinbase.com/x402/welcome" style="color:var(--blue);text-decoration:none">x402</a> — server-to-server USDC payments</span></div>
|
|
1350
|
+
</div>
|
|
1351
|
+
</div>
|
|
1352
|
+
</div>
|
|
1353
|
+
<div class="panel" id="p-x402">
|
|
1354
|
+
<div class="stats" style="grid-template-columns:repeat(4,1fr)">
|
|
1355
|
+
<div class="stat-card heal"><div class="stat-val" id="x-total">0</div><div class="stat-lbl">Total Payments</div></div>
|
|
1356
|
+
<div class="stat-card up"><div class="stat-val" id="x-success">0</div><div class="stat-lbl">Verified</div></div>
|
|
1357
|
+
<div class="stat-card roll"><div class="stat-val" id="x-revenue">$0</div><div class="stat-lbl">Total Revenue</div></div>
|
|
1358
|
+
<div class="stat-card brain"><div class="stat-val" id="x-routes">0</div><div class="stat-lbl">Paid Routes</div></div>
|
|
1359
|
+
</div>
|
|
1360
|
+
<div class="row2">
|
|
1361
|
+
<div class="card">
|
|
1362
|
+
<h3>Revenue by Route</h3>
|
|
1363
|
+
<div id="x-by-route"><div class="empty">No x402 payments yet</div></div>
|
|
1364
|
+
</div>
|
|
1365
|
+
<div class="card">
|
|
1366
|
+
<h3>Revenue Chart</h3>
|
|
1367
|
+
<div id="x-chart" style="height:160px"><div class="empty">Chart appears after payments</div></div>
|
|
1368
|
+
</div>
|
|
1369
|
+
</div>
|
|
1370
|
+
<div class="card">
|
|
1371
|
+
<h3>Recent Payments</h3>
|
|
1372
|
+
<div id="x-recent"><div class="empty">No payments recorded yet. Add x402 config to any route:<br><code style="color:var(--accent);font-size:.78rem">{ config: { x402: { price: "$0.01" } } }</code></div></div>
|
|
1373
|
+
</div>
|
|
1374
|
+
</div>
|
|
1235
1375
|
<div class="panel" id="p-command">
|
|
1236
1376
|
<div id="cmd-auth" class="auth-gate">
|
|
1237
1377
|
<h2>🔐 Admin Authentication</h2>
|
|
@@ -1570,6 +1710,76 @@ async function refresh(){
|
|
|
1570
1710
|
}).join('');
|
|
1571
1711
|
}
|
|
1572
1712
|
}
|
|
1713
|
+
// Vault + x402
|
|
1714
|
+
const [vault,x402]=await Promise.all([fetch(B+'/api/vault').then(r=>r.json()).catch(()=>({})),fetch(B+'/api/x402').then(r=>r.json()).catch(()=>({}))]);
|
|
1715
|
+
// Vault panel
|
|
1716
|
+
if(vault){
|
|
1717
|
+
$('v-status').textContent=vault.initialized?'Active':'Not Init';
|
|
1718
|
+
$('v-status').style.color=vault.initialized?'var(--green)':'var(--text2)';
|
|
1719
|
+
$('v-usdc').textContent=vault.balances?.usdc!=null?'$'+vault.balances.usdc:'—';
|
|
1720
|
+
$('v-eth').textContent=vault.balances?.eth!=null?vault.balances.eth:'—';
|
|
1721
|
+
// Overview row
|
|
1722
|
+
$('ov-usdc').textContent=vault.balances?.usdc!=null?'$'+vault.balances.usdc:'—';
|
|
1723
|
+
|
|
1724
|
+
if(vault.address){
|
|
1725
|
+
const short=vault.address.slice(0,6)+'...'+vault.address.slice(-4);
|
|
1726
|
+
$('v-details').innerHTML=[
|
|
1727
|
+
'<div class="mrow"><span>Address</span><span class="vals" style="font-family:monospace;color:var(--blue)">'+vault.address+'</span></div>',
|
|
1728
|
+
'<div class="mrow"><span>Short</span><span class="vals"><b>'+short+'</b></span></div>',
|
|
1729
|
+
'<div class="mrow"><span>USDC (Base)</span><span class="vals"><b style="color:var(--green)">$'+(vault.balances?.usdc||'0.00')+'</b></span></div>',
|
|
1730
|
+
'<div class="mrow"><span>ETH (Base)</span><span class="vals"><b>'+(vault.balances?.eth||'0.000000')+' ETH</b></span></div>',
|
|
1731
|
+
'<div class="mrow"><span>Network</span><span class="vals">Base (eip155:8453)</span></div>',
|
|
1732
|
+
].join('');
|
|
1733
|
+
}else{
|
|
1734
|
+
$('v-details').innerHTML='<div class="empty">Vault not initialized. Run <code style="color:var(--accent)">wolverine --init-vault</code></div>';
|
|
1735
|
+
}
|
|
1736
|
+
$('v-security').innerHTML=[
|
|
1737
|
+
'<div class="mrow"><span>Encryption</span><span class="vals">'+(vault.initialized?'<span style="color:var(--green)">AES-256-GCM</span>':'<span style="color:var(--text2)">N/A</span>')+'</span></div>',
|
|
1738
|
+
'<div class="mrow"><span>Master Key</span><span class="vals">'+(vault.masterKeyExists?'<span style="color:var(--green)">✓ Present</span>':'<span style="color:var(--red)">✗ Missing</span>')+'</span></div>',
|
|
1739
|
+
'<div class="mrow"><span>ETH Vault</span><span class="vals">'+(vault.ethVaultExists?'<span style="color:var(--green)">✓ Encrypted</span>':'<span style="color:var(--text2)">✗ Not created</span>')+'</span></div>',
|
|
1740
|
+
'<div class="mrow"><span>Agent Access</span><span class="vals"><span style="color:var(--green)">Blocked</span> (sandbox-protected)</span></div>',
|
|
1741
|
+
'<div class="mrow"><span>Backup</span><span class="vals">Included in every snapshot</span></div>',
|
|
1742
|
+
].join('');
|
|
1743
|
+
}
|
|
1744
|
+
// x402 panel
|
|
1745
|
+
if(x402){
|
|
1746
|
+
$('x-total').textContent=x402.totalPayments||0;
|
|
1747
|
+
$('x-success').textContent=x402.successfulPayments||0;
|
|
1748
|
+
$('x-revenue').textContent='$'+(x402.totalRevenue||'0.00');
|
|
1749
|
+
$('v-x402-rev').textContent='$'+(x402.totalRevenue||'0.00');
|
|
1750
|
+
$('ov-x402').textContent='$'+(x402.totalRevenue||'0.00');
|
|
1751
|
+
const routes=x402.byRoute||{};
|
|
1752
|
+
const routeKeys=Object.keys(routes);
|
|
1753
|
+
$('x-routes').textContent=routeKeys.length;
|
|
1754
|
+
if(routeKeys.length>0){
|
|
1755
|
+
$('x-by-route').innerHTML=routeKeys.sort((a,b)=>routes[b].revenue-routes[a].revenue).map(r=>'<div class="mrow"><span class="ep">'+esc(r)+'</span><span class="vals"><b style="color:var(--green)">$'+routes[r].revenue.toFixed(2)+'</b> · '+routes[r].calls+' calls</span></div>').join('');
|
|
1756
|
+
}
|
|
1757
|
+
const recent=x402.recentPayments||[];
|
|
1758
|
+
if(recent.length>0){
|
|
1759
|
+
$('x-recent').innerHTML=recent.slice(0,20).map(p=>{
|
|
1760
|
+
const icon=p.verified?'✅':'⏳';
|
|
1761
|
+
return '<div class="mrow" style="flex-wrap:wrap"><span>'+icon+' <span class="ep">'+esc(p.route||'?')+'</span></span><span class="vals"><b style="color:var(--green)">'+(p.amount||'?')+'</b> USDC · '+(p.from?p.from.slice(0,6)+'...'+p.from.slice(-4):'?')+'</span><div style="width:100%;font-size:.7rem;color:var(--text2)">'+new Date(p.timestamp).toLocaleString()+'</div></div>';
|
|
1762
|
+
}).join('');
|
|
1763
|
+
// Revenue chart
|
|
1764
|
+
if(recent.length>1){
|
|
1765
|
+
const max=Math.max(...recent.map(p=>parseFloat(p.amount?.replace('$','')||0)))||1;
|
|
1766
|
+
const w=$('x-chart').offsetWidth||500,h=150;
|
|
1767
|
+
const bw=Math.max(6,Math.floor(w/recent.length)-2);
|
|
1768
|
+
let svg='<svg width="'+w+'" height="'+h+'">';
|
|
1769
|
+
recent.reverse().forEach((p,i)=>{
|
|
1770
|
+
const amt=parseFloat(p.amount?.replace('$','')||0);
|
|
1771
|
+
const bh=Math.max(2,Math.round((amt/max)*h*0.85));
|
|
1772
|
+
const c=p.verified?'#50fa7b':'var(--yellow)';
|
|
1773
|
+
svg+='<rect x="'+(i*(bw+2))+'" y="'+(h-bh)+'" width="'+bw+'" height="'+bh+'" fill="'+c+'" rx="2"><title>'+p.amount+' from '+(p.from||'?').slice(0,10)+'</title></rect>';
|
|
1774
|
+
});
|
|
1775
|
+
svg+='</svg>';$('x-chart').innerHTML=svg;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
// Overview: server health
|
|
1780
|
+
if(proc&&proc.current){$('ov-mem').textContent=proc.current.rss+'MB';$('ov-cpu').textContent=proc.current.cpu+'%';}
|
|
1781
|
+
if(routeData&&routeData.summary)$('ov-routes').textContent=(routeData.summary.healthy||0)+'/'+(routeData.summary.totalRoutes||0);
|
|
1782
|
+
if(usage&&usage.session)$('ov-cost').textContent='$'+(usage.session.totalCostUsd||0).toFixed(4);
|
|
1573
1783
|
}catch(e){}
|
|
1574
1784
|
}
|
|
1575
1785
|
|
|
@@ -6,7 +6,11 @@ const path = require("path");
|
|
|
6
6
|
*
|
|
7
7
|
* Makes it dead simple to accept USDC payments on Base network.
|
|
8
8
|
* The developer marks a route with x402 config, and the middleware
|
|
9
|
-
* handles the 402 → payment → verification →
|
|
9
|
+
* handles the 402 → payment → verification → settlement flow.
|
|
10
|
+
*
|
|
11
|
+
* Settlement is handled by the x402 facilitator (Coinbase CDP),
|
|
12
|
+
* which verifies the payment signature AND executes the on-chain
|
|
13
|
+
* USDC transfer. Compatible with x402 Bazaar for API discovery.
|
|
10
14
|
*
|
|
11
15
|
* Two modes:
|
|
12
16
|
* Fixed price: { x402: { price: "$0.01" } }
|
|
@@ -37,7 +41,9 @@ const path = require("path");
|
|
|
37
41
|
|
|
38
42
|
let _payTo = null;
|
|
39
43
|
let _network = "eip155:8453";
|
|
40
|
-
|
|
44
|
+
// Production facilitator (Base mainnet) — handles verify + on-chain settlement
|
|
45
|
+
let _facilitatorUrl = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
46
|
+
// Testnet facilitator: "https://x402.org/facilitator"
|
|
41
47
|
|
|
42
48
|
async function x402Plugin(fastify, opts) {
|
|
43
49
|
// Config
|
|
@@ -62,8 +68,17 @@ async function x402Plugin(fastify, opts) {
|
|
|
62
68
|
} catch {}
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
// Auto-select facilitator based on network
|
|
72
|
+
if (!opts.facilitator) {
|
|
73
|
+
const isTestnet = _network.includes("84532") || _network.includes("11155");
|
|
74
|
+
_facilitatorUrl = isTestnet
|
|
75
|
+
? "https://x402.org/facilitator"
|
|
76
|
+
: "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
if (_payTo) {
|
|
66
80
|
console.log(` 💰 x402: payments to ${_payTo.slice(0, 6)}...${_payTo.slice(-4)} on ${_network}`);
|
|
81
|
+
console.log(` 💰 x402: facilitator ${_facilitatorUrl}`);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
// ── Route-level x402 hook (preHandler so body is parsed for variable pricing) ──
|
|
@@ -130,15 +145,37 @@ async function x402Plugin(fastify, opts) {
|
|
|
130
145
|
return;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
|
-
// Payment present —
|
|
134
|
-
const
|
|
148
|
+
// Payment present — send to facilitator for verification + on-chain settlement
|
|
149
|
+
const routeAccepts = [{
|
|
150
|
+
scheme: "exact",
|
|
151
|
+
price,
|
|
152
|
+
network: _network,
|
|
153
|
+
payTo: _payTo,
|
|
154
|
+
}];
|
|
155
|
+
|
|
156
|
+
const verified = await _settleViaFacilitator(paymentSig, routeAccepts);
|
|
135
157
|
if (verified.valid) {
|
|
136
158
|
reply.header("Payment-Response", JSON.stringify(verified.receipt || {}));
|
|
137
159
|
request.x402 = { paid: true, amount: price, receipt: verified.receipt, txHash: verified.txHash, from: verified.from };
|
|
160
|
+
// Log payment for dashboard analytics
|
|
161
|
+
_logPayment({ route: request.url, method: request.method, amount: price, from: verified.from, txHash: verified.txHash, verified: true, timestamp: Date.now() });
|
|
138
162
|
return; // continue to route handler
|
|
139
163
|
}
|
|
140
164
|
|
|
141
|
-
|
|
165
|
+
// Facilitator rejected — try self-settlement as fallback
|
|
166
|
+
if (verified.reason === "facilitator_unavailable") {
|
|
167
|
+
const selfResult = await _selfSettle(paymentSig, price);
|
|
168
|
+
if (selfResult.valid) {
|
|
169
|
+
reply.header("Payment-Response", JSON.stringify(selfResult.receipt || {}));
|
|
170
|
+
request.x402 = { paid: true, amount: price, receipt: selfResult.receipt, txHash: selfResult.txHash, from: selfResult.from };
|
|
171
|
+
_logPayment({ route: request.url, method: request.method, amount: price, from: selfResult.from, txHash: selfResult.txHash, verified: true, settled: "self", timestamp: Date.now() });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
reply.code(402).send({ error: "Payment settlement failed", reason: selfResult.reason, price, payTo: _payTo });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
reply.code(402).send({ error: "Payment verification failed", reason: verified.reason, price, payTo: _payTo });
|
|
142
179
|
});
|
|
143
180
|
|
|
144
181
|
// ── Public pricing endpoint ──
|
|
@@ -151,119 +188,201 @@ async function x402Plugin(fastify, opts) {
|
|
|
151
188
|
}));
|
|
152
189
|
}
|
|
153
190
|
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Send payment to the x402 facilitator for verification AND on-chain settlement.
|
|
193
|
+
* The facilitator:
|
|
194
|
+
* 1. Verifies the EIP-3009 signature
|
|
195
|
+
* 2. Calls transferWithAuthorization() on the USDC contract
|
|
196
|
+
* 3. Returns the transaction hash
|
|
197
|
+
*
|
|
198
|
+
* This is the standard x402 flow — compatible with Bazaar discovery.
|
|
199
|
+
*/
|
|
200
|
+
async function _settleViaFacilitator(paymentSig, routeAccepts) {
|
|
201
|
+
try {
|
|
202
|
+
const https = require("https");
|
|
203
|
+
const http = require("http");
|
|
204
|
+
const url = new (require("url").URL)(_facilitatorUrl + "/verify");
|
|
205
|
+
const body = JSON.stringify({
|
|
206
|
+
paymentSignature: paymentSig,
|
|
207
|
+
routeConfig: { accepts: routeAccepts },
|
|
208
|
+
});
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
const client = url.protocol === "https:" ? https : http;
|
|
211
|
+
const req = client.request({
|
|
212
|
+
hostname: url.hostname,
|
|
213
|
+
port: url.port,
|
|
214
|
+
path: url.pathname,
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
217
|
+
timeout: 30000, // settlement can take time (on-chain tx)
|
|
218
|
+
}, (res) => {
|
|
219
|
+
let data = "";
|
|
220
|
+
res.on("data", (c) => data += c);
|
|
221
|
+
res.on("end", () => {
|
|
222
|
+
try {
|
|
223
|
+
const p = JSON.parse(data);
|
|
224
|
+
if (p.valid || p.success) {
|
|
225
|
+
resolve({
|
|
226
|
+
valid: true,
|
|
227
|
+
receipt: p,
|
|
228
|
+
txHash: p.txHash || p.transactionHash || null,
|
|
229
|
+
from: p.from || p.payer || null,
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
resolve({ valid: false, reason: p.error || p.reason || "facilitator_rejected" });
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
resolve({ valid: false, reason: "facilitator_invalid_response" });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
req.on("error", () => resolve({ valid: false, reason: "facilitator_unavailable" }));
|
|
240
|
+
req.on("timeout", () => { req.destroy(); resolve({ valid: false, reason: "facilitator_timeout" }); });
|
|
241
|
+
req.write(body);
|
|
242
|
+
req.end();
|
|
243
|
+
});
|
|
244
|
+
} catch {
|
|
245
|
+
return { valid: false, reason: "facilitator_unavailable" };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Self-settlement fallback — verify the signature AND execute the
|
|
251
|
+
* on-chain transferWithAuthorization() using the server's vault wallet.
|
|
252
|
+
*
|
|
253
|
+
* Only used when the facilitator is unavailable. Requires ethers + RPC.
|
|
254
|
+
* The server pays gas to execute the transfer.
|
|
255
|
+
*/
|
|
256
|
+
async function _selfSettle(paymentSig, price) {
|
|
156
257
|
let payload;
|
|
157
258
|
try {
|
|
158
259
|
payload = JSON.parse(Buffer.from(paymentSig, "base64").toString());
|
|
159
260
|
} catch {
|
|
160
|
-
|
|
161
|
-
|
|
261
|
+
return { valid: false, reason: "invalid_payment_format" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!payload.payload?.authorization || !payload.payload?.signature) {
|
|
265
|
+
return { valid: false, reason: "missing_authorization" };
|
|
162
266
|
}
|
|
163
267
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const auth = payload.payload.authorization;
|
|
167
|
-
const sig = payload.payload.signature;
|
|
268
|
+
const auth = payload.payload.authorization;
|
|
269
|
+
const sig = payload.payload.signature;
|
|
168
270
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
271
|
+
// Verify amount
|
|
272
|
+
const expectedUsdc = Math.round(parseFloat(price.replace("$", "")) * 1e6);
|
|
273
|
+
// Value comes as decimal string (e.g. "1000000" for $1) or hex (legacy "0xf4240")
|
|
274
|
+
const valStr = String(auth.value);
|
|
275
|
+
const actualUsdc = valStr.startsWith("0x") ? parseInt(valStr, 16) : parseInt(valStr, 10) || 0;
|
|
276
|
+
if (actualUsdc < expectedUsdc * 0.99) {
|
|
277
|
+
return { valid: false, reason: "amount_mismatch" };
|
|
278
|
+
}
|
|
175
279
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
280
|
+
// Verify payTo matches
|
|
281
|
+
if (auth.to?.toLowerCase() !== _payTo?.toLowerCase()) {
|
|
282
|
+
return { valid: false, reason: "wrong_recipient" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Verify not expired
|
|
286
|
+
if (auth.validBefore && auth.validBefore < Math.floor(Date.now() / 1000)) {
|
|
287
|
+
return { valid: false, reason: "payment_expired" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const { ethers } = require("ethers");
|
|
292
|
+
const fromAddr = ethers.getAddress(auth.from.toLowerCase());
|
|
293
|
+
const toAddr = ethers.getAddress(auth.to.toLowerCase());
|
|
294
|
+
|
|
295
|
+
// Step 1: Verify the signature
|
|
296
|
+
const domain = {
|
|
297
|
+
name: "USD Coin",
|
|
298
|
+
version: "2",
|
|
299
|
+
chainId: 8453,
|
|
300
|
+
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
301
|
+
};
|
|
302
|
+
const types = {
|
|
303
|
+
TransferWithAuthorization: [
|
|
304
|
+
{ name: "from", type: "address" },
|
|
305
|
+
{ name: "to", type: "address" },
|
|
306
|
+
{ name: "value", type: "uint256" },
|
|
307
|
+
{ name: "validAfter", type: "uint256" },
|
|
308
|
+
{ name: "validBefore", type: "uint256" },
|
|
309
|
+
{ name: "nonce", type: "bytes32" },
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
const message = {
|
|
313
|
+
from: fromAddr,
|
|
314
|
+
to: toAddr,
|
|
315
|
+
value: auth.value,
|
|
316
|
+
validAfter: auth.validAfter,
|
|
317
|
+
validBefore: auth.validBefore,
|
|
318
|
+
nonce: auth.nonce,
|
|
319
|
+
};
|
|
180
320
|
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
return { valid: false, reason: "
|
|
321
|
+
const recoveredAddress = ethers.verifyTypedData(domain, types, message, sig);
|
|
322
|
+
if (recoveredAddress.toLowerCase() !== fromAddr.toLowerCase()) {
|
|
323
|
+
return { valid: false, reason: "signature_mismatch" };
|
|
184
324
|
}
|
|
185
325
|
|
|
186
|
-
//
|
|
326
|
+
// Step 2: Execute transferWithAuthorization on-chain
|
|
327
|
+
// Use the vault wallet to submit the transaction (server pays gas)
|
|
328
|
+
const { decryptPrivateKey } = require("../vault/vault-manager");
|
|
329
|
+
let keyBuf = null;
|
|
187
330
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
chainId: 8453,
|
|
197
|
-
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
198
|
-
};
|
|
199
|
-
const types = {
|
|
200
|
-
TransferWithAuthorization: [
|
|
201
|
-
{ name: "from", type: "address" },
|
|
202
|
-
{ name: "to", type: "address" },
|
|
203
|
-
{ name: "value", type: "uint256" },
|
|
204
|
-
{ name: "validAfter", type: "uint256" },
|
|
205
|
-
{ name: "validBefore", type: "uint256" },
|
|
206
|
-
{ name: "nonce", type: "bytes32" },
|
|
331
|
+
keyBuf = decryptPrivateKey();
|
|
332
|
+
const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
|
|
333
|
+
const wallet = new ethers.Wallet(keyBuf, provider);
|
|
334
|
+
|
|
335
|
+
const usdcContract = new ethers.Contract(
|
|
336
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
337
|
+
[
|
|
338
|
+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external",
|
|
207
339
|
],
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
from: fromAddr,
|
|
211
|
-
to: toAddr,
|
|
212
|
-
value: auth.value,
|
|
213
|
-
validAfter: auth.validAfter,
|
|
214
|
-
validBefore: auth.validBefore,
|
|
215
|
-
nonce: auth.nonce,
|
|
216
|
-
};
|
|
340
|
+
wallet
|
|
341
|
+
);
|
|
217
342
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
343
|
+
// Parse the signature into v, r, s
|
|
344
|
+
const sigBytes = ethers.getBytes(sig);
|
|
345
|
+
const r = ethers.hexlify(sigBytes.slice(0, 32));
|
|
346
|
+
const s = ethers.hexlify(sigBytes.slice(32, 64));
|
|
347
|
+
const v = sigBytes[64];
|
|
348
|
+
|
|
349
|
+
const tx = await usdcContract.transferWithAuthorization(
|
|
350
|
+
fromAddr, toAddr, auth.value,
|
|
351
|
+
auth.validAfter, auth.validBefore, auth.nonce,
|
|
352
|
+
v, r, s,
|
|
353
|
+
{ gasLimit: 100000 }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Wait for confirmation (1 block)
|
|
357
|
+
const receipt = await tx.wait(1);
|
|
358
|
+
console.log(` 💰 x402 self-settled: ${receipt.hash} (${price} USDC)`);
|
|
222
359
|
|
|
223
|
-
// Signature valid — the user authorized this USDC transfer
|
|
224
360
|
return {
|
|
225
361
|
valid: true,
|
|
226
|
-
from:
|
|
227
|
-
receipt: { authorization: auth,
|
|
228
|
-
txHash:
|
|
362
|
+
from: fromAddr,
|
|
363
|
+
receipt: { authorization: auth, settled: "self", blockNumber: receipt.blockNumber },
|
|
364
|
+
txHash: receipt.hash,
|
|
229
365
|
};
|
|
230
|
-
}
|
|
231
|
-
|
|
366
|
+
} finally {
|
|
367
|
+
if (keyBuf) keyBuf.fill(0);
|
|
232
368
|
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.log(` ⚠️ x402 self-settlement failed: ${err.message}`);
|
|
371
|
+
return { valid: false, reason: "settlement_failed: " + err.message };
|
|
233
372
|
}
|
|
234
|
-
|
|
235
|
-
// Unknown format — try facilitator
|
|
236
|
-
return _verifyViaFacilitator(paymentSig, price);
|
|
237
373
|
}
|
|
238
374
|
|
|
239
|
-
|
|
375
|
+
function _logPayment(entry) {
|
|
240
376
|
try {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const req = client.request({
|
|
251
|
-
hostname: url.hostname, port: url.port, path: url.pathname, method: "POST",
|
|
252
|
-
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
253
|
-
timeout: 10000,
|
|
254
|
-
}, (res) => {
|
|
255
|
-
let data = "";
|
|
256
|
-
res.on("data", (c) => data += c);
|
|
257
|
-
res.on("end", () => {
|
|
258
|
-
try { const p = JSON.parse(data); resolve({ valid: p.valid || p.success, receipt: p, txHash: p.txHash }); }
|
|
259
|
-
catch { resolve({ valid: false }); }
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
req.on("error", () => resolve({ valid: false }));
|
|
263
|
-
req.write(body);
|
|
264
|
-
req.end();
|
|
265
|
-
});
|
|
266
|
-
} catch { return { valid: false }; }
|
|
377
|
+
const logPath = path.join(process.cwd(), ".wolverine", "x402-payments.json");
|
|
378
|
+
let payments = [];
|
|
379
|
+
try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
|
|
380
|
+
payments.push(entry);
|
|
381
|
+
// Keep last 1000 payments
|
|
382
|
+
if (payments.length > 1000) payments = payments.slice(-1000);
|
|
383
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
384
|
+
fs.writeFileSync(logPath, JSON.stringify(payments, null, 2));
|
|
385
|
+
} catch {}
|
|
267
386
|
}
|
|
268
387
|
|
|
269
388
|
x402Plugin[Symbol.for("skip-override")] = true;
|
|
@@ -5,30 +5,36 @@
|
|
|
5
5
|
"env": "development"
|
|
6
6
|
},
|
|
7
7
|
|
|
8
|
+
"_docs_models": "Each task wolverine performs uses a specific model role. You can mix providers freely — provider is auto-detected from the model name (claude-* → Anthropic, gpt-* → OpenAI). Using the same model across roles maximizes prompt cache hits and reduces cost. View per-role cost, speed, and token analytics on the wolverine dashboard at localhost:3001/analytics",
|
|
8
9
|
"models": {
|
|
10
|
+
"_docs": "reasoning: deep error analysis & root cause diagnosis | coding: generates code fixes (fast path, no tools) | chat: summaries & brain compression | tool: agent tool-calling during complex heals | classifier: error type classification | audit: security & injection scanning | compacting: context window compression | research: deep investigation & web research",
|
|
9
11
|
"reasoning": "claude-sonnet-4-6",
|
|
10
12
|
"coding": "claude-sonnet-4-6",
|
|
11
|
-
"chat": "
|
|
12
|
-
"tool": "
|
|
13
|
-
"classifier": "
|
|
14
|
-
"audit": "
|
|
15
|
-
"compacting": "
|
|
13
|
+
"chat": "claude-sonnet-4-6",
|
|
14
|
+
"tool": "claude-sonnet-4-6",
|
|
15
|
+
"classifier": "claude-sonnet-4-6",
|
|
16
|
+
"audit": "claude-sonnet-4-6",
|
|
17
|
+
"compacting": "claude-sonnet-4-6",
|
|
16
18
|
"research": "claude-sonnet-4-6"
|
|
17
19
|
},
|
|
18
20
|
|
|
19
|
-
"
|
|
21
|
+
"_docs_embedding": "Vector embedding model for the brain's memory system. Used to store and recall past fixes. text-embedding-3-small is the default (requires OPENAI_API_KEY). wolverine-embedding-1 routes through wolverine credits at 2x markup.",
|
|
22
|
+
"embedding": "text-embedding-3-small",
|
|
20
23
|
|
|
24
|
+
"_docs_server": "port: server listen port (must be 3000 for wolverine) | maxRetries: consecutive crash restarts before giving up | maxMemoryMB: OOM threshold — process killed and healed if exceeded",
|
|
21
25
|
"server": {
|
|
22
26
|
"port": 3000,
|
|
23
27
|
"maxRetries": 3,
|
|
24
28
|
"maxMemoryMB": 512
|
|
25
29
|
},
|
|
26
30
|
|
|
31
|
+
"_docs_telemetry": "Heartbeat reports server health (memory, CPU, routes, repair stats) to the dashboard every heartbeatIntervalMs. Disable if running fully offline.",
|
|
27
32
|
"telemetry": {
|
|
28
33
|
"enabled": true,
|
|
29
34
|
"heartbeatIntervalMs": 60000
|
|
30
35
|
},
|
|
31
36
|
|
|
37
|
+
"_docs_rateLimiting": "AI call budget to prevent runaway costs. maxCallsPerWindow: total AI calls allowed in windowMs. minGapMs: minimum delay between calls. maxTokensPerHour: hard token ceiling across all models.",
|
|
32
38
|
"rateLimiting": {
|
|
33
39
|
"maxCallsPerWindow": 32,
|
|
34
40
|
"windowMs": 100000,
|
|
@@ -36,6 +42,7 @@
|
|
|
36
42
|
"maxTokensPerHour": 1000000
|
|
37
43
|
},
|
|
38
44
|
|
|
45
|
+
"_docs_healthCheck": "Probes server liveness. intervalMs: check frequency. timeoutMs: max wait for response. failThreshold: consecutive failures before triggering heal. startDelayMs: grace period after boot before first check.",
|
|
39
46
|
"healthCheck": {
|
|
40
47
|
"intervalMs": 15000,
|
|
41
48
|
"timeoutMs": 5000,
|
|
@@ -43,14 +50,16 @@
|
|
|
43
50
|
"startDelayMs": 10000
|
|
44
51
|
},
|
|
45
52
|
|
|
53
|
+
"_docs_errorMonitor": "Tracks caught 500 errors per route. defaultThreshold: how many 500s on one route before triggering a heal. windowMs: error counting window. cooldownMs: minimum time between heals on the same route.",
|
|
46
54
|
"errorMonitor": {
|
|
47
55
|
"defaultThreshold": 1,
|
|
48
56
|
"windowMs": 30000,
|
|
49
57
|
"cooldownMs": 60000
|
|
50
58
|
},
|
|
51
59
|
|
|
60
|
+
"_docs_autoUpdate": "Auto-updates wolverine framework from git. Only updates src/ and bin/ — never touches your server/ code. Checks every intervalMs.",
|
|
52
61
|
"autoUpdate": {
|
|
53
|
-
"enabled":
|
|
62
|
+
"enabled": false,
|
|
54
63
|
"intervalMs": 300000
|
|
55
64
|
},
|
|
56
65
|
|
|
@@ -59,6 +68,7 @@
|
|
|
59
68
|
"cors": ["http://localhost:3000"]
|
|
60
69
|
},
|
|
61
70
|
|
|
71
|
+
"_docs_dashboard": "Dashboard serves on port+1 (default 3001). Configure WOLVERINE_ADMIN_KEY in .env.local for the agent command interface.",
|
|
62
72
|
"dashboard": {},
|
|
63
73
|
|
|
64
74
|
"cors": {
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo API routes — shows both free endpoints and x402 paid APIs.
|
|
3
|
+
*
|
|
4
|
+
* Wolverine's vault provides an encrypted Ethereum wallet (AES-256-GCM)
|
|
5
|
+
* for each server instance. The wallet is used as the payment receiver
|
|
6
|
+
* for x402 paid APIs on the Base network (USDC).
|
|
7
|
+
*
|
|
8
|
+
* Vault commands:
|
|
9
|
+
* wolverine --init-vault Create encrypted wallet
|
|
10
|
+
* wolverine --x402-info Show wallet address & config
|
|
11
|
+
*
|
|
12
|
+
* The vault is stored in .wolverine/vault/ and is:
|
|
13
|
+
* - Auto-backed up in every server snapshot
|
|
14
|
+
* - Protected from agent access (sandbox-blocked)
|
|
15
|
+
* - Private keys never exist as JS strings (Buffer only)
|
|
16
|
+
* - Rollback-protected (never overwritten by restore)
|
|
17
|
+
*
|
|
18
|
+
* Dashboard: localhost:3001 — view wallet balances, x402 revenue,
|
|
19
|
+
* payment history, and all server health metrics in real time.
|
|
20
|
+
*/
|
|
21
|
+
|
|
1
22
|
async function routes(fastify) {
|
|
23
|
+
// ── Free endpoints ──
|
|
2
24
|
fastify.get("/", async () => ({ message: "Hello from Wolverine API" }));
|
|
3
25
|
|
|
4
26
|
fastify.get("/users", async () => ({
|
|
@@ -7,6 +29,46 @@ async function routes(fastify) {
|
|
|
7
29
|
{ id: 2, name: "Bob", role: "user" },
|
|
8
30
|
],
|
|
9
31
|
}));
|
|
32
|
+
|
|
33
|
+
// ── x402 Paid API ──────────────────────────────────────────────
|
|
34
|
+
// Any route becomes a paid API by adding x402 config.
|
|
35
|
+
// Callers without a valid USDC payment get a 402 with payment instructions.
|
|
36
|
+
// Callers with a valid Payment-Signature header get the response.
|
|
37
|
+
//
|
|
38
|
+
// Protocol: https://docs.cdp.coinbase.com/x402/welcome
|
|
39
|
+
// Network: Base (eip155:8453) — USDC payments
|
|
40
|
+
// Wallet: Auto-detected from vault (wolverine --init-vault)
|
|
41
|
+
//
|
|
42
|
+
// To change pricing live without restart:
|
|
43
|
+
// Update the price in config and the next request picks it up.
|
|
44
|
+
// Or use the dashboard command: "change /api/premium price to $0.05"
|
|
45
|
+
|
|
46
|
+
// Fixed price — charge $0.01 per call:
|
|
47
|
+
fastify.get("/premium", {
|
|
48
|
+
config: { x402: { price: "$0.01", description: "Premium data endpoint" } },
|
|
49
|
+
}, async (req) => ({
|
|
50
|
+
data: "This response cost $0.01 in USDC on Base",
|
|
51
|
+
paid: req.x402?.amount,
|
|
52
|
+
from: req.x402?.from,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Variable price — caller chooses amount (e.g. buying credits):
|
|
56
|
+
// POST /api/purchase { "dollars": "5.00" }
|
|
57
|
+
fastify.post("/purchase", {
|
|
58
|
+
config: {
|
|
59
|
+
x402: {
|
|
60
|
+
variable: true,
|
|
61
|
+
min: "$1",
|
|
62
|
+
max: "$1000",
|
|
63
|
+
priceField: "dollars",
|
|
64
|
+
description: "Purchase API credits with USDC",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}, async (req) => ({
|
|
68
|
+
message: `Received ${req.x402?.amount} USDC payment`,
|
|
69
|
+
from: req.x402?.from,
|
|
70
|
+
receipt: req.x402?.receipt ? "valid" : "none",
|
|
71
|
+
}));
|
|
10
72
|
}
|
|
11
73
|
|
|
12
74
|
module.exports = routes;
|