Quote fixed rates
Wallets, dashboards, aggregators and treasury tools can show the live fixed rates of the open offers on Tenor markets without an API key. In this guide, you build a rate table for one market: for a given amount, the rate at which the open offers would currently fill a lend or a borrow on each maturity, fees included.
All the endpoints here are public. Without a key, requests are limited per IP address. If your server makes a lot of requests, ask for a key to get higher limits.
Endpoints
| Data | Endpoint |
|---|---|
| Markets, with their collateral and maturities | GET /v1/markets |
| The rate for an amount on every maturity of a market | POST /v1/quotes/term-structure |
| The rate for an amount on one maturity | POST /v1/quotes |
Quotes include fees and take the available liquidity into account.
1. List markets
The market list has no rates. The list describes each market: the loan token, the accepted collateral, and the maturities open for new positions.
const API = "https://tenor-api.tenor.finance";
const CHAIN_ID = 8453; // Base
async function tenor<T = any>(method: "GET" | "POST", path: string, body?: object): Promise<T> {
const res = await fetch(`${API}${path}`, {
method,
headers: { "Content-Type": "application/json" },
body: body && JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
return json.data;
}
const markets = await tenor("GET", `/v1/markets?chain_id=${CHAIN_ID}`);
for (const m of markets) {
const collaterals = m.collaterals.map((c: any) => c.token.symbol).join(", ");
console.log(`${m.loan_token.symbol} against ${collaterals}: ${m.maturities.length} maturities`);
}
2. Compare rates across maturities
POST /v1/quotes/term-structure quotes one amount on every maturity of a market in a single call. Call the endpoint once to lend and once to borrow, then match the results by maturity. Amounts are whole-number strings in the token's smallest unit. USDC has 6 decimals, so 10,000 USDC is "10000000000":
const market = markets.find((m: any) => m.loan_token.symbol === "USDC");
const size = { principal: "10000000000" }; // 10,000 USDC
const [lend, borrow] = await Promise.all(
["lend", "borrow"].map((action) =>
tenor("POST", "/v1/quotes/term-structure", { chain_id: CHAIN_ID, market_id: market.market_id, action, size }),
),
);
// If a maturity can't be quoted, it has an `error` instead of a `quote`.
const rate = (m: any) => (m.quote ? `${(Number(m.quote.rate) * 100).toFixed(2)}%` : m.error.code);
console.table(
lend.maturities.map((l: any) => ({
maturity: new Date(l.matures_at * 1000).toISOString().slice(0, 10),
lend: rate(l),
borrow: rate(borrow.maturities.find((b: any) => b.maturity_id === l.maturity_id)),
})),
);
┌─────────┬──────────────┬──────────────────────────┬──────────────┐
│ (index) │ maturity │ lend │ borrow │
├─────────┼──────────────┼──────────────────────────┼──────────────┤
│ 0 │ '2027-01-29' │ '4.61%' │ '5.36%' │
│ 1 │ '2027-02-26' │ '4.70%' │ '5.41%' │
│ 2 │ '2027-03-26' │ 'INSUFFICIENT_LIQUIDITY' │ '5.52%' │
│ 3 │ '2027-04-30' │ 'NO_OFFERS' │ 'NO_OFFERS' │
└─────────┴──────────────┴──────────────────────────┴──────────────┘
- Rates are annual rates (APR) as decimal strings, fees included.
"0.0461"is 4.61%. - Errors only affect their own maturity.
NO_OFFERSmeans there are no offers on that side.INSUFFICIENT_LIQUIDITYmeans there isn't enough for the full amount. Addallow_partial: trueto quote the available amount instead. quote.size.amount_at_maturityis the amount a lend pays back, or a borrow owes, at maturity.- Rate limits. Each maturity counts as one request against the
computelimit. Quote when the user enters an amount, and avoid quoting on a timer.
3. Quote one maturity
Once the user picks a maturity, quote only that maturity with POST /v1/quotes. You get the same quote, plus the offers the quote would fill, in route:
const quote = await tenor("POST", "/v1/quotes", {
chain_id: CHAIN_ID,
maturity_id: market.maturities[0].maturity_id,
action: "borrow",
size,
});
console.log(quote.rate, quote.size.amount_at_maturity, quote.route);
A quote isn't binding. To act on a quote, prepare the transaction with a key, as in the Quickstart.
Caching and updates
- Quotes. Don't cache quotes. Request a new quote when the user changes the amount or returns to the page.
- Updates. Quotes are streamed every block over a WebSocket at
wss://router.tenor.finance/socket(Phoenix Channels), on thequotes:{maturity_id}topic. Use the stream to know when to quote again. - Markets.
GET /v1/marketsrarely changes, and comes with anETag. Send theETagback inIf-None-Match. If nothing has changed, you get a304with no body:
let etag: string | undefined;
async function marketsIfChanged() {
const res = await fetch(`${API}/v1/markets?chain_id=${CHAIN_ID}`, {
// fetch adds `Cache-Control: no-cache` when you set `If-None-Match`,
// which prevents the 304. Setting Cache-Control yourself avoids that.
headers: etag ? { "If-None-Match": etag, "Cache-Control": "max-age=0" } : {},
});
if (res.status === 304) return null; // unchanged since the last call
etag = res.headers.get("ETag") ?? undefined;
return (await res.json()).data;
}
Next steps
- Post an offer to lend or borrow at your own rate.
- Show a wallet's positions with
GET /v1/accounts/{wallet}/positions, also without a key.