Skip to main content

Post an offer at your rate

An offer stays open at your rate until counterparties take the full amount, the offer expires, or you cancel the offer.

Your tokens stay in your wallet until the offer is taken (except collateral for a borrow offer). Posting and cancelling are transactions, so each one costs gas.

This guide uses the tenor, send and track helpers from the Quickstart, and a market and maturity from GET /v1/markets. You need an API key with the trade scope. To quote every maturity of a market at once, see Passive quoting.

1. Post a lend offer​

POST /v1/offers/post/prepare posts the offer. The example below offers to lend 100,000 USDC at 5.5% on one maturity, for one week:

const payload = await tenor("POST", "/v1/offers/post/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
side: "lend",
offers: [{ maturity_id: maturity.maturity_id, rate: "0.055", size: { principal: "100000000000" } }],
expires_at: Math.floor(Date.now() / 1000) + 7 * 24 * 3600, // one week
idle_capital: true,
});

const group = payload.offers[0].group; // save this to cancel the offer later
await track(await send(payload));
  • Rate and timing. The offer is live from starts_at (now, by default) until expires_at, which must be at least 60 seconds away. If the maturity is earlier than expires_at, the offer ends at maturity. Rates can be at most 30%.
  • Idle capital. With idle_capital: true, the USDC earns the variable rate until a borrower takes the offer. Without idle_capital, the USDC stays in the wallet.
  • Approvals. The transactions include a token approval and, the first time, a one-time authorization. Both are listed in requires.

A lend offer can be posted before the wallet holds the full amount, with an OFFER_UNFUNDED warning. Borrowers can only take the amount in the wallet, so add funds before the offer is taken.

Post a borrow offer​

A borrow offer borrows at your rate when a lender takes the offer. Borrow offers need collateral, set with collateral:

const cbBTC = market.collaterals.find((c: any) => c.token.symbol === "cbBTC");

const payload = await tenor("POST", "/v1/offers/post/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
side: "borrow",
offers: [{ maturity_id: maturity.maturity_id, rate: "0.045", size: { principal: "50000000000" } }], // 50,000 USDC at 4.5%
expires_at: Math.floor(Date.now() / 1000) + 7 * 24 * 3600,
collateral: { token: cbBTC.token.address, amount: "100000000", deposit_on_fill: true }, // 1 cbBTC
});

With deposit_on_fill: true, the collateral only leaves your wallet when the offer is taken. You can also set max_ltv, the highest LTV the position can reach.

You can't post an offer that would match one of your own offers on the other side. For example, a lend offer at or below one of your borrow rates gets a 422 SELF_CROSS error.

2. Check the offer status​

GET /v1/offers?maker= lists all your offers on a chain, with their current status:

const offers = await tenor("GET", `/v1/offers?chain_id=${CHAIN_ID}&maker=${wallet}`);

for (const o of offers) {
console.log(o.offer_id, o.status, o.rate, o.size?.remaining, o.takeable_principal);
}
  • status is open or partially_filled while the offer can be taken, and scheduled before starts_at. filled, cancelled and expired are final.
  • invalid means the offer can't be taken right now, and reasons gives the cause, for example a low balance or a missing approval. The offer can be taken again once the cause is fixed.
  • size.remaining is how much of the offer is left. size is null for an offer that lends directly from your variable lend.
  • takeable_principal is the amount a taker can fill now, based on your balance, approvals and collateral.

Each fill of your offer appears in GET /v1/accounts/{wallet}/fills with role: "maker", and the new position appears in GET /v1/accounts/{wallet}/positions.

3. Check wallet readiness​

Offers fill later, without any action from you. If the wallet runs short of funds or approvals in the meantime, the offer can't be filled. GET /v1/accounts/{wallet}/readiness lists the funds and approvals the wallet still needs:

const readiness = await tenor("GET", `/v1/accounts/${wallet}/readiness?chain_id=${CHAIN_ID}&wallet_kind=eoa`);

if (!readiness.ready) {
console.log(readiness.shortfalls); // tokens to send the wallet
for (const fix of readiness.missing) {
console.log(fix.kind, fix.reason, fix.transaction); // approvals or authorizations to send
}
}

missing lists approvals to send, and shortfalls lists tokens to add. Readiness checks need an API key with the read scope.

4. Cancel the offer​

POST /v1/offers/cancel/prepare cancels offers. Select the offers with one of offer_ids, groups, maturity_id, market_id or all: true:

const payload = await tenor("POST", "/v1/offers/cancel/prepare", {
chain_id: CHAIN_ID,
wallet,
wallet_kind: "eoa",
groups: [group],
withdraw_idle_capital: true, // also withdraw the USDC earning the variable rate
});
await track(await send(payload));

To change an offer's rate, post a new offer with replace_groups: [group]. The old offer is cancelled in the same transaction. Cancels have a separate rate limit, so other requests never block a cancel. If there's nothing to cancel, you get 422 NOTHING_TO_CANCEL.