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 CHANGED
@@ -7,9 +7,9 @@
7
7
  OPENAI_API_KEY=
8
8
  ANTHROPIC_API_KEY=
9
9
 
10
- # ── Wolverine Inference (self-hosted models) ─────────────────────
11
- # Get your API key at wolverinenode.xyz $1 = 100 credits
12
- # Set provider to "wolverine" in server/config/settings.json
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.0",
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": {
@@ -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. 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 verifies signature (EIP-3009 TransferWithAuthorization for USDC, or x402 facilitator) → req.x402 = { paid: true, amount: '$5.00', from: '0x...', txHash } → handler runs. Vault wallet auto-detected as payTo address. Wolverine's adaptive rate limiter still protects paid routes from overload. Analytics track paid vs free calls. CLI: wolverine --x402-info shows setup guide.",
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 verification is automatic — the handler only runs after USDC is confirmed. No blockchain code needed in route handlers.",
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
  {
@@ -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: "reasoning" });
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
@@ -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
 
@@ -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 &amp; 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> &middot; '+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 &middot; '+(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 → callback flow.
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
- let _facilitatorUrl = "https://x402.org/facilitator";
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 — verify via facilitator or direct signature check
134
- const verified = await _verifyPayment(paymentSig, price);
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
- reply.code(402).send({ error: "Payment verification failed", price, payTo: _payTo });
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
- async function _verifyPayment(paymentSig, price) {
155
- // Decode the payment signature (base64 JSON payload from frontend)
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
- // Not base64 might be raw x402 format, try facilitator
161
- return _verifyViaFacilitator(paymentSig, price);
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
- // Direct verification: validate the EIP-3009 TransferWithAuthorization signature
165
- if (payload.payload?.authorization && payload.payload?.signature) {
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
- // Verify the amount matches the price
170
- const expectedUsdc = Math.round(parseFloat(price.replace("$", "")) * 1e6);
171
- const actualUsdc = parseInt(auth.value, 16) || parseInt(auth.value, 10) || 0;
172
- if (actualUsdc < expectedUsdc * 0.99) { // 1% tolerance for rounding
173
- return { valid: false, reason: "Amount mismatch" };
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
- // Verify payTo matches
177
- if (auth.to?.toLowerCase() !== _payTo?.toLowerCase()) {
178
- return { valid: false, reason: "Wrong recipient" };
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
- // Verify not expired
182
- if (auth.validBefore && auth.validBefore < Math.floor(Date.now() / 1000)) {
183
- return { valid: false, reason: "Payment expired" };
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
- // Recover signer from EIP-712 typed data signature
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
- const { ethers } = require("ethers");
189
- // Normalize all addresses to proper EIP-55 checksum format
190
- const fromAddr = ethers.getAddress(auth.from.toLowerCase());
191
- const toAddr = ethers.getAddress(auth.to.toLowerCase());
192
-
193
- const domain = {
194
- name: "USD Coin",
195
- version: "2",
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
- const message = {
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
- const recoveredAddress = ethers.verifyTypedData(domain, types, message, sig);
219
- if (recoveredAddress.toLowerCase() !== fromAddr.toLowerCase()) {
220
- return { valid: false, reason: "Signature mismatch" };
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: auth.from,
227
- receipt: { authorization: auth, signature: sig, verified: "direct" },
228
- txHash: null, // on-chain tx happens when we call transferWithAuthorization
362
+ from: fromAddr,
363
+ receipt: { authorization: auth, settled: "self", blockNumber: receipt.blockNumber },
364
+ txHash: receipt.hash,
229
365
  };
230
- } catch (err) {
231
- return { valid: false, reason: "Signature verification error: " + err.message };
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
- async function _verifyViaFacilitator(paymentSig, price) {
375
+ function _logPayment(entry) {
240
376
  try {
241
- const https = require("https");
242
- const http = require("http");
243
- const url = new (require("url").URL)(_facilitatorUrl + "/verify");
244
- const body = JSON.stringify({
245
- paymentSignature: paymentSig,
246
- routeConfig: { accepts: [{ scheme: "exact", price, network: _network, payTo: _payTo }] },
247
- });
248
- return new Promise((resolve) => {
249
- const client = url.protocol === "https:" ? https : http;
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": "gpt-5.4-mini",
12
- "tool": "gpt-4o-mini",
13
- "classifier": "gpt-4o-mini",
14
- "audit": "wolverine-test-1",
15
- "compacting": "wolverine-test-1",
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
- "embedding": "wolverine-embedding-1",
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": true,
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;