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

# Earn error handling

> Handle errors, retries, and rate limits when calling Earn

Every error thrown by Earn is a `KitError` carrying structured fields. Read
those fields to decide how to handle each error, instead of parsing error
strings. Errors surface from every Earn operation, including
[deposit](/app-kit/quickstarts/earn-deposit),
[withdraw](/app-kit/quickstarts/earn-withdraw), and
[preview quotes](/app-kit/tutorials/earn/preview-operations).

## `KitError` fields

| Field            | Type    | Description                                                                                                                    |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `code`           | number  | Numeric identifier (for example, `1100`).                                                                                      |
| `name`           | string  | Stable string identifier (for example, `EARN_VAULT_NOT_FOUND`).                                                                |
| `type`           | string  | Error category (see [Error types](#error-types)).                                                                              |
| `recoverability` | string  | `FATAL` (don't retry), `RETRYABLE` (back off and retry), or `RESUMABLE` (resume from where it stopped using `kit.earn.retry`). |
| `message`        | string  | Human-readable description, safe to log and display.                                                                           |
| `cause.trace`    | unknown | Optional raw error context.                                                                                                    |

Check `code` or `name` for specific handling, `type` for handling a whole
category, and `recoverability` to decide whether to retry.

## Error types

| Type         | Origin                                                                     | Typical recoverability |
| ------------ | -------------------------------------------------------------------------- | ---------------------- |
| `INPUT`      | Parameter validation, unsupported blockchain or vault, signature rejected. | `FATAL`                |
| `ONCHAIN`    | Deposit or withdraw transaction reverted onchain.                          | `FATAL`                |
| `BALANCE`    | Insufficient token, gas, or allowance (surfaced from the wallet adapter).  | `FATAL`                |
| `RPC`        | Blockchain RPC issues during execution (surfaced from the wallet adapter). | `RETRYABLE`            |
| `NETWORK`    | Connectivity or timeout reaching the Earn service.                         | `RETRYABLE`            |
| `RATE_LIMIT` | Request throttled (HTTP 429).                                              | `RETRYABLE`            |
| `SERVICE`    | Earn service backend failure (5xx, signing, internal).                     | `RETRYABLE`            |
| `UNKNOWN`    | Unrecognized error.                                                        | `FATAL`                |

## Common Earn error codes

Check these codes for specific handling. Reference the JavaScript constant as
`EarnError.<NAME>.code`. The underlying `error.name` includes the `EARN_`
prefix.

| Code   | Name (`error.name`)             | Constant (`EarnError`)     | When it happens                                                                           |
| ------ | ------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------- |
| `1100` | `EARN_VAULT_NOT_FOUND`          | `VAULT_NOT_FOUND`          | Vault address does not exist for the given blockchain.                                    |
| `1101` | `EARN_UNSUPPORTED_CHAIN`        | `UNSUPPORTED_CHAIN`        | Chain is not supported for Earn operations.                                               |
| `1102` | `EARN_UNSUPPORTED_VAULT`        | `UNSUPPORTED_VAULT`        | Vault's underlying asset or protocol is not supported.                                    |
| `1103` | `EARN_SIGNATURE_REJECTED`       | `SIGNATURE_REJECTED`       | Earn service could not verify the submitted signature.                                    |
| `1104` | `EARN_INVALID_INPUT`            | `INVALID_INPUT`            | Generic input validation failure.                                                         |
| `1105` | `EARN_UNSUPPORTED_BRIDGE_ROUTE` | `UNSUPPORTED_BRIDGE_ROUTE` | Crosschain bridge route is not configured for the source and destination blockchain pair. |
| `1106` | `EARN_BRIDGE_QUOTE_EXPIRED`     | `BRIDGE_QUOTE_EXPIRED`     | Crosschain quote expired. Start a new quote instead of retrying.                          |
| `8100` | `EARN_SIGNING_FAILED`           | `SIGNING_FAILED`           | Backend signing call failed.                                                              |
| `8101` | `EARN_PROVIDER_ERROR`           | `PROVIDER_ERROR`           | Upstream vault protocol provider request failed.                                          |
| `8102` | `EARN_REWARDS_FETCH_FAILED`     | `REWARDS_FETCH_FAILED`     | Reward data fetch failed. Retry with backoff.                                             |
| `8103` | `EARN_INTERNAL_ERROR`           | `INTERNAL_ERROR`           | Internal Earn service error.                                                              |
| `8104` | `EARN_PAUSED`                   | `PAUSED`                   | Earn interactions are temporarily paused. Retry with backoff.                             |
| `8105` | `EARN_POSITION_PNL_PENDING`     | `POSITION_PNL_PENDING`     | P\&L is still reconciling for this position. Retry with backoff.                          |

## Helper functions

Import error utilities from `@circle-fin/app-kit` (or `@circle-fin/earn-kit` if
you're using only the standalone Earn Kit):

```typescript TypeScript theme={null}
import {
  isKitError,
  isInputError,
  isRetryableError,
  isResumableError,
  isFatalError,
  getErrorCode,
  getErrorMessage,
  EarnError,
} from "@circle-fin/app-kit";
```

| Helper                    | Returns        | Use for                                                                               |
| ------------------------- | -------------- | ------------------------------------------------------------------------------------- |
| `isKitError(error)`       | type guard     | Narrow `unknown` before reading `.code` or `.type`.                                   |
| `isInputError(error)`     | boolean        | Check `type === 'INPUT'` (fix inputs and call again).                                 |
| `isFatalError(error)`     | boolean        | `recoverability === 'FATAL'` (don't retry).                                           |
| `isRetryableError(error)` | boolean        | `recoverability === 'RETRYABLE'` (back off and retry the full operation).             |
| `isResumableError(error)` | boolean        | `recoverability === 'RESUMABLE'` (resume from the failed step with `kit.earn.retry`). |
| `getErrorCode(error)`     | number \| null | Get the error code.                                                                   |
| `getErrorMessage(error)`  | string         | Safely extract the error message.                                                     |
| `EarnError`               | const map      | Code and name constants for branching.                                                |

## Handling errors

This example shows a complete error handling pattern for a deposit, including a
check for a specific error code:

```typescript TypeScript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
import {
  isKitError,
  isInputError,
  isRetryableError,
  isFatalError,
  getErrorCode,
  getErrorMessage,
  EarnError,
} from "@circle-fin/app-kit";

const kit = new AppKit();

const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
});

async function depositWithRetry(
  vaultAddress: string,
  amount: string,
  maxRetries = 3,
): Promise<void> {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      const result = await kit.earn.deposit({
        from: { adapter, chain: "Arc_Testnet" },
        vaultAddress,
        amount,
      });

      console.log("Deposit succeeded:", result.txHash);
      return;
    } catch (error) {
      if (!isKitError(error)) throw error;

      const code = getErrorCode(error);
      const message = getErrorMessage(error);

      // Specific code: tailored handling
      if (code === EarnError.VAULT_NOT_FOUND.code) {
        console.error(`Vault not found: ${vaultAddress}`);
        throw error;
      }

      if (isInputError(error)) {
        // Fix your parameters. Retrying with the same inputs won't help
        console.error(`Input error [${code}]: ${message}`);
        throw error;
      }

      if (isFatalError(error)) {
        // Cannot be recovered. Stop immediately
        console.error(`Fatal error [${code}]: ${message}`);
        throw error;
      }

      if (isRetryableError(error)) {
        attempt++;
        if (attempt >= maxRetries) {
          console.error(
            `Max retries reached. Last error [${code}]: ${message}`,
          );
          throw error;
        }
        // Exponential backoff: 1s, 2s, 4s
        const delay = Math.pow(2, attempt - 1) * 1000;
        console.warn(
          `Retryable error [${code}]: ${message}. Retrying in ${delay}ms...`,
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }
}
```

## Resume a failed operation with `kit.earn.retry`

Deposits and withdrawals run through multiple phases (`fetchParams`, `approve`,
`execute`). When a failure has `recoverability === 'RESUMABLE'`, the operation
stopped mid-flow with some phases already complete. Call `kit.earn.retry(error)`
to resume from the failed step instead of restarting from scratch. Reuse the
caught `KitError`. It carries the original inputs and step progress.

```typescript TypeScript theme={null}
try {
  await kit.earn.deposit({
    from: { adapter, chain: "Arc_Testnet" },
    vaultAddress,
    amount: "10.00",
  });
} catch (error) {
  if (isKitError(error) && isResumableError(error)) {
    await kit.earn.retry(error);
  } else {
    throw error;
  }
}
```

Use `isRetryableError` for errors where the full operation can be retried from
the beginning. Use `isResumableError` when the operation should continue from
the failed step.

Retry re-fetches execution parameters and might re-submit the execute
transaction. Treat it as best-effort recovery. If a prior attempt broadcast the
execute transaction but failed before observing the receipt, that transaction
might still be in flight.

## Observe step events

App Kit forwards Earn step events to handlers registered on the kit. Use them to
render progress or log which phase a failure occurred in. Register a handler
with `kit.on()`; detach it with `kit.off()`.

```typescript TypeScript theme={null}
kit.on("earn.deposit", (payload) => {
  console.log(`Deposit ${payload.method}: ${payload.values.state}`);
});

kit.on("earn.approve", (payload) => {
  if (payload.values.state === "success") {
    console.log("Approval tx:", payload.values.txHash);
  }
});

// Match all earn events
kit.on("earn.*", (payload) => {
  /* ... */
});
```

Available action names: `earn.approve`, `earn.deposit`, `earn.withdraw`,
`earn.crossChainDeposit`, `earn.crossChainDepositStatus`. Each payload carries
the operation, method (phase), and `values` for that step.

## Partial success in `getVaults`

`getVaults` uses partial success semantics: a network, auth, or service failure
throws a `KitError`, but a single vault that can't be resolved is returned in
the `errors` array instead of throwing. One bad vault doesn't fail the whole
batch.

```typescript TypeScript theme={null}
const { vaults, errors } = await kit.earn.getVaults({
  vaults: [
    { chain: "Arc_Testnet", vaultAddress: "0x8eB67..." },
    { chain: "Arc_Testnet", vaultAddress: "0xBEEF..." },
  ],
});

for (const e of errors) {
  console.warn(`Vault ${e.vaultAddress} failed: [${e.code}] ${e.message}`);
}
```

## Errors from your wallet adapter

`deposit` and `withdraw` submit transactions through the wallet adapter you
supply (for example, `@circle-fin/adapter-viem-v2`). Wallet and RPC failures
during execution surface as `KitError`s with these type categories:

* `BALANCE` (codes in the 9000s): insufficient token balance, gas, or allowance.
  `FATAL`.
* `ONCHAIN` (codes in the 5000s): simulation or execution failures. `FATAL`.
* `RPC` (codes in the 4000s): RPC endpoint or nonce issues. `RETRYABLE`.
* `INPUT_USER_CANCELLED` (`1099`): user rejected the wallet prompt. `FATAL`.

Handle them by `type` and `recoverability` rather than enumerating every code.

## Common error scenarios

| Scenario                       | Type         | Resolution                                                                    |
| ------------------------------ | ------------ | ----------------------------------------------------------------------------- |
| Invalid or zero deposit amount | `INPUT`      | Pass a positive USDC amount as a decimal string (for example, `"10.00"`).     |
| Vault address is not valid     | `INPUT`      | Confirm the vault address from your onboarding details.                       |
| Vault is not active            | `INPUT`      | Check `vault.status` using `getVaults` before depositing.                     |
| Insufficient USDC balance      | `BALANCE`    | Ensure the wallet holds enough USDC to cover the deposit amount.              |
| RPC or network timeout         | `NETWORK`    | Retry with exponential backoff. Consider using a dedicated RPC provider.      |
| Rate limit exceeded            | `RATE_LIMIT` | Retry with backoff. See [Rate limiting](#rate-limiting) below.                |
| Contract execution failed      | `ONCHAIN`    | Check the transaction on a block explorer. Don't retry without investigating. |

## Rate limiting

Rate limit responses are `RATE_LIMIT` errors with `RETRYABLE` recoverability.
Apply the backoff-and-retry pattern from the deposit example.

Earn rate limits are enforced at multiple layers; all relevant layers must be
satisfied for a request to succeed.

| Scope                      | Limit        | Window   | Notes                                               |
| -------------------------- | ------------ | -------- | --------------------------------------------------- |
| Per entity (authenticated) | 10 requests  | 1 second | Keyed by entity ID from the API key. Requires key.  |
| Per IP address             | 5 requests   | 1 second | Keyed by client IP. Applies to all requests.        |
| Global safeguard           | 100 requests | 1 second | Total throughput cap across all clients.            |
| Per wallet address         | 10 requests  | 1 second | Applies to every Earn operation except `getVaults`. |

In permissionless mode (no API key), only the per-IP and global limits apply.
Using an API key adds the per-entity limit, which gives more headroom for
server-side integrations sending traffic from multiple IPs.

To avoid hitting rate limits in production:

* Obtain a [Circle API key](https://developers.circle.com/api-reference/keys)
  from the [Circle Console](https://console.circle.com/api-keys) and pass it on
  each operation as `config.apiKey`. For a restricted key, select the "App Kits"
  product permission when creating the key.
* Cache `getVaults` results. Vault metadata changes infrequently. Refreshing
  every 30 to 60 seconds is sufficient.
* Batch vault address lookups. Pass up to 20 addresses in a single `getVaults`
  call instead of calling it once per address.
