diff --git a/README.md b/README.md index e6793c02..e1201fb9 100644 --- a/README.md +++ b/README.md @@ -445,48 +445,6 @@ LLM agents discover all eight operations as `blockrun_phone_*` / `blockrun_voice --- -## Crypto Data (Surf) - -Surf is BlockRun's unified crypto data API — **84 endpoints across 13 domains**: CEX/DEX markets, on-chain SQL over 80+ ClickHouse tables (Ethereum, Base, Arbitrum, BSC, TRON, HyperEVM, Tempo), 100M+ labeled wallets, prediction markets (Polymarket + Kalshi), social/CT mindshare, news, project/DeFi metrics, token analytics, unified search, VC fund intelligence. The killer feature is ad-hoc `POST /surf/onchain/sql` — agents query the warehouse directly without running an indexer. - -ClawRouter ships Surf as a **skill, not as typed wrappers**. The proxy whitelists `/v1/surf/*` so any call through the local proxy is paid x402 from the same wallet; the agent reads `skills/surf/SKILL.md` for the endpoint catalog and crafts the HTTP call. No `blockrun_surf_*` tool definitions to maintain; a new Surf endpoint requires zero ClawRouter release. - -**Flat per-call pricing** — every Surf endpoint costs the same. The old -$0.001/$0.005/$0.020 tiers no longer apply; upstream prices all three tiers -identically, and the tier now describes only endpoint weight. - -| Rail | Price per call | -| --------------------------- | ---------------------------------- | -| API key (`api.blockrun.ai`) | **$0.0075** (base rate, no tx fee) | -| Wallet — Solana | **$0.0075** | -| Wallet — Base | **$0.0085** | - -Measured 2026-09-05: the API-key rate from the account ledger, the wallet rates -from the x402 challenge, which quotes the price before anything is signed. That -quote is authoritative for the rail that issued it — the key rail issues no 402, -and the wallet quote includes a transaction fee account credit does not pay, so -the two are not interchangeable. Solana being cheaper than Base is deliberate -(the $0.001 is Base's transaction fee), not drift. - -**Usage (HTTP):** - -```bash -# Aggregated BTC spot price (Tier 1, $0.001) -curl 'http://localhost:8402/v1/surf/market/price?symbol=BTC' - -# Bulk wallet labels over 100M+ labeled wallets (Tier 2, $0.005) -curl 'http://localhost:8402/v1/surf/wallet/labels/batch?addresses=0xabc,0xdef,0x123' - -# Ad-hoc on-chain SQL (Tier 3, $0.020) -curl -X POST 'http://localhost:8402/v1/surf/onchain/sql' \ - -H 'content-type: application/json' \ - -d '{"sql":"SELECT count() FROM ethereum.transactions WHERE block_timestamp >= now() - INTERVAL 1 HOUR"}' -``` - -No Surf account, no API key — settles directly to Surf's Base treasury in USDC via the same wallet as LLM calls. Full endpoint reference: [`skills/surf/SKILL.md`](skills/surf/SKILL.md). Upstream marketplace: . - ---- - ## Models & Pricing 90 models across 9 providers, one wallet. **7 models are $0 — paid models start at fractions of a cent.** @@ -740,8 +698,7 @@ chains, the response cache and the local spend controls. - **One key covers nearly everything.** Verified live against `api.blockrun.ai` on 2026-09-05: chat completions (`/v1/chat/completions`), the Anthropic-shaped `/v1/messages`, `/v1/models`, image generation, speech, video, and the partner - APIs — Surf crypto data, Exa search, prediction markets, and phone lookup and - fraud checks. + APIs — Exa search, prediction markets, and phone lookup and fraud checks. - **The wallet-only exceptions** are the routes that bind a lease or a position to a payer address, because there is no address to bind to without a wallet: buying, renewing, listing and releasing **phone numbers** (the gateway answers diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 8ac7bde0..c2dadf1c 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,7 +1,7 @@ { "id": "blockrun-clawrouter", "name": "BlockRun ClawRouter", - "description": "Smart LLM router \u2014 82 models, x402 micropayments, 84% cost savings. Phone lookup + AI voice calls (Twilio + Bland.ai). Surf crypto-data API (84 endpoints, on-chain SQL, 100M+ labeled wallets) via skill.", + "description": "Smart LLM router \u2014 x402 micropayments, 84% cost savings. Phone lookup + AI voice calls (Twilio + Bland.ai).", "skills": ["./skills"], "activation": { "onStartup": true diff --git a/skills/clawrouter/SKILL.md b/skills/clawrouter/SKILL.md index acb6fd97..b840f87c 100644 --- a/skills/clawrouter/SKILL.md +++ b/skills/clawrouter/SKILL.md @@ -21,13 +21,6 @@ triggers: - "phone number verification" - "sim swap detection" - "outbound ai call" - - "blockrun surf" - - "surf crypto api" - - "on-chain sql query" - - "clickhouse onchain" - - "wallet labels api" - - "crypto mindshare" - - "crypto news api" homepage: https://blockrun.ai/clawrouter.md repository: https://github.com/BlockRunAI/ClawRouter license: MIT @@ -265,16 +258,6 @@ egress by default, so it works out of the box in geoblocked regions). `blockrun_predexon_*` data tools first. See the **`polymarket-trading` skill** for the golden rules and end-to-end flow. -### Crypto Data (Surf) — skill-only integration - -Surf is a unified crypto data API with **84 endpoints across 13 domains**: CEX/DEX markets, on-chain SQL over 80+ ClickHouse tables (Ethereum, Base, Arbitrum, BSC, TRON, HyperEVM, Tempo), 100M+ labeled wallets, prediction markets (Polymarket + Kalshi), social/CT mindshare, news, project/DeFi metrics, token analytics, unified search, VC fund intelligence. The killer feature is `POST /surf/onchain/sql` — ad-hoc SELECT against the warehouse, no indexer required. - -**ClawRouter ships Surf as a skill, not as typed wrappers.** The base proxy whitelists `/v1/surf/*` so any call through `http://127.0.0.1:8402/v1/surf/...` flows through the same x402 wallet that pays for LLM calls. The endpoint catalog, parameter shapes, and example flows live in the dedicated **`surf` skill** — Claude Code (or any agent) reads that skill on demand and crafts the HTTP call itself. No `blockrun_surf_*` tool definitions to maintain, no release of ClawRouter when Surf adds endpoint #85. - -Pricing tiers (per call, settled in USDC directly to Surf's Base treasury): **$0.001** prices/rankings/lists/news · **$0.005** orderbooks/candles/search/wallet details/social · **$0.02** on-chain SQL/query/schema, chat completions. No monthly minimums, no Surf account, no API key. See the `surf` skill for the full endpoint reference. - -This is the pattern for new BlockRun-marketplace APIs going forward: base proxy whitelists the namespace, a dedicated skill documents the surface — no hand-coded tool wrappers per endpoint. - ## Example Output ``` diff --git a/skills/surf/SKILL.md b/skills/surf/SKILL.md deleted file mode 100644 index 17b19519..00000000 --- a/skills/surf/SKILL.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -name: surf -description: Use this skill — NOT browser or web_fetch — for ALL Surf crypto-data calls. 83 endpoints at localhost:8402/v1/surf/* covering CEX/DEX markets, on-chain SQL over 80+ ClickHouse tables (Ethereum, Base, Arbitrum, BSC, TRON, HyperEVM, Tempo), 100M+ labeled wallets, prediction markets (Polymarket + Kalshi), social/CT intelligence, news, project + DeFi metrics, token analytics, unified search, VC fund intelligence. Paid through ClawRouter — x402 USDC from the local wallet, or BlockRun account credit if an API key is configured. No Surf account or Surf API key required either way. -triggers: - - "blockrun surf" - - "surf crypto api" - - "surf onchain sql" - - "onchain sql query" - - "clickhouse onchain query" - - "raw sql ethereum" - - "raw sql base" - - "wallet labels api" - - "labeled wallets api" - - "surf wallet detail" - - "crypto mindshare" - - "crypto news api" - - "fear and greed index crypto" - - "token holder distribution" - - "vc fund portfolio" - - "ethena tokenomics" -homepage: https://blockrun.ai/marketplace/surf -license: MIT ---- - -# Surf — Unified Crypto Data API (via ClawRouter) - -Surf bundles **83 endpoints across 12 domains** into one paid HTTP API. ClawRouter exposes them at `http://127.0.0.1:8402/v1/surf/*`, paid the same way ClawRouter pays for LLM calls — an x402 USDC micropayment from the local wallet, or a draw on BlockRun account credit if an API key is configured (verified working on both, 2026-09-05). **No Surf account and no Surf API key either way.** Upstream lives at `api.asksurf.ai/gateway/v1` — ClawRouter forwards transparently. - -**Pricing — flat per call, the same for every endpoint.** - -Surf used to be tiered at $0.001 / $0.005 / $0.020. It is not any more: upstream -now prices all three tiers identically (`SURF_TIER_{1,2,3}_PRICE` are equal). The -`Tier` column in the catalog below still describes endpoint weight, but it no -longer affects what you pay. Anything quoting the old tier prices is stale. - -| Rail | Price per call | -| --------------------------- | ------------------------------------------- | -| API key (`api.blockrun.ai`) | **$0.0075** — base rate, no transaction fee | -| Wallet, Solana | **$0.0075** | -| Wallet, Base | **$0.0085** | - -Verified 2026-09-05 by measurement, not from a price page: the API-key figure by -diffing the account ledger across one call, the wallet figures by reading the -`amount` in the x402 402 challenge, which quotes the price before anything is -signed. Base and Solana genuinely differ for the same endpoint. - -**A 402 quote is authoritative for the rail that issued it — and only that rail.** -On the wallet rail the price arrives in the challenge before you pay, so if this -table disagrees with it, believe the challenge and treat the table as stale. - -Do NOT carry a wallet-rail 402 figure over to the API-key rail. The key rail -issues no 402 at all, and the wallet quote includes the chain's transaction fee -that account credit does not pay — on Base that is $0.0085 quoted against -$0.0075 actually billed to an account. Reading across rails turns a correct -number into a wrong one; it has already happened once. - -The Base/Solana difference is deliberate, not drift: the $0.001 is the Base -transaction fee, and Solana omitting it is a migration incentive. Solana being -cheaper is expected — Solana priced _above_ Base would be the bug worth -reporting, since estimates generally assume the Base figure is the ceiling. - -> The legacy **surf-1.5 chat** surface is intentionally NOT exposed yet — it's held until per-token settlement is wired. Trying `/v1/surf/chat/completions` returns 404 ("Unknown Surf endpoint"), no payment is taken. - -All requests use GET unless the table below says otherwise. Path parameters that look like `?symbol=` are query params on a GET. POST endpoints take a JSON body. ClawRouter attaches payment transparently — an x402 header in wallet mode, a bearer token in API-key mode. - -**Required-param pre-check.** 56 of the 83 endpoints have required query params (e.g. `pair`, `symbol`, `address`, `chain`, `q`, `interval`). The route validates them **before settlement** — call with missing params and you get `400 { missing_params, all_required, docs }` and you are NOT charged. Check each row in the catalog below for the correct param name (e.g. mindshare is `q` + `interval`, not `project` + `window`). - -## When to use this skill - -- "What is the current BTC price?" → `/surf/market/price?symbol=BTC` (cheaper + more reliable than scraping) -- "Who holds the most USDC on Ethereum?" → `/surf/token/holders?address=0xA0b8...&chain=ethereum` -- "How many Ethereum transactions in the last hour?" → `POST /surf/onchain/sql { sql: 'SELECT count() FROM ethereum.transactions WHERE block_timestamp >= now() - INTERVAL 1 HOUR' }` -- "Label this list of wallets." → `/surf/wallet/labels/batch?addresses=0xabc,0xdef,...` -- "Is HYPE mindshare peaking?" → `/surf/social/mindshare?q=hyperliquid&interval=30d` -- "Find the canonical metadata for 'ethena'." → `/surf/search/project?q=ethena` - -Always prefer Surf over generic web scraping for these. Use the OpenClaw tool name `blockrun_surf_*` when invoking from an agent; use the HTTP path directly when calling from a script. - -## Endpoint catalog - -### Exchange (CEX) — 7 endpoints - -| Path | Tier | Required | -| --------------------------------- | ---- | -------- | -| `/surf/exchange/markets` | T1 | — | -| `/surf/exchange/price` | T1 | `pair` | -| `/surf/exchange/perp` | T1 | `pair` | -| `/surf/exchange/depth` | T2 | `pair` | -| `/surf/exchange/klines` | T2 | `pair` | -| `/surf/exchange/funding-history` | T2 | `pair` | -| `/surf/exchange/long-short-ratio` | T2 | `pair` | - -### Market Overview — 11 endpoints - -| Path | Tier | Required | -| ---------------------------------------- | ---- | ------------------------------------------------- | -| `/surf/market/ranking` | T1 | — | -| `/surf/market/fear-greed` | T1 | — | -| `/surf/market/futures` | T1 | — | -| `/surf/market/price` | T1 | `symbol` | -| `/surf/market/etf` | T1 | `symbol` | -| `/surf/market/options` | T1 | `symbol` | -| `/surf/market/liquidation/exchange-list` | T2 | — | -| `/surf/market/liquidation/order` | T2 | — | -| `/surf/market/liquidation/chart` | T2 | `symbol` | -| `/surf/market/onchain-indicator` | T2 | `symbol`, `metric` (NUPL, SOPR, MVRV, Puell, NVT) | -| `/surf/market/price-indicator` | T2 | `indicator` (RSI, MACD, Bollinger, EMA), `symbol` | - -### News — 2 endpoints - -| Path | Tier | Required | -| ------------------- | ---- | -------------------- | -| `/surf/news/feed` | T1 | — (`limit` optional) | -| `/surf/news/detail` | T1 | `id` | - -### On-Chain — 7 endpoints - -| Path | Method | Tier | Required | -| ------------------------------ | -------- | ---- | ------------------------ | -| `/surf/onchain/bridge/ranking` | GET | T1 | — | -| `/surf/onchain/yield/ranking` | GET | T1 | — | -| `/surf/onchain/gas-price` | GET | T1 | `chain` | -| `/surf/onchain/tx` | GET | T1 | `hash`, `chain` | -| `/surf/onchain/schema` | GET | T3 | — | -| `/surf/onchain/query` | **POST** | T3 | typed predicates in body | -| `/surf/onchain/sql` | **POST** | T3 | `{ sql: "SELECT ..." }` | - -**On-Chain SQL workflow.** Call `/surf/onchain/schema` once to get table names + columns (cache it locally — schema is stable). Then POST your SELECT against `/surf/onchain/sql`. Always include `LIMIT` on large scans — billing is per call, but slow queries time out. Multi-statement queries are rejected upstream. - -### Prediction Markets (Polymarket + Kalshi) — 17 endpoints - -| Path | Tier | Required | -| -------------------------------------------------- | ---- | --------------- | -| `/surf/prediction-market/category-metrics` | T1 | — | -| `/surf/prediction-market/polymarket/ranking` | T1 | — | -| `/surf/prediction-market/polymarket/trades` | T1 | — | -| `/surf/prediction-market/polymarket/markets` | T1 | `market_slug` | -| `/surf/prediction-market/polymarket/events` | T1 | `event_slug` | -| `/surf/prediction-market/polymarket/prices` | T1 | `condition_id` | -| `/surf/prediction-market/polymarket/volumes` | T1 | `condition_id` | -| `/surf/prediction-market/polymarket/open-interest` | T1 | `condition_id` | -| `/surf/prediction-market/polymarket/positions` | T2 | `address` | -| `/surf/prediction-market/polymarket/activity` | T2 | `address` | -| `/surf/prediction-market/kalshi/ranking` | T1 | — | -| `/surf/prediction-market/kalshi/markets` | T1 | `market_ticker` | -| `/surf/prediction-market/kalshi/events` | T1 | `event_ticker` | -| `/surf/prediction-market/kalshi/prices` | T1 | `ticker` | -| `/surf/prediction-market/kalshi/trades` | T1 | `ticker` | -| `/surf/prediction-market/kalshi/volumes` | T1 | `ticker` | -| `/surf/prediction-market/kalshi/open-interest` | T1 | `ticker` | - -(For Polymarket smart-money, wallet PnL, UMA oracle resolution, and the other prediction-market venues — Limitless, Opinion, Predict.Fun, dFlow, Binance Futures, cross-venue canonical markets — use the dedicated **Predexon** integration instead; Surf's prediction-market coverage is narrower but cheaper.) - -### Project + DeFi — 3 endpoints - -| Path | Tier | Required | -| ---------------------------- | ---- | -------- | -| `/surf/project/detail` | T1 | — | -| `/surf/project/defi/metrics` | T1 | `metric` | -| `/surf/project/defi/ranking` | T1 | `metric` | - -### Social / CT Intelligence — 11 endpoints - -| Path | Tier | Required | -| -------------------------------------- | ---- | --------------- | -| `/surf/social/detail` | T2 | — | -| `/surf/social/ranking` | T2 | — | -| `/surf/social/smart-followers/history` | T2 | — | -| `/surf/social/mindshare` | T2 | `q`, `interval` | -| `/surf/social/tweets` | T1 | `ids` | -| `/surf/social/tweet/replies` | T1 | `tweet_id` | -| `/surf/social/user` | T1 | `handle` | -| `/surf/social/user/followers` | T1 | `handle` | -| `/surf/social/user/following` | T1 | `handle` | -| `/surf/social/user/posts` | T1 | `handle` | -| `/surf/social/user/replies` | T1 | `handle` | - -### Token Analytics — 4 endpoints - -| Path | Tier | Required | -| ------------------------ | ---- | ------------------ | -| `/surf/token/tokenomics` | T1 | — | -| `/surf/token/dex-trades` | T2 | `address` | -| `/surf/token/holders` | T2 | `address`, `chain` | -| `/surf/token/transfers` | T2 | `address`, `chain` | - -### Unified Search — 11 endpoints (all Tier 2) - -| Path | Required | -| ---------------------------- | -------- | -| `/surf/search/airdrop` | — | -| `/surf/search/events` | — | -| `/surf/search/kalshi` | — | -| `/surf/search/polymarket` | — | -| `/surf/search/web` | `q` | -| `/surf/search/project` | `q` | -| `/surf/search/news` | `q` | -| `/surf/search/wallet` | `q` | -| `/surf/search/fund` | `q` | -| `/surf/search/social/people` | `q` | -| `/surf/search/social/posts` | `q` | - -### VC Fund Intelligence — 3 endpoints - -| Path | Tier | Required | -| ---------------------- | ---- | -------- | -| `/surf/fund/detail` | T1 | — | -| `/surf/fund/portfolio` | T1 | — | -| `/surf/fund/ranking` | T1 | `metric` | - -### Wallet Intelligence — 6 endpoints (all Tier 2) - -| Path | Required | -| --------------------------- | ----------------------------------- | -| `/surf/wallet/detail` | `address` | -| `/surf/wallet/history` | `address` | -| `/surf/wallet/net-worth` | `address` | -| `/surf/wallet/transfers` | `address` | -| `/surf/wallet/protocols` | `address` | -| `/surf/wallet/labels/batch` | `addresses` (comma-separated, ≤200) | - -### Web — 1 endpoint - -| Path | Tier | Required | -| ----------------- | ---- | -------- | -| `/surf/web/fetch` | T2 | `url` | - -## Example flows - -**1. "Who is wallet X and what do they hold?"** - -```bash -curl 'http://127.0.0.1:8402/v1/surf/wallet/detail?address=vitalik.eth' -``` - -If the response says they're a smart-money wallet, follow up with `/surf/wallet/protocols?address=...` for protocol breakdown or `/surf/wallet/history?address=...` for the activity timeline. - -**2. "How concentrated is supply for token Y?"** - -```bash -curl 'http://127.0.0.1:8402/v1/surf/token/holders?address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&limit=25' -``` - -Combine the top-25 balances with their wallet labels — `/surf/wallet/labels/batch?addresses=...` — to distinguish "concentration in CEX hot wallets" (normal) from "concentration in dev team multisig" (riskier). - -**3. "Run a custom on-chain query."** - -```bash -# Step 1 — fetch schema (do this once, then cache locally) -curl 'http://127.0.0.1:8402/v1/surf/onchain/schema' - -# Step 2 — run the SQL -curl -X POST 'http://127.0.0.1:8402/v1/surf/onchain/sql' \ - -H 'content-type: application/json' \ - -d '{"sql":"SELECT to_address, count() AS hits FROM ethereum.transactions WHERE block_timestamp >= now() - INTERVAL 1 DAY GROUP BY to_address ORDER BY hits DESC LIMIT 20"}' -``` - -Cost: 1 × $0.02 (schema, cached) + 1 × $0.02 (the SQL query) = **$0.04 total** for a custom 24-hour ranking that would otherwise need an indexer. - -**4. "Is project Z trending?"** - -```bash -# Resolve the canonical slug first (search uses q, not query) -curl 'http://127.0.0.1:8402/v1/surf/search/project?q=ethena' - -# Then pull mindshare (mindshare uses q + interval) -curl 'http://127.0.0.1:8402/v1/surf/social/mindshare?q=ethena&interval=30d' -``` - -## How calls are paid - -ClawRouter intercepts every `/v1/surf/*` request through `proxyPaidApiRequest`. The local x402 wallet auto-signs the USDC micropayment; the agent never sees the payment flow. Telemetry tags Surf calls with `tier: SURF` so `clawrouter stats` separates them from LLM, partner, and phone usage. - -No typed `blockrun_surf_*` tools are registered — by design. Each new BlockRun-marketplace API ships as a skill (this file) plus a one-line namespace addition to ClawRouter's proxy whitelist, so adding endpoint #85 requires zero ClawRouter release. diff --git a/src/api-key.ts b/src/api-key.ts index 824bbe3a..73f8b0b2 100644 --- a/src/api-key.ts +++ b/src/api-key.ts @@ -492,7 +492,7 @@ const HINTS: Record = { 402: `Top up your BlockRun credit at ${PORTAL_CREDITS_URL}.`, 404: `api.blockrun.ai does not serve this endpoint. Chat, Anthropic-shaped messages, images, ` + - `speech, video and the partner APIs (Surf, Exa, prediction markets, phone lookup) all work ` + + `speech, video and the partner APIs (Exa, prediction markets, phone lookup) all work ` + `on an API key. The wallet-only exceptions are the routes that bind a lease or a position to ` + `a payer address — buying/renewing/releasing phone numbers, and Polymarket trading — which ` + `need a wallet to own the thing being bought. For those, unset BLOCKRUN_API_KEY and run ` + diff --git a/src/proxy.api-key-services.test.ts b/src/proxy.api-key-services.test.ts index 6310ab16..cb1b9802 100644 --- a/src/proxy.api-key-services.test.ts +++ b/src/proxy.api-key-services.test.ts @@ -137,8 +137,6 @@ describe("account API service parity through the proxy", () => { ["POST", "/v1/phone/numbers/list"], ["POST", "/v1/voice/call"], ["GET", "/v1/voice/call/fixture-call"], - ["GET", "/v1/surf/market/ranking"], - ["POST", "/v1/surf/onchain/sql"], ["GET", "/v1/pm/polymarket/markets?q=test"], ["POST", "/v1/pm/polymarket/query"], ["GET", "/v1/crypto/price/BTC-USD"], diff --git a/src/proxy.surf-retired.test.ts b/src/proxy.surf-retired.test.ts new file mode 100644 index 00000000..6e62b912 --- /dev/null +++ b/src/proxy.surf-retired.test.ts @@ -0,0 +1,116 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { generatePrivateKey } from "viem/accounts"; + +import { startProxy, type ProxyHandle } from "./proxy.js"; +import { InMemorySpendControlStorage, SpendControl } from "./spend-control.js"; + +vi.mock("./logger.js", async (original) => ({ + ...(await original()), + logUsage: vi.fn(async () => {}), +})); + +/** + * BlockRun retired the Surf data API on 2026-09-06; every /api/v1/surf/* path + * now answers 410 endpoint_retired. ClawRouter no longer whitelists /v1/surf/* + * as a paid partner prefix, so: + * + * - wallet mode refuses it locally (unsupported_endpoint 404) — nothing reaches + * the gateway and nothing is signed; + * - API-key mode forwards it through the account passthrough like any other + * non-chat path, so the caller sees the gateway's own 410 and its hint. + */ +const RETIRED = { + error: { message: "retired", type: "invalid_request_error", code: "endpoint_retired" }, + retired_on: "2026-09-06", +}; + +describe("/v1/surf/* after the Surf retirement", () => { + let upstream: Server; + let base: string; + let calls: Array<{ path: string; payment?: string }> = []; + + beforeAll(async () => { + upstream = createServer((req, res) => { + calls.push({ + path: req.url!, + payment: req.headers["payment-signature"] as string | undefined, + }); + req.resume(); + if (req.url?.startsWith("/v1/surf/")) { + res.writeHead(410, { "content-type": "application/json" }); + res.end(JSON.stringify(RETIRED)); + return; + } + res.setHeader("content-type", "application/json"); + res.end('{"data":[]}'); + }); + await new Promise((resolve) => upstream.listen(0, "127.0.0.1", resolve)); + base = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}`; + }); + + afterAll(async () => { + await new Promise((resolve) => upstream.close(() => resolve())); + }); + + beforeEach(() => { + calls = []; + }); + + describe("wallet mode", () => { + let proxy: ProxyHandle; + beforeAll(async () => { + proxy = await startProxy({ + wallet: generatePrivateKey(), + apiBase: base, + port: 0, + skipBalanceCheck: true, + }); + }, 10_000); + afterAll(async () => { + await proxy?.close(); + }); + + it.each([ + ["GET", "/v1/surf/market/price?symbol=BTC"], + ["POST", "/v1/surf/onchain/sql"], + ])("refuses %s %s locally without reaching the gateway", async (method, path) => { + const res = await fetch(proxy.baseUrl + path, { + method, + headers: { "content-type": "application/json" }, + ...(method === "POST" ? { body: '{"sql":"SELECT 1"}' } : {}), + }); + expect(res.status).toBe(404); + const body = (await res.json()) as { error: { code: string; message: string } }; + expect(body.error.code).toBe("unsupported_endpoint"); + // The served-prefix list in the hint must no longer advertise surf. + expect(body.error.message).toContain(",phone,voice}/."); + expect(calls.filter((c) => c.path.startsWith("/v1/surf/"))).toHaveLength(0); + }); + }); + + describe("API-key mode", () => { + let proxy: ProxyHandle; + beforeAll(async () => { + proxy = await startProxy({ + spendControl: new SpendControl({ storage: new InMemorySpendControlStorage() }), + apiKey: "brk_live_surf_retired_123456789", + apiBase: base, + port: 0, + allowExistingProxy: false, + }); + }, 10_000); + afterAll(async () => { + await proxy?.close(); + }); + + it("relays the gateway's 410 endpoint_retired unchanged", async () => { + const res = await fetch(proxy.baseUrl + "/v1/surf/market/price?symbol=BTC"); + expect(res.status).toBe(410); + expect(await res.json()).toEqual(RETIRED); + const call = calls.find((c) => c.path.startsWith("/v1/surf/")); + expect(call?.payment).toBeUndefined(); + }); + }); +}); diff --git a/src/proxy.surf-routing.test.ts b/src/proxy.surf-routing.test.ts deleted file mode 100644 index 392ae2d2..00000000 --- a/src/proxy.surf-routing.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Tests that the partner-routing regex in proxy.ts matches /v1/surf/* paths - * (so Surf API calls flow through proxyPaidApiRequest with x402) without - * accidentally matching unrelated paths like /v1/surfer. - * - * Mirrors the regex literal from src/proxy.ts so a silent regex edit fails - * here loudly. - */ -import { describe, it, expect } from "vitest"; - -// MUST stay in sync with the regex in src/proxy.ts (partner-path match). -const PARTNER_PATH_REGEX = - /^\/v1\/(?:partner|pm|exa|modal|stocks|usstock|crypto|fx|commodity|phone|voice|surf)\//; - -describe("partner path regex — surf", () => { - it("matches /v1/surf/market/price", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf/market/price")).toBe(true); - }); - - it("matches /v1/surf/onchain/sql (POST endpoint)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf/onchain/sql")).toBe(true); - }); - - it("matches /v1/surf/wallet/labels/batch", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf/wallet/labels/batch")).toBe(true); - }); - - it("matches /v1/surf/prediction-market/polymarket/markets", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf/prediction-market/polymarket/markets")).toBe(true); - }); - - it("matches /v1/surf/chat/completions", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf/chat/completions")).toBe(true); - }); - - it("does NOT match /v1/surfer (word-boundary check)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surfer/profile")).toBe(false); - }); - - it("does NOT match /v1/surfaces (prefix collision check)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surfaces/list")).toBe(false); - }); - - it("does NOT match bare /v1/surf (no trailing slash)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/surf")).toBe(false); - }); - - it("still matches existing partner paths (regression guard)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/pm/polymarket/events")).toBe(true); - expect(PARTNER_PATH_REGEX.test("/v1/exa/search")).toBe(true); - expect(PARTNER_PATH_REGEX.test("/v1/phone/lookup")).toBe(true); - expect(PARTNER_PATH_REGEX.test("/v1/voice/call")).toBe(true); - expect(PARTNER_PATH_REGEX.test("/v1/stocks/us/price/AAPL")).toBe(true); - }); - - it("still rejects non-partner /v1 routes (regression guard)", () => { - expect(PARTNER_PATH_REGEX.test("/v1/chat/completions")).toBe(false); - expect(PARTNER_PATH_REGEX.test("/v1/models")).toBe(false); - expect(PARTNER_PATH_REGEX.test("/v1/images/generations")).toBe(false); - }); -}); diff --git a/src/proxy.ts b/src/proxy.ts index 224ffab6..4d503ae2 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2183,9 +2183,8 @@ function estimateImageCost(model: string, size?: string, n: number = 1): number * * Used for partner endpoints (/v1/partner/*, /v1/pm/*), market data * (/v1/stocks/*, /v1/usstock/*, /v1/crypto/*, /v1/fx/*, /v1/commodity/*), - * wallet-backed data tools such as BlockRun Exa (/v1/exa/*), and the Surf - * unified crypto data API (/v1/surf/*). No smart routing, SSE, compression, - * or sessions — just collect body, forward via payFetch (which handles 402 + * and wallet-backed data tools such as BlockRun Exa (/v1/exa/*). No smart + * routing, SSE, compression, or sessions — just collect body, forward via payFetch (which handles 402 * automatically), and stream back. */ /** @@ -2363,16 +2362,13 @@ async function proxyPaidApiRequest( const isModalSandbox = req.url?.startsWith("/v1/modal/") ?? false; const isPhone = (req.url?.startsWith("/v1/phone/") ?? false) || (req.url?.startsWith("/v1/voice/") ?? false); - const isSurf = req.url?.startsWith("/v1/surf/") ?? false; const requestLabel = isBlockrunExa ? "BlockRun Exa" : isModalSandbox ? "Modal Sandbox" : isPhone ? "Phone/Voice" - : isSurf - ? "Surf" - : "Partner"; + : "Partner"; // Collect request body const bodyChunks: Buffer[] = []; @@ -2437,7 +2433,7 @@ async function proxyPaidApiRequest( // answers "which numbers do I own", and upstream marks it public/max-age. // // Keyed on the CREDENTIAL, not on which route handled the request. The - // partner prefixes (/v1/phone, /v1/surf, /v1/pm, ...) are matched before the + // partner prefixes (/v1/phone, /v1/exa, /v1/pm, ...) are matched before the // generic account passthrough and would otherwise keep the upstream's caching // headers while still being account-authenticated. if (accountPassthrough || authMode === "api-key") { @@ -2518,10 +2514,8 @@ async function proxyPaidApiRequest( ? "modal-sandbox" : isPhone ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] // e.g. "phone/lookup", "voice/call" - : isSurf - ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] // e.g. "surf/onchain/sql" - : "partner", - tier: isPhone ? "PHONE" : isSurf ? "SURF" : "PARTNER", + : "partner", + tier: isPhone ? "PHONE" : "PARTNER", cost: requestCost, baselineCost: requestCost, savings: 0, @@ -2534,9 +2528,7 @@ async function proxyPaidApiRequest( ? "modal" : isPhone ? "phone" - : isSurf - ? "surf" - : "partner", + : "partner", }).catch(() => {}); } @@ -4244,9 +4236,12 @@ export async function startProxy(options: ProxyOptions): Promise { // --- Handle paid API paths (/v1/partner/*, /v1/pm/*, /v1/exa/*, /v1/modal/*, // /v1/stocks/*, /v1/usstock/*, /v1/crypto/*, /v1/fx/*, /v1/commodity/*, // /v1/phone/* (Twilio number intelligence + provisioning), - // /v1/voice/* (Bland.ai outbound AI voice calls), - // /v1/surf/* (Surf unified crypto data API: 84 endpoints across CEX/DEX, - // on-chain SQL, wallet intelligence, prediction markets, social, news)) --- + // /v1/voice/* (Bland.ai outbound AI voice calls)) --- + // /v1/surf/* is deliberately absent: BlockRun retired the Surf API on + // 2026-09-06 and the gateway answers every path with 410. In wallet mode it + // now gets the local unsupported_endpoint 404 below without ever reaching + // payFetch; in API-key mode the account passthrough relays the gateway's + // 410 verbatim. // Gateway endpoints that carry no `model` we route on, and so must be // forwarded verbatim rather than fed to the chat path: // /v1/messages — the Anthropic-shaped chat surface @@ -4272,7 +4267,7 @@ export async function startProxy(options: ProxyOptions): Promise { if ( isVerbatimGatewayPath || req.url?.match( - /^\/v1\/(?:partner|pm|exa|modal|stocks|usstock|crypto|fx|commodity|phone|voice|surf)\//, + /^\/v1\/(?:partner|pm|exa|modal|stocks|usstock|crypto|fx|commodity|phone|voice)\//, ) ) { try { @@ -4368,7 +4363,7 @@ export async function startProxy(options: ProxyOptions): Promise { `ClawRouter does not proxy ${req.url}. It serves /v1/chat/completions, /v1/messages, ` + `/v1/models, /v1/images/generations, /v1/images/image2image, /v1/videos/generations, ` + `/v1/audio/generations, /v1/audio/speech, and the partner prefixes /v1/{partner,pm,exa,` + - `modal,stocks,usstock,crypto,fx,commodity,phone,voice,surf}/. ` + + `modal,stocks,usstock,crypto,fx,commodity,phone,voice}/. ` + `If BlockRun serves this endpoint, call it directly against the gateway.`, type: "invalid_request_error", code: "unsupported_endpoint",