@quackai/q402-mcp 0.5.5 → 0.5.6

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.
Files changed (3) hide show
  1. package/README.md +60 -65
  2. package/dist/index.js +349 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,95 +15,89 @@ AI agents — Claude (Desktop / Code), OpenAI Codex CLI, Cursor, Cline, and any
15
15
 
16
16
  ## Quick start
17
17
 
18
- The server speaks stdio MCP, so any MCP-compatible client can use it. The two paths verified end-to-end today are **Claude (Desktop / Code)** and **OpenAI Codex CLI**.
18
+ Two steps:
19
19
 
20
- ### Claude Desktop / Claude Code
20
+ 1. Register the MCP server with your client (one-line install per client).
21
+ 2. Open your client and say: **"Set up Q402"**. The agent calls `q402_doctor` which walks you through creating a single secrets file at `~/.q402/mcp.env` and pasting in your API key + private key. Nothing else.
21
22
 
22
- ```bash
23
- claude mcp add q402 -- npx -y @quackai/q402-mcp
24
- ```
23
+ ### 1. Register the server
25
24
 
26
- Or edit `claude_desktop_config.json` directly:
27
-
28
- ```json
29
- {
30
- "mcpServers": {
31
- "q402": {
32
- "command": "npx",
33
- "args": ["-y", "@quackai/q402-mcp"]
34
- }
35
- }
36
- }
37
- ```
25
+ | Client | Command / config |
26
+ |---|---|
27
+ | **Claude Desktop / Claude Code** | `claude mcp add q402 -- npx -y @quackai/q402-mcp` |
28
+ | **OpenAI Codex CLI** | `codex mcp add q402 -- npx -y @quackai/q402-mcp` |
29
+ | **Cursor** | Add to `~/.cursor/mcp.json`: `{ "mcpServers": { "q402": { "command": "npx", "args": ["-y", "@quackai/q402-mcp"] } } }` |
30
+ | **Cline** | Cline → Settings → MCP Servers → Edit JSON. Same shape as Cursor. |
31
+ | **Any other stdio MCP client** | Point it at `npx -y @quackai/q402-mcp`. No client-specific code. |
38
32
 
39
- Restart Claude Desktop and ask:
33
+ That's it secrets are NOT configured here. The MCP server reads them from `~/.q402/mcp.env` at startup (same pattern as AWS CLI / Stripe CLI / gh CLI), so every client uses the same file with no per-client wiring.
40
34
 
41
- > *"Compare gas costs to send 50 USDC to vitalik.eth across all 9 Q402 chains."*
35
+ ### 2. First-time setup
42
36
 
43
- ### OpenAI Codex CLI
37
+ Restart your client, then ask your agent:
44
38
 
45
- Three install paths — pick the one that matches your workflow.
39
+ > *"Set up Q402"*
46
40
 
47
- **(a) Codex plugin marketplace** (recommended bundles the MCP config so users don't write TOML):
41
+ The agent calls `q402_doctor`. On first install, the tool tells the agent to:
48
42
 
49
- ```bash
50
- codex plugin marketplace add bitgett/q402-mcp
51
- codex /plugins # browse and install "q402"
52
- ```
43
+ 1. Offer to create `~/.q402/mcp.env` (placeholder values only)
44
+ 2. Open the file in your editor (`code` / `open` / `start` / `xdg-open`)
45
+ 3. Walk you through pasting in (a) your API key from <https://q402.quackai.ai/event> (free Trial) or <https://q402.quackai.ai/payment> (paid Multichain), and (b) your wallet private key — **into the file, not into chat**
46
+ 4. Restart the client and run `q402_doctor` again to verify
53
47
 
54
- This repo carries a Codex plugin manifest at [`.codex-plugin/plugin.json`](./.codex-plugin/plugin.json) and a marketplace catalog at [`.agents/plugins/marketplace.json`](./.agents/plugins/marketplace.json), so any signed-in Codex user can register it as a marketplace source and install with one click.
48
+ 🔒 **Q402 never asks you to paste your private key into chat.** The MCP server signs payments LOCALLY on your machine your key never leaves your device.
55
49
 
56
- **(b) Single MCP server via `codex mcp add`** (no plugin wrapper — just register the stdio server):
50
+ ### Manual setup (no AI)
51
+
52
+ Create `~/.q402/mcp.env` yourself:
57
53
 
58
54
  ```bash
59
- codex mcp add q402 -- npx -y @quackai/q402-mcp
55
+ # ~/.q402/mcp.env
56
+ # Pick ONE of these:
57
+ Q402_TRIAL_API_KEY=q402_live_...
58
+ # Q402_MULTICHAIN_API_KEY=q402_live_...
59
+
60
+ Q402_PRIVATE_KEY=0x...
61
+ Q402_ENABLE_REAL_PAYMENTS=1
62
+ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
63
+
64
+ # Optional safety guards:
65
+ # Q402_MAX_AMOUNT_PER_CALL=5
66
+ # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
60
67
  ```
61
68
 
62
- **(c) Direct `~/.codex/config.toml` edit** (`.codex/config.toml` for per-project scope):
69
+ Then `chmod 600 ~/.q402/mcp.env` (Unix) and restart your client. That's the full configuration.
63
70
 
64
- ```toml
65
- [mcp_servers.q402]
66
- command = "npx"
67
- args = ["-y", "@quackai/q402-mcp"]
68
- startup_timeout_sec = 20.0
69
- ```
71
+ ### Advanced — explicit env injection
72
+
73
+ If you'd rather skip the file and inject env vars yourself (e.g. via Codex `env_vars` allow-list, a secrets manager, or shell exports), the server falls through to `process.env` — and `process.env` wins over file values on conflicts. So existing shell-export setups keep working unchanged.
70
74
 
71
- To enable real on-chain payments, pass the three live-mode env vars **explicitly** under `env` — Codex does **not** forward host env vars by default:
75
+ <details>
76
+ <summary>Codex <code>env_vars</code> allow-list example</summary>
72
77
 
73
78
  ```toml
74
79
  [mcp_servers.q402]
75
80
  command = "npx"
76
81
  args = ["-y", "@quackai/q402-mcp"]
77
82
  startup_timeout_sec = 20.0
78
- env = {
79
- # Two-key model: set whichever applies — both is best.
80
- # Auto-routing rule (same for q402_pay AND q402_batch_pay):
81
- # chain="bnb" + Q402_TRIAL_API_KEY set → Trial (free sponsored)
82
- # anything else → Multichain (paid 9-chain)
83
- # Batch ambiguity: when auto would land on Trial AND recipients.length > 5,
84
- # q402_batch_pay returns status="ambiguous" WITHOUT executing so the agent
85
- # can ask the user — pass keyScope="trial" (first 5), "multichain" (all
86
- # paid), or call twice (5 free + remainder paid).
87
- # Both keys use the same q402_live_ prefix — the env var name is what
88
- # carries the scope, not the key string. Get the values from the
89
- # dashboard (each key has its own copy button per view).
90
- Q402_TRIAL_API_KEY = "q402_live_...", # BNB-only sponsored (from /event)
91
- Q402_MULTICHAIN_API_KEY = "q402_live_...", # paid 9-chain (from /payment)
92
- # Legacy fallback — used if neither scoped key above is set.
93
- Q402_API_KEY = "q402_live_...",
94
- Q402_PRIVATE_KEY = "0xabc...",
95
- Q402_ENABLE_REAL_PAYMENTS = "1",
96
- Q402_MAX_AMOUNT_PER_CALL = "5",
97
- }
83
+ env_vars = [
84
+ "Q402_TRIAL_API_KEY",
85
+ "Q402_MULTICHAIN_API_KEY",
86
+ "Q402_PRIVATE_KEY",
87
+ "Q402_ENABLE_REAL_PAYMENTS",
88
+ "Q402_RELAY_BASE_URL",
89
+ ]
98
90
  ```
99
91
 
100
- > If you'd rather not inline secrets in `config.toml`, use the `env_vars` allow-list form to forward specific names from your shell environment instead — see the [Codex config reference](https://developers.openai.com/codex/config-reference) for the full schema.
92
+ Then export the values in `~/.zshrc` / `~/.bashrc`. See the [Codex config reference](https://developers.openai.com/codex/config-reference) for the full schema.
101
93
 
102
- Then run `codex` and ask the same kind of question. The first call may take a few seconds while `npx` warms its cache; subsequent calls are instant.
94
+ </details>
103
95
 
104
- ### Any other MCP client
96
+ ### Try it without any setup
105
97
 
106
- The server has no client-specific code. If your client speaks stdio MCP, point it at `npx -y @quackai/q402-mcp` and the seven tools listed below will appear.
98
+ `q402_quote` works with zero configuration no API key, no private key, no env file. Ask:
99
+
100
+ > *"Compare gas costs to send 50 USDC to vitalik.eth across all 9 Q402 chains."*
107
101
 
108
102
  ---
109
103
 
@@ -115,6 +109,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
115
109
 
116
110
  | Tool | Auth | Purpose |
117
111
  |---|---|---|
112
+ | `q402_doctor` | none | Health check covering BOTH first-install onboarding AND ongoing operational diagnostics. AI calls this when the user says "set up Q402" / "verify Q402" / "why isn't Q402 working". On first install, returns a `recommendedActions[]` payload telling the client to create `~/.q402/mcp.env` and open it in the user's editor. Later phases verify per-scope quota, EIP-7702 delegation state per chain, relay reachability, and surface slot-mismatch warnings (e.g. Trial-tier key sitting in `Q402_MULTICHAIN_API_KEY` would silently burn paid quota). Read-only — no signing, no on-chain action. |
118
113
  | `q402_quote` | none | Compare gas cost and supported tokens across chains. Read-only. |
119
114
  | `q402_balance` | API key | Verify the API key and report its plan tier + remaining quota credits (live vs sandbox). |
120
115
  | `q402_pay` | API key + private key + flag | Send a gasless payment to a single recipient. **Sandbox by default** — see [Sandbox vs live mode](#sandbox-vs-live-mode). |
@@ -136,7 +131,7 @@ The server has no client-specific code. If your client speaks stdio MCP, point i
136
131
 
137
132
  ## Sandbox vs live mode
138
133
 
139
- By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an LLM hallucinating a payment.
134
+ By default the MCP server operates in **sandbox mode**: `q402_pay` returns a random fake transaction hash (32-byte hex, no on-chain broadcast), no funds move, no gas-tank credit is consumed. That makes it safe to plug into any MCP client without worrying about an accidental payment if the agent misreads the conversation and fires `q402_pay` before you intended, nothing moves.
140
135
 
141
136
  To enable real on-chain transactions, the resolved API key must be live (`q402_live_*`), `Q402_PRIVATE_KEY` must be set, and `Q402_ENABLE_REAL_PAYMENTS=1`:
142
137
 
@@ -211,11 +206,11 @@ Combined with the `confirm: true` argument the tool requires, this means the mod
211
206
 
212
207
  ## Why this exists
213
208
 
214
- x402 standardised "402 Payment Required" semantics for AI agents but the official Coinbase facilitator only covers a few chains and assumes ERC-3009 token support which excludes BNB USDT, Mantle USDT0, Injective USDT, and the chains where most stablecoin volume actually lives.
209
+ AI agents are becoming the default interface for software, but the moment they need to move money the stack breaks: holding gas tokens, signing every transaction, managing wallets across many chains. None of that scales when the agent is supposed to act on its own.
215
210
 
216
- Q402 implements the same payer experience (single signature, $0 gas, instant settlement) on all 9 of those chains using EIP-7702 delegated execution, which works with any ERC-20. This MCP server makes that infrastructure addressable from Claude itself.
211
+ Q402 is the payment layer for that gap. A single signing primitive (EIP-712 + EIP-7702) settles gasless stablecoin payments across 9 EVM chains, with an ECDSA-signed Trust Receipt for every transaction. The MCP package exposes that surface inside Claude, Codex, Cursor, and Cline — your agent can quote, send, batch, and audit payments from a natural-language prompt.
217
212
 
218
- If you want to dig into how the wire protocol differs from x402, see [Q402 docs](https://q402.quackai.ai/docs).
213
+ Single transfers and multi-recipient batches ship today. The next layer — recurring payouts, conditional execution, and policy-gated treasury automation — is the same primitive composed differently. We're building toward agents that operate real budgets, settle among themselves, and move value through workflows no human triggers manually.
219
214
 
220
215
  ---
221
216
 
package/dist/index.js CHANGED
@@ -9,7 +9,63 @@ import {
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
10
 
11
11
  // src/config.ts
12
+ import { existsSync, readFileSync, statSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join } from "path";
12
15
  import { isAddress } from "ethers";
16
+ var Q402_ENV_FILE = join(homedir(), ".q402", "mcp.env");
17
+ function loadQ402EnvFileFromPath(path) {
18
+ if (!existsSync(path)) return {};
19
+ if (process.platform !== "win32") {
20
+ try {
21
+ const mode = statSync(path).mode & 511;
22
+ if (mode & 63) {
23
+ process.stderr.write(
24
+ `[q402-mcp] warning: ${path} is readable by group/other (mode ${mode.toString(8)}). Run: chmod 600 ${path}
25
+ `
26
+ );
27
+ }
28
+ } catch {
29
+ }
30
+ }
31
+ const out = {};
32
+ let raw;
33
+ try {
34
+ raw = readFileSync(path, "utf-8");
35
+ } catch (e) {
36
+ process.stderr.write(
37
+ `[q402-mcp] warning: could not read ${path}: ${e instanceof Error ? e.message : String(e)}
38
+ `
39
+ );
40
+ return {};
41
+ }
42
+ for (const line of raw.split(/\r?\n/)) {
43
+ const t = line.trim();
44
+ if (!t || t.startsWith("#")) continue;
45
+ const eq = t.indexOf("=");
46
+ if (eq < 0) continue;
47
+ const k = t.slice(0, eq).trim();
48
+ if (!k.startsWith("Q402_")) continue;
49
+ const v = t.slice(eq + 1).trim().replace(/^['"](.*)['"]$/, "$1");
50
+ out[k] = v;
51
+ }
52
+ return out;
53
+ }
54
+ function loadQ402EnvFile() {
55
+ return loadQ402EnvFileFromPath(Q402_ENV_FILE);
56
+ }
57
+ var FILE_ENV = loadQ402EnvFile();
58
+ var ENV = Object.freeze({
59
+ ...FILE_ENV,
60
+ ...process.env
61
+ });
62
+ var Q402_ENV_FILE_PATH = Q402_ENV_FILE;
63
+ var Q402_ENV_FILE_PRESENT = existsSync(Q402_ENV_FILE);
64
+ var Q402_ENV_FILE_KEYS = Object.freeze(
65
+ new Set(
66
+ Object.keys(FILE_ENV).filter((k) => process.env[k] === void 0)
67
+ )
68
+ );
13
69
  var DEFAULT_RELAY_BASE = "https://q402.quackai.ai/api";
14
70
  var DEFAULT_MAX_AMOUNT = 5;
15
71
  function classifyApiKey(k) {
@@ -29,13 +85,13 @@ function parseMaxAmount(raw) {
29
85
  return n;
30
86
  }
31
87
  function loadConfig() {
32
- const trialApiKey = process.env.Q402_TRIAL_API_KEY ?? null;
33
- const multichainApiKey = process.env.Q402_MULTICHAIN_API_KEY ?? null;
34
- const legacyApiKey = process.env.Q402_API_KEY ?? null;
88
+ const trialApiKey = ENV.Q402_TRIAL_API_KEY ?? null;
89
+ const multichainApiKey = ENV.Q402_MULTICHAIN_API_KEY ?? null;
90
+ const legacyApiKey = ENV.Q402_API_KEY ?? null;
35
91
  const apiKey = multichainApiKey ?? trialApiKey ?? legacyApiKey;
36
92
  const apiKeyKind = classifyApiKey(apiKey);
37
- const privateKey = process.env.Q402_PRIVATE_KEY ?? null;
38
- const realPaymentsRequested = process.env.Q402_ENABLE_REAL_PAYMENTS === "1";
93
+ const privateKey = ENV.Q402_PRIVATE_KEY ?? null;
94
+ const realPaymentsRequested = ENV.Q402_ENABLE_REAL_PAYMENTS === "1";
39
95
  const live = realPaymentsRequested && apiKeyKind === "live" && typeof privateKey === "string" && privateKey.length > 0;
40
96
  return {
41
97
  trialApiKey,
@@ -46,9 +102,9 @@ function loadConfig() {
46
102
  privateKey,
47
103
  realPaymentsRequested,
48
104
  mode: live ? "live" : "sandbox",
49
- relayBaseUrl: (process.env.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
50
- maxAmountPerCallUsd: parseMaxAmount(process.env.Q402_MAX_AMOUNT_PER_CALL),
51
- allowedRecipients: parseAllowedRecipients(process.env.Q402_ALLOWED_RECIPIENTS)
105
+ relayBaseUrl: (ENV.Q402_RELAY_BASE_URL ?? DEFAULT_RELAY_BASE).replace(/\/$/, ""),
106
+ maxAmountPerCallUsd: parseMaxAmount(ENV.Q402_MAX_AMOUNT_PER_CALL),
107
+ allowedRecipients: parseAllowedRecipients(ENV.Q402_ALLOWED_RECIPIENTS)
52
108
  };
53
109
  }
54
110
  var CONFIG = loadConfig();
@@ -97,6 +153,10 @@ function isLiveModeFor(resolved) {
97
153
  return resolved.apiKey.startsWith("q402_live_");
98
154
  }
99
155
 
156
+ // src/version.ts
157
+ var PACKAGE_NAME = "@quackai/q402-mcp";
158
+ var PACKAGE_VERSION = "0.5.6";
159
+
100
160
  // src/tools/quote.ts
101
161
  import { z } from "zod";
102
162
 
@@ -773,7 +833,7 @@ function describeSandboxReason(resolvedKey, scope) {
773
833
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
774
834
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
775
835
  const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
776
- return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}.`;
836
+ return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
777
837
  }
778
838
  var PAY_TOOL = {
779
839
  name: "q402_pay",
@@ -960,7 +1020,7 @@ function describeSandboxReason2(resolvedKey, scope) {
960
1020
  if (missing.length === 0) return "Sandbox mode active (no env state change needed).";
961
1021
  const tier = scope === "trial" ? "Free Trial" : "Multichain";
962
1022
  const url = scope === "trial" ? "https://q402.quackai.ai/event" : "https://q402.quackai.ai/payment";
963
- return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}.`;
1023
+ return "Sandbox mode is active because the following env vars are missing or not yet set: " + missing.join(", ") + `. Get a live ${tier} key at ${url}, then call q402_doctor \u2014 it will walk the user through creating ~/.q402/mcp.env and pasting the key into the right slot.`;
964
1024
  }
965
1025
  var BATCH_PAY_TOOL = {
966
1026
  name: "q402_batch_pay",
@@ -1056,7 +1116,7 @@ async function runBalance() {
1056
1116
  apiKeyMasked: null,
1057
1117
  scopes: [],
1058
1118
  dashboardUrl: "https://q402.quackai.ai/dashboard",
1059
- setupHint: "Set Q402_TRIAL_API_KEY (BNB-only sponsored, free at /event) and/or Q402_MULTICHAIN_API_KEY (paid 9-chain from /dashboard). Single-env legacy: Q402_API_KEY also works."
1119
+ setupHint: "No API key configured. Call q402_doctor for guided setup \u2014 it will offer to create ~/.q402/mcp.env with placeholders that the user can fill in. (Manual path: set Q402_TRIAL_API_KEY for BNB-only sponsored (free at https://q402.quackai.ai/event) and/or Q402_MULTICHAIN_API_KEY for paid 9-chain (https://q402.quackai.ai/payment). Q402_API_KEY is the legacy single-env fallback.)"
1060
1120
  };
1061
1121
  }
1062
1122
  const scopes = await Promise.all(
@@ -1453,9 +1513,278 @@ var CLEAR_DELEGATION_TOOL = {
1453
1513
  }
1454
1514
  };
1455
1515
 
1516
+ // src/tools/doctor.ts
1517
+ import { z as z8 } from "zod";
1518
+ import { Wallet as Wallet4 } from "ethers";
1519
+ var DoctorInputSchema = z8.object({});
1520
+ var ENV_FILE_TEMPLATE = `# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1521
+ # Q402 MCP \u2014 secrets
1522
+ # Read automatically by @quackai/q402-mcp on startup.
1523
+ # Edit this file in your editor. NEVER paste your private key into chat.
1524
+ # After editing, restart your MCP client (Codex / Claude / Cursor / Cline).
1525
+ # \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1526
+
1527
+ # \u2500\u2500\u2500 API key \u2014 pick ONE (uncomment the one you have) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1528
+ # Free Trial: BNB Chain only, 2,000 sponsored TX
1529
+ # Get one at: https://q402.quackai.ai/event
1530
+ Q402_TRIAL_API_KEY=q402_live_...
1531
+
1532
+ # Paid Multichain: all 9 chains, per-chain Gas Tank
1533
+ # Get one at: https://q402.quackai.ai/payment
1534
+ # Q402_MULTICHAIN_API_KEY=q402_live_...
1535
+
1536
+ # \u2500\u2500\u2500 Your wallet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1537
+ # Hex EVM private key. Signs payments LOCALLY on your machine.
1538
+ # Never leaves your device, never sent to any server.
1539
+ Q402_PRIVATE_KEY=0x...
1540
+
1541
+ # \u2500\u2500\u2500 Live mode flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1542
+ # Must be exactly "1" to allow real on-chain transactions.
1543
+ # Anything else = test response (fake hash, no funds move).
1544
+ Q402_ENABLE_REAL_PAYMENTS=1
1545
+
1546
+ # \u2500\u2500\u2500 Q402 relay endpoint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1547
+ # Default canonical Q402 deployment. Only change for self-hosted.
1548
+ Q402_RELAY_BASE_URL=https://q402.quackai.ai/api
1549
+
1550
+ # \u2500\u2500\u2500 Optional safety guards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1551
+ # Max USD per single q402_pay call (default: 5)
1552
+ # Q402_MAX_AMOUNT_PER_CALL=5
1553
+ #
1554
+ # Comma-separated lowercase recipient allowlist (unset = any address OK)
1555
+ # Q402_ALLOWED_RECIPIENTS=0xabc...,0xdef...
1556
+ `;
1557
+ var SECURITY_NOTICE = "Q402 never asks you to paste your private key into chat. The MCP server signs payments LOCALLY on your machine \u2014 your key never leaves your device, never goes to a remote server. If a key was already pasted in chat by mistake, treat the wallet as exposed: move funds to a fresh wallet and use that new key in ~/.q402/mcp.env going forward.";
1558
+ function envSource(name) {
1559
+ if (process.env[name] !== void 0) return "process";
1560
+ if (Q402_ENV_FILE_KEYS.has(name)) return "file";
1561
+ if (ENV[name] !== void 0) return "file";
1562
+ return "unset";
1563
+ }
1564
+ function envSlot(name, purpose) {
1565
+ const source = envSource(name);
1566
+ return { set: source !== "unset", source, purpose };
1567
+ }
1568
+ function mask2(key) {
1569
+ if (!key || key.length < 12) return key ?? "";
1570
+ return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
1571
+ }
1572
+ function detectPhase() {
1573
+ const anyKey = !!(CONFIG.trialApiKey || CONFIG.multichainApiKey || CONFIG.legacyApiKey);
1574
+ const allEssentials = anyKey && !!CONFIG.privateKey && CONFIG.realPaymentsRequested && CONFIG.apiKeyKind === "live";
1575
+ if (allEssentials) return "live-check";
1576
+ if (anyKey || CONFIG.privateKey || CONFIG.realPaymentsRequested) return "needs-completion";
1577
+ return "first-install";
1578
+ }
1579
+ async function verifyOneKey(scope, envVar, apiKey) {
1580
+ const url = `${CONFIG.relayBaseUrl}/keys/verify`;
1581
+ try {
1582
+ const resp = await fetch(url, {
1583
+ method: "POST",
1584
+ headers: { "Content-Type": "application/json" },
1585
+ body: JSON.stringify({ apiKey })
1586
+ });
1587
+ if (!resp.ok) {
1588
+ return { scope, envVar, apiKeyMasked: mask2(apiKey), valid: false, error: `HTTP ${resp.status}` };
1589
+ }
1590
+ const body = await resp.json();
1591
+ const result = {
1592
+ scope,
1593
+ envVar,
1594
+ apiKeyMasked: mask2(apiKey),
1595
+ valid: body.valid ?? false,
1596
+ plan: body.plan,
1597
+ remainingCredits: body.remainingCredits,
1598
+ isTrial: body.isTrial,
1599
+ trialExpiresAt: body.trialExpiresAt,
1600
+ trialDaysLeft: body.trialDaysLeft
1601
+ };
1602
+ if (scope === "multichain" && body.isTrial) {
1603
+ result.slotWarning = "Trial-tier key found in Q402_MULTICHAIN_API_KEY slot. This works but you'll lose the auto-routing benefit (BNB free via Trial). Move the key to Q402_TRIAL_API_KEY in ~/.q402/mcp.env.";
1604
+ } else if (scope === "trial" && body.isTrial === false && body.plan && body.plan !== "trial") {
1605
+ result.slotWarning = "Paid Multichain-tier key found in Q402_TRIAL_API_KEY slot. BNB payments will burn your paid quota instead of using free Trial sponsorship. Move the key to Q402_MULTICHAIN_API_KEY in ~/.q402/mcp.env.";
1606
+ }
1607
+ return result;
1608
+ } catch (e) {
1609
+ return {
1610
+ scope,
1611
+ envVar,
1612
+ apiKeyMasked: mask2(apiKey),
1613
+ valid: false,
1614
+ error: e instanceof Error ? e.message : String(e)
1615
+ };
1616
+ }
1617
+ }
1618
+ async function pingRelay() {
1619
+ const url = `${CONFIG.relayBaseUrl}/health`;
1620
+ const t0 = Date.now();
1621
+ try {
1622
+ const resp = await fetch(url, { method: "GET" });
1623
+ const reachable = resp.status < 500;
1624
+ return { url, reachable, latencyMs: Date.now() - t0 };
1625
+ } catch (e) {
1626
+ return {
1627
+ url,
1628
+ reachable: false,
1629
+ latencyMs: Date.now() - t0,
1630
+ error: e instanceof Error ? e.message : String(e)
1631
+ };
1632
+ }
1633
+ }
1634
+ async function fetchDelegation(address) {
1635
+ const url = `${CONFIG.relayBaseUrl}/wallet/delegation-status?address=${address}`;
1636
+ try {
1637
+ const resp = await fetch(url);
1638
+ if (!resp.ok) {
1639
+ return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error: `HTTP ${resp.status}` }));
1640
+ }
1641
+ const body = await resp.json();
1642
+ return CHAIN_KEYS.map((chain) => {
1643
+ const s = body.chains?.[chain];
1644
+ if (!s) return { chain, delegated: false, error: "missing from response" };
1645
+ return { chain, delegated: s.delegated, impl: s.impl, error: s.error };
1646
+ });
1647
+ } catch (e) {
1648
+ const error = e instanceof Error ? e.message : String(e);
1649
+ return CHAIN_KEYS.map((chain) => ({ chain, delegated: false, error }));
1650
+ }
1651
+ }
1652
+ async function runDoctor() {
1653
+ const phase = detectPhase();
1654
+ const envFile = {
1655
+ path: Q402_ENV_FILE_PATH,
1656
+ exists: Q402_ENV_FILE_PRESENT
1657
+ };
1658
+ const envState = {
1659
+ Q402_TRIAL_API_KEY: envSlot(
1660
+ "Q402_TRIAL_API_KEY",
1661
+ "Free Trial \u2014 BNB only, 2,000 sponsored TX. Get at https://q402.quackai.ai/event"
1662
+ ),
1663
+ Q402_MULTICHAIN_API_KEY: envSlot(
1664
+ "Q402_MULTICHAIN_API_KEY",
1665
+ "Paid Multichain \u2014 all 9 chains, per-chain Gas Tank. Get at https://q402.quackai.ai/payment"
1666
+ ),
1667
+ Q402_PRIVATE_KEY: envSlot(
1668
+ "Q402_PRIVATE_KEY",
1669
+ "Hex EVM private key. Signs LOCALLY on your machine \u2014 never leaves your device."
1670
+ ),
1671
+ Q402_ENABLE_REAL_PAYMENTS: envSlot(
1672
+ "Q402_ENABLE_REAL_PAYMENTS",
1673
+ "Must be '1' to allow real TX. Anything else = test response (fake hash)."
1674
+ )
1675
+ };
1676
+ let legacyDetected;
1677
+ if (CONFIG.legacyApiKey) {
1678
+ legacyDetected = "Q402_API_KEY is set \u2014 works as a fallback for both scopes, but the newer two-key model (Q402_TRIAL_API_KEY + Q402_MULTICHAIN_API_KEY) gives you auto-routing between free Trial (BNB) and paid Multichain. Rename in ~/.q402/mcp.env when convenient.";
1679
+ }
1680
+ const missing = [];
1681
+ if (!CONFIG.trialApiKey && !CONFIG.multichainApiKey && !CONFIG.legacyApiKey) {
1682
+ missing.push(
1683
+ "An API key (Q402_TRIAL_API_KEY for free BNB OR Q402_MULTICHAIN_API_KEY for paid 9-chain)"
1684
+ );
1685
+ }
1686
+ if (!CONFIG.privateKey) missing.push("Q402_PRIVATE_KEY");
1687
+ if (!CONFIG.realPaymentsRequested) missing.push("Q402_ENABLE_REAL_PAYMENTS=1");
1688
+ const recommendedActions = [];
1689
+ if (!envFile.exists) {
1690
+ recommendedActions.push({
1691
+ id: "create-env-file",
1692
+ type: "write_file",
1693
+ path: Q402_ENV_FILE_PATH,
1694
+ createParentDirs: true,
1695
+ content: ENV_FILE_TEMPLATE,
1696
+ requiresUserConfirm: true,
1697
+ description: "Create ~/.q402/mcp.env with placeholder values, then open it in the user's editor.",
1698
+ ifExists: "skip"
1699
+ });
1700
+ }
1701
+ const warnings = [];
1702
+ if (phase !== "live-check") {
1703
+ return {
1704
+ package: PACKAGE_NAME,
1705
+ version: PACKAGE_VERSION,
1706
+ phase,
1707
+ ready: false,
1708
+ envFile,
1709
+ envState,
1710
+ missing,
1711
+ legacyDetected,
1712
+ warnings,
1713
+ recommendedActions,
1714
+ greeting: phase === "first-install" ? `Q402 MCP is installed (v${PACKAGE_VERSION}).` : `Q402 MCP is installed (v${PACKAGE_VERSION}) \u2014 partially configured.`,
1715
+ nextStep: phase === "first-install" ? "Offer to create ~/.q402/mcp.env. After yes, run the recommendedActions[].write_file action, then open the file in the user's editor (e.g. via `code` / `open` / `start` / `xdg-open`). Then walk through filling in the API key and private key, one at a time. Do NOT accept key values via chat \u2014 direct the user to edit the file in their editor." : `Tell the user which env vars are still missing (from the 'missing' list) and how to add them to ~/.q402/mcp.env. Restart needed after editing.`,
1716
+ securityNotice: SECURITY_NOTICE
1717
+ };
1718
+ }
1719
+ let walletAddress;
1720
+ try {
1721
+ walletAddress = new Wallet4(CONFIG.privateKey).address;
1722
+ } catch {
1723
+ warnings.push("Q402_PRIVATE_KEY is set but does not parse as a 32-byte hex key. Live calls will fail.");
1724
+ }
1725
+ const verifyTargets = [];
1726
+ if (CONFIG.trialApiKey) verifyTargets.push({ scope: "trial", envVar: "Q402_TRIAL_API_KEY", key: CONFIG.trialApiKey });
1727
+ if (CONFIG.multichainApiKey) verifyTargets.push({ scope: "multichain", envVar: "Q402_MULTICHAIN_API_KEY", key: CONFIG.multichainApiKey });
1728
+ if (verifyTargets.length === 0 && CONFIG.legacyApiKey) {
1729
+ verifyTargets.push({ scope: "legacy", envVar: "Q402_API_KEY", key: CONFIG.legacyApiKey });
1730
+ }
1731
+ const [keys, delegation, relay] = await Promise.all([
1732
+ Promise.all(verifyTargets.map((t) => verifyOneKey(t.scope, t.envVar, t.key))),
1733
+ walletAddress ? fetchDelegation(walletAddress) : Promise.resolve([]),
1734
+ pingRelay()
1735
+ ]);
1736
+ for (const k of keys) if (k.slotWarning) warnings.push(k.slotWarning);
1737
+ for (const k of keys) {
1738
+ if (typeof k.remainingCredits === "number" && k.remainingCredits === 0) {
1739
+ warnings.push(
1740
+ `${k.envVar} has 0 credits remaining. ` + (k.isTrial ? "Trial allotment exhausted \u2014 upgrade to a Multichain plan at https://q402.quackai.ai/payment." : "Paid plan quota exhausted \u2014 top up at https://q402.quackai.ai/dashboard?tab=billing.")
1741
+ );
1742
+ } else if (typeof k.remainingCredits === "number" && k.remainingCredits > 0 && k.remainingCredits < 50) {
1743
+ warnings.push(
1744
+ `${k.envVar} has only ${k.remainingCredits} credits left \u2014 top up before you run out.`
1745
+ );
1746
+ }
1747
+ if (!k.valid && !k.error) {
1748
+ warnings.push(`${k.envVar} verified as invalid by the relay \u2014 check the key value in ~/.q402/mcp.env.`);
1749
+ }
1750
+ }
1751
+ if (relay && !relay.reachable) {
1752
+ warnings.push(
1753
+ `Q402 relay at ${relay.url} is unreachable. Check your network or override with Q402_RELAY_BASE_URL if you self-host.`
1754
+ );
1755
+ }
1756
+ const ready = warnings.length === 0 && keys.some((k) => k.valid);
1757
+ return {
1758
+ package: PACKAGE_NAME,
1759
+ version: PACKAGE_VERSION,
1760
+ phase,
1761
+ ready,
1762
+ envFile,
1763
+ envState,
1764
+ missing,
1765
+ legacyDetected,
1766
+ wallet: walletAddress ? { address: walletAddress } : void 0,
1767
+ keys,
1768
+ delegation,
1769
+ relay,
1770
+ warnings,
1771
+ recommendedActions,
1772
+ greeting: ready ? `Q402 MCP is ready (v${PACKAGE_VERSION}).` : `Q402 MCP is installed but has ${warnings.length} issue${warnings.length === 1 ? "" : "s"} to address.`,
1773
+ nextStep: ready ? "Summarize the wallet address, plan tier(s), remaining quota, and any non-zero delegation counts to the user as a checklist. Then offer to make a test payment via q402_quote." : "Walk the user through each warning in order. For slot-mismatch warnings, the fix is editing ~/.q402/mcp.env and restarting the client.",
1774
+ securityNotice: SECURITY_NOTICE
1775
+ };
1776
+ }
1777
+ var DOCTOR_TOOL = {
1778
+ name: "q402_doctor",
1779
+ description: 'Run a Q402 health check \u2014 covers first-install onboarding AND ongoing diagnostics in one tool. Read-only, no API key required. Detects the current phase (first-install / needs-completion / live-check) and tailors output to it. \n\nUse when the user says any of: "set up Q402", "verify Q402", "why isn\'t Q402 working", "Q402 status", "check Q402". This is the FIRST tool to call after install, BEFORE q402_pay or q402_balance \u2014 it tells the agent what state the user is in. \n\nMulti-turn pattern the AI should follow when phase = first-install: (1) Tell user MCP is installed. (2) Ask one yes/no question: \'Want me to create your secrets file at ~/.q402/mcp.env?\' (3) On yes, execute the recommendedActions[].write_file action using the client\'s own filesystem tool, then open the file in the user\'s editor (e.g. `code ~/.q402/mcp.env`, `open` on macOS, `start` on Windows, `xdg-open` on Linux). (4) Guide the user through getting an API key (free Trial at https://q402.quackai.ai/event OR paid Multichain at /payment) and pasting it into the file (in their editor \u2014 NEVER in chat). (5) Same for the private key. (6) Tell them to save + restart the MCP client. (7) Call q402_doctor again to verify. \n\nSecurity policy carried in the response: AI MUST surface the securityNotice when first walking through setup. If the user pastes a private key directly in chat, DO NOT refuse \u2014 the exposure already happened. Help them by directing them to put it in the file themselves (via their editor), and inform them the chat history now contains the key (most clients store this locally, some sync to cloud) so they should treat the wallet as exposed if it holds valuables. \n\nLive-check phase additionally returns per-scope quota, EIP-7702 delegation state per chain, relay reachability, and slot-mismatch warnings (e.g. Trial key in Multichain slot silently burns paid quota \u2014 surface this to the user).',
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {},
1783
+ additionalProperties: false
1784
+ }
1785
+ };
1786
+
1456
1787
  // src/index.ts
1457
- var PACKAGE_NAME = "@quackai/q402-mcp";
1458
- var PACKAGE_VERSION = "0.5.5";
1459
1788
  function jsonText(value) {
1460
1789
  return { type: "text", text: JSON.stringify(value, null, 2) };
1461
1790
  }
@@ -1466,6 +1795,9 @@ async function main() {
1466
1795
  );
1467
1796
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1468
1797
  tools: [
1798
+ // doctor first — it's the bootstrap tool: any "set up Q402" / "is Q402
1799
+ // working" prompt should land here before quote/balance/pay are tried.
1800
+ DOCTOR_TOOL,
1469
1801
  QUOTE_TOOL,
1470
1802
  BALANCE_TOOL,
1471
1803
  PAY_TOOL,
@@ -1479,6 +1811,10 @@ async function main() {
1479
1811
  const { name, arguments: args } = req.params;
1480
1812
  try {
1481
1813
  switch (name) {
1814
+ case "q402_doctor": {
1815
+ DoctorInputSchema.parse(args ?? {});
1816
+ return { content: [jsonText(await runDoctor())] };
1817
+ }
1482
1818
  case "q402_quote": {
1483
1819
  const parsed = QuoteInputSchema.parse(args ?? {});
1484
1820
  return { content: [jsonText(runQuote(parsed))] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quackai/q402-mcp",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "MCP server for Q402 — gasless USDC, USDT, and RLUSD payments across 9 EVM chains, callable from Claude (Desktop / Code), OpenAI Codex CLI, and any other Model Context Protocol client.",
5
5
  "mcpName": "io.github.bitgett/q402-mcp",
6
6
  "keywords": [