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

# Enter and exit flows

> Implement a full enter, exit, and manage loop: build arguments from the schema, sign, submit, poll to a terminal status, and read balances.

Implement a complete enter and exit flow: build the request arguments from the yield's own schema, branch on how its transactions must be executed, sign and submit each one, poll to a terminal status, then read the resulting balance. Beyond the [Quickstart](/documentation/get-started/quickstart), it covers multi-transaction sequencing, the choice between the two submit endpoints, and follow-up actions needed to fully exit a position.

One yield runs through every example: `base-usdc-aave-v3-lending`, discovered by filtering `GET /v1/yields` for `network=base`, `inputToken=USDC`, and `types=lending`. Swap in whatever `id` your own discovery step returns; see [Discover yields](/guides/earn/discover-yields).

## Prerequisites

* A yield.xyz API key, sent as `x-api-key` on every request.
* A yield enabled for your project with `status.enter` (and, for the exit half of this guide, `status.exit`) `true`. See [Discover yields](/guides/earn/discover-yields).
* A signer for the target chain (an EOA, smart account, MPC, or custodial signer). This guide uses a generic `signAndBroadcast` placeholder; see [Transaction signing](/guides/transaction-signing/index) for chain-specific signing code, including EVM.

<Note>
  We construct. You sign. Every action returns unsigned transactions; yield.xyz never holds keys and never signs on your behalf. Inspect `unsignedTransaction` before signing it, and never modify its values.
</Note>

## Find and inspect the yield

Fetch the full metadata for the yield you discovered, and read its enter schema before building a request:

```bash cURL theme={null}
curl --request GET \
  --url https://api.yield.xyz/v1/yields/base-usdc-aave-v3-lending \
  --header "x-api-key: $YIELD_API_KEY"
```

```json Response (excerpt) theme={null}
{
  "id": "base-usdc-aave-v3-lending",
  "status": { "enter": true, "exit": true },
  "mechanics": {
    "arguments": {
      "enter": {
        "fields": [
          { "name": "amount", "type": "string", "required": true },
          { "name": "receiverAddress", "type": "string", "required": false },
          { "name": "executionMode", "type": "enum", "required": false, "options": ["individual", "batched"] }
        ]
      }
    },
    "entryLimits": { "minimum": "0", "maximum": null, "subsequentMinimum": null }
  }
}
```

`status.enter` must be `true` before you offer this yield for entry. `mechanics.arguments.enter.fields` is the schema you build the request from in the next step, not a fixed set of fields you can assume across yields.

## Build the enter arguments from the schema

Never hardcode `{ amount }` as the request body. Build it from `mechanics.arguments.enter.fields`, so the same code works for a yield that also requires a `validatorAddress`, an `inputToken`, or another field:

```typescript theme={null}
function buildArguments(fields: { name: string; required?: boolean }[], values: Record<string, unknown>) {
  const args: Record<string, unknown> = {};
  for (const field of fields) {
    if (field.required && values[field.name] == null) {
      throw new Error(`Missing required argument: ${field.name}`);
    }
    if (values[field.name] != null) args[field.name] = values[field.name];
  }
  return args;
}

const enterFields = yieldData.mechanics.arguments.enter.fields;
const args = buildArguments(enterFields, { amount: "100" });
// => { amount: "100" }
```

For `base-usdc-aave-v3-lending`, only `amount` is required; `receiverAddress` and `executionMode` are optional and omitted here.

## Enter a position

<Steps>
  <Step title="Request the enter action">
    Call `POST /v1/actions/enter` with the `yieldId`, the user's `address`, and the arguments you built. It returns `201 Created` with an action and its ordered transactions.

    <CodeGroup>
      ```bash cURL theme={null}
      curl --request POST \
        --url https://api.yield.xyz/v1/actions/enter \
        --header "x-api-key: $YIELD_API_KEY" \
        --header 'content-type: application/json' \
        --data '{
          "yieldId": "base-usdc-aave-v3-lending",
          "address": "0xYOUR_WALLET_ADDRESS",
          "arguments": { "amount": "100" }
        }'
      ```

      ```typescript TypeScript theme={null}
      const res = await fetch('https://api.yield.xyz/v1/actions/enter', {
        method: 'POST',
        headers: {
          'x-api-key': process.env.YIELD_API_KEY!,
          'content-type': 'application/json',
        },
        body: JSON.stringify({
          yieldId: 'base-usdc-aave-v3-lending',
          address: '0xYOUR_WALLET_ADDRESS',
          arguments: { amount: '100' },
        }),
      });
      const action = await res.json(); // 201 Created
      ```
    </CodeGroup>

    ```json Response (excerpt) theme={null}
    {
      "id": "action_123abc",
      "intent": "enter",
      "type": "STAKE",
      "status": "CREATED",
      "executionPattern": "synchronous",
      "transactions": [
        { "id": "tx_123abc", "stepIndex": 0, "type": "APPROVAL", "status": "CREATED", "unsignedTransaction": "SERIALIZED_APPROVAL_TX" },
        { "id": "tx_456def", "stepIndex": 1, "type": "SUPPLY", "status": "CREATED", "unsignedTransaction": "SERIALIZED_SUPPLY_TX" }
      ]
    }
    ```

    Entering USDC lending on Aave needs an ERC-20 approval before the supply call, so this action returns two transactions. The `unsignedTransaction` values above are placeholders for the serialized payloads the API returns; pass each one to your signer exactly as received.
  </Step>

  <Step title="Branch on executionPattern">
    `action.executionPattern` decides how you drive `transactions`, sorted by `stepIndex`:

    | `executionPattern` | Drive it by |
    | - | - |
    | `synchronous` | Signing and submitting one transaction at a time, waiting for each to reach a terminal transaction status before the next |
    | `asynchronous` | Signing and broadcasting every transaction in `stepIndex` order without waiting for confirmation between them, then polling each |
    | `batch` | Signing and broadcasting the single transaction in the array; there is no sequencing to wait on |

    Actions currently come back as `synchronous` or `batch`; handle `asynchronous` too so your code keeps working if a yield starts returning it. `base-usdc-aave-v3-lending` returns `synchronous` here: the supply transaction depends on the approval being confirmed first. Skip any transaction whose `status` is already terminal (`CONFIRMED`, `FAILED`, or `SKIPPED`, for example an approval skipped because an existing allowance already covers the amount) instead of signing it again.

    ```typescript theme={null}
    const TERMINAL = new Set(['CONFIRMED', 'FAILED', 'SKIPPED']);
    const ordered = action.transactions.sort((a, b) => a.stepIndex - b.stepIndex);

    if (action.executionPattern === 'synchronous') {
      for (const tx of ordered) {
        if (TERMINAL.has(tx.status)) continue;
        await runTransaction(tx); // sign, submit, poll to terminal (Step 3)
      }
    } else {
      // asynchronous or batch: still sign and broadcast in stepIndex order,
      // but don't wait for each confirmation before the next
      const pending = ordered.filter((tx) => !TERMINAL.has(tx.status));
      const ids = [];
      for (const tx of pending) ids.push(await submitTransaction(tx)); // one at a time, in order
      await Promise.all(ids.map((id) => pollToTerminal(id)));
    }
    ```
  </Step>

  <Step title="Sign, choose a submit path, and poll">
    For each transaction: sign `unsignedTransaction`, then choose one of the two submit endpoints, never both for the same transaction.

    * `POST /v1/transactions/{transactionId}/submit`, with `signedTransaction`: hand yield.xyz the signed transaction and it broadcasts. Useful when your backend has no RPC access of its own.
    * `PUT /v1/transactions/{transactionId}/submit-hash`, with `hash`: broadcast the signed transaction through your own infrastructure, then report the resulting hash.

    ```typescript theme={null}
    async function runTransaction(tx: { id: string; unsignedTransaction: string | object | null }) {
      await submitTransaction(tx);
      return pollToTerminal(tx.id);
    }

    async function submitTransaction(tx: { id: string; unsignedTransaction: string | object | null }) {
      // Path A: you broadcast, then report the hash
      const hash = await signAndBroadcast(tx.unsignedTransaction);
      await fetch(`https://api.yield.xyz/v1/transactions/${tx.id}/submit-hash`, {
        method: 'PUT',
        headers: { 'x-api-key': process.env.YIELD_API_KEY!, 'content-type': 'application/json' },
        body: JSON.stringify({ hash }),
      });

      // Path B (alternative, not both): hand yield.xyz the signed transaction
      // const signedTransaction = await sign(tx.unsignedTransaction);
      // await fetch(`https://api.yield.xyz/v1/transactions/${tx.id}/submit`, {
      //   method: 'POST',
      //   headers: { 'x-api-key': process.env.YIELD_API_KEY!, 'content-type': 'application/json' },
      //   body: JSON.stringify({ signedTransaction }),
      // });

      return tx.id;
    }

    async function pollToTerminal(transactionId: string, { intervalMs = 4_000, timeoutMs = 300_000 } = {}) {
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        const res = await fetch(`https://api.yield.xyz/v1/transactions/${transactionId}`, {
          headers: { 'x-api-key': process.env.YIELD_API_KEY! },
        });
        const tx = await res.json();
        if (TERMINAL.has(tx.status)) {
          if (tx.status === 'FAILED') {
            throw new Error(`Transaction ${transactionId} failed; request a new action instead of retrying it`);
          }
          return tx;
        }
        await new Promise((r) => setTimeout(r, intervalMs));
      }
      throw new Error(`Timed out waiting for transaction ${transactionId}`);
    }
    ```

    Poll `GET /v1/transactions/{id}` (or `GET /v1/actions/{id}` for the whole action) every few seconds with a bounded overall timeout until the status is terminal. Polling always works; [webhooks](/documentation/advanced-setup/webhooks) can push status changes too, but keep polling as the fallback. Submitting a different hash for a transaction that is already terminal returns `412 Precondition Failed`; treat that as already settled rather than retrying.
  </Step>
</Steps>

## Verify the balance

Once the last transaction confirms, read the resulting position:

```bash cURL theme={null}
curl --request POST \
  --url https://api.yield.xyz/v1/yields/base-usdc-aave-v3-lending/balances \
  --header "x-api-key: $YIELD_API_KEY" \
  --header 'content-type: application/json' \
  --data '{ "address": "0xYOUR_WALLET_ADDRESS" }'
```

```json Response (excerpt) theme={null}
{
  "balances": [
    { "type": "active", "amount": "100", "isEarning": true, "pendingActions": [] }
  ]
}
```

An `active` balance with no `pendingActions` means the position is fully entered with nothing to follow up on. See [Balances and positions](/documentation/earn/core-concepts/balances-positions) for every balance type and field.

## Exit a position

Exit follows the same request, sign, submit, poll loop as enter, through `POST /v1/actions/exit` and its own argument schema (`mechanics.arguments.exit`, not `.enter`). For `base-usdc-aave-v3-lending`, that schema offers `amount`, `useMaxAmount`, and `receiverAddress`:

```bash cURL theme={null}
curl --request POST \
  --url https://api.yield.xyz/v1/actions/exit \
  --header "x-api-key: $YIELD_API_KEY" \
  --header 'content-type: application/json' \
  --data '{
    "yieldId": "base-usdc-aave-v3-lending",
    "address": "0xYOUR_WALLET_ADDRESS",
    "arguments": { "useMaxAmount": true }
  }'
```

`useMaxAmount: true` redeems the full position instead of a specific `amount`, when the schema offers it, as this one does. The response is another `ActionDto` with its own `executionPattern` and `transactions`: run it through the same branch, sign, submit, and poll steps as enter. Aave has no cooldown, so exiting this yield settles directly, with no intermediate balance state to wait out.

<Warning>
  Some yields do have a cooldown or unbonding period. `ethena-usde-staking`, for example, holds an exited position as an `exiting` balance with no pending actions during a 7-day cooldown; once it ends, the balance becomes `withdrawable` with a `CLAIM_UNSTAKED` pending action that you submit to receive the USDe. See [Vaults](/guides/earn/defi/vaults). Always check the returned balance's `type` and `pendingActions` after an exit rather than assuming funds are immediately withdrawn.
</Warning>

## Complete a pending action (manage)

When an exit (or an entered position generally) leaves a `pendingActions` entry on a balance, for example that `CLAIM_UNSTAKED` after a cooldown, or a routine `CLAIM_REWARDS`, execute it with `POST /v1/actions/manage`. Pass the pending action's own `type` as `action` and its `passthrough` unchanged (copied from the balances response, never constructed):

```bash cURL theme={null}
curl --request POST \
  --url https://api.yield.xyz/v1/actions/manage \
  --header "x-api-key: $YIELD_API_KEY" \
  --header 'content-type: application/json' \
  --data '{
    "yieldId": "ethena-usde-staking",
    "address": "0xYOUR_WALLET_ADDRESS",
    "action": "CLAIM_UNSTAKED",
    "passthrough": "PASSTHROUGH_FROM_THE_BALANCE"
  }'
```

The response is again an `ActionDto` with `transactions[]`: run the same branch, sign, submit, and poll loop as enter and exit. See [Balances and positions](/documentation/earn/core-concepts/balances-positions#pending-actions) for where `pendingActions` come from.

## Errors that block the flow

| Condition | What you see | What to do |
| - | - | - |
| Yield not enabled for your project | `400 Bad Request` from any per-yield call | Re-run discovery; don't call an action or balance endpoint on a stale or unverified `yieldId`. See [Discover yields](/guides/earn/discover-yields) |
| `status.enter` (or `status.exit`) is `false` | `412 Precondition Failed` from the action call (for example "currently closed for new deposits") | Check `status` before showing the button, so users never reach this error |
| A transaction reaches `FAILED` | Terminal transaction status | Request a new action; don't resubmit or modify the failed transaction |
| `submit-hash` on an already-terminal transaction with a different hash | `412 Precondition Failed` | Treat it as already settled; fetch the transaction instead of resubmitting |
| An action makes no progress for 30 minutes | Action `status` becomes `STALE` (not final: a late broadcast can still complete it) | If nothing was broadcast, request a new action. If a transaction was broadcast, check it on-chain before requesting another, so it can't settle twice. See [Monitoring and alerting](/guides/earn/monitoring-alerting) |

## Next steps

<Columns cols={2}>
  <Card title="Transaction signing" icon="pen-nib" href="/guides/transaction-signing/index">
    Chain-specific signing code for EVM, Solana, Cosmos, and Tron.
  </Card>

  <Card title="Tracking balances" icon="wallet" href="/guides/earn/tracking-balances">
    Build portfolio and position views across multiple yields.
  </Card>

  <Card title="Monitoring and alerting" icon="bell" href="/guides/earn/monitoring-alerting">
    Keep action and position state current after submission.
  </Card>

  <Card title="Lending" icon="hand-holding-dollar" href="/guides/earn/defi/lending">
    Apply this loop to a lending market, from supply to withdrawal.
  </Card>
</Columns>


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