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) untilexpires_at, which must be at least 60 seconds away. If the maturity is earlier thanexpires_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. Withoutidle_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);
}
statusisopenorpartially_filledwhile the offer can be taken, andscheduledbeforestarts_at.filled,cancelledandexpiredare final.invalidmeans the offer can't be taken right now, andreasonsgives the cause, for example a low balance or a missing approval. The offer can be taken again once the cause is fixed.size.remainingis how much of the offer is left.sizeisnullfor an offer that lends directly from your variable lend.takeable_principalis 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.