Skip to main content

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​

DataEndpoint
Markets, with their collateral and maturitiesGET /v1/markets
The rate for an amount on every maturity of a marketPOST /v1/quotes/term-structure
The rate for an amount on one maturityPOST /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_OFFERS means there are no offers on that side. INSUFFICIENT_LIQUIDITY means there isn't enough for the full amount. Add allow_partial: true to quote the available amount instead.
  • quote.size.amount_at_maturity is the amount a lend pays back, or a borrow owes, at maturity.
  • Rate limits. Each maturity counts as one request against the compute limit. 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 the quotes:{maturity_id} topic. Use the stream to know when to quote again.
  • Markets. GET /v1/markets rarely changes, and comes with an ETag. Send the ETag back in If-None-Match. If nothing has changed, you get a 304 with 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.