@blockrun/mcp 0.13.0 → 0.14.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.
Files changed (3) hide show
  1. package/README.md +128 -4
  2. package/dist/index.js +374 -322
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -66,6 +66,14 @@ Prompts and a worked example for these are in [`skills/image-prompting/SKILL.md`
66
66
 
67
67
  ---
68
68
 
69
+ ## Prerequisites
70
+
71
+ - **Node.js ≥ 18** (`node -v`)
72
+ - **~$5 USDC** on Base or Solana (the server auto-creates a wallet on first run; see [Fund your wallet](#fund-your-wallet))
73
+ - **An MCP client**: Claude Code, Claude Desktop, Cursor, Windsurf, or ChatGPT Desktop
74
+
75
+ ---
76
+
69
77
  ## Install
70
78
 
71
79
  **Claude Code (recommended)**
@@ -93,6 +101,23 @@ ensures `-y` is passed to `npx`, not parsed by `claude mcp add`.
93
101
  claude mcp add blockrun -s user --transport http https://mcp.blockrun.ai/mcp
94
102
  ```
95
103
 
104
+ **Cursor** — add to `~/.cursor/mcp.json` (macOS / Linux) or `%APPDATA%\Cursor\mcp.json` (Windows):
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "blockrun": {
109
+ "command": "npx",
110
+ "args": ["-y", "@blockrun/mcp@latest"]
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Windsurf** — same JSON, in:
117
+ - macOS: `~/.codeium/windsurf/mcp_config.json`
118
+ - Linux: `~/.config/.codeium/windsurf/mcp_config.json`
119
+ - Windows: `%APPDATA%\Codeium\windsurf\mcp_config.json`
120
+
96
121
  ---
97
122
 
98
123
  ## Fund your wallet
@@ -118,7 +143,7 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
118
143
  | `blockrun_music` | MiniMax music generation | per track |
119
144
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
120
145
  | `blockrun_markets` | Polymarket (markets, candles, trades, orderbooks, leaderboards, smart-wallet PnL/clusters, UMA oracle), Kalshi, Limitless, Opinion, Predict.Fun, dFlow, Binance Futures, cross-platform match + search | $0.001–0.005/query |
121
- | `blockrun_x` | X/Twitterprofiles, tweets, followers, mentions, search (AttentionVC) | per call |
146
+ | `blockrun_surf` | Surf (asksurf.ai) 84 endpoints: CEX market data, on-chain SQL (13 chains, 80+ ClickHouse tables), 100M+ labeled wallets, Polymarket + Kalshi side-by-side, social mindshare, news, search, Surf-1.5 chat with citations | $0.001–0.02/call |
122
147
  | `blockrun_exa` | Neural web search (Exa) — research, competitors, papers, URL content | $0.01/query |
123
148
  | `blockrun_search` | Grok Live Search — web + news with citations | ~$0.025 per source |
124
149
  | `blockrun_dex` | Live DEX prices via DexScreener | free |
@@ -127,28 +152,127 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
127
152
 
128
153
  ---
129
154
 
155
+ ## Tips for effective LLMs
156
+
157
+ > **For LLMs and agents invoking BlockRun MCP tools:**
158
+
159
+ - **CRITICAL: When any `blockrun_*` tool returns a payment / balance / 402 error, call `blockrun_wallet` *first* to check status, then `action:"setup"` to get funding instructions.** Don't retry the failing tool blindly — the wallet is empty.
160
+ - **CRITICAL: For `blockrun_markets` and `blockrun_surf`, the 80+ endpoint catalog lives in `skills/markets/SKILL.md` and `skills/surf/SKILL.md`, NOT in the tool description.** Browse the skill before guessing endpoint paths.
161
+ - **CRITICAL: `blockrun_chat routing:"smart"` (ClawRouter) only works on Base wallets.** On Solana, pass `mode:` or `model:` to pick a model directly.
162
+ - **CRITICAL: `blockrun_music` and `blockrun_video` are payment-on-completion async.** Failures or client-side timeouts do NOT charge. Don't retry-loop them — they may take 60–180s.
163
+ - **CRITICAL: Before spawning child agents, allocate per-agent budget:** `blockrun_wallet action:"delegate" agent_id:"X" agent_limit:1.00`. Pass `agent_id:"X"` to every downstream `blockrun_*` call — the child is auto-blocked when the budget hits zero.
164
+ - **Free tier first for drafts**: `blockrun_chat mode:"free"` (NVIDIA), `blockrun_dex`, `blockrun_price` (crypto / FX / commodity), and `blockrun_models` are all $0. Use them to scaffold before paying for premium models.
165
+
166
+ ---
167
+
168
+ ## Key Use Cases
169
+
170
+ What kinds of questions can Claude (or any LLM agent) answer once BlockRun MCP is installed:
171
+
172
+ 1. **Price reads / market data**
173
+ > *"What's BTC trading at? Compare with last week's average."* → `blockrun_price` (free) or `blockrun_surf` path:`market/price`
174
+
175
+ 2. **Prediction-market consensus**
176
+ > *"What's Polymarket's odds for the next Fed rate decision?"* → `blockrun_markets` path:`polymarket/events` + filter
177
+
178
+ 3. **On-chain forensics**
179
+ > *"This wallet (0xabc...) — what's it labeled as? What does it hold? When did it whale up?"* → `blockrun_surf` paths:`wallet/labels/batch`, `wallet/detail`, `wallet/net-worth`
180
+
181
+ 4. **Cited research with sources**
182
+ > *"Find the 5 most-cited papers on speculative decoding from the last 90 days. Summarize the dominant approach."* → `blockrun_exa` action:`search` then `contents`
183
+
184
+ 5. **Image generation with on-image text**
185
+ > *"Generate a poster announcing GPT-5.5 on BlockRun, retro-futuristic, with the headline 'NOW LIVE'."* → `blockrun_image` + the `image-prompting` skill 5-section framework
186
+
187
+ 6. **Voice phone-out**
188
+ > *"Call +1-415-555-... and confirm the appointment on Friday at 3pm."* → `blockrun_phone` path:`voice/call`, body: `{ to, task, from }` (provision `from` first via `phone/numbers/buy`), then poll `voice/call/{call_id}`
189
+
190
+ 7. **Multi-agent research with budget cap**
191
+ > *"Spawn 3 research agents on competing L1 narratives. Cap each at $0.50."* → `blockrun_wallet delegate × 3` → children call `blockrun_chat` + `blockrun_exa` with their `agent_id`
192
+
193
+ 8. **Cross-chain SQL forensics**
194
+ > *"Top 10 tokens by DEX volume on Base in the last 24h."* → `blockrun_surf` path:`onchain/sql`, body: `{ sql: "SELECT..." }`
195
+
196
+ ---
197
+
130
198
  ## Why not just use the APIs directly?
131
199
 
132
200
  | | Direct APIs | BlockRun |
133
201
  |---|---|---|
134
202
  | Exa | Sign up, $20/mo minimum | $0.01/call, no subscription |
135
203
  | Polymarket | Undocumented, rate-limited | $0.001/call, clean JSON |
136
- | Twitter/X API | $100–$5000/month | $0.03/page, no approval |
137
- | Multiple sources | 4 accounts, 4 API keys, 4 billing pages | 1 wallet |
204
+ | Surf (asksurf.ai) | Account + monthly plan | $0.001/call, no Surf account, 84 endpoints |
205
+ | Multiple sources | 3 accounts, 3 API keys, 3 billing pages | 1 wallet |
138
206
 
139
207
  One wallet. All sources. No dashboards.
140
208
 
141
209
  ---
142
210
 
211
+ ## When NOT to use BlockRun MCP
212
+
213
+ BlockRun shines when you want **unified billing + many sources + LLM-readable errors**. It is not the right fit for:
214
+
215
+ - **High-volume single-API workloads (≥10k calls/day to one source).** Direct subscriptions amortize better past the break-even point — Polymarket's free public API plus your own caching beats $0.001 × 10k/day if you don't need cross-source aggregation.
216
+ - **Compliance-sensitive flows that need a fiat invoice / audit trail.** BlockRun settles in USDC; receipts are on-chain (Basescan / Solscan) but are not tax invoices. For enterprise procurement, contract directly with the upstream provider.
217
+ - **Latency-critical sub-100ms reads.** Each x402 call adds ~200–500ms of payment-signing + settlement overhead vs. a direct authenticated request. For HFT-style flows, run your own infra.
218
+ - **You only need one source forever.** If you'll only ever call Polymarket, or only ever Exa, save the indirection — sign up upstream and skip the wallet.
219
+
220
+ Use BlockRun when you want pay-per-call for *exploration*, *aggregation*, or *agent-driven* workloads where you can't predict which source you'll reach for next.
221
+
222
+ ---
223
+
143
224
  ## Multi-agent budget delegation
144
225
 
145
226
  Delegate a spending budget to a child agent with `agent_id`. The child is auto-blocked when the budget runs out — useful for autonomous agents that shouldn't run up unbounded costs.
146
227
 
147
228
  ---
148
229
 
230
+ ## Troubleshooting
231
+
232
+ - **`Insufficient balance` / HTTP 402 after retry** → Run `blockrun_wallet action:"setup"`. Send USDC on Base (or Solana — see [Environment Variables](#environment-variables)).
233
+ - **`Smart routing (ClawRouter) is not available on Solana`** → Pass `model:` or `mode:` explicitly to `blockrun_chat`, or switch back to Base by unsetting `SOLANA_WALLET_KEY` and removing `~/.blockrun/.solana-session`.
234
+ - **`claude mcp list` doesn't show `blockrun`** → Check `node -v` (must be ≥18). Clear the npx cache: `rm -rf ~/.npm/_npx`. Re-run the install command from above.
235
+ - **`fetch failed` / timeout when checking wallet balance** → Base RPC transient outage. The tool already falls through 3 public RPCs; retry after 30s. Persistent failures usually = local proxy / firewall blocking outbound RPC.
236
+ - **`ENOENT: ~/.blockrun/.session`** → Expected on first run. The server auto-creates the wallet; check stderr for the `WALLET_CREATED` line confirming the address.
237
+ - **`Video generation timed out` (5-min cap)** → Upstream Seedance / xAI queue congestion. **No charge** (payment-on-completion). Retry, or pick a faster model (`bytedance/seedance-1.5-pro`).
238
+ - **`Music generation timed out` (200s cap)** → Same pattern. **No charge**. Retry; if it persists, the upstream model is rate-limited — try off-peak.
239
+
240
+ ---
241
+
242
+ ## Environment Variables
243
+
244
+ | Variable / File | Default | Effect |
245
+ |---|---|---|
246
+ | `~/.blockrun/.session` | auto-created on first run | EVM private key (0x...). File exists → use Base. |
247
+ | `~/.blockrun/.solana-session` | not created | Solana private key. File exists → switch to Solana. |
248
+ | `SOLANA_WALLET_KEY` | unset | Env-var override of `.solana-session`. Set → use Solana. |
249
+
250
+ Chain selection priority (see `src/utils/wallet.ts:24`):
251
+
252
+ 1. `SOLANA_WALLET_KEY` env var present → Solana
253
+ 2. `~/.blockrun/.solana-session` exists → Solana
254
+ 3. Otherwise → Base (`~/.blockrun/.session` auto-created)
255
+
256
+ **Switching chains:**
257
+
258
+ - Base → Solana: `export SOLANA_WALLET_KEY=...`, or `echo "<secret>" > ~/.blockrun/.solana-session`
259
+ - Solana → Base: `unset SOLANA_WALLET_KEY && rm ~/.blockrun/.solana-session` (the existing `.session` is reused, so it's the same Base wallet)
260
+
261
+ The server also runs a non-blocking npm registry check at startup and prints an `Update available` notice to stderr when a newer `@blockrun/mcp` version exists. Upgrade by re-running the install command — no manual `npm update` needed.
262
+
263
+ ---
264
+
149
265
  ## How it works
150
266
 
151
- Pay-per-call via [x402](https://x402.org) micropayments in USDC on Base. Your wallet lives at `~/.blockrun/.session`. Private key never leaves your machine.
267
+ Pay-per-call via [x402](https://x402.org) micropayments in USDC. Your wallet lives at `~/.blockrun/.session` (Base) or `~/.blockrun/.solana-session` (Solana). The private key never leaves your machine.
268
+
269
+ ---
270
+
271
+ ## Contributing
272
+
273
+ PRs welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, the tool-vs-skill design rule, and how to add a new partner API.
274
+
275
+ Issues: [github.com/blockrunai/blockrun-mcp/issues](https://github.com/blockrunai/blockrun-mcp/issues)
152
276
 
153
277
  ---
154
278
 
package/dist/index.js CHANGED
@@ -6,38 +6,78 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
 
8
8
  // src/utils/wallet.ts
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
9
12
  import {
10
13
  LLMClient,
11
14
  ImageClient,
12
15
  PriceClient,
16
+ SolanaLLMClient,
13
17
  getOrCreateWallet,
18
+ loadSolanaWallet,
14
19
  getPaymentLinks,
15
20
  formatWalletCreatedMessage,
16
- formatNeedsFundingMessage
21
+ formatNeedsFundingMessage,
22
+ SOLANA_WALLET_FILE_PATH
17
23
  } from "@blockrun/llm";
18
- var _client = null;
24
+ var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
25
+ var CHAIN_PREFERENCE_FILES = [
26
+ path.join(BLOCKRUN_DIR, ".chain"),
27
+ path.join(BLOCKRUN_DIR, "payment-chain")
28
+ ];
29
+ var _evmClient = null;
19
30
  var _imageClient = null;
20
31
  var _priceClient = null;
21
- var _walletInfo = null;
22
- function ensureWallet() {
23
- if (!_walletInfo) {
24
- _walletInfo = getOrCreateWallet();
25
- if (_walletInfo.isNew) {
26
- console.error(formatWalletCreatedMessage(_walletInfo.address));
32
+ var _evmWalletInfo = null;
33
+ var _solanaClient = null;
34
+ function readChainPreference() {
35
+ for (const file of CHAIN_PREFERENCE_FILES) {
36
+ try {
37
+ if (!fs.existsSync(file)) continue;
38
+ const value = fs.readFileSync(file, "utf-8").trim().toLowerCase();
39
+ if (value === "base" || value === "solana") return value;
40
+ } catch {
27
41
  }
28
42
  }
29
- return _walletInfo;
43
+ return null;
44
+ }
45
+ function getChain() {
46
+ const preferred = readChainPreference();
47
+ if (preferred) return preferred;
48
+ if (process.env.SOLANA_WALLET_KEY) return "solana";
49
+ try {
50
+ if (fs.existsSync(SOLANA_WALLET_FILE_PATH)) return "solana";
51
+ } catch {
52
+ }
53
+ return "base";
54
+ }
55
+ function ensureEvmWallet() {
56
+ if (!_evmWalletInfo) {
57
+ _evmWalletInfo = getOrCreateWallet();
58
+ if (_evmWalletInfo.isNew) {
59
+ console.error(formatWalletCreatedMessage(_evmWalletInfo.address));
60
+ }
61
+ }
62
+ return _evmWalletInfo;
30
63
  }
31
64
  function getOrCreateWalletKey() {
32
- const info = ensureWallet();
65
+ const info = ensureEvmWallet();
33
66
  return info.privateKey;
34
67
  }
35
68
  function getClient() {
36
- if (!_client) {
69
+ if (getChain() === "solana") {
70
+ if (!_solanaClient) {
71
+ const privateKey = process.env.SOLANA_WALLET_KEY || loadSolanaWallet() || void 0;
72
+ _solanaClient = new SolanaLLMClient(privateKey ? { privateKey } : void 0);
73
+ }
74
+ return _solanaClient;
75
+ }
76
+ if (!_evmClient) {
37
77
  const privateKey = getOrCreateWalletKey();
38
- _client = new LLMClient({ privateKey });
78
+ _evmClient = new LLMClient({ privateKey });
39
79
  }
40
- return _client;
80
+ return _evmClient;
41
81
  }
42
82
  function getImageClient() {
43
83
  if (!_imageClient) {
@@ -53,8 +93,21 @@ function getPriceClient() {
53
93
  }
54
94
  return _priceClient;
55
95
  }
56
- function getWalletInfo() {
57
- const info = ensureWallet();
96
+ async function getWalletInfo() {
97
+ if (getChain() === "solana") {
98
+ const client = getClient();
99
+ const address = await client.getWalletAddress();
100
+ return {
101
+ address,
102
+ network: "Solana",
103
+ chainId: null,
104
+ currency: "USDC",
105
+ isNew: false,
106
+ explorerUrl: `https://solscan.io/account/${address}`,
107
+ fundingUrl: "https://sol.blockrun.ai"
108
+ };
109
+ }
110
+ const info = ensureEvmWallet();
58
111
  const links = getPaymentLinks(info.address);
59
112
  return {
60
113
  address: info.address,
@@ -62,11 +115,19 @@ function getWalletInfo() {
62
115
  chainId: 8453,
63
116
  currency: "USDC",
64
117
  isNew: info.isNew,
65
- basescanUrl: links.basescan,
118
+ explorerUrl: links.basescan,
66
119
  fundingUrl: links.blockrun
67
120
  };
68
121
  }
69
122
  async function getUsdcBalance(address) {
123
+ if (getChain() === "solana") {
124
+ try {
125
+ const client = getClient();
126
+ return await client.getBalance();
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
70
131
  const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
71
132
  const BASE_RPC_URLS = [
72
133
  "https://mainnet.base.org",
@@ -101,14 +162,15 @@ import { z } from "zod";
101
162
  // src/utils/qr.ts
102
163
  import QRCode from "qrcode";
103
164
  import open from "open";
104
- import * as fs from "fs";
165
+ import * as fs2 from "fs";
166
+ import sharp from "sharp";
105
167
 
106
168
  // src/utils/constants.ts
107
- import * as path from "path";
108
- import * as os from "os";
109
- var WALLET_DIR = path.join(os.homedir(), ".blockrun");
110
- var WALLET_FILE = path.join(WALLET_DIR, ".session");
111
- var QR_FILE = path.join(WALLET_DIR, "qr.png");
169
+ import * as path2 from "path";
170
+ import * as os2 from "os";
171
+ var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
172
+ var WALLET_FILE = path2.join(WALLET_DIR, ".session");
173
+ var QR_FILE = path2.join(WALLET_DIR, "qr.png");
112
174
  var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
113
175
  var BASE_CHAIN_ID = "8453";
114
176
  var MODEL_TIERS = {
@@ -123,22 +185,53 @@ var MODEL_TIERS = {
123
185
  };
124
186
 
125
187
  // src/utils/qr.ts
188
+ var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
126
189
  function getEip681Uri(address, amountUsdc = 1) {
127
190
  const amountWei = Math.floor(amountUsdc * 1e6);
128
191
  return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
129
192
  }
130
- async function generateQrPng(address) {
131
- const eip681Uri = getEip681Uri(address);
132
- if (!fs.existsSync(WALLET_DIR)) {
133
- fs.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
193
+ function getSolanaPayUri(address, amountUsdc = 1) {
194
+ return `solana:${address}?spl-token=${SOLANA_USDC_MINT}&amount=${amountUsdc}&label=BlockRun`;
195
+ }
196
+ function buildSolanaLogoSvg(size) {
197
+ const half = size / 2;
198
+ return `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
199
+ <defs>
200
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
201
+ <stop offset="0%" style="stop-color:#9945FF"/>
202
+ <stop offset="100%" style="stop-color:#14F195"/>
203
+ </linearGradient>
204
+ <clipPath id="c"><circle cx="${half}" cy="${half}" r="${half}"/></clipPath>
205
+ </defs>
206
+ <circle cx="${half}" cy="${half}" r="${half}" fill="url(#g)" clip-path="url(#c)"/>
207
+ <text x="${half}" y="${half + 14}" font-size="40" font-weight="bold" fill="white"
208
+ font-family="Arial,sans-serif" text-anchor="middle">\u25CE</text>
209
+ </svg>`;
210
+ }
211
+ async function overlayLogo(qrBuf, chain, qrSize) {
212
+ if (chain !== "solana") return qrBuf;
213
+ const logoSize = Math.round(qrSize * 0.18);
214
+ const pad = Math.round(logoSize * 0.08);
215
+ const logoBuf = await sharp(Buffer.from(buildSolanaLogoSvg(logoSize))).resize(logoSize, logoSize).extend({ top: pad, bottom: pad, left: pad, right: pad, background: { r: 255, g: 255, b: 255, alpha: 1 } }).toBuffer();
216
+ const totalSize = logoSize + pad * 2;
217
+ const offset = Math.round((qrSize - totalSize) / 2);
218
+ return sharp(qrBuf).composite([{ input: logoBuf, left: offset, top: offset }]).toBuffer();
219
+ }
220
+ async function generateQrPng(address, chain = "base") {
221
+ const uri = chain === "solana" ? getSolanaPayUri(address) : getEip681Uri(address);
222
+ const qrSize = 400;
223
+ if (!fs2.existsSync(WALLET_DIR)) {
224
+ fs2.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
134
225
  }
135
- await QRCode.toFile(QR_FILE, eip681Uri, {
226
+ const qrBuf = await QRCode.toBuffer(uri, {
136
227
  type: "png",
137
- width: 400,
228
+ width: qrSize,
138
229
  margin: 2,
139
230
  errorCorrectionLevel: "H",
140
231
  color: { dark: "#000000", light: "#FFFFFF" }
141
232
  });
233
+ const finalBuf = await overlayLogo(qrBuf, chain, qrSize);
234
+ fs2.writeFileSync(QR_FILE, finalBuf);
142
235
  return QR_FILE;
143
236
  }
144
237
  async function openQrInViewer(qrPath) {
@@ -149,6 +242,32 @@ async function openQrInViewer(qrPath) {
149
242
  }
150
243
 
151
244
  // src/utils/errors.ts
245
+ function extractErrorMessage(err) {
246
+ if (!err || typeof err !== "object") return String(err);
247
+ const e = err;
248
+ const base = typeof e.message === "string" ? e.message : String(err);
249
+ if (e.response === void 0 || e.response === null) return base;
250
+ try {
251
+ const body = e.response;
252
+ if (typeof body === "string") return body.trim() ? `${base} \u2014 ${body}` : base;
253
+ if (typeof body === "object") {
254
+ const b = body;
255
+ const parts = [];
256
+ if (typeof b.message === "string") parts.push(b.message);
257
+ if (typeof b.hint === "string") parts.push(`Hint: ${b.hint}`);
258
+ if (Array.isArray(b.missing_params) && b.missing_params.length) {
259
+ parts.push(`Missing: ${b.missing_params.join(", ")}`);
260
+ }
261
+ if (parts.length === 0) {
262
+ parts.push(JSON.stringify(b));
263
+ }
264
+ return `${base}
265
+ ${parts.join("\n")}`;
266
+ }
267
+ } catch {
268
+ }
269
+ return base;
270
+ }
152
271
  function formatError(message) {
153
272
  const msgLower = message.toLowerCase();
154
273
  const isPaymentError = msgLower.includes("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !msgLower.includes("500");
@@ -157,15 +276,17 @@ function formatError(message) {
157
276
  if (isServerError) {
158
277
  errorText += `
159
278
 
160
- This is a temporary API issue. The xAI/Grok API may be experiencing problems.
279
+ This is a temporary API issue. The API may be experiencing problems.
161
280
  Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
162
281
  } else if (isPaymentError) {
282
+ const chain = getChain();
283
+ const network = chain === "solana" ? "Solana" : "Base";
163
284
  errorText += `
164
285
 
165
286
  This error usually means your wallet needs funding.
166
287
  Run blockrun_wallet with action: "setup" to get funding instructions.
167
288
 
168
- Quick fix: Send USDC to your wallet on Base network.`;
289
+ Quick fix: Send USDC to your wallet on ${network} network.`;
169
290
  }
170
291
  return errorText;
171
292
  }
@@ -210,8 +331,9 @@ Do NOT call this for actual AI queries \u2014 use blockrun_chat for that.`,
210
331
  }
211
332
  },
212
333
  async ({ action, budget_action, budget_amount, agent_id, agent_limit }) => {
213
- const info = getWalletInfo();
334
+ const info = await getWalletInfo();
214
335
  const address = info.address;
336
+ const chain = getChain();
215
337
  if (action === "budget") {
216
338
  const budgetAct = budget_action || "check";
217
339
  if (budgetAct === "set") {
@@ -289,10 +411,11 @@ Pass agent_id: "${agent_id}" in any blockrun_* tool call to track and enforce th
289
411
  }
290
412
  if (action === "qr") {
291
413
  try {
292
- const qrPath = await generateQrPng(address);
414
+ const qrPath = await generateQrPng(address, chain);
293
415
  await openQrInViewer(qrPath);
416
+ const scanNote = chain === "solana" ? "Scan with a Solana wallet (Phantom, Solflare) to send USDC on Solana." : "Scan with MetaMask to send USDC on Base.";
294
417
  return {
295
- content: [{ type: "text", text: `QR code opened! Scan with MetaMask to send USDC on Base.
418
+ content: [{ type: "text", text: `QR code opened! ${scanNote}
296
419
 
297
420
  Address: ${address}
298
421
  QR saved: ${qrPath}` }]
@@ -307,14 +430,46 @@ QR saved: ${qrPath}` }]
307
430
  if (action === "setup") {
308
431
  let qrMessage = "";
309
432
  try {
310
- const qrPath = await generateQrPng(address);
433
+ const qrPath = await generateQrPng(address, chain);
311
434
  await openQrInViewer(qrPath);
312
435
  qrMessage = `
313
436
  QR code opened for scanning! (${qrPath})`;
314
437
  } catch {
315
438
  qrMessage = "\n(QR generation failed - use address above)";
316
439
  }
317
- const text2 = `
440
+ const text2 = chain === "solana" ? `
441
+ ================================================================================
442
+ BLOCKRUN WALLET SETUP (SOLANA)
443
+ ================================================================================
444
+
445
+ Your Solana wallet address: ${address}
446
+ ${qrMessage}
447
+
448
+ HOW TO FUND YOUR WALLET:
449
+ ------------------------
450
+
451
+ Option 1: Transfer from Coinbase
452
+ 1. Open Coinbase app or website
453
+ 2. Go to Send/Receive \u2192 Select USDC
454
+ 3. Choose "Solana" network (important!)
455
+ 4. Paste: ${address}
456
+ 5. Send $1-5 to start
457
+
458
+ Option 2: Transfer from any Solana wallet (Phantom, Solflare, Backpack)
459
+ - Send USDC (SPL) to: ${address}
460
+ - Make sure to use Solana network, not EVM
461
+
462
+ Option 3: Bridge from other chains
463
+ https://portalbridge.com \u2192 Bridge USDC to Solana \u2192 Send to address above
464
+
465
+ VERIFY BALANCE: https://solscan.io/account/${address}
466
+
467
+ PRICING (pay per use):
468
+ - GPT-4o: ~$0.005/request | Claude Sonnet: ~$0.003/request
469
+ - Gemini Flash: ~$0.0001/request | Full pricing: https://blockrun.ai/pricing
470
+
471
+ SECURITY: Private key stored at ~/.blockrun/.solana-session (never leaves your machine)
472
+ ================================================================================` : `
318
473
  ================================================================================
319
474
  BLOCKRUN WALLET SETUP
320
475
  ================================================================================
@@ -351,9 +506,10 @@ SECURITY: Private key stored at ~/.blockrun/.session (never leaves your machine)
351
506
  const balance = await getUsdcBalance(address);
352
507
  const balanceStr = balance !== null ? `$${balance.toFixed(6)} USDC` : "Unable to fetch";
353
508
  const lowBalance = balance !== null && balance < 1;
509
+ const explorerLabel = chain === "solana" ? "Solscan" : "Basescan";
354
510
  const text = `Wallet: ${address}
355
511
  Balance: ${balanceStr}${lowBalance ? " (low - add funds)" : ""}
356
- Network: Base | View: ${info.basescanUrl}
512
+ Network: ${info.network} | View: ${info.explorerUrl}
357
513
  ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions" : ""}`;
358
514
  return {
359
515
  content: [{ type: "text", text }],
@@ -363,7 +519,8 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
363
519
  network: info.network,
364
520
  chainId: info.chainId,
365
521
  isNew: info.isNew,
366
- basescanUrl: info.basescanUrl
522
+ explorerUrl: info.explorerUrl,
523
+ explorerLabel
367
524
  }
368
525
  };
369
526
  }
@@ -372,6 +529,7 @@ ${info.isNew ? "\nNEW WALLET - Run with action: 'setup' for funding instructions
372
529
 
373
530
  // src/tools/chat.ts
374
531
  import { z as z2 } from "zod";
532
+ import { LLMClient as LLMClient2 } from "@blockrun/llm";
375
533
 
376
534
  // src/utils/budget.ts
377
535
  function checkBudget(budget, agentId) {
@@ -448,6 +606,12 @@ Run blockrun_models to see all 41+ models with pricing.`,
448
606
  };
449
607
  }
450
608
  if (routing === "smart") {
609
+ if (!(llm instanceof LLMClient2)) {
610
+ return {
611
+ content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
612
+ isError: true
613
+ };
614
+ }
451
615
  try {
452
616
  const result = await llm.smartChat(message, {
453
617
  system,
@@ -1006,39 +1170,31 @@ function registerSearchTool(server) {
1006
1170
  server.registerTool(
1007
1171
  "blockrun_search",
1008
1172
  {
1009
- description: `Real-time web, X/Twitter, and news search with AI-summarized results and citations.
1173
+ description: `Grok Live Search \u2014 real-time web + X/Twitter + news with AI-summarized results and citations. ~$0.025 per source.
1174
+
1175
+ Common shape:
1176
+ - body: { query: "...", sources: ["web","x","news"], maxResults: 10, fromDate: "YYYY-MM-DD", toDate: "YYYY-MM-DD" }
1010
1177
 
1011
- Sources: web, x (X/Twitter), news \u2014 defaults to all three.
1012
- Pricing: ~$0.01/search
1178
+ \`sources\` accepts any subset of ["web","x","news"] (defaults to all three). For tweet-only searches, use ["x"].
1013
1179
 
1014
- Returns a summary with cited sources.`,
1180
+ Full request shape + worked examples in the \`search\` skill (\`skills/search/SKILL.md\`).`,
1015
1181
  inputSchema: {
1016
- query: z7.string().describe("Search query"),
1017
- sources: z7.array(z7.enum(["web", "x", "news"])).optional().describe("Sources to search (default: web + x + news)"),
1018
- max_results: z7.number().optional().default(10).describe("Max results per source (1-20)"),
1019
- from_date: z7.string().optional().describe("Start date filter (YYYY-MM-DD)"),
1020
- to_date: z7.string().optional().describe("End date filter (YYYY-MM-DD)")
1182
+ path: z7.string().optional().default("").describe("Endpoint sub-path under /v1/search/ (default empty = root /v1/search). Reserved for future surfaces."),
1183
+ body: z7.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST.")
1021
1184
  }
1022
1185
  },
1023
- async ({ query, sources, max_results, from_date, to_date }) => {
1186
+ async ({ path: path4, body }) => {
1024
1187
  try {
1025
- const llm = getClient();
1026
- const result = await llm.search(query, {
1027
- sources,
1028
- maxResults: max_results,
1029
- fromDate: from_date,
1030
- toDate: to_date
1031
- });
1188
+ const client = getClient();
1189
+ const cleanPath = (path4 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
1190
+ const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
1191
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1032
1192
  return {
1033
1193
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1034
1194
  structuredContent: result
1035
1195
  };
1036
1196
  } catch (err) {
1037
- const errMsg = err instanceof Error ? err.message : String(err);
1038
- return {
1039
- content: [{ type: "text", text: formatError(errMsg) }],
1040
- isError: true
1041
- };
1197
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1042
1198
  }
1043
1199
  }
1044
1200
  );
@@ -1050,68 +1206,34 @@ function registerExaTool(server) {
1050
1206
  server.registerTool(
1051
1207
  "blockrun_exa",
1052
1208
  {
1053
- description: `Neural web search via Exa. Understands meaning, not just keywords. Great for research.
1209
+ description: `Neural web search via Exa \u2014 understands meaning, not just keywords. Great for research.
1054
1210
 
1055
- Actions:
1056
- - search: Find semantically relevant URLs and metadata ($0.01/call)
1057
- - answer: Get a cited, hallucination-free answer grounded in real web sources ($0.01/call)
1058
- - contents: Fetch full Markdown text from URLs, ready for LLM context ($0.002/URL)
1059
- - similar: Find pages semantically similar to a given URL ($0.01/call)`,
1211
+ Common paths (all POST, body shapes documented in the exa-research skill):
1212
+ - search \u2014 body: { query, numResults?, category?, includeDomains?, excludeDomains? } ($0.01/call)
1213
+ - answer \u2014 body: { query } ($0.01/call)
1214
+ - contents \u2014 body: { urls: [...] } ($0.002/URL, up to 100)
1215
+ - find-similar \u2014 body: { url, numResults? } ($0.01/call)
1216
+
1217
+ Categories for search: "news", "research paper", "company", "tweet", "github", "pdf".
1218
+
1219
+ Full request/response shapes + worked research workflows in the \`exa-research\` skill.`,
1060
1220
  inputSchema: {
1061
- action: z8.enum(["search", "answer", "contents", "similar"]).describe("Action to perform"),
1062
- query: z8.string().optional().describe("Natural language query (for search/answer)"),
1063
- url: z8.string().optional().describe("Reference URL to find similar pages (for similar action)"),
1064
- urls: z8.array(z8.string()).optional().describe("URLs to fetch content from (for contents action, up to 100)"),
1065
- num_results: z8.number().optional().describe("Number of results to return (default: 10)"),
1066
- category: z8.string().optional().describe("Category filter: 'news', 'research paper', 'company', 'tweet', 'github', 'pdf'"),
1067
- include_domains: z8.array(z8.string()).optional().describe("Only search within these domains"),
1068
- exclude_domains: z8.array(z8.string()).optional().describe("Exclude these domains from results")
1221
+ path: z8.string().describe("Endpoint name under /v1/exa/, e.g. 'search', 'answer', 'contents', 'find-similar'"),
1222
+ body: z8.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints.")
1069
1223
  }
1070
1224
  },
1071
- async ({ action, query, url, urls, num_results, category, include_domains, exclude_domains }) => {
1225
+ async ({ path: path4, body }) => {
1072
1226
  try {
1073
- const llm = getClient();
1074
- let result;
1075
- const req = llm;
1076
- switch (action) {
1077
- case "search":
1078
- if (!query) throw new Error("query required for search action");
1079
- result = await req.requestWithPaymentRaw("/v1/exa/search", {
1080
- query,
1081
- numResults: num_results,
1082
- category,
1083
- includeDomains: include_domains,
1084
- excludeDomains: exclude_domains
1085
- });
1086
- break;
1087
- case "answer":
1088
- if (!query) throw new Error("query required for answer action");
1089
- result = await req.requestWithPaymentRaw("/v1/exa/answer", { query });
1090
- break;
1091
- case "contents":
1092
- if (!urls?.length) throw new Error("urls array required for contents action");
1093
- result = await req.requestWithPaymentRaw("/v1/exa/contents", { urls });
1094
- break;
1095
- case "similar":
1096
- if (!url) throw new Error("url required for similar action");
1097
- result = await req.requestWithPaymentRaw("/v1/exa/find-similar", {
1098
- url,
1099
- numResults: num_results
1100
- });
1101
- break;
1102
- default:
1103
- throw new Error(`Unknown action: ${action}`);
1104
- }
1227
+ const client = getClient();
1228
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1229
+ const endpoint = `/v1/exa/${cleanPath}`;
1230
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1105
1231
  return {
1106
1232
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1107
1233
  structuredContent: result
1108
1234
  };
1109
1235
  } catch (err) {
1110
- const errMsg = err instanceof Error ? err.message : String(err);
1111
- return {
1112
- content: [{ type: "text", text: formatError(errMsg) }],
1113
- isError: true
1114
- };
1236
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1115
1237
  }
1116
1238
  }
1117
1239
  );
@@ -1180,10 +1302,10 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
1180
1302
  body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)")
1181
1303
  }
1182
1304
  },
1183
- async ({ path: path3, params, body }) => {
1305
+ async ({ path: path4, params, body }) => {
1184
1306
  try {
1185
1307
  const llm = getClient();
1186
- const result = body ? await llm.pmQuery(path3, body) : await llm.pm(path3, params);
1308
+ const result = body ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
1187
1309
  return {
1188
1310
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1189
1311
  structuredContent: result
@@ -1302,132 +1424,8 @@ Examples:
1302
1424
  );
1303
1425
  }
1304
1426
 
1305
- // src/tools/twitter.ts
1306
- import { z as z11 } from "zod";
1307
- var ACTION2 = z11.enum([
1308
- "user_lookup",
1309
- "user_info",
1310
- "followers",
1311
- "followings",
1312
- "verified_followers",
1313
- "user_tweets",
1314
- "user_mentions",
1315
- "tweet_lookup",
1316
- "tweet_replies",
1317
- "tweet_thread",
1318
- "search"
1319
- ]);
1320
- function registerTwitterTool(server) {
1321
- server.registerTool(
1322
- "blockrun_x",
1323
- {
1324
- description: `Structured X/Twitter data via AttentionVC partner API.
1325
-
1326
- Actions:
1327
- - user_lookup (usernames: string | string[])
1328
- - user_info (username)
1329
- - followers (username, cursor?)
1330
- - followings (username, cursor?)
1331
- - verified_followers (user_id, cursor?)
1332
- - user_tweets (username, includeReplies?, cursor?)
1333
- - user_mentions (username, sinceTime?, untilTime?, cursor?)
1334
- - tweet_lookup (tweet_ids: string | string[])
1335
- - tweet_replies (tweet_id, cursor?, queryType?)
1336
- - tweet_thread (tweet_id, cursor?)
1337
- - search (query, queryType?, cursor?)
1338
-
1339
- Paid per request via x402; prices scale with the endpoint (e.g. user_lookup ~ $0.02, followers ~ $0.05/page).`,
1340
- inputSchema: {
1341
- action: ACTION2,
1342
- usernames: z11.union([z11.string(), z11.array(z11.string())]).optional(),
1343
- username: z11.string().optional(),
1344
- user_id: z11.string().optional(),
1345
- tweet_id: z11.string().optional(),
1346
- tweet_ids: z11.union([z11.string(), z11.array(z11.string())]).optional(),
1347
- query: z11.string().optional(),
1348
- queryType: z11.enum(["Latest", "Top", "Default"]).optional(),
1349
- cursor: z11.string().optional(),
1350
- sinceTime: z11.string().optional(),
1351
- untilTime: z11.string().optional(),
1352
- includeReplies: z11.boolean().optional()
1353
- }
1354
- },
1355
- async (args) => {
1356
- try {
1357
- const llm = getClient();
1358
- const a = args.action;
1359
- const require2 = (value, name) => {
1360
- if (value === void 0 || value === null || value === "") {
1361
- throw new Error(`${name} is required for action='${a}'`);
1362
- }
1363
- return value;
1364
- };
1365
- let result;
1366
- switch (a) {
1367
- case "user_lookup":
1368
- result = await llm.xUserLookup(require2(args.usernames, "usernames"));
1369
- break;
1370
- case "user_info":
1371
- result = await llm.xUserInfo(require2(args.username, "username"));
1372
- break;
1373
- case "followers":
1374
- result = await llm.xFollowers(require2(args.username, "username"), args.cursor);
1375
- break;
1376
- case "followings":
1377
- result = await llm.xFollowings(require2(args.username, "username"), args.cursor);
1378
- break;
1379
- case "verified_followers":
1380
- result = await llm.xVerifiedFollowers(require2(args.user_id, "user_id"), args.cursor);
1381
- break;
1382
- case "user_tweets":
1383
- result = await llm.xUserTweets(
1384
- require2(args.username, "username"),
1385
- args.includeReplies,
1386
- args.cursor
1387
- );
1388
- break;
1389
- case "user_mentions":
1390
- result = await llm.xUserMentions(
1391
- require2(args.username, "username"),
1392
- args.sinceTime,
1393
- args.untilTime,
1394
- args.cursor
1395
- );
1396
- break;
1397
- case "tweet_lookup":
1398
- result = await llm.xTweetLookup(require2(args.tweet_ids, "tweet_ids"));
1399
- break;
1400
- case "tweet_replies":
1401
- result = await llm.xTweetReplies(
1402
- require2(args.tweet_id, "tweet_id"),
1403
- args.queryType,
1404
- args.cursor
1405
- );
1406
- break;
1407
- case "tweet_thread":
1408
- result = await llm.xTweetThread(require2(args.tweet_id, "tweet_id"), args.cursor);
1409
- break;
1410
- case "search":
1411
- result = await llm.xSearch(require2(args.query, "query"), args.queryType, args.cursor);
1412
- break;
1413
- }
1414
- return {
1415
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1416
- structuredContent: result
1417
- };
1418
- } catch (err) {
1419
- const msg = err instanceof Error ? err.message : String(err);
1420
- return {
1421
- content: [{ type: "text", text: formatError(msg) }],
1422
- isError: true
1423
- };
1424
- }
1425
- }
1426
- );
1427
- }
1428
-
1429
1427
  // src/tools/dex.ts
1430
- import { z as z12 } from "zod";
1428
+ import { z as z11 } from "zod";
1431
1429
  function registerDexTool(server) {
1432
1430
  server.registerTool(
1433
1431
  "blockrun_dex",
@@ -1444,10 +1442,10 @@ Examples:
1444
1442
  blockrun_dex({ token: "So11...xxx" }) -> Get specific token data
1445
1443
  blockrun_dex({ symbol: "PEPE" }) -> Search by symbol`,
1446
1444
  inputSchema: {
1447
- query: z12.string().optional().describe("Search query (token name, symbol, or address)"),
1448
- token: z12.string().optional().describe("Token address for direct lookup"),
1449
- symbol: z12.string().optional().describe("Token symbol to search"),
1450
- chain: z12.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
1445
+ query: z11.string().optional().describe("Search query (token name, symbol, or address)"),
1446
+ token: z11.string().optional().describe("Token address for direct lookup"),
1447
+ symbol: z11.string().optional().describe("Token symbol to search"),
1448
+ chain: z11.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
1451
1449
  }
1452
1450
  },
1453
1451
  async ({ query, token, symbol, chain }) => {
@@ -1508,85 +1506,135 @@ ${lines.join("\n\n")}` }],
1508
1506
  }
1509
1507
 
1510
1508
  // src/tools/modal.ts
1511
- import { z as z13 } from "zod";
1512
- var MODAL_GPU_TYPES = ["T4", "L4", "A10G", "A100", "A100-80GB", "H100"];
1509
+ import { z as z12 } from "zod";
1513
1510
  function registerModalTool(server) {
1514
1511
  server.registerTool(
1515
1512
  "blockrun_modal",
1516
1513
  {
1517
- description: `Run isolated code in a BlockRun-hosted Modal sandbox.
1514
+ description: `Run isolated code in a BlockRun-hosted Modal sandbox \u2014 disposable remote container, optional GPU.
1518
1515
 
1519
- Use this when you need:
1520
- - a disposable remote container
1521
- - GPU access
1522
- - a clean environment that will not affect the local machine
1523
- - a safer place to run untrusted or heavy code
1516
+ Use when you need: a clean ephemeral environment, GPU access (T4/L4/A10G/A100/A100-80GB/H100), or a safer place for untrusted code. Prefer local tools for normal repo work.
1524
1517
 
1525
- Prefer local tools for normal repo work. Modal is best for isolation or remote execution.
1518
+ Common paths (all POST):
1519
+ - sandbox/create \u2014 body: { image?, timeout?, cpu?, memory?, gpu?, setup_commands? } ($0.01)
1520
+ - sandbox/exec \u2014 body: { sandbox_id, command: ["python","-c","..."], timeout? } ($0.001)
1521
+ - sandbox/status \u2014 body: { sandbox_id } ($0.001)
1522
+ - sandbox/terminate \u2014 body: { sandbox_id } ($0.001)
1526
1523
 
1527
- Pricing:
1528
- - create: $0.01
1529
- - exec/status/terminate: $0.001 each`,
1524
+ Full action shapes + GPU type details in the \`modal\` skill.`,
1530
1525
  inputSchema: {
1531
- action: z13.enum(["create", "exec", "status", "terminate"]).describe(
1532
- "Sandbox action to perform"
1533
- ),
1534
- sandbox_id: z13.string().optional().describe("Sandbox ID returned by a previous create"),
1535
- command: z13.array(z13.string()).optional().describe('Command array for exec, for example ["python", "-c", "print(2+2)"]'),
1536
- image: z13.string().optional().describe("Container image for create (default: python:3.11)"),
1537
- timeout: z13.number().optional().describe("Timeout in seconds for create or exec"),
1538
- cpu: z13.number().optional().describe("CPU cores for create"),
1539
- memory: z13.number().optional().describe("Memory in MB for create"),
1540
- gpu: z13.enum(MODAL_GPU_TYPES).optional().describe("Optional GPU type for create"),
1541
- setup_commands: z13.array(z13.string()).optional().describe("Shell commands to run during sandbox setup")
1526
+ path: z12.string().describe("Endpoint under /v1/modal/, e.g. 'sandbox/create', 'sandbox/exec'"),
1527
+ body: z12.any().optional().describe("JSON body. Sent as POST.")
1542
1528
  }
1543
1529
  },
1544
- async ({ action, sandbox_id, command, image, timeout, cpu, memory, gpu, setup_commands }) => {
1530
+ async ({ path: path4, body }) => {
1545
1531
  try {
1546
- const llm = getClient();
1547
- const req = llm;
1548
- let result;
1549
- switch (action) {
1550
- case "create":
1551
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/create", {
1552
- image,
1553
- timeout,
1554
- cpu,
1555
- memory,
1556
- gpu,
1557
- setup_commands
1558
- });
1559
- break;
1560
- case "exec":
1561
- if (!sandbox_id) throw new Error("sandbox_id required for exec action");
1562
- if (!command?.length) throw new Error("command array required for exec action");
1563
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/exec", {
1564
- sandbox_id,
1565
- command,
1566
- timeout
1567
- });
1568
- break;
1569
- case "status":
1570
- if (!sandbox_id) throw new Error("sandbox_id required for status action");
1571
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/status", { sandbox_id });
1572
- break;
1573
- case "terminate":
1574
- if (!sandbox_id) throw new Error("sandbox_id required for terminate action");
1575
- result = await req.requestWithPaymentRaw("/v1/modal/sandbox/terminate", {
1576
- sandbox_id
1577
- });
1578
- break;
1579
- default:
1580
- throw new Error(`Unknown action: ${String(action)}`);
1581
- }
1532
+ const client = getClient();
1533
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
1534
+ const endpoint = `/v1/modal/${cleanPath}`;
1535
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1582
1536
  return {
1583
1537
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1584
1538
  structuredContent: result
1585
1539
  };
1586
1540
  } catch (err) {
1587
- const errMsg = err instanceof Error ? err.message : String(err);
1541
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1542
+ }
1543
+ }
1544
+ );
1545
+ }
1546
+
1547
+ // src/tools/phone.ts
1548
+ import { z as z13 } from "zod";
1549
+ function registerPhoneTool(server) {
1550
+ server.registerTool(
1551
+ "blockrun_phone",
1552
+ {
1553
+ description: `Phone-number intelligence, US/CA number provisioning, and outbound AI voice calls.
1554
+
1555
+ Common paths (path = everything after /v1/):
1556
+ - phone/lookup POST body: { phoneNumber } ($0.01)
1557
+ - phone/lookup/fraud POST body: { phoneNumber } \u2014 SIM-swap + call-forwarding signals ($0.05)
1558
+ - phone/numbers/buy POST body: { country?: "US"|"CA", areaCode? } \u2014 30-day lease ($5.00)
1559
+ - phone/numbers/renew POST body: { phoneNumber } \u2014 extend 30 days ($5.00)
1560
+ - phone/numbers/list POST body: {} \u2014 your wallet-owned numbers ($0.001)
1561
+ - phone/numbers/release POST body: { phoneNumber } \u2014 release back to pool (free)
1562
+ - voice/call POST body: { to, task, from, voice?, max_duration?, ... } ($0.54 flat)
1563
+ - voice/call/{call_id} GET (no body) \u2014 poll status + transcript (free)
1564
+
1565
+ REQUIRED for voice/call: \`from\` must be a number your wallet owns. Provision one with \`phone/numbers/buy\` first ($5, 30-day lease).
1566
+
1567
+ Voice presets: nat, josh, maya, june, paige, derek, florian. Phone numbers use E.164: +14155550100.
1568
+
1569
+ Voice call flow + voice preset details + full body shapes in the \`phone\` skill.`,
1570
+ inputSchema: {
1571
+ path: z13.string().describe("Endpoint after /v1/. Use 'phone/...' for lookup + number ops, 'voice/call' for outbound AI calls, 'voice/call/{id}' (no body) to poll status."),
1572
+ body: z13.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id}).")
1573
+ }
1574
+ },
1575
+ async ({ path: path4, body }) => {
1576
+ try {
1577
+ const client = getClient();
1578
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\//, "");
1579
+ const endpoint = `/v1/${cleanPath}`;
1580
+ const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
1588
1581
  return {
1589
- content: [{ type: "text", text: formatError(errMsg) }],
1582
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1583
+ structuredContent: result
1584
+ };
1585
+ } catch (err) {
1586
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
1587
+ }
1588
+ }
1589
+ );
1590
+ }
1591
+
1592
+ // src/tools/surf.ts
1593
+ import { z as z14 } from "zod";
1594
+ function registerSurfTool(server) {
1595
+ server.registerTool(
1596
+ "blockrun_surf",
1597
+ {
1598
+ description: `Unified crypto data via Surf (asksurf.ai) \u2014 84 endpoints, one API.
1599
+
1600
+ Coverage: CEX market data (16 exchanges), on-chain SQL across 13 chains, 100M+ labeled wallets, prediction markets (Polymarket + Kalshi), social mindshare / CT intelligence, news, unified search, and Surf-1.5 chat with citations.
1601
+
1602
+ Pricing (settled in USDC to Surf's Base treasury):
1603
+ - Tier 1 $0.001 \u2014 prices, rankings, lists, news, profiles, simple reads
1604
+ - Tier 2 $0.005 \u2014 order books, candles, search, wallet detail, social aggregates
1605
+ - Tier 3 $0.020 \u2014 raw on-chain SQL, structured queries, surf-1.5 chat
1606
+
1607
+ Common paths (full 84-endpoint catalog in the surf skill):
1608
+ - market/price?symbol=BTC (T1)
1609
+ - exchange/price?pair=BTC-USDT (T1)
1610
+ - prediction-market/polymarket/ranking (T1)
1611
+ - search/web?q=ethereum+pectra+upgrade (T2)
1612
+ - wallet/detail?address=0x... (T2)
1613
+ - social/mindshare?q=ethereum&interval=1d (T2)
1614
+ - onchain/sql + body:{ sql: "SELECT ..." } (T3)
1615
+ - chat/completions + body:{ model:"surf/surf-1.5", messages:[]} (T3, $0.02 flat)
1616
+
1617
+ Method is auto-routed: pass 'body' for POST endpoints; otherwise GET with 'params'.
1618
+ Each Surf endpoint pre-validates required params before settling \u2014 you get a 400 (not a charge) if a required field is missing. Browse the full catalog: https://blockrun.ai/marketplace/surf`,
1619
+ inputSchema: {
1620
+ path: z14.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
1621
+ params: z14.record(z14.string(), z14.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
1622
+ body: z14.any().optional().describe("JSON body for POST endpoints. Provide for: onchain/query, onchain/sql, chat/completions. When set, the call is sent as POST; otherwise GET with params.")
1623
+ }
1624
+ },
1625
+ async ({ path: path4, params, body }) => {
1626
+ try {
1627
+ const client = getClient();
1628
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1629
+ const endpoint = `/v1/surf/${cleanPath}`;
1630
+ const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
1631
+ return {
1632
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1633
+ structuredContent: result
1634
+ };
1635
+ } catch (err) {
1636
+ return {
1637
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
1590
1638
  isError: true
1591
1639
  };
1592
1640
  }
@@ -1608,20 +1656,24 @@ function initializeMcpServer(server) {
1608
1656
  registerExaTool(server);
1609
1657
  registerMarketsTool(server);
1610
1658
  registerPriceTool(server);
1611
- registerTwitterTool(server);
1612
1659
  registerDexTool(server);
1613
1660
  registerModalTool(server);
1661
+ registerPhoneTool(server);
1662
+ registerSurfTool(server);
1614
1663
  server.registerResource(
1615
1664
  "wallet",
1616
1665
  "blockrun://wallet",
1617
1666
  { description: "Wallet address and status", mimeType: "application/json" },
1618
- async () => ({
1619
- contents: [{
1620
- uri: "blockrun://wallet",
1621
- mimeType: "application/json",
1622
- text: JSON.stringify(getWalletInfo(), null, 2)
1623
- }]
1624
- })
1667
+ async () => {
1668
+ const info = await getWalletInfo();
1669
+ return {
1670
+ contents: [{
1671
+ uri: "blockrun://wallet",
1672
+ mimeType: "application/json",
1673
+ text: JSON.stringify(info, null, 2)
1674
+ }]
1675
+ };
1676
+ }
1625
1677
  );
1626
1678
  server.registerResource(
1627
1679
  "models",
@@ -1647,9 +1699,9 @@ function initializeMcpServer(server) {
1647
1699
  }
1648
1700
 
1649
1701
  // src/utils/key-leak-scanner.ts
1650
- import fs2 from "fs";
1651
- import path2 from "path";
1652
- import os2 from "os";
1702
+ import fs3 from "fs";
1703
+ import path3 from "path";
1704
+ import os3 from "os";
1653
1705
  function looksLikeRawPrivateKey(value) {
1654
1706
  if (typeof value !== "string") return false;
1655
1707
  if (/^0x[0-9a-fA-F]{64}$/.test(value)) return true;
@@ -1674,8 +1726,8 @@ function walk(obj, file, jsonPath, out) {
1674
1726
  }
1675
1727
  function scanFile(file) {
1676
1728
  try {
1677
- if (!fs2.existsSync(file)) return [];
1678
- const raw = fs2.readFileSync(file, "utf-8");
1729
+ if (!fs3.existsSync(file)) return [];
1730
+ const raw = fs3.readFileSync(file, "utf-8");
1679
1731
  const data = JSON.parse(raw);
1680
1732
  const out = [];
1681
1733
  walk(data, file, "", out);
@@ -1685,12 +1737,12 @@ function scanFile(file) {
1685
1737
  }
1686
1738
  }
1687
1739
  function warnOnLeakedKeys() {
1688
- const home = os2.homedir();
1740
+ const home = os3.homedir();
1689
1741
  const candidates = [
1690
- path2.join(home, ".claude.json"),
1691
- path2.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1692
- path2.join(home, ".config", "claude", "claude_desktop_config.json"),
1693
- path2.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
1742
+ path3.join(home, ".claude.json"),
1743
+ path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1744
+ path3.join(home, ".config", "claude", "claude_desktop_config.json"),
1745
+ path3.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
1694
1746
  ];
1695
1747
  const findings = [];
1696
1748
  for (const f of candidates) findings.push(...scanFile(f));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "mcpName": "io.github.BlockRunAI/blockrun-mcp",
5
5
  "description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, crypto data, X/Twitter intelligence. Paid via x402 micropayments.",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsup src/index.ts --format esm --dts --clean",
17
+ "build": "tsup src/index.ts --format esm --dts --clean --external sharp",
18
18
  "dev": "tsx watch src/index.ts",
19
19
  "start": "node dist/index.js",
20
20
  "typecheck": "tsc --noEmit",
@@ -48,6 +48,7 @@
48
48
  "@modelcontextprotocol/sdk": "^1.0.0",
49
49
  "open": "^11.0.0",
50
50
  "qrcode": "^1.5.4",
51
+ "sharp": "^0.34.5",
51
52
  "viem": "^2.21.0",
52
53
  "zod": "^4.3.5"
53
54
  },