Skip to content

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.


GET/v1/arbitrage/spot-perp

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 to all. A venue the instrument master marks not live has no rows; X-Data-Venues lists the venues a filter can offer, leaving out every venue that is not_live or stale in X-Data-Missing.

  • Name
    direction
    Type
    string
    Description

    Filter by portfolio direction. One of all, long, short. Defaults to all. short means buy spot and short the perp (funding rate ≥ 0); long means 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 to 0 (no floor). A row whose open interest is unknown fails any floor above 0. 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 when limit is 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 as Venue: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 those not_live or stale (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

GET
/v1/arbitrage/spot-perp
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 fee at 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. borrowApr is 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) has requiresBorrow: true, borrowCostIncluded: false, is indicative with indicativeReason: borrow_unpriced (ranked after executable rows), and its netApr overstates the carry by the borrow interest.
  • breakEvenDays = (roundTripFeeRate + max(−entryBasisRate, 0)) ÷ ((apr − borrowApr) / 365); null without a netApr.
  • apr7d = cumRate7d × 365 / 7, sign flipped for a long-perp row: what the last 7 days of settlements paid the row's direction. carrySignFlip7d is true when it is negative. The board ranks on the current rate, not on apr7d.

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:

indicativeReasonMeaning
unpricedA leg has no price (the venue's ticker fetch failed or has no quote for the contract). netApr is null.
identity_mismatchThe instrument master places the spot and perp on different assets (a stock perp sharing a crypto ticker).
not_live, contract_type, margin_typeThe pair check failed on the venue's trading status or contract terms.
price_gapThe legs are 5–20% apart (a netApr is still computed) or more than 20% apart (two instruments under one ticker: netApr is null).
stale_quoteA leg's quote is older than 10 minutes at the time of the request.
borrow_unpricedA 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_limitA 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_bookThe perp's top-of-book depth on the side the trade hits is under $10,000.
oi_below_notionalThe perp reports open interest under $10,000.
no_spot_depthEvery 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_depthLikewise 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: Binance 1000PEPEUSDT is the PEPE row.

  • 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. null when 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. null when a price is missing, never 0.

  • Name
    netApr
    Type
    number | null
    Description

    apr after the round-trip fees, the adverse entry basis and a known borrow cost (see Formulas). May be negative. null on an unpriced row 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 (buy or sell) and perpSide (long or short).

  • Name
    nextFundingTime
    Type
    string | null
    Description

    ISO 8601 time of the next settlement, or null if 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 (1000 for 1000PEPEUSDT); null when 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. null when neither is known, never 0.

  • Name
    executionStatus
    Type
    string
    Description

    executable or indicative (see Formulas). Filter on this, not on netApr being 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_unpriced or borrow_limit. null when 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_type or price_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. null without a netApr.

  • Name
    requiresBorrow
    Type
    boolean
    Description

    true on a sell-spot / long-perp row: the base coin must be borrowed on margin.

  • Name
    borrowCostIncluded
    Type
    boolean
    Description

    true when the borrow interest is subtracted from netApr (the venue publishes the rate: Binance, OKX, Bybit, Gate.io); false when the rate is unknown (the row is then indicative borrow_unpriced), and on rows that need no borrow.

  • Name
    borrowApr
    Type
    number | null
    Description

    The borrow interest subtracted from netApr, a fraction per year; null when none is.

  • Name
    apr7d
    Type
    number | null
    Description

    The last 7 days of settlements for this direction, annualized. null without cumRate7d.

  • Name
    carrySignFlip7d
    Type
    boolean | null
    Description

    true when apr7d is negative: the current direction lost money over the last 7 days.

  • Name
    fundingIsStale
    Type
    boolean
    Description

    true when fundingAsOf is 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; null when the row has neither.

  • Name
    spotPriceAsOf
    Type
    string | null
    Description

    When the spot price was read; null without one.

  • Name
    perpPriceAsOf
    Type
    string | null
    Description

    When the venue's perpetual ticker was read; null without 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"
}

Was this page helpful?