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 theboringVaultAddress (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"
| Field | Usage |
|---|---|
boringVaultAddress | The ERC-20 share token contract; also the vaultAddress param |
withdrawQueueAddress | The spender address for the share approval |
Step 1: Approve Share Spending
Before submitting a withdrawal order, theWithdrawQueue 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)
Step 2: Prepare Withdrawal Calldata
GET /v2/amplify/withdraw returns the transaction to submit a withdrawal order.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
vaultAddress | string | Yes | BoringVault contract address (0x + 40 hex chars) |
wantAsset | string | Yes | ERC-20 token address to receive upon withdrawal |
shareAmount | string | Yes | Vault share amount to redeem, in share token base units (decimal string, 18 decimals) |
userAddress | string | Yes | Wallet submitting the withdrawal. Also the default for intendedDepositor, receiver, and refundReceiver when those are omitted. |
chainId | number | Yes | EVM chain ID |
intendedDepositor | string | No | On-chain SubmitOrderParams.intendedDepositor. Defaults to userAddress. |
receiver | string | No | On-chain SubmitOrderParams.receiver — address credited with the wantAsset on settlement. Defaults to userAddress. |
refundReceiver | string | No | On-chain SubmitOrderParams.refundReceiver — address credited with refunded shares if the order is cancelled. Defaults to userAddress. |
responseFormat | string | No | encoded (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 theto, data, and value fields. The vault shares are locked in the WithdrawQueue upon confirmation.
Step 4: Monitor Status
PollGET /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"
| Status | Meaning |
|---|---|
PENDING | Order is in the queue, waiting for processing |
COMPLETE | Assets have been transferred to the user |
PENDING_REFUND | Order is being refunded |
REFUNDED | Vault shares have been returned to the user |
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
- Node.js
- Python
- Go
- Java
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);
import os
import requests
from web3 import Web3
API_KEY = os.environ["AMPLIFY_API_KEY"]
PRIVATE_KEY = os.environ["PRIVATE_KEY"]
BASE = "https://api.paxoslabs.com"
HEADERS = {"x-api-key": API_KEY}
VAULT_ADDRESS = "0xbbbb000000000000000000000000000000000001"
WITHDRAW_QUEUE = "0xdddd000000000000000000000000000000000001"
WANT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
CHAIN_ID = 1
SHARE_AMOUNT = "1000000000000000000" # 1 share (18 decimals)
w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com"))
account = w3.eth.account.from_key(PRIVATE_KEY)
ERC20_ABI = [{"inputs":[{"name":"spender","type":"address"},
{"name":"amount","type":"uint256"}],"name":"approve",
"outputs":[{"name":"","type":"bool"}],"type":"function"}]
# Step 1: Approve share spending
print("Approving share spending...")
vault_token = w3.eth.contract(
address=Web3.to_checksum_address(VAULT_ADDRESS), abi=ERC20_ABI
)
approve_tx = vault_token.functions.approve(
Web3.to_checksum_address(WITHDRAW_QUEUE),
int(SHARE_AMOUNT),
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"chainId": CHAIN_ID,
})
signed = account.sign_transaction(approve_tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Approval confirmed: {tx_hash.hex()}")
# Step 2: Get withdrawal calldata
print("Fetching withdrawal calldata...")
withdraw_resp = requests.get(f"{BASE}/v2/amplify/withdraw", headers=HEADERS, params={
"vaultAddress": VAULT_ADDRESS,
"wantAsset": WANT_ASSET,
"shareAmount": SHARE_AMOUNT,
"userAddress": account.address,
"chainId": CHAIN_ID,
}).json()
tx = withdraw_resp["transaction"]
# Step 3: Sign and submit
print("Submitting withdrawal order...")
withdraw_tx = {
"to": Web3.to_checksum_address(tx["to"]),
"data": tx["data"],
"value": int(tx["value"]),
"gas": 300_000,
"nonce": w3.eth.get_transaction_count(account.address),
"chainId": CHAIN_ID,
}
signed = account.sign_transaction(withdraw_tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Withdrawal submitted in block {receipt['blockNumber']}: {tx_hash.hex()}")
# Step 4: Poll status (no status filter — terminal states stay visible)
print("Polling withdrawal status...")
status_resp = requests.get(f"{BASE}/v2/amplify/withdrawalRequests", headers=HEADERS, params={
"filter": f"userAddress={account.address} AND vaultAddress={VAULT_ADDRESS}",
}).json()
statuses = [o["status"] for o in status_resp["withdrawalRequests"]]
print(f"Orders: {', '.join(statuses)}")
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"os"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
const (
baseURL = "https://api.paxoslabs.com"
vaultAddress = "0xbbbb000000000000000000000000000000000001"
withdrawQueue = "0xdddd000000000000000000000000000000000001"
wantAsset = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
chainID = 1
shareAmount = "1000000000000000000"
)
func apiGet(path string, params url.Values) (map[string]interface{}, error) {
u := fmt.Sprintf("%s%s?%s", baseURL, path, params.Encode())
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("x-api-key", os.Getenv("AMPLIFY_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
func main() {
pk, _ := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
fromAddr := crypto.PubkeyToAddress(pk.PublicKey)
client, _ := ethclient.Dial("https://eth.llamarpc.com")
ctx := context.Background()
signer := types.NewEIP155Signer(big.NewInt(chainID))
// Step 1: Approve share spending
fmt.Println("Approving share spending...")
erc20ABI, _ := abi.JSON(strings.NewReader(
`[{"inputs":[{"name":"spender","type":"address"},` +
`{"name":"amount","type":"uint256"}],` +
`"name":"approve","outputs":[{"type":"bool"}],"type":"function"}]`))
amt := new(big.Int)
amt.SetString(shareAmount, 10)
approveData, _ := erc20ABI.Pack("approve", common.HexToAddress(withdrawQueue), amt)
nonce, _ := client.PendingNonceAt(ctx, fromAddr)
gasPrice, _ := client.SuggestGasPrice(ctx)
approveTx := types.NewTransaction(nonce, common.HexToAddress(vaultAddress),
big.NewInt(0), 60000, gasPrice, approveData)
signedApprove, _ := types.SignTx(approveTx, signer, pk)
client.SendTransaction(ctx, signedApprove)
fmt.Printf("Approval submitted: %s\n", signedApprove.Hash().Hex())
// Wait for approval receipt before requesting withdrawal calldata —
// otherwise the withdrawal can race ahead of the allowance.
if _, err := bind.WaitMined(ctx, client, signedApprove); err != nil {
panic(err)
}
fmt.Println("Approval confirmed")
// Step 2: Get withdrawal calldata
fmt.Println("Fetching withdrawal calldata...")
withdrawResp, _ := apiGet("/v2/amplify/withdraw", url.Values{
"vaultAddress": {vaultAddress},
"wantAsset": {wantAsset},
"shareAmount": {shareAmount},
"userAddress": {fromAddr.Hex()},
"chainId": {fmt.Sprint(chainID)},
})
txData := withdrawResp["transaction"].(map[string]interface{})
// Step 3: Sign and submit
fmt.Println("Submitting withdrawal order...")
nonce, _ = client.PendingNonceAt(ctx, fromAddr)
gasPrice, _ = client.SuggestGasPrice(ctx)
withdrawTx := types.NewTransaction(nonce,
common.HexToAddress(txData["to"].(string)),
big.NewInt(0), 300000, gasPrice,
common.FromHex(txData["data"].(string)))
signedWithdraw, _ := types.SignTx(withdrawTx, signer, pk)
client.SendTransaction(ctx, signedWithdraw)
fmt.Printf("Withdrawal submitted: %s\n", signedWithdraw.Hash().Hex())
}
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.response.PollingTransactionReceiptProcessor;
import org.web3j.tx.TransactionManager;
import org.web3j.utils.Numeric;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Arrays;
import java.util.Collections;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class AmplifyWithdraw {
static final String BASE = "https://api.paxoslabs.com";
static final String API_KEY = System.getenv("AMPLIFY_API_KEY");
static final String VAULT_ADDRESS = "0xbbbb000000000000000000000000000000000001";
static final String WITHDRAW_QUEUE = "0xdddd000000000000000000000000000000000001";
static final String WANT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
static final int CHAIN_ID = 1;
static final String SHARE_AMOUNT = "1000000000000000000";
static HttpClient httpClient = HttpClient.newHttpClient();
static JsonObject apiGet(String path) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("x-api-key", API_KEY)
.GET().build();
HttpResponse<String> resp = httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
return JsonParser.parseString(resp.body()).getAsJsonObject();
}
public static void main(String[] args) throws Exception {
Credentials credentials = Credentials.create(System.getenv("PRIVATE_KEY"));
String userAddress = credentials.getAddress();
Web3j web3 = Web3j.build(new HttpService("https://eth.llamarpc.com"));
// Step 1: Approve share spending
System.out.println("Approving share spending...");
Function approve = new Function("approve",
Arrays.asList(
new Address(WITHDRAW_QUEUE),
new Uint256(new BigInteger(SHARE_AMOUNT))),
Collections.singletonList(new TypeReference<Bool>() {}));
String approveData = FunctionEncoder.encode(approve);
BigInteger nonce = web3.ethGetTransactionCount(userAddress,
org.web3j.protocol.core.DefaultBlockParameterName.PENDING)
.send().getTransactionCount();
BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice();
RawTransaction approveTx = RawTransaction.createTransaction(
nonce, gasPrice, BigInteger.valueOf(60000),
VAULT_ADDRESS, BigInteger.ZERO, approveData);
byte[] signedApprove = TransactionEncoder.signMessage(
approveTx, CHAIN_ID, credentials);
String approveHash = web3.ethSendRawTransaction(
Numeric.toHexString(signedApprove)).send().getTransactionHash();
System.out.println("Approval submitted: " + approveHash);
// Wait for approval receipt before requesting withdrawal calldata —
// otherwise the withdrawal can race ahead of the allowance.
PollingTransactionReceiptProcessor receiptProcessor =
new PollingTransactionReceiptProcessor(
web3,
TransactionManager.DEFAULT_POLLING_FREQUENCY,
TransactionManager.DEFAULT_POLLING_ATTEMPTS_PER_TX_HASH);
TransactionReceipt approveReceipt =
receiptProcessor.waitForTransactionReceipt(approveHash);
System.out.println(
"Approval confirmed in block " + approveReceipt.getBlockNumber());
// Step 2: Get withdrawal calldata
System.out.println("Fetching withdrawal calldata...");
String withdrawPath = String.format(
"/v2/amplify/withdraw?vaultAddress=%s&wantAsset=%s" +
"&shareAmount=%s&userAddress=%s&chainId=%d",
VAULT_ADDRESS, WANT_ASSET, SHARE_AMOUNT, userAddress, CHAIN_ID);
JsonObject withdrawResp = apiGet(withdrawPath);
JsonObject tx = withdrawResp.getAsJsonObject("transaction");
// Step 3: Sign and submit
System.out.println("Submitting withdrawal order...");
nonce = web3.ethGetTransactionCount(userAddress,
org.web3j.protocol.core.DefaultBlockParameterName.PENDING)
.send().getTransactionCount();
RawTransaction withdrawTx = RawTransaction.createTransaction(
nonce, gasPrice, BigInteger.valueOf(300000),
tx.get("to").getAsString(), BigInteger.ZERO,
tx.get("data").getAsString());
byte[] signedWithdraw = TransactionEncoder.signMessage(
withdrawTx, CHAIN_ID, credentials);
String txHash = web3.ethSendRawTransaction(
Numeric.toHexString(signedWithdraw)).send().getTransactionHash();
System.out.println("Withdrawal submitted: " + txHash);
}
}
Error Responses
| Status | Meaning |
|---|---|
| 400 | Invalid parameters |
| 404 | No account found for the given vaultAddress + chainId |