Funding arbitrage scanner across venues

A table of the widest funding spreads between venues, now and for the next funding.

Cheapest plan
Free
Time to build
20 min
Endpoints
4
/funding/spreads · /funding · /funding/predicted
Languages
2
Node 18+, Python 3.9+, no installs

What you get

A scanner that lists the coins where two venues charge very different funding, with the yearly and daily spread and the open interest of the thinner leg. It reads the server-side spreads (current and predicted rates) and the funding board per venue, where Hyperliquid and Lighter sit side by side.

Widest funding spreads now (at least 10.0 % a year, thinner leg at least $1.0M open interest)
  BLUR         588.3 % a year  (1.612 % a day)  short on Binance (rate withheld), long on Bybit (rate withheld), OI -
  ANIME        159.1 % a year  (0.436 % a day)  short on Bybit (rate withheld), long on Binance (rate withheld), OI -
  EIGEN         24.4 % a year  (0.067 % a day)  short on Binance (rate withheld), long on Bybit (rate withheld), OI -
  OP            24.1 % a year  (0.066 % a day)  short on Bybit (rate withheld), long on Binance (rate withheld), OI -
  SAND          20.2 % a year  (0.055 % a day)  short on Bybit (rate withheld), long on Binance (rate withheld), OI -

Next funding (predicted): the same view on the rates venues expect to charge next

DEX pairs from the funding board (Hyperliquid)
  no coin with two published venue rates above the thresholds right now

Rates of Binance and Bybit are withheld during a licence review: their legs show the venue, not the rate.
Data by Blockmetric · Source: Hyperliquid · Lighter · Binance · Bybit · 2026-10-28T09:53:00.912Z

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

Build it step by step

  1. Ask for the spreads

    GET /funding/spreads returns one row per coin: the venue with the highest and the lowest annualised funding, the spread per year and per day, and the smaller open interest of the two legs. basis=predicted does the same on the next expected rates.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/funding/spreads?basis=current&minOiUsd=1000000&limit=20"
  2. Compare DEX venues yourself

    GET /funding has every venue side by side per coin (byVenue.hyperliquid, byVenue.lighter, ...). The scanner takes the highest and the lowest published rate of each coin and sorts by the difference.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/funding?coin=ETH"
  3. Filter what you can trade

    MIN_SPREAD_APR (default 10 % a year) and MIN_OI_USD (default $1M on the thinner leg) drop spreads that are too small or too thin to matter.

  4. Run it on a schedule

    EVERY_MINUTES=5 keeps it running; or call it from cron and write the table to a file or a chat.

The complete scanner

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
// Funding arbitrage scanner: where do two venues pay very different funding for the same coin? Node 18+, no dependencies.
// Reads the cross-venue spreads (GET /funding/spreads, current and predicted), the funding board per venue (GET /funding,
// Hyperliquid next to Lighter) and prints the widest spreads with the thinner leg's open interest.
//
//   export BLOCKMETRIC_API_KEY=bm_live_...            # optional: without a key the scanner uses keyless public access
//   export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
//   node scanner.mjs
//
// Options: MIN_SPREAD_APR (0.10 = 10 % a year), MIN_OI_USD (1000000), TOP (10), EVERY_MINUTES (0 = run once).
// A spread between published rates is not an executable return: fees, price moves and different funding times count.

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 MIN_SPREAD_APR = Number(process.env.MIN_SPREAD_APR ?? 0.1)
const MIN_OI_USD = Number(process.env.MIN_OI_USD ?? 1_000_000)
const TOP = Number(process.env.TOP ?? 10)
const EVERY_MINUTES = Number(process.env.EVERY_MINUTES ?? 0)

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()
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`)
  return body
}

const pct = (x) => (x === null || x === undefined ? 'rate withheld' : `${(x * 100).toFixed(1)} %`)
const usd = (n) => (n === null || n === undefined ? '-' : n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : `$${(n / 1e6).toFixed(1)}M`)
const NAME = { hyperliquid: 'Hyperliquid', lighter: 'Lighter', binance: 'Binance', bybit: 'Bybit', okx: 'OKX' }
const venue = (v) => NAME[v] ?? v

/** Spreads computed from the funding board: every pair of venues that both publish a rate for the coin. */
export function boardSpreads(rows) {
  const out = []
  for (const r of rows) {
    const legs = Object.entries(r.byVenue).filter(([, c]) => typeof c?.fundingApr === 'number')
    if (legs.length < 2) continue
    legs.sort((a, b) => b[1].fundingApr - a[1].fundingApr)
    const [hi, lo] = [legs[0], legs[legs.length - 1]]
    out.push({ coin: r.coin, high: hi[0], highApr: hi[1].fundingApr, low: lo[0], lowApr: lo[1].fundingApr, spreadApr: hi[1].fundingApr - lo[1].fundingApr, minOi: Math.min(hi[1].openInterestUsd ?? 0, lo[1].openInterestUsd ?? 0) })
  }
  return out.sort((a, b) => b.spreadApr - a.spreadApr)
}

async function scan() {
  const [current, predicted, board] = await Promise.all([api('/funding/spreads', { basis: 'current', limit: 100 }), api('/funding/spreads', { basis: 'predicted', limit: 100 }), api('/funding', { limit: 500 })])
  const big = (s) => s.spreadApr >= MIN_SPREAD_APR && (s.minOpenInterestUsd === null || s.minOpenInterestUsd >= MIN_OI_USD)

  console.log(`Widest funding spreads now (at least ${pct(MIN_SPREAD_APR)} a year, thinner leg at least ${usd(MIN_OI_USD)} open interest)`)
  for (const s of current.data.filter(big).slice(0, TOP))
    console.log(`  ${s.coin.padEnd(10)} ${pct(s.spreadApr).padStart(9)} a year  (${(s.spreadDaily * 100).toFixed(3)} % a day)  short on ${venue(s.high.venue)} (${pct(s.high.fundingApr)}), long on ${venue(s.low.venue)} (${pct(s.low.fundingApr)}), OI ${usd(s.minOpenInterestUsd)}`)

  console.log('\nNext funding (predicted): the same view on the rates venues expect to charge next')
  for (const s of predicted.data.filter(big).slice(0, TOP)) console.log(`  ${s.coin.padEnd(10)} ${pct(s.spreadApr).padStart(9)} a year  ${venue(s.high.venue)} over ${venue(s.low.venue)}`)

  const dex = boardSpreads(board.data).filter((s) => s.spreadApr >= MIN_SPREAD_APR && s.minOi >= MIN_OI_USD)
  console.log(`\nDEX pairs from the funding board (${board.meta.sources.map(venue).join(', ')})`)
  if (!dex.length) console.log('  no coin with two published venue rates above the thresholds right now')
  for (const s of dex.slice(0, TOP)) console.log(`  ${s.coin.padEnd(10)} ${pct(s.spreadApr).padStart(9)} a year  short on ${venue(s.high)} (${pct(s.highApr)}), long on ${venue(s.low)} (${pct(s.lowApr)}), OI ${usd(s.minOi)}`)

  if (current.meta.restricted?.length) console.log(`\nRates of ${current.meta.restricted.map(venue).join(' and ')} are withheld during a licence review: their legs show the venue, not the rate.`)
  console.log(`${current.meta.attribution.text} · ${current.meta.updatedAt}`)
}

for (;;) {
  await scan()
  if (!EVERY_MINUTES) break
  await new Promise((r) => setTimeout(r, EVERY_MINUTES * 60_000))
  console.log('')
}

Run it:

node scanner.mjs
python scanner.py
MIN_SPREAD_APR=0.2 TOP=20 node scanner.mjs

Download: scanner.mjs · scanner.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.

Widest spreads: GET /funding/spreads?basis=current&limit=2
{
  "data": [
    {
      "coin": "BLUR",
      "basis": "current",
      "high": {
        "venue": "binance",
        "fundingApr": null,
        "intervalHours": null,
        "openInterestUsd": null
      },
      "low": {
        "venue": "bybit",
        "fundingApr": null,
        "intervalHours": null,
        "openInterestUsd": null
      },
      "spreadApr": 5.8826685,
      "spreadDaily": 0.0161169,
      "minOpenInterestUsd": null,
      "venues": 3,
      "updatedAt": "2026-10-28T09:53:00.912Z"
    },
    {
      "coin": "ANIME",
      "basis": "current",
      "high": {
        "venue": "bybit",
        "fundingApr": null,
        "intervalHours": null,
        "openInterestUsd": null
      },
      "low": {
        "venue": "binance",
        "fundingApr": null,
        "intervalHours": null,
        "openInterestUsd": null
      },
      "spreadApr": 1.5908817,
      "spreadDaily": 0.00435858,
      "minOpenInterestUsd": null,
      "venues": 3,
      "updatedAt": "2026-10-28T09:53:00.912Z"
    }
  ],
  "meta": {
    "updatedAt": "2026-10-28T09:53:00.912Z",
    "sources": [
      "hyperliquid",
      "lighter",
      "binance",
      "bybit"
    ],
    "plan": "api_dev",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid · Lighter · Binance · Bybit",
      "url": "https://blockmetric.io/"
    },
    "restricted": [
      "binance",
      "bybit"
    ],
    "license": {
      "id": "internal",
      "attributionRequired": false
    }
  }
}
Funding of every venue for one coin: GET /funding?coin=ETH
{
  "data": [
    {
      "coin": "ETH",
      "hlOpenInterestUsd": 2653676112.93,
      "byVenue": {
        "hyperliquid": {
          "fundingHourly": 0.0000125,
          "fundingApr": 0.1095,
          "openInterestUsd": 2653676112.93,
          "mark": 2370.2,
          "updatedAt": "2026-10-28T09:53:00.912Z"
        }
      },
      "aprSpread": null,
      "maxAbsApr": 0.1095
    }
  ],
  "meta": {
    "updatedAt": "2026-10-28T09:53:00.912Z",
    "sources": [
      "hyperliquid"
    ],
    "plan": "api_dev",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid",
      "url": "https://blockmetric.io/"
    },
    "restricted": [
      "binance",
      "bybit"
    ],
    "license": {
      "id": "internal",
      "attributionRequired": false
    }
  }
}

Rate limits and pagination

  • Free: 10/min, 1,000/day, 30 days of history. One scan is three requests, so a scan every five minutes uses 864 a day.
  • Funding is refreshed every few minutes; scanning more often than once a minute returns the same numbers.
  • Funding fields are fractions: 0.1095 = 10.95 % a year. spreadDaily is the same spread per day.
  • Venues pay funding at different times (hourly on Hyperliquid, every 8 hours on some others): intervalHours tells you which.
  • Use basis=predicted shortly before a funding time; current rates can flip at the next print.

Common pitfalls

  • A spread between published rates is not a return: fees, slippage, price moves between the legs and different funding times all count.
  • Legs whose rate is withheld show the venue but no number; do not treat a missing rate as zero.

Who it is for

  • Traders who look for funding carry between a DEX and other venues
  • Researchers comparing funding regimes across venues
  • Anyone building a funding dashboard

Plan and limits

FreeCheapest
Every endpoint of this recipe is in the free key; three requests per scan.
10/min, 1,000/day, 30 days of history
API Dev$49/mo
Scan every minute and use the results inside your organisation.
60/min, 50,000/day, 365 days of history

Compare plans

Honest limits

  • Hyperliquid and Lighter rates are published in full. Binance and Bybit rates per exchange are withheld during their licence review: spreads between them are listed with the spread but without each rate, and only where at least three venues quote the coin.
  • Lighter appears once its collector has data for a coin; coins without a Lighter market show Hyperliquid alone.

Next steps

More recipes like this

LiveHyperliquid markets 10sOperationalHyperliquid positions & whales 6sOperationalLiquidation heatmap & levels 7sOperationalLighter markets & liquidations 7sOperationalLighter positions & heatmap 7sOperationalLighter RH positions & heatmap 10sOperationalBinance markets 11sOperationalBybit markets 44sOperationalBinance liquidations 31sOperationalBybit liquidations 31sOperationaldYdX positions & heatmap 7sOperationalGMX positions & heatmap 16sOperationalLong/short & open interest history 26sOperationalOrder books & trade flow 9sOperationalOn-chain transfers 6sOperationalSecurity monitor 6sOperationalBlockmetric Sentiment 11mOperationalOfficial news 4mOperationalAlert delivery 5sOperational10:23:21 UTCData: Blockmetric Research · Binance · Bybit