Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@

All notable changes to BlockRun MCP will be documented in this file.

## 0.33.1

Live-probes the three pre-payment rules 0.33.0 shipped on inference rather than
evidence. Two were wrong, and both cost users real money.

- **`fix(markets)` — `markets/listings` is blocked again; it is retired
upstream.** Verified against the live gateway: the route settles a payment and
THEN returns **410 Gone**. 0.33.0 unblocked it because blockrun's own registry
still lists, prices, and advertises it — but the gateway only proxies, and
Predexon has retired it. Registry presence is not evidence that a route serves.
#81 had this right and the 0.33.0 change was the regression.
- **`fix(markets)` — `window` is no longer treated as a smart-money cohort
filter.** Verified: no params 400s, `{ window: "7d" }` alone **also** 400s, and
`{ min_trades: "100" }` alone succeeds (window then defaults to `all_time`).
`window` scopes the time range; it does not define the cohort. 0.33.0 counted
it as a filter and passed a guaranteed paid 400 straight through. The check now
requires a smart-wallet criterion and says why when only `window` is present.
- **`fix(markets)` — the candlestick interval whitelist is gone.** Verified on
one market: omitting `interval` succeeds, `1440` succeeds, `1h` 422s, and
`60` returns a paid **400**. Which integer intervals a market can serve is
data-dependent, so a client-side numeric whitelist both blocks valid calls and
still lets paid failures through. Only the shape is checkable client-side:
non-numeric is rejected, integers pass.

- **`perf` — the process-global paid-call queue is removed.** 0.33.0 serialized
every paid data call on the reasoning that "concurrent authorizations from one
wallet can race at the settlement layer". Measured, that protects nothing:
Base mints a fresh random 32-byte nonce per payment (`x402.ts:235`), so two
concurrent authorizations can never collide; the real Solana collision is
handled inside `@blockrun/llm` by making each payment distinct — the
dependency floor is raised to `^3.8.4` here, because `^3.6.1` admitted seven
published versions with no distinctness at all and the lockfile pinned the
oldest of them. Its check-and-add is synchronous on the common path (the
exhausted-nonce branch does await, which is unreachable below 65 identical
payments per blockhash), so it is concurrency-safe without a caller queue. The tools that *did* have a concurrency bug (`chat`'s settled-cost
delta) were never in the queue — they use a fresh non-cached client instead.
Cost of keeping it, measured on 4 concurrent `markets/search` calls: **+4025ms
(2.68x)**, with the unserialized arm returning 4/4 clean. Tracked as #89.

Tool description and the `prediction-markets` skill corrected to match.

## 0.33.0

A 20th tool, MCP safety annotations that describe effect instead of price, and
Expand Down
2 changes: 1 addition & 1 deletion docs/stanford-trading-demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ estimated fill and cannot sign or submit an order.
- $5 per-order and $5 per-session demo caps;
- $0.15 API budget;
- pre-payment parameter validation;
- serialized x402 calls from one wallet;
- per-call payment authorization signed locally;
- explicit confirmation gate plus regional eligibility enforcement.

## Tested fallbacks
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blockrun/mcp",
"version": "0.33.0",
"version": "0.33.1",
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, and crypto data. Paid via x402 micropayments.",
"type": "module",
Expand Down Expand Up @@ -53,7 +53,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@blockrun/llm": "^3.6.1",
"@blockrun/llm": "^3.8.4",
"@modelcontextprotocol/sdk": "^1.0.0",
"@polymarket/builder-relayer-client": "^0.0.10",
"@polymarket/builder-signing-sdk": "0.0.8",
Expand Down
29 changes: 17 additions & 12 deletions skills/prediction-markets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ One tool, three params. Method auto-routes: POST when `body` is set, GET otherwi
```ts
blockrun_markets({ path: "polymarket/events", params: { limit: "10" } })

blockrun_markets({ path: "polymarket/candlesticks/0xCONDITION_ID", params: { interval: "60" } })
blockrun_markets({ path: "polymarket/candlesticks/0xCONDITION_ID", params: { interval: "1440" } })

blockrun_markets({ path: "polymarket/wallet/identities", body: {
addresses: ["0xabc...", "0xdef..."]
Expand All @@ -73,8 +73,10 @@ blockrun_markets({ path: "polymarket/wallet/identities", body: {

Paths are relative — no `/api/v1/pm/` prefix. Use `agent_id` to bill a child agent's budget.

Make paid calls sequentially when one wallet is paying. The MCP serializes them
as a second guard against concurrent x402 payment races.
Paid calls can run in parallel **on Base**: each EIP-3009 authorization carries
its own random nonce, so concurrent calls from one wallet cannot collide.
The Solana payload has no nonce field — distinctness comes from the SDK
(`@blockrun/llm` >= 3.8.4), so keep that floor if you fan out on Solana.

Current parameter contracts that prevent paid 4xx responses:

Expand All @@ -84,14 +86,18 @@ Current parameter contracts that prevent paid 4xx responses:
`q`. Use `status:"open"` rather than Gamma's `active`/`closed`, and `sort`
rather than `order`/`ascending`. `end_after`/`end_before` are supported
(Unix seconds).
- Candlestick `interval` is integer minutes: `0`, `1`, `5`, `15`, `60`, or
`1440`. Optional `start_time`/`end_time` are Unix seconds.
- Candlestick `interval` is integer minutes (`1440`, not `1h`) and is
**optional** — the server has a default. Which intervals a market can serve
varies: `1440` may work where `60` returns a paid 400. Optional
`start_time`/`end_time` are Unix seconds.
- `polymarket/orderbooks` requires `token_id`, `start_time`, and `end_time`; the
times are Unix milliseconds.
- Smart-money calls need at least one cohort filter — `window`, `min_trades`,
`min_volume`, `min_roi`, `min_realized_pnl`, `min_total_pnl`, `min_win_rate`,
or `min_profit_factor`. For general analysis use
`{ window: "30d", min_trades: "100" }`; narrower cohorts are fine too.
- Smart-money needs a smart-wallet **criterion**: `min_trades`, `min_volume`,
`min_roi`, `min_realized_pnl`, `min_total_pnl`, `min_win_rate`, or
`min_profit_factor`. `window` only scopes the time range and is **not**
sufficient alone (verified: window-only returns a paid 400). Use
`{ window: "30d", min_trades: "100" }`; narrower cohorts are fine.
- `markets/listings` is retired upstream (410 Gone) — the MCP blocks it before payment.

## Two Pricing Tiers

Expand All @@ -107,7 +113,6 @@ Pass-through pricing, 0% BlockRun margin — settles straight to Predexon's Base
|---|---|---|
| **Same question across venues** | `markets` | 1 |
| **Search every venue at once** | `markets/search` | 2 |
| Venue-native tradable listings | `markets/listings` | 1 |
| Resolve a canonical outcome ID | `outcomes/{predexon_id}` | 1 |
| **Equivalent markets (arbitrage)** | `matching-markets` | 2 |
| Active matched pairs | `matching-markets/pairs` | 2 |
Expand Down Expand Up @@ -177,7 +182,7 @@ blockrun_markets({ path: "outcomes/PXM-12345" }) // → venue listings + price

```ts
blockrun_markets({ path: "polymarket/candlesticks/0xCONDITION_ID", params: {
interval: "60", start_time: "<UNIX_SECONDS>", end_time: "<UNIX_SECONDS>"
interval: "1440", start_time: "<UNIX_SECONDS>", end_time: "<UNIX_SECONDS>"
} })
blockrun_markets({ path: "polymarket/volume-chart/0xCONDITION_ID" })
blockrun_markets({ path: "polymarket/markets/0xCONDITION_ID/open_interest" })
Expand Down Expand Up @@ -284,7 +289,7 @@ Inside the MCP, use `blockrun_markets` above. For standalone scripts:
from blockrun_llm import setup_agent_wallet # setup_agent_solana_wallet() on Solana
client = setup_agent_wallet()

client.pm("polymarket/candlesticks/0xCONDITION_ID", interval="60")
client.pm("polymarket/candlesticks/0xCONDITION_ID", interval="1440")
client.pm_query("polymarket/wallet/identities", {"addresses": ["0xabc"]})
```

Expand Down
11 changes: 6 additions & 5 deletions skills/signal-to-trade-demo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ or preparing a fallback.
4. Choose the smallest whole-dollar preview from $1–$5 that satisfies the live
`min_order_size` and book depth. Never present a smaller, non-executable
preview as valid. Do not split orders to bypass caps.
5. Make paid market-data calls sequentially. Do not launch them in parallel
against one payment wallet.
5. Paid market-data calls may run in parallel on Base. On Solana keep
`@blockrun/llm` >= 3.8.4, which is what makes concurrent payments distinct.

## 1. Private operator preflight

Expand Down Expand Up @@ -80,12 +80,13 @@ Use four independent lenses where the market supports them:
```text
blockrun_markets {
path: "polymarket/candlesticks/token/<TOKEN_ID>",
params: { interval: "60", start_time: "<UNIX_SECONDS>", end_time: "<UNIX_SECONDS>" }
params: { interval: "1440", start_time: "<UNIX_SECONDS>", end_time: "<UNIX_SECONDS>" }
}
```

`interval` is integer minutes (`60`, not `1h`); `start_time` and `end_time`
are Unix seconds.
`interval` is integer minutes (`1440`, not `1h`) and is optional. `60` was
observed returning a paid 400 where `1440` worked; `start_time` and
`end_time` are Unix seconds.
3. **Smart money:** use a meaningful cohort:

```text
Expand Down
3 changes: 1 addition & 2 deletions src/tools/defi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { z } from "zod";
import { reserveBudget, recordSpending } from "../utils/budget.js";
import { withTxFee } from "../utils/tx-fee.js";
Expand Down Expand Up @@ -69,7 +68,7 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
}
try {
const client = getClient() as unknown as RawClient;
const result = await serializePaidRequest(() => client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`));
const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
3 changes: 1 addition & 2 deletions src/tools/exa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { z } from "zod";
import { reserveBudget, recordSpending } from "../utils/budget.js";
import { withTxFee } from "../utils/tx-fee.js";
Expand Down Expand Up @@ -80,7 +79,7 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
try {
const client = getClient() as unknown as RawClient;
const endpoint = `/v1/exa/${cleanPath}`;
const result = await serializePaidRequest(() => client.requestWithPaymentRaw(endpoint, body ?? {}));
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
13 changes: 5 additions & 8 deletions src/tools/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { extractErrorMessage, formatError } from "../utils/errors.js";
import { hasPathTraversal } from "../utils/path-safety.js";
import type { BudgetState } from "../types.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { validateMarketRequest } from "../utils/markets-validation.js";

// What x402 CHARGES, which is not the 402's JSON `price` field. That field is the
Expand Down Expand Up @@ -36,7 +35,6 @@ export function registerMarketsTool(server: McpServer, budget: BudgetState): voi

CANONICAL CROSS-VENUE (Tier 1) — Predexon v2 unified data layer:
- markets — list canonical market/question containers with cross-venue Predexon IDs
- markets/listings — venue-native executable listings flattened across canonical markets
- outcomes/:predexon_id — resolve a canonical outcome ID to its market context + venue listings
Filter with ?venue=polymarket|kalshi|limitless|opinion|predictfun, ?status=, ?category=, ?league=, ?event_id=, ?pagination_key=

Expand Down Expand Up @@ -85,10 +83,9 @@ CROSS-PLATFORM:
REQUEST CONTRACTS:
- Discover current markets with markets/search (its search term is "q"), then resolve the chosen Polymarket market with polymarket/markets/keyset and condition_id.
- On polymarket/markets{,/keyset} the free-text filter is "search" (NOT "q"), and status:"open"/"closed" replaces Gamma's active/closed. "sort", "end_after", and "end_before" are supported; "order"/"ascending" are not.
- Candlesticks interval is integer minutes: 0|1|5|15|60|1440 (use "60", not "1h"); start_time/end_time are Unix seconds.
- Candlesticks interval is integer minutes ("1440", not "1h"); it is OPTIONAL (the server defaults). Which intervals a market serves varies — 1440 may work where 60 does not. start_time/end_time are Unix seconds.
- polymarket/orderbooks requires token_id plus start_time/end_time in Unix milliseconds.
- Smart-money calls need at least one cohort filter (window, min_trades, min_volume, min_roi, min_*_pnl, min_win_rate, min_profit_factor); a good default is { window: "30d", min_trades: "100" }.
- Issue paid calls sequentially. The MCP also serializes them to protect one wallet from concurrent x402 payment races.
- Smart-money needs a smart-wallet CRITERION (min_trades, min_volume, min_roi, min_*_pnl, min_win_rate, min_profit_factor). "window" only scopes time and is NOT sufficient on its own. Default: { window: "30d", min_trades: "100" }.

Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. polymarket/wallet/identities).`,
annotations: TOOL_ANNOTATIONS.readOnlyOpenWorld,
Expand Down Expand Up @@ -121,9 +118,9 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
}
try {
const llm = getClient();
const result = await serializePaidRequest(() => body !== undefined
? llm.pmQuery(path, body)
: llm.pm(path, params));
const result = body !== undefined
? await llm.pmQuery(path, body)
: await llm.pm(path, params);
recordSpending(budget, estimatedCost, agent_id);

return {
Expand Down
5 changes: 2 additions & 3 deletions src/tools/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type { BudgetState } from "../types.js";
import { getChain, getPriceClient } from "../utils/wallet.js";
import { extractErrorMessage, formatError } from "../utils/errors.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";

const CATEGORY = z.enum(["crypto", "fx", "commodity", "usstock", "stocks"]);
const MARKET = z.enum([
Expand Down Expand Up @@ -105,7 +104,7 @@ Examples:
market: market as StockMarket | undefined,
session: session as MarketSession | undefined,
});
const result = paid ? await serializePaidRequest(task) : await task();
const result = await task();
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand All @@ -123,7 +122,7 @@ Examples:
from,
to,
});
const result = paid ? await serializePaidRequest(task) : await task();
const result = await task();
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
3 changes: 1 addition & 2 deletions src/tools/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { z } from "zod";
import { reserveBudget, recordSpending } from "../utils/budget.js";
import { withTxFee } from "../utils/tx-fee.js";
Expand Down Expand Up @@ -92,7 +91,7 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
}
try {
const client = getClient() as unknown as RawClient;
const result = await serializePaidRequest(() => client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body));
const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
3 changes: 1 addition & 2 deletions src/tools/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { z } from "zod";
import { reserveBudget, recordSpending } from "../utils/budget.js";
import { asStructuredContent, coerceBody } from "../utils/body.js";
Expand Down Expand Up @@ -102,7 +101,7 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
try {
const client = getClient() as unknown as RawClient;
const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
const result = await serializePaidRequest(() => client.requestWithPaymentRaw(endpoint, body ?? {}));
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
7 changes: 3 additions & 4 deletions src/tools/surf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { TOOL_ANNOTATIONS } from "../tool-annotations.js";
import { serializePaidRequest } from "../utils/payment-serialization.js";
import { z } from "zod";
import { reserveBudget, recordSpending } from "../utils/budget.js";
import { asStructuredContent, coerceBody } from "../utils/body.js";
Expand Down Expand Up @@ -101,9 +100,9 @@ Each Surf endpoint pre-validates required params before settling — you get a 4
try {
const client = getClient() as unknown as SurfClient;
const endpoint = `/v1/surf/${cleanPath}`;
const result = await serializePaidRequest(() => body !== undefined
? client.requestWithPaymentRaw(endpoint, body)
: client.getWithPaymentRaw(endpoint, params));
const result = body !== undefined
? await client.requestWithPaymentRaw(endpoint, body)
: await client.getWithPaymentRaw(endpoint, params);
recordSpending(budget, estimatedCost, agent_id);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
Expand Down
Loading
Loading