Docs

Exit a borrow early

Exit a Midnight borrow before maturity by buying units from offers, quoting and authorizing a full or partial repayment, and releasing collateral.

Snippets assume a viem wallet client walletClient for your account on Base. Set marketId to the selected market ID and taker to walletClient.account.address.

Read the position

The API and SDK provide the indexed position and current market state. For a full exit, cancel any of your active reduce-only buy offers first, then refresh the position: units committed to those offers are reserved offchain and a direct MidnightBundlesV2 call cannot see that reservation.

REST API

Request the position by market ID and user address:

GET https://api.morpho.org/v0/midnight/markets
  /{marketId}/users/{userAddress}/position

// Response
{
  "data": {
    "chain_id":           8453,
    "market_id":          "0xd92de5e7fbb...7614",               // market the borrower borrowed from
    "user_address":       "0xYourWalletAddress",
    "last_indexed_block": "47457420",
    "loan_token":         "0x8335...2913",                      // USDC
    "maturity":           "1798761600",
    "type":               "borrow",                             // "lend" | "borrow" | "collateral_only"
    "credit":             "0",
    "debt":               "10527000000",                        // debt units to retire
    "pending_fee":        "0",
    "last_loss_factor":   "0",
    "loss_factor":        "0",
    "collaterals": [                                            // each entry is { token, amount }
      {
        "token":  "0x4200000000000000000000000000000000000006", // WETH - withdrawable in the same call
        "amount": "5000000000000000000"                         // 5 WETH (18 dec)
      }
    ]
  }
}

// Inspection only - size the transaction from the SDK or direct contract read below.

Morpho SDK

import { publicActions } from "viem";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";
import { midnightAbi } from "@morpho-org/morpho-sdk/abis";

// Extend the wallet client with the `morpho` namespace and the public reads
// the SDK needs.
const client = walletClient.extend(morphoViemExtension()).extend(publicActions);
const midnight = client.morpho.midnight(8453); // Midnight entity for Base
const MIDNIGHT = "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A"; // Midnight on Base (from the @morpho-org/morpho-ts address registry)

// The extended client doubles as the public client the execute snippet reads with.
const publicClient = client;

const block = await client.getBlock();
const positionData = (
  await midnight.getPositionData({
    marketId,
    accountAddress: taker,
    parameters: { blockNumber: block.number },
  })
).accrueInterest(block.timestamp);

const totalDebtUnits = positionData.debt;

// This full-exit example assumes there are no active reduce-only buy offers.
// Cancel any such offers and refresh positionData before using totalDebtUnits.
// If offers remain active, subtract their committed units from targetUnits and
// do not withdraw all collateral.
const targetUnits = totalDebtUnits;

// positionData.collateral[i] is the live collateral amount at index i of
// market.collateralParams - the same value the execute snippet reads with
// `collateral(marketId, taker, i)`.

Onchain read

You can also read the current debt from Midnight directly. For a full exit, cancel your active reduce-only buy offers and refresh this read first. The direct contract does not account for those offchain reservations.

import { createPublicClient, http } from "viem";
import { base } from "viem/chains";
import { midnightAbi } from "@morpho-org/morpho-sdk/abis";

const publicClient = createPublicClient({ chain: base, transport: http() });
const MIDNIGHT = "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A"; // Midnight on Base (from the @morpho-org/morpho-ts address registry)
const totalDebtUnits = await publicClient.readContract({
  address: MIDNIGHT,
  abi: midnightAbi,
  functionName: "debt",
  args: [marketId, taker],
});

// This is the full debt only when the borrower has no active reduce-only buy offers.
// Cancel them and refresh the read. Otherwise, subtract committed units from
// targetUnits and do not withdraw all collateral.
const targetUnits = totalDebtUnits;

Get a quote

The quote gives the best borrow-side offers for the requested debt units. Use a units target for both a full exit and a partial exit.

REST API

# Full exit, sized in debt units
GET https://api.morpho.org/v0/midnight/books/{marketId}/asks/quote?units=10527000000&slippage=0.5&limit=10000

# For a full-collateral exit, cancel this borrower's active reduce-only buy offers,
# then refresh the position and quote. Otherwise, target only available debt and
# do not withdraw all collateral.

{
  "data": {
    "average_best_price": "985072900000000000",
    "average_worst_price": "989981900000000000",
    "available_assets": "10838123858",
    "available_units": "11000000000",
    "takeable_offers": [
      {
        "market_id": "0xd92de5e7fbb00000000000000000000000000000000000000000000000000000",
        "units": "10527000000",
        "offer": {
          "buy": false,
          "maker": "0x2222222222222222222222222222222222222222",
          "tick": 4212,
          "reduce_only": false
        },
        "ratifier_data": "0x"
      }
    ]
  }
}

# Partial exit, sized in debt units
GET https://api.morpho.org/v0/midnight/books/{marketId}/asks/quote?units=5000000000&slippage=0.5&limit=10000

The average worst price is the conservative bound returned for the quote, not an echo of the caller's requested slippage. Available assets and units can include fallback liquidity beyond the requested target.

Interpreting the quote: Compare average_best_price and average_worst_price to estimate the offered price range. Set acceptablePrice no higher than WAD-scaled par and cap maxBuyerAssets with ceil(targetUnits * acceptablePrice / WAD); quote availability totals do not set the acceptable price.

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

const partialTargetUnits = parseUnits("5000", 6);
const fullQuote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  units: targetUnits,
  slippage: "0.5",
});
const partialQuote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  units: partialTargetUnits,
  slippage: "0.5",
});

const WAD = 10n ** 18n;
const acceptablePrice = WAD;
const maxBuyerAssets =
  (targetUnits * acceptablePrice + WAD - 1n) / WAD;
const partialMaxBuyerAssets =
  (partialTargetUnits * acceptablePrice + WAD - 1n) / WAD;
const fullTakes = fullQuote.data.takeableOffers;
const partialTakes = partialQuote.data.takeableOffers;
// Both arrays are ABI-ready for the SDK action and direct `MidnightBundlesV2` call.
// MidnightApi uses Morpho's router by default; no config is required.

Approve and authorize

MidnightBundlesV2 pulls loan tokens from the borrower to pay for the units, then withdraws collateral. The borrower must:

  1. Approve MidnightBundlesV2 to pull the loan token (USDC), sized to the maxBuyerAssets cap for either exit. The units-target call refunds unused assets.
  2. Authorize MidnightBundlesV2 on the Midnight contract (one-time).
import { erc20Abi, parseUnits } 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";
const WAD = 10n ** 18n;
const acceptablePrice = WAD;
const partialTargetUnits = parseUnits("5000", 6);
const maxBuyerAssets =
  (targetUnits * acceptablePrice + WAD - 1n) / WAD;
const partialMaxBuyerAssets =
  (partialTargetUnits * acceptablePrice + WAD - 1n) / WAD;

if (BigInt(fullQuote.data.availableUnits) < targetUnits) {
  throw new Error("Ask quote does not cover the full debt");
}

const approvalAmount = maxBuyerAssets; // Use partialMaxBuyerAssets for a partial exit.
const approvalHash = await walletClient.writeContract({
  account: taker,
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [MIDNIGHT_BUNDLES_V2, approvalAmount],
});
const approvalReceipt = await publicClient.waitForTransactionReceipt({
  hash: approvalHash,
});
if (approvalReceipt.status !== "success") {
  throw new Error("Loan-token approval failed");
}

const authorizationHash = await walletClient.writeContract({
  account: taker,
  address: MIDNIGHT,
  abi: midnightAbi,
  functionName: "setIsAuthorized",
  args: [MIDNIGHT_BUNDLES_V2, true, taker],
});
const authorizationReceipt = await publicClient.waitForTransactionReceipt({
  hash: authorizationHash,
});
if (authorizationReceipt.status !== "success") {
  throw new Error("Midnight authorization failed");
}

Execute the exit

Pick a full exit (retire all debt) or a partial exit (retire a chosen amount of debt). Both calls can release collateral in the same transaction: collateralWithdrawals lists what to withdraw and collateralReceiver where it goes. An empty list withdraws nothing. Withdrawal is bounded by the debt left after the take: Midnight re-checks the position against its LLTV at withdrawal and reverts if it would go unhealthy.

Execute through the Morpho SDK

Morpho SDK 7.0.0 provides the reduce-only takeRepayWithdrawCollateral flow. Its one-hour deadline bounds both the offer take and the collateral withdrawal.

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

const block = await client.getBlock();
const marketData = await midnight.getMarketData(marketId, {
  blockNumber: block.number,
});
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
const WAD = 10n ** 18n;
const acceptablePrice = WAD;
const collateralWithdrawals = positionData.collateral
  .map((assets, index) => ({
    collateralIndex: BigInt(index),
    assets: assets > 0n ? maxUint256 : 0n,
  }))
  .filter(({ assets }) => assets > 0n);
const fullQuote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  units: targetUnits,
  slippage: "0.5",
});
if (BigInt(fullQuote.data.availableUnits) < targetUnits) {
  throw new Error("Ask quote does not cover the full debt");
}
const maxBuyerAssets =
  (targetUnits * acceptablePrice + WAD - 1n) / WAD;
const output = midnight.takeRepayWithdrawCollateral({
  accountAddress: taker,
  marketData,
  target: { type: "units", units: targetUnits, maxBuyerAssets },
  takeableOffers: fullQuote.data.takeableOffers,
  repayEnabled: false,
  collateralWithdrawals,
  maxContinuousFee: maxUint256,
  deadline,
});

for (const requirement of await output.getRequirements()) {
  const hash = await client.sendTransaction(requirement);
  await client.waitForTransactionReceipt({ hash });
}

const hash = await client.sendTransaction(output.buildTx());
console.log("Exit tx:", hash);

See Taker flows for the full SDK surface.

Full exit: retire all debt

This full-exit example targets all debt units after canceling the borrower's active reduce-only buy offers and refreshing the read. The quote must cover targetUnits; maxBuyerAssets is the par ceiling for that amount, not the quote's availability total. The MidnightBundlesV2 contract refunds unused assets. With repayEnabled: false, it does not repay an unfilled remainder.

import { maxUint256, publicActions, zeroAddress } from "viem";
import { MidnightApi } from "@morpho-org/morpho-sdk/midnight-api";
import { midnightAbi, midnightBundlesV2Abi } from "@morpho-org/morpho-sdk/abis";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";

const client = walletClient.extend(morphoViemExtension()).extend(publicActions);
const midnight = client.morpho.midnight(8453);
const publicClient = client;

const MIDNIGHT_BUNDLES_V2 = "0x0bF30B530f816a619C3F28a3b9f44C441122555D"; // MidnightBundlesV2 on Base (from the @morpho-org/morpho-ts address registry)
const MIDNIGHT = "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A";
const marketData = await midnight.getMarketData(marketId);
const targetUnits = await publicClient.readContract({
  address: MIDNIGHT,
  abi: midnightAbi,
  functionName: "debt",
  args: [marketId, taker],
});
const fullQuote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  units: targetUnits,
  slippage: "0.5",
});
if (BigInt(fullQuote.data.availableUnits) < targetUnits) {
  throw new Error("Ask quote does not cover the full debt");
}
const WAD = 10n ** 18n;
const acceptablePrice = WAD;
const maxBuyerAssets =
  (targetUnits * acceptablePrice + WAD - 1n) / WAD;
const collateralBalances = await Promise.all(
  marketData.params.collateralParams.map((_, index) =>
    publicClient.readContract({
      address: MIDNIGHT,
      abi: midnightAbi,
      functionName: "collateral",
      args: [marketId, taker, BigInt(index)],
    }),
  ),
);
const collateralWithdrawals = collateralBalances.flatMap((assets, index) =>
  assets > 0n ? [{ collateralIndex: BigInt(index), assets: maxUint256 }] : [],
);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);

const hash = await walletClient.writeContract({
  account: taker,
  address: MIDNIGHT_BUNDLES_V2,
  abi: midnightBundlesV2Abi,
  functionName: "midnightBundlesV2BuyWithUnitsTargetAndWithdrawCollateral",
  args: [
    marketData.params, // market
    targetUnits, // targetUnits
    maxBuyerAssets, // maxBuyerAssets
    true, // reduceOnly
    false, // repayEnabled
    fullQuote.data.takeableOffers, // offerFills
    collateralWithdrawals, // collateralWithdrawals
    taker, // collateralReceiver
    0n, // referralFeePct
    zeroAddress, // referralFeeRecipient
    maxUint256, // maxContinuousFee
    deadline, // deadline
    zeroAddress, // wrappedNative, unused when msg.value is 0
  ],
});
console.log("Exit tx:", hash);

Partial exit: retire selected debt units

A partial exit retires a chosen amount of debt units rather than spending a preset loan-token amount.

import { maxUint256, parseUnits, publicActions, zeroAddress } from "viem";
import { MidnightApi } from "@morpho-org/morpho-sdk/midnight-api";
import { midnightBundlesV2Abi } from "@morpho-org/morpho-sdk/abis";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";

const client = walletClient.extend(morphoViemExtension()).extend(publicActions);
const midnight = client.morpho.midnight(8453);

const MIDNIGHT_BUNDLES_V2 = "0x0bF30B530f816a619C3F28a3b9f44C441122555D"; // MidnightBundlesV2 on Base (from the @morpho-org/morpho-ts address registry)
const targetUnits = parseUnits("5000", 6);
const WAD = 10n ** 18n;
const acceptablePrice = WAD;
const maxBuyerAssets =
  (targetUnits * acceptablePrice + WAD - 1n) / WAD;
const partialQuote = await MidnightApi.fetchBookQuote({
  marketId,
  side: "asks",
  units: targetUnits,
  slippage: "0.5",
});
if (BigInt(partialQuote.data.availableUnits) < targetUnits) {
  throw new Error("Ask quote does not cover the requested debt units");
}
const marketData = await midnight.getMarketData(marketId);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);

const hash = await walletClient.writeContract({
  account: taker,
  address: MIDNIGHT_BUNDLES_V2,
  abi: midnightBundlesV2Abi,
  functionName: "midnightBundlesV2BuyWithUnitsTargetAndWithdrawCollateral",
  args: [
    marketData.params, // market
    targetUnits, // targetUnits
    maxBuyerAssets, // maxBuyerAssets
    true, // reduceOnly
    false, // repayEnabled
    partialQuote.data.takeableOffers, // offerFills
    [], // collateralWithdrawals
    zeroAddress, // collateralReceiver
    0n, // referralFeePct
    zeroAddress, // referralFeeRecipient
    maxUint256, // maxContinuousFee
    deadline, // deadline
    zeroAddress, // wrappedNative, unused when msg.value is 0
  ],
});
console.log("Partial exit tx:", hash);

Exit at a chosen price

Instead of taking asks now, a borrower can publish a resting reduce-only buy offer at a chosen tick: it stays published until it is taken, cancelled or expires, and retires debt only when another user takes it at that price. A buy offer pulls loan tokens from the maker when taken, so the SDK maker flow needs a loan-token approval to Midnight.

On the SDK, buy-side maker offers are posted through makeRepay (the flow validates reduce-only buy: true offers and publishes through MidnightBundlesV2):

import { getChainAddress } from "@morpho-org/morpho-ts";
import { Offer } from "@morpho-org/midnight-sdk";
import { parseUnits } from "viem";

// `midnight`, `marketData`, `taker` and `publicClient` come from the execute step above.
const now = BigInt(Math.floor(Date.now() / 1000));
const priceRatifierV1 = getChainAddress(8453, "priceRatifierV1");
const offerExpiry = now + 604800n < marketData.params.maturity
  ? now + 604800n
  : marketData.params.maturity;
const offer = Offer.create({
  market: marketData.params,
  buy: true,
  maker: taker,
  tick: 5_000n,
  maxUnits: targetUnits,
  reduceOnly: true,
  expiry: offerExpiry,
  ratifier: priceRatifierV1,
});
const loanAssets = parseUnits("10000", 6);
const output = await midnight.makeRepay({
  accountAddress: taker,
  offers: { type: "priceV1", entries: [{ offer }] },
  loanToken: marketData.params.loanToken,
  loanAssets,
  deadline: now + 600n,
});

for (const requirement of await output.getRequirements()) {
  const hash = await walletClient.sendTransaction(requirement);
  await publicClient.waitForTransactionReceipt({ hash });
}
const hash = await walletClient.sendTransaction(output.buildTx());
console.log("Offer publication tx:", hash);

See Maker flows for the full pipeline (grouping, tree, ratification, atomic publication).