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

# Earn with Trails

> Discover an Earn market, deposit its required asset, read the resulting position, and withdraw with Trails and OMS Wallet.

Use the Trails Earn APIs to discover current markets and prepare deposits and withdrawals, then submit the returned transactions with an active OMS Wallet session. This guide uses a Polygon vault as a concrete example.

<Note>
  For a prebuilt React interface with market selection, deposits, and withdrawals, render the [Trails Earn component](https://docs.trails.build/sdk/modes/earn). The widget can manage wallet connections itself or share an app-owned wallet through [Trails adapters](https://docs.trails.build/sdk/adapters/overview). For example, you can share an OMS Wallet session through the [OMS Wallet connector](/wallets/sdk/typescript/using-with-wagmi-connector) and the [Trails wagmi adapter](https://docs.trails.build/sdk/adapters/wagmi). This guide instead uses OMS Wallet directly and calls the Earn APIs without the widget.
</Note>

## Prerequisites

* An active OMS Wallet session from the [TypeScript quickstart](/wallets/sdk/typescript/quickstart)
* A [Trails API key](https://dashboard.trails.build/landing)
* A balance of the selected market's input asset and enough balance in an available fee token when the transaction is not sponsored

<Warning>
  This guide submits mainnet transactions with real funds. Earn markets differ in provider, rate, fees, liquidity, and risk. Present the selected market's current details before asking the user to confirm a deposit.
</Warning>

## Install the Trails packages

```bash theme={null}
pnpm add @polygonlabs/oms-wallet @0xtrails/api 0xtrails viem
```

## Discover an Earn market

Do not hardcode a market ID. Fetch the current catalog and present the available markets for the user to select:

```typescript theme={null}
import { TrailsApi } from '@0xtrails/api'
import {
  getEarnBalances,
  getEarnMarkets,
} from '0xtrails/actions'
import {
  FeeOptionSelector,
  Networks,
  TransactionStatus,
} from '@polygonlabs/oms-wallet'
import {
  type Address,
  type Hex,
} from 'viem'

const trailsClient = new TrailsApi('YOUR_TRAILS_API_KEY')
const walletAddress = omsWallet.wallet.walletAddress as Address
const selectedNetwork = Networks.polygon

const markets = await getEarnMarkets(
  {
    chain: selectedNetwork.id,
    type: 'vault',
    limit: 50,
  },
  trailsClient,
)

const enterableMarkets = markets.items.filter(
  (candidate) =>
    candidate.status.enter && candidate.inputTokens.length > 0,
)

for (const candidate of enterableMarkets) {
  console.log({
    marketId: candidate.id,
    name: candidate.metadata.name,
    provider: candidate.providerId,
    inputAssets: candidate.inputTokens.map((token) => token.symbol),
    rate: candidate.rewardRate.total,
    tvlUsd: candidate.statistics?.tvlUsd,
    risk: candidate.risk?.ratings,
  })
}

// Use the market selected in your UI.
const selectedMarketId = 'MARKET_ID'
const market = enterableMarkets.find(
  (candidate) => candidate.id === selectedMarketId,
)

if (!market) {
  throw new Error('The selected market is not currently available.')
}

const inputToken = market.inputTokens[0]

if (!inputToken) {
  throw new Error('The selected market does not have an input asset.')
}
```

This example lists vaults and uses the selected market's first supported input asset to keep the flow focused. Use `type: 'lending'` to list lending markets, or add `search: 'USDC'` to filter by asset. Refresh the catalog before preparing a deposit because market availability can change. See [Earn markets and providers](https://docs.trails.build/sdk/composable-actions/markets-and-providers) for other filters.

## Prepare and submit a deposit

Pass a human-readable amount and the selected market's input asset to `yieldCreateEnterAction`. Trails may return more than one unsigned transaction, such as an approval followed by the deposit. Submit every non-message transaction in order:

```typescript theme={null}
const deposit = await trailsClient.yieldCreateEnterAction({
  earnMarketId: market.id,
  userWalletAddress: walletAddress,
  args: {
    amount: '10',
    inputToken: inputToken.address ?? inputToken.symbol,
    inputTokenNetwork: inputToken.network,
    receiverAddress: walletAddress,
  },
})

for (const item of deposit.action.transactions) {
  if (item.isMessage) continue

  const unsigned =
    typeof item.unsignedTransaction === 'string'
      ? JSON.parse(item.unsignedTransaction)
      : item.unsignedTransaction

  if (!unsigned?.to || Number(unsigned.chainId) !== selectedNetwork.id) {
    throw new Error('Trails returned an unsupported deposit transaction.')
  }

  const tx = await omsWallet.wallet.sendTransaction({
    network: selectedNetwork,
    to: unsigned.to as Address,
    data: (unsigned.data ?? '0x') as Hex,
    value: unsigned.value == null ? 0n : BigInt(unsigned.value),
    selectFeeOption: FeeOptionSelector.firstAvailable,
  })

  if (tx.status !== TransactionStatus.Executed || !tx.txnHash) {
    throw new Error(`The deposit transaction ended with ${tx.status}.`)
  }

  console.log('Deposit transaction:', { txnId: tx.txnId, txnHash: tx.txnHash })
}
```

Each call to `sendTransaction` has its own OMS transaction lifecycle. Confirm a successful terminal status before submitting the next transaction in the sequence.

## Read Earn positions

Earn balance reads do not require the active session and can query any wallet address. Request the wallet's positions on the selected network and match each `yieldId` to the current market catalog:

```typescript theme={null}
const balances = await getEarnBalances(
  {
    queries: [
      {
        address: walletAddress,
        network: market.network,
      },
    ],
  },
  trailsClient,
)

for (const error of balances.errors) {
  console.error(`Could not read ${error.yieldId}: ${error.error}`)
}

function getPositiveEarnBalance(
  position: (typeof balances.items)[number],
) {
  if (
    position.outputTokenBalance &&
    BigInt(position.outputTokenBalance.amountRaw) > 0n
  ) {
    return position.outputTokenBalance
  }

  return position.balances.find((item) => BigInt(item.amountRaw) > 0n)
}

for (const position of balances.items) {
  const balance = getPositiveEarnBalance(position)

  if (!balance) continue

  const currentMarket = markets.items.find(
    (candidate) => candidate.id === position.yieldId,
  )

  console.log({
    marketId: position.yieldId,
    name: currentMarket?.metadata.name ?? position.yieldId,
    amount: balance.amount,
    symbol: balance.token.symbol,
    amountUsd: balance.amountUsd,
    rate: position.rewardRate?.total ?? currentMarket?.rewardRate.total,
  })
}
```

Treat the returned rate and USD value as current display data. Refresh the position after a deposit or withdrawal reaches a successful terminal status.

## Withdraw a position

Use a positive balance from the previous response and verify that its current market permits exits. This example withdraws the full displayed position:

```typescript theme={null}
const position = balances.items.find(
  (item) => item.yieldId === market.id,
)
const balance = position
  ? getPositiveEarnBalance(position)
  : undefined

if (!position || !balance || !market.status.exit) {
  throw new Error('The selected Earn position is not withdrawable.')
}

const withdrawal = await trailsClient.yieldCreateExitAction({
  earnMarketId: position.yieldId,
  userWalletAddress: walletAddress,
  args: {
    amount: balance.amount,
    outputToken: balance.token.address ?? balance.token.symbol,
    outputTokenNetwork: balance.token.network,
  },
})

for (const item of withdrawal.action.transactions) {
  if (item.isMessage) continue

  const unsigned =
    typeof item.unsignedTransaction === 'string'
      ? JSON.parse(item.unsignedTransaction)
      : item.unsignedTransaction

  if (!unsigned?.to || Number(unsigned.chainId) !== selectedNetwork.id) {
    throw new Error('Trails returned an unsupported withdrawal transaction.')
  }

  const tx = await omsWallet.wallet.sendTransaction({
    network: selectedNetwork,
    to: unsigned.to as Address,
    data: (unsigned.data ?? '0x') as Hex,
    value: unsigned.value == null ? 0n : BigInt(unsigned.value),
    selectFeeOption: FeeOptionSelector.firstAvailable,
  })

  if (tx.status !== TransactionStatus.Executed || !tx.txnHash) {
    throw new Error(`The withdrawal transaction ended with ${tx.status}.`)
  }

  console.log('Withdrawal transaction:', { txnId: tx.txnId, txnHash: tx.txnHash })
}
```

Refresh the Earn balances after the final withdrawal transaction succeeds. Preserve each OMS `txnId` so you can resolve an uncertain transaction status before retrying.

## Combine a swap and deposit

To swap into a market's input asset and deposit it in one same-chain transaction, use Trails composable `swap` and `lend` or `deposit` actions with `dynamic()`. This action-based flow is separate from the routed intent lifecycle in the [swap guide](/wallets/sdk/guides/swap).

See [building composable actions](https://docs.trails.build/sdk/composable-actions/building-actions) for the action builders and the [Trails actions example](https://github.com/0xPolygon/oms-wallet-typescript-sdk/tree/master/examples/trails-actions) for a complete OMS Wallet implementation.
