Market data in your app

Funding, open interest and liquidations on your own site, cached and credited.

Cheapest plan
No key
Time to build
30 min
Endpoints
3
/markets · /liquidations/summary · /me
Languages
2
Node 18+, Python 3.9+, no installs

What you get

Two ways to show our data to your users. No code: paste a widget (free for editorial and non-commercial sites). Your own design: a small backend fetches the data with API Pro, caches it for all visitors and hands your frontend only what it shows, with the credit line the licence asks for.

{
  "updatedAt": "2026-10-28T09:53:01.688Z",
  "markets": [
    {
      "coin": "BTC",
      "mark": 81475.8,
      "fundingApr": 0.1095,
      "openInterestUsd": 3458884780.14,
      "volume24hUsd": 1865472208.24
    },
    {
      "coin": "ETH",
      "mark": 2370.2,
      "fundingApr": 0.1095,
      "openInterestUsd": 2653676112.93,
      "volume24hUsd": 700892496.87
    }
  ],
  "liquidations24h": {
    "totalUsd": 106495945.77,
    "longUsd": 14368941.56,
    "shortUsd": 92127004.22
  },
  "attribution": {
    "text": "Data by Blockmetric · Source: Hyperliquid · Binance · Bybit",
    "url": "https://blockmetric.io/"
  },
  "license": "display"
}

Real output of the program below, run against our API on 2026-10-28 10:38 UTC.

Build it step by step

  1. Widget or your own UI?

    A widget is an iframe or a script tag that updates itself and carries the credit. Build your own UI when you need your design, other numbers or a commercial product.

  2. Fetch on the server, never in the browser

    The key stays on your server. A request with a key from a browser answers 403 origin_not_allowed unless you allow that origin on the key.

  3. Cache and revalidate

    Keep one copy for every visitor (30 seconds is plenty for funding and open interest) and send If-None-Match with the last ETag: an unchanged answer is a 304 without a body.

    curl -si -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" -H 'If-None-Match: "<etag>"' \
      "$BLOCKMETRIC_BASE_URL/markets?venue=hyperliquid" | head -1
  4. Show the credit

    meta.attribution.text is the finished credit line with the sources of that answer ("Data by Blockmetric · Source: ..."); link it to meta.attribution.url next to the data.

The complete backend

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
// Market data in your app: a small backend that fetches Blockmetric data on the server, caches it and hands your
// frontend exactly what it shows, with the credit line the licence asks for. Node 18+, no dependencies.
// Your key never reaches the browser, and 1,000 visitors cost the same few API requests as one.
//
//   export BLOCKMETRIC_API_KEY=bm_live_...            # API Pro: display licence for data shown to your users
//   export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
//   node server.mjs                                    # http://localhost:8080 and http://localhost:8080/api/market
//
// Options: PORT (8080), CACHE_SECONDS (30), TOP (10), ONCE=1 (print the JSON once, then exit).
import http from 'node:http'

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')
const PORT = Number(process.env.PORT ?? 8080)
const CACHE_SECONDS = Number(process.env.CACHE_SECONDS ?? 30)
const TOP = Number(process.env.TOP ?? 10)

// Revalidation with ETag: an unchanged answer comes back as 304 without a body.
const etags = new Map()
async function api(path) {
  const prev = etags.get(path)
  const headers = { ...(KEY ? { Authorization: `Bearer ${KEY}` } : { 'X-Blockmetric-Access': 'public' }), ...(prev ? { 'If-None-Match': prev.etag } : {}) }
  const res = await fetch(BASE + path, { headers, signal: AbortSignal.timeout(20_000) })
  if (res.status === 304 && prev) return prev.body
  const body = await res.json()
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`)
  if (res.headers.get('etag')) etags.set(path, { etag: res.headers.get('etag'), body })
  return body
}

/** "Data by Blockmetric · Source: A · B" from several credit lines (the sources of every answer, each named once). */
export function credit(lines) {
  const sources = [...new Set(lines.flatMap((l) => (l.split(' · Source: ')[1] ?? '').split(' · ').filter(Boolean)))]
  return `${lines[0].split(' · Source: ')[0]}${sources.length ? ` · Source: ${sources.join(' · ')}` : ''}`
}

/** What the frontend needs, nothing more: top markets by open interest, 24h liquidations and the credit line. */
async function build() {
  const [markets, liq] = await Promise.all([api('/markets?venue=hyperliquid'), api('/liquidations/summary?hours=24')])
  return {
    updatedAt: markets.meta.updatedAt,
    markets: markets.data.slice(0, TOP).map((m) => ({ coin: m.coin, mark: m.mark, fundingApr: m.fundingApr, openInterestUsd: m.openInterestUsd, volume24hUsd: m.volume24hUsd })),
    liquidations24h: { totalUsd: liq.data.totalUsd, longUsd: liq.data.longUsd, shortUsd: liq.data.shortUsd },
    // Show this credit next to the data. Each answer's meta.attribution names its own sources; the page shows two
    // answers, so the credit names the sources of both.
    attribution: { text: credit([markets.meta.attribution.text, liq.meta.attribution.text]), url: markets.meta.attribution.url },
    license: markets.meta.license.id,
  }
}

let cache = { at: 0, value: null, pending: null }
async function cached() {
  if (cache.value && Date.now() - cache.at < CACHE_SECONDS * 1000) return cache.value
  cache.pending ??= build()
    .then((value) => (cache = { at: Date.now(), value, pending: null }).value)
    .catch((e) => {
      cache.pending = null
      if (cache.value) return cache.value // serve the last good answer while the API is unreachable
      throw e
    })
  return cache.pending
}

const PAGE = `<!doctype html><meta charset="utf-8"><title>Markets</title>
<table id="t"><thead><tr><th>Coin</th><th>Price</th><th>Funding (APR)</th><th>Open interest</th></tr></thead><tbody></tbody></table>
<p id="liq"></p><p><small id="credit"></small></p>
<script>
const usd = (n) => '$' + (n >= 1e9 ? (n / 1e9).toFixed(2) + 'B' : (n / 1e6).toFixed(1) + 'M')
fetch('/api/market').then((r) => r.json()).then((d) => {
  document.querySelector('#t tbody').innerHTML = d.markets.map((m) => '<tr><td>' + m.coin + '</td><td>' + m.mark.toLocaleString('en-US') + '</td><td>' + (m.fundingApr * 100).toFixed(1) + ' %</td><td>' + usd(m.openInterestUsd) + '</td></tr>').join('')
  document.querySelector('#liq').textContent = 'Liquidated in 24h: ' + usd(d.liquidations24h.totalUsd)
  const a = document.createElement('a'); a.href = d.attribution.url; a.textContent = d.attribution.text; a.rel = 'noopener'
  document.querySelector('#credit').append(a, ' · updated ' + d.updatedAt.slice(11, 16) + ' UTC')
})
</script>`

if (process.env.ONCE === '1') {
  console.log(JSON.stringify(await cached(), null, 2))
} else {
  http
    .createServer(async (req, res) => {
      if (req.url === '/api/market') {
        try {
          const body = JSON.stringify(await cached())
          res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': `public, max-age=${CACHE_SECONDS}` }).end(body)
        } catch (e) {
          res.writeHead(502, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: e.message }))
        }
      } else if (req.url === '/') res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(PAGE)
      else res.writeHead(404).end()
    })
    .listen(PORT, () => console.log(`http://localhost:${PORT}`))
}

Run it:

node server.mjs   # http://localhost:8080
python server.py
ONCE=1 node server.mjs   # print the JSON once

Download: server.mjs · server.py · requests.sh. MIT-0: use it as you like.

Real responses

What the API answered when we ran this recipe, with long lists shortened.

Markets of one venue: GET /markets?venue=hyperliquid
{
  "data": [
    {
      "venue": "hyperliquid",
      "coin": "BTC",
      "mark": 81475.8,
      "oracle": 81505.1,
      "fundingHourly": 0.0000125,
      "fundingApr": 0.1095,
      "openInterestUsd": 3458884780.14,
      "volume24hUsd": 1865472208.24,
      "premium": -0.0003202254,
      "maxLeverage": 40,
      "updatedAt": "2026-10-28T09:53:00.912Z"
    },
    "… 347 more"
  ],
  "meta": {
    "updatedAt": "2026-10-28T09:53:01.688Z",
    "sources": [
      "hyperliquid"
    ],
    "plan": "api_pro",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid",
      "url": "https://blockmetric.io/"
    },
    "restricted": [
      "binance",
      "bybit"
    ],
    "license": {
      "id": "display",
      "attributionRequired": true
    }
  }
}

Rate limits and pagination

  • API Pro: 300/min, 500,000/day, 1095 days of history. With a 30-second cache one page costs about 6 requests a minute, however many visitors it has.
  • API Dev is internal use only: dashboards for your own team, not for customers.
  • Serve the last good answer when the API is unreachable; the backend does that instead of showing an error.
  • GET /me tells your key’s licence (data.access.commercial, meta.license.id).

Common pitfalls

  • Calling the API from every visitor’s browser spends your quota per page view and exposes the key.
  • Removing or hiding the credit line breaks the licence; keep it visible near the numbers.

Who it is for

  • Product teams adding market data to an app or site
  • Media and community sites that want live numbers
  • Developers who need a pattern for caching and attribution

Plan and limits

No keyCheapest
Widgets: no account, no key, free for editorial and non-commercial sites.
10/min and 300/day per IP, 24 h of history
API Pro$249/mo
Display licence: data in your own product for up to 50,000 monthly users, with the credit line.
300/min, 500,000/day, 1095 days of history

Compare plans

Honest limits

  • Widgets show free-tier data (for the heatmap: the free coins).
  • Data shown to others needs the display licence of API Pro or an Enterprise contract for redistribution or white-label.

Next steps

More recipes like this

LiveHyperliquid markets 12sOperationalHyperliquid positions & whales 4sOperationalLiquidation heatmap & levels 9sOperationalLighter markets & liquidations 19sOperationalLighter positions & heatmap 5sOperationalLighter RH positions & heatmap 4sOperationalBinance markets 42sOperationalBybit markets 10sOperationalBinance liquidations 21sOperationalBybit liquidations 21sOperationaldYdX positions & heatmap 9sOperationalGMX positions & heatmap 6sOperationalLong/short & open interest history 22sOperationalOrder books & trade flow 4sOperationalOn-chain transfers 4sOperationalSecurity monitor 32sOperationalBlockmetric Sentiment 2mOperationalOfficial news 5mOperationalAlert delivery 4sOperational10:13:40 UTCData: Blockmetric Research · Binance · Bybit