Skip to main content
This guide walks through the complete withdrawal flow: discover the account, approve share spending, prepare the withdrawal order calldata, submit, and monitor status.
Withdrawals on Amplify are asynchronous. Submitting a withdrawal order places it in a queue. Once processed by the protocol, the requested asset is transferred to the user. Monitor order status via GET /v2/amplify/withdrawalRequests.

Prerequisites

  • A Paxos Labs API key
  • An EVM-compatible signer (private key, HSM, or wallet service)
  • The user must hold vault shares (the BoringVault ERC-20 token) from a prior deposit

Step 0: Fetch Account Details

Retrieve the account’s contract addresses. You need the boringVaultAddress (the share token) and the withdrawQueueAddress (the approval spender).
curl "https://api.paxoslabs.com/v2/amplify/vaults?filter=chainId%3D1" \
  -H "x-api-key: pxl_your_key"
FieldUsage
boringVaultAddressThe ERC-20 share token contract; also the vaultAddress param
withdrawQueueAddressThe spender address for the share approval

Step 1: Approve Share Spending

Before submitting a withdrawal order, the WithdrawQueue contract must be approved to spend the user’s vault shares. Construct a standard ERC-20 approve(spender, amount) call:
  • Token contract: the boringVaultAddress
  • Spender: the withdrawQueueAddress
  • Amount: the share amount to withdraw (in share token base units, 18 decimals)
Submit this approval transaction and wait for confirmation before proceeding.

Step 2: Prepare Withdrawal Calldata

GET /v2/amplify/withdraw returns the transaction to submit a withdrawal order.

Parameters

ParameterTypeRequiredDescription
vaultAddressstringYesBoringVault contract address (0x + 40 hex chars)
wantAssetstringYesERC-20 token address to receive upon withdrawal
shareAmountstringYesVault share amount to redeem, in share token base units (decimal string, 18 decimals)
userAddressstringYesWallet submitting the withdrawal. Also the default for intendedDepositor, receiver, and refundReceiver when those are omitted.
chainIdnumberYesEVM chain ID
intendedDepositorstringNoOn-chain SubmitOrderParams.intendedDepositor. Defaults to userAddress.
receiverstringNoOn-chain SubmitOrderParams.receiver — address credited with the wantAsset on settlement. Defaults to userAddress.
refundReceiverstringNoOn-chain SubmitOrderParams.refundReceiver — address credited with refunded shares if the order is cancelled. Defaults to userAddress.
responseFormatstringNoencoded (default), full, or structured
The server decides whether to queue the order (submitOrder) or settle it atomically (submitOrderAndProcessAll) based on the account’s on-chain RolesAuthority configuration. You do not need to pass an atomic flag — atomic-eligible accounts are routed automatically.

Response

{
  "transaction": {
    "to": "0xdddd000000000000000000000000000000000001",
    "data": "0x1a2b3c4d...",
    "value": "0",
    "abi": [{"type": "function", "name": "submitOrder", "inputs": [...]}],
    "functionName": "submitOrder",
    "args": [...]
  }
}

Step 3: Sign and Submit

Broadcast the transaction using the to, data, and value fields. The vault shares are locked in the WithdrawQueue upon confirmation.

Step 4: Monitor Status

Poll GET /v2/amplify/withdrawalRequests to track order progress. Omit the status predicate so the order remains visible as it transitions through terminal states:
curl "https://api.paxoslabs.com/v2/amplify/withdrawalRequests?\
filter=userAddress%3D0x1234...%20AND%20vaultAddress%3D0xbbbb..." \
  -H "x-api-key: pxl_your_key"
StatusMeaning
PENDINGOrder is in the queue, waiting for processing
COMPLETEAssets have been transferred to the user
PENDING_REFUNDOrder is being refunded
REFUNDEDVault shares have been returned to the user
Filtering on status=PENDING will cause the order to disappear from the response as soon as it reaches a terminal state, which hides the final outcome from readers polling for completion.

Complete Examples

import { createWalletClient, createPublicClient, http, encodeFunctionData, erc20Abi } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const API_KEY = process.env.AMPLIFY_API_KEY!;
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
const BASE = "https://api.paxoslabs.com";
const HEADERS = { "x-api-key": API_KEY };

const VAULT_ADDRESS = "0xbbbb000000000000000000000000000000000001";
const WITHDRAW_QUEUE = "0xdddd000000000000000000000000000000000001";
const WANT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const CHAIN_ID = 1;
const SHARE_AMOUNT = "1000000000000000000"; // 1 share (18 decimals)

async function main() {
  const account = privateKeyToAccount(PRIVATE_KEY);
  const walletClient = createWalletClient({
    account,
    chain: mainnet,
    transport: http(),
  });
  const publicClient = createPublicClient({
    chain: mainnet,
    transport: http(),
  });

  // Step 1: Approve share spending
  console.log("Approving share spending...");
  const approveData = encodeFunctionData({
    abi: erc20Abi,
    functionName: "approve",
    args: [WITHDRAW_QUEUE as `0x${string}`, BigInt(SHARE_AMOUNT)],
  });
  const approveHash = await walletClient.sendTransaction({
    to: VAULT_ADDRESS as `0x${string}`,
    data: approveData,
    chain: mainnet,
    account,
  });
  await publicClient.waitForTransactionReceipt({ hash: approveHash });
  console.log(`Approval confirmed: ${approveHash}`);

  // Step 2: Get withdrawal calldata
  console.log("Fetching withdrawal calldata...");
  const withdrawUrl = new URL(`${BASE}/v2/amplify/withdraw`);
  withdrawUrl.searchParams.set("vaultAddress", VAULT_ADDRESS);
  withdrawUrl.searchParams.set("wantAsset", WANT_ASSET);
  withdrawUrl.searchParams.set("shareAmount", SHARE_AMOUNT);
  withdrawUrl.searchParams.set("userAddress", account.address);
  withdrawUrl.searchParams.set("chainId", String(CHAIN_ID));

  const withdrawResp = await fetch(withdrawUrl, { headers: HEADERS }).then(
    (r) => r.json()
  );
  const tx = withdrawResp.transaction;

  // Step 3: Sign and submit
  console.log("Submitting withdrawal order...");
  const hash = await walletClient.sendTransaction({
    to: tx.to as `0x${string}`,
    data: tx.data as `0x${string}`,
    value: BigInt(tx.value),
    chain: mainnet,
    account,
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log(`Withdrawal submitted in block ${receipt.blockNumber}: ${hash}`);

  // Step 4: Poll status (no status filter — terminal states stay visible)
  console.log("Polling withdrawal status...");
  const statusUrl = new URL(`${BASE}/v2/amplify/withdrawalRequests`);
  statusUrl.searchParams.set(
    "filter",
    `userAddress=${account.address} AND vaultAddress=${VAULT_ADDRESS}`
  );
  const statusResp = await fetch(statusUrl, { headers: HEADERS }).then((r) =>
    r.json()
  );
  console.log(
    `Orders: ${statusResp.withdrawalRequests
      .map((o: { status: string }) => o.status)
      .join(", ")}`
  );
}

main().catch(console.error);

Error Responses

StatusMeaning
400Invalid parameters
404No account found for the given vaultAddress + chainId