Docs

Lend at a fixed rate

Snippets assume a viem wallet client walletClient for your account on Base, and taker set to your wallet address.

Browse markets

Fetch the list of books filtered by chain and loan token. Each book represents one market with its top ask/bid price levels:

GET https://api.morpho.org/v0/midnight/books
  ?chain_ids=8453                                                // Base
  &loan_tokens=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913        // USDC on Base
  &sort=maturity

// Example response (sample values; use the full market_id returned by the API)
{
  "cursor": "eyJzb3J0Ijpb...",                                // null when there is no next page
  "data": [
    {
      "market_id": "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
      "chain_id": 8453,
      "maturity": 1798761600,                                    // January 1, 2027
      "loan_token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "midnight": "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A", // Base Midnight address from the registry
      "collaterals": [
        {
          "token": "0x4200000000000000000000000000000000000006", // WETH on Base
          "lltv": "770000000000000000",                          // 77% LTV
          "liquidation_cursor": "250000000000000000",
          "oracle": "0xD09048c8B568Dbf5f189302beA26c9edABFC4858"
        }
      ],
      "asks": [
        {
          "tick": 4990,
          "price": "960000000000000000",                         // WAD-scaled price
          "units": "1041666666"
        },
        {
          "tick": 5010,
          "price": "965000000000000000",
          "units": "2000000000"
        }
      ],
      "bids": [
        {
          "tick": 4900,
          "price": "950000000000000000",
          "units": "1000000000"
        }
      ]
    }
  ]
}

Select market & estimate lend rate

The best ask is the cheapest unit price. Convert the price into an annualized rate:

const book = response.data[0];
const bestAsk = book.asks[0];
const marketId = book.market_id;
const price = Number(bestAsk.price) / 1e18;
const secondsToMaturity = book.maturity - Math.floor(Date.now() / 1000);
const secondsPerYear = 365 * 24 * 60 * 60;
const estimatedLendAPR = (1 / price - 1) * secondsPerYear / secondsToMaturity;

// This estimate assumes your target fits at the first ask's price.
// The router quote below returns a blended price for the target.
console.log({ marketId, estimatedLendAPR });

Get a quote for your offer

REST API

Step 1: Get a quote

Specify the amount of USDC you want to lend to a market with slippage. The Morpho router (via the quote API shown below) returns executable takeable offers with fallback liquidity (i.e. additional offers returned in case offers are partially consumed). This way, during execution, the Bundles call can skip stale offers and still take offers for the lender's request.

GET https://api.morpho.org/v0/midnight/books/{marketId}/asks/quote
  ?assets=1000000000   // 1,000 USDC (6 decimals)
  &slippage=0.5       // 0.5% slippage guard
  &limit=10000        // max offers walked/returned; default and maximum 10,000

// Response: the router's execution plan:
{
  "data": {
    "average_best_price": "947619047619047619", // realized target fill using onchain per-offer rounding, rounded against the taker
    "average_worst_price": "952300000000000000", // conservative adverse-placement bound across returned caps; not the slippage-derived guard
    "available_assets": "15000000000", // max assets in this returned limit window
    "available_units": "15750000000", // max units in this returned limit window
    "takeable_offers": [
      {
        "market_id": "0x25690ae1aee324a005be565f3bcdd16dbf8daf7969b26c181c8b8f467dad9f67",
        "units": "5250000000", // maximum this offer may contribute, not an exact allocation
        "offer": {
          "market": {
            "chain_id": 8453,
            "midnight": "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A",
            "loan_token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
            "collaterals": [
              {
                "token": "0x4200000000000000000000000000000000000006",
                "lltv": "770000000000000000",
                "liquidation_cursor": "250000000000000000",
                "oracle": "0xD09048c8B568Dbf5f189302beA26c9edABFC4858"
              }
            ],
            "maturity": 1798761600,
            "rcf_threshold": "0",
            "enter_gate": "0x0000000000000000000000000000000000000000",
            "liquidator_gate": "0x0000000000000000000000000000000000000000"
          },
          "buy": false, // sell offer (ask): the maker is borrowing
          "maker": "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
          "max_units": "0",
          "start": 1761922790,
          "expiry": 1786147200,
          "tick": 42,
          "group": "0x000000000000000000000000000000000000000000000000000000000008b8f4",
          "callback": "0x0000000000000000000000000000000000000000",
          "callback_data": "0x",
          "receiver_if_maker_is_seller": "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
          "ratifier": "0x0000000000000000000000000000000000000002",
          "reduce_only": false,
          "max_assets": "5000000000",
          "continuous_fee_cap": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
        },
        "ratifier_data": "0x4a8b...f201",
        "valid_until": 1761922799,
        "warning": null
      }
      // Additional complete offers follow in best-price order as fallback excess.
      // A grouped offer's full cap is not guaranteed: sibling fills may consume
      // group capacity before execution, which is why the router returns fallbacks.
    ]
  }
}

// Interpreting the quote
const quote = response.data;
const avgPrice = Number(quote.average_best_price) / 1e18;      // ≈ 0.9476
const impliedRate = (1 / avgPrice - 1) * (365 * 86400) / secondsToMaturity;
const targetUnits = BigInt(Math.ceil(1_000e6 / avgPrice));      // target fill; available_units is fallback-window capacity

Step 2: Convert takeable offers to ABI-encoded data

The API returns snake_case JSON, whereas the onchain ABI expects camelCase tuples. The apiOfferToAbi helper converts the API offer to an ABI struct.

const apiOfferToAbi = ({ offer, ratifier_data, units }) => ({
  offer: {
    market: {
      chainId: BigInt(offer.market.chain_id),
      midnight: offer.market.midnight,
      loanToken: offer.market.loan_token,
      collateralParams: offer.market.collaterals.map(({ token, lltv, liquidation_cursor, oracle }) => ({
        token,
        lltv: BigInt(lltv),
        liquidationCursor: BigInt(liquidation_cursor),
        oracle,
      })),
      maturity: BigInt(offer.market.maturity),
      rcfThreshold: BigInt(offer.market.rcf_threshold),
      enterGate: offer.market.enter_gate,
      liquidatorGate: offer.market.liquidator_gate,
    },
    buy: offer.buy,
    maker: offer.maker,
    start: BigInt(offer.start),
    expiry: BigInt(offer.expiry),
    tick: BigInt(offer.tick),
    group: offer.group,
    callback: offer.callback,
    callbackData: offer.callback_data,
    receiverIfMakerIsSeller: offer.receiver_if_maker_is_seller,
    ratifier: offer.ratifier,
    reduceOnly: offer.reduce_only,
    maxUnits: BigInt(offer.max_units),
    maxAssets: BigInt(offer.max_assets),
    continuousFeeCap: BigInt(offer.continuous_fee_cap),
  },
  ratifierData: ratifier_data,
  units: BigInt(units),
});

const takes = quote.takeable_offers.map(apiOfferToAbi);

SDK

MidnightApi.fetchBookQuote wraps the quote endpoint as part of the Morpho SDK's Midnight API helpers and returns ABI-ready take objects

Step 1: Install dependencies

npm install @morpho-org/morpho-sdk viem

Step 2: Get ABI-ready takeable offers

import { MidnightApi } from "@morpho-org/morpho-sdk/midnight-api";

const quote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  assets: 1_000_000_000n, // 1,000 USDC with six decimals
  slippage: "0.5",
});
const takes = quote.data.takeableOffers;

Execute the lend

The Morpho SDK path derives the USDC approval and any required MidnightBundlesV2 authorization, applies your price guard, and builds a transaction that takes offers toward your target while skipping stale offers. The direct MidnightBundlesV2 contract call below is the manual equivalent.

Morpho SDK

Extend walletClient and hand the flow the quote from the previous step (SDK tab):

import { maxUint256, parseUnits, publicActions } from "viem";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";

const client = walletClient.extend(morphoViemExtension()).extend(publicActions);
const midnight = client.morpho.midnight(8453);
const marketData = await midnight.getMarketData(marketId);
const lendAssets = parseUnits("1000", 6);
const minUnits =
  (lendAssets * 10n ** 18n) / BigInt(quote.data.averageWorstPrice);

const action = midnight.takeLend({
  accountAddress: taker,
  marketData,
  target: { type: "assets", assets: lendAssets, minUnits },
  takeableOffers: quote.data.takeableOffers,
  maxContinuousFee: maxUint256,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 600),
});

const requirements = await action.getRequirements();
// Fulfill the USDC approval and MidnightBundlesV2 authorization separately.
for (const requirement of requirements) {
  const requirementHash = await client.sendTransaction({ ...requirement, account: taker });
  await client.waitForTransactionReceipt({ hash: requirementHash });
}
const hash = await client.sendTransaction({ ...action.buildTx(), account: taker });
await client.waitForTransactionReceipt({ hash });

See Taker flows for the full surface, including takeBorrow and supplyCollateralTakeBorrow.

Raw contract call

Working below the SDK? Call MidnightBundlesV2 directly. It takes offers toward your target in one transaction and skips stale offers; handle the token approval and Midnight authorization separately.

Authorizations & Approvals

Submit the loan-token ERC-20 approval to MidnightBundlesV2 separately, then authorize it on the Midnight contract.

import { erc20Abi, maxUint256 } from "viem";
import { midnightAbi } from "@morpho-org/morpho-sdk/abis";

const MIDNIGHT_BUNDLES_V2 = "0x0bF30B530f816a619C3F28a3b9f44C441122555D"; // MidnightBundlesV2 on Base (from the @morpho-org/morpho-ts address registry)
const MIDNIGHT = "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";

await walletClient.writeContract({
  account: taker,
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [MIDNIGHT_BUNDLES_V2, maxUint256],
});

await walletClient.writeContract({
  account: taker,
  address: MIDNIGHT,
  abi: midnightAbi,
  functionName: "setIsAuthorized",
  args: [MIDNIGHT_BUNDLES_V2, true, taker],
});

Execute onchain

Specify the target amount of assets you want to buy (i.e. lend), a price guard, and the MidnightBundlesV2 call arguments.

import { maxUint256, parseUnits, zeroAddress } from "viem";
import { midnightBundlesV2Abi } from "@morpho-org/morpho-sdk/abis";

const MIDNIGHT_BUNDLES_V2 = "0x0bF30B530f816a619C3F28a3b9f44C441122555D"; // MidnightBundlesV2 on Base (from the @morpho-org/morpho-ts address registry)
const lendAssets = parseUnits("1000", 6);
const targetBuyerAssets = lendAssets;
const minUnits =
  (targetBuyerAssets * 10n ** 18n) / BigInt(quote.average_worst_price);
const offerFills = quote.takeable_offers.map(apiOfferToAbi);
const market = offerFills[0].offer.market;

await walletClient.writeContract({
  account: taker,
  address: MIDNIGHT_BUNDLES_V2,
  abi: midnightBundlesV2Abi,
  functionName: "midnightBundlesV2BuyWithAssetsTargetAndWithdrawCollateral",
  args: [
    market,                         // market
    targetBuyerAssets,              // targetBuyerAssets
    minUnits,                       // minUnits
    false,                          // reduceOnly
    false,                          // repayEnabled
    offerFills,                     // offerFills
    [],                             // collateralWithdrawals
    taker,                          // collateralReceiver
    0n,                             // referralFeePct
    zeroAddress,                    // referralFeeRecipient
    maxUint256,                     // maxContinuousFee
    BigInt(Math.floor(Date.now() / 1000) + 600), // deadline
    zeroAddress,                    // wrappedNative, unused when msg.value is 0
  ],
});