@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.
- package/README.md +3 -2
- package/dist/index.js +393 -87
- 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 ~
|
|
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.
|
|
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.
|
|
1103
|
-
"bytedance/seedance-2.0-fast": 0.
|
|
1104
|
-
"bytedance/seedance-2.0": 0.
|
|
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.
|
|
1123
|
-
- bytedance/seedance-2.0-fast (~$0.
|
|
1124
|
-
- bytedance/seedance-2.0 (~$0.
|
|
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
|
|
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/
|
|
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:
|
|
1325
|
-
body:
|
|
1326
|
-
agent_id:
|
|
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:
|
|
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 = (
|
|
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
|
|
1357
|
-
function estimateExaCost(
|
|
1358
|
-
const cleanPath =
|
|
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:
|
|
1382
|
-
body:
|
|
1383
|
-
agent_id:
|
|
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:
|
|
1691
|
+
async ({ path: path5, body, agent_id }) => {
|
|
1387
1692
|
try {
|
|
1388
|
-
const estimatedCost = estimateExaCost(
|
|
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 =
|
|
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
|
|
1414
|
-
function estimateMarketCost(
|
|
1718
|
+
import { z as z10 } from "zod";
|
|
1719
|
+
function estimateMarketCost(path5, body) {
|
|
1415
1720
|
if (body !== void 0) return 5e-3;
|
|
1416
|
-
const p =
|
|
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:
|
|
1479
|
-
params:
|
|
1480
|
-
body:
|
|
1481
|
-
agent_id:
|
|
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:
|
|
1789
|
+
async ({ path: path5, params, body, agent_id }) => {
|
|
1485
1790
|
try {
|
|
1486
|
-
const estimatedCost = estimateMarketCost(
|
|
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(
|
|
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
|
|
1514
|
-
var CATEGORY =
|
|
1515
|
-
var MARKET =
|
|
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 =
|
|
1530
|
-
var SESSION =
|
|
1531
|
-
var ACTION =
|
|
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:
|
|
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:
|
|
1564
|
-
to:
|
|
1565
|
-
query:
|
|
1566
|
-
limit:
|
|
1567
|
-
agent_id:
|
|
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
|
|
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:
|
|
1658
|
-
token:
|
|
1659
|
-
symbol:
|
|
1660
|
-
chain:
|
|
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
|
|
1722
|
-
function estimateModalCost(
|
|
1723
|
-
return
|
|
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:
|
|
1742
|
-
body:
|
|
1743
|
-
agent_id:
|
|
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:
|
|
2051
|
+
async ({ path: path5, body, agent_id }) => {
|
|
1747
2052
|
try {
|
|
1748
|
-
const cleanPath =
|
|
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
|
|
1774
|
-
function estimatePhoneCost(
|
|
1775
|
-
if (!hasBody &&
|
|
1776
|
-
if (
|
|
1777
|
-
if (
|
|
1778
|
-
if (
|
|
1779
|
-
if (
|
|
1780
|
-
if (
|
|
1781
|
-
if (
|
|
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:
|
|
1807
|
-
body:
|
|
1808
|
-
agent_id:
|
|
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:
|
|
2116
|
+
async ({ path: path5, body, agent_id }) => {
|
|
1812
2117
|
try {
|
|
1813
|
-
const cleanPath =
|
|
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
|
|
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(
|
|
1868
|
-
const p =
|
|
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:
|
|
1903
|
-
params:
|
|
1904
|
-
body:
|
|
1905
|
-
agent_id:
|
|
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:
|
|
2213
|
+
async ({ path: path5, params, body, agent_id }) => {
|
|
1909
2214
|
try {
|
|
1910
|
-
const cleanPath =
|
|
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
|
|
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
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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.
|
|
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",
|