wolverine-ai 4.8.0 → 5.0.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/bin/wolverine.js CHANGED
@@ -38,6 +38,7 @@ ${chalk.bold("Options:")}
38
38
  --workers <n> Force specific worker count
39
39
  --info Show system info and exit
40
40
  --init Scan server/ and build context map (routes, DB, config, deps)
41
+ --x402-price Set x402 price: wolverine --x402-price "POST /api" "$0.01"
41
42
 
42
43
  ${chalk.bold("Configuration:")}
43
44
  server/config/settings.json Models, telemetry, limits, health checks
@@ -66,6 +67,22 @@ if (args.includes("--info")) {
66
67
  }
67
68
 
68
69
  // --init: scan server/ and build context map
70
+ // --x402-price: set route pricing live
71
+ if (args.includes("--x402-price")) {
72
+ const idx = args.indexOf("--x402-price");
73
+ const route = args[idx + 1];
74
+ const price = args[idx + 2];
75
+ if (!route || !price) {
76
+ console.log(chalk.red(' Usage: wolverine --x402-price "POST /v1/chat/completions" "$0.001"'));
77
+ process.exit(1);
78
+ }
79
+ const { setPrice } = require("../src/middleware/x402-fastify");
80
+ const result = setPrice(route, price);
81
+ console.log(chalk.green(` ✅ x402 price updated: ${result.route} → ${result.price}`));
82
+ console.log(chalk.gray(" Change is live — no restart needed."));
83
+ process.exit(0);
84
+ }
85
+
69
86
  if (args.includes("--init")) {
70
87
  const { scan } = require("../src/core/server-context");
71
88
  console.log(chalk.blue("\n 🔍 Scanning server/ directory...\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "4.8.0",
3
+ "version": "5.0.0",
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": {
@@ -66,7 +66,8 @@
66
66
  "ethers": "^6.0.0",
67
67
  "ioredis": "^5.0.0",
68
68
  "pg": "^8.0.0",
69
- "stripe": "^18.0.0"
69
+ "stripe": "^18.0.0",
70
+ "viem": "^2.0.0"
70
71
  },
71
72
  "engines": {
72
73
  "node": ">=18.0.0"
@@ -145,6 +145,10 @@ const SEED_DOCS = [
145
145
  text: "Server context scanner (wolverine --init): scans server/ directory on every startup to build .wolverine/server-context.json. Extracts routes (HTTP methods + paths from fastify/express), middleware stack, database type + tables, config structure, dependencies, env vars used (process.env.X patterns), and full file tree. Context summary auto-injected into agent's heal prompt so it knows the server's route map, DB schema, and dependencies without re-scanning. Manual scan: wolverine --init. Auto-scan: runs silently on every boot. The context is read-only — never modified by the agent.",
146
146
  metadata: { topic: "server-context" },
147
147
  },
148
+ {
149
+ text: "Tool router (src/brain/tool-router.js): maps error types to recommended tool chains. Injected into agent prompt automatically — agent sees 'TOOL ROUTE: diagnose with [X, Y], fix with [Z]. Strategy: ...' for every error it heals. 25+ error categories mapped: TypeError→read_file+edit_file, ENOENT→list_dir+write_file, ECONNREFUSED→check_network+check_port+inspect_cache, EMFILE→check_file_descriptors+disk_cleanup, ENOSPC→disk_cleanup, certificate→inspect_certificate, OOM→check_memory+check_event_loop, websocket���check_websocket, database→inspect_db+run_db_fix (with prereq: inspect_db FIRST), missing_module→audit_deps+verify_node_modules, env_missing→inspect_env+add_env_var. Lookup is O(1) by error type + regex fallback. No AI tokens used for routing.",
150
+ metadata: { topic: "tool-router" },
151
+ },
148
152
  {
149
153
  text: "Telemetry architecture: 4 files, ~250 lines total. heartbeat.js sends one HTTP POST every 60s (5s timeout, non-blocking). register.js auto-registers and caches key in memory + disk. queue.js appends to JSONL file only on failure, trims lazily. telemetry.js collects from subsystems using optional chaining (no crashes if subsystem missing). All secrets redacted before sending. Response bodies drained immediately (res.resume). No blocking, no delays, no busy waits.",
150
154
  metadata: { topic: "telemetry-architecture" },
@@ -186,8 +190,8 @@ const SEED_DOCS = [
186
190
  metadata: { topic: "notifications" },
187
191
  },
188
192
  {
189
- text: "Vault: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key (32 bytes raw) encrypts eth.vault (Ethereum private key). Generated on first run if missing. Private key NEVER exists as a JS string — only Buffer, wiped after use. wallet-ops.js exposes getWalletAddress(), signTransaction(), signMessage() all decrypt→use→wipe with generic error messages only. Injection detector blocks heal if 0x+64hex chars detected in error (key_leak_critical). Redactor scrubs all hex key patterns. Sandbox blocks agent access to vault paths. Backed up in every snapshot. Rollback-protected (NEVER_ROLLBACK list). Vault skill in src/skills/vault.js for agent discovery. Server code uses vault skill to earn/spend ETH without touching the private key.",
190
- metadata: { topic: "vault-security" },
193
+ text: "Vault + x402 payments: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key encrypts eth.vault (Ethereum private key). Generated on first run. Private key NEVER as JS string — Buffer only, wiped after use. wallet-ops: getWalletAddress(), signTransaction(), signMessage(). x402 protocol: HTTP 402 Payment Required for API monetization. Fastify middleware (src/middleware/x402-fastify.js) auto-gates routes with USDC payments on Base network. Config in settings.json x402.routes: { 'POST /api': { price: '$0.01' } }. Live price updates: PUT /x402/price or wolverine --x402-price 'POST /api' '$0.01'. Vault wallet is the payTo address. Facilitator (x402.org or Coinbase CDP) verifies and settles payments. Buyer SDKs (@x402/fetch, @x402/axios) auto-handle the 402→sign→retry flow. Vault skill actions: x402_pricing, x402_set_price, x402_remove_price.",
194
+ metadata: { topic: "vault-x402" },
191
195
  },
192
196
  {
193
197
  text: "MCP integration: connect external tools via Model Context Protocol. Configure in .wolverine/mcp.json with per-server tool allowlists. Security: arg sanitization (secrets redacted before sending to MCP servers), result injection scanning, rate limiting per server, audit logging. Tools appear as mcp__server__tool in the agent. Supports stdio and HTTP transports.",
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Tool Router — maps error types to recommended tool chains.
3
+ *
4
+ * Lightweight adjacency graph that tells the agent which tools to use
5
+ * for each error category. Injected into the agent prompt so it doesn't
6
+ * waste turns guessing.
7
+ *
8
+ * Structure per error type:
9
+ * diagnose: tools to understand the problem (read-only)
10
+ * fix: tools to apply the solution (write)
11
+ * hint: one-line strategy for the AI
12
+ * prereq: tools that must run BEFORE fix tools
13
+ *
14
+ * Lookup: O(1) by error classifier output or regex match on error message.
15
+ */
16
+
17
+ const TOOL_ROUTES = {
18
+ // ── Code Errors ──
19
+ "TypeError": {
20
+ diagnose: ["read_file", "grep_code"],
21
+ fix: ["edit_file"],
22
+ hint: "Read the file at the error line. Find the undefined/null variable. Add a guard or fix the assignment.",
23
+ },
24
+ "ReferenceError": {
25
+ diagnose: ["read_file", "grep_code"],
26
+ fix: ["edit_file"],
27
+ hint: "Variable is not defined. Check for typos, missing imports, or scope issues.",
28
+ },
29
+ "SyntaxError": {
30
+ diagnose: ["read_file"],
31
+ fix: ["edit_file"],
32
+ hint: "Parse error in source. Read the file and fix the syntax at the reported line.",
33
+ },
34
+ "RangeError": {
35
+ diagnose: ["read_file", "check_memory"],
36
+ fix: ["edit_file"],
37
+ hint: "Stack overflow or invalid array length. Check for infinite recursion or unbounded growth.",
38
+ },
39
+
40
+ // ── Module/Dependency Errors ──
41
+ "missing_module": {
42
+ diagnose: ["audit_deps", "verify_node_modules"],
43
+ fix: ["bash_exec"],
44
+ hint: "Run npm install <module>. If in package.json but missing from disk, run npm ci.",
45
+ },
46
+ "missing_file": {
47
+ diagnose: ["read_file", "list_dir", "inspect_env"],
48
+ fix: ["write_file", "bash_exec"],
49
+ hint: "Check if the path is wrong (edit require) or if the file should exist (create it with expected content).",
50
+ },
51
+ "version_conflict": {
52
+ diagnose: ["audit_deps", "check_migration", "verify_node_modules"],
53
+ fix: ["bash_exec"],
54
+ hint: "Check peer deps and version compatibility. May need npm install package@version or full reinstall.",
55
+ },
56
+
57
+ // ── File System Errors ──
58
+ "ENOENT": {
59
+ diagnose: ["list_dir", "inspect_env", "read_file"],
60
+ fix: ["write_file", "bash_exec"],
61
+ prereq: ["read_file"],
62
+ hint: "File or directory doesn't exist. Check if path is correct, create if missing.",
63
+ },
64
+ "EACCES": {
65
+ diagnose: ["bash_exec", "list_dir"],
66
+ fix: ["bash_exec"],
67
+ hint: "Permission denied. Run chmod to fix permissions on the target file/directory.",
68
+ },
69
+ "ENOSPC": {
70
+ diagnose: ["disk_cleanup", "check_memory"],
71
+ fix: ["disk_cleanup"],
72
+ hint: "Disk full. Run disk_cleanup with dry_run=false to clear old backups and caches.",
73
+ },
74
+ "EMFILE": {
75
+ diagnose: ["check_file_descriptors", "list_processes", "check_event_loop"],
76
+ fix: ["disk_cleanup", "restart_service"],
77
+ hint: "Too many open files. Check for FD leaks, clear caches, consider raising ulimit in system profile.",
78
+ },
79
+
80
+ // ── Network Errors ──
81
+ "ECONNREFUSED": {
82
+ diagnose: ["check_network", "check_port", "inspect_cache", "inspect_env"],
83
+ fix: ["edit_file", "restart_service"],
84
+ hint: "Target service is down or wrong host/port. Check config, verify service is running.",
85
+ },
86
+ "ECONNRESET": {
87
+ diagnose: ["check_network", "check_logs"],
88
+ fix: ["edit_file"],
89
+ hint: "Connection reset by remote. Check for timeout settings, proxy config, or unstable network.",
90
+ },
91
+ "ETIMEDOUT": {
92
+ diagnose: ["check_network", "check_logs", "inspect_certificate"],
93
+ fix: ["edit_file"],
94
+ hint: "Connection timed out. Increase timeout, check DNS, verify endpoint is reachable.",
95
+ },
96
+ "EADDRINUSE": {
97
+ diagnose: ["check_port", "list_processes"],
98
+ fix: ["bash_exec"],
99
+ hint: "Port already in use. Find and kill the stale process holding the port.",
100
+ },
101
+
102
+ // ── Database Errors ──
103
+ "database": {
104
+ diagnose: ["inspect_db", "inspect_cache", "read_file"],
105
+ fix: ["run_db_fix", "edit_file"],
106
+ prereq: ["inspect_db"],
107
+ hint: "ALWAYS inspect_db before run_db_fix. Check table schema, query the affected data, then fix.",
108
+ },
109
+ "pool_exhaustion": {
110
+ diagnose: ["inspect_cache", "check_network", "check_logs"],
111
+ fix: ["edit_file", "restart_service"],
112
+ hint: "DB connection pool exhausted. Check for connection leaks, increase pool size, or restart.",
113
+ },
114
+
115
+ // ── SSL/TLS Errors ──
116
+ "certificate": {
117
+ diagnose: ["inspect_certificate", "check_network", "inspect_env"],
118
+ fix: ["bash_exec", "edit_file", "add_env_var"],
119
+ hint: "Check cert expiry, SAN list, and chain. May need renewal, hostname fix, or CA bundle.",
120
+ },
121
+
122
+ // ── Memory/Performance Errors ──
123
+ "OOM": {
124
+ diagnose: ["check_memory", "check_event_loop", "list_processes"],
125
+ fix: ["edit_file", "restart_service"],
126
+ hint: "Out of memory. Check for memory leaks (growing arrays, unclosed streams), reduce batch sizes.",
127
+ },
128
+ "event_loop_blocked": {
129
+ diagnose: ["check_event_loop", "check_logs", "check_memory"],
130
+ fix: ["edit_file"],
131
+ hint: "Synchronous operation blocking event loop. Replace readFileSync→readFile, execSync→exec, etc.",
132
+ },
133
+
134
+ // ── WebSocket Errors ──
135
+ "websocket": {
136
+ diagnose: ["check_websocket", "check_network", "check_port"],
137
+ fix: ["edit_file"],
138
+ hint: "Test WS handshake. Check upgrade headers, proxy config, and connection timeout settings.",
139
+ },
140
+
141
+ // ── Config/Environment Errors ──
142
+ "config": {
143
+ diagnose: ["read_file", "inspect_env", "list_dir"],
144
+ fix: ["write_file", "edit_file", "add_env_var"],
145
+ hint: "Check if config file exists and has expected structure. Check if required env vars are set.",
146
+ },
147
+ "env_missing": {
148
+ diagnose: ["inspect_env", "read_file"],
149
+ fix: ["add_env_var"],
150
+ hint: "Required environment variable not set. Add it to .env.local with the correct value.",
151
+ },
152
+ };
153
+
154
+ /**
155
+ * Find tool recommendations for an error.
156
+ * @param {string} errorMessage — the raw error message
157
+ * @param {string} errorType — from error-parser classifyError() (optional)
158
+ * @returns {{ diagnose: string[], fix: string[], prereq?: string[], hint: string } | null}
159
+ */
160
+ function route(errorMessage, errorType) {
161
+ const msg = (errorMessage || "").toLowerCase();
162
+
163
+ // 1. Try exact error type match
164
+ if (errorType && TOOL_ROUTES[errorType]) return TOOL_ROUTES[errorType];
165
+
166
+ // 2. Try error class match
167
+ if (/typeerror/i.test(msg)) return TOOL_ROUTES.TypeError;
168
+ if (/referenceerror/i.test(msg)) return TOOL_ROUTES.ReferenceError;
169
+ if (/syntaxerror|unexpected token/i.test(msg)) return TOOL_ROUTES.SyntaxError;
170
+ if (/rangeerror/i.test(msg)) return TOOL_ROUTES.RangeError;
171
+
172
+ // 3. Try errno/code match
173
+ if (/enoent|no such file/i.test(msg)) return TOOL_ROUTES.ENOENT;
174
+ if (/eacces|eperm/i.test(msg)) return TOOL_ROUTES.EACCES;
175
+ if (/enospc/i.test(msg)) return TOOL_ROUTES.ENOSPC;
176
+ if (/emfile|enfile/i.test(msg)) return TOOL_ROUTES.EMFILE;
177
+ if (/econnrefused/i.test(msg)) return TOOL_ROUTES.ECONNREFUSED;
178
+ if (/econnreset/i.test(msg)) return TOOL_ROUTES.ECONNRESET;
179
+ if (/etimedout/i.test(msg)) return TOOL_ROUTES.ETIMEDOUT;
180
+ if (/eaddrinuse/i.test(msg)) return TOOL_ROUTES.EADDRINUSE;
181
+
182
+ // 4. Try pattern match
183
+ if (/cannot find module/i.test(msg)) return /['"][./\\]/.test(msg) ? TOOL_ROUTES.missing_file : TOOL_ROUTES.missing_module;
184
+ if (/cert|ssl|tls|self.signed/i.test(msg)) return TOOL_ROUTES.certificate;
185
+ if (/pool.*exhaust|pool.*timeout|acquire.*timeout/i.test(msg)) return TOOL_ROUTES.pool_exhaustion;
186
+ if (/websocket|ws.*close|transport.*close/i.test(msg)) return TOOL_ROUTES.websocket;
187
+ if (/out of memory|heap.*limit|allocation.*failed/i.test(msg)) return TOOL_ROUTES.OOM;
188
+ if (/not.set|undefined.*env|missing.*env/i.test(msg)) return TOOL_ROUTES.env_missing;
189
+ if (/missing.*config|invalid.*json|invalid.*config/i.test(msg)) return TOOL_ROUTES.config;
190
+ if (/sqlite|postgres|mysql|mongo|sequelize|prisma|knex/i.test(msg)) return TOOL_ROUTES.database;
191
+ if (/peer dep|eresolve|version.*mismatch/i.test(msg)) return TOOL_ROUTES.version_conflict;
192
+
193
+ return null; // unknown — let the AI figure it out
194
+ }
195
+
196
+ /**
197
+ * Get a compact prompt injection for the agent.
198
+ * Returns a string like "TOOL ROUTE: diagnose with [check_port, list_processes], fix with [bash_exec]. Hint: ..."
199
+ */
200
+ function getRoutePrompt(errorMessage, errorType) {
201
+ const r = route(errorMessage, errorType);
202
+ if (!r) return "";
203
+ const parts = [`TOOL ROUTE for this error:`];
204
+ parts.push(` Diagnose: ${r.diagnose.join(", ")}`);
205
+ if (r.prereq) parts.push(` Prerequisites: ${r.prereq.join(", ")} (run these FIRST)`);
206
+ parts.push(` Fix: ${r.fix.join(", ")}`);
207
+ parts.push(` Strategy: ${r.hint}`);
208
+ return parts.join("\n");
209
+ }
210
+
211
+ module.exports = { route, getRoutePrompt, TOOL_ROUTES };
@@ -13,6 +13,7 @@ const { RateLimiter } = require("../security/rate-limiter");
13
13
  const { detectInjection } = require("../security/injection-detector");
14
14
  const { redact, hasSecrets } = require("../security/secret-redactor");
15
15
  const { BackupManager } = require("../backup/backup-manager");
16
+ const { getRoutePrompt } = require("../brain/tool-router");
16
17
  const { AgentEngine } = require("../agent/agent-engine");
17
18
  const { ResearchAgent } = require("../agent/research-agent");
18
19
  const { GoalLoop } = require("../agent/goal-loop");
@@ -215,6 +216,12 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
215
216
  const serverCtx = getServerContextSummary(cwd);
216
217
  if (serverCtx) brainContext += serverCtx + "\n\n";
217
218
  } catch {}
219
+ // Inject tool routing — tells agent exactly which tools to use for this error type
220
+ const toolRoute = getRoutePrompt(parsed.errorMessage, parsed.errorType);
221
+ if (toolRoute) {
222
+ brainContext += toolRoute + "\n\n";
223
+ console.log(chalk.gray(` 🗺️ Tool route: ${toolRoute.split("\n")[1]?.trim() || "matched"}`));
224
+ }
218
225
  // Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
219
226
  if (skills) {
220
227
  const skillCtx = skills.buildContext(parsed.errorMessage);
@@ -0,0 +1,290 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+
4
+ /**
5
+ * x402 Payment Middleware for Fastify — monetize any API route with USDC on Base.
6
+ *
7
+ * Implements the x402 protocol (HTTP 402 Payment Required) for Fastify.
8
+ * No official @x402/fastify exists, so we build directly on @x402/core.
9
+ *
10
+ * Usage in server/index.js:
11
+ * fastify.register(require('wolverine-ai/src/middleware/x402-fastify'), {
12
+ * payTo: '0xYourAddress', // or auto from vault
13
+ * network: 'eip155:8453', // Base mainnet
14
+ * });
15
+ *
16
+ * Route pricing in settings.json:
17
+ * "x402": {
18
+ * "enabled": true,
19
+ * "routes": {
20
+ * "POST /v1/chat/completions": { "price": "$0.001" },
21
+ * "GET /api/premium": { "price": "$0.01" }
22
+ * }
23
+ * }
24
+ *
25
+ * Live price updates:
26
+ * PUT /x402/price { route: "POST /v1/chat/completions", price: "$0.002" }
27
+ * wolverine --x402-price "POST /v1/chat/completions" "$0.002"
28
+ */
29
+
30
+ // In-memory route pricing — survives hot updates without restart
31
+ let _routePricing = {};
32
+ let _payTo = null;
33
+ let _network = "eip155:8453"; // Base mainnet default
34
+ let _facilitatorUrl = "https://x402.org/facilitator";
35
+ let _initialized = false;
36
+
37
+ /**
38
+ * Get current route pricing (for external access).
39
+ */
40
+ function getPricing() { return { ..._routePricing }; }
41
+
42
+ /**
43
+ * Update a single route's price live — no restart needed.
44
+ */
45
+ function setPrice(routeKey, price) {
46
+ if (!price.startsWith("$")) price = "$" + price;
47
+ _routePricing[routeKey] = { price };
48
+ // Persist to settings.json
49
+ _persistPricing();
50
+ return { route: routeKey, price };
51
+ }
52
+
53
+ /**
54
+ * Remove pricing from a route (make it free).
55
+ */
56
+ function removePrice(routeKey) {
57
+ delete _routePricing[routeKey];
58
+ _persistPricing();
59
+ return { route: routeKey, price: "free" };
60
+ }
61
+
62
+ function _persistPricing() {
63
+ try {
64
+ const settingsPath = path.join(process.cwd(), "server", "config", "settings.json");
65
+ if (fs.existsSync(settingsPath)) {
66
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
67
+ if (!settings.x402) settings.x402 = {};
68
+ settings.x402.routes = {};
69
+ for (const [route, cfg] of Object.entries(_routePricing)) {
70
+ settings.x402.routes[route] = { price: cfg.price };
71
+ }
72
+ const tmp = settingsPath + ".tmp";
73
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), "utf-8");
74
+ fs.renameSync(tmp, settingsPath);
75
+ }
76
+ } catch {}
77
+ }
78
+
79
+ /**
80
+ * Fastify plugin — registers x402 payment middleware.
81
+ */
82
+ async function x402Plugin(fastify, opts) {
83
+ // Load config
84
+ _payTo = opts.payTo || null;
85
+ _network = opts.network || "eip155:8453";
86
+ _facilitatorUrl = opts.facilitator || "https://x402.org/facilitator";
87
+
88
+ // Auto-detect payTo from vault if not provided
89
+ if (!_payTo) {
90
+ try {
91
+ const { getWalletAddress } = require("../vault/wallet-ops");
92
+ _payTo = await getWalletAddress();
93
+ } catch {}
94
+ }
95
+
96
+ // Load route pricing from settings.json
97
+ try {
98
+ const settingsPath = path.join(process.cwd(), "server", "config", "settings.json");
99
+ if (fs.existsSync(settingsPath)) {
100
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
101
+ if (settings.x402?.routes) {
102
+ for (const [route, cfg] of Object.entries(settings.x402.routes)) {
103
+ _routePricing[route] = { price: cfg.price };
104
+ }
105
+ }
106
+ if (settings.x402?.network) _network = settings.x402.network;
107
+ if (settings.x402?.facilitator) _facilitatorUrl = settings.x402.facilitator;
108
+ }
109
+ } catch {}
110
+
111
+ if (!_payTo) {
112
+ console.log(" ⚠️ x402: no payTo address (set in settings.json or init vault)");
113
+ return;
114
+ }
115
+
116
+ _initialized = true;
117
+ const routeCount = Object.keys(_routePricing).length;
118
+ if (routeCount > 0) {
119
+ console.log(` 💰 x402: ${routeCount} paid route(s), receiving at ${_payTo.slice(0, 6)}...${_payTo.slice(-4)}`);
120
+ }
121
+
122
+ // ── Main payment gate — onRequest hook ──
123
+ fastify.addHook("onRequest", async (request, reply) => {
124
+ if (!_initialized || Object.keys(_routePricing).length === 0) return;
125
+
126
+ const routeKey = `${request.method} ${request.url.split("?")[0]}`;
127
+ const routeConfig = _routePricing[routeKey];
128
+ if (!routeConfig) return; // Free route
129
+
130
+ const paymentSig = request.headers["payment-signature"];
131
+
132
+ // No payment — return 402 with payment instructions
133
+ if (!paymentSig) {
134
+ const paymentRequired = {
135
+ accepts: [{
136
+ scheme: "exact",
137
+ price: routeConfig.price,
138
+ network: _network,
139
+ payTo: _payTo,
140
+ }],
141
+ description: `Payment required for ${routeKey}`,
142
+ mimeType: "application/json",
143
+ };
144
+
145
+ reply
146
+ .code(402)
147
+ .header("Payment-Required", JSON.stringify(paymentRequired))
148
+ .header("X-402-Version", "1.0")
149
+ .send({
150
+ error: "Payment Required",
151
+ price: routeConfig.price,
152
+ network: _network,
153
+ payTo: _payTo,
154
+ protocol: "x402",
155
+ });
156
+ return;
157
+ }
158
+
159
+ // Payment present — verify via facilitator
160
+ try {
161
+ const verified = await _verifyPayment(paymentSig, routeConfig);
162
+ if (verified.valid) {
163
+ // Payment good — add receipt header and continue
164
+ reply.header("Payment-Response", JSON.stringify(verified.receipt || {}));
165
+ request.x402 = { paid: true, amount: routeConfig.price, txHash: verified.txHash };
166
+ return; // continue to route handler
167
+ }
168
+ } catch {}
169
+
170
+ // Verification failed
171
+ reply.code(402).send({
172
+ error: "Payment verification failed",
173
+ price: routeConfig.price,
174
+ network: _network,
175
+ payTo: _payTo,
176
+ });
177
+ });
178
+
179
+ // ── Live price management API ──
180
+ fastify.put("/x402/price", async (request, reply) => {
181
+ // Admin only
182
+ const token = request.headers.authorization?.replace("Bearer ", "");
183
+ let settings = {};
184
+ try { settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), "server", "config", "settings.json"), "utf-8")); } catch {}
185
+ if (token !== settings.platform?.apiKey) {
186
+ return reply.code(403).send({ error: "Admin only" });
187
+ }
188
+
189
+ const { route, price } = request.body || {};
190
+ if (!route || !price) return reply.code(400).send({ error: "route and price required" });
191
+ const result = setPrice(route, price);
192
+ return { updated: true, ...result };
193
+ });
194
+
195
+ fastify.delete("/x402/price", async (request, reply) => {
196
+ const token = request.headers.authorization?.replace("Bearer ", "");
197
+ let settings = {};
198
+ try { settings = JSON.parse(fs.readFileSync(path.join(process.cwd(), "server", "config", "settings.json"), "utf-8")); } catch {}
199
+ if (token !== settings.platform?.apiKey) {
200
+ return reply.code(403).send({ error: "Admin only" });
201
+ }
202
+
203
+ const { route } = request.body || {};
204
+ if (!route) return reply.code(400).send({ error: "route required" });
205
+ return removePrice(route);
206
+ });
207
+
208
+ fastify.get("/x402/pricing", async () => {
209
+ return {
210
+ payTo: _payTo,
211
+ network: _network,
212
+ routes: _routePricing,
213
+ };
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Verify a payment signature via the facilitator.
219
+ */
220
+ async function _verifyPayment(paymentSig, routeConfig) {
221
+ try {
222
+ // Try @x402/core facilitator client if available
223
+ const { HTTPFacilitatorClient } = require("@x402/core/server");
224
+ const { ExactEvmScheme } = require("@x402/evm/exact/server");
225
+
226
+ const facilitator = new HTTPFacilitatorClient({ url: _facilitatorUrl });
227
+ const result = await facilitator.verify({
228
+ paymentSignature: paymentSig,
229
+ routeConfig: {
230
+ accepts: [{
231
+ scheme: "exact",
232
+ price: routeConfig.price,
233
+ network: _network,
234
+ payTo: _payTo,
235
+ }],
236
+ },
237
+ });
238
+ return { valid: result.valid, receipt: result.receipt, txHash: result.txHash };
239
+ } catch {
240
+ // Fallback: manual HTTP verification to facilitator
241
+ try {
242
+ const https = require("https");
243
+ const http = require("http");
244
+ const url = new (require("url").URL)(_facilitatorUrl + "/verify");
245
+ const body = JSON.stringify({
246
+ paymentSignature: paymentSig,
247
+ routeConfig: {
248
+ accepts: [{
249
+ scheme: "exact",
250
+ price: routeConfig.price,
251
+ network: _network,
252
+ payTo: _payTo,
253
+ }],
254
+ },
255
+ });
256
+
257
+ return new Promise((resolve) => {
258
+ const client = url.protocol === "https:" ? https : http;
259
+ const req = client.request({
260
+ hostname: url.hostname,
261
+ port: url.port,
262
+ path: url.pathname,
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
265
+ timeout: 10000,
266
+ }, (res) => {
267
+ let data = "";
268
+ res.on("data", (c) => data += c);
269
+ res.on("end", () => {
270
+ try {
271
+ const parsed = JSON.parse(data);
272
+ resolve({ valid: parsed.valid || parsed.success, receipt: parsed, txHash: parsed.txHash });
273
+ } catch { resolve({ valid: false }); }
274
+ });
275
+ });
276
+ req.on("error", () => resolve({ valid: false }));
277
+ req.on("timeout", () => { req.destroy(); resolve({ valid: false }); });
278
+ req.write(body);
279
+ req.end();
280
+ });
281
+ } catch { return { valid: false }; }
282
+ }
283
+ }
284
+
285
+ x402Plugin[Symbol.for("skip-override")] = true; // Fastify plugin compat
286
+
287
+ module.exports = x402Plugin;
288
+ module.exports.getPricing = getPricing;
289
+ module.exports.setPrice = setPrice;
290
+ module.exports.removePrice = removePrice;
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  const SKILL_NAME = "vault";
13
- const SKILL_DESCRIPTION = "Secure Ethereum wallet — sign transactions, get address, check balance. Private key encrypted at rest, never exposed in code or errors.";
14
- const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer"];
13
+ const SKILL_DESCRIPTION = "Secure Ethereum wallet + x402 payments — sign transactions, get address, manage paid API routes. Private key encrypted at rest via AES-256-GCM, never exposed in code or errors. x402 support: set prices on routes, receive USDC on Base network.";
14
+ const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer", "x402", "payment", "usdc", "base", "price", "paid", "api"];
15
15
  const SKILL_USAGE = `
16
16
  vault.status() — check if vault is initialized, show address
17
17
  vault.address() — get the wallet's Ethereum address
@@ -61,8 +61,31 @@ async function execute(action, params = {}) {
61
61
  return { signedTransaction: signed };
62
62
  }
63
63
 
64
+ // x402 payment actions
65
+ case "x402_pricing":
66
+ try {
67
+ const { getPricing } = require("../middleware/x402-fastify");
68
+ return getPricing();
69
+ } catch { return { error: "x402 middleware not loaded" }; }
70
+
71
+ case "x402_set_price": {
72
+ if (!params.route || !params.price) return { error: "route and price required" };
73
+ try {
74
+ const { setPrice } = require("../middleware/x402-fastify");
75
+ return setPrice(params.route, params.price);
76
+ } catch { return { error: "x402 middleware not loaded" }; }
77
+ }
78
+
79
+ case "x402_remove_price": {
80
+ if (!params.route) return { error: "route required" };
81
+ try {
82
+ const { removePrice } = require("../middleware/x402-fastify");
83
+ return removePrice(params.route);
84
+ } catch { return { error: "x402 middleware not loaded" }; }
85
+ }
86
+
64
87
  default:
65
- return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key` };
88
+ return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key, x402_pricing, x402_set_price, x402_remove_price` };
66
89
  }
67
90
  }
68
91