Skip to main content

Concepts

This page covers the rules common to every endpoint. For the full details of each field and error code, see the API reference.

Quotes and prepares​

There are two kinds of calls for a trade:

  • A quote (POST /v1/quotes) returns the current rate for an amount, fees included. Quotes are public, free to repeat, and not binding.
  • A prepare (POST /v1/lend/prepare, /borrow/prepare and so on) returns the transactions for a wallet to sign. This is called a payload. Prepares need an API key with the trade scope.

Every prepare gets its own quote, so you don't need to quote first. Use quotes to show rates, and prepare when the user is ready to go ahead.

Tenor never signs or sends anything. Your wallet, Safe or custodian does.

Payloads​

Every prepare returns the same fields:

FieldContents
summaryA one-line description of the action, to show before signing
statusready to send, or preview_only for a dry run
stepsEvery transaction, in order. The main one is last
callsThe same transactions as one batch, for wallet_sendCalls and smart accounts
transactionThe main transaction alone
safe_txAll the transactions as one Safe transaction
requiresApprovals or one-time authorizations the wallet still needs. Each one is also in steps
previewThe expected outcome: the amount, the rate with fees, the offers filled, the position before and after, and the wallet's balance changes
validity, boundsHow long the transactions stay valid, and the limit rate and deadline they enforce
warningsNotices that don't block the action, such as OFFER_UNFUNDED or HEALTH_THRESHOLD

Before returning a payload, Tenor simulates the transactions from the wallet on the latest block and runs these checks: approvals must be exact, the rate and deadline must be enforced onchain, and every call must go to Tenor's contracts. If a check fails, you get an error instead.

requires is based on the wallet's state when you prepared. Wait for one payload to confirm before you prepare the next.

If a Safe, a custodian or your own code changes the calls before signing, check them again with POST /v1/simulate.

Wallet kinds​

Every prepare takes a wallet_kind, which sets how the transactions are packaged. Tenor doesn't infer the wallet kind, so always set wallet_kind. A value that doesn't match the wallet returns a WALLET_KIND_MISMATCH warning.

wallet_kindHow to sendTime to sign
eoaEach item in steps, one at a time, waiting for each to confirm5 min
delegated_eoa (EIP-7702)calls in one wallet_sendCalls, or like an eoa5 min
safesafe_tx as one Safe transaction30 min
smart_account (ERC-4337 and others)calls in one user operation30 min

The time to sign is the longest the transactions can wait before they're confirmed. If you pass wallet_kind on a quote, the quote only uses offers that stay available for the full time to sign.

Limit rate and deadline​

Every action that fills offers has two limits, enforced onchain:

  • limit_rate is the worst rate you accept: the lowest rate for a lend or a repay, and the highest for a borrow or a resell. If the fill would be worse, the transaction fails. Without a limit_rate, Tenor uses the worst rate in the quote, moved 0.10 percentage points against you, or 0.30 for a safe or smart_account.
  • deadline_at is the latest time the transaction can be confirmed, in Unix seconds. The deadline must be at least 60 seconds away, and within the time to sign. Without a deadline_at, the deadline is the end of the time to sign.

After validity.until_at, the transactions fail instead of filling late. If you can't sign before until_at minus validity.recommended_margin_s, prepare again.

Some actions don't fill offers: redeems, direct repays, collateral changes and variable-rate actions. They have no quote, and their bounds and validity.until_at are null.

Dry runs and test keys​

Add dry_run: true to any prepare to preview the action without getting anything to send. Tenor runs the same quote, simulation and checks, as if the wallet had the funds. Dry runs work even for an empty wallet. You get status: preview_only, the full preview and requires.

Use dry runs to show users the outcome of an action before they fund their wallet, and to test an integration.

Test keys start with tnr_sk_test_. They work like normal keys, but every prepare is a dry run, with a TEST_KEY_DRY_RUN warning.

Units and conventions​

ConventionRuleExample
AmountsWhole-number strings in the token's smallest unit"1000000" is 1 USDC
RatesAnnual rates (APR) as decimal strings, in fields ending in _rate"0.052" is 5.2%
TimesUnix seconds. Fields ending in _at are dates, fields ending in _s are durationsmatures_at, time_to_maturity_s
Chainschain_id on every request: in the query for a GET, in the body for a POST8453 (Base), 1 (Ethereum), 5042 (Arc). GET /v1/chains lists them
IDsStrings, unique on a chainmaturity_id
AddressesAny letter case accepted. Returned checksummed0x833589fCD6…
PagesOffers, positions and fills take cursor and limit (default 50, max 200). meta.next_cursor is null on the last pagelimit=50

Amounts come in two forms. principal is the amount that changes hands now. amount_at_maturity is the amount paid back at maturity. Quotes and previews return both.

API changes only add fields, values and error codes. Ignore fields you don't know, and handle values you don't expect.

Authentication​

Send your key in the X-Api-Key header. You don't need a key for reads and quotes. You do for prepares, simulations and readiness checks.

  • The Tenor team issues keys for your organization: email contact@tenor.finance.
  • Each key has scopes: read for private reads, and trade for preparing transactions.
  • GET /v1/access checks a key and returns the key's scopes, mode (live or test), expiry date and rate limits.
  • A key sent to a public endpoint identifies you, and an invalid key is rejected there too.
  • Keep keys on your servers. Never put one in a browser or an app.

Errors​

A successful response looks like { "data": …, "meta": … }. An error looks like { "error": … }:

{
"error": {
"code": "INSUFFICIENT_LIQUIDITY",
"category": "liquidity",
"treatment": "refetch_state",
"message": "The book can't fill the full size: `details.available` is what it can fill now, in the unit of the size you sent.",
"details": { "available": "55307", "requested": "1000000000" },
"request_id": "req_01M6HMZQV115R2PPWSBXW96H4X"
}
}

Check code to handle each error. If you get a code you don't know, use treatment:

treatmentNext step
fix_requestChange the request. The same request will fail again
refetch_stateThe market or the wallet has changed. Read the latest state, then quote or prepare again
fund_accountThe wallet doesn't have enough of a token, or enough collateral
retryA temporary problem. Try again after a short wait

Every response has an X-Request-Id header. You can send your own to match your logs with ours. Include the request ID when you contact us about a request.

Rate limits​

Each request counts against one of three separate limits. A burst of quotes never blocks a cancel. With a key, limits are per key. Without one, they're per IP address.

LimitEndpointsWith a keyWithout a key
readReads1,200 / 60 s25 / 1 s, 150 / 10 s, 1,000 / 60 s
computeQuotes, takeable offers, simulations, readiness checks, prepares120 / 60 s30 / 60 s
cancelOffer cancels and curve clears600 / 60 sKey only

Every response includes X-RateLimit-Policy, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. If you go over a limit, you get 429 RATE_LIMITED with a Retry-After header in seconds. Wait that long before trying again.

Caching​

  • Data that changes slowly, such as the market list and open offers, comes with a Cache-Control: max-age and an ETag. Send the ETag back in If-None-Match. If nothing has changed, you get a 304 with no body.
  • Quotes, payloads and wallet data are never cached (no-store). Don't cache them yourself.
  • meta.generated_at is when the data was read, and meta.block_number is the block the data was read at.

Live data​

Open offers and quotes are streamed every block over a WebSocket at wss://router.tenor.finance/socket, using Phoenix Channels. The topics are orderbook:{maturity_id} and quotes:{maturity_id}. Use the stream to know when to quote again. Always prepare through the API, which checks every transaction before you sign.