Quote fixed rates
Wallets, dashboards, aggregators and treasury tools can show Tenor's live fixed rates without an API key. In this guide, you build a rate table for one market: for a given amount, the rate a user would get to lend or 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://trading-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.