Leverage loop (simulate)
POST/v1/actions/loop/leverage
Open a leveraged position and simulate post-trade state. Same parameters as GET. Optionally send a JSON body with current portfolio state (balanceData, aprData, positions) to receive projected post-trade metrics in the simulation field — if omitted, the API fetches balances on-chain automatically. Use the data returned by the user-positions endpoint directly — always include positions for accurate health-factor and borrow-capacity projections.
Plain-text reference — POST /v1/actions/loop/leverage
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
route | query | auto, bundler3, native, composer, proxy | no | Which assembly to use, for lenders that have more than one. Morpho Blue only today. |
- (omitted) —
auto: prefer the protocol’s own tooling and fall back to the composer for anything it cannot express (a cross-market pair, a margin paid in a foreign asset, anEXACT_OUTPUTclose, a chain with no bundler3 or no swap adapter). bundler3/native— force Morpho’s bundler3. A request it cannot serve becomes an ERROR instead of silently falling back, which is what makes the route testable.composer/proxy— force the 1delta composer.
The default matters: the bundler3 route asks for setAuthorization(GeneralAdapter1),
Morpho’s own audited adapter, where the composer route asks for a standing grant on ours. |
| marketUidIn | query | string | yes | Market identifier for input side (lender:chainId:address). |
| marketUidOut | query | string | yes | Market identifier for output side (lender:chainId:address). |
| slippage | query | number | yes | Slippage tolerance (basis points) |
| account | query | string | no | Account address. Include to build transaction, omit for quote-only. |
| debtAmount | query | string | yes | Debt amount in wei |
| payAsset | query | string | no | Asset to pay with (optional zap-in asset) |
| payAmount | query | string | no | Pay amount in wei |
| marginFromAccountId | query | integer | no | Dolomite only. Fund the margin from this existing Dolomite sub-account
instead of the wallet, collapsing the open to ONE transaction: the transfer
rides inside the leverage call and no deposit or token approval is emitted.
Omit to pull payAmount from the wallet (two transactions — Dolomite's
trader proxy can only move funds between the caller's own sub-accounts).
0 is the default sub-account and a valid source. Passing the trade
account's own id is treated as absent, since the margin is already there.
The source must hold payAmount of the pay asset or the transaction reverts. |
| leverage | query | number | no | Target leverage multiplier |
| usePendleMintRedeem | query | boolean | no | Use Pendle mint/redeem |
| borrowMode | query | 0, 1, 2 | no | Borrow mode (0=NONE, 1=STABLE, 2=VARIABLE) |
| termId | query | integer | no | Lista DAO fixed-term (brokered) debt markets only. Selects the fixed term to borrow for
the loop's debt leg, from the debt market's terms[] rate card (the MarketTerm.termId).
Required when marketUidIn is a brokered market — the borrow leg routes through the broker;
ignored otherwise. |
| posId | query | string | no | Twyne only. Which collateral vault to lever — a Twyne position is a
contract the borrower deploys, and one account may own several in a market,
each with its own collateral, debt and liquidation LTV. Omit when the account
owns exactly one (or none). Passing none while owning several is refused
rather than guessed. |
| liqLtv | query | integer | no | Twyne only, and only when the account owns NO vault in this market yet.
The liquidation LTV (1e4 scale) the position this call DEPLOYS will carry.
The deploy runs as the first item of the same EVC batch, so a first-time
borrower opens a leveraged position in one transaction; the response then
carries createdPosition and a collateralVault address that does not exist
until the transaction lands.
Defaults to the market’s LIVE ceiling (maxTwyneLTVs) — the extra LTV is the
reason to borrow here, and the value is not a commitment: setTwyneLiqLTV
moves it at any time with no cooldown, and lowering it RELEASES reserved
credit and lowers what the borrower pays. Must sit inside the live band
(externalLiqLTV × externalLiqBuffer … maxTwyneLTVs); both bounds move, so
a value is validated against a fresh read and refused by name rather than
reverting ValueOutOfRange() on chain.
Ignored when a position already exists — moving an existing dial is
/v1/actions/twyne/set-liq-ltv. |
| accountId | query | string | no | Account ID (Init Capital) |
| selectedMode | query | integer | no | Position mode for new positions (Init Capital) |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
balanceData | object | yes | Aggregated balance data for a sub-account. |
balanceData.deposits | number | no | Total deposits in USD |
balanceData.debt | number | no | Total debt in USD |
balanceData.adjustedDebt | number | no | Debt adjusted for borrow factors |
balanceData.collateral | number | no | Collateral value in USD |
balanceData.collateralAllActive | number | no | Collateral if all assets were enabled |
balanceData.borrowDiscountedCollateral | number | no | Collateral discounted by borrow factors |
balanceData.borrowDiscountedCollateralAllActive | number | no | Discounted collateral if all enabled |
balanceData.nav | number | no | Net asset value (deposits - debt) |
balanceData.deposits24h | number | no | Deposits 24h ago (for change calculation) |
balanceData.debt24h | number | no | Debt 24h ago |
balanceData.nav24h | number | no | NAV 24h ago |
balanceData.rewards | object[] | no | Pending reward token claims. Each entry represents a single reward program. |
balanceData.rewards[].asset | string | no | Reward token contract address |
balanceData.rewards[].totalRewards | number | no | Total accumulated rewards (token units) |
balanceData.rewards[].claimableRewards | number | no | Immediately claimable rewards (token units) |
aprData | object | yes | APR breakdown for a sub-account. |
aprData.apr | number | no | Net APR (deposit - borrow) |
aprData.depositApr | number | no | Weighted deposit APR |
aprData.borrowApr | number | no | Weighted borrow APR |
aprData.rewardApr | number | no | Total reward APR |
aprData.rewardDepositApr | number | no | Reward APR on deposits |
aprData.rewardBorrowApr | number | no | Reward APR on borrows |
aprData.intrinsicApr | number | no | Intrinsic yield APR (e.g., stETH staking) |
aprData.intrinsicDepositApr | number | no | Intrinsic yield APR portion from deposits |
aprData.intrinsicBorrowApr | number | no | Intrinsic yield APR portion from borrows |
aprData.rewards | object | no | Per-reward-token APR breakdown. Keys are reward token addresses. |
modeId | string | no | Mode/config key from userConfig.selectedMode (defaults to "0") |
positions | object[] | no | Current lending positions from the matching sub-account's positions array. The full LendingPosition objects returned by user-positions are accepted — only the fields in SimulationPosition are used. Always include this for accurate health-factor and borrow-capacity projections. |
positions[].marketUid | string | yes | Unique market identifier (format: {lender}:{chainId}:{address}) |
positions[].depositsUSD | number | yes | Deposit amount in USD |
positions[].debtUSD | number | yes | Variable debt in USD |
positions[].debtStableUSD | number | yes | Stable debt in USD |
positions[].collateralEnabled | boolean | yes | Whether this asset is enabled as collateral |
Response 200
| Field | Type | Description |
|---|---|---|
success | True | |
data | object | Quotes and simulation results |
data.lender | string | Protocol identifier. See the LenderId schema. |
data.quotes | object[] | Candidate routes, best output first. Execute exactly one. |
data.quotes[].deltas | object | |
data.quotes[].deltas.aggregator | string | Aggregator source |
data.quotes[].deltas.tradeInput | number | Trade input amount |
data.quotes[].deltas.tradeOutput | number | Trade output amount |
data.quotes[].deltas.deltas | object | Balance deltas |
data.quotes[].rateImpact | object[] | Projected interest-rate impact per market for THIS quote (its own trade amounts). Omitted if IRM data is unavailable. |
data.quotes[].rateImpact[].marketUid | string | Market identifier (format: lender:chainId:address) |
data.quotes[].rateImpact[].utilization | object | A current/projected pair for a single rate metric. |
data.quotes[].rateImpact[].borrowRate | object | A current/projected pair for a single rate metric. |
data.quotes[].rateImpact[].depositRate | object | A current/projected pair for a single rate metric. |
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.simulation | object | Projected post-trade metrics, or null if simulation failed |
data.simulation.pre | object | Portfolio state before the trade. |
data.simulation.pre.healthFactor | number | Health factor before the trade (null-safe: capped at 1e18 when no debt) |
data.simulation.pre.borrowCapacity | number | Borrow capacity (USD) before the trade |
data.simulation.post | object | Projected portfolio state after the trade. |
data.simulation.post.healthFactor | number | Projected health factor after the trade |
data.simulation.post.borrowCapacity | number | Projected borrow capacity (USD) after the trade |
data.simulation.post.balanceData | object | Aggregated balance data for a sub-account. |
data.simulation.post.aprData | object | APR breakdown for a sub-account. |
data.simulationError | string | Error message if simulation failed |
data.lender | string | Protocol identifier. See the LenderId schema. |
data.quotes | object[] | Candidate routes, best output first. Execute exactly one. |
data.quotes[].deltas | object | |
data.quotes[].deltas.aggregator | string | Aggregator source |
data.quotes[].deltas.tradeInput | number | Trade input amount |
Example response
{
"success": true,
"data": {
"lender": "AAVE_V3",
"quotes": [
{
"deltas": {
"aggregator": "string",
"tradeInput": 1.0,
"tradeOutput": 1.0,
"deltas": {}
},
"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
}
}
]
}
],
"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
}
}
],
"simulation": {
"pre": {
"healthFactor": 1.85,
"borrowCapacity": 3000
},
"post": {
"healthFactor": 2.1,
"borrowCapacity": 3500,
"balanceData": {
"deposits": 10000.5,
"debt": 5000.25,
"adjustedDebt": 5500,
"collateral": 9000,
"collateralAllActive": 10000.5,
"borrowDiscountedCollateral": 8000,
"borrowDiscountedCollateralAllActive": 9000,
"nav": 5000.25,
"deposits24h": 9800,
"debt24h": 4900,
"nav24h": 4900,
"rewards": [
{
"asset": "0xc00e94Cb662C3520282E6f5717214004A7f26888",
"totalRewards": 12.5,
"claimableRewards": 12.5
}
]
},
"aprData": {
"apr": 2.5,
"depositApr": 3.5,
"borrowApr": 5.2,
"rewardApr": 1.2,
"rewardDepositApr": 0.8,
"rewardBorrowApr": 0.4,
"intrinsicApr": 0,
"intrinsicDepositApr": 0,
"intrinsicBorrowApr": 0,
"rewards": {}
}
}
},
"simulationError": "string"
},
"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
Quote or full build with simulation
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.