How we unify fixed-term lending
Most on-chain lending is open-ended and floating-rate. Fixed-term lending isn't - and it comes in two structurally different shapes: fixed time-to-maturity (Lista) and fixed maturity (Midnight). Here's how each works, and how we map both onto one comparable model.
Our earlier piece on lending aggregation made the general case: on-chain lending is fragmented, and the 1delta API collapses thousands of disjoint markets into one queryable surface. Almost every market in that surface shares one assumption - lending is open-ended and floating-rate. You supply, you earn a rate that moves with utilization every block, and you withdraw whenever you like.
Fixed-term lending breaks that assumption. The rate is agreed up front and locked, the loan has an end date, and you can't just "drop into the pool" - there's a counterparty and a term. It's the on-chain version of a bond or a term deposit, and it's a genuinely different primitive.
The awkward part for an aggregator: fixed-term lending isn't one shape either. The two implementations we index - Lista's fixed-term broker and Morpho Midnight - answer "when does this loan mature?" in two completely different ways. This article is about those two shapes, why the difference matters, and how we make both come out as the same comparable object.
Two flavors of "fixed"
Both protocols give you a fixed rate for a fixed term. But "fixed term" means two different things.
-
Fixed time-to-maturity (Lista). The protocol offers a menu of durations - 30 days, 60 days, 90 days. When you borrow, your clock starts now: a 30-day loan opened today matures 30 days from today. Two people who pick the same product mature on different calendar dates because they opened at different times. The duration is fixed; the maturity date is relative to you.
-
Fixed maturity (Midnight). The market has one hard-coded calendar maturity - say, 18 September 2026 - shared by everyone in it. Borrow with 40 days left and your term is 40 days; borrow the next week and your term is 33. The maturity date is fixed; your duration is whatever time is left.
That single difference - is the duration fixed, or is the date fixed? - drives everything else: how the rate is quoted, where the liquidity lives, and how you read a position back. Our job is to make both surface as the same fixed-term object, so a consumer asks "what terms can I borrow, and at what rate?" without caring which mechanism answered.
Variant one - Lista: a broker over a Morpho-fork market
Lista's fixed-term product is a LendingBroker sitting in front of a Moolah market (a Morpho Blue fork). The split is the whole idea:
- Collateral stays in Moolah, under your own address - shared and flexible across every loan you hold in that market.
- Borrow and repay route through the broker, which overlays one dynamic (variable-rate) position plus up to N fixed-term positions per user, all drawing against that shared collateral.
The broker publishes a menu of term products via getFixedTerms() - a list of (termId, durationSecs, apr) rows. Borrowing against a term mints a loan tranche stamped with start = now and end = now + duration. That's what makes it fixed time-to-maturity: the menu fixes the length; your maturity is your open time plus that length.
A few mechanics that fall out of the broker model, all of which we surface:
- Interest accrues. The APR is stored on-chain as
(1 + r) · 1e27; we convert it to a plain percent and reproduce the broker's per-second accrual so a position's outstanding interest is exact between repayments. - Early exit has a penalty. Repaying a fixed loan before maturity costs roughly half the interest that principal would still accrue over the remaining term. We compute that penalty (it drops to
0once matured) so a UI can show the true cost of closing early. - Fixed and variable coexist. The same broker also exposes a dynamic, variable-rate loan against the same collateral - so a user can mix a locked-rate tranche with a floating one.
On a brokered market the borrow side is fixed-term only: there's no composable variable borrow, so we mark variableBorrowDisabled and carry the fixed rate on stableBorrowRate. Supply yield (depositRate) stays variable - lenders still earn the floating Moolah supply rate.
Variant two - Midnight: a zero-coupon order book
Morpho Midnight is not a pool and not a Blue fork. It's a fixed-maturity, zero-coupon order book. Each market is one loan token and one hard maturity, and rates are discovered, not set by a curator.
Lending or borrowing means matching an offer in the book, quoted as a WAD discount price in (0, 1]:
pay
price · unitsof the loan token now, receiveunitsat maturity.
The gap between what you pay and what you're redeemed is the interest. So the implied period return is 1/price − 1, and the APR annualizes that over the time left to the shared maturity:
periodRate = 1/price − 1
APR = periodRate × secondsPerYear / timeToMaturity
Because the maturity is a fixed date, a given discount implies a higher APR as the maturity approaches (you're dividing by a smaller remaining time). The quoted deposit APR is the best resting lend offer - the lowest-price ask in the book - not a blended pool rate. Fill it and the next-best offer becomes the new quote.
There's no shared pool, so "total deposits" isn't a pool balance - it's the depth of resting lend offers. Utilization is undefined and reported as 0. Spot supply/borrow/repay run through MidnightBundlesV1; leverage is built through the 1delta Composer against API-sourced offers.
The two side by side
| Lista broker | Midnight | |
|---|---|---|
| Fixed thing | duration (30/60/90d menu) | maturity (one calendar date) |
| Your term | starts at your open time | whatever time is left |
| Rate source | broker menu APR, per-second accrual | order-book discount price → APR |
| Structure | broker over a Moolah (Blue-fork) market | zero-coupon order book |
| Collateral | shared in Moolah, your address | per-market, order-book matched |
| Liquidity metric | Moolah market liquidity | resting offer depth |
| Early exit | penalty ≈ ½ remaining interest | close/sell position back into book |
| Supply yield | variable (Moolah supply rate) | fixed (you are the offer) |
| Public data from | on-chain multicall | hosted order-book API |
They could hardly be more different in mechanism. The trick is that they don't have to look different downstream.
The unified model - one term card
Both variants land in the same market record every other lender uses (MorphoGeneralPublicResponse), with one addition that makes fixed-term markets self-describing: a terms[] rate card.
terms: {
termId: number // product id
durationSecs: number // term length in seconds
durationDays: number // …in days, for display
apr: number // fixed borrow APR, in percent
}[]
The rate card is where the two shapes reconcile:
- Lista emits one entry per menu product - the whole
getFixedTerms()list. A market might offer three terms, sotermshas three rows. - Midnight emits exactly one entry - the single maturity, with
durationDays= time remaining right now andapr= the best borrowable rate from the book.
Either way, a consumer reads terms[], sees the available (duration, apr) choices, and picks one. A fixed-maturity market is just a term card with a single, shrinking row; a fixed-duration market is a term card with a stable menu. The fixed borrow rate is also mirrored onto stableBorrowRate, so any consumer that already understands "stable vs variable" picks it up for free, and hasStable/variableBorrowDisabled flag that this market is fixed-term on the borrow side.
Positions read back the same way. A brokered or matured fixed loan surfaces as an extra positions[] entry carrying a term object - loanId (the repay target), termId, locked apr, maturity, accruedInterest, earlyRepayPenalty, isMatured - so a portfolio view renders every fixed loan, from either protocol, with one code path. Downstream, fixed-term-aware pipelines like the yield tracer consume the same terms shape as a generic market_terms feed.
How we aggregate the data provision
Same output, but the two markets don't even share a data source. Fixed-term markets are where "aggregation" stops meaning "call the same kind of endpoint N times" and starts meaning "run two unrelated pipelines and make them converge."
Lista is an on-chain pipeline with a second hop. The broker address isn't known up front - it depends on the market - so we resolve it with Moolah.brokers(id) (immutable per market, cached forever), then fire dedicated multicalls against the broker: getFixedTerms() for the public menu, and userFixedPositions / userDynamicPosition / getUserTotalDebt for a user's breakdown. The accrual and early-repayment penalty are then computed off-chain, mirroring the broker's on-chain math exactly, so numbers reconcile to the wei between repayments.
Midnight is an off-chain pipeline. The public view needs no RPC at all: we read the hosted order book (GET /books/{id}), reduce each side to the best executable tick plus aggregate depth, and run the tick↔price↔rate math (a dependency-free port of Morpho's TickLib) to turn the best offer into an APR. The only on-chain read is for USD pricing - each Midnight market's collateral leg carries a Morpho-style oracle, and we fetch its price() exactly the way we price Morpho Blue markets, deriving collateral USD from the loan asset's USD. That keeps a Midnight market's TVL and position values in the same units as every other lender we index.
Two pipelines - a cached second-hop multicall and a hosted order-book API - and neither leaks into the output. What a consumer sees is one fixed-term market record with a terms[] card, a fixed APR, and (for positions) a term object. The provider-specific machinery stays behind the mapping, exactly like it does for the three generations of Aave.
Why this matters for builders
Fixed-term lending is the on-chain analogue of the fixed-income desk - predictable cost of capital for borrowers, predictable yield for lenders, no rate surprises mid-position. It's showing up across DeFi precisely because floating rates are hard to build a business on. But every implementation is bespoke, and reading them raw means learning a broker's accrual math and an order book's discount curve before you can render a single row.
Through the 1delta API you don't. A fixed-duration Lista market and a fixed-maturity Midnight market arrive as the same object: a terms[] rate card you can list, a fixed APR you can sort next to variable ones, and a term-tagged position you can render in a portfolio - with the accrued interest and early-exit penalty already computed. You integrate once, against one shape, and every fixed-term market we add lands in it.
One-sentence takeaway
Fixed-term lending comes in two structurally different shapes - Lista fixes the duration, Midnight fixes the maturity - and by mapping both through their own pipelines into one terms[] rate card, we let you compare and consume them identically, without knowing which mechanism produced the rate.
Where to go next
- API reference - endpoints, response shapes, and
x-api-keysetup. - Docs site - supported protocols, network coverage, and the rest of this series.
- Cross-protocol lending aggregation - the general model this fixed-term work plugs into.
- Unifying Aave V2, V3 & V4 - the same "one record, many mappings" idea applied to a single brand's three generations.