@blockrun/mcp 0.14.4 → 0.15.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 +3 -2
  2. package/dist/index.js +393 -87
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -129,7 +129,7 @@ Run `blockrun_wallet` to see your address. Send USDC on Base.
129
129
  | Coinbase | Send → USDC → Base network → paste address |
130
130
  | Bridge from Ethereum | [bridge.base.org](https://bridge.base.org) |
131
131
 
132
- $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~20 Seedance 1.5-pro clips (5s, ~$0.23 each).
132
+ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~10 Seedance 1.5-pro clips (5s @ 720p+audio, ~$0.46 each).
133
133
 
134
134
  ---
135
135
 
@@ -139,7 +139,8 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
139
139
  |------|-------------|------|
140
140
  | `blockrun_chat` | 55+ LLMs (GPT, Claude, Gemini, DeepSeek, Kimi K2.6, GLM, NVIDIA free tier, ...) with `mode` tier routing | per token |
141
141
  | `blockrun_image` | DALL-E 3, GPT Image 1/2, Grok Imagine, Flux, CogView-4, Nano Banana — generation + editing | $0.015–0.12 |
142
- | `blockrun_video` | xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast | $0.046–0.149/sec |
142
+ | `blockrun_video` | xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast (720p + audio defaults); RealFace asset → real-person video | $0.05–0.298/sec |
143
+ | `blockrun_realface` | Enroll a real person (phone liveness → `ta_xxxx` asset) for Seedance 2.0 real-person video | free; $0.01 to enroll |
143
144
  | `blockrun_music` | MiniMax music generation | per track |
144
145
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
145
146
  | `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 |
package/dist/index.js CHANGED
@@ -170,6 +170,7 @@ import { z } from "zod";
170
170
  import QRCode from "qrcode";
171
171
  import open from "open";
172
172
  import * as fs2 from "fs";
173
+ import * as path3 from "path";
173
174
  import sharp from "sharp";
174
175
 
175
176
  // src/utils/constants.ts
@@ -241,6 +242,20 @@ async function generateQrPng(address, chain = "base") {
241
242
  fs2.writeFileSync(QR_FILE, finalBuf);
242
243
  return QR_FILE;
243
244
  }
245
+ async function generateUrlQrPng(url, fileName = "realface-qr.png") {
246
+ if (!fs2.existsSync(WALLET_DIR)) {
247
+ fs2.mkdirSync(WALLET_DIR, { recursive: true, mode: 448 });
248
+ }
249
+ const outPath = path3.join(WALLET_DIR, fileName);
250
+ await QRCode.toFile(outPath, url, {
251
+ type: "png",
252
+ width: 400,
253
+ margin: 2,
254
+ errorCorrectionLevel: "H",
255
+ color: { dark: "#000000", light: "#FFFFFF" }
256
+ });
257
+ return outPath;
258
+ }
244
259
  async function openQrInViewer(qrPath) {
245
260
  try {
246
261
  await open(qrPath);
@@ -1099,10 +1114,18 @@ var TOTAL_BUDGET_MS = 3e5;
1099
1114
  var POLL_INTERVAL_MS = 5e3;
1100
1115
  var VIDEO_PRICE_PER_SECOND = {
1101
1116
  "xai/grok-imagine-video": 0.05,
1102
- "bytedance/seedance-1.5-pro": 0.046,
1103
- "bytedance/seedance-2.0-fast": 0.119,
1104
- "bytedance/seedance-2.0": 0.149
1117
+ "bytedance/seedance-1.5-pro": 0.092,
1118
+ "bytedance/seedance-2.0-fast": 0.238,
1119
+ "bytedance/seedance-2.0": 0.298
1120
+ };
1121
+ var VIDEO_PRICE_PER_SECOND_IMAGE = {
1122
+ "bytedance/seedance-2.0-fast": 0.14,
1123
+ "bytedance/seedance-2.0": 0.183
1105
1124
  };
1125
+ var REALFACE_MODELS = /* @__PURE__ */ new Set([
1126
+ "bytedance/seedance-2.0",
1127
+ "bytedance/seedance-2.0-fast"
1128
+ ]);
1106
1129
  var VIDEO_DEFAULT_DURATION = {
1107
1130
  "xai/grok-imagine-video": 8,
1108
1131
  "bytedance/seedance-1.5-pro": 5,
@@ -1117,22 +1140,25 @@ function registerVideoTool(server, budget) {
1117
1140
 
1118
1141
  Turns a text prompt (and optional seed image) into a short MP4 clip. The tool submits the job, then polls until the video is ready (typical total wall-time 60-180s; 5 min hard cap). Payment is settled only when upstream returns a finished video \u2014 if the job fails or we give up, you are not charged.
1119
1142
 
1120
- Models:
1143
+ Models (Seedance defaults bumped to 720p + synced audio on the gateway):
1121
1144
  - xai/grok-imagine-video ($0.05/sec, 8s default -> $0.42/clip) \u2014 stylized, fast
1122
- - bytedance/seedance-1.5-pro (~$0.046/sec, 480p, 5s default up to 10s) \u2014 cheapest, token-priced upstream
1123
- - bytedance/seedance-2.0-fast (~$0.119/sec text \xB7 ~$0.07/sec image-to-video, ~60-80s gen) \u2014 sweet-spot price/quality
1124
- - bytedance/seedance-2.0 (~$0.149/sec text \xB7 ~$0.092/sec image-to-video, 480p Pro) \u2014 highest quality
1145
+ - bytedance/seedance-1.5-pro (~$0.092/sec, 720p + audio t2v, 5s default up to 10s) \u2014 cheapest Seedance, token-priced upstream
1146
+ - bytedance/seedance-2.0-fast (~$0.238/sec text \xB7 ~$0.140/sec image-to-video, 720p + audio, ~60-80s gen) \u2014 sweet-spot price/quality; supports BytePlus RealFace assets
1147
+ - bytedance/seedance-2.0 (~$0.298/sec text \xB7 ~$0.183/sec image-to-video, 720p + audio Pro) \u2014 highest quality; supports BytePlus RealFace assets
1148
+
1149
+ RealFace: to generate video of a SPECIFIC real person, first enroll them with blockrun_realface (returns a ta_xxxx asset id), then pass real_face_asset_id here with a Seedance 2.0 model. Mutually exclusive with image_url.
1125
1150
 
1126
1151
  Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GCS so URLs don't expire).`,
1127
1152
  inputSchema: {
1128
1153
  prompt: z6.string().describe("Text description of the video to generate. E.g. 'a red apple slowly spinning on a wooden table', 'a hummingbird hovering near a red flower, ultra slow motion'"),
1129
1154
  image_url: z6.string().url().optional().describe("Optional seed image URL for image-to-video generation"),
1155
+ real_face_asset_id: z6.string().regex(/^ta_[A-Za-z0-9]+$/, "token360 asset id like 'ta_xxxx'").optional().describe("BytePlus RealFace asset id (from blockrun_realface enroll/list) to generate video of a specific real person. Seedance 2.0 / 2.0-fast only. Mutually exclusive with image_url."),
1130
1156
  duration_seconds: z6.number().int().min(1).max(60).optional().describe("Duration to bill for (defaults to the model's default \u2014 8s for xAI, 5s for Seedance; Seedance supports up to 10s)."),
1131
1157
  model: z6.enum(["xai/grok-imagine-video", "bytedance/seedance-1.5-pro", "bytedance/seedance-2.0-fast", "bytedance/seedance-2.0"]).optional().default("xai/grok-imagine-video").describe("Video model to use"),
1132
1158
  agent_id: z6.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1133
1159
  }
1134
1160
  },
1135
- async ({ prompt, image_url, duration_seconds, model, agent_id }) => {
1161
+ async ({ prompt, image_url, real_face_asset_id, duration_seconds, model, agent_id }) => {
1136
1162
  try {
1137
1163
  if (getChain() !== "base") {
1138
1164
  return {
@@ -1141,8 +1167,24 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1141
1167
  };
1142
1168
  }
1143
1169
  const selectedModel = model || "xai/grok-imagine-video";
1170
+ if (real_face_asset_id) {
1171
+ if (!REALFACE_MODELS.has(selectedModel)) {
1172
+ return {
1173
+ content: [{ type: "text", text: formatError(`Model ${selectedModel} does not support RealFace assets. Use bytedance/seedance-2.0 or bytedance/seedance-2.0-fast.`) }],
1174
+ isError: true
1175
+ };
1176
+ }
1177
+ if (image_url) {
1178
+ return {
1179
+ content: [{ type: "text", text: formatError("Pass exactly one of real_face_asset_id or image_url \u2014 both seed the first frame.") }],
1180
+ isError: true
1181
+ };
1182
+ }
1183
+ }
1144
1184
  const billedSeconds = duration_seconds ?? VIDEO_DEFAULT_DURATION[selectedModel] ?? 8;
1145
- const estimatedCost = (VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05) * billedSeconds;
1185
+ const hasImageInput = Boolean(image_url || real_face_asset_id);
1186
+ const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
1187
+ const estimatedCost = perSecond * billedSeconds;
1146
1188
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1147
1189
  if (!budgetCheck.allowed) {
1148
1190
  return {
@@ -1155,6 +1197,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1155
1197
  const submitUrl = `${BLOCKRUN_API2}/v1/videos/generations`;
1156
1198
  const body = { model: selectedModel, prompt };
1157
1199
  if (image_url) body.image_url = image_url;
1200
+ if (real_face_asset_id) body.real_face_asset_id = real_face_asset_id;
1158
1201
  if (duration_seconds !== void 0) body.duration_seconds = duration_seconds;
1159
1202
  const resp402 = await fetchWithTimeout2(submitUrl, {
1160
1203
  method: "POST",
@@ -1298,8 +1341,270 @@ async function fetchWithTimeout2(url, options, timeoutMs) {
1298
1341
  }
1299
1342
  }
1300
1343
 
1301
- // src/tools/search.ts
1344
+ // src/tools/realface.ts
1302
1345
  import { z as z7 } from "zod";
1346
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
1347
+ import {
1348
+ createPaymentPayload as createPaymentPayload3,
1349
+ parsePaymentRequired as parsePaymentRequired3,
1350
+ extractPaymentDetails as extractPaymentDetails3
1351
+ } from "@blockrun/llm";
1352
+ var BLOCKRUN_API3 = "https://blockrun.ai/api";
1353
+ var ENROLLMENT_PRICE_USD = 0.01;
1354
+ async function fetchWithTimeout3(url, options, timeoutMs) {
1355
+ const controller = new AbortController();
1356
+ const id = setTimeout(() => controller.abort(), timeoutMs);
1357
+ try {
1358
+ return await fetch(url, { ...options, signal: controller.signal });
1359
+ } finally {
1360
+ clearTimeout(id);
1361
+ }
1362
+ }
1363
+ function registerRealfaceTool(server, budget) {
1364
+ server.registerTool(
1365
+ "blockrun_realface",
1366
+ {
1367
+ description: `Enroll a real person's face as a BytePlus RealFace asset, then drive Seedance 2.0 video with it (blockrun_video real_face_asset_id).
1368
+
1369
+ A RealFace asset (ta_xxxx) lets Seedance 2.0 / 2.0-fast generate video of a SPECIFIC real person \u2014 not a generic seed image. Enrollment is a multi-step flow because BytePlus requires a live phone liveness check (the real person nods + blinks on camera) before a face photo can be uploaded.
1370
+
1371
+ Actions:
1372
+ - init: FREE. Create an asset group + a phone H5 link. The tool renders the link as a QR code and opens it; the real person scans it on their phone and completes the ~1 min liveness check. Pass group_id to refresh an expired link.
1373
+ - status: FREE. Poll a group until status:"active" (ready_to_finalize:true). The H5 link is valid ~120s \u2014 re-init if it expires.
1374
+ - enroll: PAID ($0.01 USDC, Base only). After the group is active, upload a clear front-facing photo (image_url) of the SAME person. Returns the ta_xxxx asset id.
1375
+ - list: FREE. List the RealFace assets enrolled by this wallet (their ta_xxxx ids + names) so you can pick one for blockrun_video.
1376
+
1377
+ Typical flow:
1378
+ 1. blockrun_realface action:"init" name:"Alice" \u2192 scan QR on phone, do liveness
1379
+ 2. blockrun_realface action:"status" group_id:"legacy_rf_\u2026" \u2192 repeat until ready_to_finalize:true
1380
+ 3. blockrun_realface action:"enroll" name:"Alice" group_id:"legacy_rf_\u2026" image_url:"https://\u2026/alice.jpg" \u2192 ta_xxxx
1381
+ 4. blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"ta_xxxx" prompt:"\u2026"
1382
+
1383
+ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, name, and the photo URL you supply.`,
1384
+ inputSchema: {
1385
+ action: z7.enum(["init", "status", "enroll", "list"]).describe("What to do"),
1386
+ name: z7.string().min(1).max(64).optional().describe("Display name for the person (required for init and enroll)."),
1387
+ group_id: z7.string().regex(/^legacy_rf_\d+$/).optional().describe("Asset-group id from init (required for status and enroll; pass to init to refresh an expired H5 link)."),
1388
+ image_url: z7.string().url().optional().describe("Public HTTPS URL to a clear front-facing face photo (JPG/PNG/WEBP, \u226410MB). Required for enroll."),
1389
+ agent_id: z7.string().optional().describe("Agent identifier for budget tracking and enforcement (enroll only).")
1390
+ }
1391
+ },
1392
+ async ({ action, name, group_id, image_url, agent_id }) => {
1393
+ try {
1394
+ if (action === "init") {
1395
+ if (!name) {
1396
+ return { content: [{ type: "text", text: formatError('name is required for action:"init".') }], isError: true };
1397
+ }
1398
+ const body = { name };
1399
+ if (group_id) body.groupId = group_id;
1400
+ const resp = await fetchWithTimeout3(`${BLOCKRUN_API3}/v1/realface/init`, {
1401
+ method: "POST",
1402
+ headers: { "Content-Type": "application/json" },
1403
+ body: JSON.stringify(body)
1404
+ }, 3e4);
1405
+ const data = await resp.json().catch(() => ({}));
1406
+ if (resp.status === 429) {
1407
+ return { content: [{ type: "text", text: formatError(`Rate limited \u2014 retry in ${data.retryAfterSeconds ?? "a few"}s.`) }], isError: true };
1408
+ }
1409
+ if (!resp.ok) {
1410
+ return { content: [{ type: "text", text: formatError(`init failed (${resp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
1411
+ }
1412
+ const h5Link = data.h5_link;
1413
+ let qrNote = "";
1414
+ if (h5Link) {
1415
+ try {
1416
+ const qrPath = await generateUrlQrPng(h5Link, "realface-h5-qr.png");
1417
+ await openQrInViewer(qrPath);
1418
+ qrNote = `
1419
+ QR opened for scanning (${qrPath}).`;
1420
+ } catch {
1421
+ qrNote = "\n(QR generation failed \u2014 open the link below on the phone directly.)";
1422
+ }
1423
+ }
1424
+ const lines = [
1425
+ `\u{1FAAA} RealFace enrollment started${data.refreshed ? " (link refreshed)" : ""}.`,
1426
+ `Group ID: ${data.group_id}`,
1427
+ `Status: ${data.status}`,
1428
+ h5Link ? `Phone link: ${h5Link}` : "",
1429
+ data.expires_in_seconds ? `Link expires in: ${data.expires_in_seconds}s` : "",
1430
+ qrNote.trim(),
1431
+ ``,
1432
+ `Next: the real person scans the QR / opens the link on their phone and completes the liveness check (nod + blink, ~1 min). Then poll: blockrun_realface action:"status" group_id:"${data.group_id}".`
1433
+ ].filter(Boolean);
1434
+ return {
1435
+ content: [{ type: "text", text: lines.join("\n") }],
1436
+ structuredContent: {
1437
+ group_id: data.group_id,
1438
+ status: data.status,
1439
+ h5_link: h5Link,
1440
+ expires_in_seconds: data.expires_in_seconds,
1441
+ refreshed: !!data.refreshed
1442
+ }
1443
+ };
1444
+ }
1445
+ if (action === "status") {
1446
+ if (!group_id) {
1447
+ return { content: [{ type: "text", text: formatError('group_id is required for action:"status".') }], isError: true };
1448
+ }
1449
+ const resp = await fetchWithTimeout3(`${BLOCKRUN_API3}/v1/realface/status?groupId=${encodeURIComponent(group_id)}`, {
1450
+ method: "GET"
1451
+ }, 3e4);
1452
+ const data = await resp.json().catch(() => ({}));
1453
+ if (resp.status === 429) {
1454
+ return { content: [{ type: "text", text: formatError(`Rate limited \u2014 retry in ${data.retryAfterSeconds ?? "a few"}s.`) }], isError: true };
1455
+ }
1456
+ if (!resp.ok) {
1457
+ return { content: [{ type: "text", text: formatError(`status failed (${resp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
1458
+ }
1459
+ const ready = !!data.ready_to_finalize;
1460
+ const lines = [
1461
+ `RealFace group ${data.group_id}`,
1462
+ `Status: ${data.status}`,
1463
+ `Assets in group: ${data.asset_count ?? 0}`,
1464
+ ready ? `\u2705 Ready to finalize \u2014 call blockrun_realface action:"enroll" group_id:"${data.group_id}" name:"\u2026" image_url:"https://\u2026".` : `\u23F3 Not active yet. The real person must finish the phone liveness check. Re-poll, or re-init if the link expired.`
1465
+ ];
1466
+ return {
1467
+ content: [{ type: "text", text: lines.join("\n") }],
1468
+ structuredContent: {
1469
+ group_id: data.group_id,
1470
+ status: data.status,
1471
+ asset_count: data.asset_count,
1472
+ ready_to_finalize: ready
1473
+ }
1474
+ };
1475
+ }
1476
+ if (action === "list") {
1477
+ const account = privateKeyToAccount3(getOrCreateWalletKey());
1478
+ const resp = await fetchWithTimeout3(`${BLOCKRUN_API3}/v1/wallet/${account.address}/realfaces`, {
1479
+ method: "GET"
1480
+ }, 3e4);
1481
+ const data = await resp.json().catch(() => ({}));
1482
+ if (!resp.ok) {
1483
+ return { content: [{ type: "text", text: formatError(`list failed (${resp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
1484
+ }
1485
+ const faces = Array.isArray(data.realfaces) ? data.realfaces : [];
1486
+ if (faces.length === 0) {
1487
+ return {
1488
+ content: [{ type: "text", text: `No RealFace assets enrolled for ${account.address}.
1489
+ Enroll one: blockrun_realface action:"init" name:"\u2026".` }],
1490
+ structuredContent: { wallet: account.address, realfaces: [], count: 0 }
1491
+ };
1492
+ }
1493
+ const lines = [
1494
+ `RealFace assets for ${account.address} (${faces.length}):`,
1495
+ ...faces.map((f) => ` \u2022 ${f.assetId} \u2014 "${f.name}"${f.createdAt ? ` (${f.createdAt})` : ""}`),
1496
+ ``,
1497
+ `Use one: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${faces[0].assetId}" prompt:"\u2026".`
1498
+ ];
1499
+ return {
1500
+ content: [{ type: "text", text: lines.join("\n") }],
1501
+ structuredContent: { wallet: account.address, realfaces: faces, count: faces.length }
1502
+ };
1503
+ }
1504
+ if (action === "enroll") {
1505
+ if (getChain() !== "base") {
1506
+ return { content: [{ type: "text", text: formatError("blockrun_realface enroll settles on Base only. Switch BlockRun to Base (write 'base' to ~/.blockrun/.chain) and fund the Base wallet with USDC.") }], isError: true };
1507
+ }
1508
+ if (!name || !image_url || !group_id) {
1509
+ return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
1510
+ }
1511
+ const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
1512
+ if (!budgetCheck.allowed) {
1513
+ return { content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
1514
+ }
1515
+ const privateKey = getOrCreateWalletKey();
1516
+ const account = privateKeyToAccount3(privateKey);
1517
+ const enrollUrl = `${BLOCKRUN_API3}/v1/realface/enroll`;
1518
+ const reqBody = JSON.stringify({ name, image_url, group_id });
1519
+ const resp402 = await fetchWithTimeout3(enrollUrl, {
1520
+ method: "POST",
1521
+ headers: { "Content-Type": "application/json" },
1522
+ body: reqBody
1523
+ }, 15e3);
1524
+ if (resp402.status !== 402) {
1525
+ const data2 = await resp402.json().catch(() => ({}));
1526
+ throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
1527
+ }
1528
+ const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1529
+ if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1530
+ const paymentRequired = parsePaymentRequired3(prHeader);
1531
+ const details = extractPaymentDetails3(paymentRequired);
1532
+ const paymentPayload = await createPaymentPayload3(
1533
+ privateKey,
1534
+ account.address,
1535
+ details.recipient,
1536
+ details.amount,
1537
+ details.network || "eip155:8453",
1538
+ {
1539
+ resourceUrl: details.resource?.url || enrollUrl,
1540
+ resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
1541
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
1542
+ extra: details.extra
1543
+ }
1544
+ );
1545
+ const resp = await fetchWithTimeout3(enrollUrl, {
1546
+ method: "POST",
1547
+ headers: {
1548
+ "Content-Type": "application/json",
1549
+ "PAYMENT-SIGNATURE": paymentPayload
1550
+ },
1551
+ body: reqBody
1552
+ }, 9e4);
1553
+ const data = await resp.json().catch(() => ({}));
1554
+ if (resp.status === 402) {
1555
+ throw new Error("Payment rejected. Check your wallet balance.");
1556
+ }
1557
+ if (resp.status === 425) {
1558
+ return { content: [{ type: "text", text: formatError(`Group not active yet \u2014 ${data.message || "finish the phone liveness check first"}. No payment taken.`) }], isError: true };
1559
+ }
1560
+ if (resp.status === 422) {
1561
+ return { content: [{ type: "text", text: formatError(`Face match failed \u2014 ${data.hint || "use a clearer front-facing photo of the same person"}. No payment taken.`) }], isError: true };
1562
+ }
1563
+ if (!resp.ok) {
1564
+ throw new Error(`Enroll error ${resp.status}: ${data.error || JSON.stringify(data)}`);
1565
+ }
1566
+ const assetId = data.asset_id;
1567
+ if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
1568
+ recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
1569
+ const txHash = data.settlement?.tx_hash || void 0;
1570
+ const lines = [
1571
+ `\u2705 RealFace enrolled!`,
1572
+ `Asset ID: ${assetId}`,
1573
+ `Name: ${data.name || name}`,
1574
+ `Cost: $${ENROLLMENT_PRICE_USD.toFixed(2)} USDC`,
1575
+ ...txHash ? [`Tx: ${txHash}`] : [],
1576
+ ``,
1577
+ `Use it: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${assetId}" prompt:"\u2026".`
1578
+ ];
1579
+ return {
1580
+ content: [{ type: "text", text: lines.join("\n") }],
1581
+ structuredContent: {
1582
+ asset_id: assetId,
1583
+ group_id: data.group_id || group_id,
1584
+ name: data.name || name,
1585
+ price_usd: ENROLLMENT_PRICE_USD,
1586
+ ...txHash ? { txHash } : {}
1587
+ }
1588
+ };
1589
+ }
1590
+ return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
1591
+ } catch (err) {
1592
+ const errMsg = err instanceof Error ? err.message : String(err);
1593
+ if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
1594
+ return {
1595
+ content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
1596
+ Error: ${errMsg}` }],
1597
+ isError: true
1598
+ };
1599
+ }
1600
+ return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
1601
+ }
1602
+ }
1603
+ );
1604
+ }
1605
+
1606
+ // src/tools/search.ts
1607
+ import { z as z8 } from "zod";
1303
1608
  var SEARCH_PRICE_PER_SOURCE = 0.025;
1304
1609
  var SEARCH_DEFAULT_MAX_RESULTS = 10;
1305
1610
  function estimateSearchCost(body) {
@@ -1321,12 +1626,12 @@ Common shape:
1321
1626
 
1322
1627
  Full request shape + worked examples in the \`search\` skill (\`skills/search/SKILL.md\`).`,
1323
1628
  inputSchema: {
1324
- path: z7.string().optional().default("").describe("Endpoint sub-path under /v1/search/ (default empty = root /v1/search). Reserved for future surfaces."),
1325
- body: z7.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST."),
1326
- agent_id: z7.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1629
+ path: z8.string().optional().default("").describe("Endpoint sub-path under /v1/search/ (default empty = root /v1/search). Reserved for future surfaces."),
1630
+ body: z8.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST."),
1631
+ agent_id: z8.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1327
1632
  }
1328
1633
  },
1329
- async ({ path: path4, body, agent_id }) => {
1634
+ async ({ path: path5, body, agent_id }) => {
1330
1635
  try {
1331
1636
  const estimatedCost = estimateSearchCost(body);
1332
1637
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
@@ -1337,7 +1642,7 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
1337
1642
  };
1338
1643
  }
1339
1644
  const client = getClient();
1340
- const cleanPath = (path4 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
1645
+ const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
1341
1646
  const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
1342
1647
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1343
1648
  recordSpending(budget, estimatedCost, agent_id);
@@ -1353,9 +1658,9 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
1353
1658
  }
1354
1659
 
1355
1660
  // src/tools/exa.ts
1356
- import { z as z8 } from "zod";
1357
- function estimateExaCost(path4, body) {
1358
- const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1661
+ import { z as z9 } from "zod";
1662
+ function estimateExaCost(path5, body) {
1663
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1359
1664
  if (cleanPath === "contents") {
1360
1665
  const urls = body && typeof body === "object" ? body.urls : void 0;
1361
1666
  return 2e-3 * (Array.isArray(urls) && urls.length > 0 ? urls.length : 1);
@@ -1378,14 +1683,14 @@ Categories for search: "news", "research paper", "company", "tweet", "github", "
1378
1683
 
1379
1684
  Full request/response shapes + worked research workflows in the \`exa-research\` skill.`,
1380
1685
  inputSchema: {
1381
- path: z8.string().describe("Endpoint name under /v1/exa/, e.g. 'search', 'answer', 'contents', 'find-similar'"),
1382
- body: z8.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints."),
1383
- agent_id: z8.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1686
+ path: z9.string().describe("Endpoint name under /v1/exa/, e.g. 'search', 'answer', 'contents', 'find-similar'"),
1687
+ body: z9.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints."),
1688
+ agent_id: z9.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1384
1689
  }
1385
1690
  },
1386
- async ({ path: path4, body, agent_id }) => {
1691
+ async ({ path: path5, body, agent_id }) => {
1387
1692
  try {
1388
- const estimatedCost = estimateExaCost(path4, body);
1693
+ const estimatedCost = estimateExaCost(path5, body);
1389
1694
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1390
1695
  if (!budgetCheck.allowed) {
1391
1696
  return {
@@ -1394,7 +1699,7 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
1394
1699
  };
1395
1700
  }
1396
1701
  const client = getClient();
1397
- const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1702
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1398
1703
  const endpoint = `/v1/exa/${cleanPath}`;
1399
1704
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1400
1705
  recordSpending(budget, estimatedCost, agent_id);
@@ -1410,10 +1715,10 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
1410
1715
  }
1411
1716
 
1412
1717
  // src/tools/markets.ts
1413
- import { z as z9 } from "zod";
1414
- function estimateMarketCost(path4, body) {
1718
+ import { z as z10 } from "zod";
1719
+ function estimateMarketCost(path5, body) {
1415
1720
  if (body !== void 0) return 5e-3;
1416
- const p = path4.toLowerCase();
1721
+ const p = path5.toLowerCase();
1417
1722
  if (p.includes("wallet") || p.includes("smart") || p.includes("matching-markets") || p.includes("markets/search") || p.includes("binance/")) {
1418
1723
  return 5e-3;
1419
1724
  }
@@ -1475,15 +1780,15 @@ CROSS-PLATFORM:
1475
1780
 
1476
1781
  Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. polymarket/wallet/identities).`,
1477
1782
  inputSchema: {
1478
- path: z9.string().describe("Endpoint path, e.g. 'polymarket/events', 'kalshi/markets/KXBTC-25MAR14', 'polymarket/wallet/0xabc...', 'markets/search'"),
1479
- params: z9.record(z9.string(), z9.string()).optional().describe("Query parameters for GET requests (e.g. { limit: '20', active: 'true' })"),
1480
- body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)"),
1481
- agent_id: z9.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1783
+ path: z10.string().describe("Endpoint path, e.g. 'polymarket/events', 'kalshi/markets/KXBTC-25MAR14', 'polymarket/wallet/0xabc...', 'markets/search'"),
1784
+ params: z10.record(z10.string(), z10.string()).optional().describe("Query parameters for GET requests (e.g. { limit: '20', active: 'true' })"),
1785
+ body: z10.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)"),
1786
+ agent_id: z10.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1482
1787
  }
1483
1788
  },
1484
- async ({ path: path4, params, body, agent_id }) => {
1789
+ async ({ path: path5, params, body, agent_id }) => {
1485
1790
  try {
1486
- const estimatedCost = estimateMarketCost(path4, body);
1791
+ const estimatedCost = estimateMarketCost(path5, body);
1487
1792
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1488
1793
  if (!budgetCheck.allowed) {
1489
1794
  return {
@@ -1492,7 +1797,7 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
1492
1797
  };
1493
1798
  }
1494
1799
  const llm = getClient();
1495
- const result = body !== void 0 ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
1800
+ const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
1496
1801
  recordSpending(budget, estimatedCost, agent_id);
1497
1802
  return {
1498
1803
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -1510,9 +1815,9 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
1510
1815
  }
1511
1816
 
1512
1817
  // src/tools/price.ts
1513
- import { z as z10 } from "zod";
1514
- var CATEGORY = z10.enum(["crypto", "fx", "commodity", "usstock", "stocks"]);
1515
- var MARKET = z10.enum([
1818
+ import { z as z11 } from "zod";
1819
+ var CATEGORY = z11.enum(["crypto", "fx", "commodity", "usstock", "stocks"]);
1820
+ var MARKET = z11.enum([
1516
1821
  "us",
1517
1822
  "hk",
1518
1823
  "jp",
@@ -1526,9 +1831,9 @@ var MARKET = z10.enum([
1526
1831
  "cn",
1527
1832
  "ca"
1528
1833
  ]);
1529
- var RESOLUTION = z10.enum(["1", "5", "15", "60", "240", "D", "W", "M"]);
1530
- var SESSION = z10.enum(["pre", "post", "on"]);
1531
- var ACTION = z10.enum(["price", "history", "list"]);
1834
+ var RESOLUTION = z11.enum(["1", "5", "15", "60", "240", "D", "W", "M"]);
1835
+ var SESSION = z11.enum(["pre", "post", "on"]);
1836
+ var ACTION = z11.enum(["price", "history", "list"]);
1532
1837
  function isPaidPriceCall(action, category) {
1533
1838
  return action !== "list" && (category === "stocks" || category === "usstock");
1534
1839
  }
@@ -1556,15 +1861,15 @@ Examples:
1556
1861
  inputSchema: {
1557
1862
  action: ACTION.describe("Which endpoint to hit: price, history, or list."),
1558
1863
  category: CATEGORY.describe("Market category."),
1559
- symbol: z10.string().optional().describe("Ticker (required for price+history). e.g. BTC-USD, AAPL, EUR-USD."),
1864
+ symbol: z11.string().optional().describe("Ticker (required for price+history). e.g. BTC-USD, AAPL, EUR-USD."),
1560
1865
  market: MARKET.optional().describe("Stock market code \u2014 required when category='stocks'."),
1561
1866
  session: SESSION.optional().describe("Equity session hint (pre/post/on); ignored for non-equity."),
1562
1867
  resolution: RESOLUTION.optional().describe("Bar resolution for history (default D)."),
1563
- from: z10.number().optional().describe("History window start (unix seconds)."),
1564
- to: z10.number().optional().describe("History window end (unix seconds)."),
1565
- query: z10.string().optional().describe("Free-text filter for list."),
1566
- limit: z10.number().int().positive().max(2e3).optional().describe("Max items for list (default 100, max 2000)."),
1567
- agent_id: z10.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1868
+ from: z11.number().optional().describe("History window start (unix seconds)."),
1869
+ to: z11.number().optional().describe("History window end (unix seconds)."),
1870
+ query: z11.string().optional().describe("Free-text filter for list."),
1871
+ limit: z11.number().int().positive().max(2e3).optional().describe("Max items for list (default 100, max 2000)."),
1872
+ agent_id: z11.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1568
1873
  }
1569
1874
  },
1570
1875
  async ({ action, category, symbol, market, session, resolution, from, to, query, limit, agent_id }) => {
@@ -1637,7 +1942,7 @@ Examples:
1637
1942
  }
1638
1943
 
1639
1944
  // src/tools/dex.ts
1640
- import { z as z11 } from "zod";
1945
+ import { z as z12 } from "zod";
1641
1946
  function registerDexTool(server) {
1642
1947
  server.registerTool(
1643
1948
  "blockrun_dex",
@@ -1654,10 +1959,10 @@ Examples:
1654
1959
  blockrun_dex({ token: "So11...xxx" }) -> Get specific token data
1655
1960
  blockrun_dex({ symbol: "PEPE" }) -> Search by symbol`,
1656
1961
  inputSchema: {
1657
- query: z11.string().optional().describe("Search query (token name, symbol, or address)"),
1658
- token: z11.string().optional().describe("Token address for direct lookup"),
1659
- symbol: z11.string().optional().describe("Token symbol to search"),
1660
- chain: z11.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
1962
+ query: z12.string().optional().describe("Search query (token name, symbol, or address)"),
1963
+ token: z12.string().optional().describe("Token address for direct lookup"),
1964
+ symbol: z12.string().optional().describe("Token symbol to search"),
1965
+ chain: z12.string().optional().describe("Filter by chain (ethereum, solana, base, etc.)")
1661
1966
  }
1662
1967
  },
1663
1968
  async ({ query, token, symbol, chain }) => {
@@ -1718,9 +2023,9 @@ ${lines.join("\n\n")}` }],
1718
2023
  }
1719
2024
 
1720
2025
  // src/tools/modal.ts
1721
- import { z as z12 } from "zod";
1722
- function estimateModalCost(path4) {
1723
- return path4.includes("sandbox/create") ? 0.01 : 1e-3;
2026
+ import { z as z13 } from "zod";
2027
+ function estimateModalCost(path5) {
2028
+ return path5.includes("sandbox/create") ? 0.01 : 1e-3;
1724
2029
  }
1725
2030
  function registerModalTool(server, budget) {
1726
2031
  server.registerTool(
@@ -1738,14 +2043,14 @@ Common paths (all POST):
1738
2043
 
1739
2044
  Full action shapes + GPU type details in the \`modal\` skill.`,
1740
2045
  inputSchema: {
1741
- path: z12.string().describe("Endpoint under /v1/modal/, e.g. 'sandbox/create', 'sandbox/exec'"),
1742
- body: z12.any().optional().describe("JSON body. Sent as POST."),
1743
- agent_id: z12.string().optional().describe("Agent identifier for budget tracking and enforcement.")
2046
+ path: z13.string().describe("Endpoint under /v1/modal/, e.g. 'sandbox/create', 'sandbox/exec'"),
2047
+ body: z13.any().optional().describe("JSON body. Sent as POST."),
2048
+ agent_id: z13.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1744
2049
  }
1745
2050
  },
1746
- async ({ path: path4, body, agent_id }) => {
2051
+ async ({ path: path5, body, agent_id }) => {
1747
2052
  try {
1748
- const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
2053
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
1749
2054
  const estimatedCost = estimateModalCost(cleanPath);
1750
2055
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1751
2056
  if (!budgetCheck.allowed) {
@@ -1770,15 +2075,15 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
1770
2075
  }
1771
2076
 
1772
2077
  // src/tools/phone.ts
1773
- import { z as z13 } from "zod";
1774
- function estimatePhoneCost(path4, hasBody) {
1775
- if (!hasBody && path4.startsWith("voice/call/")) return 0;
1776
- if (path4 === "phone/numbers/release") return 0;
1777
- if (path4 === "phone/lookup") return 0.01;
1778
- if (path4 === "phone/lookup/fraud") return 0.05;
1779
- if (path4 === "phone/numbers/buy" || path4 === "phone/numbers/renew") return 5;
1780
- if (path4 === "phone/numbers/list") return 1e-3;
1781
- if (path4 === "voice/call") return 0.54;
2078
+ import { z as z14 } from "zod";
2079
+ function estimatePhoneCost(path5, hasBody) {
2080
+ if (!hasBody && path5.startsWith("voice/call/")) return 0;
2081
+ if (path5 === "phone/numbers/release") return 0;
2082
+ if (path5 === "phone/lookup") return 0.01;
2083
+ if (path5 === "phone/lookup/fraud") return 0.05;
2084
+ if (path5 === "phone/numbers/buy" || path5 === "phone/numbers/renew") return 5;
2085
+ if (path5 === "phone/numbers/list") return 1e-3;
2086
+ if (path5 === "voice/call") return 0.54;
1782
2087
  return hasBody ? 1e-3 : 0;
1783
2088
  }
1784
2089
  function registerPhoneTool(server, budget) {
@@ -1803,14 +2108,14 @@ Voice presets: nat, josh, maya, june, paige, derek, florian. Phone numbers use E
1803
2108
 
1804
2109
  Voice call flow + voice preset details + full body shapes in the \`phone\` skill.`,
1805
2110
  inputSchema: {
1806
- 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."),
1807
- body: z13.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id})."),
1808
- agent_id: z13.string().optional().describe("Agent identifier for budget tracking and enforcement.")
2111
+ path: z14.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."),
2112
+ body: z14.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id})."),
2113
+ agent_id: z14.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1809
2114
  }
1810
2115
  },
1811
- async ({ path: path4, body, agent_id }) => {
2116
+ async ({ path: path5, body, agent_id }) => {
1812
2117
  try {
1813
- const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\//, "");
2118
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
1814
2119
  const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
1815
2120
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1816
2121
  if (!budgetCheck.allowed) {
@@ -1835,7 +2140,7 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
1835
2140
  }
1836
2141
 
1837
2142
  // src/tools/surf.ts
1838
- import { z as z14 } from "zod";
2143
+ import { z as z15 } from "zod";
1839
2144
  var SURF_T3_PATHS = /* @__PURE__ */ new Set([
1840
2145
  "onchain/sql",
1841
2146
  "onchain/query",
@@ -1864,8 +2169,8 @@ var SURF_T2_PATHS = /* @__PURE__ */ new Set([
1864
2169
  "web/fetch"
1865
2170
  ]);
1866
2171
  var SURF_T2_PREFIXES = ["search/", "wallet/"];
1867
- function estimateSurfCost(path4) {
1868
- const p = path4.toLowerCase().replace(/^\/+/, "");
2172
+ function estimateSurfCost(path5) {
2173
+ const p = path5.toLowerCase().replace(/^\/+/, "");
1869
2174
  if (SURF_T3_PATHS.has(p)) return 0.02;
1870
2175
  if (SURF_T2_PATHS.has(p)) return 5e-3;
1871
2176
  for (const prefix of SURF_T2_PREFIXES) {
@@ -1899,15 +2204,15 @@ Common paths (full 84-endpoint catalog in the surf skill):
1899
2204
  Method is auto-routed: pass 'body' for POST endpoints; otherwise GET with 'params'.
1900
2205
  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`,
1901
2206
  inputSchema: {
1902
- path: z14.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
1903
- params: z14.record(z14.string(), z14.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
1904
- 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."),
1905
- agent_id: z14.string().optional().describe("Agent identifier for budget tracking and enforcement.")
2207
+ path: z15.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
2208
+ params: z15.record(z15.string(), z15.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
2209
+ body: z15.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."),
2210
+ agent_id: z15.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1906
2211
  }
1907
2212
  },
1908
- async ({ path: path4, params, body, agent_id }) => {
2213
+ async ({ path: path5, params, body, agent_id }) => {
1909
2214
  try {
1910
- const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
2215
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1911
2216
  const estimatedCost = estimateSurfCost(cleanPath);
1912
2217
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1913
2218
  if (!budgetCheck.allowed) {
@@ -1944,6 +2249,7 @@ function initializeMcpServer(server) {
1944
2249
  registerImageTool(server, budget);
1945
2250
  registerMusicTool(server, budget);
1946
2251
  registerVideoTool(server, budget);
2252
+ registerRealfaceTool(server, budget);
1947
2253
  registerSearchTool(server, budget);
1948
2254
  registerExaTool(server, budget);
1949
2255
  registerMarketsTool(server, budget);
@@ -1992,7 +2298,7 @@ function initializeMcpServer(server) {
1992
2298
 
1993
2299
  // src/utils/key-leak-scanner.ts
1994
2300
  import fs3 from "fs";
1995
- import path3 from "path";
2301
+ import path4 from "path";
1996
2302
  import os3 from "os";
1997
2303
  function looksLikeRawPrivateKey(value) {
1998
2304
  if (typeof value !== "string") return false;
@@ -2031,10 +2337,10 @@ function scanFile(file) {
2031
2337
  function warnOnLeakedKeys() {
2032
2338
  const home = os3.homedir();
2033
2339
  const candidates = [
2034
- path3.join(home, ".claude.json"),
2035
- path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
2036
- path3.join(home, ".config", "claude", "claude_desktop_config.json"),
2037
- path3.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
2340
+ path4.join(home, ".claude.json"),
2341
+ path4.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
2342
+ path4.join(home, ".config", "claude", "claude_desktop_config.json"),
2343
+ path4.join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
2038
2344
  ];
2039
2345
  const findings = [];
2040
2346
  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.14.4",
3
+ "version": "0.15.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",