Midnight SDK
Introduction
The Midnight SDK (@morpho-org/midnight-sdk) is the viem-based TypeScript toolkit for Morpho Midnight - fixed-rate, fixed-term lending through signed maker offers matched in an onchain orderbook.
It covers both sides of the protocol:
- Make side - construct offers locally, group offers that share a consumption cap, commit them to a Merkle tree, validate the tree against the Midnight API's mempool policy, sign or approve it through a ratifier, and publish the encoded payload onchain.
- Take side - query books, price levels, and quotes from the Midnight API, and execute the returned ABI-ready takes against the
Midnightcontract.
It also ships viem fetch helpers for onchain market and position state, entity classes with local accrual math, TypeScript ports of the protocol's tick math, protocol constants, contract ABIs, and dedicated error classes.
Source: morpho-org/sdks · packages/midnight-sdk · Package: @morpho-org/midnight-sdk on npm
This page documents the SDK surface. For protocol theory - how ticks discretize prices, how the mempool and router work, how collateral and liquidations behave - see the Midnight docs, in particular Tick structure, Market mechanics, and Mempool & router.
When to use the Midnight SDK
| Use case | Use |
|---|---|
| Make fixed-rate offers on Midnight (lend or borrow as a maker) | Midnight SDK (this page) |
| Take Midnight offers from books and quotes (lend or borrow as a taker) | Midnight SDK |
| Read Midnight market / position state, compute accrued positions offchain | Midnight SDK |
| Convert ticks ↔ prices ↔ rates ↔ APRs, or units ↔ assets | Midnight SDK |
| Production deposit / withdraw / borrow / repay transactions on VaultV2 or Morpho Blue | Morpho SDK |
| Raw entity classes for Morpho Blue offchain computations | Morpho SDK (@morpho-org/morpho-sdk/entities) |
| Low-level viem fetchers for Morpho Blue | Morpho SDK (@morpho-org/morpho-sdk/fetch) |
The Midnight SDK is a lower-level toolkit than the Morpho SDK: there is no getRequirements / buildTx flow and no bundler composition. You construct protocol objects, sign typed data, and send transactions with your own viem client.
Installation
pnpm add @morpho-org/midnight-sdk@0.1.0 @morpho-org/morpho-ts@^2.7.0 viem@^2.0.0
# or
yarn add @morpho-org/midnight-sdk@0.1.0 @morpho-org/morpho-ts@^2.7.0 viem@^2.0.0
# or
npm install @morpho-org/midnight-sdk@0.1.0 @morpho-org/morpho-ts@^2.7.0 viem@^2.0.0@morpho-org/morpho-ts and viem are peer dependencies (@morpho-org/morpho-ts@^2.7.0 and viem@^2.0.0) and must be installed alongside the SDK.
Entry points
The package has two entry points:
// Protocol utilities: offers, groups, trees, ratifier utils, payloads,
// entities, tick math, fetch helpers, constants, ABIs, errors.
import { Offer, Tree, Payload, TickLib, fetchMarket } from "@morpho-org/midnight-sdk";
// Midnight HTTP API helpers, on a dedicated subpath.
import { MidnightApi } from "@morpho-org/midnight-sdk/api";@morpho-org/midnight-sdk/api is not a stable import path and may not respect semver versioning. Pin the SDK version if you depend on it.
Core concepts
How the SDK's types map onto the protocol:
- Markets. A Midnight market is fully described by its immutable params -
loanToken,collateralParams(one{ token, lltv, liquidationCursor, oracle }entry per accepted collateral),maturity,rcfThreshold,enterGate/liquidatorGate- plus thechainIdandmidnightcontract address. The SDK models them asMarketParams, and a hydratedMarketadds mutable state. Onchain reads address markets by theirmarketIdhash. - Ticks and prices. Offer prices live on a discrete tick grid: each tick indexes a WAD-scaled price ≤ 1 (the discount paid today per unit redeemed at maturity), so a tick is equivalently a fixed rate. Ticks must be aligned to the market's tick spacing.
TickLibconverts tick ↔ price ↔ rate ↔ APR - see Tick math. - Units and assets. Offer consumption is capped either in units (the protocol's normalized quantity dimension) or in assets (loan-token amounts). Exactly one of
maxUnits/maxAssetsmust be non-zero on an offer;TakeAmountsLibconverts between the two at a given tick. - Offers. A maker offer commits to a market, a side (
buy: true- the maker buys units, i.e. lends;buy: false- the maker sells units, i.e. borrows), atick, a consumption cap, astart/expirywindow, and aratifier. Optional fields (reduceOnly,continuousFeeCap, maker callback, …) default to protocol-sensible values inOffer.create. - Ratifiers. Offers are not individual transactions - makers commit a whole tree of offers at once, and a ratifier contract proves maker intent when an offer is taken. Two routes ship with the protocol: the EcrecoverRatifier (an EOA - or authorized signer - signs the tree root as typed data) and the SetterRatifier (a contract maker approves the tree root onchain).
fetchRatifierInfoinspects the maker's bytecode and tells you which route to use. - Mempool. Encoded offer payloads are published as calldata to the Midnight mempool contract, where the offchain router and API index them into books. See Mempool & router.
Making offers
The make side is a pipeline: build offers → group the ones that should share a cap → commit everything to a tree → validate → sign or approve through the ratifier → encode → publish.
1. Define the market and create offers
import { addresses } from "@morpho-org/morpho-ts";
import { Offer } from "@morpho-org/midnight-sdk";
import { parseUnits, zeroAddress, type Address } from "viem";
const chainId = 8453;
const usdc = addresses[chainId].usdc!;
const weth = addresses[chainId].wNative!;
const midnight = addresses[chainId].midnight!;
const ecrecoverRatifier = addresses[chainId].ecrecoverRatifier!;
const maker = "0xMaker00000000000000000000000000000000000" as Address;
const wethUsdcOracle = "0xOracle0000000000000000000000000000000000" as Address;
// Immutable market description. Onchain reads address the market by its id hash.
const market = {
chainId,
midnight,
loanToken: usdc,
collateralParams: [
{
token: weth,
lltv: 770_000000000000000000n,
liquidationCursor: 250_000000000000000000n,
oracle: wethUsdcOracle,
},
],
maturity: 1_789_743_600n, // 2026-09-18 15:00:00 UTC.
rcfThreshold: 0n,
enterGate: zeroAddress,
liquidatorGate: zeroAddress,
};
const commonOffer = {
market,
maker,
expiry: 1_789_741_800n,
ratifier: ecrecoverRatifier,
};
// Maker buys units = lends. `maxAssets` caps consumption in loan-token terms.
const lend250kUsdc = Offer.create({
...commonOffer,
buy: true,
tick: 5_000n,
maxAssets: parseUnits("250000", 6),
});
// Maker sells units = borrows. `maxUnits` caps consumption in units.
const borrow50Units = Offer.create({
...commonOffer,
buy: false,
tick: 5_024n,
maxUnits: parseUnits("50", 18),
});Fields you omit get protocol defaults: start defaults to zero (immediately takeable), continuousFeeCap to the protocol maximum, callback / callbackData to none, and receiverIfMakerIsSeller to the maker on sell offers. Exactly one of maxUnits / maxAssets must be non-zero.
2. Group offers that share a consumption cap (optional)
Group.create puts several offers behind one shared consumption budget - Midnight tracks a single consumed scalar per maker and group. Offers in a group must share maker, side, loan token, and cap mode and value; the group id is derived by hashing the offers, and each offer is copied with that id.
import { Group } from "@morpho-org/midnight-sdk";
// One 250k USDC budget posted at two ticks - a fill on either tick
// consumes the same shared cap.
const lendLadder = Group.create([
Offer.create({ ...commonOffer, buy: true, tick: 5_000n, maxAssets: parseUnits("250000", 6) }),
Offer.create({ ...commonOffer, buy: true, tick: 5_012n, maxAssets: parseUnits("250000", 6) }),
]);Group creation is resource-intensive compared to offer construction - build offers first, group once.
3. Commit to a tree
Tree.create accepts a mix of standalone offers and groups and commits them into one Merkle tree - the unit that ratifiers sign or approve.
import { Tree } from "@morpho-org/midnight-sdk";
const tree = Tree.create([lendLadder, borrow50Units]);
// tree.root - Merkle root the ratifier signs or approves.
// tree.offers - non-padding offers in leaf order.
// tree.paddedOffers - ABI-compatible offers including protocol-zero padding.
// tree.leaves, tree.height4. Validate against the mempool policy
// Ratification data is optional pre-signature - validating early catches API
// policy issues before asking the maker to sign.
await tree.mempoolValidate({ chainId });mempoolValidate throws MidnightMempoolValidationError - carrying the API's issues - when policy validation fails. Pass apiUrl to target a custom Midnight API deployment.
5. Sign and ratify (EOA makers)
import { EcrecoverRatifierUtils } from "@morpho-org/midnight-sdk";
// Derives the verifying contract from `offer.ratifier` and rejects
// mixed-ratifier trees. `account` may be the maker or an authorized signer
// for every maker in the tree.
const signature = await EcrecoverRatifierUtils.sign({
tree,
client: walletClient,
account: maker,
});
// Optional: re-validate the final payload shape with real ratification data.
await tree.mempoolValidate({
chainId,
ratification: { type: "ecrecover", account: maker, signature },
});
const items = await EcrecoverRatifierUtils.ratify({
tree,
account: maker,
signature,
});Signing throws ChainIdMismatchError when the client's chain does not match the offer market's chain, and InvalidTypedDataSignatureError when the signature does not recover to the expected signer.
When the maker is a contract, use SetterRatifierUtils instead: build the Tree, validate it, submit the root approval transaction for every maker in the tree, then call SetterRatifierUtils.ratify({ tree }) before encoding. The setterRatifierAbi export covers the approval call.
6. Encode and publish
import { Payload } from "@morpho-org/midnight-sdk";
const payload = await Payload.encode(items);
const transactionHash = await walletClient.sendTransaction({
account: maker,
to: addresses[chainId].midnightMempool!,
data: payload,
});Payload.encode turns ratified items into wire bytes for mempool publication; Payload.decode inspects published maker offers, ratifier data, or attribution-tagged payloads.
Taking offers
The take side starts from API responses: book, quote, and takeable-offer endpoints return ABI-ready take objects - each with offer, ratifierData, and units for Midnight.take, plus marketId metadata. Book asks are maker sell offers; book bids are maker buy offers.
import { addresses } from "@morpho-org/morpho-ts";
import { MidnightApi } from "@morpho-org/midnight-sdk/api";
import { midnightAbi } from "@morpho-org/midnight-sdk";
import { parseUnits, zeroAddress, type Address, type Hash } from "viem";
const chainId = 8453;
const midnight = addresses[chainId].midnight!;
const marketId = "0xMarketId..." as Hash;
const taker = "0xTaker00000000000000000000000000000000000" as Address;
const receiverIfTakerIsSeller = taker;
// Quote how to fill 50 units from the ask side, tolerating 0.5% slippage.
const quote = await MidnightApi.fetchBookQuote({
marketId,
side: "asks",
units: parseUnits("50", 18),
slippage: "0.5",
});
// A quote can span several offers: execute the returned takes IN ORDER and
// clamp each take to the remaining target instead of submitting every
// returned cap blindly.
let remainingUnits = parseUnits("50", 18);
for (const take of quote.data.takeableOffers) {
if (remainingUnits === 0n) break;
const units = take.units < remainingUnits ? take.units : remainingUnits;
if (units === 0n) continue;
await walletClient.writeContract({
account: taker,
address: midnight,
abi: midnightAbi,
functionName: "take",
args: [
take.offer,
take.ratifierData,
units,
taker,
receiverIfTakerIsSeller,
zeroAddress, // no taker callback
"0x",
],
});
remainingUnits -= units;
}The Midnight API client
MidnightApi (from @morpho-org/midnight-sdk/api) is both a static helper surface and an instantiable client. Static methods target https://api.morpho.org/v1/midnight by default and accept per-call configuration; instantiate the class when an integration makes repeated calls or needs shared request options (baseUrl, a custom fetch implementation, and request options such as headers, credentials, and abort signals). The SDK owns endpoint paths, HTTP methods, request bodies, and response normalization; caller inputs and successful JSON output shapes are trusted at runtime.
import { MidnightApi } from "@morpho-org/midnight-sdk/api";
// One-off static call:
const books = await MidnightApi.fetchBooks({ chainIds: [8453], limit: 10 });
// Shared configuration for repeated calls:
const api = new MidnightApi({ baseUrl: "https://api.morpho.org/v1/midnight" });
const book = await api.fetchBook({ marketId });| Method | Reads | Returns |
|---|---|---|
fetchBooks | GET /books | Paginated active books with market metadata and top price levels, mapped to SDK camelCase fields. |
fetchBook | GET /books/{marketId} | A single book snapshot with market metadata and both sides. |
fetchBookPriceLevels | GET /books/{marketId}/{side} | One side of a book grouped by price level. |
fetchBookTakeableOffers | GET /books/{marketId}/{side}/takeable-offers | ABI-ready take objects for one side of a book. |
fetchBookQuote | GET /books/{marketId}/{side}/quote | A quote (average best/worst price, available assets and units) plus signed ABI-ready take caps for one side. |
fetchTakeableOffers | GET /takeable-offers | Paginated ABI-ready take objects for one maker's unexpired, unmatured offers. |
validateMempoolPayload | POST /mempool/validate | The API's validation issues and a valid summary for already-encoded payload bytes. |
validateMempoolItems | POST /mempool/validate | The API's validation issues and a valid summary after encoding the given SDK-native payload items. |
Every method throws MidnightApiError on a non-2xx response and InvalidMidnightApiResponseError on a malformed success body. validateMempoolPayload is the raw, non-throwing validation surface for already-encoded payload bytes - in normal make-side flows prefer tree.mempoolValidate(...), which throws MidnightMempoolValidationError with the API's issues.
The same endpoints are documented in the Midnight API reference.
Reading onchain state
The fetch helpers are plain viem functions: they resolve the Midnight contract address for the client's chain from the @morpho-org/morpho-ts address registry, and accept optional account, blockNumber, blockTag, and stateOverride call parameters.
| Fetcher | Reads | Returns |
|---|---|---|
fetchMarketParams(client, { marketId }) | Midnight.toMarket | MarketParams - the market's immutable params |
fetchMarket(client, { marketId }) | Midnight.toMarket + Midnight.marketState | Market - params hydrated with mutable state |
fetchPosition(client, { marketId, user }) | Deployless GetPosition query (fallback: Midnight.position + up to 128 collateral slots) | Position - credit, pending fee, debt, collateral balances |
fetchAccrualPosition(client, { marketId, user }) | fetchPosition + fetchMarket in parallel | AccrualPosition - the position bound to its market |
fetchRatifierInfo(client, { maker }) | The maker's bytecode | RatifierInfo - which ratifier route to use, and its address |
fetchPosition defaults to a deployless multicall-style query with a per-slot fallback; pass deployless: "force" to disable the fallback or deployless: false to skip it entirely.
AccrualPosition.accrueInterest(timestamp) reproduces Midnight's updatePositionView locally - it returns a new accrued position without an RPC round-trip. It validates its inputs and throws InvalidPositionAccrualTimestampError, InvalidPositionLossFactorError, or InvalidPositionAccrualStateError on inconsistent snapshots - fetch the market and position at the same block. Related helpers: getCollateralBalanceByIndex / getCollateralBalanceByToken and getSettlementFee(timestamp).
Tick math
TickLib is a TypeScript port of the onchain TickLib; TakeAmountsLib converts between units and assets when preparing takes. Everything is bigint / WAD math - no floats.
| Function | Converts | Notes |
|---|---|---|
TickLib.divHalfDownUnchecked(x, d) | dividend → quotient | Divides with half-down rounding without validating the denominator. |
TickLib.assertTickInRange(tick) | tick → validated bigint | Asserts the tick is non-negative and at most MAX_TICK; throws NegativeValueError or TickOutOfRangeError. |
TickLib.wExp(x) | WAD exponent → WAD exponential | The WAD-scaled exponential approximation underpinning tick pricing; inverts for negative x. |
TickLib.tickToPrice(tick) | tick → WAD price | Rounds the result to PRICE_ROUNDING_STEP; validates the tick range. |
TickLib.priceToTick(price, spacing?) | WAD price → tick | Returns the lowest spacing-aligned tick whose price is at least price; spacing defaults to DEFAULT_TICK_SPACING; throws PriceGreaterThanOneError above WAD and InvalidTickSpacingError on bad spacing. |
TickLib.snapPriceToTick(price, spacing?) | WAD price → WAD price | SDK-only convenience that snaps a price to the price of the lowest aligned tick at or above it. |
TickLib.rateToPrice(rate) | WAD fixed rate → WAD price | SDK-only rate conversion; rounds down. |
TickLib.tickToRate(tick) | tick → WAD fixed rate | SDK-only rate conversion; rounds up and throws DivisionByZeroError when the tick price is zero. |
TickLib.tickToApr(tick, timeToMaturity) | tick → WAD simple APR | SDK-only display convenience; annualizes tickToRate over timeToMaturity seconds, rounds up, throws DivisionByZeroError when maturity or price is zero. |
TickLib.assertTickAlignedToSpacing(tick, spacing?) | tick → validated bigint | SDK-only assertion that the tick is in range and aligned to spacing; throws InvalidTickSpacingError when misaligned. |
TakeAmountsLib.prices({ offer, settlementFee }) | offer + fee → buyer/seller prices | Derives buyerPrice and sellerPrice from the offer tick; throws SettlementFeeExceedsPriceError when the fee exceeds a buy offer's price. |
TakeAmountsLib.buyerAssetsToUnits({ offer, targetBuyerAssets, settlementFee }) | buyer assets → units | Rounds up for buy offers and down otherwise; throws DivisionByZeroError, PriceGreaterThanOneError, or SettlementFeeExceedsPriceError. |
TakeAmountsLib.sellerAssetsToUnits({ offer, targetSellerAssets, settlementFee }) | seller assets → units | Rounds up for buy offers and down otherwise; throws DivisionByZeroError when the seller price is zero. |
TakeAmountsLib.toUnits({ assets, price, rounding }) | assets → units | SDK-only generic conversion at an explicit WAD price with a caller-supplied rounding; throws DivisionByZeroError when price is zero. |
TakeAmountsLib.toUnitsAtTick({ assets, tick, rounding }) | assets → units | SDK-only conversion at a tick price via tickToPrice, then toUnits. |
Protocol constants
The SDK re-exports the protocol's deployed constants so your code never hardcodes them:
| Constant | Value | Meaning |
|---|---|---|
CBP | 1_000000000000n | Centibip scale used by settlement fees. |
SETTLEMENT_FEE_BREAKPOINTS | 7 bigint seconds, 0n → 31_104_000n (0, 1, 7, 30, 90, 180, 360 days) | Settlement-fee time-to-maturity breakpoints across which Midnight linearly interpolates each market's seven settlement-fee cbp buckets. |
MAX_TICK | 6744n | Maximum Midnight tick. |
PRICE_ROUNDING_STEP | 100_000000000n | WAD price quantum used by Midnight tick prices. |
DEFAULT_TICK_SPACING | 4n | Default Midnight tick spacing. |
MAX_COLLATERALS | 128n | Maximum collateral entries in a Midnight market. |
MAX_COLLATERALS_PER_BORROWER | 16n | Maximum active collateral entries per borrower. |
MAX_SETTLEMENT_FEES | 7 bigint, 14000000000000n → 5000000000000000n | Maximum settlement-fee values by Midnight fee index. |
MAX_CONTINUOUS_FEE | 317097919n | Maximum continuous fee per second. |
TIME_TO_MAX_LIF | 3600n | Seconds after maturity over which post-maturity LIF reaches the computed maximum liquidation incentive factor. |
COLLATERAL_PARAMS_TYPEHASH | 0x39ed3f92…85b841 | HashLib EIP-712 typehash for the collateral params struct. |
MARKET_TYPEHASH | 0x510b3862…20391a | HashLib EIP-712 typehash for the market struct. |
OFFER_TYPEHASH | 0xa3163484…91bb81 | HashLib EIP-712 typehash for the offer struct. |
EIP712_DOMAIN_TYPEHASH | 0x47e79534…469218 | EcrecoverRatifier EIP-712 domain typehash. |
Errors
The SDK throws dedicated error classes (no generic Errors) so you can branch on failure modes deterministically - one row per class exported from src/errors.ts:
| Error | When it triggers |
|---|---|
TickOutOfRangeError | A tick exceeds the deployed TickLib maximum during tick or offer construction. |
PriceGreaterThanOneError | A price is above the maximum WAD price (1) accepted by TickLib. |
InvalidTickSpacingError | The tick spacing is not a positive divisor of the maximum tick, or a tick is not aligned to the market's spacing. |
SettlementFeeExceedsPriceError | A buy offer's settlement fee exceeds its tick price. |
InvalidOfferParameterError | An offer builder receives a parameter that cannot satisfy Midnight protocol rules (carries the offending parameter and value). |
InvalidMarketParameterError | A market parameter cannot satisfy Midnight protocol rules (carries the offending parameter and value). |
UnknownCollateralIndexError | A market collateral index is queried that the market does not configure (carries market and collateralIndex). |
InvalidOfferGroupError | Offers grouped together violate group mechanics — they must share maker, side, loan token, and cap mode and value. |
InvalidTreeHeightError | A tree height exceeds the ratifier typehash table. |
InvalidTreeError | A tree cannot be represented by the ratifiers, for example when it is empty. |
ChainIdMismatchError | The viem client's chain id does not match the offer market's chain id required by a signing flow (carries clientChainId and expectedChainId). |
InvalidTypedDataSignatureError | A typed-data signature does not recover to the expected signer (carries the expected signer). |
PayloadDecodeError | Mempool payload bytes cannot be encoded or decoded (carries a machine-readable reason). |
MidnightApiError | The Midnight API returns a non-2xx response (carries status, and code / details / requestId when present). |
InvalidMidnightApiResponseError | The Midnight API returns a 2xx response whose body is malformed or missing expected data. |
MidnightMempoolValidationError | The Midnight API rejects a tree during mempool policy validation (carries the API's issues). |
InvalidPositionAccrualTimestampError | A local position accrual is requested for a timestamp before the position's last accrual (carries timestamp and lastAccrual). |
InvalidPositionLossFactorError | A local accrual sees a market loss factor below the position's, meaning the market and position were fetched at inconsistent blocks. |
InvalidPositionAccrualStateError | Local accrual inputs violate Midnight accounting invariants, such as a pending fee exceeding credit. |
MidnightApiError and InvalidMidnightApiResponseError are re-exported from @morpho-org/midnight-sdk/api alongside the client. The fetch helpers can additionally throw UnsupportedChainIdError / UnknownAddressError from @morpho-org/morpho-ts when the client's chain has no Midnight registry entry.
Resources
- Repository: morpho-org/sdks · packages/midnight-sdk
- Package:
@morpho-org/midnight-sdkon npm - README:
packages/midnight-sdk/README.md - Protocol docs: Get started with Midnight · Tick structure · Mempool & router
- Tutorials: Lend at a fixed rate · Borrow at a fixed rate
- API reference: Midnight API
- Contributing:
CONTRIBUTING.md - Security:
SECURITY.md