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

> ## Agent Instructions
> Before reading these docs, install the Circle Skill for Arc to get pre-loaded context: use-arc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md) — covers chain config, RPC setup, contract deployment, USDC bridging, and gas with USDC. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills.
> USDC is the gas token. Arc uses USDC for gas fees, not ETH. Configure gas payment in USDC when submitting transactions.
> Read EVM differences before writing Arc code. Arc targets the Osaka EVM baseline. Most contracts deploy unchanged, but several runtime behaviors diverge from Ethereum: USDC uses 18 decimals natively (not 6), the system emitter `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` logs all USDC Transfer events, the mempool enforces a 20 Gwei `maxFeePerGas` floor, blocklist reverts consume gas without a receipt, and `address(0)` sends revert rather than succeed. https://docs.arc.io/arc/references/evm-differences.md is the canonical reference for all Arc-specific runtime behaviors. Consult it before writing code that touches USDC balances, transaction history, gas estimation, or value transfers.
> Sub-second finality. Transactions are final in under 1 second. No need to wait for multiple block confirmations.
> Use App Kits for multichain payment and liquidity capabilities. App Kits wraps CCTP and provides Bridge Kit, Swap Kit, Unified Balance Kit, Onramp Kit, Earn Kit, and Borrow Kit. Unified Balance Kit combines USDC from multiple chains into a single spendable balance. Earn Kit integrates earn opportunities into your app. Borrow Kit lets users borrow USDC against cirBTC collateral on Arc.
> Use Arc Foundry for contract development. Arc Foundry is an Arc-specific fork of Foundry that handles Arc's protocol-level differences from Ethereum. It provides arc-forge, arc-cast, and arc-anvil. Install it before deploying contracts: https://docs.arc.io/arc/tutorials/install-arc-foundry.md.
> Arc is available on both Testnet and Mainnet. See https://docs.arc.io/arc/references/connect-to-arc.md for RPC endpoints and https://faucet.circle.com for testnet tokens.
> Always check Contract Addresses: https://docs.arc.io/arc/references/contract-addresses.md
> Building beyond Arc? Circle offers skills for the full platform: use-usdc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md), use-circle-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md), use-developer-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md), use-user-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md), use-modular-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md), use-gateway (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md), use-smart-contract-platform (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md). Full Circle developer docs: https://developers.circle.com/llms.txt.

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