Migrate position (cross-lender)
GET/v1/actions/loop/migrate
Move a whole debt + collateral position from one lender to another in a single flash-loan transaction, optionally converting ONE leg (collateral OR debt) via an aggregator swap.
Flow (no conversion): flash the debt → repay the source → withdraw the source collateral → deposit it to the target → borrow the debt from the target → repay the flash. Same-lender / different-market moves (e.g. Morpho market A→B, Aave V4 reserve→reserve) are supported.
Supported sources & targets: Aave V2/V3, Aave V4, Compound V3, Morpho Blue / Lista, Fluid (an existing position via NFT-custody, OR a brand-new position — see below), Gearbox V3 (existing credit account as source; a fresh credit account is opened in-callback as target), Euler V2 (accountId 0; collateral-share transfer through the call forwarder). Not supported: Dolomite (either side), Euler as a TARGET, non-Venus Compound V2 as a target, both legs converting at once, Fluid↔Fluid.
Fluid target — existing vs fresh: pass the position's NFT id in
accountIdto migrate INTO an existing Fluid position (delivered via the VaultFactory NFT-custody flow). Omit it (or pass 0) to open a brand-new Fluid position: the migrate emits a single dual-axis Fluidoperatethat supplies the withdrawn collateral, borrows the debt, and mints the new position NFT straight to you — no NFT custody, no target-side consent, and the transaction targets the composer (deltaCompose) normally.
Native ↔ wrapped-native (wrap conversion): a native debt — e.g. a Venus
vBNBborrow — migrates into a target lender's wrapped-native debt market (WBNB). On-behalf borrowing cannot be delegated for the native asset, so the target MUST be the wrapped form; a native TARGET debt is rejected. The builder flashes the wrapped form, unwraps it to repay the native source, and borrows the wrapped form on the target — no aggregator needed. Compound V2 / Venus can be a migration source but never a target (no borrow delegation). The collateral leg wraps BOTH ways — a native source collateral (e.g. Fluid's ETH slot) is wrapped to WETH before the target deposit, and a wrapped source collateral is unwrapped for a native-collateral target — again with no aggregator. Native collateral is only reachable as such a wrap pair; native → an unrelated ERC20 target is rejected.
Lista DAO markets: two per-market extras decide the shape. A brokered market (non-zero
broker) routes its debt leg through the fixed-term broker — passloanIdwhen it is the SOURCE (which loan to repay) andtermIdwhen it is the TARGET (which term to open); the target has no default because the broker's flexible borrow has no on-behalf variant. A market with acollateralProvider(e.g. the slisBNB markets) routes its collateral leg through that provider, which exposes noposition()getter — so a provider-gated SOURCE cannot use the withdraw-all sentinel and is sized from your live collateral balance instead (read server-side; passcollateralAmountto override).
Asset conversion (swap leg): ONE leg may change asset via an aggregator swap — either the collateral (
marketUidTargetCollateraluses a different underlying) or the debt (marketUidTargetDebt), not both. The route fetches the aggregator quote server-side and bakes the trade into the flash callback. A collateral swap is EXACT_INPUT of the withdrawn collateral → provide a concretecollateralAmount(orcollateralAmountHint) so the swap input lines up. A debt swap is EXACT_OUTPUT (buy exactly the source debt to repay, selling the flashed target debt). Tune the swap tolerance withslippage. Euler as a source still needs an off-chain eVault share-balance read via theprepareMigrateSDK.
Omit account for quote-only (returns data.quotes with price deltas). Include account to build full transaction calldata (populates actions with alternatives, transactions, and permissions).
Setup transactions (actions.permissions): returned only for the consents actually missing, to be executed BEFORE the migrate. These vary by lender pair — e.g. the source collateral withdrawal approval; Aave V4 Giver/Taker/Config Position-Manager authorizations + per-reserve borrow allowance + collateral-enable grant; Morpho setAuthorization; a Gearbox V3 SOURCE's setBotPermissions grant on the credit account; Euler collateral eVault share approval to the composer. Fluid legs need no separate permission (NFT custody — or, for a fresh open, the composer opening on your behalf — IS the authorization).
Delivery: the migrate transaction targets the composer (deltaCompose) normally, OR the Fluid VaultFactory.safeTransferFrom when a Fluid leg requires NFT custody — actions.transactions[0].to reflects this.
data.result (resulting position): a summary for the UI — from/to lenders with their collateral + debt assets (address/symbol/decimals/logo), the new position amounts (to.collateral.amount/amountUsd, to.debt.amount/amountUsd), netUsd, leverage, apr: { deposit, borrow, net } and healthFactor. The rates, liquidation threshold, decimals and prices behind those are resolved server-side from the target markets — the corresponding query params are overrides only. USD/symbol fields are best-effort (omitted when price/token metadata is unavailable). to.collateral.amount is present when the withdraw was sized server-side (Aave source) or passed explicitly.
Finding a target: GET /v1/actions/loop/migrate/targets returns the ranked destinations this endpoint accepts for a given position — support matrix, native ⇄ wrapped equivalence, un-openable fixed-term books and borrow liquidity all applied server-side.
Plain-text reference — GET /v1/actions/loop/migrate
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
marketUidSourceCollateral | query | string | yes | Source collateral market (lender:chainId:address). The position being moved FROM. |
marketUidSourceDebt | query | string | yes | Source debt market (lender:chainId:address). |
marketUidTargetCollateral | query | string | yes | Target collateral market (lender:chainId:address). The position being moved TO. May be a different lender and/or a different asset (collateral conversion). |
marketUidTargetDebt | query | string | yes | Target debt market (lender:chainId:address). May be a different lender and/or a different asset (debt conversion). |
debtAmount | query | string | yes | Live debt to migrate, in the debt asset’s wei. For a debt conversion this is the NEW-debt (flash/borrow) amount instead. The flash is sized a small buffer above this so the source repay clears in full (a residual would make the full collateral withdrawal revert). |
account | query | string | no | Account address. Include to build the transaction + permission setup; omit for a quote-only response. |
isMaxIn | query | boolean | no | Withdraw the full collateral balance from the source (default true for a same-asset move). Set false + collateralAmount for an exact-amount conversion so the swap input lines up. |
collateralAmount | query | string | no | Exact source collateral to withdraw, in wei. Required (with isMaxIn=false) for a collateral conversion so the withdrawn amount matches the swap input. |
accountId | query | string | no | Per-position id where the lender needs one: an existing Fluid NFT id (omit or 0 opens a BRAND-NEW Fluid position), Euler sub-account index (must be 0 — sub-accounts unsupported). Defaults to 0. |
loanId | query | string | no | Lista fixed-term broker SOURCE only: the loan posId to repay, or type(uint128).max for the flex/dynamic position. Required when the source debt market is brokered; ignored otherwise. The target borrow is keyed by term, not loanId. |
termId | query | integer | no | Lista fixed-term broker TARGET only: which term the migrated debt opens at. REQUIRED when the target debt market is brokered — the broker exposes no on-behalf flexible borrow, so there is no default; ignored otherwise. Mirrors the termId on /v1/actions/loop/open. |
irModeFrom | query | 0, 1, 2 | no | Aave interest mode of the source debt (repay). 2 = variable. |
irModeTo | query | 0, 1, 2 | no | Aave interest mode of the target debt (borrow). 2 = variable. |
slippage | query | string | no | Swap-leg slippage tolerance as a FRACTION (0.005 = 0.5%). Only used |
| when a leg is converted via an aggregator swap; ignored for same-asset / | ||||
| wrap moves. Defaults to 0.5%. |
⚠ This endpoint is the exception. Every other slippage in this API
is in BASIS POINTS (50 = 0.5%) — migrate takes a fraction, which is
100× smaller for the same tolerance. Sending 50 here would ask for
5000% slippage. |
| eModeTo | query | string | no | Risk-config category to read the TARGET collateral's liquidation threshold from (Aave-style e-modes publish one config per category). Defaults to 0 (no e-mode). Display only — it does not change the built transaction. |
| collateralDecimals | query | integer | no | Override. Target collateral decimals. Resolved server-side; pass only to overrule the published metadata (e.g. a market whose token-list decimals are known-wrong, which would mis-scale the data.result USD values). Never affects the on-chain amounts. |
| debtDecimals | query | integer | no | Override. TARGET debt decimals — the swap target’s when the debt converts, else the source’s. Resolved server-side; see collateralDecimals. |
| sourceDebtDecimals | query | integer | no | Override. SOURCE debt decimals, for the from-leg display when the debt is converted. Resolved server-side; defaults to debtDecimals. |
| depositApr | query | string | no | Override. Target collateral deposit APR as a FRACTION (0.05 = 5%), intrinsic yield folded in. Resolved server-side and reported as data.result.apr.deposit. |
| borrowApr | query | string | no | Override. Target debt borrow APR as a FRACTION (0.05 = 5%), intrinsic yield folded in — for a fixed-term target this is the term rate, not the 0% variable rate. Resolved server-side. |
| liqThreshold | query | string | no | Override. Target collateral liquidation threshold as a FRACTION (0.85 = 85%), driving data.result.healthFactor (= collateralUsd · liqThreshold / debtUsd; < 1 ⇒ liquidatable). Resolved server-side from the target market’s risk config — see eModeTo. |
| collateralPriceUsd | query | string | no | Override. USD price of the TARGET collateral for the display conversion. Resolved server-side, preferring the market’s own ORACLE price (what the lender liquidates against). |
| debtPriceUsd | query | string | no | Override. USD price of the TARGET debt for the display conversion. Resolved server-side; see collateralPriceUsd. |
Response 200
| Field | Type | Description |
|---|---|---|
success | True | |
data | object | Informational data (quotes, simulation results, etc.) |
data.lender | string | Protocol identifier |
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.lender | string | Protocol identifier |
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[].tx | object | An EVM transaction ready to sign and broadcast. Send to, data and value as-is; do not re-encode them. |
data.quotes[].tx.to | string | Target contract address |
data.quotes[].tx.data | string | Encoded calldata |
data.quotes[].tx.value | string | ETH value to send with the transaction |
data.quotes[].tx.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.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. |
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
}
}
]
},
"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 response
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.