Skip to main content
This guide walks through the complete deposit flow using REST API calls: discover an account, detect the authorization method, prepare calldata, sign, and submit the transaction. The unified deposit API handles three authorization paths automatically:
MethodDescription
PermitGas-efficient off-chain signature (EIP-2612). Single transaction — no separate approval needed.
ApprovalStandard ERC-20 approve transaction followed by the deposit transaction.
Already ApprovedDirect deposit when sufficient allowance already exists.

Prerequisites

  • A Paxos Labs API key
  • An EVM-compatible signer (private key, HSM, or wallet service)
  • An HTTP client library for your language

Step 0: Fetch Available Accounts

Retrieve all accounts accessible with your API key. Accounts are grouped by name, with a deployments[] array per chain. Record boringVaultAddress, chainId, and baseTokenAddress for the deployment you want to deposit into.
curl "https://api.paxoslabs.com/v2/amplify/vaults?filter=chainId%3D1%20AND%20inDeprecation%3Dfalse" \
  -H "x-api-key: pxl_your_key"
From the response, pick an account and note these fields from the matching deployments[i] entry:
FieldUsage
boringVaultAddressPassed to GET /v2/core/permit and GET /v2/amplify/deposit as vaultAddress
chainIdPassed to all endpoints
baseTokenAddressThe primary deposit asset address

Step 1: Check Authorization

GET /v2/core/permit detects whether the deposit token supports EIP-2612 permits, requires a standard ERC-20 approval, or already has sufficient allowance.

Parameters

ParameterTypeRequiredDescription
vaultAddressstringYesBoringVault contract address (boringVaultAddress from discovery)
tokenAddressstringYesERC-20 deposit token address
amountstringYesDeposit amount in token base units (decimal string)
userAddressstringYesDepositor’s wallet address
chainIdnumberYesEVM chain ID

Response Variants

The response method field tells you which path to follow: permit — Gas-efficient off-chain signature (EIP-2612). Single transaction, no separate approval needed.
{
  "method": "permit",
  "permitData": {
    "domain": {
      "name": "USD Coin",
      "version": "2",
      "chainId": 1,
      "verifyingContract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
    },
    "types": { "Permit": [...] },
    "value": {
      "owner": "0x1234...",
      "spender": "0xcccc...",
      "value": "1000000",
      "nonce": "0",
      "deadline": "9999999999"
    },
    "deadline": "9999999999"
  }
}
approval — Standard ERC-20 approve() required before depositing.
{
  "method": "approval",
  "approvalTransaction": {
    "encoded": "0x095ea7b3000000000000000000000000..."
  }
}
already_approved — Sufficient allowance exists. Skip directly to the deposit.
{
  "method": "already_approved"
}

Step 2: Handle Authorization

Permit Path

Sign the EIP-712 typed data from permitData using eth_signTypedData_v4 (or your library’s equivalent), then include the signature in the deposit request.

Approval Path

Send the approvalTransaction.encoded calldata as a transaction to the deposit token contract address (tokenAddress from step 1). Wait for confirmation, then proceed to the deposit.

Already Approved Path

Skip directly to step 3.

Step 3: Prepare Deposit Calldata

GET /v2/amplify/deposit returns the transaction object for the deposit.

Parameters

ParameterTypeRequiredDescription
vaultAddressstringYesBoringVault contract address (0x + 40 hex chars)
depositAssetstringYesERC-20 token address to deposit
depositAmountstringYesAmount in token base units (decimal string)
userAddressstringYesWallet address signing and submitting the transaction. Also the default share recipient when to is omitted.
chainIdnumberYesEVM chain ID
tostringNoDestination address that receives the vault shares (maps to the on-chain to argument on DistributorCodeDepositor.deposit()). Defaults to userAddress when omitted.
permitSignaturestringNoEIP-2612 permit signature (65-byte hex). Required for permit path.
permitDeadlinenumberNoPermit deadline as Unix timestamp. Required when permitSignature is provided.
responseFormatstringNoencoded (default), full, or structured

Response

{
  "transaction": {
    "to": "0xcccc000000000000000000000000000000000001",
    "data": "0x47e7ef24000000000000000000000000...",
    "value": "0",
    "abi": [{"type": "function", "name": "deposit", "inputs": [...]}],
    "functionName": "deposit",
    "args": ["0xA0b8...", "1000000", "999500", "0x1234..."]
  }
}
The abi, functionName, and args fields are only present when responseFormat is full or structured. See Authentication for details.

Step 4: Sign and Submit

Send the transaction using your signer or wallet infrastructure:
  • to — the contract address to call
  • data — the ABI-encoded calldata (when using encoded or full format)
  • value — ETH to send (usually "0" for ERC-20 deposits)

Complete Examples

import { createWalletClient, createPublicClient, http, custom } 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 DEPOSIT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const CHAIN_ID = 1;
const AMOUNT = "1000000"; // 1 USDC (6 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: Check authorization
  console.log("Checking authorization method...");
  const permitUrl = new URL(`${BASE}/v2/core/permit`);
  permitUrl.searchParams.set("vaultAddress", VAULT_ADDRESS);
  permitUrl.searchParams.set("tokenAddress", DEPOSIT_ASSET);
  permitUrl.searchParams.set("amount", AMOUNT);
  permitUrl.searchParams.set("userAddress", account.address);
  permitUrl.searchParams.set("chainId", String(CHAIN_ID));

  const permitResp = await fetch(permitUrl, { headers: HEADERS }).then((r) =>
    r.json()
  );
  console.log(`Authorization method: ${permitResp.method}`);

  // Step 2: Build deposit request params
  const depositUrl = new URL(`${BASE}/v2/amplify/deposit`);
  depositUrl.searchParams.set("vaultAddress", VAULT_ADDRESS);
  depositUrl.searchParams.set("depositAsset", DEPOSIT_ASSET);
  depositUrl.searchParams.set("depositAmount", AMOUNT);
  depositUrl.searchParams.set("userAddress", account.address);
  depositUrl.searchParams.set("chainId", String(CHAIN_ID));

  if (permitResp.method === "permit") {
    const { domain, types, value } = permitResp.permitData;
    const signature = await walletClient.signTypedData({
      account,
      domain,
      types,
      primaryType: "Permit",
      message: value,
    });
    depositUrl.searchParams.set("permitSignature", signature);
    depositUrl.searchParams.set(
      "permitDeadline",
      permitResp.permitData.deadline
    );
  } else if (permitResp.method === "approval") {
    console.log("Sending approval transaction...");
    const approvalHash = await walletClient.sendTransaction({
      to: DEPOSIT_ASSET as `0x${string}`,
      data: permitResp.approvalTransaction.encoded as `0x${string}`,
      chain: mainnet,
      account,
    });
    await publicClient.waitForTransactionReceipt({ hash: approvalHash });
    console.log(`Approval confirmed: ${approvalHash}`);
  }

  // Step 3: Get deposit calldata
  console.log("Fetching deposit calldata...");
  const depositResp = await fetch(depositUrl, { headers: HEADERS }).then(
    (r) => r.json()
  );
  const tx = depositResp.transaction;

  // Step 4: Sign and submit
  console.log("Submitting deposit transaction...");
  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(`Deposit confirmed in block ${receipt.blockNumber}: ${hash}`);
}

main().catch(console.error);

Error Responses

StatusMeaning
400Invalid parameters (missing field, bad address format, invalid permit signature, deposit amount below fees, etc.)
404No account found for the given vaultAddress + chainId
503Upstream RPC unavailable (share rate, fee module, or supply cap read failed)