Skip to main content

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.

Practical “I hit X, what’s wrong” guide for the 1.0.0 SDK. Every recipe assumes you have an AmplifyClient constructed like this:
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 — the public API has changed substantially.
Cause: Package isn’t installed, or it’s installed but TypeScript can’t resolve it.Fix:
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 prepareDeposit / prepareWithdrawal, 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.
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:
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,
})
  1. Catch the error and inspect the response body — the backend usually says exactly what’s wrong:
import { AmplifyError } from '@paxoslabs/amplify-sdk'

try {
  await client.vaults.list()
} catch (err) {
  if (err instanceof AmplifyError && err.statusCode === 401) {
    console.error('Auth failed:', err.body)
  }
}
  1. If you’re hitting a non-production environment, confirm the key was issued for that environment — production and staging keys are not interchangeable.
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.vaults.list() only returns vaults your key can see, so a 403 on prepareDeposit for a vault that doesn’t appear in list() is the expected behaviour — request access from your Paxos Labs contact.
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.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 unitsdepositAmount 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:
import type { Amplify } from '@paxoslabs/amplify-sdk'
import { parseUnits } from 'viem'

const request: Amplify.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.deposit.prepareDeposit(request)
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:
const { vaults } = await client.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)
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:
import { erc20Abi } from 'viem'

const vault = /* ...from client.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.permit.authorize 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.withdraw.calculateFee before prepareWithdrawal and bail out when feeAmount >= shareAmount:
const fee = await client.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.')
}
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:
import { AmplifyError } from '@paxoslabs/amplify-sdk'

try {
  const auth = await client.permit.authorize({ /* ... */ })
  // 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
  }
}
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:
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.users.getPositions(
    { userAddress },
    { timeoutInSeconds: 30, maxRetries: 0 }, // we manage retries ourselves
  ),
)
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:
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.vaults.listAssets({ filter: 'depositable=true' }) for valid pairings.
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:
const { vaults } = await client.vaults.list()
console.log(vaults.map((v) => v.name))
  1. If that’s empty, the key has zero vault access — see “401 Unauthorized” / “403 Forbidden”.
  2. If non-empty, narrow with a filter that matches the vault you want. AIP-160-style filters use AND and =:
await client.vaults.list({
  filter: 'chainId=1 AND inDeprecation=false',
})
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:
{
  "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.
The SDK draws a clean line between runtime and types:
// 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.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.
The 1.0.0 SDK is a clean break — initAmplifySDK, LogLevel, every flat function export (prepareDeposit, prepareWithdrawal, 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 for a function-by-function mapping.

Inspecting an AmplifyError

Every non-2xx response, network failure, and JSON parse error throws AmplifyError. The shape is small and stable:
import { AmplifyError } from '@paxoslabs/amplify-sdk'

try {
  await client.deposit.prepareDeposit(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.