Spot-Perp Arbitrage API
Spot-perp arbitrage captures the funding rate paid between long and short perpetual futures holders by holding an offsetting spot position on the same venue. The board covers the spot-perp venues (Binance, OKX, Bybit, Bitget, Gate.io, KuCoin, MEXC, BingX, HTX), verifies the spot or margin leg, and ranks executable rows first (then rows whose only gap is unproven book depth) by net APR: the funding APR less each venue's taker fees, the adverse entry basis and, where the venue publishes it (Binance, OKX, Bybit, Gate.io), the borrow cost of a reverse carry.
List spot-perp opportunities
Returns spot-perp arbitrage rows in board order: executable rows first by netApr, then no_spot_depth / no_depth rows, then the other indicative rows, each by netApr where they have one, else gross apr. Each row carries the current funding rate at the market's own interval, its 7-day cumulative rate, gross and net APR, the entry basis at the quoted sides, the fees charged, the days of carry that pay them back, and why a row is not executable (indicativeReason).
The board is composed every 5 minutes by a scheduled job, never per request, and is stale after 10 minutes. Executability is judged again at the time of the request: a row whose spot or perp quote is older than 10 minutes is served indicative with indicativeReason: stale_quote. Only funding rates inside the 10-minute funding freshness window are ranked, and legs the venue reports as not trading (halted, settling, delisted or pre-launch) are excluded. With no board to serve the endpoint answers 503 (service_unavailable, Retry-After: 30).
Without limit or cursor the answer is every row (about 3,000). With either, it is one page in board order and v1 adds pagination; pass pagination.cursor back as cursor for the next page. The MCP tool get_arbitrage_spot_perp asks for a 100-row page unless given limit or cursor.
Query parameters
- Name
exchange- Type
- string
- Description
Filter by exchange. One of
all,Binance,OKX,Bybit,Bitget,Gate.io,KuCoin,MEXC,BingX,HTX. Defaults toall. A venue the instrument master marks not live has no rows;X-Data-Venueslists the venues a filter can offer, leaving out every venue that isnot_liveorstaleinX-Data-Missing.
- Name
direction- Type
- string
- Description
Filter by portfolio direction. One of
all,long,short. Defaults toall.shortmeans buy spot and short the perp (funding rate ≥ 0);longmeans sell spot and long the perp (funding rate < 0), which needs the base coin borrowed on margin.
- Name
minOiUsd- Type
- number
- Description
Minimum open interest in USD on the perp leg (
perpOpenInterestUsd). Defaults to0(no floor). A row whose open interest is unknown fails any floor above0. The board order is kept.
- Name
limit- Type
- integer
- Description
Rows per page, 1 to 1000, in board order. Omitted together with
cursor: every row.
- Name
cursor- Type
- string
- Description
Opaque cursor from the previous page's
pagination.cursor(100 rows a page whenlimitis omitted).
Response headers
Besides the freshness headers, both the v1 and the free route send:
X-Data-Missing: each configured venue that is not a quoted member of the board, comma-separated asVenue:reason.not_live(the instrument master says every contract is halted, so the venue is not asked),stale(no funding row inside the funding SLA),no_ticker(rows are served but unpriced: the venue's ticker fetch failed). Absent when every venue is a quoted member.X-Data-Venues: the venues a venue filter may offer, comma-separated: the configured venues less thosenot_liveorstale(a venue whose funding rows are all past the funding SLA, such as a halted venue, is not offered).
The free route /api/arbitrage/spot-perp also takes view: rows (the default, the bare array of rows) or grid, the terminal's own projection { view, count, columns } with one array per field the board grid renders, in board order (~0.9 MB against ~2.6 MB of rows). v1 has no view.
Request
curl -G https://www.sharpe.ai/api/v1/arbitrage/spot-perp \
-H "Authorization: Bearer sk_live_your_key_here" \
-d exchange=all \
-d direction=all \
-d limit=100
Response
{
"data": [
{
"symbol": "BTC",
"exchange": "Bybit",
"fundingRate": 0.00005125,
"cumRate7d": 0.00105,
"apr": 0.05611875,
"basisRate": -0.00046325,
"netApr": 0.01276586,
"intervalHours": 8,
"direction": "short",
"portfolio": {
"spotSide": "buy",
"perpSide": "short"
},
"nextFundingTime": "2026-09-27T00:00:00.000Z",
"perpSymbol": "BTCUSDT",
"perpOpenInterestUsd": 5210000000,
"fundingIsStale": false,
"executionStatus": "indicative",
"pairStatus": "eligible",
"fundingAsOf": "2026-09-26T23:47:03.000Z",
"pricesAsOf": "2026-09-26T23:50:41.000Z",
"indicativeReason": "no_spot_depth",
"entryBasisRate": -0.00046325,
"roundTripFeeRate": 0.0031,
"breakEvenDays": 23.18,
"requiresBorrow": false,
"borrowCostIncluded": false,
"borrowApr": null,
"apr7d": 0.05475,
"carrySignFlip7d": false,
"perpLotMultiplier": 1,
"spotPriceAsOf": "2026-09-26T23:50:41.000Z",
"perpPriceAsOf": "2026-09-26T23:50:43.000Z"
}
],
"pagination": { "cursor": "MTAw", "has_more": true, "total": 3076 },
"meta": {
"request_id": "req_abc123def456ghij",
"timestamp": "2026-09-26T23:51:16.000Z",
"elapsed_ms": 12
}
}
Formulas
All rates are decimal fractions (0.30 = 30%). A funding rate is per its market's own interval (intervalHours: 1, 2, 4 or 8 hours), and every APR annualizes it on that interval. A tool that assumes 8-hour funding shows a 1-hour market 8× smaller and a 4-hour market 2× smaller than it pays.
apr = |fundingRate| × 8760 / intervalHours(simple, not compounded).entryBasisRate, positive when favourable: buy spot / short perp(perpBid − spotAsk) / spotAsk; sell spot / long perp(spotBid − perpAsk) / perpAsk. Spot is the venue's last trade; perp prices are per asset unit (the contract price ÷perpLotMultiplier).roundTripFeeRate = 2 × spot taker fee + 2 × perp taker feeat the venue's standard (VIP 0) rate.netApr = apr − (roundTripFeeRate + max(−entryBasisRate, 0)) × 365 / 30 − borrowApr. One-time costs are amortized over a 30-day hold. A favourable entry basis is never credited as income.borrowApris the coin's public spot-margin borrow rate on the row's venue (Binance, OKX, Bybit or Gate.io, from the Margin Borrow Rates module, annualized × 8,760) and is subtracted only on a sell-spot row; a sell-spot row without one (the other venues, or a coin the venue does not price) hasrequiresBorrow: true,borrowCostIncluded: false, isindicativewithindicativeReason: borrow_unpriced(ranked after executable rows), and itsnetAproverstates the carry by the borrow interest.breakEvenDays = (roundTripFeeRate + max(−entryBasisRate, 0)) ÷ ((apr − borrowApr) / 365);nullwithout anetApr.apr7d = cumRate7d × 365 / 7, sign flipped for a long-perp row: what the last 7 days of settlements paid the row's direction.carrySignFlip7distruewhen it is negative. The board ranks on the current rate, not onapr7d.
A row is executable when both legs are priced, the instrument master places both legs on one asset, the spot price is within 5% of the perp's index (else mark, else last) price per asset unit, a sell-spot row's borrow rate is known and in netApr, both quotes are at most 10 minutes old at the time of the request, and both legs report top-of-book depth of at least $10,000 on the side the trade hits. The board reads spot prices as last trades with no book behind them, so no row is executable today: a row that passes every other check is no_spot_depth, and those rows rank first. Otherwise it is indicative:
indicativeReason | Meaning |
|---|---|
unpriced | A leg has no price (the venue's ticker fetch failed or has no quote for the contract). netApr is null. |
identity_mismatch | The instrument master places the spot and perp on different assets (a stock perp sharing a crypto ticker). |
not_live, contract_type, margin_type | The pair check failed on the venue's trading status or contract terms. |
price_gap | The legs are 5–20% apart (a netApr is still computed) or more than 20% apart (two instruments under one ticker: netApr is null). |
stale_quote | A leg's quote is older than 10 minutes at the time of the request. |
borrow_unpriced | A sell-spot / long-perp row whose borrow rate the venue does not publish: netApr excludes that interest, so the row ranks after executable rows. |
borrow_limit | A sell-spot / long-perp row whose venue borrow limit for the coin (a no-VIP account; Bybit, OKX and Binance publish coin units, valued at the spot price; Gate.io publishes USDT) is under $10,000, so the short-spot leg cannot be opened at that size. A venue that publishes no limit is not demoted. |
thin_book | The perp's top-of-book depth on the side the trade hits is under $10,000. |
oi_below_notional | The perp reports open interest under $10,000. |
no_spot_depth | Every other check passes, but the spot leg is a last trade with no book behind it, so the fill at $10,000 is unproven. Ranks right after executable rows. |
no_depth | Likewise for a perp leg that reports no top-of-book size. |
Response fields
Each object in the data array is one spot-perp pair on one exchange.
- Name
symbol- Type
- string
- Description
The base asset ticker (e.g.,
BTC,PEPE). A lot contract trades against its asset's spot: Binance1000PEPEUSDTis thePEPErow.
- Name
exchange- Type
- string
- Description
The venue both legs trade on.
- Name
fundingRate- Type
- number
- Description
Current funding rate per
intervalHours, as a decimal (e.g.,0.0001= 0.01%).
- Name
cumRate7d- Type
- number | null
- Description
Cumulative funding over the last 7 days.
nullwhen no accumulated data is available.
- Name
apr- Type
- number
- Description
Gross annualized funding:
|fundingRate| × 8760 / intervalHours.
- Name
basisRate- Type
- number | null
- Description
Diagnostic basis on last prices:
(perpPrice − spotPrice) / spotPrice.nullwhen a price is missing, never0.
- Name
netApr- Type
- number | null
- Description
aprafter the round-trip fees, the adverse entry basis and a known borrow cost (see Formulas). May be negative.nullon anunpricedrow or when the legs are more than 20% apart.
- Name
intervalHours- Type
- number
- Description
Hours between this market's funding settlements (1, 2, 4 or 8).
- Name
direction- Type
- string
- Description
short(buy spot, short perp) when the funding rate is ≥ 0,long(sell spot, long perp) when it is negative.
- Name
portfolio- Type
- object
- Description
The hedge:
spotSide(buyorsell) andperpSide(longorshort).
- Name
nextFundingTime- Type
- string | null
- Description
ISO 8601 time of the next settlement, or
nullif unknown.
- Name
perpSymbol- Type
- string
- Description
The venue's perpetual contract, in the venue's own spelling (e.g.
BTCUSDT,ETH-USDT-SWAP,1000PEPEUSDT).
- Name
perpLotMultiplier- Type
- number | null
- Description
Asset units per contract unit (
1000for1000PEPEUSDT);nullwhen the instrument master does not know the contract.
- Name
perpOpenInterestUsd- Type
- number | null
- Description
One-sided open interest of that perp in USD: the venue ticker's figure, else the contract's latest stored open interest.
nullwhen neither is known, never0.
- Name
executionStatus- Type
- string
- Description
executableorindicative(see Formulas). Filter on this, not onnetAprbeing present.
- Name
indicativeReason- Type
- string | null
- Description
Why the row is
indicative:no_spot_depth,no_depth,thin_book,oi_below_notional,unpriced,identity_mismatch,not_live,contract_type,margin_type,price_gap,stale_quote,borrow_unpricedorborrow_limit.nullwhen executable.
- Name
pairStatus- Type
- string
- Description
Whether the legs are one instrument:
eligible,unpriced(a leg has no price to check), else the first check that failed:identity_mismatch,not_live,contract_type,margin_typeorprice_gap(more than 20% apart).
- Name
entryBasisRate- Type
- number | null
- Description
The entry basis at the quoted sides, positive when favourable. Only a negative value is charged in
netApr.
- Name
roundTripFeeRate- Type
- number | null
- Description
Two spot and two perp taker fills at the venue's standard fee, as a fraction of notional.
- Name
breakEvenDays- Type
- number | null
- Description
Days of carry that pay back the one-time costs.
nullwithout anetApr.
- Name
requiresBorrow- Type
- boolean
- Description
trueon a sell-spot / long-perp row: the base coin must be borrowed on margin.
- Name
borrowCostIncluded- Type
- boolean
- Description
truewhen the borrow interest is subtracted fromnetApr(the venue publishes the rate: Binance, OKX, Bybit, Gate.io);falsewhen the rate is unknown (the row is thenindicativeborrow_unpriced), and on rows that need no borrow.
- Name
borrowApr- Type
- number | null
- Description
The borrow interest subtracted from
netApr, a fraction per year;nullwhen none is.
- Name
apr7d- Type
- number | null
- Description
The last 7 days of settlements for this direction, annualized.
nullwithoutcumRate7d.
- Name
carrySignFlip7d- Type
- boolean | null
- Description
truewhenapr7dis negative: the current direction lost money over the last 7 days.
- Name
fundingIsStale- Type
- boolean
- Description
truewhenfundingAsOfis older than the funding book's 10-minute SLA at the time of the request.
- Name
fundingAsOf- Type
- string | null
- Description
ISO 8601 time the funding rate behind the row was written.
- Name
pricesAsOf- Type
- string | null
- Description
When the older of the spot and perp prices was read;
nullwhen the row has neither.
- Name
spotPriceAsOf- Type
- string | null
- Description
When the spot price was read;
nullwithout one.
- Name
perpPriceAsOf- Type
- string | null
- Description
When the venue's perpetual ticker was read;
nullwithout a perp price.
Reverse carry (needs a borrow)
{
"symbol": "BTC",
"exchange": "HTX",
"fundingRate": -0.0000300257,
"cumRate7d": -0.00063,
"apr": 0.03287814,
"basisRate": -0.00034413,
"netApr": -0.03038852,
"intervalHours": 8,
"direction": "long",
"portfolio": {
"spotSide": "sell",
"perpSide": "long"
},
"nextFundingTime": "2026-09-27T00:00:00.000Z",
"perpSymbol": "BTC-USDT",
"perpOpenInterestUsd": 612000000,
"fundingIsStale": false,
"executionStatus": "indicative",
"pairStatus": "eligible",
"fundingAsOf": "2026-09-26T23:47:03.000Z",
"pricesAsOf": "2026-09-26T23:50:40.000Z",
"indicativeReason": "borrow_unpriced",
"entryBasisRate": 0.00034425,
"roundTripFeeRate": 0.0052,
"breakEvenDays": 57.73,
"requiresBorrow": true,
"borrowCostIncluded": false,
"borrowApr": null,
"apr7d": 0.03285,
"carrySignFlip7d": false,
"perpLotMultiplier": 1,
"spotPriceAsOf": "2026-09-26T23:50:40.000Z",
"perpPriceAsOf": "2026-09-26T23:50:43.000Z"
}