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.
Single-page, greppable reference for @paxoslabs/amplify-sdk 1.0.0. Drop the rendered page into an AI coding assistant (Cursor, Copilot, Claude Code) and it has everything required to integrate without extra docs lookups.
The public surface is AmplifyClient, the Amplify type-only namespace, AmplifyError, and AmplifyTimeoutError.
Install
pnpm add @paxoslabs/amplify-sdk
# or: npm i @paxoslabs/amplify-sdk
# or: yarn add @paxoslabs/amplify-sdk
No required peer dependencies. To submit the calldata the SDK returns, bring your own EVM client — examples below use viem.
Construct the client
import { AmplifyClient } from '@paxoslabs/amplify-sdk'
const client = new AmplifyClient({
apiKey: process.env.PAXOS_LABS_API_KEY!,
})
Full constructor signature:
namespace AmplifyClient {
interface Options {
/** Required. Sent as the `x-api-key` header on every request. */
apiKey: string | (() => string) | (() => Promise<string>)
/** Optional environment override. Defaults to `AmplifyEnvironment.Production`. */
environment?: AmplifyEnvironment | string | (() => string) | (() => Promise<string>)
/** Optional base URL override (takes precedence over `environment`). */
baseUrl?: string | (() => string) | (() => Promise<string>)
}
interface RequestOptions {
timeoutInSeconds?: number // default 60
maxRetries?: number // default 2
abortSignal?: AbortSignal
headers?: Record<string, string>
}
}
Construct once at module scope and reuse across requests. The SDK defaults to production (AmplifyEnvironment.Production).
Authentication
The API key is sent on every request as the x-api-key HTTP header. The SDK reads it from the apiKey constructor option. There is no other auth mode.
const client = new AmplifyClient({
apiKey: process.env.PAXOS_LABS_API_KEY!,
})
Missing/invalid keys surface as AmplifyError with statusCode: 401. Access denied for a specific vault surfaces as statusCode: 403.
Imports cheat sheet
// Runtime
import {
AmplifyClient,
AmplifyError,
AmplifyTimeoutError,
} from '@paxoslabs/amplify-sdk'
// Types (request/response DTOs live under a single namespace)
import type { Amplify } from '@paxoslabs/amplify-sdk'
type DepositRequest = Amplify.PrepareDepositRequest
type DepositResponse = Amplify.PrepareDepositResponseDto
type Vault = Amplify.VaultDto
type Deployment = Amplify.VaultDeploymentDto
type PermitResponse = Amplify.PermitResponseDto
Subclient + method index
AmplifyClient exposes one subclient per resource. Every subclient method:
client.deposit.prepareDeposit(request)
client.withdraw.prepareWithdrawal(request)
client.withdraw.cancel(request)
client.withdraw.calculateFee(request)
client.withdraw.listRequests(request?)
client.withdraw.getVolumes(request)
client.permit.authorize(request)
client.users.getPositions(request)
client.vaults.list(request?)
client.vaults.listAssets(request?)
client.vaults.getApys(request?)
client.vaults.getTvls(request?)
client.vaults.getSupplyCaps(request?)
client.vaults.listCompositions(request?)
client.vaults.getLiquidityShortfalls(request?)
Every method takes an optional second argument: requestOptions ({ timeoutInSeconds?, maxRetries?, abortSignal?, headers? }).
client.deposit.prepareDeposit(request)
Returns ABI-encoded calldata for a vault deposit on the DistributorCodeDepositor.
- Request:
Amplify.PrepareDepositRequest
- Response:
Amplify.PrepareDepositResponseDto
const { transaction } = await client.deposit.prepareDeposit({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
depositAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
depositAmount: '1000000', // base units, decimal string
userAddress: '0xUser',
chainId: 1,
// Optional: pass after a successful permit signing flow
// permitSignature: '0x...',
// permitDeadline: 9999999999,
// Optional: defaults to userAddress
// to: '0xRecipient',
})
// transaction.to, transaction.data, transaction.value
client.withdraw.prepareWithdrawal(request)
Returns calldata for WithdrawQueue.submitOrder. Caller must have pre-approved deployment.withdrawQueueAddress to spend share tokens.
- Request:
Amplify.PrepareWithdrawalRequest
- Response:
Amplify.PrepareWithdrawResponseDto
const { transaction } = await client.withdraw.prepareWithdrawal({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
shareAmount: '1000000000000000000',
userAddress: '0xUser',
chainId: 1,
// Optional, all default to userAddress
// intendedDepositor: '0x...',
// receiver: '0x...',
// refundReceiver: '0x...',
})
client.withdraw.cancel(request)
Returns calldata for cancelling a pending withdrawal order. Look up orderIndex via listRequests.
- Request:
Amplify.CancelRequest
- Response:
Amplify.PrepareCancelWithdrawResponseDto
const { transaction } = await client.withdraw.cancel({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
orderIndex: '42',
chainId: 1,
})
client.withdraw.calculateFee(request)
Computes the fee charged for a hypothetical withdrawal.
- Request:
Amplify.CalculateFeeRequest
- Response:
Amplify.CalculateWithdrawalFeeResponseDto
const fee = await client.withdraw.calculateFee({
offerAmount: '1000000000000000000',
wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
vaultAddress: '0xbbbb000000000000000000000000000000000001',
chainId: 1,
})
// fee.feeAmount, fee.offerFeePercentage, fee.flatFee
client.withdraw.listRequests(request?)
Lists withdrawal requests with cursor pagination and AIP-160 filters.
- Request:
Amplify.ListRequestsRequest
- Response:
Amplify.WithdrawalRequestsResponseDto
const { withdrawalRequests, nextPageToken } = await client.withdraw.listRequests({
filter: 'status=PENDING AND userAddress=0xUser AND vaultAddress=0xbbbb...0001',
pageSize: 50,
})
// Each item: { id, status, orderIndex, orderAmount, vaultAddress, chainId, ... }
client.permit.authorize(request)
Tells you whether the token needs EIP-2612 permit signing, a standard ERC-20 approve, or is already approved for the requested amount.
- Request:
Amplify.AuthorizeRequest
- Response:
Amplify.PermitResponseDto
const auth = await client.permit.authorize({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // depositAsset
amount: '1000000',
userAddress: '0xUser',
chainId: 1,
})
// auth.method: 'permit' | 'approval' | 'already_approved'
// auth.permitData? present when method === 'permit'
// auth.approvalTransaction? present when method === 'approval'
For deposits pass tokenAddress = depositAsset. For withdrawals pass tokenAddress = vaultAddress (the share token). Share tokens always resolve to approval or already_approved — there is no permit path for shares.
client.users.getPositions(request)
Returns one row per (user, vault, chain) with share balance and base-asset value.
- Request:
Amplify.GetPositionsRequest
- Response:
Amplify.UserPositionsResponseDto
const { userPositions, nextPageToken } = await client.users.getPositions({
userAddress: '0xUser',
filter: 'chainId=1',
})
client.vaults.list(request?)
Returns all vaults the API key can see, grouped by vault name, with one deployments[] entry per chain.
- Request:
Amplify.ListRequest
- Response:
Amplify.VaultsResponseDto
const { vaults, nextPageToken } = await client.vaults.list({
filter: 'chainId=1 AND inDeprecation=false',
})
for (const v of vaults) {
for (const d of v.deployments) {
// d.chainId, d.boringVaultAddress, d.depositorAddress, d.withdrawQueueAddress,
// d.requiresKyt, d.inDeprecation, d.depositSupplyCap, d.minimumWithdrawalOrderSize,
// d.assets[] -> { assetAddress, depositable, withdrawable, depositFees, withdrawFees }
}
}
client.vaults.listAssets(request?)
Per-asset capability listing across all visible vaults.
- Request:
Amplify.ListAssetsRequest
- Response:
Amplify.VaultAssetsResponseDto
const { vaultAssets } = await client.vaults.listAssets({
filter: 'depositable=true AND chainId=1',
})
client.vaults.getApys(request?)
Historical + current APY series.
- Request:
Amplify.GetApysRequest
- Response:
Amplify.VaultApysResponseDto
const { vaultApys } = await client.vaults.getApys({
filter: 'vaultAddress=0xbbbb...0001',
lookback: '604800s', // 7 days
interval: '86400s', // 1 day
})
client.vaults.getTvls(request?)
Historical + current TVL series, optionally aggregated across chains.
- Request:
Amplify.GetTvlsRequest
- Response:
Amplify.VaultTvlsResponseDto
const { vaultTvls } = await client.vaults.getTvls({
filter: 'chainId=1 AND vaultAddress=0xbbbb...0001',
lookback: '604800s',
aggregate: false,
})
client.vaults.getSupplyCaps(request?)
Current totalSupplyInBase, supplyCap, and percentageFilled per (vault, chain).
- Request:
Amplify.GetSupplyCapsRequest
- Response:
Amplify.SupplyCapsResponseDto
const { supplyCaps } = await client.vaults.getSupplyCaps({
filter: 'vaultAddress=0xbbbb...0001 AND chainId=1',
})
Error handling
Every SDK method throws on failure. Two error classes only:
class AmplifyError extends Error {
readonly statusCode?: number // HTTP status, e.g. 400, 401, 403, 404, 422
readonly body?: unknown // parsed JSON body when the server returned JSON
readonly rawResponse?: RawResponse // fetch Response metadata
readonly message: string // human-readable summary
}
class AmplifyTimeoutError extends Error {
readonly message: string
}
Standard handling pattern:
import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk'
try {
await client.deposit.prepareDeposit(request)
} catch (err) {
if (err instanceof AmplifyTimeoutError) {
// request exceeded timeoutInSeconds — retry with backoff
} else if (err instanceof AmplifyError) {
switch (err.statusCode) {
case 400:
// bad request — fix inputs (see body for details)
break
case 401:
case 403:
// auth failure — check apiKey
break
case 404:
// not found — vault, asset, or order doesn't exist for these params
break
case 422:
// unprocessable — common on permit.authorize for non-2612 tokens
break
default:
// 5xx or unknown — surface to user, log details
}
} else {
throw err
}
}
Retry helper that only retries timeouts:
async function retryOnTimeout<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
}
End-to-end: deposit flow (permit + approval + already-approved)
import { AmplifyClient } from '@paxoslabs/amplify-sdk'
import type { Address, Hex } from 'viem'
import {
createPublicClient,
createWalletClient,
custom,
http,
parseUnits,
} from 'viem'
import { mainnet } from 'viem/chains'
const client = new AmplifyClient({
apiKey: process.env.PAXOS_LABS_API_KEY!,
})
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const walletClient = createWalletClient({
chain: mainnet,
transport: custom((window as any).ethereum),
})
async function deposit(params: {
vaultAddress: Address
depositAsset: Address
humanAmount: string // e.g. "100"
decimals: number // depositAsset.decimals()
userAddress: Address
chainId: number
}) {
const depositAmount = parseUnits(params.humanAmount, params.decimals).toString()
// 1) Ask the backend what kind of authorization is required.
const auth = await client.permit.authorize({
vaultAddress: params.vaultAddress,
tokenAddress: params.depositAsset,
amount: depositAmount,
userAddress: params.userAddress,
chainId: params.chainId,
})
let permitSignature: Hex | undefined
let permitDeadline: number | undefined
if (auth.method === 'permit' && auth.permitData) {
// 2a) Token supports EIP-2612 — sign typed data.
const { domain, types, value, deadline } = auth.permitData
const signature = await walletClient.signTypedData({
account: params.userAddress,
domain,
types: types as any,
primaryType: 'Permit',
message: value as any,
})
permitSignature = signature
permitDeadline = Number(deadline)
} else if (auth.method === 'approval' && auth.approvalTransaction) {
// 2b) Token doesn't permit (or allowance < amount) — send approve().
const hash = await walletClient.sendTransaction({
account: params.userAddress,
to: params.depositAsset,
data: auth.approvalTransaction.encoded as Hex,
chain: mainnet,
})
await publicClient.waitForTransactionReceipt({ hash })
}
// else: auth.method === 'already_approved' — nothing to do here.
// 3) Get the deposit calldata and submit it.
const { transaction } = await client.deposit.prepareDeposit({
vaultAddress: params.vaultAddress,
depositAsset: params.depositAsset,
depositAmount,
userAddress: params.userAddress,
chainId: params.chainId,
permitSignature,
permitDeadline,
})
const hash = await walletClient.sendTransaction({
account: params.userAddress,
to: transaction.to as Address,
data: transaction.data as Hex,
value: BigInt(transaction.value),
chain: mainnet,
})
return publicClient.waitForTransactionReceipt({ hash })
}
End-to-end: withdrawal flow
Share-token approvals always go to the WithdrawQueue — never to the vault contract itself, never via permit.
import type { Address, Hex } from 'viem'
async function withdraw(params: {
vaultAddress: Address
withdrawQueueAddress: Address // from client.vaults.list()
wantAsset: Address
shareAmount: string // share token base units
userAddress: Address
chainId: number
}) {
// 1) Authorize the WithdrawQueue to spend shares.
// `permit.authorize` returns 'approval' or 'already_approved' for share tokens.
const auth = await client.permit.authorize({
vaultAddress: params.vaultAddress,
tokenAddress: params.vaultAddress, // share token == vault address
amount: params.shareAmount,
userAddress: params.userAddress,
chainId: params.chainId,
})
if (auth.method === 'approval' && auth.approvalTransaction) {
const hash = await walletClient.sendTransaction({
account: params.userAddress,
to: params.vaultAddress, // share token
data: auth.approvalTransaction.encoded as Hex,
chain: mainnet,
})
await publicClient.waitForTransactionReceipt({ hash })
}
// else: already_approved — proceed directly
// 2) Get the submitOrder calldata.
const { transaction } = await client.withdraw.prepareWithdrawal({
vaultAddress: params.vaultAddress,
wantAsset: params.wantAsset,
shareAmount: params.shareAmount,
userAddress: params.userAddress,
chainId: params.chainId,
})
// 3) Submit.
const hash = await walletClient.sendTransaction({
account: params.userAddress,
to: transaction.to as Address,
data: transaction.data as Hex,
value: BigInt(transaction.value),
chain: mainnet,
})
return publicClient.waitForTransactionReceipt({ hash })
}
End-to-end: cancel a pending withdrawal
import type { Address, Hex } from 'viem'
async function cancelPendingWithdrawals(params: {
userAddress: Address
vaultAddress: Address
chainId: number
}) {
// 1) Find pending orders for this user.
const { withdrawalRequests } = await client.withdraw.listRequests({
filter: `status=PENDING AND userAddress=${params.userAddress} AND vaultAddress=${params.vaultAddress} AND chainId=${params.chainId}`,
})
// 2) Cancel each one.
for (const req of withdrawalRequests) {
const { transaction } = await client.withdraw.cancel({
vaultAddress: params.vaultAddress,
orderIndex: req.orderIndex,
chainId: params.chainId,
})
const hash = await walletClient.sendTransaction({
account: params.userAddress,
to: transaction.to as Address,
data: transaction.data as Hex,
value: BigInt(transaction.value),
chain: mainnet,
})
await publicClient.waitForTransactionReceipt({ hash })
}
}
Common pitfalls
depositAmount / shareAmount are base units. Always decimal strings. parseUnits('1.5', 6).toString() for USDC; never '1.5'.
- Share approvals go to the
WithdrawQueue. Approving the vault address itself causes the withdraw submitOrder to revert with panic 0x11. Read deployment.withdrawQueueAddress.
- Approve
shareAmount + feeAmount, not shareAmount. submitOrder pulls the inclusive amount. An exact-share approve reverts with panic 0x11. Always call client.withdraw.calculateFee first, then approve BigInt(shareAmount) + BigInt(fee.feeAmount).
- Preflight withdraw fees.
submitOrder also panics 0x11 when feeAmount >= shareAmount (post-fee math underflows). Bail out before prepareWithdrawal when the fee would consume the offer.
- Permit is for deposit assets only. Share tokens never return
method: 'permit' from permit.authorize. Don’t try to bypass and sign typed data manually.
- Decimal invariant on Amplify vaults. Deposit, base, and share token decimals must match — otherwise on-chain deposit reverts. Verify with viem’s
readContract against each token before depositing.
- Use
client.vaults.list() as the source of truth for addresses. Hardcoding contract addresses leaks across chain redeploys. Look up boringVaultAddress, depositorAddress, and withdrawQueueAddress per (vault, chainId) at runtime.
AmplifyTimeoutError is its own class. Retry timeouts; do not retry AmplifyError (400/401/403/404/422) without first fixing inputs.
- Branch errors on
err.statusCode and err.body. AmplifyError is the single non-timeout error class — there are no per-failure subclasses.
responseFormat: 'encoded' is the default. You always get transaction.to, transaction.data, transaction.value. Pass responseFormat: 'full' if you want the ABI fragment, function name, and args back too.
moduleResolution must be bundler / node16 / nodenext. Legacy node resolution can’t see the package’s exports map and types collapse to any.