Borrow 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
// Response (each book is one market with its top ask/bid levels)
{
"cursor": "eyJzb3J0Ijpb...", // null when there is no next page
"data": [
{
"market_id": "0xd92de5e7fbb...7614", // use this ID in the next step
"chain_id": 8453,
"midnight": "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A",
"loan_token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"collaterals": [
{
"token": "0x4200000000000000000000000000000000000006", // WETH on Base
"lltv": "770000000000000000", // 77% LTV
"liquidation_cursor": "250000000000000000",
"oracle": "0xD09048c8B568Dbf5f189302beA26c9edABFC4858"
}
],
"maturity": 1798761600, // January 1, 2027
"rcf_threshold": "0",
"enter_gate": "0x0000000000000000000000000000000000000000",
"liquidator_gate": "0x0000000000000000000000000000000000000000",
"asks": [
{ "tick": 4990, "price": "957900000000000000", "units": "5000000000", "assets": "4789500000", "count": 1 }
], // lending side; ignore for borrowing
// ┌──────────────────────────────────────────────────────────────┐
// │ FOR BORROWING → read the BIDS │
// │ Bids = lenders buying units = offering to lend you money │
// │ You (the borrower) take these bids = sell your units │
// │ The bid price is what you receive per unit → your cost │
// └──────────────────────────────────────────────────────────────┘
"bids": [
{
"tick": 4988,
"price": "956937000000000000", // ≈ 0.9569 per unit
"units": "8000000000",
"assets": "7655497000", // ≈ 7,655 USDC on the bid
"count": 2
},
{
"tick": 4976,
"price": "951000000000000000", // ≈ 0.9510 (worse for borrower)
"units": "12000000000",
"assets": "11412000000",
"count": 4
}
]
}
]
}Select market & estimate borrow rate
The best bid is the highest price. Higher means you receive more per unit sold, so lower borrowing cost.
// Select book data
const book = response.data[0];
// Obtain best bid price (highest = cheapest to borrow)
const bestBidPrice = Number(book.bids[0].price) / 1e18; // ≈ 0.9569
// Calculate time to maturity (ttm) in seconds
const ttm = book.maturity - Date.now() / 1000;
// Calculate fixed borrow APR
const borrowAPR = (1 / bestBidPrice - 1) * (365 * 86400) / ttm;
console.log(`Borrow APR: ${(borrowAPR * 100).toFixed(2)}%`);
// If the rate looks good, save the market ID for the next step
const marketId = book.market_id; // "0xd92de5e7fbb...7614"Get a quote for your offer
REST API
Step 1: Get a quote
Specify the amount of USDC you want to borrow from a market with slippage. Morpho's router returns executable takeable offers (quotes against the bids) with fallback liquidity. The total may exceed your target so the Bundles call can skip stale offers and still take offers.
const loanAssets = 10_000_000_000n; // 10,000 USDC (6 decimals)
GET https://api.morpho.org/v0/midnight/books/{marketId}/bids/quote
?assets=10000000000 // `loanAssets`
&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": "956937000000000000", // realized target fill using onchain per-offer rounding, rounded against the taker
"average_worst_price": "952200000000000000", // conservative adverse-placement bound across returned caps; not the slippage-derived guard
"available_assets": "14000000000", // max assets in this returned limit window
"available_units": "14630000000", // max units in this returned limit window
"takeable_offers": [
{
"market_id": "0xd92de5e7fbb...7614",
"units": "8000000000", // 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": true, // buy offer (bid): the maker is lending
"maker": "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
"max_units": "8000000000",
"start": 1761922790,
"expiry": 1786147200,
"tick": 4988,
"group": "0x000000000000000000000000000000000000000000000000000000000008b8f4",
"callback": "0x0000000000000000000000000000000000000000",
"callback_data": "0x",
"receiver_if_maker_is_seller": "0x7b093658BE7f90B63D7c359e8f408e503c2D9401",
"ratifier": "0x0000000000000000000000000000000000000002",
"reduce_only": false,
"max_assets": "0",
"continuous_fee_cap": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
},
"ratifier_data": "0x7c1e...d402",
"valid_until": 1761922799,
"warning": null
}
// Additional complete offers follow in best-price order as fallback excess.
]
}
}
// Interpreting the quote
const quote = response.data;
const avgPrice = Number(quote.average_best_price) / 1e18; // 0.9569
const borrowAPR = (1 / avgPrice - 1) * (365 * 86400) / ttm;
// To borrow 10,000 USDC at price ≈ 0.9569 you sell ≈ 10,450 units
// At maturity you repay 10,450 USDC → net cost ≈ 450 USDC
const unitsToSell = Math.ceil(10000e6 / avgPrice);Step 2: Convert takeable offers to ABI-encoded data
The quote 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 viemStep 2: Get ABI-ready takeable offers
import { MidnightApi } from "@morpho-org/morpho-sdk/midnight-api";
const loanAssets = 10_000_000_000n; // 10,000 USDC (6 decimals)
const quote = await MidnightApi.fetchBookQuote({
marketId,
side: "bids",
assets: loanAssets,
slippage: "0.5",
});
const takes = quote.data.takeableOffers;
const borrowAPR =
(1 / (Number(quote.data.averageBestPrice) / 1e18) - 1) * (365 * 86400) / ttm;The SDK wraps the quote endpoint and uses Morpho's router by default with no config.
Execute the borrow
The Morpho SDK path derives the collateral (WETH) approval and any required MidnightBundlesV2 authorization, applies your rate guard, and builds an atomic supply-and-borrow transaction. The direct MidnightBundlesV2 call below is the manual equivalent. Hold sufficient collateral for the debt units owed at maturity (maxUnits plus any existing debt), sized at the market's LLTV.
Morpho SDK
Extend walletClient and hand the flow the quote from the previous step (SDK tab):
import { parseUnits, publicActions } from "viem";
import { Time } from "@morpho-org/morpho-ts";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";
const client = walletClient.extend(morphoViemExtension()).extend(publicActions);
const midnight = client.morpho.midnight(8453);
const block = await client.getBlock();
const marketData = await midnight.getMarketData(marketId, {
blockNumber: block.number,
});
const targetAssets = loanAssets;
const minimumPrice = BigInt(quote.data.averageWorstPrice);
const maxUnits = (targetAssets * 10n ** 18n + minimumPrice - 1n) / minimumPrice;
// Minimum WETH = (maxUnits + existing debt units) × loan-token price / (WETH price × LLTV); add a safety buffer (5 WETH in this example).
const collateralAssets = parseUnits("5", 18);
const action = midnight.supplyCollateralTakeBorrow({
accountAddress: taker,
marketData,
collateralSupplies: [{ collateralIndex: 0n, assets: collateralAssets }],
target: { type: "assets", assets: targetAssets, maxUnits },
takeableOffers: quote.data.takeableOffers,
deadline: Time.timestamp() + 600n,
});
const requirements = await action.getRequirements();
// Fulfill the WETH 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 the borrow-only takeBorrow.
Raw contract call
For a direct integration, call MidnightBundlesV2. 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 collateral-token ERC-20 approval to MidnightBundlesV2 separately, then authorize it on the Midnight contract.
import { erc20Abi, maxUint256, zeroAddress } 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 WETH = "0x4200000000000000000000000000000000000006";
await walletClient.writeContract({
account: taker,
address: WETH,
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 sell (i.e. borrow), a price guard, the collateral you want to post, and the call's arguments.
import { parseUnits, zeroAddress } from "viem";
import { midnightBundlesV2Abi } from "@morpho-org/morpho-sdk/abis";
const collateralSupplies = [{ collateralIndex: 0n, assets: parseUnits("5", 18) }];
const targetSellerAssets = loanAssets;
const minimumPrice = BigInt(quote.average_worst_price);
const maxUnits = (targetSellerAssets * 10n ** 18n + minimumPrice - 1n) / minimumPrice;
// Minimum WETH = (maxUnits + existing debt units) × loan-token price / (WETH price × LLTV); add a safety buffer (5 WETH in this example).
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: "midnightBundlesV2SupplyCollateralAndSellWithAssetsTarget",
args: [
market, // market
targetSellerAssets, // targetSellerAssets
maxUnits, // maxUnits
false, // reduceOnly
taker, // receiver
collateralSupplies, // collateralSupplies
offerFills, // offerFills
0n, // referralFeePct
zeroAddress, // referralFeeRecipient
BigInt(Math.floor(Date.now() / 1000) + 600), // deadline
zeroAddress, // wrappedNative, unused when msg.value is 0
],
});The rate guard derives minimumPrice from quote.average_worst_price and uses it to calculate maxUnits. Collateral sizing uses the debt units owed at maturity (maxUnits plus any existing debt) at the market's LLTV to find the minimum, then adds a safety buffer; this example supplies 5 WETH. collateralIndex refers to the collateral's position in the market's collateralParams.