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 onparams.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
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
marketUid | query | string | yes | Market identifier (lender:chainId:address). The address encodes the underlying token (Aave/Morpho/CompoundV3), cToken (CompoundV2), or pool (Init Capital). |
amount | query | string | yes | Amount in wei |
mode | query | proxy, direct | no | Execution mode. proxy = 1delta composer, direct = raw protocol |
operator | query | string | no | Wallet address of the user executing the action |
receiver | query | string | no | Recipient of the lender position (deposit) or underlying tokens (borrow/withdraw). |
Defaults to operator when omitted. |
Custom-receiver support is per-lender. The API auto-selects the execution path:
Direct path supported
- Aave V2/V3 —
supply/withdrawhonorto. - Aave V4 — deposit via GiverPM
supplyOnBehalfOf. Requires the GiverPM to be curated inaave-v4-peripherals.jsonwith a name containing "giver". - Compound V3 —
supplyTo/withdrawTo. - Morpho Blue & Lista DAO ERC-20 markets — Morpho
onBehalf. - Euler V2 ERC-20 vaults — ERC-4626
deposit(receiver). Collateral auto-enable is dropped for non-sub-account receivers. - 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
accountIdis supplied — pure-depositoperate()calls bypass Fluid's owner auth gate, and the API folds a pre-flightVaultFactory.ownerOf(nftId) == receivercheck into the merged multicall (rejects with 400INVALID_PARAMon mismatch,VALIDATION_FAILEDif the RPC can't confirm ownership).
Routes to composer (proxy) path
- Compound V2 family — direct
mintismsg.sender-only. - Fluid fresh-open deposits — no
accountIdoraccountId=0. The composer mints the new position NFT to itself, then transfers it toreceivervia the encoder'snftReceiverslot. - Native-ETH vaults on Fluid (deposit / withdraw / repay) and the CompoundV2 family — the composer forwards
msg.valueto the vault's payable entrypoint. Other lenders' composer paths reject native asset and require wrapped-native aspayAsset.
Direct-only branches that throw
- Gearbox V3 credit-side
addCollateral— CA bound to operator. - Aave V4 native-gateway path —
supplyAsCollateralNativehas 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).
When the lender cannot honor a custom receiver on the direct path, getTarget falls back to proxy automatically. Forcing mode=direct on an unsupported path either throws (Gearbox credit-side, Aave V4 spoke, the explicit Silo router check) or silently credits the operator instead (Compound V2, Init, and Fluid fresh-open). |
| 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
amountcovers the full position the action is treated as "withdraw/repay all" regardless of this flag. - If
isAllis true butamountis less than the position, the API falls back to a partial action with the providedamountto 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). - Fluid — position NFT id. Omit or pass
0to 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 customreceiver(deposit lands on the receiver-owned NFT after pre-flightownerOfvalidation). | |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/receiveAssetdiffers 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 user's on-chain balances and returns projected post-trade metrics in the simulation field. Default: false. |
Response 200
| Field | Type | Description |
|---|---|---|
success | True | |
data | object | Informational data (quotes, simulation results, etc.) |
data.transaction | object | An EVM transaction ready to sign and broadcast. Send to, data and value as-is; do not re-encode them. |
data.transaction.to | string | Target contract address |
data.transaction.data | string | Encoded calldata |
data.transaction.value | string | ETH value to send with the transaction |
data.transaction.description | string | Human-readable label. For alternatives, this is the aggregator name (e.g. "Paraswap"). For transactions, describes the setup action (e.g. "Switch e-mode to 1"). |
data.permissionTxns | object[] | Approval transactions that must be executed before the main transaction. Empty when the user already has sufficient allowances. |
data.permissionTxns[].to | string | Target contract address |
data.permissionTxns[].data | string | Encoded calldata |
data.permissionTxns[].value | string | ETH value |
data.permissionTxns[].description | string | Human-readable description of the approval (e.g. "Approve borrow for AAVE_V3", "Approve ERC20") |
data.permissionTxns[].spender | string | ERC-20 approve spender (x-chain permissions). Match it against the selected quote's approvalTarget — several bridges can share one spender, so do not match by description. |
data.rateImpact | object[] | Projected interest-rate impact per market. Single-market actions produce 1 entry; loop actions produce 2. Null if IRM data is unavailable. |
data.rateImpact[].marketUid | string | Market identifier (format: lender:chainId:address) |
data.rateImpact[].utilization | object | A current/projected pair for a single rate metric. |
data.rateImpact[].utilization.current | number | Current value |
data.rateImpact[].utilization.projected | number | Projected value after the action |
data.rateImpact[].borrowRate | object | A current/projected pair for a single rate metric. |
data.rateImpact[].borrowRate.current | number | Current value |
data.rateImpact[].borrowRate.projected | number | Projected value after the action |
data.rateImpact[].depositRate | object | A current/projected pair for a single rate metric. |
data.rateImpact[].depositRate.current | number | Current value |
data.rateImpact[].depositRate.projected | number | Projected value after the action |
data.transaction | object | An EVM transaction ready to sign and broadcast. Send to, data and value as-is; do not re-encode them. |
data.transaction.to | string | Target contract address |
data.transaction.data | string | Encoded calldata |
data.transaction.value | string | ETH value to send with the transaction |
data.transaction.description | string | Human-readable label. For alternatives, this is the aggregator name (e.g. "Paraswap"). For transactions, describes the setup action (e.g. "Switch e-mode to 1"). |
data.permissionTxns | object[] | Approval transactions that must be executed before the main transaction. Empty when the user already has sufficient allowances. |
data.permissionTxns[].to | string | Target contract address |
data.permissionTxns[].data | string | Encoded calldata |
data.permissionTxns[].value | string | ETH value |
data.permissionTxns[].description | string | Human-readable description of the approval (e.g. "Approve borrow for AAVE_V3", "Approve ERC20") |
data.permissionTxns[].spender | string | ERC-20 approve spender (x-chain permissions). Match it against the selected quote's approvalTarget — several bridges can share one spender, so do not match by description. |
data.rateImpact | object[] | Projected interest-rate impact per market. Single-market actions produce 1 entry; loop actions produce 2. Null if IRM data is unavailable. |
data.rateImpact[].marketUid | string | Market identifier (format: lender:chainId:address) |
data.rateImpact[].utilization | object | A current/projected pair for a single rate metric. |
data.rateImpact[].utilization.current | number | Current value |
data.rateImpact[].utilization.projected | number | Projected value after the action |
Example response
{
"success": true,
"data": {
"transaction": {
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
},
"permissionTxns": [
{
"to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}
],
"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
}
}
]
},
"actions": {
"transactions": [
{
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"alternatives": [
{
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string"
}
],
"permissions": [
{
"to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"data": "0x617ba037000000000000000000000000c02aaa39b2",
"value": "0",
"description": "string",
"spender": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}
]
}
}
Request
Responses
- 200
- 400
- 429
- 500
- 502
Transaction calldata and approvals for repay
Validation error
Rate limited. Unauthenticated callers share a per-IP budget; send an x-api-key header to lift it. Retry with exponential backoff.
Unexpected server error. Safe to retry with backoff.
An upstream data source or protocol origin failed (error.code is ORIGIN_FAILED). error.details carries the per-origin status. This is also what a missing or malformed required parameter currently returns, rather than a 400.