presign-guard-wallet-mcp 0.1.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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/lib.js +245 -0
  4. package/package.json +57 -0
  5. package/server.js +17 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frits Zwager
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # presign-guard-wallet-mcp
2
+
3
+ A wallet with spending limits for AI agents, as an [MCP](https://modelcontextprotocol.io) server. Add it to Claude Desktop, Claude Code, Cursor or any other MCP client, and your agent can:
4
+ - pay for x402 APIs;
5
+ - send USDC.
6
+
7
+ It can only do that within the budget you set:
8
+
9
+ - **You set the limits.** For example: up to 5 USDC per payment and 20 USDC a day, and the agent is free to spend within them.
10
+ - **Above a limit, you decide.** You get a Telegram message with Approve / Deny, or the request shows up on your [wallet server](https://github.com/Fizzl13/x402-examples/tree/main/wallet-server) dashboard. No answer means no payment.
11
+ - **Every signature is checked first by [presign-guard](https://presign-guard.fizzl.eu).** It checks for known drainers, sanctioned addresses and look-alike tokens. A red verdict is never signed.
12
+ - **When in doubt, nothing is signed.** If the check, Telegram or the server can't be reached, the wallet stops.
13
+
14
+ The key stays on your machine; the server only decides whether a signature is allowed. Each check costs $0.01, paid in USDC on Base from the same wallet, or from prepaid credits.
15
+
16
+ See the dashboard (demo data): **https://wallet.fizzl.eu/demo**
17
+
18
+ ## Tools
19
+
20
+ | Tool | What it does |
21
+ |---|---|
22
+ | `wallet_status` | Address, balances, limits, what is left today, how approvals work |
23
+ | `pay_x402` | Call an API that answers 402 Payment Required, pay it in USDC, return the response (with a price cap per call) |
24
+ | `send_usdc` | Send USDC to an address |
25
+ | `send_native` | Send ETH / POL / BNB to an address |
26
+ | `pause_spending` | The agent stops itself when something looks wrong; nothing is signed until you restart or resume |
27
+
28
+ ## Set up
29
+
30
+ 1. **Make a separate wallet for the agent.** Put only what it may spend in it: a few dollars of USDC on Base, plus a little ETH for gas if it will send transfers. Never use your main wallet.
31
+ 2. **Optional: phone approvals.**
32
+ - Create a bot with [@BotFather](https://t.me/BotFather), send it `/start`.
33
+ - Get your chat id from [@userinfobot](https://t.me/userinfobot).
34
+ 3. **Add it to your MCP client.** For Claude Desktop, put this in `claude_desktop_config.json`:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "wallet": {
40
+ "command": "npx",
41
+ "args": ["-y", "presign-guard-wallet-mcp"],
42
+ "env": {
43
+ "AGENT_KEY": "0x…the agent wallet's private key…",
44
+ "LIMIT_USDC_PER_TX": "5",
45
+ "LIMIT_USDC_PER_DAY": "20",
46
+ "TELEGRAM_BOT_TOKEN": "123456:ABC…",
47
+ "TELEGRAM_CHAT_ID": "123456789"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ For Claude Code:
55
+
56
+ ```sh
57
+ claude mcp add wallet -e AGENT_KEY=0x… -e LIMIT_USDC_PER_TX=5 -e LIMIT_USDC_PER_DAY=20 -- npx -y presign-guard-wallet-mcp
58
+ ```
59
+
60
+ Then ask your agent, for example: *"Check my wallet, then get the BTC signal from https://ichimoku-signal.fizzl.eu/signal/BTC-USDT"*.
61
+
62
+ ### One budget for all your agents
63
+
64
+ Run the [wallet server](https://github.com/Fizzl13/x402-examples/tree/main/wallet-server) and make an agent key on its dashboard. It gives you:
65
+ - one price rule and one daily budget for all your agents;
66
+ - approvals on the dashboard and on Telegram;
67
+ - an activity log.
68
+
69
+ Then use these variables instead of `LIMIT_*` and `TELEGRAM_*`:
70
+
71
+ ```json
72
+ "env": {
73
+ "AGENT_KEY": "0x…",
74
+ "WALLET_SERVER_URL": "https://your-wallet-server.example",
75
+ "WALLET_SERVER_KEY": "awk_…"
76
+ }
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ | Variable | |
82
+ |---|---|
83
+ | `AGENT_KEY` | **required**: private key of the agent's own wallet |
84
+ | `LIMIT_USDC_PER_TX`, `LIMIT_USDC_PER_DAY` | USDC limits (payments and transfers together). Limits or a wallet server are required |
85
+ | `LIMIT_NATIVE_PER_TX`, `LIMIT_NATIVE_PER_DAY` | limits for the native coin; without them, sending it needs approval |
86
+ | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` | approvals on Telegram; without them, anything over a limit is refused |
87
+ | `WALLET_SERVER_URL`, `WALLET_SERVER_KEY` | a wallet server instead of the above |
88
+ | `CHAIN` | `base` (default), `ethereum`, `optimism`, `arbitrum`, `polygon` or `bsc` |
89
+ | `MAX_PAYMENT_USD` | most one `pay_x402` call may cost (default `1`); a cap on top of the limits |
90
+ | `PRESIGN_CREDIT_KEY` | pay the $0.01 checks from [prepaid credits](https://presign-guard.fizzl.eu) (`pgc_…`) |
91
+ | `RPC_URL` | your own RPC for the chain |
92
+ | `AGENT_LABEL` | the name shown in Telegram approval messages (default `mcp-agent`) |
93
+
94
+ One Telegram bot serves one running server at a time, and the bot must not have a webhook. To run several agents on one bot, use the wallet server.
95
+
96
+ ## When something is refused
97
+
98
+ The tool returns an error the agent can read and pass on to you. Examples:
99
+ - `Not done (over_limit): 8 USDC is over the limit of 5 per transaction (denied by the owner)`;
100
+ - `Not done (red): not signed: red (known_drainer)`.
101
+
102
+ ## Also available
103
+
104
+ - [presign-guard-wallet](https://github.com/Fizzl13/x402-examples/tree/main/guard-wallet): the same guard for your own viem code.
105
+ - [presign-guard-agentkit](https://github.com/Fizzl13/x402-examples/tree/main/agentkit-guard): for Coinbase AgentKit agents.
106
+
107
+ ## License
108
+
109
+ MIT
package/lib.js ADDED
@@ -0,0 +1,245 @@
1
+ // The wallet behind presign-guard-wallet-mcp: an agent's own key, wrapped in
2
+ // presign-guard-wallet, so every payment and transfer a model asks for is
3
+ // checked by presign-guard and kept to the owner's limits, with approval on
4
+ // Telegram or the wallet server above them.
5
+ import { createPublicClient, createWalletClient, erc20Abi, formatEther, formatUnits, http, isAddress, parseEther, parseUnits } from "viem";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { arbitrum, base, bsc, mainnet, optimism, polygon } from "viem/chains";
8
+ import { wrapFetchWithPayment, x402Client, decodePaymentResponseHeader } from "@x402/fetch";
9
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
10
+ import { guardWallet, PresignBlockedError } from "presign-guard-wallet";
11
+ import { telegramApprover } from "presign-guard-wallet/telegram";
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { z } from "zod";
14
+
15
+ export const VERSION = "0.1.0";
16
+
17
+ export const CHAINS = {
18
+ base: { chain: base, usdc: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6] },
19
+ ethereum: { chain: mainnet, usdc: ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6] },
20
+ optimism: { chain: optimism, usdc: ["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", 6] },
21
+ arbitrum: { chain: arbitrum, usdc: ["0xaf88d065e77c8cC2239327C5EDb3A432268e5831", 6] },
22
+ polygon: { chain: polygon, usdc: ["0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", 6] },
23
+ bsc: { chain: bsc, usdc: ["0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", 18] },
24
+ };
25
+
26
+ const BODY_LIMIT = 20_000;
27
+ const CHECK_PRICE_CAP = "$0.02"; // a presign-guard check costs $0.01
28
+
29
+ const amountString = z.string().regex(/^\d+(\.\d+)?$/, "a decimal amount like \"2.5\"");
30
+ const address = z.string().refine((a) => isAddress(a, { strict: false }), "an 0x… address");
31
+
32
+ /**
33
+ * Read the configuration from environment variables (see README).
34
+ * Throws a readable error for anything missing or unsafe.
35
+ */
36
+ export function configFromEnv(env = process.env) {
37
+ const key = env.AGENT_KEY?.trim();
38
+ if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error("AGENT_KEY must be the agent wallet's private key (0x + 64 hex). Use a separate wallet with only what the agent may spend.");
39
+ const chainName = (env.CHAIN ?? "base").toLowerCase();
40
+ if (!CHAINS[chainName]) throw new Error(`CHAIN must be one of ${Object.keys(CHAINS).join(", ")}`);
41
+
42
+ const server = env.WALLET_SERVER_URL || env.WALLET_SERVER_KEY ? { url: env.WALLET_SERVER_URL, key: env.WALLET_SERVER_KEY } : undefined;
43
+ const tokens = {};
44
+ const usdc = pick(env.LIMIT_USDC_PER_TX, env.LIMIT_USDC_PER_DAY);
45
+ if (usdc) tokens.USDC = usdc;
46
+ const nativeSymbol = CHAINS[chainName].chain.nativeCurrency.symbol;
47
+ const native = pick(env.LIMIT_NATIVE_PER_TX, env.LIMIT_NATIVE_PER_DAY);
48
+ if (native) tokens[nativeSymbol === "MATIC" ? "POL" : nativeSymbol] = native;
49
+ if (server && Object.keys(tokens).length) throw new Error("with WALLET_SERVER_URL the limits live on the wallet server: leave out the LIMIT_* variables");
50
+ if (!server && !Object.keys(tokens).length) throw new Error("set spending limits (LIMIT_USDC_PER_TX and/or LIMIT_USDC_PER_DAY) or a wallet server (WALLET_SERVER_URL + WALLET_SERVER_KEY): this wallet does not run without a budget");
51
+
52
+ const telegram = env.TELEGRAM_BOT_TOKEN || env.TELEGRAM_CHAT_ID ? { token: env.TELEGRAM_BOT_TOKEN, chatId: env.TELEGRAM_CHAT_ID } : undefined;
53
+ if (telegram && (!telegram.token || !telegram.chatId)) throw new Error("Telegram approval needs both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID");
54
+ if (telegram && server) throw new Error("with WALLET_SERVER_URL, approvals go through the wallet server (its own Telegram): leave out TELEGRAM_*");
55
+
56
+ const maxPrice = env.MAX_PAYMENT_USD ?? "1";
57
+ if (!/^\d+(\.\d+)?$/.test(maxPrice)) throw new Error("MAX_PAYMENT_USD must be a number of dollars, e.g. 1 or 0.25");
58
+
59
+ return {
60
+ key,
61
+ chainName,
62
+ rpcUrl: env.RPC_URL || undefined,
63
+ creditKey: env.PRESIGN_CREDIT_KEY || undefined,
64
+ server,
65
+ limits: server ? undefined : { tokens },
66
+ telegram,
67
+ label: env.AGENT_LABEL || "mcp-agent",
68
+ maxPaymentUsd: Number(maxPrice),
69
+ };
70
+ }
71
+
72
+ function pick(perTx, perDay) {
73
+ const out = {};
74
+ for (const [k, v] of [["perTx", perTx], ["perDay", perDay]]) {
75
+ if (v === undefined || v === "") continue;
76
+ if (!/^\d+(\.\d+)?$/.test(v)) throw new Error(`limits must be amounts like "5" or "0.5" (got "${v}")`);
77
+ out[k] = v;
78
+ }
79
+ return Object.keys(out).length ? out : null;
80
+ }
81
+
82
+ /**
83
+ * The guarded wallet and what the tools do with it. `overrides` is for tests
84
+ * (walletClient, publicClient, fetch, presign options such as verifyReceipts).
85
+ */
86
+ export function createWallet(config, overrides = {}) {
87
+ const { chain, usdc } = CHAINS[config.chainName];
88
+ const account = privateKeyToAccount(config.key);
89
+ const transport = http(config.rpcUrl);
90
+ const walletClient = overrides.walletClient ?? createWalletClient({ account, chain, transport });
91
+ const publicClient = overrides.publicClient ?? createPublicClient({ chain, transport });
92
+ const plainFetch = overrides.fetch ?? globalThis.fetch;
93
+
94
+ // Pays the presign-guard checks ($0.01 each, USDC on Base) with the agent's key,
95
+ // capped per payment. Not guarded itself: guarding the check would check the check.
96
+ const checkPayer = new x402Client().setSpendControls({ maxAmountPerPayment: CHECK_PRICE_CAP });
97
+ checkPayer.register("eip155:8453", new ExactEvmScheme(account));
98
+ const pay = wrapFetchWithPayment(plainFetch, checkPayer);
99
+
100
+ const onOverLimit = config.telegram ? telegramApprover({ ...config.telegram, label: config.label, fetch: plainFetch }) : undefined;
101
+ const guarded = guardWallet(walletClient, {
102
+ pay,
103
+ creditKey: config.creditKey,
104
+ fetch: plainFetch,
105
+ limits: config.limits,
106
+ onOverLimit,
107
+ server: config.server ? { ...config.server, fetch: plainFetch } : undefined,
108
+ ...overrides.guard,
109
+ });
110
+
111
+ // x402 payments are signed by the guarded wallet: checked, and counted toward the USDC limits.
112
+ const signer = {
113
+ address: account.address,
114
+ signTypedData: (typedData) => guarded.signTypedData({ account, ...typedData }),
115
+ readContract: (args) => publicClient.readContract(args),
116
+ };
117
+
118
+ return {
119
+ address: account.address,
120
+ chain,
121
+ guard: guarded,
122
+
123
+ async status() {
124
+ const [native, usdcBalance] = await Promise.all([
125
+ publicClient.getBalance({ address: account.address }).catch(() => null),
126
+ publicClient.readContract({ address: usdc[0], abi: erc20Abi, functionName: "balanceOf", args: [account.address] }).catch(() => null),
127
+ ]);
128
+ return {
129
+ address: account.address,
130
+ chain: `${config.chainName} (${chain.id})`,
131
+ balances: {
132
+ [chain.nativeCurrency.symbol]: native === null ? "unknown" : formatEther(native),
133
+ USDC: usdcBalance === null ? "unknown" : formatUnits(usdcBalance, usdc[1]),
134
+ },
135
+ limits: config.server ? `kept by the wallet server ${config.server.url}` : config.limits.tokens,
136
+ spending: await guarded.spending().catch((err) => `unavailable (${err.message})`),
137
+ approvals: config.server ? "wallet server (dashboard / Telegram)" : config.telegram ? "Telegram" : "none: anything over a limit is refused",
138
+ maxPaymentUsd: config.maxPaymentUsd,
139
+ paused: guarded.paused(),
140
+ };
141
+ },
142
+
143
+ async sendUsdc({ to, amount }) {
144
+ const value = parseUnits(amount, usdc[1]);
145
+ const hash = await guarded.writeContract({ address: usdc[0], abi: erc20Abi, functionName: "transfer", args: [to, value] });
146
+ return { hash, sent: `${amount} USDC`, to, chain: config.chainName };
147
+ },
148
+
149
+ async sendNative({ to, amount }) {
150
+ const hash = await guarded.sendTransaction({ to, value: parseEther(amount) });
151
+ return { hash, sent: `${amount} ${chain.nativeCurrency.symbol}`, to, chain: config.chainName };
152
+ },
153
+
154
+ async payX402({ url, method = "GET", body, headers, maxPriceUsd }) {
155
+ if (!/^https?:\/\//.test(url)) throw new TypeError("url must start with https:// or http://");
156
+ const cap = Math.min(maxPriceUsd ?? config.maxPaymentUsd, config.maxPaymentUsd);
157
+ const client = new x402Client().setSpendControls({ maxAmountPerPayment: `$${cap}` });
158
+ client.register(`eip155:${chain.id}`, new ExactEvmScheme(signer));
159
+ const res = await wrapFetchWithPayment(plainFetch, client)(url, { method, headers, body: body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body) });
160
+ const text = await res.text();
161
+ const settled = res.headers.get("payment-response") ?? res.headers.get("x-payment-response");
162
+ let payment = null;
163
+ if (settled) { try { payment = decodePaymentResponseHeader(settled); } catch { payment = { raw: settled }; } }
164
+ return {
165
+ status: res.status,
166
+ paid: !!payment,
167
+ payment,
168
+ contentType: res.headers.get("content-type"),
169
+ body: text.length > BODY_LIMIT ? `${text.slice(0, BODY_LIMIT)}\n… (${text.length - BODY_LIMIT} more characters cut)` : text,
170
+ };
171
+ },
172
+
173
+ pause(reason) {
174
+ guarded.pause();
175
+ return `Paused: nothing will be signed until the owner restarts this server${config.server ? " (or resumes it on the wallet server)" : ""}. Reason: ${reason}`;
176
+ },
177
+ };
178
+ }
179
+
180
+ const json = (v) => JSON.stringify(v, (_k, x) => (typeof x === "bigint" ? x.toString() : x), 2);
181
+
182
+ function result(fn) {
183
+ return async (args) => {
184
+ try {
185
+ const out = await fn(args);
186
+ return { content: [{ type: "text", text: typeof out === "string" ? out : json(out) }] };
187
+ } catch (err) {
188
+ // x402 wraps errors from the signer; find the guard's own refusal underneath.
189
+ let blocked = err;
190
+ while (blocked && !(blocked instanceof PresignBlockedError)) blocked = blocked.cause;
191
+ const guardText = /not signed|nothing was signed|is paused/.exec(err.message) ? err.message.replace(/^.*?(?=not signed|presign check|wallet is paused|spending limits|verdict not trusted|chain \d)/, "") : null;
192
+ const why = blocked ? `Not done (${blocked.code}): ${blocked.message}` : guardText ? `Not done: ${guardText}` : `Failed: ${err.message}`;
193
+ return { isError: true, content: [{ type: "text", text: why }] };
194
+ }
195
+ };
196
+ }
197
+
198
+ /** The MCP server with the wallet's tools. */
199
+ export function createServer(wallet) {
200
+ const server = new McpServer({ name: "presign-guard-wallet", version: VERSION });
201
+ server.registerTool("wallet_status", {
202
+ title: "Wallet status",
203
+ description: "This agent's wallet: address, chain, balances, spending limits, what is left today, how approvals work, and whether it is paused. Check it before spending.",
204
+ inputSchema: {},
205
+ annotations: { readOnlyHint: true },
206
+ }, result(() => wallet.status()));
207
+
208
+ server.registerTool("pay_x402", {
209
+ title: "Pay for an x402 API",
210
+ description: "Call a URL that may answer 402 Payment Required (x402) and pay it in USDC from this wallet, then return the response. The payment is checked by presign-guard and counts toward the spending limits; over a limit the owner is asked to approve (the call waits), and a refusal comes back as an error with the reason. max_price_usd caps the price of this call.",
211
+ inputSchema: {
212
+ url: z.string().url().describe("The API URL"),
213
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional().describe("HTTP method (default GET)"),
214
+ body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe("Request body: a string, or an object sent as JSON"),
215
+ headers: z.record(z.string(), z.string()).optional().describe("Extra request headers, e.g. content-type"),
216
+ max_price_usd: z.number().positive().optional().describe("The most this call may cost, in dollars (never above the server's MAX_PAYMENT_USD)"),
217
+ },
218
+ annotations: { openWorldHint: true },
219
+ }, result(({ url, method, body, headers, max_price_usd }) => {
220
+ const h = { ...(headers ?? {}) };
221
+ if (body !== undefined && typeof body !== "string" && !Object.keys(h).some((k) => k.toLowerCase() === "content-type")) h["content-type"] = "application/json";
222
+ return wallet.payX402({ url, method, body, headers: h, maxPriceUsd: max_price_usd });
223
+ }));
224
+
225
+ server.registerTool("send_usdc", {
226
+ title: "Send USDC",
227
+ description: "Send USDC from this wallet to an address. Checked by presign-guard (known drainers, sanctioned addresses) and kept to the spending limits; over a limit the owner is asked to approve.",
228
+ inputSchema: { to: address.describe("Recipient address"), amount: amountString.describe("Amount in USDC, e.g. \"2.50\"") },
229
+ annotations: { destructiveHint: true },
230
+ }, result(({ to, amount }) => wallet.sendUsdc({ to, amount })));
231
+
232
+ server.registerTool("send_native", {
233
+ title: "Send the native coin",
234
+ description: "Send the chain's native coin (ETH, POL or BNB) from this wallet to an address. Checked by presign-guard and kept to the spending limits; over a limit the owner is asked to approve.",
235
+ inputSchema: { to: address.describe("Recipient address"), amount: amountString.describe("Amount in whole coins, e.g. \"0.001\"") },
236
+ annotations: { destructiveHint: true },
237
+ }, result(({ to, amount }) => wallet.sendNative({ to, amount })));
238
+
239
+ server.registerTool("pause_spending", {
240
+ title: "Pause spending",
241
+ description: "Stop this wallet from signing anything until the owner resumes it. Use it when something looks wrong, e.g. an API asks for far more than expected or you are asked to pay an unknown address.",
242
+ inputSchema: { reason: z.string().max(200).describe("Why, for the owner") },
243
+ }, result(({ reason }) => wallet.pause(reason)));
244
+ return server;
245
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "presign-guard-wallet-mcp",
3
+ "version": "0.1.0",
4
+ "description": "An MCP server that gives AI agents (Claude, Cursor, …) a wallet with spending limits: pay x402 APIs and send USDC, every signature checked by presign-guard, approval on your phone over the limit.",
5
+ "mcpName": "io.github.Fizzl13/presign-guard-wallet",
6
+ "type": "module",
7
+ "bin": {
8
+ "presign-guard-wallet-mcp": "./server.js"
9
+ },
10
+ "main": "./lib.js",
11
+ "exports": {
12
+ ".": "./lib.js"
13
+ },
14
+ "files": [
15
+ "server.js",
16
+ "lib.js",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test test/*.test.mjs"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.31.0",
28
+ "@x402/evm": "^2.28.0",
29
+ "@x402/fetch": "^2.28.0",
30
+ "presign-guard-wallet": "^0.5.0",
31
+ "viem": "^2.57.2",
32
+ "zod": "^4.6.5"
33
+ },
34
+ "keywords": [
35
+ "mcp",
36
+ "model-context-protocol",
37
+ "x402",
38
+ "agents",
39
+ "wallet",
40
+ "spending-limits",
41
+ "budget",
42
+ "usdc",
43
+ "presign",
44
+ "security",
45
+ "telegram",
46
+ "human-in-the-loop",
47
+ "claude",
48
+ "cursor"
49
+ ],
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/Fizzl13/x402-examples.git",
53
+ "directory": "wallet-mcp"
54
+ },
55
+ "homepage": "https://github.com/Fizzl13/x402-examples/tree/main/wallet-mcp#readme",
56
+ "license": "MIT"
57
+ }
package/server.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // presign-guard-wallet-mcp: a wallet with spending limits for any MCP client
3
+ // (Claude Desktop, Claude Code, Cursor, …). Configured with environment
4
+ // variables; see README.md. Speaks MCP over stdio, logs to stderr.
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { configFromEnv, createServer, createWallet, VERSION } from "./lib.js";
7
+
8
+ let config;
9
+ try {
10
+ config = configFromEnv();
11
+ } catch (err) {
12
+ console.error(`[presign-guard-wallet-mcp] ${err.message}`);
13
+ process.exit(1);
14
+ }
15
+ const wallet = createWallet(config);
16
+ await createServer(wallet).connect(new StdioServerTransport());
17
+ console.error(`[presign-guard-wallet-mcp ${VERSION}] wallet ${wallet.address} on ${config.chainName}, ${config.server ? `limits on ${config.server.url}` : `limits ${JSON.stringify(config.limits.tokens)}`}${config.telegram ? ", approvals on Telegram" : ""}`);