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

# Troubleshooting

> Common issues and fixes when integrating @paxoslabs/amplify-sdk 1.0.0

Practical "I hit X, what's wrong" guide for the 1.0.0 SDK. Every recipe assumes you have an `AmplifyClient` constructed like this:

```ts theme={null}
import { AmplifyClient } from '@paxoslabs/amplify-sdk'

const client = new AmplifyClient({
  apiKey: process.env.PAXOS_LABS_API_KEY!,
})
```

If you're upgrading from 0.5.x, start with the [migration guide](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) — the public API has changed substantially.

<AccordionGroup>
  <Accordion title="Cannot find module '@paxoslabs/amplify-sdk'">
    **Cause:** Package isn't installed, or it's installed but TypeScript can't resolve it.

    **Fix:**

    ```bash theme={null}
    pnpm add @paxoslabs/amplify-sdk
    # or: npm i @paxoslabs/amplify-sdk
    # or: yarn add @paxoslabs/amplify-sdk
    ```

    There are no required peer dependencies — the SDK ships its own HTTP layer. You still need a wallet/RPC client (e.g. `viem`, `wagmi`, `ethers`) to submit the calldata returned by `prepare` / `prepare`, but those are your choice.

    If installation succeeds but the import still fails, see "Types resolve to `any`" below — it's almost always a `moduleResolution` issue.
  </Accordion>

  <Accordion title="AmplifyError: 401 Unauthorized">
    **Cause:** API key is missing, malformed, or wrong for the environment you're hitting.

    The SDK sends the key as the `x-api-key` header on every request. The value comes from whatever you passed as `apiKey` in the constructor.

    **Fix:**

    1. Confirm the env var is loaded before constructing the client:

    ```ts theme={null}
    if (!process.env.PAXOS_LABS_API_KEY) {
      throw new Error('PAXOS_LABS_API_KEY is not set')
    }

    const client = new AmplifyClient({
      apiKey: process.env.PAXOS_LABS_API_KEY,
    })
    ```

    2. Catch the error and inspect the response body — the backend usually says exactly what's wrong:

    ```ts theme={null}
    import { AmplifyError } from '@paxoslabs/amplify-sdk'

    try {
      await client.amplify.vaults.list()
    } catch (err) {
      if (err instanceof AmplifyError && err.statusCode === 401) {
        console.error('Auth failed:', err.body)
      }
    }
    ```

    3. If you're hitting a non-production environment, confirm the key was issued for that environment — production and staging keys are not interchangeable.
  </Accordion>

  <Accordion title="AmplifyError: 403 Forbidden">
    **Cause:** API key is valid, but the account associated with it doesn't have access to the vault or operation you're calling.

    **Fix:** Verify the vault is enabled for your API key. `client.amplify.vaults.list()` only returns vaults your key can see, so a 403 on `prepare` for a vault that doesn't appear in `list()` is the expected behaviour — request access from your Paxos Labs contact.
  </Accordion>

  <Accordion title="AmplifyError: 400 on prepare">
    **Cause:** Almost always a bad input. The most common offenders, in order:

    * **Bad `vaultAddress`** — must be `0x` + 40 hex chars (the BoringVault contract address). Get it from `client.amplify.vaults.list()` → `deployments[].boringVaultAddress`.
    * **Wrong `chainId`** — the vault isn't deployed on that chain. Each `VaultDto.deployments[]` entry has its own `chainId`; pass the one that matches your wallet's network.
    * **`depositAmount` in human units instead of base units** — `depositAmount` is a **decimal string in base units**. `"1.5"` USDC is wrong; `"1500000"` (1.5 USDC at 6 decimals) is right.
    * **Missing `userAddress`** — required even when `to` is also passed.

    **Fix:**

    ```ts theme={null}
    import type { Amplify } from '@paxoslabs/amplify-sdk'
    import { parseUnits } from 'viem'

    const request: Amplify.amplify.deposit.PrepareDepositRequest = {
      vaultAddress: vault.boringVaultAddress,
      depositAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
      depositAmount: parseUnits('1.5', 6).toString(), // base units
      userAddress: wallet.account.address,
      chainId: vault.chainId,
    }

    const { transaction } = await client.amplify.deposit.prepare(request)
    ```
  </Accordion>

  <Accordion title="AmplifyError: 404 on prepare">
    **Cause:** `wantAsset` is not an asset this vault redeems to on this chain.

    A vault accepts deposits in one set of assets and pays out withdrawals in another. The withdrawable set is on each vault's `assets[]` entry where `withdrawable: true`.

    **Fix:**

    ```ts theme={null}
    const { vaults } = await client.amplify.vaults.list({ filter: 'chainId=1' })
    const vault = vaults
      .flatMap((v) => v.deployments)
      .find((d) => d.boringVaultAddress.toLowerCase() === vaultAddress.toLowerCase())

    const withdrawableAssets = vault?.assets
      .filter((a) => a.withdrawable)
      .map((a) => a.assetAddress)
    ```
  </Accordion>

  <Accordion title="Withdrawal submitOrder reverts with panic 0x11">
    Panic `0x11` is an arithmetic underflow. On the withdraw path it has two distinct causes — fix the right one.

    **Cause 1 — wrong approval spender.** The single most common mistake is approving the **vault address** (the share token itself) as the spender. The spender must be the **`WithdrawQueue`** address from the same vault.

    **Fix:** Read `vault.withdrawQueueAddress` and pass that as the spender on a standard ERC-20 `approve`:

    ```ts theme={null}
    import { erc20Abi } from 'viem'

    const vault = /* ...from client.amplify.vaults.list() */
    await wallet.writeContract({
      address: vault.boringVaultAddress, // share token
      abi: erc20Abi,
      functionName: 'approve',
      args: [vault.withdrawQueueAddress, shareAmount],
    })
    ```

    EIP-2612 permit is **not supported** for vault shares — `client.core.authorization.detect` with `tokenAddress: vaultAddress` returns `method: 'approval'` or `method: 'already_approved'`. There is no permit-signature path on the withdraw flow.

    **Cause 2 — fee consumes the entire offer.** When the offer-fee percentage plus the flat fee adds up to ≥ `shareAmount`, the post-fee math inside `submitOrder` underflows and panics. Common on small redemptions of vaults that carry a non-trivial flat fee.

    **Fix:** Call `client.amplify.withdraw.calculateFee` before `prepare` and bail out when `feeAmount >= shareAmount`:

    ```ts theme={null}
    const fee = await client.amplify.withdraw.calculateFee({
      offerAmount: shareAmount,
      wantAsset,
      vaultAddress,
      chainId,
    })
    if (BigInt(fee.feeAmount) >= BigInt(shareAmount)) {
      throw new Error('Amount is too small — fees would consume the entire withdrawal.')
    }
    ```
  </Accordion>

  <Accordion title="AmplifyError: 422 on authorization.detect">
    **Cause:** The token doesn't implement EIP-2612 (no `permit`, `nonces`, or `DOMAIN_SEPARATOR`), or it implements a non-standard variant the backend can't match.

    The SDK returns `method: 'approval'` instead of throwing for tokens it knows can't permit. A 422 here means the backend tried and got an unexpected result — fall back to a standard `approve` call.

    **Fix:**

    ```ts theme={null}
    import { AmplifyError } from '@paxoslabs/amplify-sdk'

    try {
      const auth = await client.core.authorization.detect({ /* ... */ })
      // auth.method === 'permit' | 'approval' | 'already_approved'
    } catch (err) {
      if (err instanceof AmplifyError && err.statusCode === 422) {
        // Token can't permit — do a plain approve(spender, amount) yourself.
      } else {
        throw err
      }
    }
    ```
  </Accordion>

  <Accordion title="AmplifyTimeoutError">
    **Cause:** The request exceeded `requestOptions.timeoutInSeconds` (default `60`). Usually a transient network or backend slowness.

    **Fix:** Retry with backoff. Each method accepts a per-call `requestOptions.maxRetries` (default `2`); set a higher cap when you control retry budget yourself:

    ```ts theme={null}
    import { AmplifyTimeoutError } from '@paxoslabs/amplify-sdk'

    async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
      let lastErr: unknown
      for (let i = 0; i < attempts; i++) {
        try {
          return await fn()
        } catch (err) {
          lastErr = err
          if (!(err instanceof AmplifyTimeoutError)) throw err
          await new Promise((r) => setTimeout(r, 500 * 2 ** i))
        }
      }
      throw lastErr
    }

    const positions = await withRetry(() =>
      client.amplify.users.getPositions(
        { userAddress },
        { timeoutInSeconds: 30, maxRetries: 0 }, // we manage retries ourselves
      ),
    )
    ```
  </Accordion>

  <Accordion title="Deposit reverts on-chain — decimal mismatch">
    **Cause:** For an Amplify vault, the depositor token, base token, and share token must all share the same number of `decimals`. The vault math collapses if they don't, and an on-chain deposit will revert.

    **Fix:** Read `decimals()` from each token via viem (or your wallet client) and compare against the vault's configured assets:

    ```ts theme={null}
    import { erc20Abi } from 'viem'

    const [depositDecimals, shareDecimals] = await Promise.all([
      publicClient.readContract({
        address: depositAsset,
        abi: erc20Abi,
        functionName: 'decimals',
      }),
      publicClient.readContract({
        address: vault.boringVaultAddress,
        abi: erc20Abi,
        functionName: 'decimals',
      }),
    ])

    if (depositDecimals !== shareDecimals) {
      throw new Error(
        `Decimal mismatch: depositAsset=${depositDecimals} shareToken=${shareDecimals}`,
      )
    }
    ```

    If they don't match, you've picked the wrong vault for this asset — check `client.amplify.vaults.listAssets({ filter: 'depositable=true' })` for valid pairings.
  </Accordion>

  <Accordion title="client.amplify.vaults.list() returns no vaults">
    **Cause:** Almost always a missing/invalid API key or a filter that excludes everything.

    **Fix:**

    1. Call without a filter to see what your key has access to:

    ```ts theme={null}
    const { vaults } = await client.amplify.vaults.list()
    console.log(vaults.map((v) => v.name))
    ```

    2. If that's empty, the key has zero vault access — see "401 Unauthorized" / "403 Forbidden".
    3. If non-empty, narrow with a filter that matches the vault you want. AIP-160-style filters use `AND` and `=`:

    ```ts theme={null}
    await client.amplify.vaults.list({
      filter: 'chainId=1 AND inDeprecation=false',
    })
    ```
  </Accordion>

  <Accordion title="Types resolve to `any`">
    **Cause:** `tsconfig.json` `moduleResolution` is set to `node` (the legacy CJS resolver) and can't see the package's `exports` map.

    **Fix:** Set `moduleResolution` to `bundler` (Vite/Next/most modern toolchains) or `node16` / `nodenext`:

    ```jsonc theme={null}
    {
      "compilerOptions": {
        "moduleResolution": "bundler",
        "module": "esnext",
        "target": "es2022"
      }
    }
    ```

    Then restart your TS server. `import type { Amplify } from '@paxoslabs/amplify-sdk'` should resolve to a namespace of DTOs.
  </Accordion>

  <Accordion title="Type imports — what to import where">
    The SDK draws a clean line between runtime and types:

    ```ts theme={null}
    // Runtime — the client class and error classes
    import {
      AmplifyClient,
      AmplifyError,
      AmplifyTimeoutError,
    } from '@paxoslabs/amplify-sdk'

    // Types — request/response DTOs only
    import type { Amplify } from '@paxoslabs/amplify-sdk'

    type Req = Amplify.amplify.deposit.PrepareDepositRequest
    type Res = Amplify.PrepareDepositResponseDto
    type Vault = Amplify.VaultDto
    ```

    `Amplify` is a type-only namespace re-export. Importing it as a value (`import { Amplify }`) works at runtime but is meaningless — there are no runtime properties on it. Use `import type` to make intent obvious and to let the bundler tree-shake it.
  </Accordion>

  <Accordion title="Migrating from 0.5.x">
    The 1.0.0 SDK is a clean break — `initAmplifySDK`, `LogLevel`, every flat function export (`prepare`, `prepare`, `getVaultsByConfig`, etc.), every typed error class, and all ABI/EIP-712 helpers are gone. Everything now hangs off `AmplifyClient` subclients, and the only error types are `AmplifyError` and `AmplifyTimeoutError`.

    See [Migrating from 0.5.x](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) for a function-by-function mapping.
  </Accordion>
</AccordionGroup>

## Inspecting an `AmplifyError`

Every non-2xx response, network failure, and JSON parse error throws `AmplifyError`. The shape is small and stable:

```ts theme={null}
import { AmplifyError } from '@paxoslabs/amplify-sdk'

try {
  await client.amplify.deposit.prepare(request)
} catch (err) {
  if (err instanceof AmplifyError) {
    err.statusCode // number | undefined  — HTTP status when applicable
    err.body // unknown — parsed JSON body when the server returned JSON
    err.rawResponse // RawResponse | undefined — original fetch Response metadata
    err.message // string — human-readable summary
  }
}
```

`body` is `unknown` because the backend may return any JSON shape. Narrow defensively before reading fields off it — never destructure blind.

## Getting help

If a request is failing in a way none of the recipes above explain, capture the full `AmplifyError` (including `statusCode`, `body`, and the request input that triggered it) and reach out to [support@paxoslabs.com](mailto:support@paxoslabs.com).
