@blockrun/mcp 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +176 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -135,7 +135,7 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
135
135
  | `blockrun_chat` | 66+ LLMs (GPT, Claude, Gemini, DeepSeek, Kimi K2.6, GLM, NVIDIA free tier, ...) with `mode` tier routing | per token |
136
136
  | `blockrun_image` | GPT Image 1/2, Nano Banana / Pro (up to 4K), Grok Imagine, CogView-4 — generation + img2img editing | $0.015–0.15 |
137
137
  | `blockrun_video` | Sora 2 + xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast (720p + audio defaults); RealFace asset → real-person video | $0.05–0.30/sec |
138
- | `blockrun_realface` | Enroll a real person (phone liveness `ta_xxxx` asset) for Seedance 2.0 real-person video | free; $0.01 to enroll |
138
+ | `blockrun_realface` | Enroll a real person (phone liveness) or an AI character (Virtual Portrait, no liveness) as a `ta_xxxx` asset for Seedance 2.0 video | free; $0.01 to enroll |
139
139
  | `blockrun_music` | MiniMax music generation | per track |
140
140
  | `blockrun_speech` | ElevenLabs text-to-speech (Flash/Turbo/Multilingual/v3, 8 voice aliases) + cinematic sound effects; free voice listing | $0.05–0.10/1k chars; $0.0525/effect |
141
141
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
@@ -145,6 +145,7 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
145
145
  | `blockrun_search` | Grok Live Search — web + news with citations | $0.025 × max_results (default 10) |
146
146
  | `blockrun_dex` | Live DEX prices via DexScreener | free |
147
147
  | `blockrun_rpc` | Raw JSON-RPC on 40+ chains (Ethereum, Base, Solana, Bitcoin, Sui, NEAR, ...) via Tatum gateway — eth_call, balances, blocks, logs | $0.002/call |
148
+ | `blockrun_defi` | DefiLlama — protocol TVL, chain TVL, yield pools (APY), token prices | $0.001–0.005/call |
148
149
  | `blockrun_modal` | Isolated code execution in a BlockRun-hosted Modal sandbox — disposable container, optional GPU (T4 → H100) | $0.01 create; $0.001/op |
149
150
  | `blockrun_phone` | Outbound AI voice calls (Bland) + wallet-owned US/CA numbers (Twilio), carrier + fraud lookups | $0.54/call; $5/number |
150
151
  | `blockrun_models` | Live catalogue of every LLM/image/video/music model + pricing | free |
package/dist/index.js CHANGED
@@ -1907,6 +1907,46 @@ async function fetchWithTimeout4(url, options, timeoutMs) {
1907
1907
  clearTimeout(id);
1908
1908
  }
1909
1909
  }
1910
+ async function payAndPostJson(url, reqBody, fallbackDescription) {
1911
+ const privateKey = getOrCreateWalletKey();
1912
+ const account = privateKeyToAccount4(privateKey);
1913
+ const resp402 = await fetchWithTimeout4(url, {
1914
+ method: "POST",
1915
+ headers: { "Content-Type": "application/json" },
1916
+ body: reqBody
1917
+ }, 15e3);
1918
+ if (resp402.status !== 402) {
1919
+ const data2 = await resp402.json().catch(() => ({}));
1920
+ throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
1921
+ }
1922
+ const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1923
+ if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1924
+ const paymentRequired = parsePaymentRequired4(prHeader);
1925
+ const details = extractPaymentDetails4(paymentRequired);
1926
+ const paymentPayload = await createPaymentPayload4(
1927
+ privateKey,
1928
+ account.address,
1929
+ details.recipient,
1930
+ details.amount,
1931
+ details.network || "eip155:8453",
1932
+ {
1933
+ resourceUrl: details.resource?.url || url,
1934
+ resourceDescription: details.resource?.description || fallbackDescription,
1935
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
1936
+ extra: details.extra
1937
+ }
1938
+ );
1939
+ const resp = await fetchWithTimeout4(url, {
1940
+ method: "POST",
1941
+ headers: {
1942
+ "Content-Type": "application/json",
1943
+ "PAYMENT-SIGNATURE": paymentPayload
1944
+ },
1945
+ body: reqBody
1946
+ }, 9e4);
1947
+ const data = await resp.json().catch(() => ({}));
1948
+ return { status: resp.status, data };
1949
+ }
1910
1950
  function registerRealfaceTool(server, budget) {
1911
1951
  server.registerTool(
1912
1952
  "blockrun_realface",
@@ -1919,7 +1959,8 @@ Actions:
1919
1959
  - 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.
1920
1960
  - status: FREE. Poll a group until status:"active" (ready_to_finalize:true). The H5 link is valid ~120s \u2014 re-init if it expires.
1921
1961
  - 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.
1922
- - list: FREE. List the RealFace assets enrolled by this wallet (their ta_xxxx ids + names) so you can pick one for blockrun_video.
1962
+ - portrait: PAID ($0.01 USDC, Base only). Virtual Portrait \u2014 enroll an AI-GENERATED character from an image URL directly, NO liveness needed (one step: name + image_url \u2192 ta_xxxx). For fictional/AI characters only; for a real person use the init\u2192status\u2192enroll liveness flow.
1963
+ - list: FREE. List the RealFace + Virtual Portrait assets enrolled by this wallet (their ta_xxxx ids + names) so you can pick one for blockrun_video.
1923
1964
 
1924
1965
  Typical flow:
1925
1966
  1. blockrun_realface action:"init" name:"Alice" \u2192 scan QR on phone, do liveness
@@ -1929,10 +1970,10 @@ Typical flow:
1929
1970
 
1930
1971
  Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, name, and the photo URL you supply.`,
1931
1972
  inputSchema: {
1932
- action: z8.enum(["init", "status", "enroll", "list"]).describe("What to do"),
1933
- name: z8.string().min(1).max(64).optional().describe("Display name for the person (required for init and enroll)."),
1934
- group_id: z8.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)."),
1935
- image_url: z8.string().url().optional().describe("Public HTTPS URL to a clear front-facing face photo (JPG/PNG/WEBP, \u226410MB). Required for enroll."),
1973
+ action: z8.enum(["init", "status", "enroll", "portrait", "list"]).describe("What to do"),
1974
+ name: z8.string().min(1).max(64).optional().describe("Display name for the person/character (required for init, enroll, and portrait)."),
1975
+ group_id: z8.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). Not used by portrait."),
1976
+ image_url: z8.string().url().optional().describe("Public HTTPS URL to a clear front-facing face image (JPG/PNG/WEBP, \u226410MB). Required for enroll and portrait."),
1936
1977
  agent_id: z8.string().optional().describe("Agent identifier for budget tracking and enforcement (enroll only).")
1937
1978
  }
1938
1979
  },
@@ -2022,30 +2063,88 @@ QR opened for scanning (${qrPath}).`;
2022
2063
  }
2023
2064
  if (action === "list") {
2024
2065
  const account = privateKeyToAccount4(getOrCreateWalletKey());
2025
- const resp = await fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, {
2026
- method: "GET"
2027
- }, 3e4);
2028
- const data = await resp.json().catch(() => ({}));
2029
- if (!resp.ok) {
2030
- return { content: [{ type: "text", text: formatError(`list failed (${resp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
2066
+ const [rfResp, vpResp] = await Promise.all([
2067
+ fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/realfaces`, { method: "GET" }, 3e4),
2068
+ fetchWithTimeout4(`${BLOCKRUN_API4}/v1/wallet/${account.address}/portraits`, { method: "GET" }, 3e4).catch(() => null)
2069
+ ]);
2070
+ const data = await rfResp.json().catch(() => ({}));
2071
+ if (!rfResp.ok) {
2072
+ return { content: [{ type: "text", text: formatError(`list failed (${rfResp.status}): ${data.error || JSON.stringify(data)}`) }], isError: true };
2031
2073
  }
2032
2074
  const faces = Array.isArray(data.realfaces) ? data.realfaces : [];
2033
- if (faces.length === 0) {
2075
+ let portraits = [];
2076
+ if (vpResp?.ok) {
2077
+ const vpData = await vpResp.json().catch(() => ({}));
2078
+ portraits = Array.isArray(vpData.portraits) ? vpData.portraits : [];
2079
+ }
2080
+ if (faces.length === 0 && portraits.length === 0) {
2034
2081
  return {
2035
- content: [{ type: "text", text: `No RealFace assets enrolled for ${account.address}.
2036
- Enroll one: blockrun_realface action:"init" name:"\u2026".` }],
2037
- structuredContent: { wallet: account.address, realfaces: [], count: 0 }
2082
+ content: [{ type: "text", text: `No RealFace or Virtual Portrait assets enrolled for ${account.address}.
2083
+ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or action:"portrait" name:"\u2026" image_url:"https://\u2026" (AI character).` }],
2084
+ structuredContent: { wallet: account.address, realfaces: [], portraits: [], count: 0 }
2038
2085
  };
2039
2086
  }
2087
+ const first = faces[0] ?? portraits[0];
2040
2088
  const lines = [
2041
- `RealFace assets for ${account.address} (${faces.length}):`,
2042
- ...faces.map((f) => ` \u2022 ${f.assetId} \u2014 "${f.name}"${f.createdAt ? ` (${f.createdAt})` : ""}`),
2089
+ `Assets for ${account.address} (${faces.length} RealFace, ${portraits.length} Virtual Portrait):`,
2090
+ ...faces.map((f) => ` \u2022 ${f.assetId} \u2014 "${f.name}" [realface]${f.createdAt ? ` (${f.createdAt})` : ""}`),
2091
+ ...portraits.map((p) => ` \u2022 ${p.assetId} \u2014 "${p.name}" [portrait]${p.createdAt ? ` (${p.createdAt})` : ""}`),
2043
2092
  ``,
2044
- `Use one: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${faces[0].assetId}" prompt:"\u2026".`
2093
+ `Use one: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${first.assetId}" prompt:"\u2026".`
2045
2094
  ];
2046
2095
  return {
2047
2096
  content: [{ type: "text", text: lines.join("\n") }],
2048
- structuredContent: { wallet: account.address, realfaces: faces, count: faces.length }
2097
+ structuredContent: { wallet: account.address, realfaces: faces, portraits, count: faces.length + portraits.length }
2098
+ };
2099
+ }
2100
+ if (action === "portrait") {
2101
+ if (getChain() !== "base") {
2102
+ return { content: [{ type: "text", text: formatError("blockrun_realface portrait settles on Base only. Switch BlockRun to Base (run blockrun_wallet with action:chain chain:base) and fund the Base wallet with USDC.") }], isError: true };
2103
+ }
2104
+ if (!name || !image_url) {
2105
+ return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
2106
+ }
2107
+ const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2108
+ if (!budgetCheck.allowed) {
2109
+ return { content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2110
+ }
2111
+ const { status, data } = await payAndPostJson(
2112
+ `${BLOCKRUN_API4}/v1/portrait/enroll`,
2113
+ JSON.stringify({ name, image_url }),
2114
+ "BlockRun Virtual Portrait enrollment"
2115
+ );
2116
+ if (status === 402) {
2117
+ throw new Error("Payment rejected. Check your wallet balance.");
2118
+ }
2119
+ if (status === 422) {
2120
+ return { content: [{ type: "text", text: formatError(`Portrait rejected \u2014 ${data.hint || data.message || "use a clear front-facing character image"}. No payment taken.`) }], isError: true };
2121
+ }
2122
+ if (status < 200 || status >= 300) {
2123
+ throw new Error(`Portrait enroll error ${status}: ${data.error || JSON.stringify(data)}`);
2124
+ }
2125
+ const assetId = data.asset_id;
2126
+ if (!assetId) throw new Error(`Portrait response missing asset_id: ${JSON.stringify(data)}`);
2127
+ recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2128
+ const txHash = data.settlement?.tx_hash || void 0;
2129
+ const lines = [
2130
+ `\u2705 Virtual Portrait enrolled!`,
2131
+ `Asset ID: ${assetId}`,
2132
+ `Name: ${data.name || name}`,
2133
+ `Cost: $${ENROLLMENT_PRICE_USD.toFixed(2)} USDC`,
2134
+ ...txHash ? [`Tx: ${txHash}`] : [],
2135
+ ``,
2136
+ `Use it: blockrun_video model:"bytedance/seedance-2.0" real_face_asset_id:"${assetId}" prompt:"\u2026".`
2137
+ ];
2138
+ return {
2139
+ content: [{ type: "text", text: lines.join("\n") }],
2140
+ structuredContent: {
2141
+ asset_id: assetId,
2142
+ group_id: data.group_id,
2143
+ name: data.name || name,
2144
+ image_url: data.image_url,
2145
+ price_usd: ENROLLMENT_PRICE_USD,
2146
+ ...txHash ? { txHash } : {}
2147
+ }
2049
2148
  };
2050
2149
  }
2051
2150
  if (action === "enroll") {
@@ -2874,6 +2973,63 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
2874
2973
  );
2875
2974
  }
2876
2975
 
2976
+ // src/tools/defi.ts
2977
+ import { z as z18 } from "zod";
2978
+ function estimateDefiCost(path5) {
2979
+ return path5.startsWith("prices") ? 1e-3 : 5e-3;
2980
+ }
2981
+ function registerDefiTool(server, budget) {
2982
+ server.registerTool(
2983
+ "blockrun_defi",
2984
+ {
2985
+ description: `DeFi fundamentals via DefiLlama \u2014 protocol TVL, chain TVL, yield pools (APY), token prices. Pays per call in USDC, no API key.
2986
+
2987
+ Paths (GET only):
2988
+ - protocols ($0.005) \u2014 all DeFi protocols ranked by TVL
2989
+ - protocol/{slug} ($0.005) \u2014 one protocol's TVL history + chain breakdown, e.g. protocol/aave-v3
2990
+ - chains ($0.005) \u2014 TVL by chain
2991
+ - yields ($0.005) \u2014 yield pools with APY + TVL (large; filter client-side)
2992
+ - prices/{coins} ($0.001) \u2014 token prices, coins like 'base:0x833589...,coingecko:ethereum'
2993
+
2994
+ Examples:
2995
+ blockrun_defi({ path: "protocol/uniswap-v3" })
2996
+ blockrun_defi({ path: "prices/coingecko:bitcoin,coingecko:ethereum" })
2997
+ blockrun_defi({ path: "chains" })
2998
+
2999
+ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pairs, blockrun_surf for labeled on-chain data \u2014 this tool is for protocol/TVL/yield fundamentals.`,
3000
+ inputSchema: {
3001
+ path: z18.string().describe("Endpoint under /v1/defillama/, e.g. 'protocols', 'protocol/aave-v3', 'chains', 'yields', 'prices/coingecko:ethereum'"),
3002
+ agent_id: z18.string().optional().describe("Agent identifier for budget tracking and enforcement.")
3003
+ }
3004
+ },
3005
+ async ({ path: path5, agent_id }) => {
3006
+ try {
3007
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
3008
+ const estimatedCost = estimateDefiCost(cleanPath);
3009
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3010
+ if (!budgetCheck.allowed) {
3011
+ return {
3012
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3013
+ isError: true
3014
+ };
3015
+ }
3016
+ const client = getClient();
3017
+ const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
3018
+ recordSpending(budget, estimatedCost, agent_id);
3019
+ return {
3020
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3021
+ structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3022
+ };
3023
+ } catch (err) {
3024
+ return {
3025
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
3026
+ isError: true
3027
+ };
3028
+ }
3029
+ }
3030
+ );
3031
+ }
3032
+
2877
3033
  // src/mcp-handler.ts
2878
3034
  function initializeMcpServer(server) {
2879
3035
  const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
@@ -2895,6 +3051,7 @@ function initializeMcpServer(server) {
2895
3051
  registerPhoneTool(server, budget);
2896
3052
  registerSurfTool(server, budget);
2897
3053
  registerRpcTool(server, budget);
3054
+ registerDefiTool(server, budget);
2898
3055
  server.registerResource(
2899
3056
  "wallet",
2900
3057
  "blockrun://wallet",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
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",