Comparable rates for one pair
GET/v1/data/lending/comparables
The N best comparable rates for a single pair, priced at the caller’s size and holding period. One round-trip, small payload — built to sit behind a rate pill next to a borrow input.
Unlike /pairs/optimize (rank every leveraged pair), this answers the question a borrow form asks: I am about to borrow X of asset D against collateral C — what are the best rates for that same trade, at my size, over my horizon?
Three rates, deliberately kept apart
aprPct— what the venue advertises: spot at 0 notional, or top of book.aprAtAmountPct— the same rate at your size. A utilization pool re-prices its whole balance to the post-borrow rate (marginal == average); an order book fills cheapest-first, so this is the VWAP of the tiers consumed. Null without an amount.effectiveAprPct— the size-priced rate normalized tohorizonDays. This is whatranksorts on.
Horizon normalization
A 4-week fixed rate and a floating pool are not comparable until both are priced over the same holding period. Each venue supplies a repayment rule: a Lista loan exited early pays roughly half the remaining term’s interest (r·(T+h)/(2h) — 1%/1yr exited at 6 months is 1.5% effective); Midnight/Term charge no penalty but exiting means a resale at the then-current price (horizon.priceRisk); Exactly rebates unassigned earnings; a CDP’s one-time origination fee amortizes over the horizon, so it dominates on short holds. A horizon that outlasts the term assumes a roll at today’s rate and reports locked: false.
Always render horizon.locked and horizon.assumptions next to the number — a normalized rate is not a contractual one.
Obtainability is part of the answer
Term Finance originates only inside periodic sealed-bid rounds; between rounds its rate card still quotes the LAST round’s clearing rate — real, but not takeable. Those rows are dropped by default and, with includeUnobtainable=true, returned flagged (obtainable: false, quoteBasis: "last-clearing") and ranked last. A venue that cannot fund the requested size is marked depth.capped and demoted rather than ranked on a rate it can’t honour.
Comparability filters (what makes this a comparison rather than a list)
Depth band — floor = min(minLiquidityRatio × anchor, liquidityFloorCapUsd), default min(50% × anchor, $10M). A venue an order of magnitude shallower is a different product, and it is exactly those markets that quote absurd rates — a near-empty isolated market with reward emissions prices borrowing at −25%, a real number and a useless comparison against a $5B pool. The cap is not optional: a pure ratio breaks on the biggest pairs, where half of a $5B pool is a $2.5B floor nothing clears, so the comparison would come back empty exactly where alternatives matter most. The anchor is referenceMarketUid's own depth when given, else the deepest candidate. droppedIlliquid + liquidityFloorUsd report it — the filter is never silent; minLiquidityUsd replaces the formula and includeIlliquid=true turns it off.
Stale venues. A lender whose ingest lagged past the freshness window drops out of the underlying pairs view — which reads exactly like "that lender doesn't offer this pair". Those rows are excluded from ranking but counted (droppedStale, staleMaxHours), because a $19B deployment vanishing with no signal is worse than a short list. includeStale=true ranks them anyway.
One collateral basis. When the caller pins no collateral (a standalone borrow form knows none), comparing "borrow USDC" across every collateral in existence is not a comparison. The server picks ONE representative collateral per chain — first hit in collateralPreference (default ETH,BTC,USDC,USDT), else the collateral group backing the deepest markets, which resolves to the wrapped native where neither is listed — and compares only venues backed by it. The choice comes back as collateralBasis (e.g. {"1":"ETH"}) so the UI can say "vs WETH-backed". An explicit collateral/collateralGroups always wins and disables this.
Rewards
The headline rate folds in reward emissions (same composition as /pairs/optimize), which is why a borrow rate can be negative. That is real but transient, so each row also carries rewardAprPct and aprExRewardsPct — the structural cost without emissions. Ranking stays on the headline; showing both is the caller's job.
Ranking
obtainable first, then non-capped, then best effectiveAprPct (cheapest for a borrow, highest for a supply), then deepest liquidity as tie-break. Results are deduped to one row per (chain, lender, market, term) — the pairs view is combinatorial in the other leg, so without it the "5 best rates" would be five copies of the same market.
Plain-text reference — GET /v1/data/lending/comparables
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
chainId | query | string | no | Single chain ID. When set, asset filters operate on token addresses. Pass one for an actionable list — cross-chain rows are a comparison, not one executable position. See the ChainId schema for the full set of supported chains. |
chainIds | query | string | no | CSV of chain IDs. When two or more, asset filters operate on asset groups. See the ChainId schema for the full set of supported chains. |
side | query | borrow, supply | no | Which leg to compare. |
debt | query | string | no | The borrowed asset — address (single chain) or asset group. Required for side=borrow. Aliases: debts, debtGroups. |
debtGroups | query | string | no | CSV of debt-side asset groups (works in any chain mode). |
collateral | query | string | no | The collateral asset. Required for side=supply; strongly recommended for side=borrow — a market that will not take your collateral is not a comparable. Aliases: collaterals, collateralGroups. |
collateralGroups | query | string | no | CSV of collateral-side asset groups (works in any chain mode). |
amount | query | number | no | Notional in TOKEN UNITS of the priced leg (the debt asset for a borrow). Requires exactly one asset on that side. |
amountUsd | query | number | no | Same notional in USD. Works with multi-asset selections. Mutually exclusive with amount. |
horizonDays | query | number | no | Holding period every venue is repriced to. At 365 a floating pool’s effective rate equals its sticker rate. |
rateType | query | all, fixed, float | no | Post-filter applied before dedupe and ranking. |
limit | query | integer | no | How many comparables to return (max 25). |
referenceMarketUid | query | string | no | The venue the caller is already on. Returned as reference and excluded from items, so the pill can render "you: X% · best: Y%". |
referenceTermId | query | string | no | Disambiguates which term of the reference market is the caller’s own. |
includeUnobtainable | query | boolean | no | Keep quotes that cannot be taken right now (closed Term auction rounds), flagged and ranked last. |
minLiquidityRatio | query | number | no | Ratio side of the depth floor min(ratio × anchor, cap). |
liquidityFloorCapUsd | query | number | no | Cap side of the depth floor — stops a huge anchor from demanding a comparably huge venue and returning nothing. |
minLiquidityUsd | query | number | no | Absolute USD depth floor that REPLACES the computed min(ratio × anchor, cap). |
includeStale | query | boolean | no | Rank venues whose market data is older than the freshness window. Excluded but counted (droppedStale) by default. |
includeIlliquid | query | boolean | no | Disable the depth band entirely and rank every venue regardless of size. |
collateralPreference | query | string | no | CSV of collateral asset groups tried in order when picking the comparison basis (only used when no collateral is pinned). |
lenders | query | string | no | CSV of lender keys (prefix-expanded) to restrict the comparison to. See the LenderId schema for the full set of accepted values. |
excludeLenders | query | string | no | CSV of lender keys to exclude (prefix-expanded). |
maxConfigRiskScore | query | number | no | Config-risk ceiling (default 4, as on the pair endpoints). |
maxTokenRiskScore | query | number | no | Token-risk ceiling across both legs. |
Response 200
| Field | Type | Description |
|---|---|---|
success | True | |
data | object | |
data.side | borrow, supply | |
data.horizonDays | number | |
data.amount | number | |
data.amountUsd | number | |
data.chainIds | string[] | EVM chain ids, as decimal strings. See the ChainId schema. |
data.scanned | integer | Pair rows considered before ranking. |
data.truncated | boolean | The candidate guard cap bound, so ranking saw only the deepest rows — narrow the filter. Never a silent trim. |
data.available | integer | Distinct comparables that existed before limit was applied. |
data.liquidityFloorUsd | number | Venues below this USD depth were dropped as not comparable (0 = no floor applied). |
data.droppedIlliquid | integer | How many venues the depth band removed. An opinionated filter must never be silent — surface this rather than implying nothing else exists. |
data.droppedStale | integer | Venues excluded because their market data is older than staleMaxHours — an ingest lag, not an absent lender. Non-zero is the difference between "no one else offers this pair" and "we cannot currently see who does". includeStale=true ranks them anyway. |
data.staleMaxHours | number | The freshness window droppedStale was measured against. |
data.collateralBasis | object | The single collateral group each chain's rows were compared against when the caller pinned none, e.g. {"1":"ETH"}. Empty when a collateral was supplied, or on the supply side. |
data.reference | object | The venue the caller is already on (referenceMarketUid), pulled out of items so the UI can render "you: X% · best: Y%". |
data.count | integer | Number of entries in items. |
data.items | object[] | The result set for this response. |
data.items[].rank | integer | 1-based position in the ranking. 0 on the reference item. |
data.items[].chainId | string | EVM chain id, as a decimal string. See the ChainId schema. |
data.items[].lender | string | Raw lender key — stable, use it for keying and deeplinks. Per-market lenders (Morpho Blue, Silo, Euler) encode a hashed market id here, so it is NOT presentable. |
data.items[].lenderName | string | Display name from the lender registry, falling back to the raw key when none is registered. This is what a UI should render. |
data.items[].lenderLogoUri | string | |
data.items[].marketUid | string | The market the rate belongs to: the debt market for a borrow quote, the collateral market for a supply quote. |
data.items[].marketName | string | |
data.items[].curatorName | string | |
data.items[].eMode | string | |
data.items[].rateType | fixed, float | |
data.items[].rateModel | variable, lista, midnight, term, exactly, teller, … (9 values) | Which repayment rule priced this quote. |
data.items[].aprPct | number | Sticker rate, APR %. Includes intrinsic yield and reward APR (same composition as borrowAprShort / depositAprLong); a one-time origination fee is NOT in it — the horizon model amortizes that separately. |
data.items[].aprAtAmountPct | number | The rate at the requested size. Null when no amount was supplied. A utilization pool re-prices its whole balance to the post-action rate; an order book fills cheapest-first, so this is the VWAP of the tiers consumed. |
data.items[].rewardAprPct | number | Reward emissions folded into the headline (positive = subsidising a borrow). Large emissions can push a borrow rate NEGATIVE. |
data.items[].aprExRewardsPct | number | The rate WITHOUT reward emissions — the structural, sustainable cost. Show it next to a reward-inflated headline; emissions are transient. |
data.items[].effectiveAprPct | number | Size-priced rate normalized to horizonDays. The ranking number. |
data.items[].costPct | number | Total cost over the horizon as a percent of principal. |
data.items[].horizon | object | How the effective rate was arrived at — the caveats a UI must show next to a normalized number. |
data.items[].horizon.basis | flat-forward, early-exit, held-to-maturity, rolled | flat-forward: no maturity, today’s floating rate assumed to hold. early-exit: horizon shorter than the term, the venue’s exit rule applied. held-to-maturity: horizon matches the term (the clean case). rolled: horizon outlasts the term, a roll at today’s rate assumed. |
data.items[].horizon.locked | boolean | Is the rate contractually fixed for the WHOLE horizon? False for floating pools and for a fixed term that has to be rolled to cover the horizon. |
data.items[].horizon.priceRisk | boolean | Exiting early means unwinding on an order book at the then-current price (Midnight/Term), so the realized cost can land either side of the quote. Not priced in — flagged. |
data.items[].horizon.assumptions | string[] | Display-ready caveats, most important first. |
Example response
{
"success": true,
"data": {
"side": "borrow",
"horizonDays": 1.0,
"amount": 1.0,
"amountUsd": 1.0,
"chainIds": [
"1"
],
"scanned": 1,
"truncated": true,
"available": 1,
"liquidityFloorUsd": 1.0,
"droppedIlliquid": 1,
"droppedStale": 1,
"staleMaxHours": 1.0,
"collateralBasis": {},
"reference": {
"rank": 1,
"chainId": "1",
"lender": "AAVE_V3",
"lenderName": "AAVE_V3",
"lenderLogoUri": "AAVE_V3",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"marketName": "string",
"curatorName": "string",
"eMode": "string",
"rateType": "fixed",
"rateModel": "variable",
"aprPct": 1.0,
"aprAtAmountPct": 1.0,
"rewardAprPct": 1.0,
"aprExRewardsPct": 1.0,
"effectiveAprPct": 1.0,
"costPct": 1.0,
"horizon": {
"basis": "flat-forward",
"locked": true,
"priceRisk": true,
"assumptions": [
"string"
]
},
"termId": "string",
"durationDays": 1.0,
"maturity": 1.0,
"termDays": 1.0,
"obtainable": true,
"obtainableReason": "string",
"quoteBasis": "live",
"depth": {
"fillable": 1.0,
"capped": true,
"liquidityUsd": 1.0,
"utilization": 1.0
},
"collateral": {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"assetGroup": "string",
"symbol": "string",
"decimals": 1.0,
"logoUri": "string",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
"debt": {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"assetGroup": "string",
"symbol": "string",
"decimals": 1.0,
"logoUri": "string",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
"maxLeverage": 1.0,
"ltv": 1.0,
"risk": {
"configScore": 1.0,
"maxTokenScore": 1.0
}
},
"count": 1,
"items": [
{
"rank": 1,
"chainId": "1",
"lender": "AAVE_V3",
"lenderName": "AAVE_V3",
"lenderLogoUri": "AAVE_V3",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"marketName": "string",
"curatorName": "string",
"eMode": "string",
"rateType": "fixed",
"rateModel": "variable",
"aprPct": 1.0,
"aprAtAmountPct": 1.0,
"rewardAprPct": 1.0,
"aprExRewardsPct": 1.0,
"effectiveAprPct": 1.0,
"costPct": 1.0,
"horizon": {
"basis": "flat-forward",
"locked": true,
"priceRisk": true,
"assumptions": [
"string"
]
},
"termId": "string",
"durationDays": 1.0,
"maturity": 1.0,
"termDays": 1.0,
"obtainable": true,
"obtainableReason": "string",
"quoteBasis": "live",
"depth": {
"fillable": 1.0,
"capped": true,
"liquidityUsd": 1.0,
"utilization": 1.0
},
"collateral": {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"assetGroup": "string",
"symbol": "string",
"decimals": 1.0,
"logoUri": "string",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
"debt": {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"assetGroup": "string",
"symbol": "string",
"decimals": 1.0,
"logoUri": "string",
"marketUid": "AAVE_V3:1:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
"maxLeverage": 1.0,
"ltv": 1.0,
"risk": {
"configScore": 1.0,
"maxTokenScore": 1.0
}
}
]
}
}
Request
Responses
- 200
- 400
- 429
- 500
- 502
Comparable rates
Validation error
Rate limited. Unauthenticated callers share a per-IP budget; send an x-api-key header to lift it. Retry with exponential backoff.
Unexpected server error. Safe to retry with backoff.
An upstream data source or protocol origin failed (error.code is ORIGIN_FAILED). error.details carries the per-origin status. This is also what a missing or malformed required parameter currently returns, rather than a 400.