The unified DeFi term sheet
Every prime broker hands you a term sheet. DeFi hands you a contract address. We built a structured termSheet for every lending market and vault we index, one shape for every protocol, and it makes the differences between Aave V4, Midnight, Liquity V2, Frankencoin and LlamaLend legible instead of tribal knowledge.
Ask a prime broker for financing and you get a term sheet: rate, tenor, what happens at maturity, what it costs to get out early, what they can do to you and when. One page, and every desk on the street writes it the same way, so you can put two of them side by side and actually compare.
Ask an onchain lending protocol the same question and you get a contract address.
The information exists, all of it, onchain, in the open. What does not exist is a shape. Aave calls it a liquidation threshold, Liquity calls it an MCR, Frankencoin has no threshold at all because it has no oracle. Morpho Midnight's "interest rate" is not a rate stored anywhere; it is implied by the price you paid. LlamaLend's collateral factor is not a market parameter; it is a number the borrower picks. Every integrator who has shipped more than one protocol has ended up writing the same file: a pile of per-protocol special cases, in application code, that nobody outside the team can read.
So we built the missing shape. Every market we index, and every vault, now carries a structured termSheet, in one schema, describing what you are actually agreeing to.
What it is
A term sheet hangs off a marketUid and has two sides plus an envelope.
The envelope carries what is true regardless of which side you take: the oracle (as a single address wherever one exists), governance and its timelock, utilization, whether positions are cross-margin or isolated, and a coverage block that says explicitly which parts we could fill and which we could not. That last one matters more than it sounds: an absent oracle block must never be read as "this market has no oracle" when the truth is "we have not classified it yet". coverage.notApplicable and coverage.pending are different states, and both are said out loud.
The supply side is what a depositor agrees to. The borrow side is what a borrower agrees to. Markets that only have one (a Stability Pool, a savings vault, a collateral-only asset) simply omit the other, and coverage records why.
Each side ends with an info block: a one-line headline you can put in a table row, a paragraph of prose, and a set of machine-readable tags. The long prose is not repeated per market, because hundreds of markets share the same wording: the market carries a profileId like liquity.cdp@v1 or midnight.orderbook@v1 and the prose is served once. There are 31 such profiles today, and the @v1 suffix is load-bearing: bumping it re-arms the "I have read these terms" acknowledgement in any UI keyed on it.
Vaults are the same problem
The second half of this is that a vault is a term sheet with only a supply side.
That sounds like a simplification and it is the opposite. Everything a depositor needs to know about a savings vault is exactly the vocabulary the borrow side made us invent: is the exit instant, cooling down, queued, or does it need a buyer? Is the share price backed by an onchain invariant or attested by an operator? Does this tranche absorb losses before the other one?
So savings vaults, curated lending vaults, NAV-attested funds, senior and junior tranches and undercollateralised credit pools all get the same object with borrow omitted: vault.savings@v1, vault.nav@v1, vault.tranche-junior@v1, vault.credit@v1. A junior tranche is not a footnote in a description; it is principal.protected: false with a first-loss tag that ranks as critical. A NAV vault is nav-attested, meaning the share price is published rather than proven. A queued exit is exit.mode: 'queued' with the actual cooldown in seconds, next to a liquidity figure that says how much can actually leave today.
One consumer, one renderer, and the deposit side of a vault and the deposit side of a lending market stop being two separate integrations.
Example one: Midnight and Aave V4
Take two markets that both let you borrow a dollar stablecoin against BTC, and put their sheets side by side, rendered the way our own UI renders them.
The Aave card is the shape everyone already has in their head, and most of it is the generic model doing its job. Two rows are not, and both are places where V4 quietly stopped being V3.
The rate row: V4 splits the protocol into a Hub holding liquidity and Spokes holding risk configuration, and the interest-rate model lives on the hub. The utilization that sets your rate is drawn / (liquidity + drawn + swept) aggregated across every spoke sharing that asset, not the ratio of the market you happen to be looking at. Computed per-spoke the number is meaningless, and for a borrow-only spoke not even well-formed. So the sheet carries utilization.basis, which says which denominator produced the number, and ships the hub totals alongside it so a rate-impact simulation shifts the right ones.
The liquidation row: V4 has no close factor. Every other pool answers "how much of my debt can be taken at once?" with a constant — V3 halves it, Compound reads closeFactorMantissa, isolated markets allow the lot — and a V4 liquidator instead repays whatever it takes to restore the position to the spoke's targetHealthFactor, with a bonus that scales with how far under water you are rather than a fixed percentage. That target is a real per-spoke number, 1.0137 to 1.3075 across the twelve on Ethereum, so the card states the sizing rule and the target instead of a share, and marks the published penalty as the ceiling it is. The model enum gained a member for it, repay-to-target-hf, which is the point of leaving every enum open: a protocol that liquidates differently says so instead of being flattened into repay-and-seize.
Even e-modes stay structured: modes[], each with its own LTV and threshold, 49 of them behind one asset on the V3 mainnet market.
The Midnight card is the same field names describing a Base-only order book of zero-coupon units: interest lives entirely in the entry price, so a fixed rate exists without being stored anywhere. Two of its rows are more than trivia. Past-due liquidation carries the tag time-liquidation, classified critical and surfaced before a user signs, because no variable-rate intuition prepares anyone for a healthy position being liquidated for lateness. And exit.overRepayReverts: true is a uint128 underflow with no dedicated error, so the repayment must be sized to the exact debt: the kind of fact that lives in a Discord message until someone writes a field for it. The lender exit row is a full sentence on its own: exit.mode: 'market-sale' with priceRisk: 'market-price' means you are selling units rather than withdrawing a deposit, two ongoing fees are charged to the lender, and bad debt is socialised across lenders.
Same schema. Same three lines of consumer code. One market lets you leave whenever you like at the cost of a floating rate; the other locks the rate, locks the date, and will liquidate you for being late on a position that never became unhealthy.
Example two: three protocols that are all "a CDP"
The second example is sharper, because here the protocols are nominally the same category. All three mint or lend a stablecoin against collateral you post, with no depositor on the other side. A category label tells you nothing. The term sheet tells you everything.
Liquity V2: the borrower sets the rate, and the rate is a queue position
rate.kind: 'user-set'. Not a number we read but a number the borrower supplies, with userSet.required: true so a borrow form knows the input is mandatory, plus min, max, and a sensible default. Without that flag a UI has no way to know it must ask.
And the choice is not cosmetic, which is the term that surprises people: your collateral can be redeemed at par while your position is perfectly healthy. Any holder of BOLD may exchange it for collateral at face value, and the protocol serves them from the lowest-rate trove on the branch first. No health factor predicts this. It is not a liquidation, and no liquidationLtv describes it.
So redemption gets its own object rather than a footnote, and every field in it answers a question a borrower actually asks:
trigger: 'permissionless-arbitrage'. Not a governance action, not a liquidation. Anyone, any time.driver: 'below-peg'. It only pays while BOLD trades under target, which is the entire point: redemptions are the mechanism that pushes it back up.order: 'lowest-rate-first'. The rate you picked IS your position in the queue. Cheapest is first.valueImpact: 'usd-neutral'. The redemption fee stays in your position as extra collateral, so you are roughly whole in dollars. What you lose is exposure, involuntarily, at the worst possible moment.defence: 'Raising your rate moves you back in that queue.'The one action available.
Liquidation is absorber: 'stability-pool' with two penalties, because a single number cannot express it: one rate when the Stability Pool absorbs the debt (the normal path) and a materially worse one when the pool is empty and the debt is redistributed across the branch's other troves (badDebt: 'redistributed'). Plus a minDebt floor you cannot leave a position below, and a positionModel: 'cdp-id' where the id is a keccak of owner, branch and index.
Frankencoin: there is no oracle
oracle.kind: 'none', and this is not a coverage gap. Frankencoin has no price feed by design. The liquidation price is declared by the position owner and policed by a Dutch-auction challenge game: anyone may challenge a declared price, and the auction decides whether they were right.
The consequences run straight through the data model:
liquidation.model: 'auction', with the challenge period aswindowSecsand the challenger's reward as the penalty.challenge()has no health gate. The trigger ismarketPrice < declaredPrice, independent of how much is borrowed. Repaying does not reduce your challenge risk; only re-declaring the price does. Our health factor here is a borrow-capacity metric, and the universal rule "HF below 1 means liquidatable" must never be applied to it.- Debt is not what you received. Minting withholds a 10–40 %
reserveContributioninto the equity reserve and charges an upfront pro-rata fee. Both arefees[]entries withwhen: 'entry'and an explicit note that you owe the full amount but receive materially less. The generic sheet's "repay any time at no extra cost" is false on three separate counts here, which is exactly why the adapter exists. - Positions expire.
maturity.kind: 'fixed-date'with a real expiration andatMaturity: 'liquidatable'. A protocol filed under "CDP" turns out to sit on the fixed-term seam. - Each position is its own contract, cloned permissionlessly from the market's "original", so
positionIdMeaningsays so, and a market key is an original position rather than an asset pair.
LlamaLend: the collateral factor is an input, not a parameter
We wrote about LlamaLend's soft liquidation in detail. In term-sheet terms it is the one lender the generic model describes wrongly rather than incompletely:
liquidation.model: 'soft-band',absorber: 'amm', and the field that matters most:reversible: true. Collateral is spread across N price bands inside the market's own AMM and converted gradually as the price enters the range; if the price recovers, the conversion unwinds. "In liquidation" is not terminal, which every other lender's UI implies that it is.penalty: 0. Soft liquidation costs nothing. The hard-liquidation discount still exists, so it appears as a named entry inpenalties[]with an explicit note that it applies only in the terminal case: visible, and not mistaken for the ordinary path.- There is no liquidation price to render, because none exists.
- And the collateral factor is a function of N, which the borrower chooses at open: roughly 0.991 at N=4 against 0.886 at N=50, on the same market. The headline LTV is only true at the default. So the sheet ships the whole
bandLtvcurve plus anopenParameterblock:domain: 4..50, a default, andimmutableAfterOpen: true, because changing N on a live loan means closing and reopening it.
That last field is a small thing that shows what the format is for. openParameter is a generic concept: a choice the borrower makes at open that changes the terms. LlamaLend's band count is the first instance. It will not be the last, and the next one will not need a new consumer.
What the shape buys you
Three things, in order of how much they matter.
Comparison actually works. Rate is not the offer. A 3 % fixed borrow that liquidates you for being one hour late is not comparable to a 5 % floating borrow you can exit in a block, and until the difference is structured, no ranking of the two means anything.
Risk gets classified from data, not from a list. Every tag maps to a severity. critical means you can lose more than the amount at stake, or lose it without doing anything wrong: full-collateral seizure, time-based liquidation, redeemable-while-healthy, first-loss capital, an EOA that can change parameters with no delay. warn means it costs money or blocks you. Nothing else. There is no hand-maintained list of scary markets, so a newly integrated protocol is classified correctly the moment its fields are right. And only critical is ever allowed to gate a signature, because a disclosure that fires on a plain USDC deposit trains exactly the reflex it exists to prevent.
Unknown is a value. The most useful discipline the format imposed on us was refusing to default. A liquidation penalty we could not read used to render as "liquidator takes debt repaid + 0 %", a confident, wrong number that came from nothing. 0 means no bonus; absent means we do not know. The row disappears instead of lying, and every enum is open (| (string & {})), so a protocol whose liquidation model we have never seen renders as something unrecognised rather than silently as the default.
The field next to it has since taught us the second half of that lesson: a default that is usually right is worse than an obviously missing one. closeFactor fell back to 1, full liquidation, which is correct for every isolated market we serve and therefore looked correct everywhere. It was not being read at all — our data origin serves it one level down, under caps, and a lookup that missed produced a plausible number instead of an error. Every Aave and Compound market on that endpoint published "your whole debt can be repaid in one go" against a real close factor of 0.5: 191 of the 630 markets we checked. V4 was carrying V3's 0.5 from the other direction, a deliberate cross-protocol filler that happens to be a true statement about V3 and a claim about V4 its contracts never make. Both are fixed, and the regression test now pins the shape of the origin, not just the shape of the sheet — the bug was never in the schema.
Getting it
Term sheets are attached to every lending market, every pool row, every leverage pair and every comparable we serve. Endpoints take ?terms=digest (the default: headline, tags and the rollup, cheap enough for a table of hundreds of rows) or ?terms=full, which adds exit terms, liquidation parameters, fees, modes, oracle and governance.
Full reference in the 1delta API docs. If you are integrating lending for the first time, how we make three thousand markets comparable is the companion piece; if you want the protocol-by-protocol detail behind the examples above, we have written up fixed-term lending, order-book lending, CDPs and LlamaLend individually.
The protocols are not going to converge. The description of them can.