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.
This is an Earn Intelligence endpoint. It requires an API key on a Pro or Enterprise plan. Anonymous, Free, Starter, and Growth requests receive 403 PLAN_UPGRADE_REQUIRED (see Errors). Compare plans at pro.bitcompare.net/pricing or issue a key from your dashboard. The same feed is available to agents as the get_rate_changes MCP tool.
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.
| Plan | Endpoint access | Max lookback (since floor) |
|---|---|---|
| Anonymous (no key) | 403 PLAN_UPGRADE_REQUIRED | — |
| Free | 403 PLAN_UPGRADE_REQUIRED | — |
| Starter | 403 PLAN_UPGRADE_REQUIRED | — |
| Growth | 403 PLAN_UPGRADE_REQUIRED | — |
| Pro | Full | 365 days |
| Enterprise | Full | 5 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, orstaking. Price ticks are not part of this feed.
- Name
rate- Type
- number
- Description
The observed rate as an annual percentage (
4.5means 4.5% APY).
- Name
previousRate- Type
- number | null
- Description
The rate this observation replaced.
nullfor the first observation of a series.
- Name
change- Type
- number | null
- Description
rate - previousRate, in percentage points.nullwhen 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
rawValueto reachrate(e.g.100when 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, orbackfill.
- 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
nullwhen 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.
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-01or2026-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, orstaking.
- 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
# 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 lookback — since 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 absoluteupgradeUrl.
- Name
422 VALIDATION_ERROR- Description
since/untilnot ISO dates,sincemissing,limitoutside 1–500, an unknowncategory, or a malformedcursor.
- 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
- Earn Products — the product and variant terms behind each rate
- Stablecoin Earn Benchmark — the BEI-Stable index computed from these observations
- Coverage — how many observations exist and how far back they are directly observed
- Rates — the current-rate endpoints
- MCP Tool Catalog —
get_rate_changesfor agents