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:
- Pull the list of supported chains at runtime instead of hard-coding it.
- RPC connectivity belongs to your wallet stack (wagmi / viem / Privy / Dynamic), not the SDK.
- 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.