Passive quoting
Passive quoting sets lend and exit rates on every maturity of a market from one curve, instead of posting individual offers. Tenor turns the curve into offers on each maturity and updates the offers every block, without any transaction from you.
- Lend rate (
lend_rate). When a borrower takes your quote, capital is pulled from your default yield source to fund a fixed-rate lend at that rate. - Exit rate (
resell_rate). Before maturity, someone can buy your fixed-rate lend at this rate, for example a borrower repaying early. The proceeds go back to the default yield source.
The default yield source is your variable-rate lend in the market. Capital that isn't matched, or that comes back from a lend, earns the variable rate there and is quoted again. This supports strategies such as auto-rolling lend positions or market making.
This guide uses the tenor, send and track helpers from the Quickstart, and a market from GET /v1/markets. You need an API key with the trade scope. To post a single offer on one maturity instead, see Post an offer.
Curve pricing
A curve has 1 to 8 points, each with a time to maturity, a lend_rate and a resell_rate. Between two points, rates are interpolated linearly. The curve only quotes maturities between its first and last points.
| Point | Time to maturity | Lend rate | Exit rate |
|---|---|---|---|
| 1 | 30 days | 5.0% | 4.6% |
| 2 | 90 days | 5.4% | 5.0% |
| 3 | 180 days | 5.8% | 5.4% |
With these points, a maturity 60 days away is quoted at 5.2% to lend and 4.8% to exit. As a maturity approaches, the quoted rates move along the curve.
Points must go in order of time to maturity, with the first point at least one hour before maturity. Each exit rate must be lower than its lend rate, and lend rates can be at most 30%. Otherwise you get a 400 CURVE_INVALID error.
Renewal cadence
The renewal cadence is a contract that sets which maturities the curve can lend into. Curves set with POST /v1/curves/set/prepare use the Last Friday of Month Cadence, which matches the monthly maturities of Tenor markets.
Lenders can choose a different cadence contract: any contract that implements IRenewalCadence, including one of their own. The cadence is the renewalCadence field of the curve's lend and exit slots in the Migration Ratifier, set onchain with setParams. The API has no cadence parameter, so setting the curve again through the API writes the Last Friday of Month Cadence back.
1. Set the curve
Set a curve with POST /v1/curves/set/prepare. The first time, fund the default yield source with fund.principal. The deposit earns the variable rate until a borrower takes a quote:
const DAY = 24 * 3600;
const payload = await tenor("POST", "/v1/curves/set/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
market_id: market.market_id,
points: [
{ time_to_maturity_s: 30 * DAY, lend_rate: "0.050", resell_rate: "0.046" },
{ time_to_maturity_s: 90 * DAY, lend_rate: "0.054", resell_rate: "0.050" },
{ time_to_maturity_s: 180 * DAY, lend_rate: "0.058", resell_rate: "0.054" },
],
fund: { principal: "1000000000000" }, // 1,000,000 USDC
});
console.log(payload.curve.mode); // "enable" the first time
console.log(payload.curve.maturities); // the curve's quotes on each maturity
await track(await send(payload));
The first time, the transactions also include the approvals and setup the curve needs. The curve starts quoting within a minute.
2. Preview the curve quotes
GET /v1/curves/preview returns the lend and exit quotes of the curve on each maturity, with the amount available:
const preview = await tenor(
"GET",
`/v1/curves/preview?chain_id=${CHAIN_ID}&maker=${wallet}&market_id=${market.market_id}`,
);
console.log(preview.curve.status, preview.curve.readiness); // "active", { authorized, approved, idle_capital }
for (const m of preview.maturities) {
const resell = m.resell ? `${m.resell.status} ${m.resell.available}` : "nothing to resell";
console.log(m.matures_at, m.lend_rate, m.lend.status, m.lend.available, "|", m.resell_rate, resell);
}
lendis the curve's lend quote on that maturity. When the curve skips a maturity,reasongives the cause, for exampleOUTSIDE_WINDOWorUNFUNDED.resellis the exit quote,nulluntil you hold a fixed-rate lend on that maturity.- The curve
statusisactivewhile the curve is quoting.pausedmeans Tenor has paused all curves.
3. Update the curve
To change your rates, set the curve again with new points. If the first and last points keep the same time_to_maturity_s, the call only updates the rates (curve.mode: "reprice"), and fund isn't needed:
const payload = await tenor("POST", "/v1/curves/set/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
market_id: market.market_id,
points: [
{ time_to_maturity_s: 30 * DAY, lend_rate: "0.052", resell_rate: "0.048" },
{ time_to_maturity_s: 90 * DAY, lend_rate: "0.056", resell_rate: "0.052" },
{ time_to_maturity_s: 180 * DAY, lend_rate: "0.060", resell_rate: "0.056" },
],
});
console.log(payload.curve.mode); // "reprice"
await track(await send(payload));
The new rates are live within a minute, for new quotes and for the exit rates of your open lends. If you move the first or last point, the setup is written again.
To know when to change your rates, subscribe to the open offers over the WebSocket at wss://router.tenor.finance/socket (Phoenix Channels), on the orderbook:{maturity_id} topic. The topic updates every block.
4. Track fills and positions
Each fill of a quote appears in GET /v1/accounts/{wallet}/fills with role: "maker". Your capital is held in two places, both listed in GET /v1/accounts/{wallet}/positions:
const fills = await tenor("GET", `/v1/accounts/${wallet}/fills?chain_id=${CHAIN_ID}`);
for (const f of fills.filter((f: any) => f.role === "maker")) {
console.log(f.filled_at, f.maturity_id, f.side, f.rate, f.size.principal);
}
const positions = await tenor("GET", `/v1/accounts/${wallet}/positions?chain_id=${CHAIN_ID}&side=lend`);
for (const p of positions) {
// fixed: matched lends. variable: the default yield source.
console.log(p.rate_type, p.status, p.matures_at, p.balance.current_value);
}
- Fixed-rate lends are your matched quotes. They can exit early at your exit rate, or pay out at maturity. Collect a matured lend with
POST /v1/redeem/prepare. - The variable lend is the default yield source.
readiness.idle_capitalshows the same amount.
To fetch new fills, set from_block a few blocks before the last fill you saw. Fills are sometimes recorded a few blocks late.
5. Add or withdraw capital
Quotes are funded from the default yield source:
- Add capital with
POST /v1/variable/lend/prepare. - Withdraw capital with
POST /v1/variable/withdraw/prepare.
If you withdraw everything, nothing is left to quote, and you get a CURVE_UNFUNDED warning.
6. Clear the curve
POST /v1/curves/clear/prepare stops the curve from quoting:
const payload = await tenor("POST", "/v1/curves/clear/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
market_id: market.market_id,
});
await track(await send(payload));
The setup stays in place, so setting the curve again later is quick. Add disable: true to remove the setup too. Either way, your capital keeps earning the variable rate, and your fixed-rate lends stay in the wallet. Fixed-rate lends can exit early with POST /v1/resell/prepare, or be collected at maturity with POST /v1/redeem/prepare.
Clears have a separate rate limit, so other requests never block a clear.
Rate limits
| Limit | Endpoints | With a key |
|---|---|---|
read | Curves and their previews, positions, fills | 1,200 / 60 s |
compute | Quotes and prepares | 120 / 60 s |
cancel | Curve clears and offer cancels | 600 / 60 s |
Use the WebSocket to know when offers change, instead of polling. See Rate limits.