Skip to main content

Repay

GET 

/v1/actions/lending/repay

Build calldata for repaying a loan. Identify the market via marketUid (format: lender:chainId:address). Approval transactions are automatically filtered: if the user already has sufficient allowances, permissions (envelope) and permissionTxns (per-entry) will be empty. Pass simulate=true to include projected post-trade metrics. When on-chain debt data is available and amount covers the full debt, the API uses protocol-level max-repay mechanisms automatically. If isAll=true but amount is less than the debt, the API falls back to a partial repay to avoid reverts.

Lista DAO fixed-term (brokered) markets: pass loanId to target a specific loan (use the per-loan loanId from the user-positions response, or the type(uint128).max sentinel for the flexible/dynamic position). The broker repays interest-first plus an early-repayment penalty on not-yet-matured fixed loans and refunds any excess, so to fully close a loan fund outstanding + accruedInterest + earlyRepayPenalty from the per-loan term.

Morpho Midnight fixed-term markets (MORPHO_MIDNIGHT_<id>) — repaying to avoid liquidation. A Midnight loan is zero-coupon and fixed-maturity: you owe a static face value — the position's debt in loan-token units — repaid 1:1, which does not grow over time. So the amount to repay is fixed and known up front (the continuous + settlement fees accrue on the lender's side and are never added to your debt), and there is no early-repayment penalty (unlike Lista) — you may repay any time before maturity at face value.

A borrower faces two liquidation triggers; both are cleared by repaying:

  • Before maturity — health (LTV) based. As with any collateralised loan, if the collateral's oracle value falls enough to breach the market's LLTV, the position can be liquidated. Keep an LTV buffer, add collateral, or repay down.
  • At/after maturity — default. The fixed maturity (surfaced on params.market.maturity) is a hard deadline. Once it passes, an unrepaid loan is in default and can be liquidated regardless of health or LTV — being past-due is itself the trigger. This is the Midnight-specific risk to watch: mark the maturity date and fully repay before it, even if the position is comfortably collateralised.

Repay exactly the debt. Over-repaying reverts on-chain (there is no over-repay buffer — do not pad the amount, and isAll does not auto-size here), while under-repaying leaves a dust position that stays open and therefore still liquidatable after maturity. Set amount to the position's current debt (loan-token units) from /v1/data/lending/user-positions and approve exactly that to the Midnight core. To fully close and reclaim collateral, repay the debt and then withdraw the collateral via /v1/actions/lending/withdraw — collateral cannot be freed while any debt remains.

Teller markets (TELLER_<pool>): pass posId = the bidId to repay (from the user-positions term.loanId). A full repay (isAll, or an amount ≥ the amount owed) uses repayLoanFull which repays principal + interest and releases ALL the bid's collateral — Teller has no keep-collateral partial close. A partial repay (amount < owed) uses repayLoan and keeps the collateral escrowed. ⚠ Liquidation is TIME-based and aggressive: after the term you have only a short grace window (params.market.teller.paymentDefaultDuration, as low as 5 min) to roll over or repay — miss it and the loan DEFAULTS, and a liquidator can seize your ENTIRE collateral (not just the amount owed). Repay or roll over before the deadline; there is no price buffer.

Plain-text reference — GET /v1/actions/lending/repay

Parameters

ParameterInTypeRequiredDescription
marketUidquerystringyesMarket identifier (lender:chainId:address). The address encodes the underlying token (Aave/Morpho/CompoundV3), cToken (CompoundV2), or pool (Init Capital).
amountquerystringyesAmount in wei
modequeryproxy, directnoExecution mode. Omit it (recommended) and the API picks the path per request: direct (a raw protocol call) when the protocol can serve the request as given, otherwise proxy (the 1delta composer) — e.g. a custom receiver the protocol call cannot express, or a payAsset that needs wrapping. A payAsset that needs a swap routes through a DEX aggregator.

The transaction to is therefore NOT always the protocol contract: on the proxy path it is the composer, and the ERC-20 approval in actions.permissions targets the composer too. Send what the response returns.

Pass direct or proxy only to force a path; a forced path that cannot serve the request (for instance a custom receiver on a lender whose direct call has no recipient slot) is refused with 400 INVALID_PARAM, never built. | | operator | query | string | yes | Wallet address that signs and sends the transaction (and pays, for deposit/repay). Not necessarily the position owner — see receiver. | | receiver | query | string | no | Recipient of the lender position (deposit), of the underlying tokens (borrow/withdraw), or the account whose debt is repaid (repay). Defaults to operator when omitted — so an on-behalf deposit MUST pass this parameter; there is no onBehalf / onBehalfOf alias, and an unknown parameter name is ignored.

Custom-receiver support is per-lender. The API auto-selects the execution path:

Direct path supported

  • Aave V2/V3 — supply / withdraw honor to.
  • Aave V4 — deposit via GiverPM supplyOnBehalfOf. Requires the GiverPM to be curated in aave-v4-peripherals.json with a name containing "giver".
  • Compound V3 — supplyTo / withdrawTo (a borrow is a withdrawTo of the base asset).
  • Morpho Blue & Lista DAO ERC-20 markets — Morpho onBehalf (repay included).
  • Euler V2 ERC-20 vaults — ERC-4626 deposit(receiver), repay(assets, receiver). Collateral auto-enable is dropped for non-sub-account receivers.
  • Exactly — repay / repayAtMaturity take the borrower.
  • Silo V2/V3 ERC-20 — deposit(_, receiver, _).
  • Gearbox V3 passive-pool deposits — ERC-4626 deposit(_, receiver).
  • Fluid borrow & withdraw — operate(..., to_) outflow slot.
  • Fluid deposit to an existing NFT when accountId is supplied — pure-deposit operate() calls bypass Fluid's owner auth gate, and the API folds a pre-flight VaultFactory.ownerOf(nftId) == receiver check into the merged multicall (rejects with 400 INVALID_PARAM on mismatch, VALIDATION_FAILED if the RPC can't confirm ownership).

Routes to composer (proxy) path

  • Compound V2 family — direct mint is msg.sender-only.
  • Fluid fresh-open deposits — no accountId or accountId=0. The composer mints the new position NFT to itself, then transfers it to receiver via the encoder's nftReceiver slot.
  • Native-ETH vaults on Fluid (deposit / withdraw / repay) and the CompoundV2 family — the composer forwards msg.value to the vault's payable entrypoint. Other lenders' composer paths reject native asset and require wrapped-native as payAsset.

Refused with a receiver (the on-chain call has no recipient)

  • Gearbox V3 credit-side addCollateral — CA bound to operator — and Gearbox V3 borrow (the funds stay in the credit account).
  • Liquity V2 family — Stability Pool deposit / withdrawal (provideToSP / withdrawFromSP) and trove collateral withdrawal (withdrawColl pays the trove's own receiver).
  • Aave V4 native-gateway path — supplyAsCollateralNative has no recipient slot.

Unsupported in any single tx

  • Init Capital — position NFT swept to operator.
  • Euler V2 / Silo V2/V3 native deposit paths — router/orchestrator credits msg.sender.
  • Fluid repay — on-chain primitive permits it, direct handler doesn't expose a receiver slot yet (routes to proxy as a conservative default).

Repay with a receiver settles the RECEIVER's debt with the operator's funds. On Euler V2, Morpho Blue / Lista and Exactly, isAll=true with a receiver is refused: the amount owed would be read from the operator, not the receiver — pass an explicit amount.

When the lender cannot honor a custom receiver on the direct path and mode is omitted, the API routes the request through the composer automatically — the response then targets the composer (see mode). Forcing mode=direct with a custom receiver the direct call cannot express (Compound V2, Init, a Fluid fresh open) is refused with 400 INVALID_PARAM, and a leg with no recipient (above) with 400 ACTION_FAILED; a receiver is never silently replaced by the operator.

The optional simulation preview is computed for the operator, not the receiver: with a custom receiver it shows the position change on the operator's account. Read the receiver's position from the user-positions endpoint after execution. | | isAll | query | boolean | no | Withdraw/repay full balance.

When on-chain position data is available, the API compares amount against the actual balance to decide the effective mode:

  • If amount covers the full position the action is treated as "withdraw/repay all" regardless of this flag.
  • If isAll is true but amount is less than the position, the API falls back to a partial action with the provided amount to avoid on-chain reverts.

Without on-chain data, the flag is trusted as-is. | | lendingMode | query | string | no | Interest rate mode (0=NONE, 1=STABLE, 2=VARIABLE) | | loanId | query | string | no | Lista DAO fixed-term (brokered) markets only. Identifies which loan to repay.

Pass the loanId of the target loan from the user's positions (each per-loan position in the user-positions response carries loanId and a term object). To repay the flexible / dynamic position instead, pass the sentinel 340282366920938463463374607431768211455 (type(uint128).max).

The broker repays interest-first (plus an early-repayment penalty for not-yet-matured fixed loans) and refunds any excess to the caller, so to fully close a loan fund outstanding + accruedInterest + earlyRepayPenalty (all on the per-loan term).

Required on brokered repays; ignored for non-brokered lenders. | | payAsset | query | string | no | Asset to pay with.

Use the zero address to pay with native ETH on lenders whose vault accepts native debt repayment (e.g. Init Capital, Fluid native-debt vaults, CompoundV2 cETH markets).

Proxy mode: defaults to market asset. Native is forwarded as msg.value only when the lender supports it (e.g. Fluid, CompoundV2); otherwise the composer rejects. | | isShares | query | boolean | no | Amount is in shares (direct mode) | | accountId | query | string | no | Per-position identifier. Lender-specific:

  • Init Capital — account ID, required for borrow/withdraw/repay.
  • Euler V2 — sub-account index (0..255), defaults to 0.
  • Gearbox V3 — credit-account address (alias: creditAccount).
  • Dolomite — margin account number (uint256, 0 = the default cross-margin account), or iso:&lt;marketId&gt;:&lt;n&gt; for a position inside an isolation-mode VAULT (dGM, dsavETH, dGMX …). The vault, not the user, owns that DolomiteMargin account; n must be >= 100 for a borrow position and 0 is the vault's supply-only balance. Position ids come back in this form from /v1/data/lending/positions.
  • Fluid — position NFT id. Omit or pass 0 to mint a new position; supply an existing nftId to act on an existing position. Required for borrow/withdraw/repay; for deposit it unlocks the direct path with a custom receiver (deposit lands on the receiver-owned NFT after pre-flight ownerOf validation). | | slippage | query | number | no | Slippage tolerance in basis points (50 = 0.5%, 100 = 1%).

Required whenever the action has to price something:

  • Aggregator swap — when payAsset / receiveAsset differs from the market's underlying, the currency conversion is routed through a DEX aggregator and this bounds that swap.
  • Order-book take (Morpho Midnight) — bounds the worst execution price accepted while filling offers.

Ignored when neither applies (same-asset action on a pool lender). | | simulate | query | boolean | no | When true, the API fetches the operator's on-chain balances and returns projected post-trade metrics in the simulation field. The projection is always of the operator's account: with a custom receiver it does not show the receiver's position. Default: false. |

Response 200

FieldTypeDescription
successTrue
dataobjectInformational data (quotes, simulation results, etc.)
data.rateImpactobject[]Projected interest-rate impact per market. Single-market actions produce 1 entry; loop actions produce 2. Null if IRM data is unavailable.
data.rateImpact[].marketUidstringMarket identifier (format: lender:chainId:address)
data.rateImpact[].utilizationobjectA current/projected pair for a single rate metric.
data.rateImpact[].utilization.currentnumberCurrent value
data.rateImpact[].utilization.projectednumberProjected value after the action
data.rateImpact[].borrowRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].borrowRate.currentnumberCurrent value
data.rateImpact[].borrowRate.projectednumberProjected value after the action
data.rateImpact[].depositRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].depositRate.currentnumberCurrent value
data.rateImpact[].depositRate.projectednumberProjected value after the action
data.rateImpact[].intrinsicYieldnumberThe asset's own yield in percent (LST staking yield, RWA NAV growth, a Gearbox quota rate as a negative), size-invariant, ADDED to both sides: earned on a deposit, forgone on a borrow. Absent when the lender meta was not readable when the response was built — never defaulted to 0 in that case.
data.rateImpact[].depositRewardAprnumberSum of reward-stream APRs (%) on the deposit side. Absent with intrinsicYield.
data.rateImpact[].borrowRewardAprnumberSum of reward-stream APRs (%) rebated on the borrow side. Absent with intrinsicYield.
data.rateImpact[].effectiveDepositRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].effectiveDepositRate.currentnumberCurrent value
data.rateImpact[].effectiveDepositRate.projectednumberProjected value after the action
data.rateImpact[].effectiveBorrowRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].effectiveBorrowRate.currentnumberCurrent value
data.rateImpact[].effectiveBorrowRate.projectednumberProjected value after the action
data.rateImpact[].flatbooleanTrue when the market has no utilization curve to shift (Morpho Blue collateral, a fixed-rate broker): current === projected, taken from the published rates, and utilization is NaN. Present so the leg is not silently missing from a position preview.
data.rateImpactobject[]Projected interest-rate impact per market. Single-market actions produce 1 entry; loop actions produce 2. Null if IRM data is unavailable.
data.rateImpact[].marketUidstringMarket identifier (format: lender:chainId:address)
data.rateImpact[].utilizationobjectA current/projected pair for a single rate metric.
data.rateImpact[].utilization.currentnumberCurrent value
data.rateImpact[].utilization.projectednumberProjected value after the action
data.rateImpact[].borrowRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].borrowRate.currentnumberCurrent value
data.rateImpact[].borrowRate.projectednumberProjected value after the action
data.rateImpact[].depositRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].depositRate.currentnumberCurrent value
data.rateImpact[].depositRate.projectednumberProjected value after the action
data.rateImpact[].intrinsicYieldnumberThe asset's own yield in percent (LST staking yield, RWA NAV growth, a Gearbox quota rate as a negative), size-invariant, ADDED to both sides: earned on a deposit, forgone on a borrow. Absent when the lender meta was not readable when the response was built — never defaulted to 0 in that case.
data.rateImpact[].depositRewardAprnumberSum of reward-stream APRs (%) on the deposit side. Absent with intrinsicYield.
data.rateImpact[].borrowRewardAprnumberSum of reward-stream APRs (%) rebated on the borrow side. Absent with intrinsicYield.
data.rateImpact[].effectiveDepositRateobjectA current/projected pair for a single rate metric.
data.rateImpact[].effectiveDepositRate.currentnumberCurrent value
data.rateImpact[].effectiveDepositRate.projectednumberProjected value after the action

Example response

{
"success": true,
"data": {
"rateImpact": [
{
"marketUid": "AAVE_V3:8453:0x4200000000000000000000000000000000000006",
"utilization": {
"current": 1.0,
"projected": 1.0
},
"borrowRate": {
"current": 1.0,
"projected": 1.0
},
"depositRate": {
"current": 1.0,
"projected": 1.0
},
"intrinsicYield": 10.37,
"depositRewardApr": 1.0,
"borrowRewardApr": 1.0,
"effectiveDepositRate": {
"current": 1.0,
"projected": 1.0
},
"effectiveBorrowRate": {
"current": 1.0,
"projected": 1.0
},
"flat": true
}
]
},
"actions": {
"transactions": [
{
"chainType": "evm",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"alternatives": [
{
"chainType": "evm",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"permissions": [
{
"to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"type": "ERC20"
}
],
"signatures": [
{
"permitId": "string",
"kind": "erc2612",
"typedData": {},
"replaces": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"description": "string",
"unscoped": true,
"deadline": "string"
}
],
"signs": [
{
"purpose": "authorize-action",
"format": "eip712",
"typedData": {},
"message": "string",
"signer": "string",
"deadline": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"description": "string",
"submit": {
"method": "GET",
"endpoint": "string",
"params": {},
"body": {},
"signatureField": "string"
}
}
],
"permitSkipped": [
{
"replaces": "string",
"reason": "string"
}
]
}
}

Request​

Responses​

Transaction calldata and approvals for repay