Rate Changes

The rate-change feed is the observation log behind every Bitcompare rate, served as a time-ascending stream. Each row is one observed yield move — what the rate was, what it became, how it was collected, and how much we trust it — and rows are never rewritten once written. Use it to answer "what changed since X?", to build alerting, or to keep an audit trail that does not depend on polling /rates.

Access and plan gating

Authenticate with your key as a Bearer token (see Authentication). The plan on the key decides two things: whether the endpoint answers at all, and how far back since may reach.

PlanEndpoint accessMax lookback (since floor)
Anonymous (no key)403 PLAN_UPGRADE_REQUIRED
Free403 PLAN_UPGRADE_REQUIRED
Starter403 PLAN_UPGRADE_REQUIRED
Growth403 PLAN_UPGRADE_REQUIRED
ProFull365 days
EnterpriseFull5 years (1,825 days)

A since earlier than your plan's floor is clamped up to the floor silently — the response is still 200, it just starts later than you asked. The Coverage endpoint tells you how far back the observed (as opposed to reconstructed) log actually goes.


The observation model

Every row in the feed is one observation of one (provider, symbol, category) rate at one instant. The feed is ordered by time, then provider, symbol, category, and that full tuple is what the cursor encodes, so rows with identical timestamps can never be skipped or duplicated across pages.

Properties

  • Name
    time
    Type
    string
    Description

    ISO 8601 instant the rate was observed.

  • Name
    provider
    Type
    string
    Description

    Provider slug, e.g. "aave-v3", "nexo", "kraken".

  • Name
    symbol
    Type
    string
    Description

    Asset symbol, upper-case, e.g. "USDC".

  • Name
    category
    Type
    string
    Description

    lending, borrowing, or staking. Price ticks are not part of this feed.

  • Name
    rate
    Type
    number
    Description

    The observed rate as an annual percentage (4.5 means 4.5% APY).

  • Name
    previousRate
    Type
    number | null
    Description

    The rate this observation replaced. null for the first observation of a series.

  • Name
    change
    Type
    number | null
    Description

    rate - previousRate, in percentage points. null when there is no previous rate.

  • Name
    coinId
    Type
    string | null
    Description

    Canonical coin identifier the symbol resolved to, when known.

  • Name
    quoteCurrency
    Type
    string | null
    Description

    Currency the rate is denominated in, when the provider quotes one.

  • Name
    rawValue
    Type
    number | null
    Description

    The value exactly as the source reported it, before normalisation.

  • Name
    multiplier
    Type
    number | null
    Description

    Factor applied to rawValue to reach rate (e.g. 100 when the source reports a fraction).

  • Name
    unitBasis
    Type
    string | null
    Description

    What the source's unit was (APY, APR, daily, etc.) before normalisation to annual percentage.

  • Name
    collectionMethod
    Type
    string
    Description

    How the observation was gathered: pull_api, push, onchain, websocket, multi_source, manual, provider_confirmed, or backfill.

  • Name
    adapterVersion
    Type
    string | null
    Description

    Version of the provider adapter that produced this row.

  • Name
    sourceRef
    Type
    string | null
    Description

    A pointer back to the source: an API URL, contract address, or reviewer reference.

  • Name
    confidence
    Type
    integer | null
    Description

    0–100 confidence score for this observation, or null when no score was assigned.

  • Name
    baseRate
    Type
    number | null
    Description

    The non-promotional component of rate, when the provider splits base and bonus.

  • Name
    bonusRate
    Type
    number | null
    Description

    The promotional or incentive component of rate, when split.


GET/api/v1/rates/changes

List rate changes

Return observations in [since, until], oldest first. since is required; everything else is optional.

Required attributes

  • Name
    since
    Type
    string
    Description

    ISO 8601 date or datetime lower bound, e.g. 2026-09-01 or 2026-09-01T00:00:00Z. Clamped to your plan's lookback floor.

Optional attributes

  • Name
    until
    Type
    string
    Description

    ISO 8601 upper bound. Defaults to now.

  • Name
    provider
    Type
    string
    Description

    Restrict to one provider slug. Lifts the 31-day window cap (see below).

  • Name
    symbol
    Type
    string
    Description

    Restrict to one asset symbol. Also lifts the 31-day window cap.

  • Name
    category
    Type
    string
    Description

    lending, borrowing, or staking.

  • Name
    cursor
    Type
    string
    Description

    Opaque cursor from a previous response's pagination.nextCursor. Pass it back verbatim.

  • Name
    limit
    Type
    integer
    Description

    Page size, 1–500. Defaults to 100.

Response

The body is { data, pagination }. data is an array of observations; pagination carries nextCursor (null on the last page), hasMore, and the effective limit. Responses are Cache-Control: private, max-age=30.

Request

GET
/api/v1/rates/changes
# BITCOMPARE_API_KEY is your ck_live_… key from https://pro.bitcompare.net/dashboard/keys
curl -G https://api.bitcompare.net/api/v1/rates/changes \
  -H "Authorization: Bearer $BITCOMPARE_API_KEY" \
  -d since=2026-09-01 \
  -d symbol=USDC \
  -d category=lending \
  -d limit=2

Response

{
  "data": [
    {
      "time": "2026-09-01T00:15:12.418Z",
      "provider": "aave-v3",
      "symbol": "USDC",
      "category": "lending",
      "rate": 4.81,
      "previousRate": 4.76,
      "change": 0.05,
      "coinId": "usd-coin",
      "quoteCurrency": null,
      "rawValue": 0.0481,
      "multiplier": 100,
      "unitBasis": "apy",
      "collectionMethod": "onchain",
      "adapterVersion": "aave-v3@2.3.0",
      "sourceRef": "0xA97684ead0e402dC232d5A977953DF7ECBaB3CDb",
      "confidence": 95,
      "baseRate": 4.81,
      "bonusRate": null
    },
    {
      "time": "2026-09-01T00:15:13.002Z",
      "provider": "nexo",
      "symbol": "USDC",
      "category": "lending",
      "rate": 12,
      "previousRate": 11.5,
      "change": 0.5,
      "coinId": "usd-coin",
      "quoteCurrency": null,
      "rawValue": 12,
      "multiplier": 1,
      "unitBasis": "apy",
      "collectionMethod": "pull_api",
      "adapterVersion": "nexo@1.8.2",
      "sourceRef": "https://nexo.com/api/v2/earn/rates",
      "confidence": 85,
      "baseRate": 8,
      "bonusRate": 4
    }
  ],
  "pagination": {
    "nextCursor": "eyJ0aW1lIjoiMjAyNi0wOS0wMVQwMDoxNToxMy4wMDJaIiwicHJvdmlkZXIiOiJuZXhvIiwic3ltYm9sIjoiVVNEQyIsImNhdGVnb3J5IjoibGVuZGluZyJ9",
    "hasMore": true,
    "limit": 2
  }
}

Paging with the cursor

Keep your original since, until, and filters fixed and pass pagination.nextCursor back as cursor until hasMore is false. The cursor is an opaque keyset token — it encodes the last row's (time, provider, symbol, category), so continuation is exact even when many rows share a timestamp. Do not construct or edit cursors; a malformed one returns 422 VALIDATION_ERROR.

Because the feed is append-only and time-ascending, a long-running consumer can store the last cursor it processed and resume from it later. To catch up after downtime, either resume from the stored cursor or start a fresh walk from the time of your last processed row.

Walk a full range

async function* rateChanges(params, key) {
  let cursor
  do {
    const qs = new URLSearchParams({ ...params, limit: '500' })
    if (cursor) qs.set('cursor', cursor)
    const res = await fetch(
      `https://api.bitcompare.net/api/v1/rates/changes?${qs}`,
      { headers: { Authorization: `Bearer ${key}` } }
    )
    const { data, pagination } = await res.json()
    yield* data
    cursor = pagination.hasMore ? pagination.nextCursor : undefined
  } while (cursor)
}

for await (const row of rateChanges(
  { since: '2026-08-01', provider: 'aave-v3' },
  'ck_live_your_key_here'
)) {
  console.log(row.time, row.symbol, row.previousRate, '→', row.rate)
}

Window and history limits

Two limits apply, for different reasons.

Plan lookbacksince cannot reach further back than your plan allows (365 days on Pro, 5 years on Enterprise). Earlier values are clamped up silently.

31-day unfiltered window — a request with no provider and no symbol scans the whole log, so until - since is capped at 31 days per request. Longer unfiltered ranges return 422 WINDOW_TOO_LARGE. To cover a longer period, either walk it in 31-day windows, or add a provider or symbol filter, which uses a targeted index and may span any range within your plan's lookback.

422 WINDOW_TOO_LARGE

{
  "error": {
    "message": "Unfiltered windows are capped at 31 days — page with the cursor, or filter by provider/symbol for deep ranges",
    "code": "WINDOW_TOO_LARGE"
  }
}

Errors

  • Name
    403 PLAN_UPGRADE_REQUIRED
    Description

    No key, or a key on a plan without Earn Intelligence (Free, Starter, Growth). The body carries feature: "intelligence" and an absolute upgradeUrl.

  • Name
    422 VALIDATION_ERROR
    Description

    since/until not ISO dates, since missing, limit outside 1–500, an unknown category, or a malformed cursor.

  • Name
    422 WINDOW_TOO_LARGE
    Description

    Unfiltered range longer than 31 days — see above.

  • Name
    429 RATE_LIMIT_EXCEEDED
    Description

    Plan per-minute budget exhausted. See Rate Limiting.

The full error envelope is described on the Errors page.

403 PLAN_UPGRADE_REQUIRED

{
  "error": {
    "message": "Earn Intelligence endpoints require a Pro or Enterprise plan.",
    "code": "PLAN_UPGRADE_REQUIRED",
    "feature": "intelligence",
    "upgradeUrl": "https://pro.bitcompare.net/pricing"
  }
}

See also

Was this page helpful?