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.

@paxoslabs/amplify-sdk@1.0.0 is chain-agnostic. Every method that touches the chain takes a chainId, and the API returns chain-correct contract addresses and calldata. That has three consequences for your app:
  1. Pull the list of supported chains at runtime instead of hard-coding it.
  2. RPC connectivity belongs to your wallet stack (wagmi / viem / Privy / Dynamic), not the SDK.
  3. Switching chains mid-flow is “pass a different chainId to the next call.” There is no client-side state to reset.

Supported chains

Networks the SDK currently serves: The snippet is the source of truth for the docs site. At runtime, derive the set dynamically (see Discovering chains below) — the backend can light up new chains without an SDK release.

Discovering chains

client.vaults.list returns vault groups, each with a deployments[] array describing every chain a vault is live on:
import { AmplifyClient } from '@paxoslabs/amplify-sdk'

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

const { vaults } = await client.vaults.list()

const supportedChainIds = new Set<number>(
  vaults.flatMap((v) => v.deployments.map((d) => d.chainId)),
)
Filter at the API layer when you only want one chain — it is faster and avoids paginating through deployments you do not care about:
const { vaults } = await client.vaults.list({
  filter: 'chainId=1 AND inDeprecation=false',
})
The filter string follows the same shape as every other listing endpoint: comma-separated field=value clauses with optional AND joins. Supported flags: chainId, inDeprecation, requiresKyt.
Don’t ship a hard-coded chain list in your app. Pull it from client.vaults.list() (and cache for a few minutes) so a new chain coming online doesn’t require a release.

Per-chain RPC belongs to your wallet

The SDK never opens an RPC connection — it speaks HTTP to api.paxoslabs.com. Submitting prepared.transaction to a chain is the wallet’s job, and configuring per-chain RPC URLs is wagmi/viem territory:
import { http, createConfig } from 'wagmi'
import { mainnet, base, optimism } from 'wagmi/chains'

export const config = createConfig({
  chains: [mainnet, base, optimism],
  transports: {
    [mainnet.id]: http(process.env.NEXT_PUBLIC_ETH_RPC_URL),
    [base.id]: http(process.env.NEXT_PUBLIC_BASE_RPC_URL),
    [optimism.id]: http(process.env.NEXT_PUBLIC_OPTIMISM_RPC_URL),
  },
})
That is the only place RPC URLs need to exist in your app.

Same vault, multiple chains

A single vault from client.vaults.list() uses the same boringVaultAddress across all chains it’s deployed on. Each deployment carries its own accepted assets and configuration:
const { vaults } = await client.vaults.list()

for (const vault of vaults) {
  for (const d of vault.deployments) {
    // d.chainId
    // d.boringVaultAddress      — same address across all chains for this vault
    // d.depositorAddress        — DistributorCodeDepositor for deposits
    // d.withdrawQueueAddress    — WithdrawQueue for submitOrder / cancel
    // d.assets                  — accepted deposit / want assets on this chain
    // d.minimumWithdrawalOrderSize
  }
}
When you store vault deployments in app state, key them by (boringVaultAddress, chainId). The vault address is consistent across chains, making it a stable identifier:
import type { Address } from 'viem'

type Key = `${Address}:${number}`

const deployments = new Map<Key, (typeof vaults)[number]['deployments'][number]>()
for (const v of vaults) {
  for (const d of v.deployments) {
    deployments.set(`${d.boringVaultAddress}:${d.chainId}`, d)
  }
}
This also provides stable React keys when rendering cross-chain deployments in a list.

Chain guard before write

Always check the connected wallet’s chain matches the chain you’re preparing for. The SDK happily prepares calldata for any chain — the wallet will broadcast it on whichever network it is connected to, which is rarely what the user wants:
function assertChain(expected: number, connected: number) {
  if (expected !== connected) {
    throw new Error(`Wrong network: expected ${expected}, got ${connected}`)
  }
}

// before submitting:
assertChain(chainId, await walletClient.getChainId())
For wagmi-based apps, prefer useSwitchChain to actively switch the wallet before submitting, then re-read the chain id and assert.

Deposit flow on a specific chain

import { AmplifyClient, AmplifyError } from '@paxoslabs/amplify-sdk'
import type { Address, Hex } from 'viem'

async function depositOnChain(params: {
  chainId: number
  userAddress: Address
  depositAsset: Address
  depositAmount: string
}) {
  const { vaults } = await client.vaults.list({
    filter: `chainId=${params.chainId} AND inDeprecation=false`,
  })

  const vault = vaults
    .flatMap((v) => v.deployments)
    .find((d) =>
      d.assets.some(
        (a) => a.assetAddress.toLowerCase() === params.depositAsset.toLowerCase(),
      ),
    )
  if (!vault) {
    throw new Error(
      `No vault on chain ${params.chainId} accepts ${params.depositAsset}`,
    )
  }

  const auth = await client.permit.authorize({
    vaultAddress: vault.boringVaultAddress,
    tokenAddress: params.depositAsset,
    amount: params.depositAmount,
    userAddress: params.userAddress,
    chainId: params.chainId,
  })

  let permitSignature: Hex | undefined
  let permitDeadline: number | undefined

  if (auth.method === 'permit') {
    permitSignature = await walletClient.signTypedData({
      account: params.userAddress,
      domain: auth.permitData.domain,
      types: auth.permitData.types,
      primaryType: 'Permit',
      message: auth.permitData.value,
    })
    permitDeadline = Number(auth.permitData.deadline)
  } else if (auth.method === 'approval') {
    const approvalHash = await walletClient.sendTransaction({
      to: params.depositAsset,
      data: auth.approvalTransaction.encoded as Hex,
      chainId: params.chainId,
    })
    await publicClient.waitForTransactionReceipt({ hash: approvalHash })
  }
  // 'already_approved' → nothing to do

  const prepared = await client.deposit.prepareDeposit({
    vaultAddress: vault.boringVaultAddress,
    depositAsset: params.depositAsset,
    depositAmount: params.depositAmount,
    userAddress: params.userAddress,
    chainId: params.chainId,
    ...(permitSignature ? { permitSignature, permitDeadline } : {}),
  })

  return walletClient.sendTransaction({
    to: prepared.transaction.to as Address,
    data: prepared.transaction.data as Hex,
    value: BigInt(prepared.transaction.value),
    chainId: params.chainId,
  })
}

Withdrawal flow on a specific chain

import type { Address, Hex } from 'viem'

async function withdrawOnChain(params: {
  chainId: number
  userAddress: Address
  vaultAddress: Address
  wantAsset: Address
  shareAmount: string
}) {
  const auth = await client.permit.authorize({
    vaultAddress: params.vaultAddress,
    tokenAddress: params.vaultAddress, // share token
    amount: params.shareAmount,
    userAddress: params.userAddress,
    chainId: params.chainId,
  })

  if (auth.method === 'approval') {
    const approvalHash = await walletClient.sendTransaction({
      to: params.vaultAddress,
      data: auth.approvalTransaction.encoded as Hex,
      chainId: params.chainId,
    })
    await publicClient.waitForTransactionReceipt({ hash: approvalHash })
  }

  const prepared = await client.withdraw.prepareWithdrawal({
    vaultAddress: params.vaultAddress,
    wantAsset: params.wantAsset,
    shareAmount: params.shareAmount,
    userAddress: params.userAddress,
    chainId: params.chainId,
  })

  return walletClient.sendTransaction({
    to: prepared.transaction.to as Address,
    data: prepared.transaction.data as Hex,
    value: BigInt(prepared.transaction.value),
    chainId: params.chainId,
  })
}

Switching chains mid-flow

Because the SDK keeps no chain state, switching chains is just “pass the new chainId”:
// User flips the chain selector from Ethereum (1) to Base (8453).
await switchChain({ chainId: 8453 })

const prepared = await client.deposit.prepareDeposit({
  vaultAddress: baseDeployment.boringVaultAddress,
  depositAsset: usdcOnBase,
  depositAmount,
  userAddress,
  chainId: 8453, // new chain
})
Cancel any in-flight prepare requests for the old chain before you do, so a late response can’t overwrite the new one’s UI:
const controller = new AbortController()

const prepared = await client.deposit.prepareDeposit(
  { /* ... */ },
  { abortSignal: controller.signal },
)

// on chain switch:
controller.abort()

Listing across chains

Most list* endpoints accept chainId= in the filter and paginate via pageToken:
async function listAllVaults() {
  const all: Awaited<ReturnType<typeof client.vaults.list>>['vaults'] = []
  let pageToken: string | undefined

  do {
    const page = await client.vaults.list({ pageSize: 100, pageToken })
    all.push(...page.vaults)
    pageToken = page.nextPageToken
  } while (pageToken)

  return all
}
The same pattern applies to client.vaults.listAssets, getApys, getTvls, and getSupplyCaps. Filter by chainId server-side when you only need one chain — it’s cheaper than scanning all pages.

Error handling across chains

import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk'

try {
  await depositOnChain({ chainId: 8453, /* ... */ })
} catch (err) {
  if (err instanceof AmplifyTimeoutError) {
    // Network slow — surface a retry.
  } else if (err instanceof AmplifyError) {
    // err.statusCode === 400 + err.body might tell you the chain
    // doesn't host the requested vault, the asset isn't accepted, etc.
  } else {
    // Wallet error — wrong chain selected, user rejected, RPC down.
  }
}
A common 400 on multi-chain flows is “vault not deployed on chainId” — usually because you cached a deployment list before a new chain came online, or because the user switched chains between discovery and submission. Re-fetch client.vaults.list({ filter: 'chainId=<id>' }) on chain-switch events.