> ## 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.

# Earn with Yield.xyz

> Learn how to discover, enter, manage, and exit yield opportunities.

Portal's Android SDK provides comprehensive yield opportunities capabilities through the `portal.yield.yieldxyz` API. This guide covers discovering yield opportunities, entering positions, managing existing positions, and exiting yield opportunities.

## Overview

The yield functionality allows users to:

* **Discover** available yield opportunities across different protocols and networks
* **Enter** yield positions by depositing tokens into yield opportunities
* **Manage** existing positions (claim rewards, voting, etc.)
* **Exit** yield positions to withdraw aggregated tokens and rewards
* **Track** yield balances and historical yield actions

## Prerequisites

Before using yield operations, ensure you have:

* A properly initialized Portal client
* An active wallet with the required token(s) on the target network (see [Create a wallet](./create-a-wallet))
* Yield.xyz integration enabled in your Portal Dashboard (see [Yield.xyz Integration](/integrations/Yield/yield-xyz))

## Discovering Yield Opportunities

Use the `discover` method to find available yield opportunities.

For complete API documentation, see the [Yield.xyz API reference](https://docs.yield.xyz/reference/yieldscontroller_getyields).

```kotlin theme={null}
val request = YieldXyzGetYieldsRequest(
    offset = 0,
    limit = 10,
    network = "eip155:11155111", // Sepolia network
    // ... other parameters
)

val result = portal.yield.yieldxyz.discover(request)

if (result.isSuccess) {
    val response = result.getOrThrow()
    val rawResponse = response.data?.rawResponse
    val yieldOpportunities = rawResponse?.items

    // Process and display yield opportunities
}
```

<Tip>
  Popular, high-quality USDC yield options with no lockups or limits:

  * USDC Aave V3 Lending:
    * `base-usdc-aave-v3-lending`
  * USDC Fluid Vault:
    * `base-usdc-fusdc-0xf42f5795d9ac7e9d757db633d693cd548cfd9169-4626-vault`
  * USDC Spark Savings Vault:
    * `ethereum-usdc-spusdc-0x28b3a8fb53b741a8fd78c0fb9a6b2393d896a43d-4626-vault`
</Tip>

## Entering Yield Positions

To enter a yield position, first discover the specific yield, then use the `enter` method.

For complete API documentation, see the [Yield.xyz enter yield reference](https://docs.yield.xyz/reference/actionscontroller_enteryield).

For the example below, we will use the yield opportunity with the ID `"ethereum-sepolia-link-aave-v3-lending"`. Fund your Portal client with the required `LINK` token to enter the position.

```kotlin theme={null}
val userAddress = portal.getAddress(PortalNamespace.EIP155) ?: return

val enterRequest = YieldXyzEnterRequest(
    yieldId = "ethereum-sepolia-link-aave-v3-lending",
    address = userAddress,
    arguments = EnterYieldArguments(
        amount = "1", // 1 LINK token
    )
)

val enterResult = portal.yield.yieldxyz.enter(enterRequest)
if (enterResult.isSuccess) {
    val enterResponse = enterResult.getOrThrow()
    val rawResponse = enterResponse.data?.rawResponse
    val transactions = rawResponse?.transactions

    // Process transactions, this is described in the "Transaction Processing" section below
    processTransactions(transactions)
}
```

## Checking Yield Balances

Retrieve current yield positions and balances.

For complete API documentation, see the [Yield.xyz get balances reference](https://docs.yield.xyz/reference/yieldscontroller_getaggregatebalances).

```kotlin theme={null}
val userAddress = portal.getAddress(PortalNamespace.EIP155) ?: return

val balanceRequest = YieldXyzGetBalancesRequest(
    queries = listOf(
        YieldBalanceQuery(
            address = userAddress,
            network = "eip155:11155111" // Sepolia testnet
        )
    )
)

val balanceResult = portal.yield.yieldxyz.getBalances(balanceRequest)
if (balanceResult.isSuccess) {
    val response = balanceResult.getOrThrow()
    val rawResponse = response.data?.rawResponse
    val yieldPositions = rawResponse?.items

    // Process and display yield positions information
}
```

<Note>
  We recommend always specifying a `yieldId` on each balance query. When `yieldId` is provided, Yield.xyz can resolve balances directly, so you don't need to call the `track` endpoint after entering or exiting positions.

  ```kotlin theme={null}
  val balanceRequest = YieldXyzGetBalancesRequest(
      queries = listOf(
          YieldBalanceQuery(
              address = userAddress,
              network = "eip155:11155111",
              yieldId = "ethereum-sepolia-link-aave-v3-lending"
          )
      )
  )
  ```
</Note>

## Exiting Yield Positions

Use the `exit` method to withdraw from yield positions.

For complete API documentation, see the [Yield.xyz exit yield reference](https://docs.yield.xyz/reference/actionscontroller_exityield).

```kotlin theme={null}
val userAddress = portal.getAddress(PortalNamespace.EIP155) ?: return

val exitRequest = YieldXyzExitRequest(
    yieldId = "ethereum-sepolia-link-aave-v3-lending",
    address = userAddress,
    arguments = YieldXyzEnterArguments(amount = "0.001")
)

val exitResult = portal.yield.yieldxyz.exit(exitRequest)
if (exitResult.isSuccess) {
    val exitResponse = exitResult.getOrThrow()
    val rawResponse = exitResponse.data?.rawResponse
    val transactions = rawResponse?.transactions

    // Process transactions, this is described in the "Transaction Processing" section below
    processTransactions(transactions)
}
```

## High-Level Methods

Use `deposit` and `withdraw` when you want one call for the full flow: resolve the yield, build the action, sign and send each transaction in order, wait for confirmation between steps, and report each hash back to Yield.xyz.

Both are `suspend` and return `Result<T>`. `YieldWithdrawParams` and `YieldWithdrawResult` are type aliases for the deposit types, so the two methods take an identical shape.

### Signatures

```kotlin theme={null}
suspend fun deposit(
    params: YieldDepositParams,
    options: YieldSubmitOptions? = null
): Result<YieldDepositResult>

suspend fun withdraw(
    params: YieldWithdrawParams,
    options: YieldSubmitOptions? = null
): Result<YieldWithdrawResult>
```

`options` is defaulted, so `deposit(params)` alone is valid. Both run on `Dispatchers.IO`.

### Essential parameters

`YieldDepositParams`:

| Parameter | Type | Required | Description |
| - | - | - | - |
| `target` | `YieldActionTarget` | Yes | Which yield to act on. See below. |
| `amount` | `String` | Yes | Amount to deposit or withdraw. Merged into the action arguments. |
| `arguments` | `YieldXyzEnterArguments?` | No | Protocol-specific inputs, such as `validatorAddress` for native staking. |

There is **no `address` parameter.** The wallet address is resolved from your `Portal` instance for whichever chain the yield resolves to. If no wallet exists for that chain, the call fails with `YieldXyzActionException.AddressUnavailable`.

`YieldActionTarget` is a sealed class with two subtypes, so you cannot supply both forms or neither:

```kotlin theme={null}
sealed class YieldActionTarget {
  data class ByYieldId(val yieldId: String) : YieldActionTarget()
  data class ByChainAndToken(val chain: String, val token: String) : YieldActionTarget()
}
```

| Subtype | Behavior |
| - | - |
| `ByYieldId(yieldId)` | Acts on that yield directly, skipping the defaults lookup. |
| `ByChainAndToken(chain, token)` | Resolves the yield from your Portal yield defaults. `chain` must be a **full CAIP-2 id** (`"eip155:1"`); `token` must match the defaults exactly. |

<Warning>
  `ByChainAndToken` requires full CAIP-2. A bare `"1"` fails with `YieldXyzActionException.InvalidChainId`. This differs from Li.Fi's `tradeAsset`, which forwards whatever chain format you give it — the two APIs shipped in the same release but do not accept the same values.
</Warning>

`YieldSubmitOptions`:

| Option | Type | Default | Description |
| - | - | - | - |
| `onProgress` | `((YieldSubmitProgress) -> Unit)?` | `null` | Fired per transaction with `signing`, `submitted`, `confirming`, `confirmed`. |
| `pollIntervalMs` | `Long` | `4_000` | **Milliseconds** between confirmation polls. Coerced to at least `1`. |
| `timeoutMs` | `Long` | `900_000` | **Milliseconds** to wait for a transaction to confirm before treating it as uncertain. Coerced to at least `pollIntervalMs`. |

<Warning>
  `onProgress` is invoked on `Dispatchers.IO`, not the main thread. Switch dispatchers before touching any UI from inside the callback.
</Warning>

<Note>
  These are **milliseconds**. The iOS SDK uses `pollIntervalSeconds` and `timeoutSeconds` for the same two concepts, so a value copied across platforms will be wrong by a factor of 1000.
</Note>

Note the enum member casing: `YieldSubmitStep` members are lowercase (`signing`, `submitted`, `confirming`, `confirmed`), while `YieldSubmitResultStatus` members are uppercase (`SUCCESS`, `PARTIAL_SUCCESS`, `FAILED`). Both are written exactly as the SDK declares them.

There is no per-call signer or confirmation override — signing always goes through the Portal MPC signer.

### Return value

`YieldDepositResult`:

| Field | Type | Description |
| - | - | - |
| `hashes` | `List<String>` | Submitted transaction hashes, in order. Contains every hash that was submitted, regardless of final outcome. |
| `yieldId` | `String` | The resolved yield id. |
| `status` | `YieldSubmitResultStatus` | `SUCCESS`, `PARTIAL_SUCCESS`, or `FAILED`. See below. |
| `chain` | `String?` | Set only when you targeted via `ByChainAndToken`. |
| `token` | `String?` | Set only when you targeted via `ByChainAndToken`. |
| `yieldOpportunityDetails` | `YieldOpportunityDetails` | Action metadata — `yieldId`, `intent`, `type`, `executionPattern`, `status`, `amount`, `amountUsd`. This is action-level detail, not the full opportunity; use `discover` for that. |

### Handling Results

| Status | Meaning |
| - | - |
| `SUCCESS` | Every transaction confirmed, or the action required no confirmations. |
| `PARTIAL_SUCCESS` | Execution stopped before all transactions confirmed — typically a confirmation timeout. Earlier transactions may have landed. |
| `FAILED` | A transaction was confirmed as failed on-chain. Execution stopped immediately at that point. |

```kotlin theme={null}
portal.yield.yieldxyz.deposit(params).onSuccess { result ->
    when (result.status) {
        YieldSubmitResultStatus.SUCCESS ->
            Log.i("Portal", "All transactions confirmed: ${result.hashes}")
        YieldSubmitResultStatus.PARTIAL_SUCCESS ->
            Log.w("Portal", "Stopped before completing: ${result.hashes} — check the yield balance")
        YieldSubmitResultStatus.FAILED ->
            Log.e("Portal", "Transaction failed on-chain: ${result.hashes.lastOrNull()}")
    }
}
```

The `when` is exhaustive over the three states with no `else`.

<Warning>
  A non-empty `hashes` list does **not** mean success. Hashes are recorded as transactions are submitted, before their outcome is known. Always branch on `status`.
</Warning>

### Example (deposit with progress)

```kotlin theme={null}
import io.portalhq.android.yield.YieldActionTarget
import io.portalhq.android.yield.YieldDepositParams
import io.portalhq.android.yield.YieldSubmitOptions
import io.portalhq.android.yield.YieldSubmitResultStatus

lifecycleScope.launch {
    portal.yield.yieldxyz.deposit(
        params = YieldDepositParams(
            target = YieldActionTarget.ByYieldId("ethereum-sepolia-link-aave-v3-lending"),
            amount = "0.001"
        ),
        options = YieldSubmitOptions(
            onProgress = { progress ->
                // Runs on Dispatchers.IO — hop to Main before touching UI.
                Log.i("Portal", "${progress.step} ${progress.index + 1}/${progress.total} ${progress.hash ?: ""}")
            },
            pollIntervalMs = 4_000,   // milliseconds
            timeoutMs = 300_000
        )
    ).onSuccess { result ->
        Log.i("Portal", "Status: ${result.status}")
        Log.i("Portal", "Hashes: ${result.hashes}")
    }.onFailure { error ->
        Log.e("Portal", "Deposit failed: ${error.message}", error)
    }
}
```

### Example (targeting by chain and token)

```kotlin theme={null}
lifecycleScope.launch {
    portal.yield.yieldxyz.deposit(
        params = YieldDepositParams(
            target = YieldActionTarget.ByChainAndToken(
                chain = "eip155:11155111",
                token = "ETH"
            ),
            amount = "0.0000001"
        )
    ).onSuccess { result ->
        Log.i("Portal", "Resolved yield: ${result.yieldId}")
        Log.i("Portal", "Chain: ${result.chain} Token: ${result.token}")
    }
}
```

`chain` and `token` on the result are populated only when you target this way.

### Example (withdraw)

`withdraw` takes identical parameter shapes:

```kotlin theme={null}
lifecycleScope.launch {
    portal.yield.yieldxyz.withdraw(
        params = YieldWithdrawParams(
            target = YieldActionTarget.ByYieldId("ethereum-sepolia-link-aave-v3-lending"),
            amount = "0.001"
        )
    ).onSuccess { result ->
        Log.i("Portal", "Status: ${result.status}")
    }
}
```

### Errors

Failures arrive inside the returned `Result` as a `YieldXyzActionException`:

| Case | When |
| - | - |
| `PortalNotInitialized` | No Portal dependency set for signing. Cannot occur on `portal.yield.yieldxyz`. |
| `EmptyYieldId` | An empty `yieldId` was supplied. |
| `InvalidChainId(chain)` | `chain` was not a full CAIP-2 id. |
| `NoYieldForChainToken(key)` | No default yield matches that chain and token pair. |
| `YieldNotFound(yieldId)` | The yield id does not exist. |
| `AddressUnavailable(chain)` | No wallet address for the resolved chain. Create or load a wallet first. |
| `NoTransactions` | The action response contained no transactions. |
| `MissingTransactionField(field)` | A transaction was missing a required field. |
| `UnsupportedNetwork(network)` | The action resolved to a network the high-level flow cannot sign for. |
| `InvalidSignResponse` | Signing returned an unusable response. |
| `InvalidUnsignedTransaction` | `unsignedTransaction` was not valid JSON and could not be parsed. |
| `NoValidators(yieldId)` | `getValidators` found none for that yield. |
| `ApiError(errorMessage)` | The backend returned an error payload. Carries the message. |

`PortalNotInitialized`, `EmptyYieldId`, `NoTransactions`, `InvalidSignResponse`, and `InvalidUnsignedTransaction` are `object` singletons — match them with `is`. The rest are data classes carrying detail.

```kotlin theme={null}
.onFailure { error ->
    when (error) {
        is YieldXyzActionException.InvalidChainId ->
            Log.e("Portal", "Use full CAIP-2, got: ${error.chain}")
        is YieldXyzActionException.AddressUnavailable ->
            Log.e("Portal", "No wallet on ${error.chain}")
        else ->
            Log.e("Portal", "Deposit failed", error)
    }
}
```

## Get Validators

Fetches the validator addresses for a native-staking yield. These are used for approval flows and for populating `arguments.validatorAddress`.

```kotlin theme={null}
suspend fun getValidators(yieldId: String): Result<List<YieldXyzValidator>>
```

Available on both the namespace and the provider — `portal.yield.getValidators(yieldId)` is a passthrough to `portal.yield.yieldxyz.getValidators(yieldId)`.

```kotlin theme={null}
lifecycleScope.launch {
    portal.yield.getValidators("monad-testnet-mon-native-staking")
        .onSuccess { validators ->
            validators.forEach { validator ->
                Log.i("Portal", "${validator.address} ${validator.name ?: "unnamed"}")
            }
        }
        .onFailure { error ->
            Log.e("Portal", "getValidators failed", error)
        }
}
```

Only `address` is non-nullable on `YieldXyzValidator`. Everything else — `name`, `logoURI`, `website`, `rewardRate`, `provider`, `commission`, `tvlUsd`, `votingPower`, `preferred`, `minimumStake`, `status`, and the rest — depends on the protocol and may be absent.

Fails with `YieldXyzActionException.NoValidators` when the response contains no validators, and `YieldXyzActionException.ApiError` when the backend returns an error payload.

### Low-level additions

Two API methods back the high-level flow and are available directly on `portal.api.yieldxyz`:

```kotlin theme={null}
suspend fun getYieldDefaults(includeOpportunities: Boolean? = null): Result<YieldXyzGetDefaultsResponse>
suspend fun getYieldValidators(yieldId: String): Result<YieldXyzGetValidatorsResponse>
```

`getYieldDefaults` returns `data` as a map keyed **`"{caip2}:{TOKEN}"`** — for example `"eip155:1:USDC"`. That key is exactly what `ByChainAndToken` resolves against, so this is how you discover valid chain and token pairs. Pass `includeOpportunities = true` to populate each entry's `opportunity` field.

<Note>
  Unlike the other Yield.xyz responses, the defaults payload is **not** wrapped in `rawResponse` — read it from `data` directly.
</Note>

## Managing Yield Positions

If your Portal client has entered into a yield balance, they may have a yield balance that has an available `pendingActions`. You can use the `manage` method to perform actions on existing yield positions. For example, if the balance has a `pendingAction` of `WITHDRAW` or `CLAIM_REWARDS`, you can use the `manage` method to withdraw or claim rewards from the yield balance.

For complete API documentation, see the [Yield.xyz manage yield reference](https://docs.yield.xyz/reference/actionscontroller_manageyield).

```kotlin theme={null}
val userAddress = portal.getAddress(PortalNamespace.EIP155) ?: return

val manageRequest = YieldXyzManageYieldRequest(
    yieldId = "ethereum-sepolia-link-aave-v3-lending",
    address = userAddress,
    action = YieldActionType.WITHDRAW, // Replace with the balance's `pendingAction` item's `type` value.
    passthrough = "eyJhZGRyZXNzZXMiOnsiYWRkcmVzcyI6ImNvc21vczF5ZXk..." // Replace with the balance's `pendingAction` item's `passthrough` value.
)

val manageResult = portal.yield.yieldxyz.manage(manageRequest)
if (manageResult.isSuccess) {
    val manageResponse = manageResult.getOrThrow()
    val rawResponse = manageResponse.data?.rawResponse
    val transactions = rawResponse?.transactions

    // Process transactions, this is described in the "Transaction Processing" section below
    processTransactions(transactions)
}
```

## Getting Historical Actions

Retrieve the history of yield actions for an address.

For complete API documentation, see the [Yield.xyz get actions reference](https://docs.yield.xyz/reference/actionscontroller_getactions).

```kotlin theme={null}
val userAddress = portal.getAddress(PortalNamespace.EIP155) ?: return

val request = YieldXyzGetHistoricalActionsRequest(address = userAddress)
val result = portal.yield.yieldxyz.getHistoricalActions(request)

if (result.isSuccess) {
    val response = result.getOrThrow()
    val rawResponse = response.data?.rawResponse
    val pastActions = rawResponse?.items

    // Process and display past yield actions
}
```

## Transaction Processing (Low-Level Enter / Exit / Manage)

If you use [`deposit`](#high-level-methods) or [`withdraw`](#high-level-methods), skip this section — the SDK already sequences transactions, waits for confirmation between steps, and reports hashes to Yield.xyz.

This section applies to manual flows built on `enter`, `exit`, or `manage`.

<Tip>
  When `deposit` and `withdraw` sign an EVM transaction they rebuild it from the yield action's `unsignedTransaction`, copying `to`, `from`, `value`, `data` and the fee fields (`gasLimit`/`gas`, `maxFeePerGas`, `maxPriorityFeePerGas`, `gasPrice`) but **deliberately omitting `nonce`**, so the MPC signer fetches the pending nonce at signing time. If you sign manually, drop the planning `nonce` the same way — reusing it across a multi-transaction action causes nonce collisions.
</Tip>

Yield operations can require multiple transactions. Process them sequentially, submit each, track it, and wait for on-chain confirmation (e.g. using `eth_getTransactionReceipt`) before proceeding to the next.

For complete API documentation, see the [Yield.xyz submit transaction hash reference](https://docs.yield.xyz/reference/transactionscontroller_submittransactionhash) and [get transaction details reference](https://docs.yield.xyz/reference/transactionscontroller_gettransaction).

<Tip>
  For account abstraction enabled Portal clients, use `eth_getUserOperationReceipt` instead of `eth_getTransactionReceipt` to wait for confirmation, since signing returns a *user operation hash*, not a transaction hash.

  If you don't specify a `yieldId` on your balance queries, you'll need to call `track` after each transaction so Yield.xyz can attribute the position. Pass the **transaction hash** (extracted from `response.result.receipt.transactionHash` for AA clients), not the user operation hash.
</Tip>

```kotlin theme={null}
fun processTransactions(transactions: List<YieldActionTransaction>?) {
    val sorted = transactions?.sortedBy { it.stepIndex } ?: emptyList()
    for (tx in sorted) {
        if (tx.unsignedTransaction != null && tx.status == YieldActionTransactionStatus.CREATED) {
            val ok = runBlocking { signAndSubmitAndConfirm(tx) }
            if (!ok) break
        }
    }
}

suspend fun signAndSubmitAndConfirm(transaction: YieldActionTransaction): Boolean {
    val unsignedTxJson = transaction.unsignedTransaction as? String ?: return false

    val gson = Gson()
    val type = object : TypeToken<Map<String, Any>>() {}.type
    val txParams = gson.fromJson<Map<String, Any>>(unsignedTxJson, type)

    val ethTransaction = EthTransactionParam(
        from = txParams["from"] as? String ?: "",
        to = txParams["to"] as? String ?: "",
        value = txParams["value"] as? String ?: "0x0",
        data = txParams["data"] as? String ?: "0x",
        gas = null, // Let Portal handle gas estimation`
        gasPrice = null,
        maxFeePerGas = null,
        maxPriorityFeePerGas = null
    )

    val send = portal.request(
        chainId = transaction.network,
        method = PortalRequestMethod.eth_sendTransaction,
        params = listOf(ethTransaction),
        options = RequestOptions(signatureApprovalMemo = "Yield transaction") // Optional signature approval memo to use for the request
    )

    val txHash = send.result as? String ?: return false

    portal.yield.yieldxyz.track(transaction.id, txHash)

    return waitForReceipt(txHash, transaction.network)
}

suspend fun waitForReceipt(txHash: String, chainId: String, maxAttempts: Int = 30, delayMs: Long = 2000): Boolean {
    repeat(maxAttempts) {
        kotlinx.coroutines.delay(delayMs)

        val receiptResp = portal.request(
            chainId = chainId,
            method = PortalRequestMethod.eth_getTransactionReceipt,
            params = listOf(txHash)
        )

        val receipt = receiptResp.result as? Map<String, Any>
        val status = receipt?.get("status") as? String
        if (status == "0x1") return true // Was mined
        if (status == "0x0") return false // Was not mined yet
    }
    return false
}
```

## Enum handling

### Unknown values

Yield.xyz aggregates many protocols and onboards new ones regularly. Before 9.1.0, a response containing an enum value the SDK did not recognize decoded to `null` rather than failing — Gson's default enum adapter returns `null` for an unrecognized name, and that `null` was written into a non-null Kotlin field. The call still returned a successful `Result`, so there was nothing to catch in `onFailure`; the failure surfaced later, as a `NullPointerException` at the point where your code read the field. A single new value from an upstream provider could crash a screen rendering `getBalances` output, far from the call that fetched it.

As of 9.1.0, unrecognized values deserialize to `UNKNOWN` instead. Your app keeps working when Yield.xyz onboards a new protocol, action type, or reward schedule, without waiting for an SDK upgrade — and the value you have to handle now shows up at the call site, in the type system, rather than as a late crash.

<Warning>
  Adding `UNKNOWN` is a source-breaking change. Any exhaustive `when` over one of the affected enums below will fail to compile until you add an `UNKNOWN` branch or an `else` branch.
</Warning>

If you arrived here from a compiler error, this is the fix:

```kotlin theme={null}
// Before 9.1.0 this compiled. Under 9.1.0 it does not — the compiler reports
// "'when' expression must be exhaustive. Add the 'UNKNOWN' branch or an 'else' branch."
when (component.yieldSource) {
    YieldXyzSource.staking -> showStakingBadge()
    YieldXyzSource.lending -> showLendingBadge()
    // … remaining values
}

// 9.1.0 — add a branch for values the SDK does not recognize.
when (component.yieldSource) {
    YieldXyzSource.staking -> showStakingBadge()
    YieldXyzSource.lending -> showLendingBadge()
    // … remaining values
    YieldXyzSource.UNKNOWN -> showGenericBadge()
}
```

Note the enum member casing: `YieldXyzSource` members are lowercase (`staking`, `lending`, `protocol_incentive`, …) while `UNKNOWN` is uppercase. That is not a typo in these docs — it is how the SDK declares them, and the same mix appears in most of the enums listed below. Write `UNKNOWN` in uppercase even when every other member of the same enum is lowercase.

<Tip>
  Prefer an explicit `UNKNOWN` branch over `else`. With `else`, the next value Portal adds falls into it silently; with an explicit `UNKNOWN`, you get a compiler error and a chance to handle the new value deliberately.
</Tip>

### Affected enums

Thirteen public enums gained `UNKNOWN` in 9.1.0, grouped here by the file that declares them so you can find the one your compiler flagged:

**From `YieldXyzGetYieldsResponse.kt`:** `YieldXyzRateType`, `YieldXyzSource`, `YieldXyzRewardSchedule`, `YieldXyzRewardClaiming`, `YieldXyzArgumentFieldName`, `YieldXyzArgumentFieldType`

**From `YieldXyzEnterYieldResponse.kt`:** `YieldXyzActionIntent`, `YieldXyzActionType`, `YieldXyzActionExecutionPattern`, `YieldXyzActionStatus`, `YieldXyzActionTransactionStatus`, `YieldXyzActionTransactionType`

**From `YieldXyzGetYieldsRequest.kt`:** `YieldXyzMechanicsType`

All thirteen live in `io.portalhq.android.api.data.yieldxyz`.

The reach extends past `discover`. `YieldXyzActionIntent`, `YieldXyzActionType`, `YieldXyzActionExecutionPattern`, and `YieldXyzActionStatus` are all fields on `YieldOpportunityDetails`, which is returned by the high-level [`deposit` and `withdraw`](#high-level-methods) methods. Code reading `result.yieldOpportunityDetails.status` is affected — note that these four fields are nullable, so an exhaustive `when` needs a `null` branch as well as an `UNKNOWN` branch:

```kotlin theme={null}
when (result.yieldOpportunityDetails.status) {
    YieldXyzActionStatus.SUCCESS -> markComplete()
    YieldXyzActionStatus.PROCESSING,
    YieldXyzActionStatus.WAITING_FOR_NEXT,
    YieldXyzActionStatus.CREATED -> keepPolling()
    YieldXyzActionStatus.FAILED,
    YieldXyzActionStatus.CANCELED,
    YieldXyzActionStatus.STALE -> markFailed()
    YieldXyzActionStatus.UNKNOWN -> showRawStatus()
    null -> showPending()
}
```

### Handling UNKNOWN in your code

1. Treat `UNKNOWN` as "display it, don't act on it". Render a neutral label rather than hiding the row, so users still see their position.
2. Don't branch business logic on `UNKNOWN`. If a decision depends on knowing the exact source or status, read the raw response value rather than inferring from the enum.
3. Log occurrences along with the `yieldId`. A rising `UNKNOWN` rate is a signal to upgrade the SDK.

### Custom Gson instances

Tolerance is implemented as a public Gson factory:

```kotlin theme={null}
class YieldXyzUnknownEnumAdapterFactory : TypeAdapterFactory
```

`YieldXyzApi` registers it on its own Gson instance, so every SDK call is tolerant out of the box and you do not need to do anything. It matters only when you build your own Gson to deserialize Yield.xyz payloads — decoding a webhook body or a cached response, for example:

```kotlin theme={null}
import com.google.gson.GsonBuilder
import io.portalhq.android.api.data.yieldxyz.YieldXyzUnknownEnumAdapterFactory

val gson = GsonBuilder()
    .registerTypeAdapterFactory(YieldXyzUnknownEnumAdapterFactory())
    .create()
```

<Warning>
  Without the factory registered, your own Gson maps an unrecognized value to `null`, which then violates the non-null Kotlin field it is assigned to and surfaces later as a `NullPointerException` — even though the SDK's own calls decode the same payload fine. Register the factory on any Gson instance you point at a Yield.xyz payload.
</Warning>

The factory applies only to enums that declare an `UNKNOWN` constant; every other enum falls through to Gson's default adapter, so registering it is safe on a shared Gson instance. Alongside unrecognized strings, it also maps JSON `null` and non-string tokens (numbers, booleans, objects, arrays) to `UNKNOWN` rather than leaving a non-null field null.

### New lending source

`YieldXyzSource` gained a `lending` member in 9.1.0. You encounter this enum on each entry of a discovered opportunity's `rewardRate.components` list, as `component.yieldSource`. The full member list as of 9.1.0:

```kotlin theme={null}
enum class YieldXyzSource {
    staking,
    restaking,
    protocol_incentive,
    points,
    lending,
    lending_interest,
    mev,
    real_world_asset_yield,
    validator_commission,
    UNKNOWN
}
```

No member carries a `@SerializedName` annotation or a constructor value, so the wire strings are the member names exactly as written above — `protocol_incentive` on the wire is `YieldXyzSource.protocol_incentive` in Kotlin.

<Note>
  `lending` and `lending_interest` are separate members and Yield.xyz uses both. Treat them as distinct values rather than assuming one supersedes the other.
</Note>

## Best Practices

1. **Always check yield availability** before attempting to enter positions
2. **Process transactions sequentially** as yield operations often require multiple steps and are dependent on previous transactions being mined successfully
3. **Handle network errors gracefully** and provide user feedback
4. **Monitor transaction status** and provide progress updates to users
5. **Validate user balances** before initiating yield operations

## Supported Networks

The yield functionality supports various networks including:

* Monad (`eip155:143`)
* Monad Testnet (`eip155:10143`)
* Arbitrum (`eip155:42161`)
* Avalanche C (`eip155:43114`)
* Base (`eip155:8453`)
* Base Sepolia (`eip155:84532`)
* Celo (`eip155:42220`)
* Core (`eip155:1116`)
* Ethereum (`eip155:1`)
* Ethereum Sepolia (`eip155:11155111`)
* Fantom (`eip155:250`)
* Gnosis (`eip155:100`)
* Harmony (`eip155:1666600000`)
* Hyperevm (`eip155:999`)
* Katana (`eip155:747474`)
* Linea (`eip155:59144`)
* Moonriver (`eip155:1285`)
* Optimism (`eip155:10`)
* Optimism Sepolia (`eip155:11155420`)
* Plasma (`eip155:9745`)
* Polygon (`eip155:137`)
* Polygon Amoy (`eip155:80002`)
* Sonic (`eip155:146`)
* Unichain (`eip155:130`)
* Viction (`eip155:88`)
* zkSync (`eip155:324`)
* Solana (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`)
* Solana Devnet (`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`)
* Stellar (`stellar:pubnet`)
* Stellar Testnet (`stellar:testnet`)
* Tron (`tron:mainnet`)

## Next Steps

* Learn about [managing wallet lifecycle states](./manage-wallet-lifecycle-states)
* Explore [transaction simulation](./evaluate-a-transaction)
* Check out [Portal API methods](./portal-api-methods)


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