Skip to main content

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.

PointTime to maturityLend rateExit rate
130 days5.0%4.6%
290 days5.4%5.0%
3180 days5.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);
}
  • lend is the curve's lend quote on that maturity. When the curve skips a maturity, reason gives the cause, for example OUTSIDE_WINDOW or UNFUNDED.
  • resell is the exit quote, null until you hold a fixed-rate lend on that maturity.
  • The curve status is active while the curve is quoting. paused means 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_capital shows 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​

LimitEndpointsWith a key
readCurves and their previews, positions, fills1,200 / 60 s
computeQuotes and prepares120 / 60 s
cancelCurve clears and offer cancels600 / 60 s

Use the WebSocket to know when offers change, instead of polling. See Rate limits.