AI agent with live market data
An assistant that answers with live liquidations, clusters, funding and whale moves, and cites them.
What you get
Give a language model four tools (liquidation clusters, liquidation totals, funding, whale moves) with JSON-Schema definitions any tool-calling API accepts, plus /llms.txt as context. Answers come trimmed to what a model needs, with the time of the data and the credit line, so it cites correctly.
liquidation_clusters(coin)
liquidations_summary(coin, hours)
funding(coin)
whale_moves(coin, min_usd, limit)
liquidation_clusters({"coin":"BTC"}) -> {"coin":"BTC","mark":81486,"above":[{"price":91500,"usd":104183604,"distance":0.1229},{"price":97500,"usd":92616070,"distance":0.1965},{"price":89500,"usd":70949011,"distance":0.0…
liquidations_summary({"hours":24}) -> {"hours":24,"totalUsd":106495945.77,"longUsd":14368941.56,"shortUsd":92127004.22,"count":33659,"byVenue":[{"venue":"binance","longUsd":11931819.87,"shortUsd":59750027.18,"count":265…
funding({"coin":"ETH"}) -> {"coin":"ETH","byVenue":{"hyperliquid":0.1095},"aprSpread":null,"updatedAt":"2026-10-28T09:53:00.912Z","credit":"Data by Blockmetric · Source: Hyperliquid"}
whale_moves({"limit":3}) -> {"moves":[{"ts":"2026-10-28T09:51:11.745Z","wallet":"0xf71ab4e4172730ed096b832f37982942935c7215","coin":"ETH","kind":"close","side":"short","usdDelta":-1044746,"price":2371.6},{"ts":"2026-10-…Real output of the program below, run against our API on 2026-10-28 10:38 UTC.
Build it step by step
Give the model the map
Put /llms.txt into the system prompt: every endpoint, the auth header, units and limits on one page. /llms-full.txt is the full reference when the model needs field details.
curl -s "$SITE/llms.txt"Register the tools
Pass tools (name, description, parameters as JSON Schema) to your model. Some SDKs call the schema field input_schema; rename it there.
Run the calls
When the model asks for a tool, run callTool(name, args) / call_tool(name, args) and send the result back. Errors are returned as { error, message } instead of exceptions.
Let it cite
Every result carries updatedAt and credit. Ask the model to name the time and the source in its answer.
The complete tool set
Set this once in the shell that runs the code:
export BLOCKMETRIC_API_KEY=bm_live_… # create one on /account/api
export BLOCKMETRIC_BASE_URL=https://api.blockmetric.io/v1
export SITE=https://blockmetric.io// Tools for an AI agent: four Blockmetric functions with JSON-Schema definitions that any tool-calling LLM API accepts
// (pass `tools` to your model, run `callTool(name, args)` when it asks, send the result back). Node 18+, no dependencies.
// The answers are trimmed to what a model needs, with the time of the data and the credit line, so it cites correctly.
//
// export BLOCKMETRIC_API_KEY=bm_live_... # optional: without a key the tools use keyless public access
// export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
// node agent-tools.mjs # demo: prints every tool and one call of each
//
// Context for the model: put the text of /llms.txt into the system prompt; it lists every endpoint in one page.
import { pathToFileURL } from 'node:url'
const BASE = (process.env.BLOCKMETRIC_BASE_URL ?? '').replace(/\/+$/, '')
const KEY = process.env.BLOCKMETRIC_API_KEY
if (!BASE) throw new Error('Set BLOCKMETRIC_BASE_URL, e.g. https://<domain>/api/v1')
async function api(path, params = {}) {
const url = new URL(BASE + path)
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v))
const res = await fetch(url, { headers: KEY ? { Authorization: `Bearer ${KEY}` } : { 'X-Blockmetric-Access': 'public' }, signal: AbortSignal.timeout(30_000) })
const body = await res.json()
// Errors go back to the model as data: code and message tell it how to correct the call.
if (!res.ok) return { error: body.error?.code, message: body.error?.message }
return body
}
const meta = (b) => ({ updatedAt: b.meta.updatedAt, credit: b.meta.attribution.text })
/** Tool definitions (JSON Schema). Some SDKs call the schema field input_schema instead of parameters. */
export const tools = [
{
name: 'liquidation_clusters',
description: 'Where liquidations would happen next for one coin: the largest clusters of liquidation prices above and below the current price, from real tracked Hyperliquid positions. Distance is a fraction of the price (0.02 = 2 %).',
parameters: { type: 'object', properties: { coin: { type: 'string', description: 'Coin, e.g. BTC, ETH, HYPE' } }, required: ['coin'] },
},
{
name: 'liquidations_summary',
description: 'Liquidations that happened: long and short USD, count, per venue and the largest single one over the last hours.',
parameters: { type: 'object', properties: { coin: { type: 'string' }, hours: { type: 'integer', minimum: 1, maximum: 168, default: 24 } } },
},
{
name: 'funding',
description: 'Funding rates of one coin on every venue (annualised fractions, 0.10 = 10 % a year) and the spread between them.',
parameters: { type: 'object', properties: { coin: { type: 'string' } }, required: ['coin'] },
},
{
name: 'whale_moves',
description: 'Recent position changes of large tracked Hyperliquid wallets (open, add, cut, close, flip, liquidated), newest first.',
parameters: { type: 'object', properties: { coin: { type: 'string' }, min_usd: { type: 'number', default: 1000000 }, limit: { type: 'integer', default: 10, maximum: 50 } } },
},
]
export async function callTool(name, args = {}) {
if (name === 'liquidation_clusters') {
const b = await api(`/heatmap/${encodeURIComponent(args.coin)}`, { tf: '15m', hours: 1, clusters: 5 })
if (b.error) return b
const c = b.data.clusters
const pick = (x) => ({ price: x.price, usd: Math.round(x.usd), distance: Number(x.distance.toFixed(4)) })
return { coin: b.data.coin, mark: c?.mark ?? null, above: c?.above.map(pick) ?? [], below: c?.below.map(pick) ?? [], ...meta(b) }
}
if (name === 'liquidations_summary') {
const b = await api('/liquidations/summary', { coin: args.coin, hours: args.hours ?? 24 })
if (b.error) return b
const { totalUsd, longUsd, shortUsd, count, byVenue, largest } = b.data
return { hours: b.data.hours, totalUsd, longUsd, shortUsd, count, byVenue, largest, ...meta(b) }
}
if (name === 'funding') {
const b = await api('/funding', { coin: args.coin })
if (b.error) return b
const r = b.data[0]
if (!r) return { error: 'not_found', message: `No funding for ${args.coin}` }
return { coin: r.coin, byVenue: Object.fromEntries(Object.entries(r.byVenue).map(([v, x]) => [v, x?.fundingApr ?? null])), aprSpread: r.aprSpread, ...meta(b) }
}
if (name === 'whale_moves') {
const b = await api('/whales/feed', { coin: args.coin, minUsd: args.min_usd ?? 1_000_000, limit: Math.min(args.limit ?? 10, 50) })
if (b.error) return b
return { moves: b.data.map((e) => ({ ts: e.ts, wallet: e.label ?? e.address, coin: e.coin, kind: e.kind, side: e.side, usdDelta: Math.round(e.usdDelta), price: e.price })), ...meta(b) }
}
return { error: 'unknown_tool', message: `No tool named ${name}` }
}
// Demo: what the model would receive for one call of each tool.
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
for (const t of tools) console.log(`${t.name}(${Object.keys(t.parameters.properties).join(', ')})`)
console.log('')
for (const [name, args] of [['liquidation_clusters', { coin: 'BTC' }], ['liquidations_summary', { hours: 24 }], ['funding', { coin: 'ETH' }], ['whale_moves', { limit: 3 }]])
console.log(`${name}(${JSON.stringify(args)}) ->`, JSON.stringify(await callTool(name, args)))
}
Run it:
node agent-tools.mjs # prints every tool and one call of each
python agent_tools.pyDownload: agent-tools.mjs · agent_tools.py · requests.sh. MIT-0: use it as you like.
Rate limits and pagination
- One tool call is one API request. An agent that calls three tools per question uses three requests.
- Keyless access is limited per IP (10/min); errors come back as data, so the model can explain or retry.
- Trim answers before they reach the model: the heatmap tool returns clusters, not the whole grid.
- Keep the key in the tool runner, never in the prompt.
- OpenAPI 3.1 (/api/v1/openapi.json) works for frameworks that build tools from a spec.
Common pitfalls
- Models invent parameters. The tool descriptions name the units (fractions, USD) and the API answers invalid_param with the allowed values.
- A model that loops on one tool burns requests; cap tool calls per question.
Who it is for
- Builders of trading assistants and research agents
- Teams adding a chat interface to market data
- Anyone who wants an LLM to call the API correctly the first time
Plan and limits
Honest limits
- Data from public sources, not investment advice; tell the model to say so.
- The MCP server is being prepared for release; until then these tools give an agent the same data through the REST API.
Next steps
More recipes like this
/markets/liquidations/summary+1Multi-chain dApp backendBalances, blocks and token transfers on eight chains with one token.RPC Free20 min to buildeth_getBalanceeth_blockNumber+2