> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Lend and borrow with Yield.xyz

> Learn how to supply collateral, borrow, repay and withdraw on lending protocols with the Web SDK and the Yield.xyz integration.

Portal's Web SDK lets your users lend and borrow on Aave V3, Morpho Blue, SparkLend and Lista through the `portal.borrow.yieldXyz` API. This guide covers discovering lending markets, building supply, borrow, repay and withdraw actions, and signing, submitting and tracking them with the Portal wallet.

## Overview

With `portal.borrow.yieldXyz` you can:

* **Discover** the lending integrations (protocols) and their markets, with live rates, LTVs, liquidation thresholds and liquidity
* **Supply** collateral, **borrow** against it, **repay** debt and **withdraw** collateral
* **Execute** an action in one call: the SDK signs each transaction with the Portal wallet, submits it to Yield.xyz, waits for Yield.xyz to confirm it and fetches the next step until the action succeeds
* **Track** positions (supply and debt balances, health factor, LTV and available borrow), past actions and liquidations

The [Yield.xyz Borrow integration page](/integrations/Yield/yield-xyz-borrow#how-lending-and-borrowing-works) explains how lending and borrowing work: collateral, `maxLtv`, the health factor and liquidations. Every method except `executeAction` and `signAndSubmitAction` resolves with the Portal API response, wrapped as `{ data }`; those two resolve with the execution result itself. Chain ids are [CAIP-2](/resources/chain-id-formatting) everywhere (`eip155:8453` for Base).

## Prerequisites

Before using lend and borrow operations, ensure you have:

* A properly initialized Portal client with the iframe ready (`portal.onReady`; see [Getting started](./getting-started))
* An active wallet with the required token(s) **and gas** on the target network (see [Create a wallet](./create-a-wallet))
* Yield.xyz integration enabled in your Portal Dashboard (see [Yield.xyz Borrow](/integrations/Yield/yield-xyz-borrow))
* An `rpcConfig` entry for every chain you sign on. The default `rpcConfig` covers the [13 built-in chains](./getting-started#initializing-portal), including Ethereum, Base, Arbitrum One, Optimism, Polygon, Avalanche and Monad. An explicit `rpcConfig` replaces the defaults rather than extending them, so start from `buildDefaultRpcConfig` when you add a chain such as BNB Chain (`eip155:56`).

<Warning>
  Lending integrations are live on **mainnets only**: there are no Sepolia, Base Sepolia or Amoy markets. Every signed action moves real funds, so test with small amounts on an inexpensive chain such as Base. The examples below use Aave V3 USDC on Base.
</Warning>

The examples in this guide use a `portal` instance and the wallet's EVM address:

```typescript theme={null}
import Portal from '@portal-hq/web'

const portal = new Portal({
  apiKey: 'YOUR_PORTAL_CLIENT_API_KEY',
  // Without rpcConfig, Portal's gateway serves the 13 built-in chains, Base included.
})

// Read the address once the iframe is ready: getEip155Address() resolves with '' instead of throwing
// when no address is available yet.
const address = await new Promise<string>((resolve, reject) => {
  portal.onReady(() => portal.getEip155Address().then(resolve, reject))
})
if (!address) throw new Error('No EVM address found')
```

For account abstraction enabled clients, this is the smart-account address. Pass it as `address` in every Borrow call.

## Discovering integrations

`getIntegrations` lists the lending protocols, the networks each supports and the actions each exposes. `getIntegration` returns one integration, including the JSON schema of every action's `args`.

```typescript theme={null}
const { data: integrations } = await portal.borrow.yieldXyz.getIntegrations()

for (const integration of integrations) {
  console.log(
    integration.id, // e.g. 'aave-borrow'
    integration.networks, // e.g. ['eip155:1', 'eip155:8453', ...]
    integration.actions.map((action) => action.id), // e.g. ['supply', 'borrow', 'repay', 'withdraw', ...]
  )
}

const { data: aave } = await portal.borrow.yieldXyz.getIntegration('aave-borrow')
const supplyArgsSchema = aave.actions.find((action) => action.id === 'supply')?.schema
console.log(supplyArgsSchema)
```

Integration ids look like `aave-borrow`, `morpho-blue-borrow`, `sparklend-borrow` and `lista-borrow`; use the ids `getIntegrations` returns rather than a fixed list. `networks` holds CAIP-2 ids for the chains Portal supports and Yield.xyz network names for the others; only the CAIP-2 entries can be passed as `network`.

## Discovering markets

`getMarkets` lists lending markets and `getMarket` returns one by id. Pass `scope: 'all'`: when `scope` is omitted, Yield.xyz lists only the markets enabled for your project, and the Yield.xyz dashboard does not yet support enabling lending markets, so that list is empty.

```typescript theme={null}
const { data: page } = await portal.borrow.yieldXyz.getMarkets({
  integrationId: 'aave-borrow',
  network: 'eip155:8453',
  scope: 'all',
  limit: 50,
})

for (const market of page.items) {
  console.log(market.id, market.loanToken.symbol, market.borrowRate, market.availableLiquidity)
}

const { data: market } = await portal.borrow.yieldXyz.getMarket(
  'aave-borrow-base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
)
for (const { token, maxLtv, liquidationThreshold } of market.collateralTokens) {
  console.log(`${token.symbol}: max LTV ${maxLtv}, liquidation threshold ${liquidationThreshold}`)
}
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `integrationId` | `string` | No | Only markets of this integration. |
| `network` | `string` | No | CAIP-2 chain id, for example `eip155:8453`. |
| `scope` | `'enabled' \| 'all'` | No | `all` lists every market Yield.xyz supports; `enabled` (what Yield.xyz applies when omitted) lists only the markets enabled for your project. |
| `offset` | `number` | No | Pagination offset. |
| `limit` | `number` | No | Page size, at most `100`. |

`getMarkets` resolves with `{ data: { total, offset, limit, items } }`, where each item is a `YieldXyzBorrowMarket` with `loanToken`, `collateralTokens[]` (`maxLtv`, `liquidationThreshold`, `liquidationPenalty`, `supplyRate`), `borrowRate`, `availableLiquidity`, `utilizationRate`, `isBorrowEnabled`, `minLoan` and the fee settings. See the [API reference](/api-reference/yieldxyz-borrow/list-lending-markets) for every field.

Treat market ids as opaque values returned by `getMarkets`: they embed the Yield.xyz network slug, not the CAIP-2 chain id, so do not build them yourself. Many markets hold little liquidity, so filter on `availableLiquidity` before showing a market to users.

## Supplying collateral

Every lending action takes two calls:

1. `createAction` builds the action and returns its unsigned transactions and the predicted impact on the position. Nothing is signed.
2. `signAndSubmitAction` signs each transaction with the Portal wallet, submits it to Yield.xyz and tracks the action until it succeeds (see [Signing and execution](#signing-and-execution)).

```typescript theme={null}
const { data: action } = await portal.borrow.yieldXyz.createAction({
  integrationId: 'aave-borrow',
  action: 'supply',
  address,
  args: {
    marketId: 'aave-borrow-base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
    tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
    amount: '5',
  },
})

// Show the predicted impact before the user signs. A `null` health factor means unknown, never safe.
console.log('Predicted health factor:', action.metadata?.predictedHealthFactor ?? 'unknown')
console.log('Predicted LTV:', action.metadata?.predictedLtv ?? 'unknown')
console.log(action.transactions.map((transaction) => transaction.type)) // e.g. ['APPROVAL', 'SUPPLY']

const result = await portal.borrow.yieldXyz.signAndSubmitAction(action)
console.log(result.status, result.transactions.map((transaction) => transaction.transactionHash))
```

`createAction` takes:

| Parameter | Type | Required | Description |
| - | - | - | - |
| `integrationId` | `string` | Yes | Integration id from `getIntegrations`. |
| `action` | `YieldXyzBorrowActionType` | Yes | `supply`, `borrow`, `repay`, `withdraw`, `enableCollateral`, `disableCollateral` or `supplyAndBorrow`. |
| `address` | `string` | Yes | The wallet's EVM address (the smart-account address for account abstraction enabled clients). The SDK signs only actions built for the Portal wallet's address. |
| `args` | `YieldXyzBorrowActionArguments` | Yes | The action's arguments; always includes `marketId`. |

The most common `args`:

| Field | Description |
| - | - |
| `marketId` | Market id, exactly as `getMarkets` returned it. Always required. |
| `amount` | Human-readable amount, for example `'1.5'`. Provide either `amount` or `amountRaw`. |
| `amountRaw` | Amount in raw token units. |
| `repayAll` | `repay` only: repay the whole debt. Omit `amount` and `amountRaw`. |
| `tokenAddress` | Required for `supply` and `withdraw` on pool-based protocols (Aave): the market's `loanToken.address`, or one of `collateralTokens[].token.address`. |
| `collateralTokenAddress`, `collateralAmount`, `collateralAmountRaw`, `borrowAmount`, `targetLtv` | Accepted only by some actions; check the action's schema in `getIntegration` before you pass them. |

`getIntegration` returns the exact schema of each action's `args` for an integration.

`createAction` resolves with `{ data: action }`. The action carries its `id`, its `status`, the `transactions` of the current step in signing order, `hasNextStep`, `currentStep` and `totalSteps`, and `metadata` with the predicted `predictedHealthFactor`, `predictedLtv`, `predictedTotalSupplyUsd` and `predictedTotalDebtUsd`.

<Note>
  Isolated-market protocols (Morpho Blue) infer the token from `marketId`, and on them `supply` only posts collateral: the loan token is borrowed or repaid, never supplied, so passing it as `tokenAddress` is rejected. When the collateral is a wrapped native token (for example WMON on Monad) and the wallet holds the native token instead, Yield.xyz adds a `WRAP` transaction before the approval; the SDK signs it like any other transaction. Yield.xyz builds the transactions even when the wallet does not hold the tokens yet: the balance check happens on-chain.
</Note>

## Borrowing

Borrowing uses the same flow with `action: 'borrow'`. Supplied assets on Aave are collateral by default; on other integrations, or if you disabled it, run an `enableCollateral` action first. Integrations that offer `supplyAndBorrow`, which supplies collateral and borrows in one action, list it in `getIntegration` with the `args` it accepts.

```typescript theme={null}
const marketId = 'aave-borrow-base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'

const { data: borrow } = await portal.borrow.yieldXyz.createAction({
  integrationId: 'aave-borrow',
  action: 'borrow',
  address,
  args: { marketId, amount: '2' },
})

const healthFactor = borrow.metadata?.predictedHealthFactor ?? null
console.log(`Health factor after borrowing: ${healthFactor ?? 'unknown'}`)

await portal.borrow.yieldXyz.signAndSubmitAction(borrow)
```

<Note>
  Show `metadata.predictedHealthFactor` and `metadata.predictedLtv` to the user before they sign: a health factor at or below `1` means the position can be liquidated. `predictedHealthFactor` can be `null` when Yield.xyz does not compute it for a market (Morpho Blue on Monad returns `null` even with open debt); treat `null` as unknown, never as safe.
</Note>

## Checking positions

`getPositions` returns the wallet's position on one integration and network.

```typescript theme={null}
const { data: position } = await portal.borrow.yieldXyz.getPositions({
  integrationId: 'aave-borrow',
  network: 'eip155:8453',
  address,
})

console.log('Health factor:', position.healthFactor ?? 'unknown')
console.log('Supplied:', position.totalSuppliedUsd, 'Borrowed:', position.totalBorrowedUsd)

for (const balance of position.debtBalances) {
  console.log(balance.tokenSymbol, balance.balance, balance.pendingActions.map((pending) => pending.type))
}
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `integrationId` | `string` | Yes | Integration id. |
| `network` | `string` | Yes | CAIP-2 chain id. |
| `address` | `string` | Yes | The wallet's EVM address. |

The position holds the totals (`totalSuppliedUsd`, `totalCollateralUsd`, `totalBorrowedUsd`, `netWorthUsd`), `healthFactor`, `currentLtv`, `availableToBorrowUsd`, `netApy`, and the `supplyBalances` and `debtBalances`. For isolated-market protocols such as Morpho Blue, the account-level `healthFactor` and `availableToBorrowUsd` are always `null`: read each supply balance's `positionState` instead, which carries that market's `healthFactor`, `availableToBorrowUsd`, `currentLtv` and `liquidationThreshold`. A `healthFactor` is also `null` when there is no debt; treat any other `null` as unknown.

Each balance lists `pendingActions`, the actions available for it now, with pre-filled `args` you can pass to `createAction`:

```typescript theme={null}
const repay = position.debtBalances[0]?.pendingActions.find((pending) => pending.type === 'repay')

if (repay) {
  const { data: action } = await portal.borrow.yieldXyz.createAction({
    integrationId: position.integrationId,
    action: 'repay',
    address,
    args: repay.args,
  })
  await portal.borrow.yieldXyz.signAndSubmitAction(action)
}
```

## Repaying and withdrawing

Repay debt with `action: 'repay'` (pass `repayAll: true` to clear the position, which avoids dust from accrued interest), then withdraw collateral with `action: 'withdraw'`.

```typescript theme={null}
const marketId = 'aave-borrow-base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'

const { data: repay } = await portal.borrow.yieldXyz.createAction({
  integrationId: 'aave-borrow',
  action: 'repay',
  address,
  args: { marketId, repayAll: true },
})
await portal.borrow.yieldXyz.signAndSubmitAction(repay)

const { data: withdraw } = await portal.borrow.yieldXyz.createAction({
  integrationId: 'aave-borrow',
  action: 'withdraw',
  address,
  args: {
    marketId,
    tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    amount: '5',
  },
})
await portal.borrow.yieldXyz.signAndSubmitAction(withdraw)
```

Withdrawing a wrapped native collateral (for example WMON) returns the **wrapped** token. If your users expect the native token back, unwrap it after the withdraw confirms.

## Signing and execution

### How an action is executed

`signAndSubmitAction(action, options?)` executes an action step by step:

1. It reads the action back from Yield.xyz. The action's current state decides what still needs signing; the action you pass gives the signing order.
2. For each transaction of the current step, in order, it signs and broadcasts the transactions awaiting a signature (`CREATED` or `WAITING_FOR_SIGNATURE`) with `eth_sendTransaction` on the transaction's own chain, using the payload Yield.xyz lists at signing time. It submits the on-chain transaction hash to Yield.xyz, then polls until Yield.xyz reports the transaction `CONFIRMED` or `SKIPPED` before signing the next one. A transaction in any other status is waited on until it can be signed or is confirmed. The SDK never re-signs a transaction Yield.xyz has already recorded, but a transaction broadcast without its hash being returned is not recorded yet, so a blind retry can broadcast it again (see [Resuming a failed execution](#resuming-a-failed-execution)).
3. When the step is confirmed and `hasNextStep` is `true`, it fetches the next step with `stepAction` and repeats.
4. It resolves when the action reaches `SUCCESS`. Any failure throws a [`YieldXyzBorrowExecutionError`](#handling-errors).

Before signing a transaction, the SDK checks that its `signingFormat` is `EVM_TRANSACTION` (the only format Borrow signs), that its `network` is an EIP-155 chain with an `rpcConfig` entry, and that the action, the transaction and the payload's `from` were all built for the Portal wallet's EVM address. Only `from`, `to`, `data`, `gasLimit` (sent as `gas`) and `value` are forwarded: the signer sets the nonce and fees.

Each transaction goes through Portal's signing flow. Unless `autoApprove` is `true`, approve each request in your `portal_signingRequested` handler (see [Add custom signature hooks](./add-custom-signature-hooks)). A rejected request stops the execution with `SIGNATURE_REJECTED`. Without a `portal_signingRequested` handler, the request throws before anything is signed, so the execution stops with `SIGNING_FAILED`.

For account abstraction enabled clients, `eth_sendTransaction` returns a user operation hash. The SDK waits for the bundle transaction and submits its on-chain hash to Yield.xyz; progress events and results carry both hashes.

### Executing in one call

`executeAction(params, options?)` calls `createAction` and then `signAndSubmitAction`. Use the two-call flow above when you want to show the predicted health factor before the user signs.

```typescript theme={null}
const result = await portal.borrow.yieldXyz.executeAction(
  {
    integrationId: 'aave-borrow',
    action: 'borrow',
    address,
    args: { marketId: 'aave-borrow-base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', amount: '2' },
  },
  { onProgress: (event) => console.log(event.step, event.transactionId ?? '') },
)
```

When `createAction` fails, `executeAction` throws the Portal API error as it is: no action exists yet, so there is nothing to resume.

### Options

Both methods take the same `YieldXyzBorrowExecuteOptions`:

| Name | Type | Required | Description |
| - | - | - | - |
| `onProgress` | `(event: YieldXyzBorrowExecutionProgress) => void` | No | Called at each execution step. An exception it throws is logged and does not affect the execution. |
| `pollAction` | `{ intervalMs?: number; timeoutMs?: number }` | No | How the action is polled. `intervalMs` defaults to `2000`. `timeoutMs` defaults to `600000` and applies to each wait: resolving a transaction's on-chain hash, its confirmation, and the action's final status. |
| `signAndSendTransaction` | `YieldXyzBorrowSignAndSendTransaction` | No | Per-call signer used instead of the Portal wallet's. The SDK skips its own `rpcConfig` check for it, but a signer that signs through `portal.request`, like the one below, still needs the chain's `rpcConfig` entry. |
| `broadcastHashes` | `Record<string, string>` | No | Transactions an earlier attempt already broadcast, by transaction id. They are never signed again. See [Resuming a failed execution](#resuming-a-failed-execution). |

### Progress events

`onProgress` receives a `YieldXyzBorrowExecutionProgress` with the `step`, the `actionId` and the fields that apply to that step:

| `step` | Meaning | Fields |
| - | - | - |
| `signing` | About to sign a transaction. | `transactionId`, `index` |
| `confirming` | The transaction was broadcast; the SDK is resolving its on-chain hash (an account abstraction enabled client waits for inclusion). | `transactionId`, `index`, `broadcastHash` |
| `submitted` | Yield.xyz accepted the on-chain transaction hash. | `transactionId`, `index`, `transactionHash`, `userOperationHash` |
| `polling` | The action was read back. | `actionStatus` |
| `confirmed` | Yield.xyz reports the transaction `CONFIRMED` or `SKIPPED`. | `transactionId`, `index`, `transactionHash`, `userOperationHash` |
| `nextStep` | The next step's transactions were fetched. | `currentStep`, `totalSteps`, `actionStatus` |
| `failed` | Execution stopped. | `error`, plus the transaction's id and hashes when known |

`index` counts the transactions this call signed or submitted before this one, across steps.

```typescript theme={null}
import type { YieldXyzBorrowExecutionProgress } from '@portal-hq/web'

function onProgress(event: YieldXyzBorrowExecutionProgress) {
  switch (event.step) {
    case 'signing':
      console.log(`Signing transaction ${event.transactionId}`)
      break
    case 'confirming':
      console.log(`Broadcast ${event.broadcastHash}`)
      break
    case 'confirmed':
      console.log(`Confirmed ${event.transactionHash}`)
      break
    case 'nextStep':
      console.log(`Step ${event.currentStep} of ${event.totalSteps}`)
      break
    case 'failed':
      console.error(`Stopped: ${event.error?.reason}`)
      break
  }
}

await portal.borrow.yieldXyz.signAndSubmitAction(action, { onProgress })
```

### Return value

Both methods resolve with a `YieldXyzBorrowExecutionResult`:

| Field | Description |
| - | - |
| `actionId` | The action id. |
| `action` | The latest state of the action, with its transactions in signing order. |
| `status` | The action's final status (`SUCCESS`). |
| `transactions` | One entry per transaction this call signed or submitted: `transactionId`, `type`, `transactionHash` (the on-chain hash reported to Yield.xyz), `userOperationHash` (account abstraction enabled clients only) and `submitResult` (absent when the submission's outcome was confirmed by reading the action back). |

### Using your own signer

Pass `signAndSendTransaction` to sign with your own logic, for example to add a confirmation step. It receives the `eth_sendTransaction` params (`from`, `to`, `data`, `gas` and `value`, as hex quantities) and the CAIP-2 chain id, and resolves with the hash, or with `undefined` when the request is declined. The SDK still checks that every transaction was built for the Portal wallet's EVM address. A signer that signs through `portal.request`, like this one, still needs an `rpcConfig` entry for the chain: without one, the request throws before anything is sent.

```typescript theme={null}
import type { YieldXyzBorrowSignAndSendTransaction } from '@portal-hq/web'

const signAndSendTransaction: YieldXyzBorrowSignAndSendTransaction = async (transaction, chainId) => {
  if (!window.confirm(`Send this transaction to ${transaction.to} on ${chainId}?`)) {
    return undefined // stops the execution with SIGNATURE_REJECTED
  }
  return portal.request({ chainId, method: 'eth_sendTransaction', params: [transaction] })
}

await portal.borrow.yieldXyz.signAndSubmitAction(action, { signAndSendTransaction })
```

## Resuming a failed execution

A failed execution throws a `YieldXyzBorrowExecutionError` that carries the `actionId`, the `transactionId` and `action`: the latest known state of the action, with its transactions in signing order. Persist it and resume with `signAndSubmitAction(error.action, { broadcastHashes })`. Prefer `error.action` (or the action `createAction` returned) over `getAction`, which may list the transactions in a different order.

When the error carries a `broadcastHash`, that transaction was already broadcast and Yield.xyz may not have recorded it yet. Pass it in `broadcastHashes`, keyed by transaction id, so the SDK resolves and submits it instead of signing it again. The exception is `USER_OPERATION_FAILED`: that user operation was already processed and did not succeed, so drop its entry and let the retry sign a new transaction.

```typescript theme={null}
import {
  YieldXyzBorrowExecutionError,
  YieldXyzBorrowExecutionErrorReason,
  type YieldXyzBorrowAction,
} from '@portal-hq/web'

function recordBroadcastHash(error: YieldXyzBorrowExecutionError, broadcastHashes: Record<string, string>) {
  if (!error.transactionId) return
  if (error.reason === YieldXyzBorrowExecutionErrorReason.UserOperationFailed) {
    delete broadcastHashes[error.transactionId]
  } else if (error.broadcastHash) {
    broadcastHashes[error.transactionId] = error.broadcastHash
  }
}

const broadcastHashes: Record<string, string> = {}
let pending: YieldXyzBorrowAction = action

// Call once to execute, and again only when the user chooses to retry.
async function execute() {
  try {
    return await portal.borrow.yieldXyz.signAndSubmitAction(pending, { broadcastHashes })
  } catch (error) {
    if (YieldXyzBorrowExecutionError.is(error)) {
      recordBroadcastHash(error, broadcastHashes)
      pending = error.action ?? pending
    }
    throw error
  }
}

try {
  await execute()
} catch (error) {
  if (!YieldXyzBorrowExecutionError.is(error)) throw error
  if (
    error.reason === YieldXyzBorrowExecutionErrorReason.SigningFailed &&
    error.transactionId &&
    !error.broadcastHash
  ) {
    // The transaction may still have been sent. Don't offer a retry until you have checked the wallet's
    // transactions on that chain; if you find it, add its hash to broadcastHashes under error.transactionId.
  } else {
    // Persist `pending` and `broadcastHashes`, and call execute() again when the user retries.
    // The table below lists the reasons that can't be retried as is.
  }
}
```

Not every failure should be retried as is:

| Reason | What to do |
| - | - |
| `ACTION_FAILED` | The action ended as `FAILED`, `CANCELED` or `STALE`. It cannot be resumed; create a new action. |
| `ACTION_IN_PROGRESS` | Another call on this Portal instance is already executing the action. This call signed nothing. |
| `UNSUPPORTED_SIGNING_FORMAT`, `UNSUPPORTED_NETWORK`, `INVALID_SIGNABLE_PAYLOAD`, `MISSING_WALLET_ADDRESS`, `SIGNER_ADDRESS_MISMATCH`, `SIGNING_CHAIN_NOT_CONFIGURED`, `DEPENDENCY_UNAVAILABLE` | Raised before the SDK signed anything in this call, but a plain retry fails the same way. Fix the cause (configuration, wallet or address) or create a new action. |
| `SIGNING_FAILED` | Signing or broadcasting failed. With a `transactionId`, the transaction may have been broadcast without its hash being returned. `getAction` cannot rule this out, since that hash was never submitted to Yield.xyz. Before retrying, check the wallet's transactions on that chain. If the transaction was sent, resume with its hash in `broadcastHashes`; a retry without it would broadcast the transaction a second time. Without a `transactionId`, it is an unexpected error not tied to a specific transaction: check the action with `getAction` before retrying. |
| `SUBMIT_OUTCOME_UNKNOWN` | The submission failed ambiguously. Check the action before retrying. |
| `NEXT_STEP_FAILED` | The next step could not be fetched or had nothing to sign. Yield.xyz may have moved to the next step anyway, so `error.action` can still be the previous step's; resuming from it signs the new step's transactions in the order `getAction` lists them, which may not be their signing order. If the new step has several transactions awaiting a signature, check their order before resuming: an approval must be signed before the transaction that spends it. |

## Handling errors

Two kinds of error reach your app:

* **Portal API errors**, thrown by the read methods and `createAction`, and carried as `cause` by execution errors. On Web they are `PortalMpcError`s with the HTTP `status`, the stable error `id` and `details`. `getYieldXyzBorrowApiError(error)` returns the API error whether it was thrown directly or wrapped.
* **`YieldXyzBorrowExecutionError`**, thrown by `signAndSubmitAction` and `executeAction`. Check it with `YieldXyzBorrowExecutionError.is(error)` and branch on `reason`.

```typescript theme={null}
import {
  YieldXyzBorrowExecutionError,
  getYieldXyzBorrowApiError,
  isYieldXyzBorrowRateLimitError,
  isYieldXyzBorrowSchemaDriftError,
} from '@portal-hq/web'

try {
  await portal.borrow.yieldXyz.signAndSubmitAction(action)
} catch (error) {
  if (isYieldXyzBorrowSchemaDriftError(error)) {
    console.error('Yield.xyz Borrow is temporarily unavailable.')
  } else if (isYieldXyzBorrowRateLimitError(error)) {
    console.error('Rate limited by Yield.xyz; try again shortly.')
  } else if (YieldXyzBorrowExecutionError.is(error)) {
    console.error(error.reason, error.actionId, error.transactionId, error.broadcastHash)
  }

  const apiError = getYieldXyzBorrowApiError(error)
  if (apiError) {
    console.error(apiError.status, apiError.id, apiError.message, apiError.details)
  }
}
```

The Portal API answers with `400` (invalid request, unsupported chain id, a request Yield.xyz rejected, or a missing or rejected Yield.xyz API key), `404`, `429`, `503`, or `500` with `id: INTEGRATION_RESPONSE_SCHEMA_DRIFT` when Yield.xyz changed a response shape. See [Errors](/integrations/Yield/yield-xyz-borrow#errors) on the integration page. While executing, the SDK retries rate-limited (`429`) submissions and step requests and tolerates up to five consecutive transient failures while reading the action back. After an ambiguous submission failure, it resubmits the transaction hash once, and only if the action shows Yield.xyz did not record it. Signing is never retried.

`YieldXyzBorrowExecutionErrorReason` values:

| Reason | Meaning |
| - | - |
| `UNSUPPORTED_SIGNING_FORMAT` | The transaction's `signingFormat` is not `EVM_TRANSACTION`. |
| `UNSUPPORTED_NETWORK` | The transaction's `network` is not an EIP-155 chain Portal supports. |
| `INVALID_SIGNABLE_PAYLOAD` | The `signablePayload` is missing or is not a valid EVM transaction. |
| `MISSING_WALLET_ADDRESS` | No EVM wallet address is available to sign with. |
| `SIGNER_ADDRESS_MISMATCH` | The action or the transaction was built for an address other than the Portal wallet's EVM address. |
| `SIGNING_CHAIN_NOT_CONFIGURED` | The transaction's chain has no `rpcConfig` entry. Add one. |
| `DEPENDENCY_UNAVAILABLE` | A signer or transaction lookup needed to execute the action is not configured. |
| `SIGNATURE_REJECTED` | The signing request was rejected: no hash was returned. |
| `INVALID_TRANSACTION_HASH` | The signer, or `broadcastHashes`, supplied a value that is not a 32-byte hex hash. |
| `SIGNING_FAILED` | Signing or broadcasting failed; `cause` holds the underlying error. Also used for unexpected errors. |
| `USER_OPERATION_FAILED` | The user operation was included on-chain but did not succeed. |
| `TRANSACTION_FAILED` | Yield.xyz reported the transaction as `FAILED`. |
| `SUBMIT_REJECTED` | The Portal API rejected the submission; `cause` holds the API error. |
| `SUBMIT_FAILED` | The submission was accepted, but Yield.xyz reported the transaction as `FAILED`. |
| `SUBMIT_OUTCOME_UNKNOWN` | The submission failed ambiguously and its outcome could not be confirmed. |
| `NEXT_STEP_FAILED` | The next step could not be fetched, or had nothing to sign. |
| `ACTION_FAILED` | The action ended as `FAILED`, `CANCELED` or `STALE`. |
| `ACTION_IN_PROGRESS` | Another call on this Portal instance is already executing the action. |
| `POLLING_FAILED` | The action, or a transaction's on-chain hash, could not be read back while tracking it; `cause` holds the error, and `broadcastHash` is set when the transaction was already broadcast. |
| `TIMEOUT` | A transaction or the action did not reach the awaited status before `pollAction.timeoutMs`. |

The error also carries `broadcastHash` (the hash the signer returned, set once the transaction was broadcast), `transactionHash` (the on-chain hash, once known), `userOperationHash` and `cause`.

## History and liquidations

`getActions` lists the wallet's actions, filtered by `integrationId`, `action`, `status` or `statuses`, and `getAction` reads one action. `getLiquidations` returns a position's liquidation history (Morpho Blue integrations only).

```typescript theme={null}
const { data: actions } = await portal.borrow.yieldXyz.getActions({
  address,
  integrationId: 'aave-borrow',
  statuses: ['SUCCESS', 'FAILED'],
  limit: 20,
})

for (const item of actions.items) {
  console.log(item.id, item.action, item.status, item.createdAt)
}

// Read one action back by id, for example the one createAction returned
const { data: current } = await portal.borrow.yieldXyz.getAction(action.id)
console.log(current.status, current.currentStep, current.totalSteps)

const { data: liquidations } = await portal.borrow.yieldXyz.getLiquidations({
  integrationId: 'morpho-blue-borrow',
  network: 'eip155:1',
  address,
})
console.log(liquidations.total)
```

Both list methods take `offset` and `limit` (at most `100`) and resolve with a page: `{ data: { total, offset, limit, items } }`.

## Low-level APIs

`signAndSubmitAction` covers signing, submitting and stepping. If you drive the transactions yourself, use the low-level methods with the [Enclave MPC API transaction flow](/apis/enclave-mpc/guide/yield-xyz-borrow#transaction-processing) as the reference:

| Method | Description |
| - | - |
| `submitTransaction(transactionId, params)` | Submits `{ transactionHash }` for a transaction you broadcast, or `{ signedPayload }` for Yield.xyz to broadcast. Resolves with `{ data: { status, link, transactionHash, error, details } }`; a `status` of `FAILED` means the transaction failed even though the request succeeded. |
| `stepAction(actionId)` | Fetches the next step's transactions. Call it only once every transaction of the current step is `CONFIRMED` or `SKIPPED` and the action has `hasNextStep: true`. |
| `getHealth()` | Returns the integration's health: `{ data: { status: 'OK' \| 'FAIL', timestamp } }`. |

```typescript theme={null}
const transactionId = action.transactions[0].id

// Reports the hash to Yield.xyz; it does not wait for the transaction to confirm.
const { data: submitted } = await portal.borrow.yieldXyz.submitTransaction(transactionId, {
  transactionHash: '0x...',
})
console.log(submitted.status, submitted.link)

// Advance only once the current step is confirmed and another step follows.
const { data: current } = await portal.borrow.yieldXyz.getAction(action.id)
const stepConfirmed = current.transactions.every(
  ({ status }) => status === 'CONFIRMED' || status === 'SKIPPED',
)
if (current.hasNextStep && stepConfirmed) {
  const { data: next } = await portal.borrow.yieldXyz.stepAction(action.id)
  console.log(next.currentStep, next.transactions.length)
}
```

Each transaction's `signablePayload` is the JSON-encoded unsigned transaction. For account abstraction enabled clients, submit the on-chain transaction hash, never the user operation hash.

## Best practices

1. **Show the predicted health factor and LTV** from `metadata` before the user signs a borrow or withdraw: build with `createAction`, then execute with `signAndSubmitAction`
2. **Sign soon after building**: the transactions carry the gas limit Yield.xyz estimated when the action was built, so create a new action if the user waits long before signing
3. **Persist `error.action` and `broadcastHashes`** so a failed execution can be resumed without signing a transaction twice
4. **Use `repayAll`** when closing a position so accrued interest does not leave dust
5. **Pass `scope: 'all'` and filter** markets to the ones your UI offers, for example by `availableLiquidity`
6. **Handle `INTEGRATION_RESPONSE_SCHEMA_DRIFT`** as a feature outage rather than a user error
7. **Keep gas on the wallet**: every transaction is on-chain

## Supported networks

Lending integrations run on mainnets only; the exact list per protocol is returned by `getIntegrations` (`networks`, as CAIP-2). Aave V3 covers the major EVM chains (Ethereum `eip155:1`, Base `eip155:8453`, Arbitrum `eip155:42161`, Optimism `eip155:10`, Polygon `eip155:137`, Avalanche `eip155:43114`, and more); Morpho Blue covers Ethereum, Base, Monad (`eip155:143`) and other EVM chains; SparkLend is Ethereum-only; Lista runs on BNB Chain (`eip155:56`) and Ethereum.

Signing needs an `rpcConfig` entry for the transaction's chain. The default configuration covers the 13 built-in chains; add any other chain, such as BNB Chain, yourself.

## Next Steps

* Set up the integration: [Yield.xyz Borrow](/integrations/Yield/yield-xyz-borrow)
* Use the [Enclave MPC API](/apis/enclave-mpc/guide/yield-xyz-borrow) to lend and borrow from your backend
* Check out the [Yield.xyz Borrow API reference](/api-reference/yieldxyz-borrow/get-positions)
* Approve signing requests with [custom signature hooks](./add-custom-signature-hooks)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.