Funding arbitrage scanner across venues
A table of the widest funding spreads between venues, now and for the next funding.
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.912ZReal output of the program below, run against our API on 2026-10-28 10:38 UTC.
Build it step by step
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"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"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.
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.mjsDownload: 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
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
/heatmap/{coin}/candles/{coin}Research notebook in PythonA reproducible study: what happened after the hours with the largest liquidations.Free30 min to build/liquidations/summary/candles/{coin}+1