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 Midnight contract.

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.

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 caseUse
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 offchainMidnight SDK
Convert ticks ↔ prices ↔ rates ↔ APRs, or units ↔ assetsMidnight SDK
Production deposit / withdraw / borrow / repay transactions on VaultV2 or Morpho BlueMorpho SDK
Raw entity classes for Morpho Blue offchain computationsMorpho SDK (@morpho-org/morpho-sdk/entities)
Low-level viem fetchers for Morpho BlueMorpho 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 the chainId and midnight contract address. The SDK models them as MarketParams, and a hydrated Market adds mutable state. Onchain reads address markets by their marketId hash.
  • 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. TickLib converts 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 / maxAssets must be non-zero on an offer; TakeAmountsLib converts 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), a tick, a consumption cap, a start / expiry window, and a ratifier. Optional fields (reduceOnly, continuousFeeCap, maker callback, …) default to protocol-sensible values in Offer.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). fetchRatifierInfo inspects 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.height

4. 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 });
MethodReadsReturns
fetchBooksGET /booksPaginated active books with market metadata and top price levels, mapped to SDK camelCase fields.
fetchBookGET /books/{marketId}A single book snapshot with market metadata and both sides.
fetchBookPriceLevelsGET /books/{marketId}/{side}One side of a book grouped by price level.
fetchBookTakeableOffersGET /books/{marketId}/{side}/takeable-offersABI-ready take objects for one side of a book.
fetchBookQuoteGET /books/{marketId}/{side}/quoteA quote (average best/worst price, available assets and units) plus signed ABI-ready take caps for one side.
fetchTakeableOffersGET /takeable-offersPaginated ABI-ready take objects for one maker's unexpired, unmatured offers.
validateMempoolPayloadPOST /mempool/validateThe API's validation issues and a valid summary for already-encoded payload bytes.
validateMempoolItemsPOST /mempool/validateThe 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.

FetcherReadsReturns
fetchMarketParams(client, { marketId })Midnight.toMarketMarketParams - the market's immutable params
fetchMarket(client, { marketId })Midnight.toMarket + Midnight.marketStateMarket - 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 parallelAccrualPosition - the position bound to its market
fetchRatifierInfo(client, { maker })The maker's bytecodeRatifierInfo - 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.

FunctionConvertsNotes
TickLib.divHalfDownUnchecked(x, d)dividend → quotientDivides with half-down rounding without validating the denominator.
TickLib.assertTickInRange(tick)tick → validated bigintAsserts the tick is non-negative and at most MAX_TICK; throws NegativeValueError or TickOutOfRangeError.
TickLib.wExp(x)WAD exponent → WAD exponentialThe WAD-scaled exponential approximation underpinning tick pricing; inverts for negative x.
TickLib.tickToPrice(tick)tick → WAD priceRounds the result to PRICE_ROUNDING_STEP; validates the tick range.
TickLib.priceToTick(price, spacing?)WAD price → tickReturns 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 priceSDK-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 priceSDK-only rate conversion; rounds down.
TickLib.tickToRate(tick)tick → WAD fixed rateSDK-only rate conversion; rounds up and throws DivisionByZeroError when the tick price is zero.
TickLib.tickToApr(tick, timeToMaturity)tick → WAD simple APRSDK-only display convenience; annualizes tickToRate over timeToMaturity seconds, rounds up, throws DivisionByZeroError when maturity or price is zero.
TickLib.assertTickAlignedToSpacing(tick, spacing?)tick → validated bigintSDK-only assertion that the tick is in range and aligned to spacing; throws InvalidTickSpacingError when misaligned.
TakeAmountsLib.prices({ offer, settlementFee })offer + fee → buyer/seller pricesDerives 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 → unitsRounds up for buy offers and down otherwise; throws DivisionByZeroError, PriceGreaterThanOneError, or SettlementFeeExceedsPriceError.
TakeAmountsLib.sellerAssetsToUnits({ offer, targetSellerAssets, settlementFee })seller assets → unitsRounds up for buy offers and down otherwise; throws DivisionByZeroError when the seller price is zero.
TakeAmountsLib.toUnits({ assets, price, rounding })assets → unitsSDK-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 → unitsSDK-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:

ConstantValueMeaning
CBP1_000000000000nCentibip scale used by settlement fees.
SETTLEMENT_FEE_BREAKPOINTS7 bigint seconds, 0n31_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_TICK6744nMaximum Midnight tick.
PRICE_ROUNDING_STEP100_000000000nWAD price quantum used by Midnight tick prices.
DEFAULT_TICK_SPACING4nDefault Midnight tick spacing.
MAX_COLLATERALS128nMaximum collateral entries in a Midnight market.
MAX_COLLATERALS_PER_BORROWER16nMaximum active collateral entries per borrower.
MAX_SETTLEMENT_FEES7 bigint, 14000000000000n5000000000000000nMaximum settlement-fee values by Midnight fee index.
MAX_CONTINUOUS_FEE317097919nMaximum continuous fee per second.
TIME_TO_MAX_LIF3600nSeconds after maturity over which post-maturity LIF reaches the computed maximum liquidation incentive factor.
COLLATERAL_PARAMS_TYPEHASH0x39ed3f92…85b841HashLib EIP-712 typehash for the collateral params struct.
MARKET_TYPEHASH0x510b3862…20391aHashLib EIP-712 typehash for the market struct.
OFFER_TYPEHASH0xa3163484…91bb81HashLib EIP-712 typehash for the offer struct.
EIP712_DOMAIN_TYPEHASH0x47e79534…469218EcrecoverRatifier 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:

ErrorWhen it triggers
TickOutOfRangeErrorA tick exceeds the deployed TickLib maximum during tick or offer construction.
PriceGreaterThanOneErrorA price is above the maximum WAD price (1) accepted by TickLib.
InvalidTickSpacingErrorThe tick spacing is not a positive divisor of the maximum tick, or a tick is not aligned to the market's spacing.
SettlementFeeExceedsPriceErrorA buy offer's settlement fee exceeds its tick price.
InvalidOfferParameterErrorAn offer builder receives a parameter that cannot satisfy Midnight protocol rules (carries the offending parameter and value).
InvalidMarketParameterErrorA market parameter cannot satisfy Midnight protocol rules (carries the offending parameter and value).
UnknownCollateralIndexErrorA market collateral index is queried that the market does not configure (carries market and collateralIndex).
InvalidOfferGroupErrorOffers grouped together violate group mechanics — they must share maker, side, loan token, and cap mode and value.
InvalidTreeHeightErrorA tree height exceeds the ratifier typehash table.
InvalidTreeErrorA tree cannot be represented by the ratifiers, for example when it is empty.
ChainIdMismatchErrorThe viem client's chain id does not match the offer market's chain id required by a signing flow (carries clientChainId and expectedChainId).
InvalidTypedDataSignatureErrorA typed-data signature does not recover to the expected signer (carries the expected signer).
PayloadDecodeErrorMempool payload bytes cannot be encoded or decoded (carries a machine-readable reason).
MidnightApiErrorThe Midnight API returns a non-2xx response (carries status, and code / details / requestId when present).
InvalidMidnightApiResponseErrorThe Midnight API returns a 2xx response whose body is malformed or missing expected data.
MidnightMempoolValidationErrorThe Midnight API rejects a tree during mempool policy validation (carries the API's issues).
InvalidPositionAccrualTimestampErrorA local position accrual is requested for a timestamp before the position's last accrual (carries timestamp and lastAccrual).
InvalidPositionLossFactorErrorA local accrual sees a market loss factor below the position's, meaning the market and position were fetched at inconsistent blocks.
InvalidPositionAccrualStateErrorLocal 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

On this page