# List address book entries Source: https://developers.paxoslabs.com/v1.0.0/api-reference/address-book/list-address-book-entries /v1.0.0/api-reference/openapi.yml get /v2/addressBookEntries Returns address book entries for known addresses. Filter by chain ID, address, or name. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # Detect authorization requirements for vault deposit Source: https://developers.paxoslabs.com/v1.0.0/api-reference/authorization/detect-authorization-requirements-for-vault-deposit /v1.0.0/api-reference/openapi.yml get /v2/core/authorization Deprecated in favor of GET /v3/core/authorization, which returns all available authorization strategies and takes spenderAddress directly. # List authorization strategies for an ERC-20 spend Source: https://developers.paxoslabs.com/v1.0.0/api-reference/authorization/list-authorization-strategies-for-an-erc-20-spend /v1.0.0/api-reference/openapi.yml get /v3/core/authorization # Prepare deposit transaction Source: https://developers.paxoslabs.com/v1.0.0/api-reference/deposit/prepare-deposit-transaction /v1.0.0/api-reference/openapi.yml get /v2/amplify/deposit Returns ABI-encoded calldata for a vault deposit. Identify the vault by its BoringVault contract address. Provide a permit signature from the authorize step (GET /v2/core/authorization) to use the gas-free permit path; omit to use a standard approve+deposit flow. Note: permit signatures are bound to a specific spender, amount, nonce, and deadline, so replay is bounded — but because they are passed as query params they may appear in access logs, browser history, and proxy/CDN caches. Callers should use short `permitDeadline` values to minimize the exposure window. # Get health Source: https://developers.paxoslabs.com/v1.0.0/api-reference/health/get-health /v1.0.0/api-reference/openapi.yml get /health # Introduction Source: https://developers.paxoslabs.com/v1.0.0/api-reference/introduction Paxos Labs API V2 reference # Paxos Labs API The Paxos Labs API V2 provides REST (and GraphQL) endpoints for analytics and data across Paxos onchain products. All list endpoints support cursor-based pagination and a flexible `filter` query parameter. ## Authentication Include your API key in the `x-api-key` header: ``` x-api-key: pxl__ ``` ## Base URL ``` https://api.paxoslabs.com ``` ## Endpoints ### Amplify — Analytics | Endpoint | Method | Description | | --------------------------------- | ------ | ---------------------------------------------------- | | `/v2/amplify/vaultAssets` | GET | List depositable/withdrawable assets for accounts | | `/v2/amplify/vaultApys` | GET | Historical and current account APY data | | `/v2/amplify/vaultTvls` | GET | Historical and current account TVL data | | `/v2/amplify/vaultCompositions` | GET | Current asset composition for accounts | | `/v2/amplify/withdrawalRequests` | GET | Withdrawal requests filtered by status, chain, asset | | `/v2/amplify/withdrawalVolumes` | GET | Historical withdrawal volume by time interval | | `/v2/amplify/liquidityShortfalls` | GET | Current liquidity shortfalls across accounts | ### Amplify — Discovery & Configuration | Endpoint | Method | Description | | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | `/v2/amplify/vaults` | GET | Accounts grouped by name with per-chain deployments, contract addresses, per-asset fees, supply caps, min order size, and SLAs | ### Amplify — Transaction Preparation (Calldata) | Endpoint | Method | Description | | ----------------------------- | ------ | ------------------------------------------------------------------ | | `/v2/core/authorization` | GET | Detect authorization method (permit / approval / already approved) | | `/v2/amplify/deposit` | GET | Prepare deposit transaction calldata | | `/v2/amplify/withdraw` | GET | Prepare withdrawal order calldata | | `/v2/amplify/withdraw/cancel` | GET | Prepare cancel-withdrawal calldata | ### Amplify — Display Helpers | Endpoint | Method | Description | | ------------------------------------ | ------ | ---------------------------------------------------------------------------------------- | | `/v2/amplify/calculateWithdrawalFee` | GET | Preview the withdrawal fee for a specific `offerAmount` + `wantAsset` pair | | `/v2/amplify/supplyCaps` | GET | Read total deposits, supply cap, and percentage filled (per chain deployment) | | `/v2/amplify/userPositions` | GET | Read a user's share balance, position value in base, and current exchange rate per chain | ### WPAXG — Swaps | Endpoint | Method | Description | | ---------------------- | ------ | ---------------------------------------------------- | | `/v1/wpaxg/swapQuotes` | GET | Get swap quote and calldata for buying/selling WPAXG | ### Address Book | Endpoint | Method | Description | | ------------------------ | ------ | --------------------------------------------------------- | | `/v2/addressBookEntries` | GET | Known address entries filtered by chain, address, or name | ### Webhooks Register webhook endpoints in the Enterprise Console to receive real-time notifications when events occur that are relevant to your organization. For a comprehensive list of event types, see the [Event Catalog](/v1.0.0/intro/webhooks/event-catalog). ## Pagination All list endpoints accept `pageSize` (default 25, max 100) and `pageToken`. Omit `pageToken` for the first page; use the returned `nextPageToken` for subsequent pages. ## Filtering List endpoints accept a `filter` query string using `=`, `!=`, `AND`, and `OR` operators: ``` filter=chainId=1 AND status=PENDING ``` *** # Generate KYT compliance attestation Source: https://developers.paxoslabs.com/v1.0.0/api-reference/kyt/generate-kyt-compliance-attestation /v1.0.0/api-reference/openapi.yml post /v2/kytAttestation Returns KYT compliance attestation for the given transaction details. # Get PAXG swap quote Source: https://developers.paxoslabs.com/v1.0.0/api-reference/paxg/get-paxg-swap-quote /v1.0.0/api-reference/openapi.yml get /v1/paxg/swapQuotes Returns swap calldata for buying or selling PAXG via DEX aggregators. Supports Ethereum mainnet (chainId 1). The response includes the target contract, encoded calldata, and expected output amount. # List vault rewards (net of fees) Source: https://developers.paxoslabs.com/v1.0.0/api-reference/paxos-rewards/list-vault-rewards-net-of-fees /v1.0.0/api-reference/openapi.yml get /v2/paxosRewards/vaultRewards Returns the time-series of rewards distributed to a vault. All yield values are net of management and performance fees. `vaultAddress` and `chainId` are required. Time range: when both startTime and endTime are omitted, defaults to (now - 14 days, now); pass both to override. Pagination: pageSize default 25 (max 100); omit pageToken for the first page. # Create a smart deposit address for a user. Source: https://developers.paxoslabs.com/v1.0.0/api-reference/smart-deposit-routing/create-a-smart-deposit-address-for-a-user /v1.0.0/api-reference/openapi.yml post /v2/amplify/smartDepositAddresses Returns the smart deposit address for a given user, vault, input token, and chain. # Get submitOrder calldata Source: https://developers.paxoslabs.com/v1.0.0/api-reference/transit/get-submitorder-calldata /v1.0.0/api-reference/openapi.yml get /v1/transit/orders/quote Returns the TransitStation address, ABI-encoded `submitOrder(...)` calldata, and ETH `value` for a cross-chain Transit order. # Get Transit order Source: https://developers.paxoslabs.com/v1.0.0/api-reference/transit/get-transit-order /v1.0.0/api-reference/openapi.yml get /v1/transit/orders/{orderId} Returns the indexer-backed status and details for a Transit order. Poll this endpoint to track order progress. # List Transit orders Source: https://developers.paxoslabs.com/v1.0.0/api-reference/transit/list-transit-orders /v1.0.0/api-reference/openapi.yml get /v1/transit/orders Returns a paginated list of historical Transit orders for the given user address. # List Transit routes Source: https://developers.paxoslabs.com/v1.0.0/api-reference/transit/list-transit-routes /v1.0.0/api-reference/openapi.yml get /v1/transit/routes Returns the set of routes (source chain, destination LayerZero EID, offer/want assets) available to the caller. # List user positions in a vault Source: https://developers.paxoslabs.com/v1.0.0/api-reference/users/list-user-positions-in-a-vault /v1.0.0/api-reference/openapi.yml get /v2/amplify/userPositions Returns a user's share balance and current position value (in the vault base asset) per chain each vault is deployed on. userAddress is required as a top-level query param. filter is optional; narrow with the vaultAddress and/or chainId flags, or omit it to return the user's position in every live vault deployment. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List Amplify vaults Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-amplify-vaults /v1.0.0/api-reference/openapi.yml get /v2/amplify/vaults Returns aggregated vault configuration grouped by vault name: chain deployments (contract addresses, KYT flag, deposit supply cap, minimum withdrawal order size, in-deprecation flag) with nested per-asset config (depositable/withdrawable flags, deposit/withdraw fees, withdrawal SLAs). # List liquidity shortfalls Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-liquidity-shortfalls /v1.0.0/api-reference/openapi.yml get /v2/amplify/liquidityShortfalls Returns current liquidity shortfalls, indicating assets where demand exceeds available liquidity. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List vault APYs Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-vault-apys /v1.0.0/api-reference/openapi.yml get /v2/amplify/vaultApys Returns historical and current APY data for vaults. APY is returned in percent (e.g. 4.25 for 4.25%). Time range: provide startTime+endTime, or lookback from now; if all omitted, lookback defaults to 30 days (2592000s). startTime/endTime take precedence over lookback when both are set. Defaults: interval=86400s (1 day); orderByTimestamp=asc; pageSize=25 (max 100); omit pageToken for the first page. # List vault assets Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-vault-assets /v1.0.0/api-reference/openapi.yml get /v2/amplify/vaultAssets Returns depositable and withdrawable assets for vaults. Filter by vault address, chain ID, or asset capabilities. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List vault compositions Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-vault-compositions /v1.0.0/api-reference/openapi.yml get /v2/amplify/vaultCompositions Returns current asset composition for vaults, including component weights and values. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List vault deposit-cap utilization Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-vault-deposit-cap-utilization /v1.0.0/api-reference/openapi.yml get /v2/amplify/supplyCaps Returns up-to-the-block totalSupplyInBase, supplyCap, and percentageFilled per chain each vault is deployed on. totalSupplyInBase and supplyCap are both denominated in the vault base asset, so they are directly comparable. The filter parameter is optional; narrow with the vaultAddress and/or chainId flags, or omit it to return every live vault deployment. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List vault TVLs Source: https://developers.paxoslabs.com/v1.0.0/api-reference/vaults/list-vault-tvls /v1.0.0/api-reference/openapi.yml get /v2/amplify/vaultTvls Returns historical and current TVL data for vaults. Time range: provide startTime+endTime, or lookback from now; if all omitted, lookback defaults to 30 days (2592000s). startTime/endTime take precedence over lookback when both are set. Defaults: interval=86400s (1 day); orderByTimestamp=asc; pageSize=25 (max 100); omit pageToken for the first page. # Calculate withdrawal fee Source: https://developers.paxoslabs.com/v1.0.0/api-reference/withdraw/calculate-withdrawal-fee /v1.0.0/api-reference/openapi.yml get /v2/amplify/calculateWithdrawalFee Calculates the withdrawal fee the user would be charged for a given offer amount and want asset. # List withdrawal requests Source: https://developers.paxoslabs.com/v1.0.0/api-reference/withdraw/list-withdrawal-requests /v1.0.0/api-reference/openapi.yml get /v2/amplify/withdrawalRequests Returns withdrawal requests filtered by status, chain, asset, participants, and queue-processing metadata. Defaults: pageSize=25 (max 100); omit pageToken for the first page. # List withdrawal volumes Source: https://developers.paxoslabs.com/v1.0.0/api-reference/withdraw/list-withdrawal-volumes /v1.0.0/api-reference/openapi.yml get /v2/amplify/withdrawalVolumes Returns historical withdrawal volume aggregated by time interval. startTime, endTime, and vaultAddress are required. Optionally filter by chainId and wantAssetAddress using the filter parameter. Defaults: orderByTimestamp=asc; pageSize=25 (max 100); omit pageToken for the first page. interval is optional. # Prepare cancel withdrawal transaction Source: https://developers.paxoslabs.com/v1.0.0/api-reference/withdraw/prepare-cancel-withdrawal-transaction /v1.0.0/api-reference/openapi.yml get /v2/amplify/withdraw/cancel Returns ABI-encoded calldata for cancelling a pending vault withdrawal order on the WithdrawQueue contract. Identify the vault by its BoringVault contract address and the order by its index in the WithdrawQueue. Only the original order owner can successfully submit this transaction — ownership is enforced on-chain via msg.sender. To retrieve the required `orderIndex` parameter, call `GET /v2/amplify/withdrawalRequests` filtered by `userAddress` and `vaultAddress`; each returned item includes its `orderIndex`. # Prepare withdrawal transaction Source: https://developers.paxoslabs.com/v1.0.0/api-reference/withdraw/prepare-withdrawal-transaction /v1.0.0/api-reference/openapi.yml get /v2/amplify/withdraw Returns ABI-encoded calldata for a vault withdrawal. Identify the vault by its BoringVault contract address. The withdrawal mode is selected server-side per caller: eligible callers receive an atomic withdrawal that settles in a single transaction with no prior share approval, while all other callers submit a withdrawal order and must have pre-approved the WithdrawQueue to spend vault shares. # Get WPAXG swap quote Source: https://developers.paxoslabs.com/v1.0.0/api-reference/wpaxg/get-wpaxg-swap-quote /v1.0.0/api-reference/openapi.yml get /v1/wpaxg/swapQuotes Returns swap calldata for buying or selling WPAXG via DEX aggregators. Supports Optimism (chainId 10). The response includes the target contract, encoded calldata, and expected output amount. # Build with Paxos Labs Source: https://developers.paxoslabs.com/v1.0.0/index Enterprise-grade infrastructure for onchain products ## Amplify Issue, manage, and scale fully backed branded stablecoins native to your ecosystem. Enable your users to put their idle assets to work with native yield on stablecoins. Same-chain and cross-chain stablecoin swaps via Transit. ## Assets Our flagship stablecoin, fully backed by USDG, a regulated asset issued by Paxos. ## Utilities Turn fiat on-ramp deposits into yield-bearing positions in a single step. Receive real-time notifications when events occur across Paxos Labs products. # API Calldata AI Coding Reference Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/api-calldata-ai-reference Condensed REST calldata integration reference optimized for AI coding assistants Copy this page into your AI coding assistant (Cursor, Copilot, Claude, etc.) for accurate Amplify REST calldata API completions — the path that returns ready-to-sign EVM transactions without requiring the TypeScript SDK. This page is a condensed, single-file reference for the REST calldata API. For full documentation with per-language tabs and step-by-step walkthroughs, see the [API Calldata Integration guide](/v1.0.0/intro/products/earn/developers/guides/api-calldata/index). **Using a skill-aware tool?** The same content is exposed as the `amplify-earn-api-calldata` Agent Skill and is auto-discovered at [`/.well-known/agent-skills/index.json`](https://docs.paxoslabs.com/.well-known/agent-skills/index.json). Prefer the skill in agents that support them — it loads progressively. Use this page when you need to paste a single reference into a general-purpose AI chat. ## When to Use * Backend transaction builders in any language (Python, Go, Rust, Java, Node) * Custodial wallets, HSMs, multisig services — fetch calldata, sign elsewhere * Non-JS stacks that can't consume `@paxoslabs/amplify-sdk` * Prototyping with `curl` before application code For the TypeScript SDK with built-in wallet hooks, see the [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). For calling contracts directly without the API, see the [Contract AI Reference](/v1.0.0/intro/products/earn/developers/direct-contract-ai-reference). ## Base URL & Authentication ``` Base URL: https://api.paxoslabs.com API prefix: /v2 Auth: x-api-key: pxl__ ``` * Obtain a key at [https://app.paxoslabs.com](https://app.paxoslabs.com). * Send the key as a header — never as a query param (it ends up in access logs). ### OpenAPI Spec * [https://docs.paxoslabs.com/v1.0.0/api-reference/openapi.yml](https://docs.paxoslabs.com/v1.0.0/api-reference/openapi.yml) ## Endpoint Map ### Transaction preparation | Endpoint | Purpose | | --------------------------------- | ------------------------------------------------------------------------ | | `GET /v2/core/authorization` | Detect authorization method (`permit` / `approval` / `already_approved`) | | `GET /v2/amplify/deposit` | Build deposit calldata | | `GET /v2/amplify/withdraw` | Build withdrawal order calldata | | `GET /v2/amplify/withdraw/cancel` | Build cancel-order calldata | ### Discovery | Endpoint | Purpose | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /v2/amplify/vaults` | Accounts grouped by `name` with per-chain `deployments[]`; each deployment carries contract addresses, per-asset flags, fees, supply caps, min order size, SLAs | ### Order status | Endpoint | Purpose | | ------------------------------------ | ------------------------------------------------ | | `GET /v2/amplify/withdrawalRequests` | Withdrawal orders with `orderIndex` and `status` | ### Analytics | Endpoint | Purpose | | ------------------------------------- | ------------------------------------------------------------------------------- | | `GET /v2/amplify/vaultApys` | Historical APY time series per vault | | `GET /v2/amplify/vaultTvls` | Historical TVL time series (+ current on-chain TVL on the final page) | | `GET /v2/amplify/vaultAssets` | Per-asset depositable/withdrawable flags (paginated, flat) | | `GET /v2/amplify/vaultCompositions` | Current portfolio composition from the rate provider | | `GET /v2/amplify/withdrawalVolumes` | Historical withdrawal volumes (requires `vaultAddress`, `startTime`, `endTime`) | | `GET /v2/amplify/liquidityShortfalls` | Pending withdrawal demand in excess of current vault balance | ### Display helpers | Endpoint | Purpose | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `GET /v2/amplify/calculateWithdrawalFee` | Preview fee for a specific `offerAmount` + `wantAsset` redemption | | `GET /v2/amplify/supplyCaps` | Up-to-the-block `totalSupplyInBase`, `supplyCap`, and `percentageFilled` per chain deployment | | `GET /v2/amplify/userPositions` | User shareBalance, positionValueInBase, baseAssetAddress, and exchangeRateToBase per chain deployment | **On-`/v2/amplify/vaults`** (no dedicated endpoint needed): `depositSupplyCap`, `minimumWithdrawalOrderSize`, `assets[].depositFees`, `assets[].withdrawFees`, `assets[].withdrawalSLAs`. ## Response Format (calldata endpoints only) `/v2/amplify/deposit`, `/v2/amplify/withdraw`, and `/v2/amplify/withdraw/cancel` accept a `responseFormat` query parameter: | `responseFormat` | `data` (hex) | `abi` / `functionName` / `args` | Use when | | --------------------- | ------------ | ------------------------------- | --------------------------------------------------------------------------------- | | `encoded` *(default)* | ✅ | ❌ | Signer consumes raw `data` (`eth_sendTransaction`, HSM) | | `full` | ✅ | ✅ | Debugging; need raw + decoded views | | `structured` | ❌ | ✅ | Encode locally (viem `encodeFunctionData`, ethers `Interface.encodeFunctionData`) | ## Filter Syntax (`filter` query parameter) ``` filter=field%3Dvalue # equality filter=field1%3Dvalue1%20AND%20field2%3Dvalue2 # conjunction ``` * URL-encode once: `%3D` = `=`, `%20` = space. * Separate query params (`chainId=1&inDeprecation=false`) are **ignored**. ## Discovery — `GET /v2/amplify/vaults` Single aggregated endpoint. Params (all optional): `filter` (flags: `name`, `chainId`, `inDeprecation`, `requiresKyt`), `pageSize` (1–100, default 25), `pageToken`. ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/vaults?filter=chainId%3D1%20AND%20inDeprecation%3Dfalse" \ -H "x-api-key: $AMPLIFY_API_KEY" ``` ```json theme={null} { "vaults": [{ "name": "Amplify USDC Core", "deployments": [{ "chainId": 1, "boringVaultAddress": "0xbbbb...", "depositorAddress": "0xcccc...", "withdrawQueueAddress": "0xdddd...", "requiresKyt": false, "baseTokenAddress": "0xA0b8...", "accountantAddress": "0xaaaa...", "tellerAddress": "0xeeee...", "depositFeeAddress": null, "withdrawFeeAddress": null, "inDeprecation": false, "depositSupplyCap": { "raw": "1000000000000", "formatted": "1000000.0", "decimals": 6, "hasCap": true }, "minimumWithdrawalOrderSize": { "raw": "1000000000000000000", "formatted": "1.0", "decimals": 18 }, "assets": [{ "assetAddress": "0xA0b8...", "depositable": true, "withdrawable": true, "depositFees": { "bps": 0, "percentage": "0.0000" }, "withdrawFees": { "bps": 25, "percentage": "0.2500" }, "withdrawalSLAs": { "expectedDelay": "86400s", "expiryBuffer": "36000s", "internalWithdrawalQueueDelaySLA": "43200s", "externalWithdrawalQueueDelaySLA": "86400s", "internalAccountantRateUpdateDelaySLA": "43200s", "externalAccountantRateUpdateSLA": "86400s" } }] }] }], "nextPageToken": null } ``` ### Field → Transaction Parameter Mapping | Discovery field | Used as | | ------------------------------------------ | -------------------------------------------------------------------------------------------------- | | `deployments[].boringVaultAddress` | `vaultAddress` in `/v2/core/authorization` and all calldata endpoints; also the ERC-20 share token | | `deployments[].depositorAddress` | `transaction.to` returned by the prepared deposit | | `deployments[].withdrawQueueAddress` | Spender for the share approval before withdrawal | | `deployments[].baseTokenAddress` | Default `depositAsset` / `wantAsset` | | `deployments[].requiresKyt: true` | KYT attestation is resolved server-side — no special client handling required | | `deployments[].depositSupplyCap` | `{ raw, formatted, decimals, hasCap }`; `hasCap=false` → uncapped | | `deployments[].minimumWithdrawalOrderSize` | Minimum `shareAmount` accepted by `/v2/amplify/withdraw` | ## Deposit Flow Three authorization branches handled by one API: | Method | Meaning | | ------------------ | ---------------------------------------------------------------- | | `permit` | EIP-2612 off-chain signature — gas-efficient, single on-chain tx | | `approval` | Standard ERC-20 `approve` required first, then the deposit | | `already_approved` | Sufficient allowance exists — deposit directly | ### Step 1 — Detect authorization `GET /v2/core/authorization` | Param | Required | Notes | | -------------- | -------- | ------------------------------------------------------------ | | `vaultAddress` | Yes | `deployments[].boringVaultAddress` from `/v2/amplify/vaults` | | `tokenAddress` | Yes | ERC-20 deposit token | | `amount` | Yes | Decimal string, token base units | | `userAddress` | Yes | Depositor wallet | | `chainId` | Yes | EVM chain ID | Response variants: ```json theme={null} // permit { "method": "permit", "permitData": { "domain": { "name": "USD Coin", "version": "2", "chainId": 1, "verifyingContract": "0xA0b8..." }, "types": { "Permit": [ ... ] }, "value": { "owner": "0x1234...", "spender": "0xcccc...", "value": "1000000", "nonce": "0", "deadline": "9999999999" }, "deadline": "9999999999" } } // approval { "method": "approval", "approvalTransaction": { "encoded": "0x095ea7b3..." } } // already_approved { "method": "already_approved" } ``` ### Step 2 — Satisfy authorization | Branch | Action | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `permit` | Sign `domain` + `types` + `value` via `eth_signTypedData_v4` (`primaryType: "Permit"`). Keep signature **and** `permitData.deadline`. | | `approval` | Send tx with `to = tokenAddress`, `data = approvalTransaction.encoded`. Wait for receipt. | | `already_approved` | Skip to step 3. | ### Step 3 — Prepare deposit calldata `GET /v2/amplify/deposit` | Param | Required | Notes | | ----------------- | ----------- | -------------------------------------------------------------------------------- | | `vaultAddress` | Yes | `boringVaultAddress` | | `depositAsset` | Yes | ERC-20 to deposit | | `depositAmount` | Yes | Decimal string, token base units | | `userAddress` | Yes | Wallet that signs and submits; also the default share recipient | | `chainId` | Yes | EVM chain ID | | `to` | No | Share recipient override (maps to on-chain `to` arg). Defaults to `userAddress`. | | `permitSignature` | Conditional | Required on permit path | | `permitDeadline` | Conditional | From `permitData.deadline`; required with `permitSignature` | | `responseFormat` | No | `encoded` (default), `full`, `structured` | Response (encoded): ```json theme={null} { "transaction": { "to": "0xcccc...", "data": "0x47e7ef24...", "value": "0" } } ``` ### Step 4 — Sign & broadcast Send `{ to, data, value }`. `value` is always `"0"` for ERC-20 deposits. ### Reference implementation (Node / viem) ```ts theme={null} import { createWalletClient, createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' const BASE = 'https://api.paxoslabs.com' const HEADERS = { 'x-api-key': process.env.AMPLIFY_API_KEY! } // 1. Detect authorization const permitUrl = new URL(`${BASE}/v2/core/authorization`) 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', '1') const permit = await fetch(permitUrl, { headers: HEADERS }).then((r) => r.json()) // 2. Build deposit params, branch on method 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', '1') if (permit.method === 'permit') { const sig = await walletClient.signTypedData({ account, domain: permit.permitData.domain, types: permit.permitData.types, primaryType: 'Permit', message: permit.permitData.value, }) depositUrl.searchParams.set('permitSignature', sig) depositUrl.searchParams.set('permitDeadline', permit.permitData.deadline) } else if (permit.method === 'approval') { const hash = await walletClient.sendTransaction({ to: DEPOSIT_ASSET as `0x${string}`, data: permit.approvalTransaction.encoded as `0x${string}`, chain: mainnet, account, }) await publicClient.waitForTransactionReceipt({ hash }) } // 3. Fetch + submit deposit const { transaction: tx } = await fetch(depositUrl, { headers: HEADERS }).then((r) => r.json()) await walletClient.sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), chain: mainnet, account, }) ``` ## Withdrawal Flow Queued withdrawals are **asynchronous**: submitting an order locks shares in the `WithdrawQueue`; the protocol processes the queue off-cycle and transfers the asset on completion. Callers eligible for an **atomic withdrawal** (see Step 2) settle **synchronously** — the withdrawal completes when the prepared transaction confirms, so the approval and polling steps below do not apply to them. Branch on the prepared transaction's function: call Step 2 with `responseFormat=full` and read `transaction.functionName`. `bulkWithdraw` (atomic withdrawal) → skip Step 1 and Step 4; `submitOrder` / `submitOrderAndProcessAll` (queued order) → follow all steps. ### Step 1 — Approve share spending (queued withdrawals only) Skip when `functionName` is `bulkWithdraw` — atomic withdrawals involve no share allowance. Otherwise, standard ERC-20 `approve(spender, amount)`: * Token contract (`to`): `boringVaultAddress` * `spender`: `withdrawQueueAddress` * `amount`: share amount to redeem (always 18 decimals) Wait for a receipt before submitting the withdrawal. ### Step 2 — Prepare withdrawal calldata `GET /v2/amplify/withdraw` | Param | Required | Notes | | ------------------- | -------- | -------------------------------------------------------------------------------------------------- | | `vaultAddress` | Yes | `boringVaultAddress` | | `wantAsset` | Yes | ERC-20 to receive | | `shareAmount` | Yes | Decimal string, 18 decimals | | `userAddress` | Yes | Submitter; also the default `intendedDepositor`/`receiver`/`refundReceiver` when those are omitted | | `chainId` | Yes | EVM chain ID | | `intendedDepositor` | No | On-chain `SubmitOrderParams.intendedDepositor`. Defaults to `userAddress`. | | `receiver` | No | Address that receives `wantAsset` on settlement. Defaults to `userAddress`. | | `refundReceiver` | No | Address that receives refunded shares if the order is cancelled. Defaults to `userAddress`. | | `responseFormat` | No | `encoded` / `full` / `structured` | The server selects the withdrawal mode automatically for each caller and account — there is no client-side `atomic` or `bulk` flag. Atomic withdrawals return `functionName: 'bulkWithdraw'` (instant settlement, no queue order, no share approval, server-computed minimum-output floor at the current rate); all other callers receive a queued order — `submitOrderAndProcessAll` when the account settles orders in the same transaction, otherwise `submitOrder`. Response: ```json theme={null} { "transaction": { "to": "0xdddd...", "data": "0x1a2b3c4d...", "value": "0" } } ``` ### Step 3 — Sign & submit Broadcast. For queued orders (`submitOrder` / `submitOrderAndProcessAll`), confirmation locks shares under a new `orderIndex`. For atomic withdrawals (`bulkWithdraw`), confirmation **is** settlement — the `wantAsset` is paid out in this transaction and no `orderIndex` exists. ### Step 4 — Poll order status (queued withdrawals only) An atomic withdrawal never creates an order, so nothing will appear here — treat transaction confirmation as completion. ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/withdrawalRequests?\ filter=userAddress%3D0x1234...%20AND%20vaultAddress%3D0xbbbb..." \ -H "x-api-key: $AMPLIFY_API_KEY" ``` | Status | Meaning | | ---------------- | ----------------- | | `PENDING` | In queue | | `COMPLETE` | Asset transferred | | `PENDING_REFUND` | Being refunded | | `REFUNDED` | Shares returned | **Do not** filter on `status=PENDING` while polling — orders disappear the moment they reach a terminal state and you lose visibility into the outcome. Filter on `userAddress` + `vaultAddress` (and optionally `chainId`). ### Reference implementation (Node / viem) ```ts theme={null} import { encodeFunctionData, erc20Abi } from 'viem' // 1. Fetch withdraw calldata — responseFormat=full exposes functionName for branching const url = new URL('https://api.paxoslabs.com/v2/amplify/withdraw') url.searchParams.set('vaultAddress', VAULT_ADDRESS) url.searchParams.set('wantAsset', WANT_ASSET) url.searchParams.set('shareAmount', SHARE_AMOUNT) url.searchParams.set('userAddress', account.address) url.searchParams.set('chainId', '1') url.searchParams.set('responseFormat', 'full') const { transaction: tx } = await fetch(url, { headers: HEADERS }).then((r) => r.json()) // 2. Approve shares to the queue — not needed for atomic withdrawals if (tx.functionName !== 'bulkWithdraw') { 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 }) } // 3. Submit — for atomic withdrawals (bulkWithdraw), confirmation of this tx IS settlement (skip polling) await walletClient.sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), chain: mainnet, account, }) ``` ## Cancellation Flow Only `PENDING` orders are cancellable. The wallet submitting the cancel tx **must be** the same address that submitted the original order (`msg.sender` enforced on-chain). ### Step 1 — Find `orderIndex` ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/withdrawalRequests?\ filter=userAddress%3D0x1234...%20AND%20vaultAddress%3D0xbbbb...%20AND%20status%3DPENDING" \ -H "x-api-key: $AMPLIFY_API_KEY" ``` `orderIndex` is a **decimal string** — pass it verbatim; never coerce to a JS `number`. ### Step 2 — Prepare cancel calldata `GET /v2/amplify/withdraw/cancel` | Param | Required | Notes | | ---------------- | -------- | ------------------------------------------------- | | `vaultAddress` | Yes | `boringVaultAddress` | | `orderIndex` | Yes | Exact string from `withdrawalRequests.orderIndex` | | `chainId` | Yes | EVM chain ID | | `responseFormat` | No | `encoded` / `full` / `structured` | Response: ```json theme={null} { "transaction": { "to": "0xdddd...", "data": "0x5c975abb...", "value": "0" } } ``` ### Step 3 — Sign & broadcast Send from the same wallet that submitted the order. On confirmation, locked shares return to that wallet. ## Display Helpers (read-only) Most display data is returned inline on `/v2/amplify/vaults` (see Discovery above). Three dedicated helpers cover what isn't on the discovery response: fee preview for a specific redemption, up-to-the-block supply-cap utilization, and a user's current position. | Endpoint | Required params | Returns | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /v2/amplify/calculateWithdrawalFee` | `offerAmount`, `wantAsset`, `vaultAddress`, `chainId` | `{ feeAmount, offerFeePercentage: { bps, percentage }, flatFee }` — all decimal strings / integers, scaled to human-readable units | | `GET /v2/amplify/supplyCaps` | optional `filter` (flags: `vaultAddress`, `chainId`); optional `pageSize`, `pageToken` | `{ supplyCaps: [{ vaultAddress, chainId, totalSupplyInBase, supplyCap, percentageFilled }], nextPageToken }` — `supplyCap` / `percentageFilled` are `null` for uncapped vaults. With no filter, every live vault deployment is returned. Filter flags: `vaultAddress` (optional), `chainId` (optional). | | `GET /v2/amplify/userPositions` | required `userAddress` (top-level, hex); optional `filter` (flags: `vaultAddress`, `chainId`); optional `pageSize`, `pageToken` | `{ userPositions: [{ vaultAddress, chainId, shareBalance, positionValueInBase, baseAssetAddress, exchangeRateToBase }], nextPageToken, tokenMetadata }` — `baseAssetAddress` is read from the DB (or via `accountant.base()` for stand-in vaults where the canonical base asset isn't natively deployed). With no filter, the user position is returned for every live vault deployment. Filter flags: `vaultAddress` (optional), `chainId` (optional). | Fields already on `/v2/amplify/vaults` (no dedicated endpoint): | Field | Shape | Notes | | ----------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------- | | `deployments[].depositSupplyCap` | `{ raw, formatted, decimals, hasCap }` | `hasCap: false` → uncapped (maxUint256) | | `deployments[].minimumWithdrawalOrderSize` | `{ raw, formatted, decimals }` | Minimum `shareAmount` accepted by `/v2/amplify/withdraw` | | `deployments[].assets[].depositFees` / `withdrawFees` | `{ bps, percentage }` | Per-asset fees | | `deployments[].assets[].withdrawalSLAs` | Protobuf duration strings (e.g. `"86400s"`) | Queue + rate-update SLAs | Client caching of 10–30s is safe; invalidate after any of the caller's own deposits, withdrawals, or cancellations. ## Error Envelope ```json theme={null} { "error": { "code": 400, "message": "vaultAddress is not a valid hex address", "status": "INVALID_ARGUMENT", "details": [{ "@type": "type.paxoslabs.dev/errors/BadRequest", "fieldViolations": [{ "field": "vaultAddress", "description": "..." }] }] } } ``` | HTTP | `error.status` | Action | | ---- | -------------------- | ------------------------------------------- | | 400 | `INVALID_ARGUMENT` | Fix request; inspect `fieldViolations` | | 401 | `UNAUTHENTICATED` | Verify `x-api-key` header | | 403 | `PERMISSION_DENIED` | Rotate the key | | 404 | `NOT_FOUND` | Re-discover via `/v2/amplify/vaults` | | 429 | `RESOURCE_EXHAUSTED` | Back off (respect `Retry-After` if present) | | 503 | `INTERNAL` | Retry with exponential backoff | Retries: `429` and `503` are safe. `400`, `401`, `403`, `404` are deterministic — do not retry without changing inputs. ## Common Gotchas | Gotcha | Consequence | Fix | | ----------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- | | Filter passed as separate query params | Predicates silently ignored | Use single `filter=field%3Dvalue%20AND%20...` | | `permitSignature` without `permitDeadline` | `400 INVALID_ARGUMENT` | Send both from the permit response | | Expired permit deadline | On-chain revert | Re-fetch `/v2/core/authorization` | | Approval tx not mined before deposit | Revert (insufficient allowance) | Wait for receipt before preparing deposit | | Passing a client-side `atomic` or `bulk` flag to `/v2/amplify/withdraw` | Ignored | The server selects the mode automatically per caller and account — branch on the returned `functionName` | | Polling `withdrawalRequests` after an atomic withdrawal | No order ever appears | Atomic withdrawals settle in the withdrawal transaction itself — treat tx confirmation as completion | | Deposit `amount` uses wrong decimals | Revert / rounding | USDC/USDT = 6 decimals; shares always 18 | | `shareAmount` uses token decimals | Too-small withdrawal or `400` | Share tokens are always 18 decimals | | Polling with `status=PENDING` | Order "vanishes" at completion | Drop the `status` predicate from the poll query | | `orderIndex` coerced to `number` | Off-by-one for large indices | Keep as string end-to-end | | Cancel from a different wallet | On-chain revert | Sign from the original submitter | | Stale address cache | Calls hit deprecated contracts | Respect `inDeprecation`; refresh on startup | ## Supported Chains | Chain | ID | | -------------- | -------- | | Ethereum | 1 | | Sepolia | 11155111 | | Base | 8453 | | HyperEVM | 999 | | Stable Testnet | 2201 | ## Canonical Docs * Overview: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/index](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/index) * Authentication: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/authentication](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/authentication) * Discovery: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/discovery](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/discovery) * Deposits: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/deposits](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/deposits) * Withdrawals: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/withdrawals](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/withdrawals) * Cancellations: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/cancellations](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/cancellations) * Display helpers: [https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/index](https://docs.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/index) * OpenAPI spec: [https://docs.paxoslabs.com/v1.0.0/api-reference/openapi.yml](https://docs.paxoslabs.com/v1.0.0/api-reference/openapi.yml) * Agent Skill: [https://docs.paxoslabs.com/.well-known/agent-skills/index.json](https://docs.paxoslabs.com/.well-known/agent-skills/index.json) # Changelog Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/changelog Release history and migration guides for the Amplify SDK All notable changes to the Amplify SDK are documented here. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Latest Release ### Breaking — 1.0.0 rewrite `@paxoslabs/amplify-sdk@1.0.0` is a ground-up rewrite with a new public surface. See the [migration guide](./migrating-from-0-5) for a side-by-side rename table covering every removed export. ### Added * **`AmplifyClient`** — single entry point. Construct with `{ apiKey }`, reuse anywhere. Defaults to production; pass `environment` only for non-production use. * **Subclients** — `client.deposit`, `client.withdraw`, `client.vaults`, `client.authorization`, `client.users`. Each method returns a typed `Promise`. * **`AmplifyError` / `AmplifyTimeoutError`** — single error hierarchy. Surface non-2xx responses, network failures, and timeouts. * **`client.core.authorization.detect(...)`** — authorization flow that returns one of `permit`, `approval`, or `already_approved`. Replaces the `prepareDepositAuthorization` / `prepareWithdrawalAuthorization` helpers. ### Changed * **Endpoint rename: `/v2/core/permit` → `/v2/core/authorization`** — The authorization detection endpoint has been renamed to better reflect its purpose (detecting whether a permit, approval, or already-approved path applies). The old `/v2/core/permit` path returns a **308 Permanent Redirect** to the new path for backwards compatibility. SDK users should update to use `client.core.authorization.detect(...)` instead of `client.permit.detect(...)`. The response shape is unchanged — `method: 'permit'`, `permitData`, and `approvalTransaction` fields retain their original names since they describe the actual EIP-2612 mechanism. ### Removed * `initAmplifySDK`, the in-memory `VaultCache`, and all cache helpers. * All flat-function exports: `prepareDeposit`, `prepareDepositTxData`, `prepareDepositWithPermitTxData`, `prepareDepositPermitSignature`, `prepareDepositAuthorization`, `prepareWithdrawal`, `prepareWithdrawalAuthorization`, `prepareWithdrawOrderTxData`, `prepareApproveWithdrawOrderTxData`, `prepareCancelWithdrawOrderTxData`, `prepareApproveDepositTokenTxData`. * All ABI exports. * EIP-712 helpers (`parsePermitSignature`, `toEthSignTypedDataV4`, `PERMIT_TYPES`). * On-chain check helpers (`isDepositSpendApproved`, `isWithdrawalSpendApproved`). * Typed error hierarchy (`VaultNotFoundByAddressError`, `UnauthorizedVaultAccessError`, `UnsupportedChainError`, `VaultConfigIncompleteError`, …) — replaced by a single `AmplifyError`. * SDK-managed telemetry, logger, and `LogLevel` enum. * Display helpers (`getVaultTVL`, `getVaultAPY`, `getMinimumMint`, `getMinimumWithdrawalOrderSize`, `getDepositCap`, `getWithdrawalFee`, `getWithdrawalRequests`, `calculateDepositFee`, `findVaultByConfig`, `getSupportedAssets`, `getVaultsByConfig`, `getWithdrawSupportedAssets`). * `vaultName`, `yieldType`, `distributorCode`, `DepositAuthMethod`, `WithdrawAuthMethod`, and `YieldType` parameters / types — replaced by `vaultAddress` and the backend's resolution. ### Migration from v0.5.x Upgrade is **fully breaking**. Every integration must be updated. See the [migration guide](./migrating-from-0-5) for the rename table and per-flow walkthroughs. ### Terminology Change Starting in v0.5.3, the documentation and UI refer to **"accounts"** instead of **"vaults"**. This is a naming change only — all SDK API identifiers (`getVaultsByConfig`, `AmplifyVault`, `vaultName`, `vaultAddress`, etc.) remain unchanged. No code migration is required. ### New Features * **Supply cap awareness** — The SDK now reads and enforces account deposit supply caps on-chain. Deposits that would exceed the account's `supplyCapInBase` (read from the `DistributorCodeDepositor` contract) are rejected with a descriptive `APIError` before submitting a transaction. The cached `AmplifyVault` type exposes a new `depositCap?: VaultDepositCap` field with `supplyCapInBase` and `hasDepositCap`. * **`getDepositCap()`** — New display helper that reads the supply cap and fee module address from the DCD contract on-chain. Returns `supplyCapInBase`, `hasDepositCap`, `depositFeeModuleAddress`, and `hasFees`. * **`calculateDepositFee()`** — New display helper that reads deposit fee parameters from the on-chain fee module. Returns a full fee breakdown: `feePercentage`, `flatFee`, `variableFeeAmount`, `totalFeeAmount`, `depositAmountAfterFees`, and `assetDecimals`. * **Deposit fee data on accounts** — `AmplifyVault.depositFees?: VaultDepositFees` carries per-asset fee data, with defaults applied when the fee module is configured but no data is available. Includes `hasFees`, `feeModuleAddress`, and per-asset `feePercentage`/`flatFee`. * **Withdraw fee data on accounts** — `AmplifyVault.withdrawFees?: VaultWithdrawFees` carries withdraw queue fee metadata. Includes `hasFees`, `feeModuleAddress`, `offerFeeBps`, and `offerFeeRate`. * **Account enrichment** — `getVaults()` now automatically enriches accounts with on-chain supply caps and default fee structures during cache population. Supply cap reads only run for accounts with both a `communityCodeDepositorAddress` and `depositFeeModuleAddress`. ### Improvements * **`VaultContracts` expanded** — Added `depositFeeModuleAddress` and `withdrawFeeModuleAddress` fields to the account contracts interface. * **Deposit cap enforcement** — `prepareDepositTxData()` and `prepareDepositWithPermitTxData()` now validate deposits against the supply cap before building the transaction, comparing the deposit amount (converted to base units via accountant rate) against remaining capacity. ### Migration from v0.5.2 Upgrade is **non-breaking**. Install `@paxoslabs/amplify-sdk@0.5.3` to pick up the new features. The new `depositCap`, `depositFees`, and `withdrawFees` fields on `AmplifyVault` are optional — existing code that destructures account objects will not break. If you want to display fee or cap information, use the new display helpers: ```ts theme={null} import { getDepositCap, calculateDepositFee } from '@paxoslabs/amplify-sdk' ``` ### Direct Contract Integration — KYT Accounts * **KYT-only deposit documentation** — The Direct Contract deposits guide now documents the `DistributorCodeDepositorV1` (KYT) contract exclusively. Both `deposit()` and `depositWithPermit()` include the required `_attestation` tuple parameter with empty/zero default values. Contact the Paxos Labs team to implement a compliance policy. * **KYT callout on Direct Contract overview** — Added a warning banner to the Direct Contract overview page about the KYT account requirement. ### Documentation Fixes * **Permit deposit `txData` shape corrected** — Code examples in the Deposit Workflow, Deposits Guide (Privy, Wagmi, and Viem tabs), and Quickstart now use the flat `txData` structure (`txData.abi`, `txData.functionName`, `txData.args`) introduced in v0.5.2. Previously these examples incorrectly referenced the pre-v0.5.2 nested shape (`txData.data.abi`). * **BigInt serialization fix for Privy permit signing** — Replaced `JSON.stringify(auth.permitData)` with the SDK's `toEthSignTypedDataV4()` helper in the Deposits Guide (Privy tab) and Quickstart. `JSON.stringify` throws a `TypeError` on BigInt values in `permitData.message`. * **`getRateInQuoteSafe` ABI corrected** — The Accountant ABI in the Direct Contract Deposits and Withdrawals guides now includes the required `quote: address` input parameter. The previous ABI had an empty `inputs` array, which would produce the wrong function selector on-chain. * **`VaultContracts` field name corrected** — Renamed `distributorCodeDepositorAddress` to `communityCodeDepositorAddress` in the Types reference to match the SDK's actual `VaultContracts` interface. * **`AmplifyVault` type updated** — Added missing `inDeprecation: boolean` and `enterpriseConfig?: EnterpriseConfig` fields to the `AmplifyVault` interface in the Types reference, `getVaultsByConfig`, `findVaultByConfig`, and AI Reference docs. * **`getWithdrawalFee()` parameters corrected** — Fixed parameter names (`withdrawAssetAddress` → `assetAddress`, `withdrawAmount` → `amount`) and added missing required parameters (`offerAsset`, `wantAsset`, `receiver`) in the AI Reference. Return type corrected from `string` to `bigint`. * **`getMinimumWithdrawalOrderSize()` parameter corrected** — Fixed parameter name (`withdrawAssetAddress` → `assetAddress`) in the AI Reference. Return type corrected from `string` to `bigint` with additional `shareDecimals: number` field. ### New Features * **KYT deposit routing** — Deposits automatically route to the KYT-enabled `DistributorCodeDepositorV1` contract when an account has `enterpriseConfig.predicatePolicyId` set. The SDK returns a discriminated union (`StandardDepositTxData | KytDepositTxData`) with a `depositType` field so consumers can distinguish between standard and KYT deposits. Type guards `isKytDeposit()` and `isStandardDeposit()` are exported for narrowing. * **Account deprecation warnings** — Accounts with `inDeprecation: true` now trigger a warning via the SDK logger when `prepareDepositTxData()` or `prepareDepositWithPermitTxData()` is called. The `AmplifyVault` type exposes the new `inDeprecation: boolean` field. * **Enterprise config on accounts** — `AmplifyVault.enterpriseConfig` carries `predicatePolicyId` from the GraphQL schema, enabling client-side KYT policy awareness. ### Improvements * **Flattened permit deposit return** — `prepareDepositWithPermitTxData()` now returns a flat object (`{ depositType, abi, address, functionName, args, chainId }`) matching the shape of `prepareDepositTxData()`. Previously the ABI and args were nested under a `.data` property. This makes both functions directly spreadable into viem's `writeContract`. ### Migration from v0.5.1 Upgrade is **non-breaking** for standard deposits. If you use `prepareDepositWithPermitTxData()` directly, update property access: ```ts theme={null} // Before (v0.5.1) await writeContract({ address: txData.address, abi: txData.data.abi, functionName: txData.data.functionName, args: txData.data.args, }) // After (v0.5.2) await writeContract({ address: txData.address, abi: txData.abi, functionName: txData.functionName, args: txData.args, }) ``` If you use the unified `prepareDeposit()` workflow, no changes are needed — it handles the new shape internally. ### New Features * **Base mainnet** — Base (`chainId` **8453**) is included in the SDK’s built-in chain map, so `getVaultsByConfig()`, deposit and withdrawal preparation, and on-chain reads resolve a proper `viem` `Chain` for Base without extra configuration. Pass `chainId: 8453` anywhere you already pass Ethereum or HyperEVM IDs. * **Cross-chain chain resolution** — When the wallet’s chain differs from the account’s chain (for example, multi-chain or bridged-asset flows), chain utilities can resolve the correct built-in chain config for the user’s `chainId` as well as the account’s. Upgrade from v0.5.0 is non-breaking for application code; install `@paxoslabs/amplify-sdk@0.5.1` (or latest patch) to pick up Base support. ### Breaking Changes This release contains breaking changes. All transaction and display helper functions now identify accounts by `vaultName` instead of `yieldType`. Review the migration steps below before upgrading. | Change | Before (v0.4.2) | After (v0.5.0) | | ------------------------------------------- | ---------------------------------- | ---------------------------------- | | Account identifier in transaction functions | `yieldType: YieldType` | `vaultName: string` | | Account identifier in display helpers | `yieldType: YieldType` | `vaultName: string` | | Account discovery | Implicit (SDK resolved internally) | Explicit via `getVaultsByConfig()` | | Withdrawal asset discovery | Not available | `getWithdrawSupportedAssets()` | | Account fetching | `fetchVaults()` | `getVaults()` | | Asset fetching | `fetchSupportedAssets()` | `getSupportedAssets()` | **Affected functions** — the `yieldType` parameter has been replaced by `vaultName` in all of the following: | Category | Functions | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Deposit | `prepareDepositAuthorization`, `prepareDeposit`, `prepareDepositTxData`, `prepareDepositPermitSignature`, `prepareDepositWithPermitTxData`, `prepareApproveDepositTokenTxData` | | Withdrawal | `prepareWithdrawalAuthorization`, `prepareWithdrawal`, `prepareWithdrawOrderTxData`, `prepareApproveWithdrawOrderTxData`, `prepareCancelWithdrawOrderTxData` | | Display | `getMinimumMint`, `getWithdrawalFee`, `getMinimumWithdrawalOrderSize` | ### New Features * **`getVaults()`** — Renamed from `fetchVaults()`. Same cache-first behavior, new name for consistency. * **`getSupportedAssets()`** — Renamed from `fetchSupportedAssets()`. Same cache-first behavior, new name for consistency. * **`getVaultsByConfig()`** — Multi-filter account discovery. Filter by `yieldType`, `chainId`, `depositAssetAddress`, `withdrawAssetAddress`, and `settlementAssetAddress`. * **`getWithdrawSupportedAssets()`** — Fetch all supported withdrawal assets grouped by token with their available accounts. * **`vaultName` parameter on display helpers** — `getVaultAPY`, `getVaultTVL`, and `getWithdrawalRequests` now accept `vaultName` as an alternative to `vaultAddress`. * **Cache management** — Fully documented: `initializeCache()`, `getCache()`, `refreshVaultCache()`, `isCacheReady()`, `waitForCacheReady()`. ### Migration Guide #### Why this change? In v0.4.2, `yieldType` served as the account identifier. This worked when each yield type mapped to a single account per chain, but broke down as multiple accounts with the same yield type launched on the same chain. The `vaultName` identifier is unique per account and future-proof. The new pattern is: **discover → then transact**. ```ts theme={null} // 1. Discover (once, at app startup or on chain/asset change) const [vault] = await getVaultsByConfig({ yieldType: YieldType.CORE, chainId: 1, depositAssetAddress: USDC, }) // 2. Transact (using the vault's name) await prepareDeposit({ vaultName: vault.name, depositAsset: USDC, depositAmount: '1000', to: userAddress, chainId: 1, }) ``` Add a call to `getVaultsByConfig()` wherever you previously passed `yieldType` directly to a transaction function. Cache the result for the session. ```ts theme={null} import { getVaultsByConfig, YieldType } from '@paxoslabs/amplify-sdk' // Before — no discovery step needed // After — discover the vault first const [vault] = await getVaultsByConfig({ yieldType: YieldType.CORE, chainId: 1, depositAssetAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', }) // vault.name is used in all subsequent calls ``` Replace `yieldType` with `vaultName` in every deposit function call. ```ts theme={null} // Before (v0.4.2) await prepareDepositTxData({ yieldType: YieldType.CORE, depositAsset: '0x...', depositAmount: '1000', to: userAddress, chainId: 1, }) // After (v0.5.0) await prepareDepositTxData({ vaultName: vault.name, depositAsset: '0x...', depositAmount: '1000', to: userAddress, chainId: 1, }) ``` The same change applies to: * `prepareDepositAuthorization()` * `prepareDeposit()` * `prepareDepositPermitSignature()` * `prepareDepositWithPermitTxData()` * `prepareApproveDepositTokenTxData()` ```ts theme={null} // Before (v0.4.2) await prepareWithdrawalAuthorization({ yieldType: 'CORE', wantAsset: USDC, withdrawAmount: '1.0', userAddress, chainId: 1, }) // After (v0.5.0) const [vault] = await getVaultsByConfig({ yieldType: 'CORE', chainId: 1 }) await prepareWithdrawalAuthorization({ vaultName: vault.name, wantAsset: USDC, withdrawAmount: '1.0', userAddress, chainId: 1, }) ``` The same change applies to: * `prepareWithdrawal()` * `prepareWithdrawOrderTxData()` * `prepareApproveWithdrawOrderTxData()` * `prepareCancelWithdrawOrderTxData()` Display helpers that previously used `yieldType` to locate an account now use `vaultName`. ```ts theme={null} // Before (v0.4.2) const result = await getMinimumMint({ yieldType: YieldType.CORE, chainId: 1, depositAssetAddress: USDC, depositAmount: '1000.0', }) // After (v0.5.0) const [vault] = await getVaultsByConfig({ yieldType: YieldType.CORE, chainId: 1, }) const result = await getMinimumMint({ vaultName: vault.name, chainId: 1, depositAssetAddress: USDC, depositAmount: '1000.0', }) ``` The same change applies to: * `getWithdrawalFee()` * `getMinimumWithdrawalOrderSize()` Additionally, `getVaultAPY()`, `getVaultTVL()`, and `getWithdrawalRequests()` now accept `vaultName` as an alternative to `vaultAddress`: ```ts theme={null} // New — use vaultName instead of looking up vaultAddress manually const apy = await getVaultAPY({ vaultName: vault.name }) const tvl = await getVaultTVL({ vaultName: vault.name, chainId: 1 }) ``` If you wrapped SDK calls in React hooks, update the interface and params: ```ts theme={null} // Before (v0.4.2) interface DepositParams { amount: string depositAsset: `0x${string}` yieldType: YieldType } const params = { yieldType, depositAsset, depositAmount: amount, to: address, chainId, } // After (v0.5.0) interface DepositParams { amount: string depositAsset: `0x${string}` vaultName: string } const params = { vaultName, depositAsset, depositAmount: amount, to: address, chainId, } ``` Use `getVaultsByConfig()` in a `useQuery` or `useEffect` to discover the account name at component mount time, then pass it to your deposit/withdrawal hooks. Use these patterns to locate all call sites in your codebase: ```bash theme={null} # Find all yieldType usages in SDK calls rg "yieldType.*YieldType\." --type ts rg "yieldType.*['\"]CORE['\"]" --type ts # Find all affected function calls rg "prepare(Deposit|Withdrawal|WithdrawOrder|ApproveWithdraw|CancelWithdrawOrder)" --type ts rg "get(MinimumMint|WithdrawalFee|MinimumWithdrawalOrderSize)" --type ts ``` *** ### Breaking Changes * Withdrawal flow migrated from AtomicQueue to WithdrawQueue. * Renamed withdrawal APIs: * `prepareWithdrawTransactionData()` -> `prepareWithdrawOrderTxData()` * `prepareApproveWithdrawToken()` -> `prepareApproveWithdrawOrderTxData()` * Added `prepareCancelWithdrawOrderTxData()` for order cancellation. * Withdrawal slippage parameters removed from the new flow. ### Highlights * Added unified `prepareWithdrawal()` wrapper for withdrawal execution. * Added unified `prepareWithdrawalAuthorization()` wrapper. * Added smart-wallet detection for deposit/withdraw authorization routing. * Added `forceMethod` parameter for explicit authorization routing. *** ### Breaking Changes This release contains breaking changes. Review the migration steps below before upgrading. | Change | Before | After | | ----------------------- | ---------------------------------- | ------------------------------ | | Deposit parameter names | `depositToken`, `recipientAddress` | `depositAsset`, `to` | | Node.js requirement | 20+ | 22+ | | Yield type constants | `PRIME`, `TBILL`, `LENDING` | `CORE`, `TREASURY`, `FRONTIER` | ```ts theme={null} // Before (v0.2.x) await prepareDepositTxData({ depositToken: "0x...", recipientAddress: "0x...", // ... }); // After (v0.3.0) await prepareDepositTxData({ depositAsset: "0x...", to: "0x...", // ... }); ``` ```ts theme={null} // Before (v0.2.x) import { YieldType } from "@paxoslabs/amplify-sdk"; YieldType.PRIME; YieldType.TBILL; YieldType.LENDING; // After (v0.3.0) import { YieldType } from "@paxoslabs/amplify-sdk"; YieldType.CORE; YieldType.TREASURY; YieldType.FRONTIER; ``` ### Features * Improved TypeScript inference for deposit functions * Added `eth_signTypedData_v4` helper for better wallet compatibility ### Bug Fixes * Fixed `instanceof` checks for `APIError` and `WithdrawError` in transpiled code ### Refactoring * Standardized parameter naming across deposit APIs to match contract terminology * Removed deprecated display module and bridge functionality * Added explicit exports for tree-shaking optimization *** ## Previous Releases ### Bug Fixes * Fixed spender address and decimals for permit flow * Improved cache-based lookup for token address resolution * Aligned `EIP712Domain` with viem's `TypedDataDomain` ### Bug Fixes * Use non-interactive test command in release hooks * Resolved preact JSON VNode Injection vulnerability * Use `DistributorCodeDepositor` as approval spender * Aligned slippage defaults to `DEFAULT_SLIPPAGE_BPS` (50 bps) ### Documentation * Completed Quick Start deposit example in README ### Refactoring * Converted `LogLevel` enum to `as const` pattern for better tree-shaking ### Features * **Unified Deposit API**: Added `prepareDeposit` and `prepareDepositAuthorization` wrapper functions * **Observability**: Added logging and telemetry infrastructure * **ERC-20 Enhancements**: Added `getTokenPermitInfoWithAllowance` with unified multicall ### Bug Fixes * Updated Sei chain ID from 713715 to 1329 * Fixed missing multicall mock in deposit-with-permit tests * Prevented duplicate buffer-full warning messages in telemetry ### Refactoring * Converted `DepositAuthMethod` enum to `as const` pattern * Centralized `API_BASE_URL` constant * Use unified multicall for `isDepositSpendApproved` ### Bug Fixes * Corrected `DepositTxData` args tuple * Fixed withdraw documentation * Resolved `whatBump is not a function` release error ### Refactoring * Use `DistributorCodeDepositor` for all deposits and permit spender ### Features * Export `CommunityCodeDepositTxData` type * Fixed cache check for account data ### Bug Fixes * Updated `DistributorCodeDepositor` support for partner code deposits * Improved type safety and chain cache initialization ### Refactoring * Updated branding from Earn SDK to Amplify SDK ### Features * **Initial SDK Release** * Comprehensive `AmplifyVault` support * Multi-chain support for yield accounts * Complete deposit functionality with approval management * Slippage protection for all operations * **Withdraw Flow** * `prepareWithdrawTransactionData()` for transaction preparation * Automatic account data fetching via `fetchSupportedAssets()` * Three-field account resolution (yieldType + wantToken + chainId) * Configurable slippage protection (default 0.5%) * **Developer Experience** * Full TypeScript support with type safety * Comprehensive error handling with specific error codes * Exchange rate calculations ### Build System * Automated release workflow with conventional commits * Semantic versioning with alpha/beta/rc support * CI/CD pipeline with quality gates * Security auditing and dependency scanning * Automated NPM publishing with provenance *** ## SDK Rename Migration The SDK was renamed from **Earn SDK** to **Amplify SDK** in version 0.1.0. Follow the migration steps below if upgrading from `@paxoslabs/earn-sdk`. ```bash npm uninstall @paxoslabs/earn-sdk npm install @paxoslabs/amplify-sdk theme={null} ``` ```ts theme={null} // Before import { initEarnSDK, type EarnVault } from "@paxoslabs/earn-sdk"; // After import { initAmplifySDK, type AmplifyVault } from "@paxoslabs/amplify-sdk"; ``` ```ts theme={null} // Before await initEarnSDK("pxl_your_api_key"); // After await initAmplifySDK("pxl_your_api_key"); ``` ```ts theme={null} // Before const vault: EarnVault = /* ... */; // After const vault: AmplifyVault = /_ ... _/; ``` API endpoints continue to use `/v1/earn-sdk/*` for backwards compatibility. No backend changes required. # Contract AI Coding Reference Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/direct-contract-ai-reference Condensed smart contract integration reference optimized for AI coding assistants Copy this page into your AI coding assistant (Cursor, Copilot, Claude, etc.) for accurate Amplify account direct contract completions. This page is a condensed, single-file reference for calling Amplify account contracts directly — without the SDK. For full documentation with interactive examples, see the [Direct Contract Integration guides](/v1.0.0/intro/products/earn/developers/guides/direct-contract/index). **Prefer server-side calldata instead of direct contract calls?** Use the [Calldata AI Reference](/v1.0.0/intro/products/earn/developers/api-calldata-ai-reference) — the REST path returns ready-to-sign transaction data with no local ABI management. Skill-aware tools also auto-discover the `amplify-earn-api-calldata` Agent Skill at [`/.well-known/agent-skills/index.json`](https://docs.paxoslabs.com/.well-known/agent-skills/index.json). ## Contract Source Open-source Solidity: [github.com/paxoslabs/nucleus-boring-account](https://github.com/paxoslabs/nucleus-boring-vault) ## Architecture | Contract | Role | GraphQL Field | | ---------------------------- | ----------------------------------------------------- | ---------------------------------------- | | **DistributorCodeDepositor** | Deposits (standard + permit) | `vault.communityCodeDepositorModuleId` | | **WithdrawQueue** | Withdrawal orders + cancellations (ERC-721 per order) | `vault.withdrawQueueModuleId` | | **BoringVault** | ERC-20 account share token (18 decimals) | `vault.boringVaultAddress` | | **Accountant** | Exchange rate oracle | `vault.accountantModuleId` | | **Teller** | Pause state | `vault.tellerModuleId` | | **FeeModule** | Withdrawal fee calculator | Obtained via `WithdrawQueue.feeModule()` | > `communityCodeDepositorModuleId` is a legacy GraphQL field name. It returns the **DistributorCodeDepositor** address. ## Obtain Addresses — GraphQL API ``` POST https://api.paxoslabs.com/graphql Headers: Content-Type: application/json, x-api-key: ``` ### Query ```graphql theme={null} query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } } ``` ### Example cURL ```bash theme={null} curl -s -X POST "https://api.paxoslabs.com/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: $AMPLIFY_API_KEY" \ -d '{ "query": "query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } }", "variables": { "chainId": 1 } }' | jq ``` ### Response Shape ```json theme={null} { "data": { "amplifySdkConfigs": [ { "id": "config-id", "chainId": 1, "yieldType": "CORE", "vault": { "id": "vault-id", "name": "Amplify Core", "chainId": 1, "boringVaultAddress": "0x...", "tellerModuleId": "0x...", "accountantModuleId": "0x...", "withdrawQueueModuleId": "0x...", "communityCodeDepositorModuleId": "0x...", "supportedAssets": [ { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "depositable": true, "withdrawable": true, "symbol": "USDC", "tokenName": "USD Coin", "decimals": 6 } ] } } ] } } ``` Multiple configs can reference the same account. Deduplicate by `vault.id`. `withdrawQueueModuleId` and `communityCodeDepositorModuleId` can be `null` if the account doesn't support those operations. ### GraphQL Variables | Variable | Type | Description | | ----------- | ----------- | --------------------------------------------------------------------------------- | | `chainId` | `Int` | Filter by chain (e.g., `1` for Ethereum, `8453` for Base) | | `yieldType` | `YieldType` | Filter by yield type: `CORE`, `TREASURY`, `FRONTIER`, `PRIME`, `TBILL`, `LENDING` | *** ## ABIs ### DistributorCodeDepositor ```json theme={null} [ { "inputs": [ { "name": "depositAsset", "type": "address" }, { "name": "depositAmount", "type": "uint256" }, { "name": "minimumMint", "type": "uint256" }, { "name": "to", "type": "address" }, { "name": "distributorCode", "type": "bytes" } ], "name": "deposit", "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "depositAsset", "type": "address" }, { "name": "depositAmount", "type": "uint256" }, { "name": "minimumMint", "type": "uint256" }, { "name": "to", "type": "address" }, { "name": "distributorCode", "type": "bytes" }, { "name": "deadline", "type": "uint256" }, { "name": "v", "type": "uint8" }, { "name": "r", "type": "bytes32" }, { "name": "s", "type": "bytes32" } ], "name": "depositWithPermit", "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" } ] ``` ### WithdrawQueue ```json theme={null} [ { "inputs": [ { "components": [ { "name": "amountOffer", "type": "uint256" }, { "name": "wantAsset", "type": "address" }, { "name": "intendedDepositor", "type": "address" }, { "name": "receiver", "type": "address" }, { "name": "refundReceiver", "type": "address" }, { "components": [ { "name": "approvalMethod", "type": "uint8" }, { "name": "approvalV", "type": "uint8" }, { "name": "approvalR", "type": "bytes32" }, { "name": "approvalS", "type": "bytes32" }, { "name": "submitWithSignature", "type": "bool" }, { "name": "deadline", "type": "uint256" }, { "name": "eip2612Signature", "type": "bytes" } ], "name": "signatureParams", "type": "tuple" } ], "name": "params", "type": "tuple" } ], "name": "submitOrder", "outputs": [{ "name": "orderIndex", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "name": "orderIndex", "type": "uint256" }], "name": "cancelOrder", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "orderIndex", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "cancelSignature", "type": "bytes" } ], "name": "cancelOrderWithSignature", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "name": "orderIndex", "type": "uint256" }], "name": "getOrderStatus", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "minimumOrderSize", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "feeModule", "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [ { "name": "owner", "type": "address" }, { "name": "index", "type": "uint256" } ], "name": "tokenOfOwnerByIndex", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "tokenId", "type": "uint256" }], "name": "ownerOf", "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view", "type": "function" } ] ``` ### ERC-20 (BoringVault shares + deposit tokens) ```json theme={null} [ { "inputs": [ { "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "owner", "type": "address" }, { "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "account", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "owner", "type": "address" }], "name": "nonces", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### Accountant ```json theme={null} [ { "inputs": [{ "name": "quote", "type": "address" }], "name": "getRateInQuoteSafe", "outputs": [{ "name": "rateInQuote", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### FeeModule ```json theme={null} [ { "inputs": [ { "name": "amount", "type": "uint256" }, { "name": "offerAsset", "type": "address" }, { "name": "wantAsset", "type": "address" }, { "name": "receiver", "type": "address" } ], "name": "calculateOfferFees", "outputs": [{ "name": "feeAmount", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "offerFeePercentage", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### Teller ```json theme={null} [ { "inputs": [], "name": "isPaused", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" } ] ``` *** ## Pre-Flight: Pause State Check Before submitting any deposit or withdrawal transaction, check whether the account is paused: ```ts theme={null} const paused = await publicClient.readContract({ address: TELLER, abi: tellerAbi, functionName: 'isPaused', }) if (paused) throw new Error('Vault is paused — transactions will revert with TellerIsPaused') ``` *** ## Deposit Flow — Standard (Approve + Deposit) Two transactions: ERC-20 `approve()` then `deposit()`. ```ts theme={null} // Step 1: Check allowance const allowance = await publicClient.readContract({ address: DEPOSIT_TOKEN, abi: erc20Abi, functionName: 'allowance', args: [account.address, DISTRIBUTOR_CODE_DEPOSITOR], }) // Step 2: Approve if needed if (allowance < depositAmount) { // USDT QUIRK: if token is USDT and allowance > 0, reset to 0 first // await walletClient.writeContract({ address: DEPOSIT_TOKEN, abi: erc20Abi, functionName: 'approve', args: [DISTRIBUTOR_CODE_DEPOSITOR, 0n] }) const hash = await walletClient.writeContract({ address: DEPOSIT_TOKEN, abi: erc20Abi, functionName: 'approve', args: [DISTRIBUTOR_CODE_DEPOSITOR, depositAmount], }) await publicClient.waitForTransactionReceipt({ hash }) } // Step 3: Calculate minimumMint (slippage protection) const rate = await publicClient.readContract({ address: ACCOUNTANT, abi: accountantAbi, functionName: 'getRateInQuoteSafe', args: [DEPOSIT_TOKEN], }) const SLIPPAGE_BPS = 50n // 0.5% const expectedShares = (depositAmount * 10n ** 18n) / rate const minimumMint = expectedShares - (expectedShares * SLIPPAGE_BPS) / 10000n // Step 4: Deposit const hash = await walletClient.writeContract({ address: DISTRIBUTOR_CODE_DEPOSITOR, abi: depositorAbi, functionName: 'deposit', args: [DEPOSIT_TOKEN, depositAmount, minimumMint, account.address, '0x'], }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) ``` ### Deposit Parameters | Parameter | Type | Description | | ----------------- | --------- | --------------------------------------------------------------------------- | | `depositAsset` | `address` | ERC-20 token to deposit (e.g., USDC) | | `depositAmount` | `uint256` | Amount in token's smallest unit (USDC 6 decimals: 1000 USDC = `1000000000`) | | `minimumMint` | `uint256` | Min shares to receive (slippage protection). `0` to disable. | | `to` | `address` | Recipient of minted account shares | | `distributorCode` | `bytes` | Referral code as bytes. `0x` if none. | ### Distributor Codes If Paxos Labs provided a distributor code, encode it as bytes: `toHex('your_code')` in JS, `b'your_code'` in Python, `[]byte("your_code")` in Go. Otherwise pass `0x` / `b""` / `[]byte{}`. *** ## Deposit Flow — Permit (Single Transaction) For tokens supporting EIP-2612 (USDC, USDG, pyUSD, USD₮0). Not available for USDT. Not available for smart contract wallets (Privy Smart Wallets, Safe) — they cannot sign typed data. ```ts theme={null} // Step 1: Get permit nonce const nonce = await publicClient.readContract({ address: DEPOSIT_TOKEN, abi: erc20Abi, functionName: 'nonces', args: [account.address], }) // Step 2: Sign EIP-712 permit const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) const signature = await walletClient.signTypedData({ account, domain: { name: 'USD Coin', // varies by token — must match token's EIP-712 domain version: '2', // varies by token chainId: 1, verifyingContract: DEPOSIT_TOKEN, }, types: { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }, primaryType: 'Permit', message: { owner: account.address, spender: DISTRIBUTOR_CODE_DEPOSITOR, value: depositAmount, nonce, deadline, }, }) // Step 3: Parse signature into v, r, s const { v, r, s } = parseSignature(signature) // viem: import { parseSignature } from 'viem' // Step 4: Calculate minimumMint (same as standard flow) const rate = await publicClient.readContract({ address: ACCOUNTANT, abi: accountantAbi, functionName: 'getRateInQuoteSafe', args: [DEPOSIT_TOKEN], }) const expectedShares = (depositAmount * 10n ** 18n) / rate const minimumMint = expectedShares - (expectedShares * 50n) / 10000n // Step 5: depositWithPermit (single transaction — no separate approve needed) const hash = await walletClient.writeContract({ address: DISTRIBUTOR_CODE_DEPOSITOR, abi: depositorAbi, functionName: 'depositWithPermit', args: [DEPOSIT_TOKEN, depositAmount, minimumMint, account.address, '0x', deadline, v, r, s], }) ``` ### Permit EIP-712 Domain (varies by token) | Token | `name` | `version` | | ----- | ------------ | --------- | | USDC | `"USD Coin"` | `"2"` | | USDG | Check token | Check | | pyUSD | Check token | Check | | USD₮0 | Check token | Check | Always read the token's `name()`, `version()` (or `EIP712_VERSION()`) and use those for the domain. *** ## Slippage / minimumMint Formula ``` rate = Accountant.getRateInQuoteSafe(depositTokenAddress) expectedShares = (depositAmount × 1e18) / rate minimumMint = expectedShares × (10000 − SLIPPAGE_BPS) / 10000 ``` Common default: `SLIPPAGE_BPS = 50` (0.5%). Setting `minimumMint = 0` disables slippage protection (fine for testing, not production). *** ## Withdrawal Flow (Complete) Withdrawals are **order-based, not instant**. The account operator fulfills orders (typically within 24 hours). ```ts theme={null} // Step 1: Check share balance const shareBalance = await publicClient.readContract({ address: BORING_VAULT, abi: erc20Abi, functionName: 'balanceOf', args: [account.address], }) // Step 2: Convert want amount to shares const rate = await publicClient.readContract({ address: ACCOUNTANT, abi: accountantAbi, functionName: 'getRateInQuoteSafe', args: [WANT_ASSET], // e.g., USDC address }) const wantAmount = parseUnits('1000', 6) // 1000 USDC let sharesNeeded = (wantAmount * 10n ** 18n) / rate // Step 3: Calculate withdrawal fee const feeModuleAddr = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'feeModule', }) const feeAmount = await publicClient.readContract({ address: feeModuleAddr, abi: feeModuleAbi, functionName: 'calculateOfferFees', args: [sharesNeeded, BORING_VAULT, WANT_ASSET, account.address], }) // Fee is deducted from offered shares. To receive full wantAmount, add fee: sharesNeeded = sharesNeeded + feeAmount // Step 4: Check minimum order size const minimumShares = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'minimumOrderSize', }) if (sharesNeeded < minimumShares) throw new Error('Below minimum order size') // Step 5: Approve vault shares to WithdrawQueue const allowance = await publicClient.readContract({ address: BORING_VAULT, abi: erc20Abi, functionName: 'allowance', args: [account.address, WITHDRAW_QUEUE], }) if (allowance < sharesNeeded) { const hash = await walletClient.writeContract({ address: BORING_VAULT, abi: erc20Abi, functionName: 'approve', args: [WITHDRAW_QUEUE, sharesNeeded], }) await publicClient.waitForTransactionReceipt({ hash }) } // Step 6: Submit withdrawal order const EMPTY_SIG = { approvalMethod: 0, approvalV: 0, approvalR: ('0x' + '00'.repeat(32)) as `0x${string}`, approvalS: ('0x' + '00'.repeat(32)) as `0x${string}`, submitWithSignature: false, deadline: 0n, eip2612Signature: '0x' as `0x${string}`, } const hash = await walletClient.writeContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'submitOrder', args: [ { amountOffer: sharesNeeded, wantAsset: WANT_ASSET, intendedDepositor: account.address, // MUST match msg.sender receiver: account.address, refundReceiver: account.address, signatureParams: EMPTY_SIG, }, ], }) const receipt = await publicClient.waitForTransactionReceipt({ hash }) // Extract orderIndex from receipt logs or return value // Step 7: Poll for completion let status = 1 while (status === 1) { await new Promise((r) => setTimeout(r, 60_000)) status = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'getOrderStatus', args: [orderIndex], }) } // status 2 = COMPLETE, 5 = REFUNDED, 6 = FAILED_TRANSFER_REFUNDED ``` ### submitOrder Parameters | Field | Type | Description | | ------------------- | --------- | ------------------------------------------------------- | | `amountOffer` | `uint256` | Account shares to offer (18 decimals) | | `wantAsset` | `address` | Token to receive (e.g., USDC). Must be `withdrawable`. | | `intendedDepositor` | `address` | **Must match `msg.sender`** | | `receiver` | `address` | Where the want asset is sent when fulfilled | | `refundReceiver` | `address` | Where shares go if order is cancelled | | `signatureParams` | `tuple` | Pass all-zeros struct for standard ERC-20 approval flow | ### Share-to-Asset Conversion ``` rate = Accountant.getRateInQuoteSafe(wantAssetAddress) sharesNeeded = (wantAmount × 1e18) / rate ``` ### Withdrawal Fee Calculation ``` feeModuleAddr = WithdrawQueue.feeModule() feeAmount = FeeModule.calculateOfferFees(shareAmount, boringVaultAddress, wantAssetAddress, receiverAddress) feePercentage = FeeModule.offerFeePercentage() // 18-decimal precision, e.g., 5000000000000000 = 0.5% ``` Fee is subtracted from offered shares. To receive the full want amount, offer `sharesNeeded + feeAmount`. *** ## Cancellation Flow — Direct Only orders with `PENDING` status (value `1`) can be cancelled. The WithdrawQueue is an ERC-721 — each order is an NFT. ```ts theme={null} // Enumerate order NFTs const count = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'balanceOf', args: [account.address], }) for (let i = 0n; i < count; i++) { const orderIndex = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'tokenOfOwnerByIndex', args: [account.address, i], }) const status = await publicClient.readContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'getOrderStatus', args: [orderIndex], }) if (status === 1) { // PENDING — cancel it const hash = await walletClient.writeContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'cancelOrder', args: [orderIndex], }) await publicClient.waitForTransactionReceipt({ hash }) // Shares returned to refundReceiver set during submitOrder } } ``` If you already have the `orderIndex` (from the `OrderSubmitted` event), skip enumeration and call `cancelOrder(orderIndex)` directly after verifying `getOrderStatus(orderIndex) === 1`. *** ## Cancellation Flow — Meta-Transaction (Gasless) A relayer submits the cancellation on behalf of the order owner using an EIP-712 signature. ### EIP-712 Cancel Signature ```ts theme={null} // Order owner signs off-chain const cancelDeadline = BigInt(Math.floor(Date.now() / 1000) + 3600) const signature = await walletClient.signTypedData({ account, domain: { name: 'WithdrawQueue', chainId: 1, verifyingContract: WITHDRAW_QUEUE, }, types: { Cancel: [ { name: 'orderIndex', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, { name: 'queueAddress', type: 'address' }, { name: 'chainId', type: 'uint256' }, ], }, primaryType: 'Cancel', message: { orderIndex, deadline: cancelDeadline, queueAddress: WITHDRAW_QUEUE, chainId: 1n, }, }) // Relayer submits (anyone can call this) await walletClient.writeContract({ address: WITHDRAW_QUEUE, abi: queueAbi, functionName: 'cancelOrderWithSignature', args: [orderIndex, cancelDeadline, signature], }) ``` The `deadline` in the signed message and the function call **must match exactly**. Reverts with `SignatureExpired` if the block timestamp exceeds the deadline. *** ## REST API Endpoints All require `x-api-key: ` header. Base URL: `https://api.paxoslabs.com` ### Account APY ``` GET /v2/amplify/vaultApys?filter=vaultAddress%3D{VAULT_ADDRESS}&orderByTimestamp=desc&pageSize=1 ``` ```json theme={null} { "vaultApys": [{ "vaultAddress": "0x...", "chainId": 1, "apy": "0.0523", "timestamp": "2025-01-15T12:00:00Z" }] } ``` `apy` is a decimal string — multiply by 100 for percentage (e.g., `0.0523` = 5.23%). ### Account TVL ``` GET /v2/amplify/vaultTvls?filter=vaultAddress%3D{VAULT_ADDRESS}&includeCurrent=true&orderByTimestamp=desc&pageSize=1 ``` ```json theme={null} { "vaultTvls": [{ "vaultAddress": "0x...", "chainId": 1, "tvl": "15234567.89", "timestamp": "2025-01-15T12:00:00Z" }] } ``` ### Account Assets (simplified account discovery) ``` GET /v2/amplify/vaultAssets?pageSize=100 ``` ```json theme={null} { "vaultAssets": [{ "vaultAddress": "0x...", "chainId": 1, "assetAddress": "0x...", "depositable": true, "withdrawable": true }], "nextPageToken": null, "tokenMetadata": { "1:0xA0b8...": { "address": "0xA0b8...", "chain_id": "1", "symbol": "USDC", "name": "USD Coin", "decimals": "6" } } } ``` Only returns `vaultAddress` (BoringVault). Use the GraphQL API for full contract addresses. ### Withdrawal Requests ``` GET /v2/amplify/withdrawalRequests?filter=userAddress%3D{USER_ADDRESS}&pageSize=20 ``` ```json theme={null} { "withdrawalRequests": [{ "orderIndex": "42", "userAddress": "0x...", "vaultAddress": "0x...", "chainId": 1, "offerAmount": "1000000000000000000", "wantAsset": "0x...", "status": "PENDING", "createdAt": "2025-01-15T10:30:00Z", "fulfilledAt": null }], "nextPageToken": null } ``` REST status values: `PENDING`, `COMPLETE`, `PENDING_REFUND`, `REFUNDED`. *** ## Order Status Enum (On-Chain) | Value | Status | Cancellable? | | ----- | -------------------------- | ------------ | | 0 | NOT\_FOUND | No | | 1 | PENDING | **Yes** | | 2 | COMPLETE | No | | 3 | COMPLETE\_PRE\_FILLED | No | | 4 | PENDING\_REFUND | No | | 5 | COMPLETE\_REFUNDED | No | | 6 | FAILED\_TRANSFER\_REFUNDED | No | *** ## Token Notes | Token | Decimals | Permit (EIP-2612) | Notes | | ----- | -------- | ----------------- | -------------------------------------------------- | | USDC | 6 | Yes | — | | USDG | 6 | Yes | — | | pyUSD | 6 | Yes | — | | USD₮0 | 6 | Yes | — | | USDT | 6 | **No** | Must reset allowance to 0 before setting new value | For USDT: if current allowance > 0, call `approve(spender, 0)` first, then `approve(spender, amount)`. Smart contract wallets (Privy Smart Wallets, Safe) **cannot sign permits** — use the standard approve + deposit flow. *** ## Common Revert Errors | Error | Cause | Fix | | ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | | `TellerIsPaused` | Account operations paused | Check `Teller.isPaused()` before submitting | | `PermitFailedAndAllowanceTooLow` | Invalid permit sig and no existing ERC-20 approval | Fix EIP-712 domain params or use approve flow | | `ERC20: insufficient allowance` | Spender not approved | Call `approve()` on token first | | `ERC20: transfer amount exceeds balance` | Insufficient token balance | Check `balanceOf()` before transacting | | `AmountBelowMinimum` | Withdrawal order below minimum size | Check `WithdrawQueue.minimumOrderSize()` | | `InvalidDepositor` | `intendedDepositor` ≠ `msg.sender` | Set `intendedDepositor` to the transaction sender | | `AssetNotSupported` | Want asset not enabled for this account | Check `supportedAssets` with `withdrawable: true` | | `OnlyOrderOwnerCanCancel` | Caller doesn't own the order NFT | Only the NFT holder can cancel | | `InvalidOrderType` | Order not in PENDING status | Check `getOrderStatus()` — only status `1` cancellable | | `SignatureExpired` | Meta-tx cancel deadline passed | Re-sign with future deadline | *** ## Supported Chains SDK built-in networks (for parity with `@paxoslabs/amplify-sdk` on-chain reads): | Chain | ID | | -------------- | -------- | | Ethereum | 1 | | Sepolia | 11155111 | | Base | 8453 | | HyperEVM | 999 | | Stable Testnet | 2201 | # Concepts Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/getting-started/concepts Key ideas to understand before integrating Amplify Earn. Before wiring up calldata, align on a few terms used throughout the docs. ## Vaults and share tokens * **Vault** — an on-chain contract that issues an ERC-20 *share token* representing a user's position. The vault address is the share token address. * **Share token** — the ERC-20 a user holds after depositing. Its `decimals()` may differ from the underlying asset's decimals; always read live, never assume. * **Deposit asset** — the ERC-20 a user deposits into the vault. * **Want asset** — the ERC-20 a user receives when withdrawing; can be the same as the deposit asset, or different. ## How the SDK works For **transactions** (`deposit.prepare`, `withdraw.prepare`, `withdraw.cancel`), the response includes ABI-encoded calldata that your app submits via the user's wallet — viem, wagmi, ethers, or anything else that can call `eth_sendTransaction`. For **reads** (`vaults.list`, `vaults.getApys`, `users.getPositions`, …), the response is the parsed JSON payload. The SDK does not sign transactions or hold private keys. ## The deposit flow 1. **Authorize** — call `client.core.authorization.detect({ tokenAddress: depositAsset, ... })`. The response tells you whether the user already has allowance, needs a standard `approve()` transaction, or can use an EIP-2612 permit signature. 2. **Sign or approve** — if `method === 'permit'`, ask the user's wallet to sign the typed data the API returned. If `method === 'approval'`, submit the encoded `approve()` transaction. 3. **Prepare** — call `client.amplify.deposit.prepare(...)`. If you signed a permit in step 2, pass `permitSignature` + `permitDeadline`. 4. **Submit** — send the transaction the prepare response returned. ## The withdrawal flow Withdrawals settle through a `WithdrawQueue` contract. There is no inline permit path: 1. **Authorize** — call `client.core.authorization.detect({ tokenAddress: vaultAddress, ... })` against the share token. If `method !== 'already_approved'`, the user must submit a standard `approve(WithdrawQueue, amount)` transaction first. 2. **Prepare** — call `client.amplify.withdraw.prepare(...)`. The response is calldata for `WithdrawQueue.submitOrder`. 3. **Submit** — send the transaction. 4. (Optional) **Cancel** — `client.amplify.withdraw.cancel(...)` returns calldata for cancelling a pending order. Look up `orderIndex` via `client.amplify.withdraw.listRequests(...)`. ## Amounts and decimals All amount fields in the SDK are **decimal strings in token base units**. For a user input of "10" against a 6-decimal token, send `"10000000"`. For 18 decimals, send `"10000000000000000000"`. Always read `decimals()` live from the relevant token contract. Different vaults and chains use different decimals; hardcoded fallbacks are bugs. ## Errors Every method throws `AmplifyError` with `statusCode?`, `body?`, `message`, and `rawResponse?`. Timeouts surface as `AmplifyTimeoutError`. Use `err instanceof AmplifyError` (or `AmplifyTimeoutError`) to narrow. # Authentication & Response Formats Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/authentication API key setup, base URL, response format options, and error handling for the calldata API ## Base URL ``` https://api.paxoslabs.com ``` All endpoints are versioned under `/v2`. ## Authentication Include your API key in the `x-api-key` header on every request: ```bash theme={null} curl https://api.paxoslabs.com/v2/amplify/vaults \ -H "x-api-key: pxl_your_public_id_your_secret" ``` API keys use the format `pxl__`. Obtain one from the [Paxos Labs dashboard](https://app.paxoslabs.com). ## Response Format The three transaction-preparation endpoints (`deposit`, `withdraw`, `withdraw/cancel`) accept a `responseFormat` query parameter that controls which fields appear in the response: | Format | `data` (hex calldata) | `abi` / `functionName` / `args` | Default | | ------------ | --------------------- | ------------------------------- | ------- | | `encoded` | Yes | No | Yes | | `full` | Yes | Yes | | | `structured` | No | Yes | | ### `encoded` (default) Returns only the ABI-encoded calldata hex string. Use this when your signer accepts raw `data` fields (e.g. `eth_sendTransaction`). ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/deposit?\ vaultAddress=0xbbbb000000000000000000000000000000000001&\ depositAsset=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&\ depositAmount=1000000&\ userAddress=0x1234567890abcdef1234567890abcdef12345678&\ chainId=1" \ -H "x-api-key: pxl_your_key" ``` ```json theme={null} { "transaction": { "to": "0xcccc000000000000000000000000000000000001", "data": "0x47e7ef24000000000000000000000000...", "value": "0" } } ``` ### `full` Returns encoded calldata **plus** the ABI fragment, function name, and decoded args. Useful for debugging or when you need both representations. ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/deposit?\ vaultAddress=0xbbbb...&depositAsset=0xA0b8...&depositAmount=1000000&\ userAddress=0x1234...&chainId=1&responseFormat=full" \ -H "x-api-key: pxl_your_key" ``` ```json theme={null} { "transaction": { "to": "0xcccc000000000000000000000000000000000001", "data": "0x47e7ef24000000000000000000000000...", "value": "0", "abi": [{ "type": "function", "name": "deposit", "inputs": [...], "outputs": [...] }], "functionName": "deposit", "args": ["0xA0b8...", "1000000", "999500", "0x1234..."] } } ``` ### `structured` Returns the ABI fragment, function name, and args **without** encoded calldata. Use this when your library handles encoding (e.g. viem `encodeFunctionData`, ethers `interface.encodeFunctionData`). ```json theme={null} { "transaction": { "to": "0xcccc000000000000000000000000000000000001", "value": "0", "abi": [{ "type": "function", "name": "deposit", "inputs": [...], "outputs": [...] }], "functionName": "deposit", "args": ["0xA0b8...", "1000000", "999500", "0x1234..."] } } ``` ## Error Handling All error responses use a standard envelope: ```json theme={null} { "error": { "code": 400, "message": "vaultAddress is not a valid hex address", "status": "INVALID_ARGUMENT", "details": [ { "@type": "type.paxoslabs.dev/errors/BadRequest", "fieldViolations": [ { "field": "vaultAddress", "description": "vaultAddress 0xinvalid is not a valid hex address." } ] } ] } } ``` | HTTP Status | `error.status` | Meaning | | ----------- | -------------------- | --------------------------------------------- | | 400 | `INVALID_ARGUMENT` | Malformed or missing parameters | | 401 | `UNAUTHENTICATED` | Missing or malformed API key | | 403 | `PERMISSION_DENIED` | Invalid, inactive, or expired API key | | 404 | `NOT_FOUND` | Vault not found for the given address + chain | | 429 | `RESOURCE_EXHAUSTED` | Rate limit exceeded | | 503 | `INTERNAL` | RPC or upstream service unavailable | # Cancellations Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/cancellations Complete cancellation walkthrough using the Paxos Labs REST API calldata endpoints Cancel a pending withdrawal order to recover locked vault shares. Only the original order owner (verified on-chain via `msg.sender`) can cancel. Only orders with status `PENDING` can be cancelled. Orders that have already been processed (`COMPLETE`) or are mid-refund (`PENDING_REFUND`, `REFUNDED`) cannot be cancelled. ## Prerequisites * A Paxos Labs API key * An EVM-compatible signer (the same address that submitted the original withdrawal order) * The `orderIndex` of the pending withdrawal order ## Step 1: Find the Order Index Query `GET /v2/amplify/withdrawalRequests` to find the `orderIndex` of the order you want to cancel: ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/withdrawalRequests?\ filter=userAddress%3D0x1234567890abcdef1234567890abcdef12345678%20AND%20\ vaultAddress%3D0xbbbb000000000000000000000000000000000001%20AND%20\ status%3DPENDING" \ -H "x-api-key: pxl_your_key" ``` Each item in the response includes an `orderIndex` field: ```json theme={null} { "withdrawalRequests": [ { "id": "wr_abc123", "orderIndex": "42", "status": "PENDING", "vaultAddress": "0xbbbb...", "chainId": 1, "userAddress": "0x1234...", "orderAmount": "1000000000000000000", "wantAssetAddress": "0xA0b8..." } ], "nextPageToken": null } ``` ## Step 2: Prepare Cancel Calldata `GET /v2/amplify/withdraw/cancel` returns the transaction to cancel a pending withdrawal order. ### Parameters | Parameter | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------- | | `vaultAddress` | `string` | Yes | BoringVault contract address (`0x` + 40 hex chars) | | `orderIndex` | `string` | Yes | Index of the withdrawal order to cancel (decimal string from `withdrawalRequests` response) | | `chainId` | `number` | Yes | EVM chain ID | | `responseFormat` | `string` | No | `encoded` (default), `full`, or `structured` | ### Response ```json theme={null} { "transaction": { "to": "0xdddd000000000000000000000000000000000001", "data": "0x5c975abb...", "value": "0", "abi": [{"type": "function", "name": "cancelOrder", "inputs": [...]}], "functionName": "cancelOrder", "args": ["42"] } } ``` ## Step 3: Sign and Submit Broadcast the cancellation transaction. The transaction must be sent from the same address that originally submitted the withdrawal order — ownership is enforced on-chain via `msg.sender`. After confirmation, the locked vault shares are returned to the user's wallet. ## Complete Examples ```ts theme={null} import { createWalletClient, createPublicClient, http } 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 CHAIN_ID = 1; 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: Find pending withdrawal orders console.log("Fetching pending withdrawal orders..."); const statusUrl = new URL(`${BASE}/v2/amplify/withdrawalRequests`); statusUrl.searchParams.set( "filter", `userAddress=${account.address} AND vaultAddress=${VAULT_ADDRESS} AND status=PENDING` ); const statusResp = await fetch(statusUrl, { headers: HEADERS }).then( (r) => r.json() ); const orders = statusResp.withdrawalRequests; if (orders.length === 0) { console.log("No pending orders to cancel."); return; } const orderIndex = orders[0].orderIndex; console.log(`Cancelling order index: ${orderIndex}`); // Step 2: Get cancel calldata const cancelUrl = new URL(`${BASE}/v2/amplify/withdraw/cancel`); cancelUrl.searchParams.set("vaultAddress", VAULT_ADDRESS); cancelUrl.searchParams.set("orderIndex", orderIndex); cancelUrl.searchParams.set("chainId", String(CHAIN_ID)); const cancelResp = await fetch(cancelUrl, { headers: HEADERS }).then( (r) => r.json() ); const tx = cancelResp.transaction; // Step 3: Sign and submit console.log("Submitting cancellation..."); 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( `Cancellation confirmed in block ${receipt.blockNumber}: ${hash}` ); } main().catch(console.error); ``` ```python theme={null} 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" CHAIN_ID = 1 w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com")) account = w3.eth.account.from_key(PRIVATE_KEY) # Step 1: Find pending withdrawal orders print("Fetching pending withdrawal orders...") status_resp = requests.get(f"{BASE}/v2/amplify/withdrawalRequests", headers=HEADERS, params={ "filter": f"userAddress={account.address} AND vaultAddress={VAULT_ADDRESS} AND status=PENDING", }).json() orders = status_resp["withdrawalRequests"] if not orders: print("No pending orders to cancel.") exit() order_index = orders[0]["orderIndex"] print(f"Cancelling order index: {order_index}") # Step 2: Get cancel calldata cancel_resp = requests.get(f"{BASE}/v2/amplify/withdraw/cancel", headers=HEADERS, params={ "vaultAddress": VAULT_ADDRESS, "orderIndex": order_index, "chainId": CHAIN_ID, }).json() tx = cancel_resp["transaction"] # Step 3: Sign and submit print("Submitting cancellation...") cancel_tx = { "to": Web3.to_checksum_address(tx["to"]), "data": tx["data"], "value": int(tx["value"]), "gas": 200_000, "nonce": w3.eth.get_transaction_count(account.address), "chainId": CHAIN_ID, } signed = account.sign_transaction(cancel_tx) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Cancellation confirmed in block {receipt['blockNumber']}: {tx_hash.hex()}") ``` ```go theme={null} package main import ( "context" "encoding/json" "fmt" "io" "math/big" "net/http" "net/url" "os" "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" chainID = 1 ) 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: Find pending withdrawal orders fmt.Println("Fetching pending withdrawal orders...") statusResp, _ := apiGet("/v2/amplify/withdrawalRequests", url.Values{ "filter": {fmt.Sprintf("userAddress=%s AND vaultAddress=%s AND status=PENDING", fromAddr.Hex(), vaultAddress)}, }) orders := statusResp["withdrawalRequests"].([]interface{}) if len(orders) == 0 { fmt.Println("No pending orders to cancel.") return } order := orders[0].(map[string]interface{}) orderIndex := order["orderIndex"].(string) fmt.Printf("Cancelling order index: %s\n", orderIndex) // Step 2: Get cancel calldata fmt.Println("Fetching cancel calldata...") cancelResp, _ := apiGet("/v2/amplify/withdraw/cancel", url.Values{ "vaultAddress": {vaultAddress}, "orderIndex": {orderIndex}, "chainId": {fmt.Sprint(chainID)}, }) txData := cancelResp["transaction"].(map[string]interface{}) // Step 3: Sign and submit fmt.Println("Submitting cancellation...") nonce, _ := client.PendingNonceAt(ctx, fromAddr) gasPrice, _ := client.SuggestGasPrice(ctx) cancelTx := types.NewTransaction(nonce, common.HexToAddress(txData["to"].(string)), big.NewInt(0), 200000, gasPrice, common.FromHex(txData["data"].(string))) signedCancel, _ := types.SignTx(cancelTx, signer, pk) client.SendTransaction(ctx, signedCancel) fmt.Printf("Cancellation submitted: %s\n", signedCancel.Hash().Hex()) } ``` ```java theme={null} import org.web3j.crypto.Credentials; import org.web3j.crypto.RawTransaction; import org.web3j.crypto.TransactionEncoder; import org.web3j.protocol.Web3j; import org.web3j.protocol.http.HttpService; 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 com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; public class AmplifyCancelWithdraw { 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 int CHAIN_ID = 1; 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 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: Find pending withdrawal orders System.out.println("Fetching pending withdrawal orders..."); String filter = String.format( "userAddress=%s AND vaultAddress=%s AND status=PENDING", userAddress, VAULT_ADDRESS); String statusPath = "/v2/amplify/withdrawalRequests?filter=" + java.net.URLEncoder.encode(filter, "UTF-8"); JsonObject statusResp = apiGet(statusPath); JsonArray orders = statusResp.getAsJsonArray("withdrawalRequests"); if (orders.size() == 0) { System.out.println("No pending orders to cancel."); return; } String orderIndex = orders.get(0).getAsJsonObject() .get("orderIndex").getAsString(); System.out.println("Cancelling order index: " + orderIndex); // Step 2: Get cancel calldata System.out.println("Fetching cancel calldata..."); String cancelPath = String.format( "/v2/amplify/withdraw/cancel?vaultAddress=%s&orderIndex=%s&chainId=%d", VAULT_ADDRESS, orderIndex, CHAIN_ID); JsonObject cancelResp = apiGet(cancelPath); JsonObject tx = cancelResp.getAsJsonObject("transaction"); // Step 3: Sign and submit System.out.println("Submitting cancellation..."); BigInteger nonce = web3.ethGetTransactionCount(userAddress, org.web3j.protocol.core.DefaultBlockParameterName.PENDING) .send().getTransactionCount(); BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice(); RawTransaction cancelTx = RawTransaction.createTransaction( nonce, gasPrice, BigInteger.valueOf(200000), tx.get("to").getAsString(), BigInteger.ZERO, tx.get("data").getAsString()); byte[] signedCancel = TransactionEncoder.signMessage( cancelTx, CHAIN_ID, credentials); String txHash = web3.ethSendRawTransaction( Numeric.toHexString(signedCancel)).send().getTransactionHash(); System.out.println("Cancellation submitted: " + txHash); } } ``` ## Error Responses | Status | Meaning | | ------ | ------------------------------------------------------------ | | 400 | Invalid parameters (bad address, missing `orderIndex`, etc.) | | 404 | No account found for the given `vaultAddress` + `chainId` | # Deposits Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/deposits Complete deposit integration walkthrough using the Paxos Labs REST API calldata endpoints 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: | Method | Description | | -------------------- | ----------------------------------------------------------------------------------------------- | | **Permit** | Gas-efficient off-chain signature (EIP-2612). Single transaction — no separate approval needed. | | **Approval** | Standard ERC-20 `approve` transaction followed by the deposit transaction. | | **Already Approved** | Direct 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. ```bash theme={null} 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: | Field | Usage | | -------------------- | -------------------------------------------------------------------------------------- | | `boringVaultAddress` | Passed to `GET /v2/core/authorization` and `GET /v2/amplify/deposit` as `vaultAddress` | | `chainId` | Passed to all endpoints | | `baseTokenAddress` | The primary deposit asset address | ## Step 1: Check Authorization `GET /v2/core/authorization` detects whether the deposit token supports EIP-2612 permits, requires a standard ERC-20 approval, or already has sufficient allowance. ### Parameters | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ------------------------------------------------------------------ | | `vaultAddress` | `string` | Yes | BoringVault contract address (`boringVaultAddress` from discovery) | | `tokenAddress` | `string` | Yes | ERC-20 deposit token address | | `amount` | `string` | Yes | Deposit amount in token base units (decimal string) | | `userAddress` | `string` | Yes | Depositor's wallet address | | `chainId` | `number` | Yes | EVM 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. ```json theme={null} { "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. ```json theme={null} { "method": "approval", "approvalTransaction": { "encoded": "0x095ea7b3000000000000000000000000..." } } ``` **`already_approved`** — Sufficient allowance exists. Skip directly to the deposit. ```json theme={null} { "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 | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `vaultAddress` | `string` | Yes | BoringVault contract address (`0x` + 40 hex chars) | | `depositAsset` | `string` | Yes | ERC-20 token address to deposit | | `depositAmount` | `string` | Yes | Amount in token base units (decimal string) | | `userAddress` | `string` | Yes | Wallet address signing and submitting the transaction. Also the default share recipient when `to` is omitted. | | `chainId` | `number` | Yes | EVM chain ID | | `to` | `string` | No | Destination address that receives the vault shares (maps to the on-chain `to` argument on `DistributorCodeDepositor.deposit()`). Defaults to `userAddress` when omitted. | | `permitSignature` | `string` | No | EIP-2612 permit signature (65-byte hex). Required for permit path. | | `permitDeadline` | `number` | No | Permit deadline as Unix timestamp. Required when `permitSignature` is provided. | | `responseFormat` | `string` | No | `encoded` (default), `full`, or `structured` | ### Response ```json theme={null} { "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](/v1.0.0/intro/products/earn/developers/guides/api-calldata/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 ```ts theme={null} 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/authorization`); 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); ``` ```python theme={null} 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" DEPOSIT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" CHAIN_ID = 1 AMOUNT = "1000000" # 1 USDC (6 decimals) w3 = Web3(Web3.HTTPProvider("https://eth.llamarpc.com")) account = w3.eth.account.from_key(PRIVATE_KEY) # Step 1: Check authorization print("Checking authorization method...") permit_resp = requests.get(f"{BASE}/v2/core/authorization", headers=HEADERS, params={ "vaultAddress": VAULT_ADDRESS, "tokenAddress": DEPOSIT_ASSET, "amount": AMOUNT, "userAddress": account.address, "chainId": CHAIN_ID, }).json() print(f"Authorization method: {permit_resp['method']}") # Step 2: Handle authorization deposit_params = { "vaultAddress": VAULT_ADDRESS, "depositAsset": DEPOSIT_ASSET, "depositAmount": AMOUNT, "userAddress": account.address, "chainId": CHAIN_ID, } if permit_resp["method"] == "permit": sig = w3.eth.account.sign_typed_data( PRIVATE_KEY, permit_resp["permitData"]["domain"], permit_resp["permitData"]["types"], permit_resp["permitData"]["value"], ) deposit_params["permitSignature"] = sig.signature.hex() deposit_params["permitDeadline"] = permit_resp["permitData"]["deadline"] elif permit_resp["method"] == "approval": print("Sending approval transaction...") approval_tx = { "to": Web3.to_checksum_address(DEPOSIT_ASSET), "data": permit_resp["approvalTransaction"]["encoded"], "gas": 60_000, "nonce": w3.eth.get_transaction_count(account.address), "chainId": CHAIN_ID, } signed = account.sign_transaction(approval_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 3: Get deposit calldata print("Fetching deposit calldata...") deposit_resp = requests.get( f"{BASE}/v2/amplify/deposit", headers=HEADERS, params=deposit_params ).json() tx = deposit_resp["transaction"] # Step 4: Sign and submit print("Submitting deposit transaction...") deposit_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(deposit_tx) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Deposit confirmed in block {receipt['blockNumber']}: {tx_hash.hex()}") ``` ```go theme={null} package main import ( "context" "encoding/json" "fmt" "io" "math/big" "net/http" "net/url" "os" "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" depositAsset = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" chainID = 1 amount = "1000000" ) type Transaction struct { To string `json:"to"` Data string `json:"data"` Value string `json:"value"` } func apiGet(path string, params url.Values) (map[string]interface{}, error) { apiKey := os.Getenv("AMPLIFY_API_KEY") u := fmt.Sprintf("%s%s?%s", baseURL, path, params.Encode()) req, _ := http.NewRequest("GET", u, nil) req.Header.Set("x-api-key", apiKey) 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() // Step 1: Check authorization fmt.Println("Checking authorization method...") permitResp, _ := apiGet("/v2/core/authorization", url.Values{ "vaultAddress": {vaultAddress}, "tokenAddress": {depositAsset}, "amount": {amount}, "userAddress": {fromAddr.Hex()}, "chainId": {fmt.Sprint(chainID)}, }) method := permitResp["method"].(string) fmt.Printf("Authorization method: %s\n", method) // Step 2: Handle authorization depositParams := url.Values{ "vaultAddress": {vaultAddress}, "depositAsset": {depositAsset}, "depositAmount": {amount}, "userAddress": {fromAddr.Hex()}, "chainId": {fmt.Sprint(chainID)}, } if method == "approval" { fmt.Println("Sending approval transaction...") approvalTx := permitResp["approvalTransaction"].(map[string]interface{}) encoded := approvalTx["encoded"].(string) nonce, _ := client.PendingNonceAt(ctx, fromAddr) gasPrice, _ := client.SuggestGasPrice(ctx) tx := types.NewTransaction( nonce, common.HexToAddress(depositAsset), big.NewInt(0), 60000, gasPrice, common.FromHex(encoded), ) signer := types.NewEIP155Signer(big.NewInt(chainID)) signedTx, _ := types.SignTx(tx, signer, pk) client.SendTransaction(ctx, signedTx) fmt.Printf("Approval submitted: %s\n", signedTx.Hash().Hex()) } // For permit: sign EIP-712 typed data and add to depositParams // Step 3: Get deposit calldata fmt.Println("Fetching deposit calldata...") depositResp, _ := apiGet("/v2/amplify/deposit", depositParams) txData := depositResp["transaction"].(map[string]interface{}) // Step 4: Sign and submit fmt.Println("Submitting deposit transaction...") nonce, _ := client.PendingNonceAt(ctx, fromAddr) gasPrice, _ := client.SuggestGasPrice(ctx) depositTx := types.NewTransaction( nonce, common.HexToAddress(txData["to"].(string)), big.NewInt(0), 300000, gasPrice, common.FromHex(txData["data"].(string)), ) signedDepositTx, _ := types.SignTx( depositTx, types.NewEIP155Signer(big.NewInt(chainID)), pk, ) client.SendTransaction(ctx, signedDepositTx) fmt.Printf("Deposit submitted: %s\n", signedDepositTx.Hash().Hex()) } ``` ```java theme={null} import org.web3j.crypto.Credentials; import org.web3j.crypto.RawTransaction; import org.web3j.crypto.TransactionEncoder; import org.web3j.protocol.Web3j; import org.web3j.protocol.http.HttpService; 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 com.google.gson.JsonObject; import com.google.gson.JsonParser; public class AmplifyDeposit { 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 DEPOSIT_ASSET = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; static final int CHAIN_ID = 1; static final String AMOUNT = "1000000"; 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 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: Check authorization System.out.println("Checking authorization method..."); String permitPath = String.format( "/v2/core/authorization?vaultAddress=%s&tokenAddress=%s&amount=%s&userAddress=%s&chainId=%d", VAULT_ADDRESS, DEPOSIT_ASSET, AMOUNT, userAddress, CHAIN_ID); JsonObject permitResp = apiGet(permitPath); String method = permitResp.get("method").getAsString(); System.out.println("Authorization method: " + method); // Step 2: Handle approval if needed if ("approval".equals(method)) { System.out.println("Sending approval transaction..."); String encoded = permitResp.getAsJsonObject("approvalTransaction") .get("encoded").getAsString(); BigInteger nonce = web3.ethGetTransactionCount( userAddress, org.web3j.protocol.core.DefaultBlockParameterName.PENDING ).send().getTransactionCount(); BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice(); RawTransaction approvalTx = RawTransaction.createTransaction( nonce, gasPrice, BigInteger.valueOf(60000), DEPOSIT_ASSET, BigInteger.ZERO, encoded); byte[] signedApproval = TransactionEncoder.signMessage( approvalTx, CHAIN_ID, credentials); web3.ethSendRawTransaction(Numeric.toHexString(signedApproval)) .send(); System.out.println("Approval submitted"); } // Step 3: Get deposit calldata System.out.println("Fetching deposit calldata..."); String depositPath = String.format( "/v2/amplify/deposit?vaultAddress=%s&depositAsset=%s" + "&depositAmount=%s&userAddress=%s&chainId=%d", VAULT_ADDRESS, DEPOSIT_ASSET, AMOUNT, userAddress, CHAIN_ID); JsonObject depositResp = apiGet(depositPath); JsonObject tx = depositResp.getAsJsonObject("transaction"); // Step 4: Sign and submit System.out.println("Submitting deposit transaction..."); BigInteger nonce = web3.ethGetTransactionCount( userAddress, org.web3j.protocol.core.DefaultBlockParameterName.PENDING ).send().getTransactionCount(); BigInteger gasPrice = web3.ethGasPrice().send().getGasPrice(); RawTransaction depositTx = RawTransaction.createTransaction( nonce, gasPrice, BigInteger.valueOf(300000), tx.get("to").getAsString(), BigInteger.ZERO, tx.get("data").getAsString()); byte[] signedDeposit = TransactionEncoder.signMessage( depositTx, CHAIN_ID, credentials); String txHash = web3.ethSendRawTransaction( Numeric.toHexString(signedDeposit)).send().getTransactionHash(); System.out.println("Deposit submitted: " + txHash); } } ``` ## Error Responses | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------------- | | 400 | Invalid parameters (missing field, bad address format, invalid permit signature, deposit amount below fees, etc.) | | 404 | No account found for the given `vaultAddress` + `chainId` | | 503 | Upstream RPC unavailable (share rate, fee module, or supply cap read failed) | # Account Discovery Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/discovery Find available Amplify accounts, contract addresses, fees, and supported assets via API Before preparing transactions, discover which accounts are available and fetch their contract addresses. `GET /v2/amplify/vaults` is the single entry point — it returns accounts grouped by `name`, with per-chain `deployments[]` and nested per-asset configuration, fees, supply caps, minimum withdrawal sizes, and withdrawal SLAs. ## List Accounts `GET /v2/amplify/vaults` returns accounts grouped by `name`. Each item has a `deployments[]` array with one entry per chain the account is deployed on. ### Parameters | Parameter | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `filter` | `string` | No | Filter string. Flags: `chainId`, `inDeprecation`, `requiresKyt`. Example: `chainId=1 AND inDeprecation=false` | | `pageSize` | `number` | No | Max items per page (1–100, default 25) | | `pageToken` | `string` | No | Pagination token. Omit for the first page. | ### Example ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/vaults?filter=chainId%3D1%20AND%20inDeprecation%3Dfalse" \ -H "x-api-key: pxl_your_key" ``` ```ts theme={null} const BASE = "https://api.paxoslabs.com"; const HEADERS = { "x-api-key": process.env.AMPLIFY_API_KEY! }; const url = new URL(`${BASE}/v2/amplify/vaults`); url.searchParams.set("filter", "chainId=1 AND inDeprecation=false"); const resp = await fetch(url, { headers: HEADERS }); const data = await resp.json(); for (const vault of data.vaults) { console.log(`Account: ${vault.name}`); for (const d of vault.deployments) { console.log(` Chain ${d.chainId}`); console.log(` BoringVault: ${d.boringVaultAddress}`); console.log(` Depositor: ${d.depositorAddress}`); console.log(` WithdrawQueue: ${d.withdrawQueueAddress}`); for (const a of d.assets) { console.log( ` Asset ${a.assetAddress}: deposit=${a.depositable}, withdraw=${a.withdrawable}` ); } } } ``` ```python theme={null} import requests resp = requests.get( "https://api.paxoslabs.com/v2/amplify/vaults", headers={"x-api-key": "pxl_your_key"}, params={"filter": "chainId=1 AND inDeprecation=false"}, ) data = resp.json() for vault in data["vaults"]: print(f"Account: {vault['name']}") for d in vault["deployments"]: print(f" Chain {d['chainId']}") print(f" BoringVault: {d['boringVaultAddress']}") print(f" Depositor: {d['depositorAddress']}") print(f" WithdrawQueue: {d['withdrawQueueAddress']}") for a in d["assets"]: print( f" Asset {a['assetAddress']}: " f"deposit={a['depositable']}, withdraw={a['withdrawable']}" ) ``` ```go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.paxoslabs.com/v2/amplify/vaults?"+ "filter=chainId%3D1%20AND%20inDeprecation%3Dfalse", nil) req.Header.Set("x-api-key", os.Getenv("AMPLIFY_API_KEY")) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var data map[string]interface{} json.Unmarshal(body, &data) // iterate data["vaults"][i].deployments[j] to access contract // addresses, per-asset flags, fees, supply caps, and SLAs fmt.Println(data) } ``` ```java theme={null} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import com.google.gson.JsonObject; import com.google.gson.JsonParser; public class ListVaults { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create( "https://api.paxoslabs.com/v2/amplify/vaults?" + "filter=chainId%3D1%20AND%20inDeprecation%3Dfalse")) .header("x-api-key", System.getenv("AMPLIFY_API_KEY")) .GET().build(); HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); JsonObject data = JsonParser.parseString(resp.body()).getAsJsonObject(); // iterate data.vaults[i].deployments[j] to access contract // addresses, per-asset flags, fees, supply caps, and SLAs } } ``` ### Response ```json theme={null} { "vaults": [ { "name": "Amplify USDC Core", "deployments": [ { "chainId": 1, "boringVaultAddress": "0xbbbb000000000000000000000000000000000001", "depositorAddress": "0xcccc000000000000000000000000000000000001", "withdrawQueueAddress": "0xdddd000000000000000000000000000000000001", "requiresKyt": false, "baseTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "accountantAddress": "0xaaaa000000000000000000000000000000000001", "tellerAddress": "0xeeee000000000000000000000000000000000001", "depositFeeAddress": null, "withdrawFeeAddress": null, "inDeprecation": false, "depositSupplyCap": { "raw": "1000000000000", "formatted": "1000000.0", "decimals": 6, "hasCap": true }, "minimumWithdrawalOrderSize": { "raw": "1000000000000000000", "formatted": "1.0", "decimals": 18 }, "assets": [ { "assetAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "depositable": true, "withdrawable": true, "depositFees": { "bps": 0, "percentage": "0.0000" }, "withdrawFees": { "bps": 25, "percentage": "0.2500" }, "withdrawalSLAs": { "expectedDelay": "86400s", "expiryBuffer": "36000s", "internalWithdrawalQueueDelaySLA": "43200s", "externalWithdrawalQueueDelaySLA": "86400s", "internalAccountantRateUpdateDelaySLA": "43200s", "externalAccountantRateUpdateSLA": "86400s" } } ] } ] } ], "nextPageToken": null } ``` ### Field Mapping for Transaction Preparation | Field | Used For | | ------------------------------------------ | ------------------------------------------------------------------------------- | | `deployments[].boringVaultAddress` | `vaultAddress` parameter in all calldata endpoints and `/v2/core/authorization` | | `deployments[].depositorAddress` | Target of the prepared deposit transaction (`transaction.to`) | | `deployments[].withdrawQueueAddress` | Spender address for the share approval before a withdrawal | | `deployments[].requiresKyt` | If `true`, the account requires a KYT attestation — resolved server-side | | `deployments[].baseTokenAddress` | The primary deposit asset address | | `deployments[].depositSupplyCap` | `{ raw, formatted, decimals, hasCap }`; `hasCap=false` means uncapped | | `deployments[].minimumWithdrawalOrderSize` | Minimum `shareAmount` accepted by `/v2/amplify/withdraw` | | `deployments[].assets[].depositFees` | `{ bps, percentage }` — deposit fee per asset | | `deployments[].assets[].withdrawFees` | `{ bps, percentage }` — withdraw fee per asset | | `deployments[].assets[].withdrawalSLAs` | Protobuf duration strings (e.g. `"86400s"`) for queue / rate-update SLAs | Use this single endpoint to build account selector UIs, fee previews, supply-cap progress bars, and withdrawal time estimates. # Display & Helper Endpoints Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/index Preview withdrawal fees and read account state via the REST API Most read-only data your UI needs to render deposit and withdrawal flows — minimum order sizes, per-asset fees, and SLAs — is returned **inline on the discovery response** (`GET /v2/amplify/vaults`). Three dedicated helpers cover what isn't on the discovery response: previewing the fee for a specific redemption, reading up-to-the-block supply-cap utilization, and reading a user's current position. None of these requires an RPC connection. ## Dedicated Helpers | Endpoint | Description | | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | [`GET /v2/amplify/calculateWithdrawalFee`](/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/withdrawal-fee) | Compute the fee a user would be charged for a specific `offerAmount` + `wantAsset` redemption | | [`GET /v2/amplify/supplyCaps`](/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/supply-caps) | Up-to-the-block `totalSupplyInBase`, `supplyCap`, and `percentageFilled` per chain deployment. `null` cap / percentage means the vault is uncapped | | [`GET /v2/amplify/userPositions`](/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/user-positions) | A user's `shareBalance`, `positionValueInBase`, `baseAssetAddress`, and `exchangeRateToBase` per chain deployment | ## Fields Returned by `/v2/amplify/vaults` The following per-deployment fields are already present on the [discovery response](/v1.0.0/intro/products/earn/developers/guides/api-calldata/discovery) — there are no separate endpoints for them: | Field | Shape | Description | | ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------ | | `depositSupplyCap` | `{ raw, formatted, decimals, hasCap }` | Maximum total deposit. `hasCap=false` means the cap is `maxUint256` (effectively uncapped) | | `minimumWithdrawalOrderSize` | `{ raw, formatted, decimals }` | Minimum `shareAmount` accepted by `/v2/amplify/withdraw` | | `assets[].depositFees` / `assets[].withdrawFees` | `{ bps, percentage }` | Per-asset fees (basis points + human-readable percentage) | | `assets[].withdrawalSLAs` | Protobuf duration strings (e.g. `"86400s"`) | Expected delay, expiry buffer, queue and accountant-rate SLAs | Fetch them once during account discovery and cache them client-side; they change rarely. ## Common Error Responses | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | 400 | Invalid parameters | | 404 | Account or fee module not found for the given `vaultAddress` + `chainId` | | 503 | Upstream service unavailable | # List Supply Caps Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/supply-caps Read up-to-the-block total deposits, supply cap, and percentage filled per chain `GET /v2/amplify/supplyCaps` returns up-to-the-block `totalSupplyInBase`, `supplyCap`, and `percentageFilled` per chain each vault is deployed on. Use it to render deposit-cap utilization (e.g. a progress bar) on a deposit screen, or to soft-block deposits that would exceed the cap before sending them on-chain. `totalSupplyInBase` and `supplyCap` are both denominated in the vault base asset, so they are directly comparable without any unit conversion in the UI. `filter` is a single AIP-160-style query parameter and is optional. Both flags inside the filter are optional: `vaultAddress` (hex) scopes to one vault, `chainId` (number) scopes to one chain. With no filter, every live vault deployment is returned. Uncapped vaults return `supplyCap: null` and `percentageFilled: null`. A vault is uncapped when `supplyCapInBase()` on-chain returns `0` or `2^256 - 1`, or when the deployment predates the supply-cap upgrade (no deposit fee module configured). Render those as "uncapped" in the UI rather than treating `null` as an error. ## Parameters | Parameter | Type | Required | Description | | ----------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filter` | `string` | No | AIP-160-style filter. Optional flags: `vaultAddress` (hex), `chainId` (number). Both support `OR`. With no filter, every live vault deployment is returned. Example: `vaultAddress=0xbbbb...0001 AND chainId=84532`. | | `pageSize` | `number` | No | Maximum items per page. Default: 25. Min: 1, max: 100. | | `pageToken` | `string` | No | Opaque cursor returned as `nextPageToken` on the previous response. Omit for the first page. | ## Example ```bash theme={null} # Every live deployment, no filter curl --get "https://api.paxoslabs.com/v2/amplify/supplyCaps" \ -H "x-api-key: pxl_your_key" # Scoped to one vault and one chain curl --get "https://api.paxoslabs.com/v2/amplify/supplyCaps" \ --data-urlencode "filter=vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532" \ -H "x-api-key: pxl_your_key" ``` ```ts theme={null} const url = new URL("https://api.paxoslabs.com/v2/amplify/supplyCaps"); // Optional: scope to one vault and chain. url.searchParams.set( "filter", "vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532" ); const resp = await fetch(url, { headers: { "x-api-key": process.env.AMPLIFY_API_KEY! }, }); const { supplyCaps } = await resp.json(); for (const entry of supplyCaps) { const pct = entry.percentageFilled === null ? "uncapped" : `${entry.percentageFilled}%`; console.log(`${entry.chainId}: ${entry.totalSupplyInBase} / ${entry.supplyCap ?? "∞"} (${pct})`); } ``` ```python theme={null} import requests resp = requests.get( "https://api.paxoslabs.com/v2/amplify/supplyCaps", headers={"x-api-key": "pxl_your_key"}, # filter is optional — omit to return every live deployment. params={ "filter": ( "vaultAddress=0xbbbb000000000000000000000000000000000001 " "AND chainId=84532" ), }, ) for entry in resp.json()["supplyCaps"]: pct = f'{entry["percentageFilled"]}%' if entry["percentageFilled"] is not None else "uncapped" cap = entry["supplyCap"] or "∞" print(f"{entry['chainId']}: {entry['totalSupplyInBase']} / {cap} ({pct})") ``` ```go theme={null} // filter is optional — omit to return every live deployment. params := url.Values{ "filter": {"vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532"}, } req, _ := http.NewRequest("GET", "https://api.paxoslabs.com/v2/amplify/supplyCaps?"+params.Encode(), nil) req.Header.Set("x-api-key", os.Getenv("AMPLIFY_API_KEY")) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() ``` ## Response ```json theme={null} { "supplyCaps": [ { "vaultAddress": "0xbbbb000000000000000000000000000000000001", "chainId": 84532, "totalSupplyInBase": "1037.937273", "supplyCap": "5000.00", "percentageFilled": "20.7587" } ], "nextPageToken": null } ``` | Field | Type | Description | | -------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `supplyCaps[].vaultAddress` | `string` | BoringVault contract address (lowercased) | | `supplyCaps[].chainId` | `number` | EVM chain ID for this entry | | `supplyCaps[].totalSupplyInBase` | `string` (decimal) | Total deposited value denominated in the vault base asset (computed as `totalSupply * shareToBaseRate`); directly comparable to `supplyCap` | | `supplyCaps[].supplyCap` | `string \| null` (decimal) | Deposit supply cap denominated in the vault base asset, or `null` when the vault is uncapped | | `supplyCaps[].percentageFilled` | `string \| null` | Percentage of supply cap currently filled, formatted with four decimals (e.g. `"20.7587"`). Clamped to `"100.0000"` if over-cap. `null` when the vault is uncapped | | `nextPageToken` | `string \| null` | Cursor for the next page. `null` when there are no more results. | ## Error Responses | Status | Meaning | | ------ | --------------------------------------------------------------------------------------------------------------------------- | | 400 | Invalid `filter` (invalid hex address, unknown flag, non-positive `chainId`, malformed segment) or `pageSize` outside 1–100 | | 503 | Upstream RPC unavailable | # List User Positions Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/user-positions Read a user share balance, position value in the vault base asset, and current exchange rate per chain `GET /v2/amplify/userPositions` returns a user's `shareBalance`, `positionValueInBase`, `baseAssetAddress`, and `exchangeRateToBase` per chain each vault is deployed on. Use it to render a user's holdings in a portfolio or vault detail view without re-implementing the share-to-base math client-side. `positionValueInBase` is computed as `shareBalance * exchangeRateToBase` at the accountant contract's current rate, so the value reflects up-to-the-block on-chain state. `userAddress` is required as a top-level query parameter. `filter` is a single AIP-160-style query parameter and is optional. The flags inside the filter are also optional: `vaultAddress` (hex) scopes to one vault, `chainId` (number) scopes to one chain. With no filter, the user's position is returned for every live vault deployment. Some vaults are deployed as **stand-in** vaults on a chain where the canonical base asset isn't natively available (e.g. soohoUSDGt on Base). In that case the endpoint reports `baseAssetAddress` from the on-chain `accountant.base()` call. The endpoint transparently falls back to `accountant.base()` whenever the DB has no `baseAssetAddress` for that deployment. Each response includes a `tokenMetadata` map keyed by lowercase address, populated for the `vaultAddress` and `baseAssetAddress` returned in `userPositions[]`. Use it to render token symbols/decimals without making a separate metadata call. ## Parameters | Parameter | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `userAddress` | `string` | Yes | User wallet address (hex). Top-level query parameter — not part of the filter. | | `filter` | `string` | No | AIP-160-style filter. Optional flags: `vaultAddress` (hex), `chainId` (number). Both support `OR`. With no filter, every live vault deployment is returned. Example: `vaultAddress=0xbbbb...0001 AND chainId=84532`. | | `pageSize` | `number` | No | Maximum items per page. Default: 25. Min: 1, max: 100. | | `pageToken` | `string` | No | Opaque cursor returned as `nextPageToken` on the previous response. Omit for the first page. | ## Example ```bash theme={null} # Every live deployment for this user curl --get "https://api.paxoslabs.com/v2/amplify/userPositions" \ --data-urlencode "userAddress=0xdEADdE9539A00Bbd9A8494f45EB38aEe89d7C001" \ -H "x-api-key: pxl_your_key" # Scoped to one vault and one chain curl --get "https://api.paxoslabs.com/v2/amplify/userPositions" \ --data-urlencode "userAddress=0xdEADdE9539A00Bbd9A8494f45EB38aEe89d7C001" \ --data-urlencode "filter=vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532" \ -H "x-api-key: pxl_your_key" ``` ```ts theme={null} const url = new URL("https://api.paxoslabs.com/v2/amplify/userPositions"); url.searchParams.set( "userAddress", "0xdEADdE9539A00Bbd9A8494f45EB38aEe89d7C001" ); // Optional: scope to one vault and chain. url.searchParams.set( "filter", "vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532" ); const resp = await fetch(url, { headers: { "x-api-key": process.env.AMPLIFY_API_KEY! }, }); const { userPositions } = await resp.json(); for (const p of userPositions) { console.log( `${p.chainId}: ${p.shareBalance} shares = ${p.positionValueInBase} (rate ${p.exchangeRateToBase})` ); } ``` ```python theme={null} import requests resp = requests.get( "https://api.paxoslabs.com/v2/amplify/userPositions", headers={"x-api-key": "pxl_your_key"}, params={ "userAddress": "0xdEADdE9539A00Bbd9A8494f45EB38aEe89d7C001", # filter is optional — omit to return every live deployment. "filter": ( "vaultAddress=0xbbbb000000000000000000000000000000000001 " "AND chainId=84532" ), }, ) for p in resp.json()["userPositions"]: print( f"{p['chainId']}: {p['shareBalance']} shares = " f"{p['positionValueInBase']} (rate {p['exchangeRateToBase']})" ) ``` ```go theme={null} // filter is optional — omit to return every live deployment. params := url.Values{ "userAddress": {"0xdEADdE9539A00Bbd9A8494f45EB38aEe89d7C001"}, "filter": {"vaultAddress=0xbbbb000000000000000000000000000000000001 AND chainId=84532"}, } req, _ := http.NewRequest("GET", "https://api.paxoslabs.com/v2/amplify/userPositions?"+params.Encode(), nil) req.Header.Set("x-api-key", os.Getenv("AMPLIFY_API_KEY")) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() ``` ## Response ```json theme={null} { "userPositions": [ { "vaultAddress": "0xbbbb000000000000000000000000000000000001", "chainId": 84532, "shareBalance": "0.1", "positionValueInBase": "0.101811", "baseAssetAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "exchangeRateToBase": "1.018116" } ], "nextPageToken": null, "tokenMetadata": { "0xbbbb000000000000000000000000000000000001": { "symbol": "soohoUSDG", "decimals": 6 }, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "symbol": "USDC", "decimals": 6 } } } ``` | Field | Type | Description | | ------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `userPositions[].vaultAddress` | `string` | BoringVault contract address (lowercased) | | `userPositions[].chainId` | `number` | EVM chain ID for this entry | | `userPositions[].shareBalance` | `string` (decimal) | User share-token balance, human-readable (scaled by share decimals) | | `userPositions[].positionValueInBase` | `string` (decimal) | Position value denominated in the vault base asset (`shareBalance * exchangeRateToBase`), human-readable | | `userPositions[].baseAssetAddress` | `string` | Vault base asset contract address (lowercased); falls back to `accountant.base()` for stand-in vaults | | `userPositions[].exchangeRateToBase` | `string` (decimal) | Exchange rate of one share to the base asset, at the accountant contract's current precision | | `nextPageToken` | `string \| null` | Cursor for the next page. `null` when there are no more results. | | `tokenMetadata` | `object` | Map keyed by lowercase address with `{ symbol, decimals, ... }`. Missing metadata is represented as `null`. | ## Error Responses | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | Missing or invalid `userAddress`, invalid `filter` (invalid hex address, unknown flag, non-positive `chainId`, malformed segment), or `pageSize` outside 1–100 | | 503 | Upstream RPC unavailable | # Calculate Withdrawal Fee Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/withdrawal-fee Compute the withdrawal fee a user would be charged for a given offer amount and want asset `GET /v2/amplify/calculateWithdrawalFee` returns the fee a user would be charged for redeeming a specific `offerAmount` of shares for a given `wantAsset`. Use this endpoint to preview fees in a withdrawal confirmation UI before submitting the order via `GET /v2/amplify/withdraw`. The withdrawal endpoint already accounts for fees — you do **not** need to call `calculateWithdrawalFee` before preparing withdrawal calldata. It exists purely for display. ## Parameters | Parameter | Type | Required | Description | | -------------- | -------- | -------- | -------------------------------------------------------------------- | | `offerAmount` | `string` | Yes | Share amount to withdraw in base units (decimal string, 18 decimals) | | `wantAsset` | `string` | Yes | ERC-20 token address the user wants to receive | | `vaultAddress` | `string` | Yes | BoringVault contract address (`0x` + 40 hex chars) | | `chainId` | `number` | Yes | EVM chain ID | ## Example ```bash theme={null} curl "https://api.paxoslabs.com/v2/amplify/calculateWithdrawalFee?\ offerAmount=1000000000000000000&\ wantAsset=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&\ vaultAddress=0xbbbb000000000000000000000000000000000001&\ chainId=1" \ -H "x-api-key: pxl_your_key" ``` ```ts theme={null} const url = new URL( "https://api.paxoslabs.com/v2/amplify/calculateWithdrawalFee" ); url.searchParams.set("offerAmount", "1000000000000000000"); url.searchParams.set("wantAsset", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); url.searchParams.set("vaultAddress", "0xbbbb000000000000000000000000000000000001"); url.searchParams.set("chainId", "1"); const resp = await fetch(url, { headers: { "x-api-key": process.env.AMPLIFY_API_KEY! }, }); const data = await resp.json(); console.log( `Fee: ${data.feeAmount} (${data.offerFeePercentage.percentage}%), flat: ${data.flatFee}` ); ``` ```python theme={null} import requests resp = requests.get( "https://api.paxoslabs.com/v2/amplify/calculateWithdrawalFee", headers={"x-api-key": "pxl_your_key"}, params={ "offerAmount": "1000000000000000000", "wantAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "vaultAddress": "0xbbbb000000000000000000000000000000000001", "chainId": 1, }, ) data = resp.json() print( f"Fee: {data['feeAmount']} " f"({data['offerFeePercentage']['percentage']}%), " f"flat: {data['flatFee']}" ) ``` ```go theme={null} params := url.Values{ "offerAmount": {"1000000000000000000"}, "wantAsset": {"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "vaultAddress": {"0xbbbb000000000000000000000000000000000001"}, "chainId": {"1"}, } req, _ := http.NewRequest("GET", "https://api.paxoslabs.com/v2/amplify/calculateWithdrawalFee?"+params.Encode(), nil) req.Header.Set("x-api-key", os.Getenv("AMPLIFY_API_KEY")) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() ``` ```java theme={null} String feeUrl = String.format( "https://api.paxoslabs.com/v2/amplify/calculateWithdrawalFee?" + "offerAmount=%s&wantAsset=%s&vaultAddress=%s&chainId=%d", "1000000000000000000", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xbbbb000000000000000000000000000000000001", 1); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(feeUrl)) .header("x-api-key", System.getenv("AMPLIFY_API_KEY")) .GET().build(); HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); JsonObject data = JsonParser.parseString(resp.body()).getAsJsonObject(); ``` ## Response ```json theme={null} { "feeAmount": "0.05", "offerFeePercentage": { "bps": 25, "percentage": "0.2500" }, "flatFee": "1.05" } ``` | Field | Type | Description | | ------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------- | | `feeAmount` | `string` (decimal) | Total fee for this `offerAmount` + `wantAsset`, human-readable (scaled by the want-asset decimals) | | `offerFeePercentage.bps` | `number` | Fee percentage in basis points (`25` = 0.25%) | | `offerFeePercentage.percentage` | `string` | Human-readable percentage with 4 decimals (e.g. `"0.2500"`) | | `flatFee` | `string` (decimal) | Flat per-withdrawal fee, denominated in the want asset (human-readable units) | ## Error Responses | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | 400 | Invalid parameters (bad address, non-numeric `offerAmount`, etc.) | | 404 | No fee module configured for the given `vaultAddress` + `wantAsset` pair | | 503 | Upstream service unavailable | # API Calldata Integration Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/index Use REST endpoints to get ready-to-sign transaction calldata for deposits, withdrawals, and cancellations The API Calldata path lets you prepare Amplify transactions with plain HTTP requests. The server handles ABI encoding, address resolution, and contract selection — you get back a transaction object ready to sign and broadcast from any language or platform. This path is ideal when you want server-side calldata generation without managing ABIs or contract addresses locally. For a TypeScript SDK with built-in wallet integrations, see the [SDK path](/v1.0.0/intro/products/earn/developers/guides/index). For full control over contract calls, see [Direct Contract Integration](/v1.0.0/intro/products/earn/developers/guides/direct-contract/index). **Using an AI coding assistant?** Paste the [Calldata AI Reference](/v1.0.0/intro/products/earn/developers/api-calldata-ai-reference) into Cursor, Copilot, or Claude for a condensed single-page reference. Skill-aware tools (Claude Code, Cursor, Windsurf) auto-discover the `amplify-earn-api-calldata` Agent Skill at [`/.well-known/agent-skills/index.json`](https://docs.paxoslabs.com/.well-known/agent-skills/index.json). ## When to Use This Path * **Server-side transaction builders** — construct deposit/withdraw transactions from a backend in any language * **Custodial wallets and multisig services** — fetch calldata via API, then route to your signing infrastructure * **Non-JS stacks** — Python, Go, Rust, or any HTTP client; no npm packages required * **Prototyping** — test the full deposit/withdraw flow with `curl` before writing application code ## Prerequisites * A Paxos Labs API key (format: `pxl__`) * An EVM wallet or signer capable of submitting raw transactions * An HTTP client (`curl`, `fetch`, `requests`, etc.) ## Endpoints at a Glance ### Transaction Preparation | Endpoint | Description | | --------------------------------- | ------------------------------------------------------------------ | | `GET /v2/core/authorization` | Detect authorization method (permit / approval / already approved) | | `GET /v2/amplify/deposit` | Prepare deposit calldata | | `GET /v2/amplify/withdraw` | Prepare withdrawal order calldata | | `GET /v2/amplify/withdraw/cancel` | Prepare cancel-withdrawal calldata | ### Discovery | Endpoint | Description | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `GET /v2/amplify/vaults` | Accounts grouped by name with per-chain deployments, contract addresses, per-asset deposit/withdraw flags, fees, SLAs, and supply caps | ### Display Helpers | Endpoint | Description | | ---------------------------------------- | --------------------------------------------------------------------------------------- | | `GET /v2/amplify/calculateWithdrawalFee` | Withdrawal fee preview | | `GET /v2/amplify/supplyCaps` | Supply cap utilization (`totalSupplyInBase`, `supplyCap`, `percentageFilled`) per chain | | `GET /v2/amplify/userPositions` | User share balance and position value in the vault base asset per chain | ## Getting Started API key usage, base URL, response format options, and error handling. Find available accounts and their contract addresses, fees, and supported assets. End-to-end deposit flow: authorize, prepare calldata, sign and submit. Prepare and submit withdrawal orders via API. Cancel pending withdrawal orders. Read deposit caps, supply, fees, and share values via API. ## Learning Path 1. **[Authentication](/v1.0.0/intro/products/earn/developers/guides/api-calldata/authentication)** — Set up your API key and understand response formats 2. **[Discovery](/v1.0.0/intro/products/earn/developers/guides/api-calldata/discovery)** — Find the right account and its contract addresses 3. **[Deposits](/v1.0.0/intro/products/earn/developers/guides/api-calldata/deposits)** — Execute your first deposit 4. **[Withdrawals](/v1.0.0/intro/products/earn/developers/guides/api-calldata/withdrawals)** — Submit withdrawal orders 5. **[Cancellations](/v1.0.0/intro/products/earn/developers/guides/api-calldata/cancellations)** — Cancel pending withdrawals 6. **[Display Helpers](/v1.0.0/intro/products/earn/developers/guides/api-calldata/display-helpers/index)** — Read account state for your UI # Withdrawals Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/api-calldata/withdrawals Complete withdrawal integration walkthrough using the Paxos Labs REST API calldata endpoints This guide walks through the complete withdrawal flow: discover the account, approve share spending, prepare the withdrawal order calldata, submit, and monitor status. Queue-routed 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`. Callers eligible for an **atomic withdrawal** (see Step 2) settle **synchronously** instead — for them the approval and monitoring steps below do not apply. ## 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). ```bash theme={null} 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 (queued withdrawals only) Before submitting a withdrawal order, the `WithdrawQueue` contract must be approved to spend the user's vault shares. This step does not apply to atomic withdrawals — they involve no share allowance. To learn the mode before approving, run Step 2 first with `responseFormat=full`: atomic withdrawals return `functionName: 'bulkWithdraw'` (the approval only needs to confirm before the withdrawal is submitted). 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 | 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 selects the withdrawal mode automatically for each caller and account — you do not pass an `atomic` flag: 1. **Atomic withdrawal** — for eligible callers, shares are redeemed and the `wantAsset` paid out in a single transaction. No share approval and no order to track. 2. **Queued withdrawal order** — all other callers submit an order to the WithdrawQueue. Depending on the account's configuration, the order settles in the same transaction or asynchronously once the queue is processed. Atomic withdrawals are identified by `functionName: 'bulkWithdraw'` in `responseFormat=full` or `structured` responses (their transaction's `to` is not the WithdrawQueue). The Step 1 share approval is not required, and no order appears in `GET /v2/amplify/withdrawalRequests` — settlement is complete when the transaction confirms. The calldata includes a server-computed minimum-output floor at the current exchange rate. ### Response ```json theme={null} { "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. For queued withdrawal orders, the vault shares are locked in the WithdrawQueue upon confirmation. For atomic withdrawals, confirmation **is** settlement — the `wantAsset` is paid out in this transaction. ## Step 4: Monitor Status (queued withdrawals only) An atomic withdrawal creates no order, so it never appears in `withdrawalRequests` (and there is nothing to cancel) — treat transaction confirmation as completion. For queued orders, poll `GET /v2/amplify/withdrawalRequests` to track order progress. Omit the `status` predicate so the order remains visible as it transitions through terminal states: ```bash theme={null} 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 | 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 These examples show the queued path end-to-end. Callers routed to an atomic withdrawal skip the approval and polling sections — see the note in Step 2. ```ts theme={null} 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); ``` ```python theme={null} 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)}") ``` ```go theme={null} 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()) } ``` ```java theme={null} 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 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() {})); 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` | # Deposits Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/deposits End-to-end deposit flow using AmplifyClient — authorize, sign or approve, then submit. A deposit moves an ERC-20 asset from the user's wallet into a BoringVault and mints share tokens back. The flow is the same regardless of vault: 1. Call `client.core.authorization.detect(...)` to discover whether the deposit asset needs a permit signature, a separate `approve` transaction, or already has sufficient allowance. 2. Handle the three response shapes (`permit` / `approval` / `already_approved`). 3. Call `client.amplify.deposit.prepare(...)` to get ABI-encoded calldata. 4. Submit the returned transaction with the user's wallet. Throughout this guide, `client` refers to a singleton `AmplifyClient` created on the server — see [Project setup](./project-setup) for the wiring. All token amounts are base-units decimal strings; the SDK does no decimal parsing. See the [AI Coding Reference](../sdk-ai-reference) for the full parameter list on every method shown here. ## Step 1: Decide on permit vs. approval ```ts theme={null} import type { Amplify } from '@paxoslabs/amplify-sdk' const auth = await client.core.authorization.detect({ vaultAddress: '0xbbbb000000000000000000000000000000000001', tokenAddress: depositAsset, // the ERC-20 you're depositing (e.g. USDC) amount: depositAmount, // base-units decimal string userAddress, chainId: 1, }) ``` The response is a discriminated union on `method`: ```ts theme={null} type AuthorizationResponseDto = | { method: 'permit'; permitData: Amplify.PermitTypedDataResponseDto } | { method: 'approval'; approvalTransaction: Amplify.ApprovalTransactionDto } | { method: 'already_approved' } ``` | `method` | What to do | | ------------------ | ------------------------------------------------------------------------------------------------------ | | `permit` | Sign `auth.permitData` off-chain (gasless) and pass `permitSignature` + `permitDeadline` to `prepare`. | | `approval` | Submit `auth.approvalTransaction` first, wait for it to confirm, then call `prepare`. | | `already_approved` | Sufficient allowance already exists. Go straight to `prepare`. | ```ts theme={null} import type { Address, Hex } from 'viem' let permitSignature: Hex | undefined let permitDeadline: number | undefined if (auth.method === 'permit') { // see Step 2a } else if (auth.method === 'approval') { // see Step 2b } else if (auth.method === 'already_approved') { // skip to Step 3 } ``` For permit-supporting tokens (USDC mainnet, DAI, most modern ERC-20s with EIP-2612), sign the typed data with viem and forward the signature to the backend. The `primaryType` is always `'Permit'`. ```ts theme={null} import { createWalletClient, custom } from 'viem' import { mainnet } from 'viem/chains' if (auth.method === 'permit') { const walletClient = createWalletClient({ account: userAddress, chain: mainnet, transport: custom(window.ethereum), }) permitSignature = await walletClient.signTypedData({ account: userAddress, domain: auth.permitData.domain, types: auth.permitData.types, primaryType: 'Permit', message: auth.permitData.value, }) permitDeadline = Number(auth.permitData.deadline) } ``` With wagmi: ```ts theme={null} import { useSignTypedData } from 'wagmi' const { signTypedDataAsync } = useSignTypedData() if (auth.method === 'permit') { permitSignature = await signTypedDataAsync({ domain: auth.permitData.domain, types: auth.permitData.types, primaryType: 'Permit', message: auth.permitData.value, }) permitDeadline = Number(auth.permitData.deadline) } ``` When the token does not support permit (or you want to force a standard ERC-20 flow), the backend returns a ready-to-submit `approvalTransaction` containing ABI-encoded `approve()` calldata. Submit it to the deposit asset address and wait for the receipt before calling `prepare`. ```ts theme={null} import { useSendTransaction, usePublicClient } from 'wagmi' const { sendTransactionAsync } = useSendTransaction() const publicClient = usePublicClient() if (auth.method === 'approval') { const approvalHash = await sendTransactionAsync({ to: depositAsset, // the token being approved data: auth.approvalTransaction.encoded as Hex, chainId, }) await publicClient!.waitForTransactionReceipt({ hash: approvalHash }) } ``` Always wait for the approval receipt before submitting the deposit. If the deposit lands before the approval is mined, the deposit transaction will revert with an `ERC20: insufficient allowance` style error. ```ts theme={null} if (auth.method === 'already_approved') { // Nothing to do — fall through to Step 3. } ``` ```ts theme={null} const prepared = await client.amplify.deposit.prepare({ vaultAddress: '0xbbbb000000000000000000000000000000000001', depositAsset, // e.g. '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC depositAmount, // base-units decimal string userAddress, // Optional: send shares to a different address than the signer // to: receiverAddress, chainId: 1, // Permit fields — pass BOTH or NEITHER ...(permitSignature && permitDeadline ? { permitSignature, permitDeadline } : {}), }) // prepared.transaction = { to, data, value, ... } ``` Required fields: * `vaultAddress` — BoringVault contract address. * `depositAsset` — ERC-20 token address you're depositing. * `depositAmount` — amount in base units (decimal string). * `userAddress` — wallet that signs and submits the deposit. Also the default share recipient when `to` is omitted. * `chainId` — EVM chain ID. Optional fields: * `to` — destination address that receives the vault shares. Defaults to `userAddress`. * `permitSignature` + `permitDeadline` — required together when you came through the permit branch in Step 2a. * `responseFormat` — `'encoded'` (default), `'full'`, or `'structured'`. Pass `'full'` to also receive `abi`, `functionName`, and `args`. ```ts theme={null} const tx = prepared.transaction const depositHash = await sendTransactionAsync({ to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), chainId, }) await publicClient!.waitForTransactionReceipt({ hash: depositHash }) ``` `tx.value` is a decimal string (usually `"0"` for ERC-20 deposits); cast to `BigInt` before passing to viem/wagmi. ## End-to-end example ```ts theme={null} 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!, }) async function deposit({ userAddress, depositAsset, userInput, chainId, }: { userAddress: Address depositAsset: Address userInput: string // e.g. "10" for 10 USDC chainId: number }) { const publicClient = createPublicClient({ chain: mainnet, transport: http() }) const walletClient = createWalletClient({ account: userAddress, chain: mainnet, transport: custom(window.ethereum), }) // 1. Discover a vault that accepts the deposit asset const { vaults } = await client.amplify.vaults.list({ filter: `chainId=${chainId}`, }) const vault = vaults .flatMap((v) => v.deployments) .find((d) => d.assets.some( (a) => a.assetAddress.toLowerCase() === depositAsset.toLowerCase() && a.depositable, ), ) if (!vault) { throw new Error(`No vault on chain ${chainId} accepts ${depositAsset}`) } // 2. Read decimals from the deposit asset const decimals = await publicClient.readContract({ address: depositAsset, abi: [{ type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }], functionName: 'decimals', }) const depositAmount = parseUnits(userInput, decimals).toString() // 3. Authorize const auth = await client.core.authorization.detect({ vaultAddress: vault.boringVaultAddress as Address, tokenAddress: depositAsset, amount: depositAmount, userAddress, chainId, }) let permitSignature: Hex | undefined let permitDeadline: number | undefined // 4. Handle the three branches if (auth.method === 'permit') { permitSignature = await walletClient.signTypedData({ account: 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 hash = await walletClient.sendTransaction({ to: depositAsset, data: auth.approvalTransaction.encoded as Hex, }) await publicClient.waitForTransactionReceipt({ hash }) } // 5. Prepare const prepared = await client.amplify.deposit.prepare({ vaultAddress: vault.boringVaultAddress as Address, depositAsset, depositAmount, userAddress, chainId, ...(permitSignature && permitDeadline ? { permitSignature, permitDeadline } : {}), }) // 6. Submit const tx = prepared.transaction const depositHash = await walletClient.sendTransaction({ to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), }) await publicClient.waitForTransactionReceipt({ hash: depositHash }) return depositHash } ``` ## Converting user input to base units ```ts theme={null} import { parseUnits } from 'viem' // Read decimals live from the contract (do not hardcode). const decimals = await publicClient.readContract({ address: depositAsset, abi: [{ type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }], functionName: 'decimals', }) const depositAmount = parseUnits(userInput, decimals).toString() ``` ## Error handling ```ts theme={null} import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { await client.amplify.deposit.prepare({ /* ... */ }) } catch (err) { if (err instanceof AmplifyTimeoutError) { // Retry, surface a timeout UI, etc. } else if (err instanceof AmplifyError) { // err.statusCode number — HTTP status (e.g. 400, 401, 500) // err.body unknown — parsed backend error body (when JSON) // err.message string — human-readable summary // err.rawResponse RawResponse — the original Response console.error('Amplify error', err.statusCode, err.message) } else { throw err } } ``` When surfacing errors to the browser, log `err.body` and `err.rawResponse` server-side and return a generic message to the client. ## Next steps * [Withdrawals guide](./withdrawals) — redeem shares for the underlying asset. * [AI Coding Reference](../sdk-ai-reference) — all parameters and response shapes. * [Migrating from 0.5.x](../migrating-from-0-5) — field-by-field rename table. # Direct Withdrawal Cancellations Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations Cancel pending withdrawal orders by calling the WithdrawQueue contract directly This guide walks you through cancelling a pending [withdrawal order](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) by calling `cancelOrder()` on the **WithdrawQueue** contract directly. It covers both direct cancellation (you submit the transaction) and meta-transaction cancellation (a relayer submits on your behalf). ## How Cancellations Work When you submitted a withdrawal order, the WithdrawQueue locked your account shares and minted an **ERC-721 NFT** to your address representing ownership of the order. To cancel: Enumerate your order NFTs using the ERC-721 enumerable interface, then filter by `PENDING` status. Call `cancelOrder(orderIndex)`. The contract verifies you own the order NFT, then returns your account shares to the `refundReceiver` address. There is no approval step — the only requirement is that `msg.sender` holds the NFT for the given order. Only orders with `PENDING` status (value `1`) can be cancelled. Once an order has been fulfilled or is being processed, it cannot be cancelled. *** ## What You'll Need | Requirement | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Contract address | `withdrawQueueAddress` — see [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) | | ABI | Provided below | | RPC endpoint | An Ethereum node URL | | Private key or wallet | The same wallet that submitted the withdrawal order | | Order index | From the `OrderSubmitted` event or NFT enumeration (shown below) | *** ## ABI Reference ```json theme={null} [ { "inputs": [{ "name": "orderIndex", "type": "uint256" }], "name": "cancelOrder", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "orderIndex", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "cancelSignature", "type": "bytes" } ], "name": "cancelOrderWithSignature", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "name": "orderIndex", "type": "uint256" }], "name": "getOrderStatus", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "tokenId", "type": "uint256" }], "name": "ownerOf", "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [ { "name": "owner", "type": "address" }, { "name": "index", "type": "uint256" } ], "name": "tokenOfOwnerByIndex", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` *** ## Contract Method Reference ### `cancelOrder(orderIndex)` | Parameter | Type | Description | | ------------ | --------- | ---------------------------------------------------------------------------------------------------- | | `orderIndex` | `uint256` | Index of the order to cancel. Get this from the `OrderSubmitted` event or by enumerating order NFTs. | ### `cancelOrderWithSignature(orderIndex, deadline, cancelSignature)` For meta-transaction support — a relayer can cancel on behalf of the order owner. | Parameter | Type | Description | | ----------------- | --------- | ------------------------------------------------------------------------------------------------------------------ | | `orderIndex` | `uint256` | Index of the order to cancel | | `deadline` | `uint256` | Unix timestamp after which the signature expires | | `cancelSignature` | `bytes` | EIP-712 signature from the order owner (see [Meta-Transaction Cancellation](#meta-transaction-cancellation) below) | ### ERC-721 Enumeration (finding your orders) The WithdrawQueue is an ERC-721 contract. Each withdrawal order is represented by an NFT. | Method | Returns | Description | | ----------------------------------- | --------- | --------------------------------------------------- | | `balanceOf(owner)` | `uint256` | Number of order NFTs held by the address | | `tokenOfOwnerByIndex(owner, index)` | `uint256` | The order index (token ID) at the given position | | `ownerOf(tokenId)` | `address` | The current owner of a specific order NFT | | `getOrderStatus(orderIndex)` | `uint8` | The order's current status (see status table below) | ### Order Statuses | Value | Status | Cancellable? | | ----- | -------------------------- | ------------ | | `0` | NOT\_FOUND | No | | `1` | PENDING | **Yes** | | `2` | COMPLETE | No | | `3` | COMPLETE\_PRE\_FILLED | No | | `4` | PENDING\_REFUND | No | | `5` | COMPLETE\_REFUNDED | No | | `6` | FAILED\_TRANSFER\_REFUNDED | No | *** ## Direct Cancellation Walkthrough Get the number of order NFTs held by your address. ``` WithdrawQueue.balanceOf(yourAddress) → uint256 orderCount ``` If `orderCount` is 0, you have no orders to cancel. Loop through your NFTs and check each order's status. For each `i` from `0` to `orderCount - 1`: ``` WithdrawQueue.tokenOfOwnerByIndex(yourAddress, i) → uint256 orderIndex WithdrawQueue.getOrderStatus(orderIndex) → uint8 status ``` Only orders with `status == 1` (PENDING) can be cancelled. Call `cancelOrder()` for each pending order you want to cancel. ``` WithdrawQueue.cancelOrder(orderIndex) ``` The contract: 1. Verifies that `msg.sender` owns the order NFT 2. Returns the locked account shares to the `refundReceiver` address (set during order submission) 3. Burns the order NFT Wait for the transaction to be mined. Your account shares are now unlocked and available. Confirm the order status changed. ``` WithdrawQueue.getOrderStatus(orderIndex) → uint8 status ``` Expected: `5` (COMPLETE\_REFUNDED) If you already know the `orderIndex` (e.g., you saved it from the `OrderSubmitted` event when you submitted the withdrawal), you can skip Steps 1–2 and go directly to cancellation. Just verify the order is still `PENDING` first. *** ## Meta-Transaction Cancellation For relayer or gasless cancellation, the order owner signs an EIP-712 message off-chain and anyone can submit the transaction. ### EIP-712 Type ``` Cancel(uint256 orderIndex, uint256 deadline, address queueAddress, uint256 chainId) ``` ### Walkthrough **EIP-712 Domain**: | Field | Value | | ------------------- | --------------------- | | `name` | `"WithdrawQueue"` | | `chainId` | Chain ID (e.g., `1`) | | `verifyingContract` | WithdrawQueue address | **Cancel message**: | Field | Value | | -------------- | ----------------------------------- | | `orderIndex` | The order to cancel | | `deadline` | Unix timestamp (e.g., now + 1 hour) | | `queueAddress` | WithdrawQueue address | | `chainId` | Chain ID (e.g., `1`) | The order owner signs the EIP-712 typed data using their wallet's `signTypedData` method. This produces a 65-byte signature. Anyone can submit the cancellation using the owner's signature: ``` WithdrawQueue.cancelOrderWithSignature( orderIndex, // the order to cancel deadline, // from the signed message cancelSignature // 65-byte EIP-712 signature ) ``` The contract verifies the signature came from the order NFT owner and that the deadline hasn't passed. The `deadline` in the signed message and the `deadline` passed to `cancelOrderWithSignature()` **must match exactly**. If the current block timestamp exceeds the deadline, the transaction will revert with `SignatureExpired`. *** ## Troubleshooting Your wallet doesn't own this order's NFT. Only the original submitter (or whoever holds the NFT) can cancel. The order isn't in `PENDING` status. Check `getOrderStatus()` — only status `1` is cancellable. The order index doesn't exist. Verify it from the `OrderSubmitted` event or NFT enumeration. The deadline in `cancelOrderWithSignature` has passed. Generate a fresh signature with a future deadline. *** ## Next Steps * [Direct Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) — Deposit tokens into an account * [Direct Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) — Submit a withdrawal order * [Account Queries & Monitoring](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/index) — Read APY, TVL, check balances, and monitor withdrawal status * [SDK Withdrawals Guide](/v1.0.0/intro/products/earn/developers/guides/withdrawals) — Cancel via the Amplify SDK instead # Concepts Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/concepts Key ideas to understand before integrating with Amplify smart contracts directly If you're new to on-chain integrations, read through these before diving into the [guides](/v1.0.0/intro/products/earn/developers/guides/direct-contract/index). ## Accounts & Yield Types * **Account** — An on-chain contract that issues ERC-20 shares representing a position in an underlying strategy. * **Yield Type** — Logical grouping of accounts with similar mandates (`CORE`, `TREASURY`, `FRONTIER`). Used as a filter when [discovering accounts](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) via the GraphQL API. * **Want Asset** — The token users receive when withdrawing from an account; often different from the deposit token. ### Account Discovery All contract addresses are available via the GraphQL API at `POST https://api.paxoslabs.com/graphql`. See [Setup & Prerequisites](/v1.0.0/intro/products/earn/developers/guides/direct-contract/setup#obtaining-contract-addresses) for the full query and response format. ## What is a Smart Contract? A smart contract is a program deployed on the Ethereum blockchain. Once deployed, anyone can call its functions by sending a **transaction** (for writes) or making a **call** (for reads). You interact with contracts using their **address** (like a URL) and their **ABI** (like an API schema). ## What is an ABI? An **Application Binary Interface (ABI)** is a JSON description of a contract's functions — their names, parameter types, and return types. Think of it as an OpenAPI/Swagger spec for a smart contract. Your code uses the ABI to encode function calls into the binary format the blockchain expects. ## What is a Token Approval? ERC-20 tokens (like USDC) don't allow contracts to spend your tokens by default. Before a contract can move tokens on your behalf, you must **approve** it by calling the token's `approve(spender, amount)` function. This is a safety mechanism — you explicitly opt in to each contract interaction. ## What is an EIP-2612 Permit? A **permit** is a gasless alternative to `approve` defined by [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612). Instead of sending an on-chain approval transaction, you sign an off-chain message that authorizes a contract to spend your tokens. The contract verifies this signature on-chain. Not all tokens support permits — USDT notably does not. ## Shares vs. Assets * [Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) mint **account shares** to the recipient. Shares are ERC-20 tokens representing your proportional ownership of the account's assets. * [Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) burn shares and return the **want asset** (e.g., USDC). The want asset may differ from the token you deposited. * The exchange rate between shares and the underlying tokens changes over time as the account earns yield. * All on-chain values are in **base units** (e.g., 1 USDC = `1000000` with 6 decimals). ## Slippage Controls When depositing, the `minimumMint` parameter protects you from receiving fewer shares than expected due to exchange rate changes between when you prepare and when your transaction is mined. This is expressed as a minimum number of shares to receive — if the account would mint fewer, the transaction reverts. A common default is 0.5% (50 basis points) below the expected share amount. ## Withdrawals * Withdrawals are **order-based**, not instant. You submit an order to the [WithdrawQueue](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) and the account operator fulfills it (typically within 24 hours). * You must **approve** your account shares to the WithdrawQueue before submitting an order. * Once fulfilled, the want asset is sent directly to the address that submitted the order. * Pending orders can be [cancelled](/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations) to recover your locked account shares. ## Execution Responsibilities When integrating directly with smart contracts, your application is responsible for: * **Building calldata** — ABI-encoding function calls with the correct parameters * **Submitting transactions** — Signing and broadcasting via your [chosen Ethereum library](/v1.0.0/intro/products/earn/developers/guides/direct-contract/setup#code-examples) * **Handling gas** — Estimating gas limits and setting appropriate gas prices * **Error handling** — Parsing revert reasons from failed transactions *** ## Contract Architecture Amplify accounts are built on the [Nucleus BoringVault](https://github.com/paxoslabs/nucleus-boring-vault) framework — an open-source, audited account architecture. You can browse the full Solidity source, deployment configs, and audit reports in the [GitHub repository](https://github.com/paxoslabs/nucleus-boring-vault). Three contracts handle user-facing operations: | Contract | Role | What You Call It For | | ---------------------------- | -------------------------- | -------------------------------------------------- | | **DistributorCodeDepositor** | Deposit entry point | Depositing tokens into the account | | **WithdrawQueue** | Withdrawal order manager | Submitting and cancelling withdrawal orders | | **BoringVault** | ERC-20 account share token | Approving shares for withdrawal, checking balances | ### Lifecycle You approve the deposit token (e.g., USDC) to the DistributorCodeDepositor, then call [`deposit()`](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits). The account mints shares to your address. You approve your account shares to the WithdrawQueue, then call [`submitOrder()`](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals). The account operator fulfills your order (typically within 24 hours) and sends the want asset to your address. If your withdrawal is still pending, you can call [`cancelOrder()`](/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations) on the WithdrawQueue. Your locked account shares are returned to you. *** ## Glossary | Term | Definition | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | **ABI** | Application Binary Interface — a JSON schema that describes how to encode/decode calls to a contract | | **Approval** | An on-chain transaction granting a contract permission to transfer your tokens | | **Basis points (bps)** | A unit of measurement where 1 bps = 0.01%. 50 bps = 0.5%, 100 bps = 1%. | | **EIP-2612 Permit** | An off-chain signature that authorizes token spending without an on-chain approval transaction | | **ERC-20** | The standard interface for fungible tokens (USDC, USDT, etc.) | | **Gas** | The fee paid in ETH to execute a transaction on Ethereum | | **Nonce** | A sequential counter that prevents transaction replay | | **RPC** | Remote Procedure Call — the API protocol used to communicate with an Ethereum node | | **Slippage** | The difference between the expected and actual exchange rate. Controlled via `minimumMint`. | | **Account shares** | ERC-20 tokens representing your deposit in the account; their value appreciates as the account earns yield | | **Want asset** | The token you want to receive when withdrawing (e.g., USDC) | | **WithdrawQueue** | The contract that manages withdrawal orders as an ordered queue | # Direct Deposits Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits Deposit tokens into an Amplify account by calling the DistributorCodeDepositor contract directly This guide walks you through depositing tokens into an Amplify account by calling the **DistributorCodeDepositor** smart contract directly. It covers every contract method you need to call, in order, for both the standard approval flow and the single-transaction permit flow. ## How Deposits Work When you deposit tokens (e.g., USDC) into an account, you receive **account shares** in return. These shares represent your proportional ownership of the account's assets and appreciate in value as the account earns yield. There are two ways to deposit: | Flow | Transactions | Best For | | -------------------------------- | -------------- | ------------------------------------------------ | | **Standard (Approve + Deposit)** | 2 transactions | All tokens, all wallets | | **Permit (Sign + Deposit)** | 1 transaction | Supported tokens only (USDC, USDG, pyUSD, USD₮0) | ### KYT Accounts As of v0.5.2, Amplify accounts use the **KYT** (Know Your Transaction) depositor contract (`DistributorCodeDepositorV1`). Both `deposit()` and `depositWithPermit()` require an `Attestation` parameter for compliance policy integration. For now, pass **empty/zero values** for the attestation fields as shown in the examples below. Contact the [Paxos Labs team](mailto:support@paxoslabs.com) to implement the compliance policy of your choice — at which point you will receive real attestation values to pass here. The `Attestation` struct is passed as the last parameter before any permit fields: ```json theme={null} { "uuid": "", "expiration": 0, "attester": "0x0000000000000000000000000000000000000000", "signature": "0x" } ``` *** ## What You'll Need | Requirement | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Contract addresses | `distributorCodeDepositorAddress` and `accountantAddress` — see [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) | | ABI | Provided below — just copy into your project | | RPC endpoint | An Ethereum node URL (Alchemy, Infura, QuickNode, etc.) | | Private key or wallet | To sign and send transactions | | Deposit tokens | The token you're depositing (e.g., USDC) + ETH for gas | *** ## ABI Reference You need the DistributorCodeDepositor ABI, the ERC-20 ABI, and the Accountant ABI. Copy these into your project as a JSON file or inline constant. ### DistributorCodeDepositor ABI Both `deposit()` and `depositWithPermit()` include the `_attestation` tuple parameter for KYT compliance. ```json theme={null} [ { "inputs": [ { "name": "depositAsset", "type": "address" }, { "name": "depositAmount", "type": "uint256" }, { "name": "minimumMint", "type": "uint256" }, { "name": "to", "type": "address" }, { "name": "distributorCode", "type": "bytes" }, { "components": [ { "name": "uuid", "type": "string" }, { "name": "expiration", "type": "uint256" }, { "name": "attester", "type": "address" }, { "name": "signature", "type": "bytes" } ], "name": "_attestation", "type": "tuple" } ], "name": "deposit", "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "depositAsset", "type": "address" }, { "name": "depositAmount", "type": "uint256" }, { "name": "minimumMint", "type": "uint256" }, { "name": "to", "type": "address" }, { "name": "distributorCode", "type": "bytes" }, { "components": [ { "name": "uuid", "type": "string" }, { "name": "expiration", "type": "uint256" }, { "name": "attester", "type": "address" }, { "name": "signature", "type": "bytes" } ], "name": "_attestation", "type": "tuple" }, { "name": "deadline", "type": "uint256" }, { "name": "v", "type": "uint8" }, { "name": "r", "type": "bytes32" }, { "name": "s", "type": "bytes32" } ], "name": "depositWithPermit", "outputs": [{ "name": "shares", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" } ] ``` ### ERC-20 ABI (for approvals and allowance checks) ```json theme={null} [ { "inputs": [ { "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "owner", "type": "address" }, { "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "owner", "type": "address" }], "name": "nonces", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### Accountant ABI (for slippage calculation) ```json theme={null} [ { "inputs": [ { "name": "quote", "type": "address" } ], "name": "getRateInQuoteSafe", "outputs": [{ "name": "rateInQuote", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` *** ## Contract Method Reference ### `deposit` **`deposit(depositAsset, depositAmount, minimumMint, to, distributorCode, _attestation)`** | Parameter | Type | Description | | ----------------- | --------- | ---------------------------------------------------------------------------------------------- | | `depositAsset` | `address` | The ERC-20 token address to deposit (e.g., USDC: `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) | | `depositAmount` | `uint256` | Amount in the token's smallest unit. USDC has 6 decimals, so `1000000` = 1 USDC. | | `minimumMint` | `uint256` | Minimum shares to receive — protects against slippage. Set to `0` to accept any amount. | | `to` | `address` | Address that receives the minted account shares (usually the depositor) | | `distributorCode` | `bytes` | Tracking code for referrals/attribution. Pass `0x` (empty bytes) if not applicable. | | `_attestation` | `tuple` | Compliance attestation struct. Pass empty/zero values for now — see [note above](#kyt-vaults). | **Attestation fields**: | Field | Type | Description | Default Value | | ------------ | --------- | ------------------------------------------- | -------------------------------------------- | | `uuid` | `string` | Unique attestation identifier | `""` (empty string) | | `expiration` | `uint256` | Unix timestamp when the attestation expires | `0` | | `attester` | `address` | Address of the attestation provider | `0x0000000000000000000000000000000000000000` | | `signature` | `bytes` | Cryptographic signature from the attester | `0x` (empty bytes) | **Important**: `depositAmount` is in the token's smallest unit. USDC has 6 decimals, so 1,000 USDC = `1000000000` (1000 × 10^6). Getting this wrong is the most common integration mistake. ### `depositWithPermit` **`depositWithPermit(depositAsset, depositAmount, minimumMint, to, distributorCode, _attestation, deadline, v, r, s)`** Same parameters as `deposit()` (including the `_attestation` tuple), plus the permit fields appended **after** the attestation: | Parameter | Type | Description | | ---------- | --------- | ---------------------------------------------- | | `deadline` | `uint256` | Unix timestamp after which the permit expires | | `v` | `uint8` | Recovery byte of the EIP-2612 permit signature | | `r` | `bytes32` | First 32 bytes of the permit signature | | `s` | `bytes32` | Second 32 bytes of the permit signature | ### Distributor Codes The `distributorCode` parameter is used for referral attribution. If Paxos Labs has provided you with a distributor code, encode it as bytes (e.g., `toHex('your_code')` in JavaScript, `b'your_code'` in Python, `[]byte("your_code")` in Go). If you don't have one, pass empty bytes (`0x`, `b""`, `[]byte{}`). *** ## Calculating `minimumMint` (Slippage Protection) The account's **Accountant** contract publishes the exchange rate between the deposit token and account shares. Use it to calculate a safe `minimumMint`. ### Contract call ``` Accountant.getRateInQuoteSafe(depositAssetAddress) → uint256 rateInQuote ``` `rateInQuote` represents the amount of the deposit asset per 1e18 account shares (i.e., per one full share). Pass the deposit token address as the `quote` parameter. ### Calculation ``` expectedShares = (depositAmount × 1e18) / rate minimumMint = expectedShares − (expectedShares × SLIPPAGE_BPS) / 10000 ``` Where `SLIPPAGE_BPS` is your slippage tolerance in basis points (e.g., `50` = 0.5%). Setting `minimumMint` to `0` disables slippage protection entirely. This is fine for testing but not recommended for production — a front-running bot could manipulate the rate between your transaction submission and execution. *** ## Standard Deposit Walkthrough The standard flow requires two transactions: an ERC-20 approval followed by the deposit. Call the deposit token's `allowance()` to see if the DistributorCodeDepositor already has sufficient spending permission. ``` DepositToken.allowance(yourAddress, distributorCodeDepositorAddress) → uint256 ``` If the returned value is ≥ your `depositAmount`, skip to Step 3. Call `approve()` on the deposit token, granting the DistributorCodeDepositor permission to transfer your tokens. ``` DepositToken.approve(distributorCodeDepositorAddress, depositAmount) → bool ``` Wait for the transaction to be mined before proceeding. **USDT special case**: USDT requires resetting the allowance to `0` before setting a new value if there's an existing non-zero allowance. Call `approve(spender, 0)` first, then `approve(spender, amount)`. Query the exchange rate and compute slippage protection. ``` Accountant.getRateInQuoteSafe(depositAssetAddress) → uint256 rateInQuote ``` Then calculate: `minimumMint = ((depositAmount × 1e18) / rateInQuote) × (10000 − SLIPPAGE_BPS) / 10000` Call `deposit()` on the DistributorCodeDepositor. ``` DistributorCodeDepositor.deposit( depositAsset, // e.g., USDC address depositAmount, // e.g., 1000000000 (1,000 USDC) minimumMint, // from Step 3 to, // recipient of vault shares (usually your address) distributorCode, // 0x if none { // _attestation — use empty values for now uuid: "", expiration: 0, attester: "0x0000000000000000000000000000000000000000", signature: "0x" } ) → uint256 shares ``` The return value is the number of account shares minted. Wait for the transaction receipt. The deposit is complete once the transaction is included in a block. You can verify by calling `BoringVault.balanceOf(yourAddress)` to see your new share balance. ### Example values (1,000 USDC deposit on Ethereum mainnet) | Parameter | Value | | ----------------- | ------------------------------------------------------------------------------------------------------ | | `depositAsset` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | `depositAmount` | `1000000000` (1,000 × 10^6) | | `minimumMint` | Calculated from Accountant rate | | `to` | Your wallet address | | `distributorCode` | `0x` (empty) | | `_attestation` | `{ uuid: "", expiration: 0, attester: "0x0000000000000000000000000000000000000000", signature: "0x" }` | *** ## Permit Deposit Walkthrough For tokens that support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) permits, you can combine approval and deposit into a single transaction. | Token | Permit Support | | ----- | --------------------------------------- | | USDC | Yes | | USDG | Yes | | pyUSD | Yes | | USD₮0 | Yes | | USDT | **No** — use standard approve + deposit | Query the token's current nonce for your address. ``` DepositToken.nonces(yourAddress) → uint256 nonce ``` Construct and sign an EIP-712 typed data message. This is an off-chain signature — no gas required. **EIP-712 Domain** (varies by token — this example is for USDC on Ethereum): | Field | Value | | ------------------- | ------------------ | | `name` | `"USD Coin"` | | `version` | `"2"` | | `chainId` | `1` | | `verifyingContract` | USDC token address | **Permit message**: | Field | Value | | ---------- | ----------------------------------- | | `owner` | Your wallet address | | `spender` | `distributorCodeDepositorAddress` | | `value` | `depositAmount` | | `nonce` | From Step 1 | | `deadline` | Unix timestamp (e.g., now + 1 hour) | The EIP-712 type structure: ``` Permit(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline) ``` Sign using your wallet's `signTypedData` (or equivalent EIP-712 signing method). Parse the resulting signature into `v`, `r`, `s` components. Same as the standard flow: ``` Accountant.getRateInQuoteSafe(depositAssetAddress) → uint256 rateInQuote ``` Call `depositWithPermit()` on the DistributorCodeDepositor with the permit signature. Note the `_attestation` tuple is placed **before** the permit parameters. ``` DistributorCodeDepositor.depositWithPermit( depositAsset, // e.g., USDC address depositAmount, // e.g., 1000000000 (1,000 USDC) minimumMint, // from Step 3 to, // recipient of vault shares distributorCode, // 0x if none { // _attestation — use empty values for now uuid: "", expiration: 0, attester: "0x0000000000000000000000000000000000000000", signature: "0x" }, deadline, // from Step 2 v, // signature recovery byte r, // signature first 32 bytes s // signature second 32 bytes ) → uint256 shares ``` The contract verifies the permit signature on-chain, transfers your tokens, and mints account shares — all in a single transaction. Smart contract wallets (like Privy Smart Wallets or Safe) **cannot sign permits** because they don't have a private key. Use the standard approval flow instead. *** ## Troubleshooting The most common cause is that `minimumMint` is set too high relative to the current exchange rate. Set it to `0` for testing or recalculate from the Accountant's `getRateInQuoteSafe()`. The permit signature is invalid and there's no existing ERC-20 approval. Double-check the permit domain parameters (`name`, `version`, `verifyingContract`) match the token's EIP-712 domain. Or switch to the standard approve + deposit flow. You haven't approved the DistributorCodeDepositor to spend your tokens. Call `approve()` on the token contract before calling `deposit()`. Your wallet doesn't hold enough of the deposit token. Check your balance before depositing. USDT requires resetting the allowance to `0` before setting a new value if there's an existing non-zero allowance. Call `approve(spender, 0)` first, then `approve(spender, amount)`. Other tokens (USDC, USDG, pyUSD) allow overwriting an existing approval directly. The `DistributorCodeDepositorV1` contract requires the `_attestation` tuple parameter. If you omit it or use an older ABI without the attestation field, the transaction will revert. Ensure you're using the ABI from this guide and passing the attestation struct (empty values are fine for now). Contact the [Paxos Labs team](mailto:support@paxoslabs.com) to implement your compliance policy. *** ## Next Steps * [Direct Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) — Submit a withdrawal order * [Direct Cancellations](/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations) — Cancel a pending withdrawal * [Account Queries & Monitoring](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/index) — Read APY, TVL, check balances, and monitor withdrawal status * [SDK Deposits Guide](/v1.0.0/intro/products/earn/developers/guides/deposits) — Use the Amplify SDK instead # Direct Contract Integration Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/index Call Amplify account smart contracts directly without the SDK This section walks you through interacting with Amplify account smart contracts **without** the `@paxoslabs/amplify-sdk`. Use this approach when you need to integrate from a non-JavaScript backend, a mobile app, or when you want full control over transaction construction. If you're building a JavaScript or TypeScript app and want the fastest path to production, the [Amplify SDK](/v1.0.0/intro/products/earn/developers/index) handles ABI encoding, address resolution, token approvals, and error handling for you. As of v0.5.2, Amplify accounts use **KYT** (Know Your Transaction) enabled depositor contracts. Deposit functions now require an `Attestation` parameter for compliance policy integration. For now, pass empty/zero attestation values — contact the [Paxos Labs team](mailto:support@paxoslabs.com) to implement the compliance policy of your choice. See [Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits#kyt-vaults) for details. Obtain contract addresses, configure multi-chain support, and install an Ethereum library for your language. Smart contracts, ABIs, token approvals, permits, account shares, slippage, and contract architecture. Deposit tokens into an account and receive account shares. Covers both the standard approval flow and the single-transaction permit flow. Submit a withdrawal order offering account shares in exchange for stablecoins. Covers order construction, submission, and status polling. Cancel a pending withdrawal order and recover your locked account shares. Covers direct cancellation and meta-transaction cancellation. Discover accounts, read APY/TVL, check balances, withdrawal fees, pause state, and monitor withdrawal request history via REST API and on-chain reads. ## Learning Path 1. **[Setup & Prerequisites](/v1.0.0/intro/products/earn/developers/guides/direct-contract/setup)** — Get contract addresses and install your Ethereum library 2. **[Concepts](/v1.0.0/intro/products/earn/developers/guides/direct-contract/concepts)** — Understand ABIs, approvals, permits, account shares, and contract architecture 3. **[Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits)** — Implement your first deposit flow 4. **[Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals)** — Submit withdrawal orders 5. **[Cancellations](/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations)** — Cancel pending withdrawals 6. **[Account Queries](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/index)** — Read account state and monitor requests *** ## Using the SDK? If you're integrating from JavaScript or TypeScript, see the [SDK Integration](/v1.0.0/intro/products/earn/developers/guides/index) guides — they cover the same deposit, withdrawal, and cancellation workflows using the `@paxoslabs/amplify-sdk`. # Setup & Prerequisites Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/setup Obtain contract addresses, configure your RPC client, and install an Ethereum library ## Prerequisites Before you begin, make sure you have: 1. **An Amplify API key** — Contact [Paxos Labs](mailto:support@paxoslabs.com) to get one 2. **An Ethereum RPC endpoint** — From [Alchemy](https://www.alchemy.com/), [Infura](https://www.infura.io/), [QuickNode](https://www.quicknode.com/), or any provider 3. **A wallet** with the deposit token (e.g., USDC) and ETH for gas fees 4. **The ABI snippets** — Provided in each guide (no need to compile Solidity). See [Concepts](/v1.0.0/intro/products/earn/developers/guides/direct-contract/concepts#what-is-an-abi) for an explanation of ABIs. *** ## Obtaining Contract Addresses Contract addresses differ by **account** and **chain**. All contract addresses needed for direct integration are available via the Amplify GraphQL API. ### GraphQL Query Send a POST request to `https://api.paxoslabs.com/graphql` with your API key: ```bash theme={null} curl -s -X POST "https://api.paxoslabs.com/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: $AMPLIFY_API_KEY" \ -d '{ "query": "query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } }", "variables": {} }' | jq ``` Pass `variables` to filter results. For example, `{"chainId": 1}` returns only Ethereum mainnet accounts, and `{"yieldType": "CORE"}` returns only CORE strategy accounts. ### Response Each item in `amplifySdkConfigs` contains a `vault` object with all the contract addresses you need: ```json theme={null} { "data": { "amplifySdkConfigs": [ { "id": "config-id", "chainId": 1, "yieldType": "CORE", "vault": { "id": "vault-id", "name": "Amplify Core", "chainId": 1, "boringVaultAddress": "0x...", "tellerModuleId": "0x...", "accountantModuleId": "0x...", "withdrawQueueModuleId": "0x...", "communityCodeDepositorModuleId": "0x...", "supportedAssets": [ { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "depositable": true, "withdrawable": true, "symbol": "USDC", "tokenName": "USD Coin", "decimals": 6 } ] } } ] } } ``` ### Mapping Response Fields to Contract Addresses | GraphQL Field | Contract | Used For | | -------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `vault.boringVaultAddress` | **BoringVault** | ERC-20 account share token — approvals, balances | | `vault.communityCodeDepositorModuleId` | **DistributorCodeDepositor** | [Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) — `deposit()`, `depositWithPermit()` | | `vault.withdrawQueueModuleId` | **WithdrawQueue** | [Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) — `submitOrder()`, `cancelOrder()` | | `vault.accountantModuleId` | **Accountant** | Exchange rate — `getRateInQuoteSafe()` for slippage/share conversion | | `vault.tellerModuleId` | **Teller** | Pause state — `isPaused()` | The GraphQL field `communityCodeDepositorModuleId` is a legacy name. The contract it points to is the **DistributorCodeDepositor**. Use this address wherever the documentation refers to the `distributorCodeDepositorAddress`. `withdrawQueueModuleId` and `communityCodeDepositorModuleId` can be `null` for accounts that don't support external withdrawals or deposits. Check for non-null values before using. ### Supported Assets The `supportedAssets` array on each account tells you which tokens can be deposited or withdrawn: | Field | Description | | -------------- | -------------------------------------------------------------- | | `address` | ERC-20 token contract address | | `chainId` | Chain the token is on | | `depositable` | Whether this token can be deposited | | `withdrawable` | Whether this token can be used as the want asset on withdrawal | | `symbol` | Token symbol (e.g., `"USDC"`) | | `decimals` | Token decimals (e.g., `6` for USDC) | ### Filtering by Chain or Yield Type Pass `variables` to narrow results: ```bash theme={null} # Ethereum mainnet only curl -s -X POST "https://api.paxoslabs.com/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: $AMPLIFY_API_KEY" \ -d '{ "query": "query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } }", "variables": { "chainId": 1 } }' | jq ``` ```bash theme={null} # CORE yield type only curl -s -X POST "https://api.paxoslabs.com/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: $AMPLIFY_API_KEY" \ -d '{ "query": "query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } }", "variables": { "yieldType": "CORE" } }' | jq ``` ### Available Yield Types | Value | Description | | ---------- | --------------------------- | | `CORE` | Standard yield strategy | | `PRIME` | Maps to CORE in the SDK | | `TREASURY` | Treasury-backed strategy | | `TBILL` | Maps to TREASURY in the SDK | | `FRONTIER` | Higher-risk/reward strategy | | `LENDING` | Maps to FRONTIER in the SDK | *** ## Code Examples Any language with an Ethereum JSON-RPC library can interact with Amplify contracts. The guides cover the contract methods you need to call — here are popular libraries for each language: **Library**: [viem](https://viem.sh) ```bash theme={null} pnpm add viem ``` **Library**: [web3.py](https://web3py.readthedocs.io/) ```bash theme={null} pip install web3 ``` **Library**: [go-ethereum](https://geth.ethereum.org/) with `abigen` ```bash theme={null} go get github.com/ethereum/go-ethereum go install github.com/ethereum/go-ethereum/cmd/abigen@latest ``` **Library**: [web3swift](https://github.com/web3swift-team/web3swift) Add via Swift Package Manager: ``` https://github.com/web3swift-team/web3swift.git ``` **Library**: [web3j](https://docs.web3j.io/) ```kotlin theme={null} // build.gradle.kts dependencies { implementation("org.web3j:core:4.12.0") } ``` **Library**: [web3dart](https://pub.dev/packages/web3dart) ```yaml theme={null} # pubspec.yaml dependencies: web3dart: ^2.7.3 http: ^1.2.0 ``` *** ## Next Steps * [Concepts](/v1.0.0/intro/products/earn/developers/guides/direct-contract/concepts) — Understand smart contracts, ABIs, approvals, permits, and account architecture * [Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) — Implement your first deposit flow # Allowance Checking Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/allowances Check ERC-20 approval allowances for deposits and withdrawals The SDK's `client.core.authorization.detect(...)` returns `'permit' | 'approval' | 'already_approved'`. The `ERC20.allowance(owner, spender)` check shown below only differentiates between `'already_approved'` (allowance ≥ amount) and `'approval'` (allowance \< amount). The `'permit'` branch is returned by the SDK only after additional on-chain permit metadata reads succeed (nonces, DOMAIN\_SEPARATOR, name, version). Direct-contract callers who only need the approval path can use allowance alone. ## Contract Method ### `allowance(owner, spender)` Returns the amount of tokens the `spender` is approved to transfer on behalf of the `owner`. | Parameter | Type | Description | | --------- | --------- | -------------------------------- | | `owner` | `address` | The token holder's address | | `spender` | `address` | The contract authorized to spend | **Returns**: `uint256` — approved amount in the token's smallest unit ## ABI ```json theme={null} [ { "inputs": [ { "name": "owner", "type": "address" }, { "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ## Usage | Check | Token contract | Owner | Spender | | ---------------------- | -------------------------- | -------------- | ----------------------------------------------------------------- | | Deposit token approval | Deposit token (e.g., USDC) | User's address | `vault.communityCodeDepositorModuleId` (DistributorCodeDepositor) | | Account share approval | BoringVault | User's address | `vault.withdrawQueueModuleId` (WithdrawQueue) | If the returned allowance is less than the amount you intend to deposit or withdraw, you need to call `approve()` first. See the [Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) or [Withdrawals](/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals) guides for the full flow. Contract addresses are obtained via [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery). # Exchange Rate Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/exchange-rate Read the share-to-asset exchange rate from the Accountant contract The Accountant contract tracks the exchange rate between account shares and the underlying asset. ## Contract Method ### `getRateInQuoteSafe(quote)` Returns the amount of the quote asset per 1e18 account shares. | Parameter | Type | Description | | --------- | --------- | ----------------------------------------------------------- | | `quote` | `address` | The token address to quote the rate in (e.g., USDC address) | **Returns**: `uint256 rateInQuote` — amount of the quote asset per one full share (1e18) ## ABI ```json theme={null} [ { "inputs": [{ "name": "quote", "type": "address" }], "name": "getRateInQuoteSafe", "outputs": [{ "name": "rateInQuote", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ## Usage Call on the **Accountant** contract (`vault.accountantModuleId` from [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery)). ### For deposits (calculating minimumMint) ``` rate = Accountant.getRateInQuoteSafe(depositTokenAddress) expectedShares = (depositAmount × 1e18) / rate minimumMint = expectedShares × (10000 − SLIPPAGE_BPS) / 10000 ``` ### For withdrawals (converting want amount to shares) ``` rate = Accountant.getRateInQuoteSafe(wantAssetAddress) sharesNeeded = (wantAmount × 1e18) / rate ``` The rate uses the quote asset's decimals. For example, if quoting in USDC (6 decimals) and the rate is `1050000`, that means 1 account share ≈ 1.05 USDC. # Account Queries & Monitoring Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/index Discover accounts, read APY/TVL, check balances and fees, and monitor withdrawal status — using REST API calls and on-chain reads The Amplify SDK wraps several API endpoints and on-chain reads behind convenient functions like `getVaultAPY()` and `getWithdrawalRequests()`. When integrating directly, you call these yourself. This section covers every read-only operation the SDK provides, with the raw API calls and contract reads you need. All API endpoints require an API key passed via the `x-api-key` header. Contact [Paxos Labs](mailto:support@paxoslabs.com) to get one. The GraphQL endpoint (`POST /graphql`) returns account contract addresses needed for integration. See [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) for the full query. Discover accounts, contract addresses, and supported assets via the GraphQL API. Fetch the latest annual percentage yield for an account. Fetch total value locked for an account. Query withdrawal order history and status for a user. Read ERC-20 balances directly from the blockchain. Check ERC-20 approval allowances for deposits and withdrawals. Read the share-to-asset exchange rate from the Accountant contract. Calculate withdrawal fees via the FeeModule contract. Check whether account operations are paused before submitting transactions. *** ## SDK-to-Direct Mapping Reference | v1.0.0 SDK call | Direct contract / API equivalent | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `new AmplifyClient({ apiKey })` | No equivalent needed — configure your HTTP client and contract addresses directly | | `client.amplify.vaults.list({ filter? })` | Amplify GraphQL API — see [vault-discovery](./vault-discovery) | | `client.amplify.vaults.listAssets({ filter? })` | [`GET /v2/amplify/vaultAssets`](/v1.0.0/api-reference/introduction) — filter by `depositable: true` or `withdrawable: true` | | `client.amplify.vaults.getApys({ filter? })` | [`GET /v2/amplify/vaultApys`](/v1.0.0/api-reference/introduction) | | `client.amplify.vaults.getTvls({ filter? })` | [`GET /v2/amplify/vaultTvls`](/v1.0.0/api-reference/introduction) | | `client.amplify.vaults.getSupplyCaps({ filter? })` | On-chain: `DistributorCodeDepositor.supplyCapInBase()` per asset | | `client.amplify.withdraw.listRequests({ filter? })` | [`GET /v2/amplify/withdrawalRequests`](/v1.0.0/api-reference/introduction) | | `client.amplify.withdraw.calculateFee({...})` | On-chain: `WithdrawQueue.feeModule()` → `FeeModule.calculateOfferFees()` | | `client.amplify.users.getPositions({...})` | On-chain: `BoringVault.balanceOf(user)` + `Accountant.getRateInQuoteSafe(quote)` | | `client.core.authorization.detect({...})` | `allowance` for `already_approved`/`approval`; `permit` requires additional reads (nonces, DOMAIN\_SEPARATOR, name, version) | | `client.amplify.deposit.prepare({...})` | Encode `DistributorCodeDepositor.deposit()` or `depositWithPermit()` directly | | `client.amplify.withdraw.prepare({...})` | Encode `WithdrawQueue.submitOrder()` directly | | `client.amplify.withdraw.cancel({...})` | Encode `WithdrawQueue.cancelOrder()` directly | # Pause State Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/pause-state Check whether account operations are paused before submitting transactions Before submitting transactions, check whether the account is paused. ## Contract Method ### `isPaused()` Returns whether the account's Teller contract is currently paused. **Returns**: `bool` — `true` if paused, `false` if operational ## ABI ```json theme={null} [ { "inputs": [], "name": "isPaused", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" } ] ``` ## Usage Call on the **Teller** contract (`vault.tellerModuleId` from [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery)). ``` isPaused = Teller.isPaused() ``` If the Teller is paused, deposit and withdrawal transactions will revert with `TellerIsPaused`. Check pause state before submitting transactions in production to provide a better user experience. # Token Balances Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/token-balances Read ERC-20 token balances directly from the blockchain Read ERC-20 token balances directly from the blockchain. ## Contract Methods ### `balanceOf(account)` Returns the token balance for an address. | Parameter | Type | Description | | --------- | --------- | ----------------------- | | `account` | `address` | Wallet address to query | **Returns**: `uint256` — token balance in the token's smallest unit ### `decimals()` Returns the number of decimals the token uses. **Returns**: `uint8` — decimal places (e.g., `6` for USDC, `18` for account shares) ## ABI ```json theme={null} [ { "inputs": [{ "name": "account", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" } ] ``` ## Usage Call these on any ERC-20 token contract, including: | What to check | Contract to call | Address source from [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) | | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Account share balance | **BoringVault** | `vault.boringVaultAddress` | | Deposit token balance | **ERC-20 token** | `vault.supportedAssets[].address` | To convert a raw balance to a human-readable amount, divide by `10 ^ decimals`. For example, a USDC `balanceOf` of `1000000000` with `decimals` of `6` = 1,000 USDC. # Account APY Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-apy Fetch the latest annual percentage yield for an account via the REST API Equivalent to SDK's `client.amplify.vaults.getApys()` — see [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). ## Endpoint ``` GET https://api.paxoslabs.com/v2/amplify/vaultApys ``` ## Query Parameters | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ----------------------------------------------------- | | `filter` | `string` | Yes | Filter expression — must include `vaultAddress=0x...` | | `orderByTimestamp` | `string` | No | Sort order: `desc` (newest first) or `asc` | | `pageSize` | `int` | No | Results per page (use `1` for latest only) | ## Example Request ```bash theme={null} VAULT_ADDRESS="0x..." curl -s "https://api.paxoslabs.com/v2/amplify/vaultApys?filter=vaultAddress%3D${VAULT_ADDRESS}&orderByTimestamp=desc&pageSize=1" \ -H "x-api-key: $AMPLIFY_API_KEY" | jq '.vaultApys[0]' ``` ## Response ```json theme={null} { "vaultApys": [ { "vaultAddress": "0x...", "chainId": 1, "apy": "0.0523", "timestamp": "2025-01-15T12:00:00Z" } ] } ``` ## Response Types | Field | Type | Description | | -------------- | --------- | -------------------------------------------------- | | `vaultAddress` | `address` | BoringVault address | | `chainId` | `int` | Chain ID | | `apy` | `string` | APY as a decimal string (e.g., `"0.0523"` = 5.23%) | | `timestamp` | `string` | ISO 8601 timestamp of the APY measurement | The `apy` field is a decimal (e.g., `0.0523` = 5.23%). Multiply by 100 to display as a percentage. # Account Discovery Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery Discover accounts, contract addresses, and supported assets via the GraphQL API Equivalent to SDK's `client.amplify.vaults.list()` and `client.amplify.vaults.listAssets()` — see [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). ## GraphQL API (Recommended) The GraphQL API returns **all contract addresses** needed for direct integration in a single request. ### Endpoint ``` POST https://api.paxoslabs.com/graphql ``` ### Headers | Header | Value | | -------------- | -------------------- | | `Content-Type` | `application/json` | | `x-api-key` | Your Amplify API key | ### Query ```graphql theme={null} query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } } ``` ### Variables (all optional) | Variable | Type | Description | | ----------- | ----------- | ----------------------------------------------------------- | | `chainId` | `Int` | Filter by chain (e.g., `1` for Ethereum, `8453` for Base) | | `yieldType` | `YieldType` | Filter by yield type (`CORE`, `TREASURY`, `FRONTIER`, etc.) | ### Example Request ```bash theme={null} curl -s -X POST "https://api.paxoslabs.com/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: $AMPLIFY_API_KEY" \ -d '{ "query": "query AmplifySdkConfigs($chainId: Int, $yieldType: YieldType) { amplifySdkConfigs(chainId: $chainId, yieldType: $yieldType) { id chainId yieldType vault { id name chainId boringVaultAddress tellerModuleId accountantModuleId withdrawQueueModuleId communityCodeDepositorModuleId supportedAssets { address chainId depositable withdrawable symbol tokenName decimals } } } }", "variables": { "chainId": 1 } }' | jq ``` ### Response ```json theme={null} { "data": { "amplifySdkConfigs": [ { "id": "config-id", "chainId": 1, "yieldType": "CORE", "vault": { "id": "vault-id", "name": "Amplify Core", "chainId": 1, "boringVaultAddress": "0x...", "tellerModuleId": "0x...", "accountantModuleId": "0x...", "withdrawQueueModuleId": "0x...", "communityCodeDepositorModuleId": "0x...", "supportedAssets": [ { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "depositable": true, "withdrawable": true, "symbol": "USDC", "tokenName": "USD Coin", "decimals": 6 } ] } } ] } } ``` ### Response Types | Field | Type | Description | | -------------------------------------- | ---------- | ---------------------------------------------- | | `id` | `string` | SDK config identifier | | `chainId` | `int` | Blockchain network ID | | `yieldType` | `string` | Yield strategy (`CORE`, `TREASURY`, etc.) | | `vault.id` | `string` | Account identifier (use for deduplication) | | `vault.name` | `string` | Human-readable account name | | `vault.boringVaultAddress` | `address` | BoringVault — ERC-20 share token | | `vault.communityCodeDepositorModuleId` | `address?` | DistributorCodeDepositor — deposit entry point | | `vault.withdrawQueueModuleId` | `address?` | WithdrawQueue — withdrawal orders | | `vault.accountantModuleId` | `address` | Accountant — exchange rate oracle | | `vault.tellerModuleId` | `address` | Teller — pause state | | `vault.supportedAssets[].address` | `address` | ERC-20 token contract address | | `vault.supportedAssets[].depositable` | `boolean` | Whether this token can be deposited | | `vault.supportedAssets[].withdrawable` | `boolean` | Whether this token can be used as want asset | | `vault.supportedAssets[].symbol` | `string?` | Token symbol (e.g., `"USDC"`) | | `vault.supportedAssets[].decimals` | `int?` | Token decimals (e.g., `6`) | Multiple configs can reference the same account (one per deposit token). Deduplicate by `vault.id` — each account has a single set of contract addresses and a `supportedAssets` array listing all tokens. `withdrawQueueModuleId` and `communityCodeDepositorModuleId` can be `null` for accounts that don't support external withdrawals or deposits. ### Contract Address Mapping | GraphQL Field | Contract | Used For | | -------------------------------------- | ---------------------------- | -------------------------------------- | | `vault.boringVaultAddress` | **BoringVault** | Account share token (ERC-20) | | `vault.communityCodeDepositorModuleId` | **DistributorCodeDepositor** | `deposit()`, `depositWithPermit()` | | `vault.withdrawQueueModuleId` | **WithdrawQueue** | `submitOrder()`, `cancelOrder()` | | `vault.accountantModuleId` | **Accountant** | `getRateInQuoteSafe()` (exchange rate) | | `vault.tellerModuleId` | **Teller** | `isPaused()` (pause state) | The GraphQL field `communityCodeDepositorModuleId` is a legacy name. The contract it points to is the **DistributorCodeDepositor**. Use this address wherever the documentation refers to the `distributorCodeDepositorAddress`. ### Available Yield Types | Value | SDK Mapping | | ---------- | ----------- | | `CORE` | CORE | | `PRIME` | CORE | | `TREASURY` | TREASURY | | `TBILL` | TREASURY | | `FRONTIER` | FRONTIER | | `LENDING` | FRONTIER | *** ## REST API (Simpler Alternative) If you only need to list account–asset pairs without full contract addresses, you can use the REST endpoint. ### Endpoint ``` GET https://api.paxoslabs.com/v2/amplify/vaultAssets ``` ### Query Parameters | Parameter | Type | Required | Description | | ----------- | -------- | -------- | ---------------------------------------------------------- | | `pageSize` | `int` | No | Results per page (default 20, max 100) | | `pageToken` | `string` | No | Pagination token from `nextPageToken` in previous response | | `filter` | `string` | No | Filter expression (e.g., `chainId=1`, `withdrawable=true`) | ### Example Request ```bash theme={null} curl -s "https://api.paxoslabs.com/v2/amplify/vaultAssets?pageSize=100" \ -H "x-api-key: $AMPLIFY_API_KEY" | jq ``` ### Response ```json theme={null} { "vaultAssets": [ { "vaultAddress": "0x...", "chainId": 1, "assetAddress": "0x...", "depositable": true, "withdrawable": true } ], "nextPageToken": null, "tokenMetadata": { "1:0xA0b8...": { "address": "0xA0b8...", "chain_id": "1", "symbol": "USDC", "name": "USD Coin", "decimals": "6" } } } ``` ### Response Types | Field | Type | Description | | ---------------------------- | --------- | ---------------------------------------------- | | `vaultAssets[].vaultAddress` | `address` | BoringVault address (share token) | | `vaultAssets[].chainId` | `int` | Chain ID | | `vaultAssets[].assetAddress` | `address` | ERC-20 token address | | `vaultAssets[].depositable` | `boolean` | Whether this token can be deposited | | `vaultAssets[].withdrawable` | `boolean` | Whether this token can be used as want asset | | `nextPageToken` | `string?` | Token for next page (`null` if no more pages) | | `tokenMetadata` | `object` | Map keyed by `chainId:address` with token info | | `tokenMetadata[].symbol` | `string` | Token symbol | | `tokenMetadata[].name` | `string` | Token name | | `tokenMetadata[].decimals` | `string` | Token decimals (as string) | The REST endpoint only returns `vaultAddress` (the BoringVault). For the full set of contract addresses needed for deposits, withdrawals, and cancellations, use the [GraphQL API](#graphql-api-recommended) above. *** ## Withdraw-Eligible Assets Filter by `withdrawable: true` in either API: * **GraphQL**: Filter `vault.supportedAssets` where `withdrawable == true` * **REST**: `GET /v2/amplify/vaultAssets?filter=withdrawable%3Dtrue&pageSize=100` # Account TVL Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-tvl Fetch total value locked for an account via the REST API Equivalent to SDK's `client.amplify.vaults.getTvls()` — see [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). ## Endpoint ``` GET https://api.paxoslabs.com/v2/amplify/vaultTvls ``` ## Query Parameters | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------------------------------------------------------------- | | `filter` | `string` | Yes | Filter expression — must include `vaultAddress=0x...`. Optionally add `AND chainId=1` | | `includeCurrent` | `string` | No | Set to `true` to include the current (real-time) TVL entry | | `orderByTimestamp` | `string` | No | Sort order: `desc` (newest first) or `asc` | | `pageSize` | `int` | No | Results per page (use `1` for latest only) | ## Example Request ```bash theme={null} VAULT_ADDRESS="0x..." curl -s "https://api.paxoslabs.com/v2/amplify/vaultTvls?filter=vaultAddress%3D${VAULT_ADDRESS}&includeCurrent=true&orderByTimestamp=desc&pageSize=1" \ -H "x-api-key: $AMPLIFY_API_KEY" | jq '.vaultTvls[0]' ``` ## Response ```json theme={null} { "vaultTvls": [ { "vaultAddress": "0x...", "chainId": 1, "tvl": "15234567.89", "timestamp": "2025-01-15T12:00:00Z" } ] } ``` ## Response Types | Field | Type | Description | | -------------- | --------- | -------------------------------------------------------- | | `vaultAddress` | `address` | BoringVault address | | `chainId` | `int` | Chain ID | | `tvl` | `string` | Total value locked as a decimal string (USD denominated) | | `timestamp` | `string` | ISO 8601 timestamp of the TVL measurement | # Withdrawal Fees Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/withdrawal-fees Calculate withdrawal fees via the FeeModule contract Equivalent to SDK's `client.amplify.withdraw.calculateFee()` — see [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). Reads from the WithdrawQueue's FeeModule on-chain. ## Contract Methods Reading withdrawal fees requires two calls — first to get the FeeModule address, then to calculate the fee. ### Step 1: `WithdrawQueue.feeModule()` Returns the address of the FeeModule contract. **Returns**: `address` — FeeModule contract address ### Step 2: `FeeModule.calculateOfferFees(amount, offerAsset, wantAsset, receiver)` Calculates the fee that will be deducted from a withdrawal order. | Parameter | Type | Description | | ------------ | --------- | --------------------------------------------------- | | `amount` | `uint256` | Account shares to offer | | `offerAsset` | `address` | BoringVault address (the share token being offered) | | `wantAsset` | `address` | Token the user wants to receive (e.g., USDC) | | `receiver` | `address` | Address that will receive the want asset | **Returns**: `uint256 feeAmount` — fee in account shares (18 decimals) ### `FeeModule.offerFeePercentage()` Returns the fee percentage applied to withdrawal orders. **Returns**: `uint256` — fee percentage with 18-decimal precision (e.g., `5000000000000000` = 0.5%) ## ABI ### WithdrawQueue (fee module lookup) ```json theme={null} [ { "inputs": [], "name": "feeModule", "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view", "type": "function" } ] ``` ### FeeModule ```json theme={null} [ { "inputs": [ { "name": "amount", "type": "uint256" }, { "name": "offerAsset", "type": "address" }, { "name": "wantAsset", "type": "address" }, { "name": "receiver", "type": "address" } ], "name": "calculateOfferFees", "outputs": [{ "name": "feeAmount", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "offerFeePercentage", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ## Usage Call `feeModule()` on the **WithdrawQueue** contract (`vault.withdrawQueueModuleId` from [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery)), then call `calculateOfferFees()` on the returned FeeModule address. ``` feeModuleAddress = WithdrawQueue.feeModule() feeAmount = FeeModule.calculateOfferFees(shareAmount, boringVaultAddress, wantAssetAddress, receiverAddress) feePercentage = FeeModule.offerFeePercentage() ``` The fee is subtracted from the account shares you offer. If you offer 100 shares and the fee is 0.5 shares, only 99.5 shares worth of the want asset will be sent to the receiver. Account for this when displaying withdrawal amounts to users. # Withdrawal Request History Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/withdrawal-requests Query withdrawal order history and status for a user via the REST API Equivalent to SDK's `client.amplify.withdraw.listRequests()` — see [SDK AI Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference). ## Endpoint ``` GET https://api.paxoslabs.com/v2/amplify/withdrawalRequests ``` ## Query Parameters | Parameter | Type | Required | Description | | ----------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `filter` | `string` | Yes | Filter expression — must include `userAddress=0x...`. Optionally add `AND chainId=1`, `AND status=PENDING`, etc. | | `pageSize` | `int` | No | Results per page (default 20) | | `pageToken` | `string` | No | Pagination token from previous response | ## Example Request ```bash theme={null} USER_ADDRESS="0x..." curl -s "https://api.paxoslabs.com/v2/amplify/withdrawalRequests?filter=userAddress%3D${USER_ADDRESS}&pageSize=20" \ -H "x-api-key: $AMPLIFY_API_KEY" | jq ``` ## Response ```json theme={null} { "withdrawalRequests": [ { "orderIndex": "42", "userAddress": "0x...", "vaultAddress": "0x...", "chainId": 1, "offerAmount": "1000000000000000000", "wantAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "status": "PENDING", "createdAt": "2025-01-15T10:30:00Z", "fulfilledAt": null } ], "nextPageToken": null } ``` ## Response Types | Field | Type | Description | | -------------- | --------- | ----------------------------------------------------- | | `orderIndex` | `string` | WithdrawQueue order index | | `userAddress` | `address` | Address that submitted the order | | `vaultAddress` | `address` | BoringVault address | | `chainId` | `int` | Chain ID | | `offerAmount` | `string` | Account shares offered (18 decimals, as string) | | `wantAsset` | `address` | Token address the user wants to receive | | `status` | `string` | Order status (see below) | | `createdAt` | `string` | ISO 8601 timestamp of order creation | | `fulfilledAt` | `string?` | ISO 8601 timestamp of fulfillment (`null` if pending) | ## Status Values | Status | Description | | ---------------- | ------------------------------------------------------------- | | `PENDING` | Order submitted, awaiting fulfillment by the account operator | | `COMPLETE` | Fulfilled — want asset sent to receiver | | `PENDING_REFUND` | Cancellation in progress — shares being returned | | `REFUNDED` | Cancelled — shares returned to refund receiver | # Direct Withdrawals Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/direct-contract/withdrawals Submit withdrawal orders by calling the WithdrawQueue contract directly This guide walks you through withdrawing from an Amplify account by calling the **WithdrawQueue** smart contract directly. It covers every contract method you need to call, in order, including fee calculation, share conversion, and order status polling. ## How Withdrawals Work Unlike deposits, withdrawals are **not instant**. They use a queue-based model: Grant the WithdrawQueue permission to transfer your account shares by calling `approve()` on the BoringVault (share token) contract. Call `submitOrder()` on the WithdrawQueue. Your shares are locked and a withdrawal order is created. The account operator processes the queue (typically within 24 hours) and transfers the want asset (e.g., USDC) to your receiver address. **Why can't I withdraw instantly?** Account assets may be deployed in DeFi strategies. The account operator needs time to unwind positions and source liquidity. This is standard across DeFi account protocols. *** ## What You'll Need | Requirement | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Contract addresses | `boringVaultAddress`, `withdrawQueueAddress`, and `accountantAddress` — see [Account Discovery](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/vault-discovery) | | ABI | Provided below | | RPC endpoint | An Ethereum node URL | | Private key or wallet | To sign and send transactions | | Account shares | You must have shares from a [prior deposit](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) | *** ## ABI Reference ### WithdrawQueue ABI ```json theme={null} [ { "inputs": [ { "components": [ { "name": "amountOffer", "type": "uint256" }, { "name": "wantAsset", "type": "address" }, { "name": "intendedDepositor", "type": "address" }, { "name": "receiver", "type": "address" }, { "name": "refundReceiver", "type": "address" }, { "components": [ { "name": "approvalMethod", "type": "uint8" }, { "name": "approvalV", "type": "uint8" }, { "name": "approvalR", "type": "bytes32" }, { "name": "approvalS", "type": "bytes32" }, { "name": "submitWithSignature", "type": "bool" }, { "name": "deadline", "type": "uint256" }, { "name": "eip2612Signature", "type": "bytes" } ], "name": "signatureParams", "type": "tuple" } ], "name": "params", "type": "tuple" } ], "name": "submitOrder", "outputs": [{ "name": "orderIndex", "type": "uint256" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "name": "orderIndex", "type": "uint256" }], "name": "getOrderStatus", "outputs": [{ "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "minimumOrderSize", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "feeModule", "outputs": [{ "name": "", "type": "address" }], "stateMutability": "view", "type": "function" } ] ``` ### FeeModule ABI ```json theme={null} [ { "inputs": [ { "name": "amount", "type": "uint256" }, { "name": "offerAsset", "type": "address" }, { "name": "wantAsset", "type": "address" }, { "name": "receiver", "type": "address" } ], "name": "calculateOfferFees", "outputs": [{ "name": "feeAmount", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "offerFeePercentage", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### BoringVault ABI (ERC-20 for share approval) ```json theme={null} [ { "inputs": [ { "name": "spender", "type": "address" }, { "name": "amount", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "name": "owner", "type": "address" }, { "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "name": "account", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` ### Accountant ABI (for share conversion) ```json theme={null} [ { "inputs": [ { "name": "quote", "type": "address" } ], "name": "getRateInQuoteSafe", "outputs": [{ "name": "rateInQuote", "type": "uint256" }], "stateMutability": "view", "type": "function" } ] ``` *** ## Contract Method Reference ### `submitOrder(params)` | Field | Type | Description | | ------------------- | --------- | ------------------------------------------------------------------------------------- | | `amountOffer` | `uint256` | Account shares to offer. Shares have 18 decimals, so `1000000000000000000` = 1 share. | | `wantAsset` | `address` | Token you want to receive (e.g., USDC address) | | `intendedDepositor` | `address` | **Must match the caller** (`msg.sender`) | | `receiver` | `address` | Where the want asset is sent when fulfilled | | `refundReceiver` | `address` | Where account shares are returned if the order is cancelled | | `signatureParams` | `tuple` | For standard ERC-20 approval flow, pass all zeros (see below) | ### Empty Signature Params When you've already called `approve()`, pass empty signature params to indicate standard ERC-20 approval: | Field | Value | | --------------------- | -------------------- | | `approvalMethod` | `0` (EIP20\_APPROVE) | | `approvalV` | `0` | | `approvalR` | `0x` + 32 zero bytes | | `approvalS` | `0x` + 32 zero bytes | | `submitWithSignature` | `false` | | `deadline` | `0` | | `eip2612Signature` | `0x` (empty bytes) | ### Order Statuses | Value | Status | Meaning | | ----- | -------------------------- | ------------------------------------------- | | `0` | NOT\_FOUND | Order doesn't exist | | `1` | PENDING | Waiting for the account operator to fulfill | | `2` | COMPLETE | Fulfilled — want asset sent to receiver | | `3` | COMPLETE\_PRE\_FILLED | Filled during submission | | `4` | PENDING\_REFUND | Being refunded | | `5` | COMPLETE\_REFUNDED | Cancelled — shares returned | | `6` | FAILED\_TRANSFER\_REFUNDED | Transfer failed — shares returned | *** ## Withdrawal Fees The WithdrawQueue charges a fee on withdrawal orders via its **FeeModule**. You should check the fee before submitting to display accurate amounts to your users. The fee is deducted from your `amountOffer` in account shares — meaning you receive the want asset equivalent of `amountOffer - feeAmount`. ### How to read the fee This requires two contract reads: **Step 1** — Get the FeeModule address from the WithdrawQueue: ``` WithdrawQueue.feeModule() → address feeModuleAddress ``` **Step 2** — Calculate the fee for your specific withdrawal: ``` FeeModule.calculateOfferFees( amount, // vault share amount (uint256) offerAsset, // BoringVault address (the share token) wantAsset, // token you want to receive (e.g., USDC) receiver // address receiving the want asset ) → uint256 feeAmount ``` The `feeAmount` is in account shares (18 decimals). To get the fee percentage: ``` FeeModule.offerFeePercentage() → uint256 feePercentage ``` The returned value uses 18-decimal precision (e.g., `5000000000000000` = 0.5%). The fee is subtracted from the account shares you offer. If you offer 100 shares and the fee is 0.5 shares, only 99.5 shares worth of the want asset will be sent to the receiver. Account for this when displaying withdrawal amounts to users. *** ## Converting Withdrawal Amount to Shares Users typically think in terms of the want asset (e.g., "I want to withdraw 1,000 USDC"), but `amountOffer` is denominated in **account shares**. Use the Accountant's exchange rate to convert. ### Contract call ``` Accountant.getRateInQuoteSafe(wantAssetAddress) → uint256 rateInQuote ``` `rateInQuote` represents the amount of the want asset per 1e18 account shares. Pass the want asset (withdrawal token) address as the `quote` parameter. ### Conversion formula To withdraw a specific amount of the want asset: ``` sharesNeeded = (wantAmount × 1e18) / rate ``` For example, to withdraw 1,000 USDC (6 decimals): ``` wantAmount = 1000 × 1e6 = 1000000000 sharesNeeded = (1000000000 × 1e18) / rate ``` Don't forget to account for [withdrawal fees](#withdrawal-fees) when calculating the share amount. If there's a 0.5% fee, you'll need to offer slightly more shares: `sharesNeeded / (1 - feePercentage)`. *** ## Minimum Order Size Each WithdrawQueue enforces a minimum order size. Check it before submitting: ``` WithdrawQueue.minimumOrderSize() → uint256 minimumShares ``` If your `amountOffer` is below this threshold, the transaction will revert with `AmountBelowMinimum`. *** ## Withdrawal Walkthrough Confirm you hold enough account shares to withdraw. ``` BoringVault.balanceOf(yourAddress) → uint256 shareBalance ``` If you know the want asset amount, convert it to shares. ``` Accountant.getRateInQuoteSafe(wantAssetAddress) → uint256 rateInQuote ``` Then calculate: `sharesNeeded = (wantAmount × 1e18) / rateInQuote` If you already know the share amount you want to offer, skip this step. Get the FeeModule address and calculate the fee that will be deducted. ``` WithdrawQueue.feeModule() → address feeModuleAddress FeeModule.calculateOfferFees( sharesNeeded, // from Step 2 boringVaultAddress, // the share token (offer asset) wantAssetAddress, // e.g., USDC receiverAddress // where want asset goes ) → uint256 feeAmount ``` The net shares applied to the withdrawal = `sharesNeeded - feeAmount`. Display this to the user so they know the effective withdrawal amount. Ensure your order meets the minimum. ``` WithdrawQueue.minimumOrderSize() → uint256 minimumShares ``` If `sharesNeeded < minimumShares`, the order will be rejected. See if the WithdrawQueue already has permission to transfer your shares. ``` BoringVault.allowance(yourAddress, withdrawQueueAddress) → uint256 ``` If the returned value is ≥ your `sharesNeeded`, skip to Step 7. Grant the WithdrawQueue permission to transfer your account shares. ``` BoringVault.approve(withdrawQueueAddress, sharesNeeded) → bool ``` Wait for the transaction to be mined before proceeding. Call `submitOrder()` on the WithdrawQueue with empty signature params (since you used ERC-20 approval in Step 6). ``` WithdrawQueue.submitOrder({ amountOffer: sharesNeeded, wantAsset: wantAssetAddress, // e.g., USDC intendedDepositor: yourAddress, // must be msg.sender receiver: receiverAddress, // where want asset goes refundReceiver: yourAddress, // where shares go if cancelled signatureParams: { approvalMethod: 0, approvalV: 0, approvalR: bytes32(0), approvalS: bytes32(0), submitWithSignature: false, deadline: 0, eip2612Signature: 0x } }) → uint256 orderIndex ``` The return value `orderIndex` uniquely identifies your order. Save it to check status or cancel later. You can also extract `orderIndex` from the `OrderSubmitted` event in the transaction receipt logs. The account operator typically fulfills orders within 24 hours. Check the status: ``` WithdrawQueue.getOrderStatus(orderIndex) → uint8 status ``` Poll periodically (e.g., every 60 seconds) until `status ≥ 2`: * `2` (COMPLETE) — Want asset has been sent to your receiver address * `5` (COMPLETE\_REFUNDED) — Order was cancelled, shares returned * `6` (FAILED\_TRANSFER\_REFUNDED) — Transfer failed, shares returned ### Example values (withdrawing \~1,000 USDC worth of shares on Ethereum mainnet) | Parameter | Value | | ------------------- | ------------------------------------------------------- | | `amountOffer` | Calculated from exchange rate (e.g., `990099009900...`) | | `wantAsset` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` (USDC) | | `intendedDepositor` | Your wallet address | | `receiver` | Your wallet address | | `refundReceiver` | Your wallet address | | `signatureParams` | All zeros (see empty signature params table above) | *** ## Troubleshooting Your `amountOffer` is below the queue's minimum order size. Call `minimumOrderSize()` to check the threshold, and increase your amount. The `intendedDepositor` field doesn't match the transaction sender. Set it to the wallet address that's sending the transaction. The `wantAsset` address isn't enabled for this account. Query the account config to find supported withdrawal assets. Account shares haven't been approved to the WithdrawQueue. Call `BoringVault.approve()` first. Account operations are temporarily paused by the operator. Wait and retry later. You can check programmatically with `Teller.isPaused()` — see [Pause State](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/pause-state). *** ## Next Steps * [Direct Cancellations](/v1.0.0/intro/products/earn/developers/guides/direct-contract/cancellations) — Cancel a pending withdrawal order * [Account Queries & Monitoring](/v1.0.0/intro/products/earn/developers/guides/direct-contract/vault-queries/index) — Read withdrawal fees, check pause state, monitor withdrawal history * [Direct Deposits](/v1.0.0/intro/products/earn/developers/guides/direct-contract/deposits) — Deposit tokens into an account * [SDK Withdrawals Guide](/v1.0.0/intro/products/earn/developers/guides/withdrawals) — Use the Amplify SDK instead # Multi-Chain Integration Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/multi-chain Discover vault deployments across chains and submit chain-correct deposit and withdrawal transactions with AmplifyClient. `@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: 1. Pull the list of supported chains at runtime instead of hard-coding it. 2. RPC connectivity belongs to your wallet stack (wagmi / viem / Privy / Dynamic), not the SDK. 3. 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](#discovering-chains) below) — the backend can light up new chains without an SDK release. ## Discovering chains `client.amplify.vaults.list` returns vault groups, each with a `deployments[]` array describing every chain a vault is live on: ```ts theme={null} import { AmplifyClient } from '@paxoslabs/amplify-sdk' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) const { vaults } = await client.amplify.vaults.list() const supportedChainIds = new Set( 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: ```ts theme={null} const { vaults } = await client.amplify.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.amplify.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: ```ts theme={null} 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.amplify.vaults.list()` uses the **same `boringVaultAddress`** across all chains it's deployed on. Each deployment carries its own accepted assets and configuration: ```ts theme={null} const { vaults } = await client.amplify.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: ```ts theme={null} import type { Address } from 'viem' type Key = `${Address}:${number}` const deployments = new Map() 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: ```ts theme={null} 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 ```ts theme={null} 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.amplify.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.core.authorization.detect({ 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.amplify.deposit.prepare({ 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 ```ts theme={null} import type { Address, Hex } from 'viem' async function withdrawOnChain(params: { chainId: number userAddress: Address vaultAddress: Address wantAsset: Address shareAmount: string }) { const auth = await client.core.authorization.detect({ 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.amplify.withdraw.prepare({ 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`": ```ts theme={null} // User flips the chain selector from Ethereum (1) to Base (8453). await switchChain({ chainId: 8453 }) const prepared = await client.amplify.deposit.prepare({ 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: ```ts theme={null} const controller = new AbortController() const prepared = await client.amplify.deposit.prepare( { /* ... */ }, { abortSignal: controller.signal }, ) // on chain switch: controller.abort() ``` ## Listing across chains Most `list*` endpoints accept `chainId=` in the filter and paginate via `pageToken`: ```ts theme={null} async function listAllVaults() { const all: Awaited>['vaults'] = [] let pageToken: string | undefined do { const page = await client.amplify.vaults.list({ pageSize: 100, pageToken }) all.push(...page.vaults) pageToken = page.nextPageToken } while (pageToken) return all } ``` The same pattern applies to `client.amplify.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 ```ts theme={null} 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.amplify.vaults.list({ filter: 'chainId=' })` on chain-switch events. ## Related * [AI Coding Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference) — full method list. * [Deposits guide](/v1.0.0/intro/products/earn/developers/guides/deposits) * [Withdrawals guide](/v1.0.0/intro/products/earn/developers/guides/withdrawals) * [Smart wallets guide](/v1.0.0/intro/products/earn/developers/guides/smart-wallets) — submitting prepared transactions from AA wallets. * [Migrating from 0.5](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) — what changed about chain config. # Project setup Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/project-setup Install the Amplify SDK, get an API key, and instantiate the client. Install `@paxoslabs/amplify-sdk@1.0.0`, instantiate `AmplifyClient` once, and call its subclient methods. ## Requirements * Node.js `>= 24` * An API key from Paxos Labs The SDK has no required peer dependencies. Most consumers also install [`viem`](https://viem.sh) (and optionally [`wagmi`](https://wagmi.sh)) to sign and broadcast the transactions the SDK returns. ## Install ```bash theme={null} pnpm add @paxoslabs/amplify-sdk ``` ```bash theme={null} npm install @paxoslabs/amplify-sdk ``` ```bash theme={null} yarn add @paxoslabs/amplify-sdk ``` ```bash theme={null} bun add @paxoslabs/amplify-sdk ``` ### Optional signing / wallet libraries These are not pulled in by the SDK, but you'll typically install them in the consumer: ```bash theme={null} pnpm add viem ``` ```bash theme={null} pnpm add wagmi viem @tanstack/react-query ``` ## Get an API key API keys are issued through the Paxos Labs onboarding process — contact your Paxos Labs representative if you don't have one yet. Store the secret in a server-side environment variable; never bundle it into a browser. ```env theme={null} # .env.local (or your secrets manager of choice) PAXOS_LABS_API_KEY=pxl_xxxxxxxxxxxxxxxxxxxxxxxxxx ``` ## Instantiate the client Construct an `AmplifyClient` once and reuse it across requests on a server. ```ts theme={null} // lib/amplify.ts import { AmplifyClient } from '@paxoslabs/amplify-sdk' let client: AmplifyClient | undefined export function getAmplifyClient(): AmplifyClient { if (client) return client const apiKey = process.env.PAXOS_LABS_API_KEY if (!apiKey) { throw new Error('PAXOS_LABS_API_KEY is not set') } client = new AmplifyClient({ apiKey }) return client } ``` The lazy-singleton pattern throws a clear error at first use instead of at module load, which is friendlier in build environments where the secret may be absent. ### Constructor options ```ts theme={null} interface AmplifyClientOptions { /** API key (required). Sent as the `x-api-key` header on every request. */ apiKey: string /** Optional environment override. Defaults to `AmplifyEnvironment.Production`. */ environment?: AmplifyEnvironment | string /** Optional base URL override (takes precedence over `environment`). */ baseUrl?: string } ``` The SDK defaults to production (`AmplifyEnvironment.Production`). You only need to pass `environment` or `baseUrl` for non-production use cases. ## Server-side vs. client-side usage The API key authenticates every call and **must not** ship to the browser. The recommended pattern is: 1. Browser code calls your own backend (a Next.js Route Handler, a Server Action, an Express route, etc.). 2. Your backend constructs the `AmplifyClient`, calls the SDK, and returns the prepared `{ to, data, value }` payload. 3. Browser code signs and broadcasts the transaction with the user's wallet (wagmi / viem / Privy / Dynamic / etc.). ### Next.js Route Handler ```ts theme={null} // app/api/amplify/deposit/prepare/route.ts import { NextResponse } from 'next/server' import { AmplifyError } from '@paxoslabs/amplify-sdk' import { getAmplifyClient } from '@/lib/amplify' export async function POST(request: Request) { const body = (await request.json()) as { vaultAddress: string depositAsset: string depositAmount: string userAddress: string chainId: number } try { const prepared = await getAmplifyClient().deposit.prepare(body) return NextResponse.json(prepared) } catch (err) { if (err instanceof AmplifyError) { return NextResponse.json( { error: err.message }, { status: err.statusCode ?? 500 }, ) } return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } ``` ### Browser-side submission ```tsx theme={null} 'use client' import type { Address, Hex } from 'viem' import { useSendTransaction } from 'wagmi' interface PreparedTransaction { transaction: { to: Address; data: Hex; value: string } } export function DepositButton({ vaultAddress, depositAsset, depositAmount, userAddress, chainId, }: { vaultAddress: Address depositAsset: Address depositAmount: string userAddress: Address chainId: number }) { const { sendTransactionAsync } = useSendTransaction() async function onClick() { const res = await fetch('/api/amplify/deposit/prepare', { method: 'POST', body: JSON.stringify({ vaultAddress, depositAsset, depositAmount, userAddress, chainId }), }) if (!res.ok) throw new Error(await res.text()) const prepared = (await res.json()) as PreparedTransaction await sendTransactionAsync({ to: prepared.transaction.to, data: prepared.transaction.data, value: BigInt(prepared.transaction.value), chainId, }) } return } ``` ## First call A quick smoke test to confirm your key works: ```ts theme={null} import { getAmplifyClient } from '@/lib/amplify' const { vaults } = await getAmplifyClient().vaults.list({ filter: 'chainId=1' }) console.log(`Found ${vaults.length} vault(s) on mainnet`) ``` If the key is invalid or the request fails, the SDK throws `AmplifyError` (see [Errors](#errors)). ## Response format By default, `prepare*` and `cancel` calls return ABI-encoded calldata only: ```ts theme={null} const prepared = await client.amplify.deposit.prepare({ /* ... */ }) // prepared.transaction = { to, data, value, ... } ``` Pass `responseFormat: 'full'` if you want the ABI fragment, function name, and args alongside the encoded calldata — useful when you want to re-encode in your own ABI plumbing or render the call in a UI. ```ts theme={null} const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, userAddress, chainId, responseFormat: 'full', }) // prepared.transaction = { to, data, value, abi, functionName, args } ``` ## Decimals The SDK accepts and returns token amounts as **base-units decimal strings**. It never parses human-readable inputs. Convert before calling: ```ts theme={null} import { parseUnits } from 'viem' // User typed "10" intending 10 USDC (6 decimals) const depositAmount = parseUnits('10', 6).toString() // '10000000' ``` Always read `decimals()` live from the token contract (e.g. via viem's `readContract` or wagmi's `useBalance`). ## Errors Every method throws `AmplifyError` on non-2xx responses, network failures, and serialization errors. Timeouts throw `AmplifyTimeoutError`. ```ts theme={null} import { AmplifyClient, AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { await client.amplify.vaults.list() } catch (err) { if (err instanceof AmplifyTimeoutError) { // Retry, surface a timeout UI, etc. } else if (err instanceof AmplifyError) { // err.statusCode number — HTTP status // err.body unknown — parsed backend error body (when JSON) // err.message string — human-readable summary // err.rawResponse RawResponse — original Response object } else { throw err } } ``` ## Next steps * [Deposits guide](./deposits) — full deposit flow with permit and approval branches * [Withdrawals guide](./withdrawals) — `submitOrder`, fee estimation, listing, and cancel * [AI Coding Reference](../sdk-ai-reference) — every subclient method, with params and response shapes * [Migrating from 0.5.x](../migrating-from-0-5) — for upgrading an existing integration # Smart Wallets Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/smart-wallets Submit prepared Amplify transactions from smart-contract wallets (ERC-4337, Privy, Dynamic) and server-side signers. `@paxoslabs/amplify-sdk@1.0.0` is wallet-agnostic. Every `prepare` / `cancel` method returns ```ts theme={null} import type { Address, Hex } from 'viem' { transaction: { to: Address data: Hex value: string // decimal, usually "0" // optional, only with responseFormat: 'full' | 'structured' abi?: unknown functionName?: string args?: unknown[] } } ``` The SDK does not bundle, sign, or submit anything. It just produces calldata. That means you can submit Amplify transactions from any wallet that can issue a contract call — EOAs via wagmi/viem, smart-contract accounts via Privy or Dynamic, ERC-4337 bundlers via Alchemy or Pimlico, and server-side `walletClient` signers. If you arrived from the v0.5.x docs, the SDK no longer exposes smart-wallet-specific helpers, auth-method enums, or type guards. The flow is now: call `authorization.detect`, branch on `auth.method`, then submit `prepared.transaction`. See [Migrating from 0.5](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) for the full mapping. ## What the SDK gives you `prepared.transaction` is the same shape regardless of vault, chain, or wallet: ```ts theme={null} import { AmplifyClient } from '@paxoslabs/amplify-sdk' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) const prepared = await client.amplify.deposit.prepare({ vaultAddress: '0xbbbb000000000000000000000000000000000001', depositAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', depositAmount: '1000000', userAddress: '0xUserOrSmartAccount', chainId: 1, }) // prepared.transaction.{to,data,value} — submit from any wallet ``` Your job is to route `{ to, data, value }` to whichever sender the connected wallet exposes. The rest of this guide shows that routing for the wallet types Amplify integrators most often use. ## `userAddress` vs. `to` Two address fields appear on most `prepare` requests: | Field | Meaning | | ------------- | -------------------------------------------------------------------------------------------------------- | | `userAddress` | The account that submits the transaction. For EOAs, the signer. For AA, the smart-account address. | | `to` | (Optional) The address that receives the resulting shares (deposits) or assets (withdrawals settlement). | Set `to` explicitly when a session key submits the transaction but the smart account should own the shares: ```ts theme={null} const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, userAddress: sessionKeyAddress, // submits the tx to: smartAccountAddress, // owns the shares chainId, }) ``` Withdrawals expose the same idea via `intendedDepositor`, `receiver`, and `refundReceiver` — pass each explicitly when the submitter and the share/asset owner differ. See the [Withdrawals guide](/v1.0.0/intro/products/earn/developers/guides/withdrawals). ## Permit vs. approval on smart wallets For deposits you first call `client.core.authorization.detect(...)`. It returns one of three shapes: ```ts theme={null} import type { Hex } from 'viem' type AuthorizationResponseDto = | { method: 'permit'; permitData: { domain; types; value; deadline } } | { method: 'approval'; approvalTransaction: { encoded: Hex; /* … */ } } | { method: 'already_approved' } ``` * **Smart wallets that expose `signTypedData` (EIP-712)** — most Privy embedded smart wallets, Dynamic embedded wallets, and Safe — can sign EIP-2612 permits. Treat `method: 'permit'` identically to an EOA: sign the typed data, pass `permitSignature` + `permitDeadline` to `prepare`, and submit one transaction. * **Smart wallets without typed-data signing** (some session-key flows, restricted policy-engine accounts) cannot produce a permit. Fall back to the approval path: submit `approvalTransaction.encoded` to the deposit asset, wait for confirmation, then call `prepare` *without* `permitSignature` / `permitDeadline`. A defensive client handles all three branches: ```ts theme={null} const auth = await client.core.authorization.detect({ vaultAddress, tokenAddress: depositAsset, amount: depositAmount, userAddress, chainId, }) let permitSignature: Hex | undefined let permitDeadline: number | undefined if (auth.method === 'permit' && supportsTypedData(wallet)) { permitSignature = await wallet.signTypedData({ account: 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' || auth.method === 'permit') { // Submit approval — either because the token requires it, or because // this wallet can't sign EIP-712. const approvalTx = auth.method === 'approval' ? auth.approvalTransaction : await fallbackApprovalFor(depositAsset, vaultAddress, depositAmount) const hash = await wallet.sendTransaction({ to: depositAsset, data: approvalTx.encoded, chainId, }) await publicClient.waitForTransactionReceipt({ hash }) } // method === 'already_approved' → nothing to do const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, userAddress, chainId, ...(permitSignature ? { permitSignature, permitDeadline } : {}), }) ``` The same `auth.method === 'approval'` path applies to withdrawals — except `tokenAddress` is the **share token** (`vaultAddress`), and the only valid response shapes are `approval` or `already_approved`. ## Pattern 1 — Privy / Dynamic smart wallets (UserOps under the hood) Both Privy's `useSmartWallets` and Dynamic's embedded smart-account adapter expose a `sendTransaction`-style API that internally builds a UserOperation, sends it to a bundler, and resolves with a hash. From the SDK's perspective they are identical — just pass `prepared.transaction.{to,data,value}` through. ```tsx theme={null} // pseudo-code; consult Privy / Dynamic docs for the exact hook shape import { useSmartWallets } from '@privy-io/react-auth/smart-wallets' import { AmplifyClient } from '@paxoslabs/amplify-sdk' import type { Address, Hex } from 'viem' const client = new AmplifyClient({ apiKey }) function useAmplifyDeposit() { const { client: smartWallet } = useSmartWallets() return async function deposit(params: { vaultAddress: Address depositAsset: Address depositAmount: string smartAccountAddress: Address chainId: number }) { const auth = await client.core.authorization.detect({ vaultAddress: params.vaultAddress, tokenAddress: params.depositAsset, amount: params.depositAmount, userAddress: params.smartAccountAddress, chainId: params.chainId, }) const calls: { to: Address; data: Hex; value?: bigint }[] = [] if (auth.method === 'approval') { calls.push({ to: params.depositAsset, data: auth.approvalTransaction.encoded as Hex, }) } const prepared = await client.amplify.deposit.prepare({ vaultAddress: params.vaultAddress, depositAsset: params.depositAsset, depositAmount: params.depositAmount, userAddress: params.smartAccountAddress, chainId: params.chainId, // No permitSignature — smart wallet is using the approval path. }) calls.push({ to: prepared.transaction.to as Address, data: prepared.transaction.data as Hex, value: BigInt(prepared.transaction.value), }) // One user confirmation, one UserOperation — both calls bundle together. return smartWallet.sendTransaction({ calls }) } } ``` The hook signatures, `sendTransaction` parameters, and how gas sponsorship is configured are dictated by Privy and Dynamic — not by the Amplify SDK. The code above is illustrative; consult the wallet provider's docs for the authoritative API. If the asset supports permit **and** the smart wallet supports `signTypedData`, you can collapse to a single call by signing the permit first and skipping the approval step entirely. ## Pattern 2 — ERC-4337 bundlers (Alchemy, Pimlico, Biconomy) Native ERC-4337 SDKs expose batched submission as `sendTransactions({ requests })` or `sendUserOperation`. The response is a `userOpHash`, not a transaction hash: ```ts theme={null} import type { Address, Hex } from 'viem' const userOpHash = await smartAccountClient.sendTransactions({ requests: [ auth.method === 'approval' && { to: depositAsset, data: auth.approvalTransaction.encoded as Hex, }, { to: prepared.transaction.to as Address, data: prepared.transaction.data as Hex, value: BigInt(prepared.transaction.value), }, ].filter(Boolean) as { to: Address; data: Hex; value?: bigint }[], }) const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash: userOpHash, }) const txHash = receipt.receipt.transactionHash ``` Two things to remember: 1. **Do not** use wagmi's `useWaitForTransactionReceipt` with a `userOpHash` — poll the bundler instead (`waitForUserOperationReceipt`). 2. Gas sponsorship is configured on the smart-account client (paymaster), not on the prepared transaction. The SDK never sets `gasPrice` or `maxFeePerGas`. ## Pattern 3 — viem `walletClient` (EOA or AA exposed as JSON-RPC) For non-AA wallets and for AA wallets exposed through a viem `walletClient` (Safe via `safe-apps-sdk`, MetaMask Smart Account, etc.), submit directly: ```ts theme={null} import type { Address, Hex } from 'viem' import { createWalletClient, custom, parseEther } from 'viem' import { mainnet } from 'viem/chains' const walletClient = createWalletClient({ account: userAddress, chain: mainnet, transport: custom(window.ethereum), }) const prepared = await client.amplify.deposit.prepare({ /* ... */ }) const hash = await walletClient.sendTransaction({ to: prepared.transaction.to as Address, data: prepared.transaction.data as Hex, value: BigInt(prepared.transaction.value), chain: mainnet, }) ``` Permit signing uses the same client: ```ts theme={null} const signature = await walletClient.signTypedData({ account: userAddress, domain: auth.permitData.domain, types: auth.permitData.types, primaryType: 'Permit', message: auth.permitData.value, }) ``` ## Pattern 4 — Server-side signers When the signer lives on your backend (custodial flows, automation), the same `walletClient` works with an in-memory account: ```ts theme={null} import type { Address, Hex } from 'viem' import { createWalletClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains' import { AmplifyClient } from '@paxoslabs/amplify-sdk' const account = privateKeyToAccount(process.env.SIGNER_PRIVATE_KEY as Hex) const walletClient = createWalletClient({ account, chain: mainnet, transport: http(process.env.RPC_URL), }) const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, userAddress: account.address, chainId: mainnet.id, }) const hash = await walletClient.sendTransaction({ to: prepared.transaction.to as Address, data: prepared.transaction.data as Hex, value: BigInt(prepared.transaction.value), }) ``` Server-side keys must never be sent to a browser. Keep the signer behind a trusted endpoint and surface only `prepared.transaction` to clients that need to display previews. ## Error handling Smart-wallet submission can fail in three distinct layers — handle each: ```ts theme={null} import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { const prepared = await client.amplify.deposit.prepare({ /* ... */ }) await smartWallet.sendTransaction({ /* prepared.transaction */ }) } catch (err) { if (err instanceof AmplifyTimeoutError) { // SDK couldn't reach the backend in time. Retry or surface a UI hint. } else if (err instanceof AmplifyError) { // err.statusCode — backend HTTP status // err.body — parsed error body (when JSON) // err.message — human-readable summary // err.rawResponse — raw fetch Response, for debugging } else { // Wallet / bundler error — UserOp rejected, paymaster denied, etc. } } ``` ## Checklist Use the `permit` path. One UserOp / one transaction for the whole deposit. Use the `approval` path. Batch `approve` + `deposit` calls together so the user signs once. If the wallet cannot batch (rare for modern AA), submit sequentially and wait for the approval receipt before preparing the deposit. Pass the session-key address as `userAddress`, and the smart account as `to` (deposits) or `intendedDepositor` / `receiver` / `refundReceiver` (withdrawals). The session key submits the tx; the smart account owns the shares. Use a viem `walletClient` with a `privateKeyToAccount`. Keep the key out of the browser; expose only the prepared transaction shape to clients. ## Related * [Deposits guide](/v1.0.0/intro/products/earn/developers/guides/deposits) — full deposit flow with viem + wagmi. * [Withdrawals guide](/v1.0.0/intro/products/earn/developers/guides/withdrawals) — share-token approval + `submitOrder`. * [AI Coding Reference](/v1.0.0/intro/products/earn/developers/sdk-ai-reference) — every subclient method. * [Migrating from 0.5](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) — old smart-wallet helpers and their 1.0.0 equivalents. # Withdrawals Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/guides/withdrawals Submit, list, and cancel vault withdrawal orders through the WithdrawQueue. Withdrawals redeem vault share tokens for the underlying `wantAsset` through the `WithdrawQueue` contract. Unlike deposits, **there is no inline-permit path** in `1.0.0`: the share token must have an ERC-20 `approve(WithdrawQueue, amount)` allowance before `submitOrder` can pull the shares. Throughout this guide, `client` refers to a singleton `AmplifyClient` created on the server — see [Project setup](./project-setup) for the wiring. All amounts are base-units decimal strings. Withdrawals have no permit path. For the share token, `authorization.detect` always returns `method: 'approval'` or `'already_approved'` — never `'permit'`. Approve the `WithdrawQueue` before calling `submitOrder`. Call `authorization.detect` with `tokenAddress` set to the **vault address** (the share token is the vault contract). The response tells you whether you need to send an `approve` transaction or whether sufficient allowance already exists. ```ts theme={null} const auth = await client.core.authorization.detect({ vaultAddress: '0xbbbb000000000000000000000000000000000001', tokenAddress: '0xbbbb000000000000000000000000000000000001', // share token = vault amount: shareAmount, // base-units decimal string userAddress, chainId: 1, }) ``` Two possible response shapes: * `auth.method === 'already_approved'` — skip to Step 3. * `auth.method === 'approval'` — submit `auth.approvalTransaction.encoded` to the share-token address (the vault address). The `spender` baked into the calldata is the `WithdrawQueue` address. For the `approval` branch, just forward the returned transaction: ```ts theme={null} import { useSendTransaction, usePublicClient } from 'wagmi' const { sendTransactionAsync } = useSendTransaction() const publicClient = usePublicClient() if (auth.method === 'approval') { const approvalHash = await sendTransactionAsync({ to: vaultAddress, // share token = vault contract data: auth.approvalTransaction.encoded as Hex, chainId, }) await publicClient!.waitForTransactionReceipt({ hash: approvalHash }) } ``` The `spender` in the `approve` call is always the `WithdrawQueue` address, never the vault address. Using the vault address as `spender` will cause `submitOrder` to panic with `0x11` when it tries to `transferFrom` the shares. `client.amplify.withdraw.calculateFee` returns the fee the user will pay for a given offer amount. Use it both to render a preview in your UI and to short-circuit submissions that would revert on-chain. ```ts theme={null} const fee = await client.amplify.withdraw.calculateFee({ offerAmount: shareAmount, // base-units decimal string of shares to redeem wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', vaultAddress: '0xbbbb000000000000000000000000000000000001', chainId: 1, }) // fee = { // feeAmount, // base-units decimal string of `wantAsset` // offerFeePercentage: { bps, percentage }, // flatFee, // } if (BigInt(fee.feeAmount) >= BigInt(shareAmount)) { throw new Error( 'Amount is too small — fees would consume the entire withdrawal. Increase the amount and retry.' ) } ``` ```ts theme={null} const prepared = await client.amplify.withdraw.prepare({ vaultAddress: '0xbbbb000000000000000000000000000000000001', wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', shareAmount, // base units of the share token userAddress, chainId: 1, // Optional: override default depositor / receiver / refund receiver // intendedDepositor, // receiver, // refundReceiver, }) // prepared.transaction = { to: WithdrawQueue, data: submitOrder calldata, value: '0' } ``` Required fields: * `vaultAddress` — BoringVault contract address. * `wantAsset` — ERC-20 the user wants to receive on settlement. * `shareAmount` — vault shares to redeem (base-units decimal string). * `userAddress` — wallet submitting the order. Also the default `intendedDepositor`, `receiver`, and `refundReceiver`. * `chainId` — EVM chain ID. Optional fields: * `intendedDepositor` — on-chain `SubmitOrderParams.intendedDepositor`. Defaults to `userAddress`. * `receiver` — address that receives `wantAsset` on settlement. Defaults to `userAddress`. * `refundReceiver` — address that receives refunded shares if the order is cancelled. Defaults to `userAddress`. * `responseFormat` — `'encoded'` (default), `'full'`, or `'structured'`. ```ts theme={null} import type { Address, Hex } from 'viem' const tx = prepared.transaction const withdrawHash = await sendTransactionAsync({ to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), chainId, }) await publicClient!.waitForTransactionReceipt({ hash: withdrawHash }) ``` ## End-to-end example ```ts theme={null} import { AmplifyClient } from '@paxoslabs/amplify-sdk' import type { Address, Hex } from 'viem' import { createPublicClient, createWalletClient, custom, http, } from 'viem' import { mainnet } from 'viem/chains' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) async function withdraw({ userAddress, vaultAddress, wantAsset, shareAmount, chainId, }: { userAddress: Address vaultAddress: Address wantAsset: Address shareAmount: string chainId: number }) { const publicClient = createPublicClient({ chain: mainnet, transport: http() }) const walletClient = createWalletClient({ account: userAddress, chain: mainnet, transport: custom(window.ethereum), }) // 1. Check the share-token allowance const auth = await client.core.authorization.detect({ vaultAddress, tokenAddress: vaultAddress, // share token = vault contract amount: shareAmount, userAddress, chainId, }) // 2. Approve the WithdrawQueue if needed if (auth.method === 'approval') { const hash = await walletClient.sendTransaction({ to: vaultAddress, // share token = vault contract data: auth.approvalTransaction.encoded as Hex, }) await publicClient.waitForTransactionReceipt({ hash }) } // 3. Prepare const prepared = await client.amplify.withdraw.prepare({ vaultAddress, wantAsset, shareAmount, userAddress, chainId, }) // 4. Submit const tx = prepared.transaction const withdrawHash = await walletClient.sendTransaction({ to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), }) await publicClient.waitForTransactionReceipt({ hash: withdrawHash }) return withdrawHash } ``` ## Listing a user's withdrawals `client.amplify.withdraw.listRequests` supports cursor pagination and an AIP-160-style `filter` string. Available filter keys: `status` (`PENDING`, `COMPLETE`, `PENDING_REFUND`, `REFUNDED`), `chainId`, `wantAssetAddress`, `vaultAddress`, `userAddress`, `receiverAddress`, `refundReceiverAddress`, `orderIndex`, `isSubmittedViaSignature`, `isForceProcessed`, `isMarkedForRefund`, `isMarkedForRefundByUser`, `didOrderFailTransfer`. ```ts theme={null} const firstPage = await client.amplify.withdraw.listRequests({ filter: `userAddress=${userAddress} AND vaultAddress=${VAULT} AND status=PENDING`, pageSize: 25, }) // firstPage = { withdrawalRequests, nextPageToken } for (const r of firstPage.withdrawalRequests) { // r.orderIndex, r.orderAmount, r.wantAssetAddress, r.status, ... } // Paginate by passing the previous nextPageToken back in. let pageToken = firstPage.nextPageToken while (pageToken) { const next = await client.amplify.withdraw.listRequests({ filter: `userAddress=${userAddress} AND vaultAddress=${VAULT}`, pageSize: 25, pageToken, }) // ...consume next.withdrawalRequests pageToken = next.nextPageToken } ``` ## Cancelling a pending order `orderIndex` comes from a row returned by `listRequests`. Cancel returns calldata for the `WithdrawQueue.cancel` (or equivalent) call; submit it the same way as the deposit/withdrawal flows above. ```ts theme={null} import type { Address, Hex } from 'viem' const prepared = await client.amplify.withdraw.cancel({ vaultAddress: '0xbbbb000000000000000000000000000000000001', orderIndex: '42', // decimal string, from listRequests chainId: 1, }) const tx = prepared.transaction const cancelHash = await sendTransactionAsync({ to: tx.to as Address, data: tx.data as Hex, value: BigInt(tx.value), chainId, }) await publicClient!.waitForTransactionReceipt({ hash: cancelHash }) ``` ## Converting share amounts ```ts theme={null} import { parseUnits } from 'viem' // Read the share-token decimals live from the vault contract. const decimals = await publicClient.readContract({ address: VAULT, abi: [{ type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }], functionName: 'decimals', }) const shareAmount = parseUnits(userInput, decimals).toString() ``` ## Error handling ```ts theme={null} import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { await client.amplify.withdraw.prepare({ /* ... */ }) } catch (err) { if (err instanceof AmplifyTimeoutError) { // Retry, surface a timeout UI, etc. } else if (err instanceof AmplifyError) { // err.statusCode number — HTTP status (e.g. 400, 401, 500) // err.body unknown — parsed backend error body (when JSON) // err.message string — human-readable summary // err.rawResponse RawResponse — the original Response console.error('Amplify error', err.statusCode, err.message) } else { throw err } } ``` When surfacing errors to the browser, log `err.body` and `err.rawResponse` server-side and return a generic message to the client. ## Next steps * [Deposits guide](./deposits) — the inverse flow, including permit support. * [AI Coding Reference](../sdk-ai-reference) — all parameters and response shapes for every subclient. * [Migrating from 0.5.x](../migrating-from-0-5) — field-by-field rename table for `prepare`, `cancel`, and `listRequests`. # Amplify Earn Developers Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/index Integrate stablecoin yield via the Amplify SDK, direct contract calls, or the REST API. Three integration paths into Amplify Earn. Pick the one that matches your stack. TypeScript / JavaScript client. Calls the REST API for you and returns ready-to-submit transaction calldata. Any language. Call the on-chain contracts directly using your own ABI tooling. Any language. Hit the REST API directly — it returns ABI-encoded calldata your signer submits. | | SDK | Direct Contract | API Calldata | | ------------------- | --------------------------------- | ---------------------------------------- | -------------------------------------------- | | **Languages** | TypeScript / JavaScript | Any EVM-capable language | Any language with an HTTP client | | **Setup** | `pnpm add @paxoslabs/amplify-sdk` | RPC + contract addresses + ABI | API key + HTTP client | | **Calldata source** | SDK calls API on your behalf | Encode locally with viem/web3/etc. | REST API | | **Best for** | Web + Node.js apps | Mobile, non-JS backends, bespoke clients | Server builders, custodial wallets, multisig | ## Start here Install the SDK, create a client, make your first call. Vaults, share tokens, deposit + withdraw flows, decimals. End-to-end deposit flow with permit and approval paths. Submit, preview fees, list pending, and cancel orders. Every method, parameter, and response on `AmplifyClient`. Breaking changes and a side-by-side rename table. ## AI-ready references Condensed SDK surface for Cursor, Copilot, and Claude. On-chain integration reference for AI coding assistants. REST endpoint reference for AI coding assistants. # Migrating from 0.5.x to 1.0.0 Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/migrating-from-0-5 Breaking changes and a side-by-side upgrade guide for 1.0.0. `@paxoslabs/amplify-sdk@1.0.0` is a hard break from `0.5.x`. There is no compatibility shim — every integration must be updated. This page lists what changed and the smallest correct update for each surface. ## Removed APIs The following are gone in `1.0.0`: * `initAmplifySDK(...)`. * All flat-function exports (`prepareDeposit`, `prepareDepositTxData`, `prepareDepositWithPermitTxData`, `prepareDepositPermitSignature`, `prepareDepositAuthorization`, `prepareWithdrawal`, `prepareWithdrawalAuthorization`, `prepareWithdrawOrderTxData`, `prepareApproveWithdrawOrderTxData`, `prepareCancelWithdrawOrderTxData`, `prepareApproveDepositTokenTxData`). * All ABI exports. * EIP-712 helpers (`parsePermitSignature`, `toEthSignTypedDataV4`, `PERMIT_TYPES`). Use viem (`signTypedData`) directly on the typed data the SDK returns. * On-chain approval-status helpers (`isDepositSpendApproved`, `isWithdrawalSpendApproved`). * The typed-error hierarchy (`VaultNotFoundByAddressError`, `UnauthorizedVaultAccessError`, `UnsupportedChainError`, `VaultConfigIncompleteError`, …). All errors now flow through a single `AmplifyError`. * SDK-managed telemetry and logger. * Display helpers (`getVaultTVL`, `getVaultAPY`, `getMinimumMint`, `getMinimumWithdrawalOrderSize`, `getDepositCap`, `getWithdrawalFee`, `getWithdrawalRequests`, `calculateDepositFee`). The same data is available directly via the client's `vaults.*`, `withdraw.*`, and `users.*` namespaces. ## The new shape ```ts theme={null} import { AmplifyClient, AmplifyError } from '@paxoslabs/amplify-sdk' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, // base units (decimal string) userAddress, chainId, }) // Submit with viem / wagmi: // sendTransaction({ to: prepared.transaction.to, data: prepared.transaction.data, value: BigInt(prepared.transaction.value) }) ``` ## Field-by-field rename table ### Deposit | `0.5.x` | `1.0.0` | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `prepareDeposit({...})` | `client.amplify.deposit.prepare({...})` | | `prepareDepositAuthorization({...})` | `client.core.authorization.detect({ vaultAddress, tokenAddress: depositAsset, amount, userAddress, chainId })` | | `signature` | `permitSignature` | | `deadline` (`bigint`) | `permitDeadline` (`number`, Unix seconds) | | `vaultName` | removed — pass `vaultAddress` | | `yieldType` | removed | | `distributorCode` | removed | | (implicit) | `userAddress` (required) | ### Withdraw | `0.5.x` | `1.0.0` | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `prepareWithdrawal({...})` | `client.amplify.withdraw.prepare({...})` | | `prepareWithdrawalAuthorization({...})` | `client.core.authorization.detect({ vaultAddress, tokenAddress: vaultAddress, amount, userAddress, chainId })` | | `withdrawAmount` | `shareAmount` — base units of the share token | | `prepareCancelWithdrawOrderTxData({...})` | `client.amplify.withdraw.cancel({ vaultAddress, orderIndex, chainId })` | | `wantAsset` (on cancel) | removed | | `getWithdrawalFee({...})` | `client.amplify.withdraw.calculateFee({ offerAmount, wantAsset, vaultAddress, chainId })` | | `getWithdrawalRequests({...})` | `client.amplify.withdraw.listRequests({...})` | ### Vaults / reads | `0.5.x` | `1.0.0` | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `getVaults()`, `findVaultByConfig(...)`, `getVaultsByConfig(...)` | `client.amplify.vaults.list({ filter?, pageSize?, pageToken? })` | | `getSupportedAssets(...)`, `getWithdrawSupportedAssets(...)` | `client.amplify.vaults.listAssets({...})` | | `getVaultTVL(...)` | `client.amplify.vaults.getTvls({...})` | | `getVaultAPY(...)` | `client.amplify.vaults.getApys({...})` | | `getDepositCap(...)` | `client.amplify.vaults.getSupplyCaps({...})` | | `getMinimumMint(...)`, `getMinimumWithdrawalOrderSize(...)` | Read from `client.amplify.vaults.list()` deployment data (`depositSupplyCap`, `minimumWithdrawalOrderSize`). | | `calculateDepositFee(...)` | Read from `client.amplify.vaults.list()` deployment `assets[].depositFees`. | ### Users | `0.5.x` | `1.0.0` | | ------------------------ | --------------------------------------------------------- | | (custom GraphQL queries) | `client.amplify.users.getPositions({ userAddress, ... })` | ## Error handling ```ts theme={null} import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { await client.amplify.deposit.prepare({ /* ... */ }) } catch (err) { if (err instanceof AmplifyTimeoutError) { // Retry or surface a timeout state. } else if (err instanceof AmplifyError) { // err.statusCode, err.body, err.message, err.rawResponse } else { throw err } } ``` There are no longer separate error classes per failure mode. Switch on `err.statusCode` and inspect `err.body` for backend-provided error details. ## Permit deposits ```ts theme={null} const auth = await client.core.authorization.detect({ vaultAddress, tokenAddress: depositAsset, amount, userAddress, chainId, }) if (auth.method === 'permit' && auth.permitData) { const sig = await wallet.signTypedData({ domain: auth.permitData.domain, types: auth.permitData.types, primaryType: 'Permit', message: auth.permitData.value, }) const prepared = await client.amplify.deposit.prepare({ vaultAddress, depositAsset, depositAmount, userAddress, chainId, permitSignature: sig, permitDeadline: Number(auth.permitData.deadline), }) } ``` ## Withdrawals Withdrawals settle through `WithdrawQueue.submitOrder` and require a prior ERC-20 `approve(WithdrawQueue, amount)` on the share token. Call `client.core.authorization.detect({ tokenAddress: vaultAddress, ... })` to detect whether allowance is already sufficient (`method: 'already_approved'`); otherwise submit a standard `approve` transaction before calling `client.amplify.withdraw.prepare(...)`. ## Decimals Token amounts in `1.0.0` are decimal strings in base units. The SDK does not parse human-readable inputs. Always read `decimals()` live from the token contract (e.g. via viem's `readContract` / `useBalance`) and convert with `parseUnits(value, decimals).toString()` before calling the SDK. # AI Coding Reference Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/sdk-ai-reference Single-page reference for @paxoslabs/amplify-sdk 1.0.0 — optimized for LLM context windows 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 ```bash theme={null} 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`. ```bash theme={null} pnpm add viem ``` ## Construct the client ```ts theme={null} import { AmplifyClient } from '@paxoslabs/amplify-sdk' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) ``` Full constructor signature: ```ts theme={null} namespace AmplifyClient { interface Options { /** Required. Sent as the `x-api-key` header on every request. */ apiKey: string | (() => string) | (() => Promise) /** Optional environment override. Defaults to `AmplifyEnvironment.Production`. */ environment?: AmplifyEnvironment | string | (() => string) | (() => Promise) /** Optional base URL override (takes precedence over `environment`). */ baseUrl?: string | (() => string) | (() => Promise) } interface RequestOptions { timeoutInSeconds?: number // default 60 maxRetries?: number // default 2 abortSignal?: AbortSignal headers?: Record } } ``` 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. ```ts theme={null} 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 ```ts theme={null} // 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.amplify.deposit.PrepareDepositRequest type DepositResponse = Amplify.PrepareDepositResponseDto type Vault = Amplify.VaultDto type Deployment = Amplify.VaultDeploymentDto type AuthorizationResponse = Amplify.AuthorizationResponseDto ``` ## Subclient + method index `AmplifyClient` exposes one subclient per resource. Every subclient method: ```ts theme={null} client.amplify.deposit.prepare(request) client.amplify.withdraw.prepare(request) client.amplify.withdraw.cancel(request) client.amplify.withdraw.calculateFee(request) client.amplify.withdraw.listRequests(request?) client.amplify.withdraw.getVolumes(request) client.core.authorization.detect(request) client.amplify.users.getPositions(request) client.amplify.vaults.list(request?) client.amplify.vaults.listAssets(request?) client.amplify.vaults.getApys(request?) client.amplify.vaults.getTvls(request?) client.amplify.vaults.getSupplyCaps(request?) client.amplify.vaults.listCompositions(request?) client.amplify.vaults.getLiquidityShortfalls(request?) client.amplify.smartDepositAddresses.get(request) ``` Every method takes an optional second argument: `requestOptions` (`{ timeoutInSeconds?, maxRetries?, abortSignal?, headers? }`). ### `client.amplify.deposit.prepare(request)` Returns ABI-encoded calldata for a vault deposit on the `DistributorCodeDepositor`. * Request: `Amplify.amplify.deposit.PrepareDepositRequest` * Response: `Amplify.PrepareDepositResponseDto` ```ts theme={null} const { transaction } = await client.amplify.deposit.prepare({ 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.amplify.withdraw.prepare(request)` Returns calldata for `WithdrawQueue.submitOrder`. Caller must have pre-approved `deployment.withdrawQueueAddress` to spend share tokens. * Request: `Amplify.amplify.withdraw.PrepareWithdrawRequest` * Response: `Amplify.PrepareWithdrawResponseDto` ```ts theme={null} const { transaction } = await client.amplify.withdraw.prepare({ vaultAddress: '0xbbbb000000000000000000000000000000000001', wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', shareAmount: '1000000000000000000', userAddress: '0xUser', chainId: 1, // Optional, all default to userAddress // intendedDepositor: '0x...', // receiver: '0x...', // refundReceiver: '0x...', }) ``` ### `client.amplify.withdraw.cancel(request)` Returns calldata for cancelling a pending withdrawal order. Look up `orderIndex` via `listRequests`. * Request: `Amplify.amplify.withdraw.CancelWithdrawRequest` * Response: `Amplify.PrepareCancelWithdrawResponseDto` ```ts theme={null} const { transaction } = await client.amplify.withdraw.cancel({ vaultAddress: '0xbbbb000000000000000000000000000000000001', orderIndex: '42', chainId: 1, }) ``` ### `client.amplify.withdraw.calculateFee(request)` Computes the fee charged for a hypothetical withdrawal. * Request: `Amplify.CalculateFeeRequest` * Response: `Amplify.CalculateWithdrawalFeeResponseDto` ```ts theme={null} const fee = await client.amplify.withdraw.calculateFee({ offerAmount: '1000000000000000000', wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', vaultAddress: '0xbbbb000000000000000000000000000000000001', chainId: 1, }) // fee.feeAmount, fee.offerFeePercentage, fee.flatFee ``` ### `client.amplify.withdraw.listRequests(request?)` Lists withdrawal requests with cursor pagination and AIP-160 filters. * Request: `Amplify.amplify.withdraw.ListRequestsWithdrawRequest` * Response: `Amplify.WithdrawalRequestsResponseDto` ```ts theme={null} const { withdrawalRequests, nextPageToken } = await client.amplify.withdraw.listRequests({ filter: 'status=PENDING AND userAddress=0xUser AND vaultAddress=0xbbbb...0001', pageSize: 50, }) // Each item: { id, status, orderIndex, orderAmount, vaultAddress, chainId, ... } ``` ### `client.core.authorization.detect(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.core.authorization.DetectAuthorizationRequest` * Response: `Amplify.AuthorizationResponseDto` ```ts theme={null} const auth = await client.core.authorization.detect({ 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.amplify.users.getPositions(request)` Returns one row per `(user, vault, chain)` with share balance and base-asset value. * Request: `Amplify.GetPositionsRequest` * Response: `Amplify.UserPositionsResponseDto` ```ts theme={null} const { userPositions, nextPageToken } = await client.amplify.users.getPositions({ userAddress: '0xUser', filter: 'chainId=1', }) ``` ### `client.amplify.vaults.list(request?)` Returns all vaults the API key can see, grouped by vault name, with one `deployments[]` entry per chain. * Request: `Amplify.amplify.vaults.ListVaultsRequest` * Response: `Amplify.VaultsResponseDto` ```ts theme={null} const { vaults, nextPageToken } = await client.amplify.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.amplify.vaults.listAssets(request?)` Per-asset capability listing across all visible vaults. * Request: `Amplify.ListAssetsRequest` * Response: `Amplify.VaultAssetsResponseDto` ```ts theme={null} const { vaultAssets } = await client.amplify.vaults.listAssets({ filter: 'depositable=true AND chainId=1', }) ``` ### `client.amplify.vaults.getApys(request?)` Historical + current APY series. * Request: `Amplify.GetApysRequest` * Response: `Amplify.VaultApysResponseDto` ```ts theme={null} const { vaultApys } = await client.amplify.vaults.getApys({ filter: 'vaultAddress=0xbbbb...0001', lookback: '604800s', // 7 days interval: '86400s', // 1 day }) ``` ### `client.amplify.vaults.getTvls(request?)` Historical + current TVL series, optionally aggregated across chains. * Request: `Amplify.GetTvlsRequest` * Response: `Amplify.VaultTvlsResponseDto` ```ts theme={null} const { vaultTvls } = await client.amplify.vaults.getTvls({ filter: 'chainId=1 AND vaultAddress=0xbbbb...0001', lookback: '604800s', aggregate: false, }) ``` ### `client.amplify.vaults.getSupplyCaps(request?)` Current `totalSupplyInBase`, `supplyCap`, and `percentageFilled` per `(vault, chain)`. * Request: `Amplify.GetSupplyCapsRequest` * Response: `Amplify.SupplyCapsResponseDto` ```ts theme={null} const { supplyCaps } = await client.amplify.vaults.getSupplyCaps({ filter: 'vaultAddress=0xbbbb...0001 AND chainId=1', }) ``` ### `client.amplify.smartDeposits.getAddress(request)` Returns a deterministic Smart Deposit Address for routing on-ramp funds directly into a vault. * Request: `Amplify.amplify.SmartDepositAddressRequestDto` * Response: `Amplify.SmartDepositAddressResponseDto` ```ts theme={null} const { smartDepositAddress } = await client.amplify.smartDeposits.getAddress({ vaultAddress: '0xbbbb000000000000000000000000000000000001', userDestinationAddress: '0xUser', inputToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC chainId: 1, // Optional: generate unique addresses per customer // customerId: 'user_abc123', }) // smartDepositAddress: '0x...' — deterministic address to receive deposits ``` When `customerId` is provided, each unique value produces a distinct deposit address even when other parameters are identical. The `customerId` is included in webhook payloads for reconciliation. Must contain only alphanumeric characters, hyphens, and underscores (max 256 characters). ## Error handling Every SDK method throws on failure. Two error classes only: ```ts theme={null} 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: ```ts theme={null} import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' try { await client.amplify.deposit.prepare(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 authorization.detect for non-2612 tokens break default: // 5xx or unknown — surface to user, log details } } else { throw err } } ``` Retry helper that only retries timeouts: ```ts theme={null} async function retryOnTimeout(fn: () => Promise, attempts = 3): Promise { 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) ```ts theme={null} 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.core.authorization.detect({ 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.amplify.deposit.prepare({ 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. ```ts theme={null} import type { Address, Hex } from 'viem' async function withdraw(params: { vaultAddress: Address withdrawQueueAddress: Address // from client.amplify.vaults.list() wantAsset: Address shareAmount: string // share token base units userAddress: Address chainId: number }) { // 1) Authorize the WithdrawQueue to spend shares. // `authorization.detect` returns 'approval' or 'already_approved' for share tokens. const auth = await client.core.authorization.detect({ 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.amplify.withdraw.prepare({ 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 ```ts theme={null} 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.amplify.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.amplify.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.amplify.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 `withdraw.prepare` when the fee would consume the offer. * **Permit is for deposit assets only.** Share tokens never return `method: 'permit'` from `authorization.detect`. 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.amplify.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`. # Troubleshooting Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/developers/troubleshooting Common issues and fixes when integrating @paxoslabs/amplify-sdk 1.0.0 Practical "I hit X, what's wrong" guide for the 1.0.0 SDK. Every recipe assumes you have an `AmplifyClient` constructed like this: ```ts theme={null} import { AmplifyClient } from '@paxoslabs/amplify-sdk' const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY!, }) ``` If you're upgrading from 0.5.x, start with the [migration guide](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) — the public API has changed substantially. **Cause:** Package isn't installed, or it's installed but TypeScript can't resolve it. **Fix:** ```bash theme={null} pnpm add @paxoslabs/amplify-sdk # or: npm i @paxoslabs/amplify-sdk # or: yarn add @paxoslabs/amplify-sdk ``` There are no required peer dependencies — the SDK ships its own HTTP layer. You still need a wallet/RPC client (e.g. `viem`, `wagmi`, `ethers`) to submit the calldata returned by `prepare` / `prepare`, but those are your choice. If installation succeeds but the import still fails, see "Types resolve to `any`" below — it's almost always a `moduleResolution` issue. **Cause:** API key is missing, malformed, or wrong for the environment you're hitting. The SDK sends the key as the `x-api-key` header on every request. The value comes from whatever you passed as `apiKey` in the constructor. **Fix:** 1. Confirm the env var is loaded before constructing the client: ```ts theme={null} if (!process.env.PAXOS_LABS_API_KEY) { throw new Error('PAXOS_LABS_API_KEY is not set') } const client = new AmplifyClient({ apiKey: process.env.PAXOS_LABS_API_KEY, }) ``` 2. Catch the error and inspect the response body — the backend usually says exactly what's wrong: ```ts theme={null} import { AmplifyError } from '@paxoslabs/amplify-sdk' try { await client.amplify.vaults.list() } catch (err) { if (err instanceof AmplifyError && err.statusCode === 401) { console.error('Auth failed:', err.body) } } ``` 3. If you're hitting a non-production environment, confirm the key was issued for that environment — production and staging keys are not interchangeable. **Cause:** API key is valid, but the account associated with it doesn't have access to the vault or operation you're calling. **Fix:** Verify the vault is enabled for your API key. `client.amplify.vaults.list()` only returns vaults your key can see, so a 403 on `prepare` for a vault that doesn't appear in `list()` is the expected behaviour — request access from your Paxos Labs contact. **Cause:** Almost always a bad input. The most common offenders, in order: * **Bad `vaultAddress`** — must be `0x` + 40 hex chars (the BoringVault contract address). Get it from `client.amplify.vaults.list()` → `deployments[].boringVaultAddress`. * **Wrong `chainId`** — the vault isn't deployed on that chain. Each `VaultDto.deployments[]` entry has its own `chainId`; pass the one that matches your wallet's network. * **`depositAmount` in human units instead of base units** — `depositAmount` is a **decimal string in base units**. `"1.5"` USDC is wrong; `"1500000"` (1.5 USDC at 6 decimals) is right. * **Missing `userAddress`** — required even when `to` is also passed. **Fix:** ```ts theme={null} import type { Amplify } from '@paxoslabs/amplify-sdk' import { parseUnits } from 'viem' const request: Amplify.amplify.deposit.PrepareDepositRequest = { vaultAddress: vault.boringVaultAddress, depositAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC depositAmount: parseUnits('1.5', 6).toString(), // base units userAddress: wallet.account.address, chainId: vault.chainId, } const { transaction } = await client.amplify.deposit.prepare(request) ``` **Cause:** `wantAsset` is not an asset this vault redeems to on this chain. A vault accepts deposits in one set of assets and pays out withdrawals in another. The withdrawable set is on each vault's `assets[]` entry where `withdrawable: true`. **Fix:** ```ts theme={null} const { vaults } = await client.amplify.vaults.list({ filter: 'chainId=1' }) const vault = vaults .flatMap((v) => v.deployments) .find((d) => d.boringVaultAddress.toLowerCase() === vaultAddress.toLowerCase()) const withdrawableAssets = vault?.assets .filter((a) => a.withdrawable) .map((a) => a.assetAddress) ``` Panic `0x11` is an arithmetic underflow. On the withdraw path it has two distinct causes — fix the right one. **Cause 1 — wrong approval spender.** The single most common mistake is approving the **vault address** (the share token itself) as the spender. The spender must be the **`WithdrawQueue`** address from the same vault. **Fix:** Read `vault.withdrawQueueAddress` and pass that as the spender on a standard ERC-20 `approve`: ```ts theme={null} import { erc20Abi } from 'viem' const vault = /* ...from client.amplify.vaults.list() */ await wallet.writeContract({ address: vault.boringVaultAddress, // share token abi: erc20Abi, functionName: 'approve', args: [vault.withdrawQueueAddress, shareAmount], }) ``` EIP-2612 permit is **not supported** for vault shares — `client.core.authorization.detect` with `tokenAddress: vaultAddress` returns `method: 'approval'` or `method: 'already_approved'`. There is no permit-signature path on the withdraw flow. **Cause 2 — fee consumes the entire offer.** When the offer-fee percentage plus the flat fee adds up to ≥ `shareAmount`, the post-fee math inside `submitOrder` underflows and panics. Common on small redemptions of vaults that carry a non-trivial flat fee. **Fix:** Call `client.amplify.withdraw.calculateFee` before `prepare` and bail out when `feeAmount >= shareAmount`: ```ts theme={null} const fee = await client.amplify.withdraw.calculateFee({ offerAmount: shareAmount, wantAsset, vaultAddress, chainId, }) if (BigInt(fee.feeAmount) >= BigInt(shareAmount)) { throw new Error('Amount is too small — fees would consume the entire withdrawal.') } ``` **Cause:** The token doesn't implement EIP-2612 (no `permit`, `nonces`, or `DOMAIN_SEPARATOR`), or it implements a non-standard variant the backend can't match. The SDK returns `method: 'approval'` instead of throwing for tokens it knows can't permit. A 422 here means the backend tried and got an unexpected result — fall back to a standard `approve` call. **Fix:** ```ts theme={null} import { AmplifyError } from '@paxoslabs/amplify-sdk' try { const auth = await client.core.authorization.detect({ /* ... */ }) // auth.method === 'permit' | 'approval' | 'already_approved' } catch (err) { if (err instanceof AmplifyError && err.statusCode === 422) { // Token can't permit — do a plain approve(spender, amount) yourself. } else { throw err } } ``` **Cause:** The request exceeded `requestOptions.timeoutInSeconds` (default `60`). Usually a transient network or backend slowness. **Fix:** Retry with backoff. Each method accepts a per-call `requestOptions.maxRetries` (default `2`); set a higher cap when you control retry budget yourself: ```ts theme={null} import { AmplifyTimeoutError } from '@paxoslabs/amplify-sdk' async function withRetry(fn: () => Promise, attempts = 3): Promise { 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 } const positions = await withRetry(() => client.amplify.users.getPositions( { userAddress }, { timeoutInSeconds: 30, maxRetries: 0 }, // we manage retries ourselves ), ) ``` **Cause:** For an Amplify vault, the depositor token, base token, and share token must all share the same number of `decimals`. The vault math collapses if they don't, and an on-chain deposit will revert. **Fix:** Read `decimals()` from each token via viem (or your wallet client) and compare against the vault's configured assets: ```ts theme={null} import { erc20Abi } from 'viem' const [depositDecimals, shareDecimals] = await Promise.all([ publicClient.readContract({ address: depositAsset, abi: erc20Abi, functionName: 'decimals', }), publicClient.readContract({ address: vault.boringVaultAddress, abi: erc20Abi, functionName: 'decimals', }), ]) if (depositDecimals !== shareDecimals) { throw new Error( `Decimal mismatch: depositAsset=${depositDecimals} shareToken=${shareDecimals}`, ) } ``` If they don't match, you've picked the wrong vault for this asset — check `client.amplify.vaults.listAssets({ filter: 'depositable=true' })` for valid pairings. **Cause:** Almost always a missing/invalid API key or a filter that excludes everything. **Fix:** 1. Call without a filter to see what your key has access to: ```ts theme={null} const { vaults } = await client.amplify.vaults.list() console.log(vaults.map((v) => v.name)) ``` 2. If that's empty, the key has zero vault access — see "401 Unauthorized" / "403 Forbidden". 3. If non-empty, narrow with a filter that matches the vault you want. AIP-160-style filters use `AND` and `=`: ```ts theme={null} await client.amplify.vaults.list({ filter: 'chainId=1 AND inDeprecation=false', }) ``` **Cause:** `tsconfig.json` `moduleResolution` is set to `node` (the legacy CJS resolver) and can't see the package's `exports` map. **Fix:** Set `moduleResolution` to `bundler` (Vite/Next/most modern toolchains) or `node16` / `nodenext`: ```jsonc theme={null} { "compilerOptions": { "moduleResolution": "bundler", "module": "esnext", "target": "es2022" } } ``` Then restart your TS server. `import type { Amplify } from '@paxoslabs/amplify-sdk'` should resolve to a namespace of DTOs. The SDK draws a clean line between runtime and types: ```ts theme={null} // Runtime — the client class and error classes import { AmplifyClient, AmplifyError, AmplifyTimeoutError, } from '@paxoslabs/amplify-sdk' // Types — request/response DTOs only import type { Amplify } from '@paxoslabs/amplify-sdk' type Req = Amplify.amplify.deposit.PrepareDepositRequest type Res = Amplify.PrepareDepositResponseDto type Vault = Amplify.VaultDto ``` `Amplify` is a type-only namespace re-export. Importing it as a value (`import { Amplify }`) works at runtime but is meaningless — there are no runtime properties on it. Use `import type` to make intent obvious and to let the bundler tree-shake it. The 1.0.0 SDK is a clean break — `initAmplifySDK`, `LogLevel`, every flat function export (`prepare`, `prepare`, `getVaultsByConfig`, etc.), every typed error class, and all ABI/EIP-712 helpers are gone. Everything now hangs off `AmplifyClient` subclients, and the only error types are `AmplifyError` and `AmplifyTimeoutError`. See [Migrating from 0.5.x](/v1.0.0/intro/products/earn/developers/migrating-from-0-5) for a function-by-function mapping. ## Inspecting an `AmplifyError` Every non-2xx response, network failure, and JSON parse error throws `AmplifyError`. The shape is small and stable: ```ts theme={null} import { AmplifyError } from '@paxoslabs/amplify-sdk' try { await client.amplify.deposit.prepare(request) } catch (err) { if (err instanceof AmplifyError) { err.statusCode // number | undefined — HTTP status when applicable err.body // unknown — parsed JSON body when the server returned JSON err.rawResponse // RawResponse | undefined — original fetch Response metadata err.message // string — human-readable summary } } ``` `body` is `unknown` because the backend may return any JSON shape. Narrow defensively before reading fields off it — never destructure blind. ## Getting help If a request is failing in a way none of the recipes above explain, capture the full `AmplifyError` (including `statusCode`, `body`, and the request input that triggered it) and reach out to [support@paxoslabs.com](mailto:support@paxoslabs.com). # Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/earn/product-overview Enable your users to put their idle assets to work with native yield on stablecoins. # Earn Enable your users to put their idle assets to work. Earn is the yield module of the Paxos Labs Amplify suite — let your users deposit stablecoins, earn yield, and withdraw at any time through a simple SDK integration. ## Program Types Paxos Labs offers several flagship stablecoin yield programs: * **Treasury:** Deploys stablecoins into tokenized assets that generate yield from U.S. government treasury bills. This is the most conservative yield program, where assets are only deployed into highly trusted vehicles such as Paxos' USDG. Currently generates **3–4%** yield. * **Core:** Generates yield from onchain lending markets that are highly vetted, managed by trusted counterparties, and accept blue-chip collateral (e.g., BTC, ETH). Currently generates **5–7%** yield. * **Frontier:** Generates yield from onchain lending markets that accept additional collateral assets outside BTC and ETH, including derivative assets such as Pendle. While introducing higher risk, Paxos Labs conducts extensive diligence to selectively deploy into these higher yielding strategies. Typically generates **8–12%** yield. * **Compliant Core:** Screens all deposits against sanctioned and blacklisted addresses to ensure funds remain compliant. Uses the same markets as Core but incurs slightly higher costs, resulting in yields marginally below Core. ## Supported Assets and Networks | Asset | Network | Status | | ----- | -------- | ----------- | | USDC | Ethereum | ✅ Supported | | USDT | Ethereum | ✅ Supported | | USDG | Ethereum | ✅ Supported | | PYUSD | Ethereum | ✅ Supported | | USDC | Base | ✅ Supported | ## Enterprise Console All integration partners receive access to the Paxos Labs Enterprise Console. The console has two key interfaces: * **API Keys:** Generate, view, and manage the API keys used to authenticate your integration with the Paxos Labs Amplify APIs. * **Amplify Accounts:** A high-level overview of each Amplify account — view TVL, APY, and withdraw activity, and initiate deposits or withdrawals directly from the console. # Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/mint/overview Issue, manage, and scale fully backed branded stablecoins native to your ecosystem. # Mint Issue, manage, and scale fully backed **branded stablecoins** native to your ecosystem. Mint is the stablecoin issuance module of the Paxos Labs Amplify suite — integrate minting, redemption, and reserve management directly into your application. * Mint and redeem * Supported backing assets * Card settlement * Yield distribution *** ## Key Components of Stablecoin Integration When building your branded stablecoin, your integration connects to three core systems: 1. **Minting and Redemption**\ Handle issuance and redemption of your stablecoin programmatically through Paxos Labs APIs. 2. **Reserve and Custody Management**\ Ensure all issued tokens are fully backed by high-quality reserves. 3. **Compliance and Controls**\ Maintain transparency and compliance screening via Paxos Labs' embedded monitoring and reporting systems. *** ## Architecture Overview Building on Paxos Labs' infrastructure provides: * **Full Reserve Backing** — All tokens are 1:1 backed by trusted reserve assets. * **Monetization** — Capture the economics on your platform's dollars. * **Operational Automation** — Integrate issuance, redemption, and reconciliation through a unified API. *** ## Technical Integration Outline A typical branded stablecoin setup includes: ### 1. Initialize SDK and Configure API Keys Install the Paxos SDK or REST API client and configure credentials for your program.\ Define token parameters (name, symbol, supply model). ### 2. Minting Flow Enable authorized issuers to mint new tokens through the Paxos API.\ Each mint request triggers a corresponding fiat funding verification event. ### 3. Redemption Flow Allow users or counterparties to redeem tokens back to fiat.\ The Paxos API automates reserve reconciliation and redemption settlement. ### 4. Reserve Transparency Use reporting endpoints to fetch reserve composition and reconciliation status.\ Integrate reserve dashboards or disclosures in your frontend. *** ## Supported Reserve Assets | Asset | Issuer | | ----- | ------ | | USDG | Paxos | | USDC | Circle | | PYUSD | Paxos | | USDT | Tether | # Developer Guide Source: https://developers.paxoslabs.com/v1.0.0/intro/products/paxg/developers Integrate PAXG swaps into your application via REST API. # PAXG Swap Integration This guide walks through integrating the PAXG swap REST API into your application. Currently only available via REST API. ## Prerequisites * A Paxos Labs API key * An Ethereum mainnet RPC endpoint * A wallet library (viem, ethers, wagmi, etc.) ## Supported Token Pairs PAXG must be either the `offerAsset` (sell) or `wantAsset` (buy) in every swap request. Supported stablecoins for the other side of the trade: | Token | Address | | ----- | -------------------------------------------- | | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | | PYUSD | `0x6c3ea9036406852006290770BEdFcAbA0e23A0e8` | | USDG | `0xe343167631d89B6Ffc58B88d6b7fB0228795491D` | ## Getting a Swap Quote Request a quote by calling the `/v1/paxg/swapQuotes` endpoint: ```typescript theme={null} const response = await fetch( 'https://api.paxoslabs.com/v1/paxg/swapQuotes?' + new URLSearchParams({ chainId: '1', offerAsset: '0x45804880De22913dAFE09f4980848ECE6EcbAf78', // PAXG wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC offerAmount: '1000000000000000000', // 1 PAXG in wei userAddress: '0x...', // User's wallet address }), { headers: { 'x-api-key': 'pxl__', }, } ); const quote = await response.json(); ``` ## Response Structure The quote response includes: ```typescript theme={null} interface SwapQuoteResponse { transaction: { to: string; // Contract to send the transaction to data: string; // Encoded swap calldata value: string; // ETH value (usually "0" for token swaps) gas: string; // Estimated gas limit gasPrice: string; // Suggested gas price }; buyAmount: string; // Expected output amount minBuyAmount: string; // Minimum output (with slippage) allowanceTarget: string; // Address to approve tokens to currentAllowance?: string; // User's current allowance (if any) } ``` ## Handling Token Approvals Before executing the swap, ensure the user has approved sufficient tokens: ```typescript theme={null} import { erc20Abi } from 'viem'; // Check if approval is needed const needsApproval = BigInt(quote.currentAllowance ?? '0') < BigInt(offerAmount); if (needsApproval) { // Request approval const approveTx = await walletClient.writeContract({ address: offerAsset, abi: erc20Abi, functionName: 'approve', args: [quote.allowanceTarget, offerAmount], }); // Wait for approval confirmation await publicClient.waitForTransactionReceipt({ hash: approveTx }); } ``` ## Executing the Swap Once approved, execute the swap using the provided calldata: ```typescript theme={null} const swapTx = await walletClient.sendTransaction({ to: quote.transaction.to, data: quote.transaction.data, value: BigInt(quote.transaction.value), gas: BigInt(quote.transaction.gas), }); const receipt = await publicClient.waitForTransactionReceipt({ hash: swapTx }); ``` ## Using Permit2 Authorization For gasless approvals, use the `permit2` auth method: ```typescript theme={null} const response = await fetch( 'https://api.paxoslabs.com/v1/paxg/swapQuotes?' + new URLSearchParams({ chainId: '1', offerAsset: '0x45804880De22913dAFE09f4980848ECE6EcbAf78', wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', offerAmount: '1000000000000000000', userAddress: '0x...', authMethod: 'permit2', // Use Permit2 }), { headers: { 'x-api-key': 'pxl__', }, } ); const quote = await response.json(); // The response includes EIP-712 typed data for signing if (quote.permit2) { const signature = await walletClient.signTypedData({ domain: quote.permit2.eip712.domain, types: quote.permit2.eip712.types, primaryType: quote.permit2.eip712.primaryType, message: quote.permit2.eip712.message, }); // Include signature when executing the swap // (implementation depends on your setup) } ``` ## Error Handling The API returns standard HTTP error codes: | Status | Description | | ------ | -------------------------------------------- | | 400 | Invalid parameters or insufficient liquidity | | 503 | DEX aggregator temporarily unavailable | Example error response: ```json theme={null} { "error": { "code": 400, "message": "No liquidity available for this swap. Try a smaller amount or different token pair.", "status": "INVALID_ARGUMENT" } } ``` Always validate the `minBuyAmount` before executing to protect against slippage. Consider showing users the expected output and allowing them to set slippage tolerance. # Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/paxg/overview Swap PAXG (Pax Gold) on Ethereum mainnet via DEX aggregators. # PAXG Swaps The PAXG Swap API enables buying and selling of PAXG (Pax Gold) tokens on Ethereum mainnet. The API returns ready-to-execute transaction calldata for swaps via DEX aggregators, handling liquidity sourcing and routing automatically. Currently only available via REST API. ## Supported Networks | Network | Chain ID | | ---------------- | -------- | | Ethereum mainnet | 1 | ## Supported Tokens The following tokens can be used as the offer or want asset: | Token | Address (Ethereum) | | ----- | -------------------------------------------- | | PAXG | `0x45804880De22913dAFE09f4980848ECE6EcbAf78` | | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | | PYUSD | `0x6c3ea9036406852006290770BEdFcAbA0e23A0e8` | | USDG | `0x2a54bA2964C8Cd459Dc568853F79813a60761B58` | ## How It Works 1. **Request a quote** — Call the swap quote endpoint with the offer asset, want asset, amount, and user address 2. **Review the quote** — The response includes the expected output amount, transaction calldata, and any required token approvals 3. **Execute the swap** — Submit the transaction using the provided calldata ## Authorization Methods The API supports two authorization methods for token approvals: * **`allowance_holder`** (default) — Standard ERC-20 approve flow using the AllowanceHolder contract * **`permit2`** — Gasless approval using EIP-712 signed permits For most use cases, the default `allowance_holder` method is recommended as it's simpler to implement. # Developers Source: https://developers.paxoslabs.com/v1.0.0/intro/products/smart-deposit-routing/developers Integrate Smart Deposit Routing into your application. # Developers Integrate Smart Deposit Routing into your application to automatically allocate deposits across supported destinations. *** ## Technical Integration Outline A typical Smart Deposit Routing setup includes: ### 1. Initialize SDK and Configure API Keys Install the Paxos Labs SDK or REST API client and configure credentials for your program. Define the accounts, input tokens, and chains you intend to support. ### 2. Register a Webhook URL Register an HTTPS webhook URL in the [Paxos Labs Console](https://console.paxoslabs.com) and subscribe to the smart deposit event types ([event reference](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing)). Follow the [webhook quick start](/v1.0.0/intro/webhooks/quickstart) to store your signing secret (starts with `pxlwh_`), subscribe to every event starting with `smart_deposit`, and verify a test delivery. You'll use these events to keep your UI in sync without watching the chain yourself. ### 3. Generate a Deposit Address Call the Smart Deposit Routing API to generate a deterministic address for a given end-user, account, input token, and chain. The same inputs always produce the same address, so addresses can be safely re-derived on demand. #### Optional Customer Identifier Pass the optional `customerId` parameter to **generate unique deposit addresses** per customer: ```json theme={null} POST /v2/amplify/smartDepositAddresses { "userDestinationAddress": "0xDestinationAddress", "vaultAddress": "0xVaultAddress", "inputToken": "0xUSDCAddress", "chainId": 1, "customerId": "user_abc123" } ``` When `customerId` is provided: * Each unique `customerId` produces a distinct deposit address, even when other parameters are identical * The `customerId` is included in webhook payloads for reconciliation * Must contain only alphanumeric characters, hyphens, and underscores (max 256 characters) When `customerId` is omitted, the address is derived from `(userDestinationAddress, vaultAddress, inputToken, chainId)` alone. ### 4. Hand the Address to Your On-Ramp Provider Pass the generated address to your on-ramp provider as the destination for your end-user's fiat purchase. The end-user completes their fiat payment inside your app exactly as they would for any on-ramp — no additional signatures or steps required. ### 5. React to Webhook Events Every event for a deposit carries the same `deposit_id`, so you can track it across its lifecycle. A deposit follows one of two paths: `pending` → `confirmed` → `finalized`, or `pending` → `failed` (failed deposits never emit `finalized`). If a failure leaves funds `retained` in the Smart Deposit Address, any automatic retry starts a new lifecycle with a new `deposit_id`. See [tracking a deposit across events](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#tracking-a-deposit-across-events). The first time you see a `deposit_id` is the `pending` event — deposits are detected on-chain, so the ID is never returned by an API call. On `pending`, resolve your end-user from `customer_id` (or `smart_deposit_address`), create your internal deposit record keyed by `deposit_id`, and let every later event update that record. When you receive the `smart_deposit.deposit.confirmed` webhook, notify your end-user that their on-ramped funds are now earning yield! Or, if you receive a `smart_deposit.deposit.failed` event, read the `remediation` field to decide what to tell your end-user, and the `error` field for the underlying reason: * `refunded` — the deposit couldn't proceed, so we sent the stablecoins back to the end-user's destination address. Tell them their funds were returned; `forward_tx_hash` is the on-chain proof. * `recovered` — the end-user cannot legally hold the token (sanctions screening failure, blacklisted or frozen address), so we swept the stablecoins to a Paxos Labs recovery wallet. Their funds were **not** returned — escalate through your compliance process. * `retained` — we encountered an unexpected error and the funds remain in the Smart Deposit Address. Rest assured, we have been alerted and are investigating any issues of this category. Present this to your end-user as *delayed*, not failed — a successful retry arrives as a fresh `pending` → `confirmed` sequence under a **new** `deposit_id`. Branch on `remediation`, not on `error` or `forward_tx_hash` — `error` is free-form prose meant for humans and may change at any time. See the [Smart Deposit Routing Events](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing) reference for payloads, and the [event catalog](/v1.0.0/intro/webhooks/event-catalog#common-patterns) for handler examples. # Product Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/smart-deposit-routing/product-overview Turn fiat on-ramp deposits into yield-bearing positions in a single step. # Smart Deposit Routing Turn your existing fiat on-ramp into a one-step path to yield. Smart Deposit Routing issues a dedicated deposit address per end-user for your on-ramp provider to settle into directly. Paxos Labs handles compliance screening and yield-bearing Account deposits on behalf of the end-user, providing you with webhook-driven status updates every step of the way. # Transit API Reference Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/developers/api-reference REST API reference for cross-chain Transit order submission. ## Base URL ``` Base URL: https://api.paxoslabs.com ``` All Transit endpoints are public and do not require authentication. ## Response Format The `/v1/transit/orders/quote` endpoint accepts a `responseFormat` query parameter: | `responseFormat` | `data` (hex) | `abi` / `functionName` / `args` | Use when | | --------------------- | ------------ | ------------------------------- | ------------------------------------------------------- | | `encoded` *(default)* | Yes | No | Signer consumes raw `data` (`eth_sendTransaction`, HSM) | | `full` | Yes | Yes | Debugging; need raw + decoded views | | `structured` | No | Yes | Encode locally (viem `encodeFunctionData`) | *** ## Order Quote `GET /v1/transit/orders/quote` Returns everything needed to submit a `submitOrder` transaction on-chain: the TransitStation address, ABI-encoded calldata, and ETH value for cross-chain messaging. ### Request Parameters | Parameter | Type | Required | Description | | ----------------------- | ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------- | | `userAddress` | hex string | Yes | Wallet address receiving the want asset on the destination chain | | `offerAmount` | decimal string | Yes | Amount of offer asset in base units. Minimum \$35 USD equivalent. | | `offerAsset` | hex string | Yes | Token contract address on source chain (debited) | | `wantAsset` | hex string | Yes | Token contract address on destination chain (credited) | | `sourceChainId` | integer | Yes | EVM chain ID of source chain (where tx is submitted) | | `destinationChainId` | integer | Yes | EVM chain ID of destination chain | | `permitSignature` | hex string (130 chars) | Conditional | EIP-2612 permit signature. Required if `permitDeadline` is provided. | | `permitDeadline` | integer | Conditional | Unix timestamp. Required if `permitSignature` is provided. | | `integratorFee` | decimal string | Conditional | Fee in offer-asset base units. Required if `integratorFeeReceiver` is provided. | | `integratorFeeReceiver` | hex string | Conditional | Address to receive integrator fee. Required if `integratorFee` is provided. | | `distributorCode` | hex string (64 chars) | Optional | 32-byte code tagging order source (emitted on-chain). Defaults to organization's configured distributor code. | | `responseFormat` | enum | Optional | `encoded` (default), `full`, or `structured` | ### Response ```json theme={null} { "transaction": { "to": "0x...", "data": "0x...", "value": "1500000000000000", "abi": [], "functionName": "submitOrder", "args": [] }, "amountOut": "49990000", "protocolFee": "10000", "integratorFee": "0", "totalFees": "10000", "estimatedLatencyMs": 300000 } ``` | Field | Description | | -------------------------- | ------------------------------------------------------------------------------- | | `transaction.to` | TransitStation contract address on source chain | | `transaction.data` | ABI-encoded calldata (present when `responseFormat` is `encoded` or `full`) | | `transaction.value` | ETH value to send (decimal string, wei). Covers cross-chain messaging fee. | | `transaction.abi` | Solidity ABI fragment (present when `responseFormat` is `full` or `structured`) | | `transaction.functionName` | `submitOrder` or `submitOrderWithPermit` | | `transaction.args` | Positional arguments (BigInt values as decimal strings) | | `amountOut` | Net credit in want-asset base units after fees | | `protocolFee` | Protocol fee in offer-asset base units | | `integratorFee` | Integrator fee in offer-asset base units (`"0"` when none) | | `totalFees` | Sum of `protocolFee` + `integratorFee` | | `estimatedLatencyMs` | Estimated delivery time in milliseconds (undefined if no SLA data) | ### On-Chain Structs The `args` array contains the Quote struct and signature: ```solidity theme={null} struct Route { uint32 destEID; address offerAsset; address wantAsset; } struct Quote { Route route; uint256 offerAmount; address receiver; uint256 protocolFee; uint256 integratorFee; address integratorFeeReceiver; bytes32 distributorCode; uint256 deadline; bytes32 salt; } function submitOrder(Quote quote, bytes signature) payable returns (bytes32 uuid) function submitOrderWithPermit( Quote quote, bytes signature, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s ) payable returns (bytes32 uuid) ``` ### Example ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/orders/quote?\ userAddress=0x1234567890abcdef1234567890abcdef12345678&\ offerAmount=50000000&\ offerAsset=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&\ wantAsset=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&\ sourceChainId=1&\ destinationChainId=4663" \ ``` ### Code Example (viem) ```ts theme={null} import { createWalletClient, http } from 'viem' import { mainnet } from 'viem/chains' const BASE = 'https://api.paxoslabs.com' // 1. Fetch quote calldata const url = new URL(`${BASE}/v1/transit/orders/quote`) url.searchParams.set('userAddress', account.address) url.searchParams.set('offerAmount', '50000000') // $50 USDC url.searchParams.set('offerAsset', USDC_ETH) url.searchParams.set('wantAsset', USDG_RH) url.searchParams.set('sourceChainId', '1') url.searchParams.set('destinationChainId', '4663') const { transaction: tx } = await fetch(url).then((r) => r.json()) // 2. Submit order (value covers messaging fee) await walletClient.sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), chain: mainnet, account, }) ``` *** ## Routes `GET /v1/transit/routes` Lists available Transit routes for the caller's organization. ### Request Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | `filter` | string | Optional | AIP-160 filter expression. Keys: `sourceChainId`, `destinationChainId`, `offerAsset`, `wantAsset` | ### Response ```json theme={null} { "routes": [ { "sourceChainId": 1, "destinationChainId": 4663, "destinationChainEID": 40451, "offerAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "wantAsset": "0x...", "minOrderSize": "35000000", "tokenMetadataMap": { "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "chain_id": "1", "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "name": "USD Coin", "symbol": "USDC", "decimals": "6", "token_standard": "ERC20" } } } ] } ``` | Field | Description | | --------------------- | -------------------------------------------- | | `sourceChainId` | EVM chain ID of source chain | | `destinationChainId` | EVM chain ID of destination chain | | `destinationChainEID` | Endpoint ID of destination chain | | `offerAsset` | Token address on source chain | | `wantAsset` | Token address on destination chain | | `minOrderSize` | Minimum order size in offer-asset base units | | `tokenMetadataMap` | Token metadata keyed by lowercase address | *** ## Get Order `GET /v1/transit/orders/:orderId` Returns indexer-backed status and details for a single Transit order. ### Path Parameters | Parameter | Type | Description | | --------- | --------------------- | ---------------------------------------------- | | `orderId` | hex string (64 chars) | 32-byte order ID (from `OrderSubmitted` event) | ### Response ```json theme={null} { "order": { "id": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "offerAsset": "0x...", "wantAsset": "0x...", "amountDue": "50000000", "remainingAmountDue": "0", "offerAmount": "50000000", "receiver": "0x...", "distributorCode": "0x0000...0000", "destinationChainId": 4663, "sourceChainId": 1, "receiveTime": 1234567890, "status": "PROCESSED", "user": "0x...", "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:05:00Z", "tokenMetadata": {}, "orderExecuteds": [ { "id": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "amount": "50000000", "remaining": "0", "timestamp": 1234567890, "txHash": "0x...", "chainId": 4663 } ] } } ``` ### Order Status Values | Status | Meaning | | ---------------- | ------------------------------------------- | | `PENDING_BRIDGE` | Order submitted, awaiting bridge processing | | `PROCESSING` | Order is being processed | | `PROCESSED` | Order fulfilled | | `REMOVED` | Order removed | *** ## List Orders `GET /v1/transit/orders` Returns a paginated list of orders for a user address. ### Request Parameters | Parameter | Type | Required | Description | | ------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------- | | `userAddress` | hex string | Yes | User wallet address to filter by | | `pageSize` | integer | Optional | Results per page (default: 10, max: 100) | | `pageToken` | string | Optional | Opaque cursor for next page | | `filter` | string | Optional | AIP-160 filter expression. Keys: `offerAsset`, `wantAsset`, `sourceChain`, `destinationChain`, `status` | ### Response ```json theme={null} { "orders": [], "nextPageToken": "opaque-cursor" } ``` *** ## Error Handling ```json theme={null} { "error": { "code": 400, "message": "Order amount is below the minimum of $35", "status": "INVALID_ARGUMENT" } } ``` | HTTP | `error.status` | Action | | ---- | ------------------ | ----------------------------------------- | | 400 | `INVALID_ARGUMENT` | Fix request (e.g., increase offer amount) | | 404 | `NOT_FOUND` | Order or route not found | | 503 | `INTERNAL` | Retry with exponential backoff | *** ## Common Errors | Issue | Error | Fix | | ---------------------------------------------------- | ---------------------- | -------------------------------------------------- | | Offer amount below \$35 USD | `400 INVALID_ARGUMENT` | Increase offer amount | | Missing `permitDeadline` with `permitSignature` | `400 INVALID_ARGUMENT` | Provide both or neither | | Missing `integratorFeeReceiver` with `integratorFee` | `400 INVALID_ARGUMENT` | Provide both or neither | | Invalid `distributorCode` length | `400 INVALID_ARGUMENT` | Must be 32 bytes (0x + 64 hex chars) | | Insufficient ETH for `value` | On-chain revert | Ensure wallet has ETH to cover `transaction.value` | | Route not available | `404 NOT_FOUND` | Check `/v1/transit/routes` for available pairs | *** ## Supported Chains | Chain | ID | | -------- | ---- | | Ethereum | 1 | | RH Chain | 4663 | # Transit API Integration Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/developers/guides/index Perform same-chain and cross-chain stablecoin swaps using our REST API. The Transit API returns ready-to-sign transaction calldata for stablecoin swaps — both same-chain conversions and cross-chain transfers. The server handles quote generation, fee calculation, and ABI encoding — you get back a transaction object to sign and broadcast. All Transit endpoints are public and do not require authentication. ## When to Use Transit * **Same-chain stablecoin swaps** — Convert between USDC and USDG on Ethereum * **Cross-chain stablecoin transfers** — Move USDC, PYUSD, or USDG between Ethereum and RH Chain * **Backend transaction builders** — Fetch calldata via API, sign and submit from any language * **Custodial wallets** — Get transaction data server-side, route to your signing infrastructure ## Prerequisites * An EVM wallet with sufficient balance for the offer amount plus native gas * An HTTP client (`curl`, `fetch`, `requests`, etc.) ## Endpoints | Endpoint | Purpose | | --------------------------------- | -------------------------------------------------------- | | `GET /v1/transit/routes` | Discover available asset pairs and minimum order sizes | | `GET /v3/core/authorization` | Check token approval status and get permit/approval data | | `GET /v1/transit/orders/quote` | Build order submission calldata | | `GET /v1/transit/orders/:orderId` | Get order status and execution details | | `GET /v1/transit/orders` | List orders for a user address | ## Integration Flow Query available asset pairs to find a route matching your source and destination chains. Call `/v3/core/authorization` to detect whether the offer token requires a permit signature, an approval transaction, or is already approved for the TransitStation contract. Fetch signed calldata for the `submitOrder` transaction, including the messaging fee. If using permits, pass the signature and deadline. Broadcast the transaction on the source chain with the returned `to`, `data`, and `value`. Poll the order status endpoint until fulfillment completes on the destination chain. ## Supported Chains | Chain | ID | | -------- | ---- | | Ethereum | 1 | | RH Chain | 4663 | ## Getting Started Find available asset pairs and check minimum order sizes. Get quote calldata and submit cross-chain orders. Monitor order status and view execution history. Condensed reference for AI coding assistants. # Order Submission Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/developers/guides/order-submission Get quote calldata and submit cross-chain Transit orders. This guide walks through the complete order submission flow: check authorization, fetch quote calldata, sign, and broadcast the transaction. ## Prerequisites * A route identified from [Route Discovery](/v1.0.0/intro/products/transit/developers/guides/route-discovery) * Wallet with sufficient offer asset balance (minimum \$35 USD equivalent) * Native token (ETH) for gas and messaging fees ## Step 0: Check Authorization Before submitting an order, check whether the offer token requires approval for the TransitStation contract. `GET /v3/core/authorization` | Parameter | Type | Required | Description | | ---------------- | -------------- | -------- | ---------------------------------------------------------------------------- | | `spenderAddress` | hex string | Yes | TransitStation contract (get from quote `transaction.to` or route discovery) | | `tokenAddress` | hex string | Yes | Offer token address | | `amount` | decimal string | Yes | Offer amount in base units | | `userAddress` | hex string | Yes | User's wallet address | | `chainId` | integer | Yes | Source chain ID | ### Response Variants **`permit`** — Token supports EIP-2612. Sign off-chain, then pass to quote endpoint. ```json theme={null} { "method": "permit", "permitData": { "domain": { "name": "USD Coin", "version": "2", "chainId": 1, "verifyingContract": "0xA0b8..." }, "types": { "Permit": [...] }, "value": { "owner": "0x...", "spender": "0x...", "value": "50000000", "nonce": "0", "deadline": "..." }, "deadline": "..." } } ``` **`approval`** — Standard ERC-20 approval required first. ```json theme={null} { "method": "approval", "approvalTransaction": { "encoded": "0x095ea7b3..." } } ``` **`already_approved`** — Sufficient allowance exists. Proceed to quote. ```json theme={null} { "method": "already_approved" } ``` ### Handling Each Path | Method | Action | | ------------------ | ---------------------------------------------------------------------------------------------- | | `permit` | Sign `permitData` via `eth_signTypedData_v4`, pass signature + deadline to quote | | `approval` | Send approval tx (`to = tokenAddress`, `data = approvalTransaction.encoded`), wait for receipt | | `already_approved` | Skip to Step 1 | ## Step 1: Get Order Quote `GET /v1/transit/orders/quote` This endpoint returns everything needed to submit the transaction: the contract address, ABI-encoded calldata, and the native token value for messaging fees. ### Parameters | Parameter | Type | Required | Description | | ----------------------- | -------------- | ----------- | --------------------------------------------------------------------- | | `userAddress` | hex string | Yes | Wallet receiving funds on destination chain | | `offerAmount` | decimal string | Yes | Amount in offer-asset base units (min \$35 USD) | | `offerAsset` | hex string | Yes | Token address on source chain | | `wantAsset` | hex string | Yes | Token address on destination chain | | `sourceChainId` | integer | Yes | Source chain EVM ID | | `destinationChainId` | integer | Yes | Destination chain EVM ID | | `permitSignature` | hex string | Conditional | EIP-2612 permit signature (required with `permitDeadline`) | | `permitDeadline` | integer | Conditional | Permit expiry timestamp (required with `permitSignature`) | | `integratorFee` | decimal string | Conditional | Fee in offer-asset base units (required with `integratorFeeReceiver`) | | `integratorFeeReceiver` | hex string | Conditional | Address receiving integrator fee | | `distributorCode` | hex string | No | 32-byte tracking code (defaults to organization's configured value) | | `responseFormat` | enum | No | `encoded` (default), `full`, or `structured` | ### Example Request ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/orders/quote?\ userAddress=0x1234567890abcdef1234567890abcdef12345678&\ offerAmount=50000000&\ offerAsset=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&\ wantAsset=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&\ sourceChainId=1&\ destinationChainId=4663" ``` ### Response ```json theme={null} { "transaction": { "to": "0x...", "data": "0x...", "value": "1500000000000000" }, "amountOut": "49990000", "protocolFee": "10000", "integratorFee": "0", "totalFees": "10000", "estimatedLatencyMs": 300000 } ``` ## Step 2: Understand the Response | Field | Description | | -------------------- | ------------------------------------------------------ | | `transaction.to` | TransitStation contract address | | `transaction.data` | ABI-encoded `submitOrder` calldata | | `transaction.value` | Native token for messaging fee (wei) | | `amountOut` | Net amount credited after fees (want-asset base units) | | `protocolFee` | Protocol fee (offer-asset base units) | | `integratorFee` | Integrator fee if specified (offer-asset base units) | | `totalFees` | Sum of protocol and integrator fees | | `estimatedLatencyMs` | Expected delivery time in milliseconds | ## Step 3: Submit the Transaction Broadcast the transaction using the returned `to`, `data`, and `value` fields. ### Code Example (viem) ```ts theme={null} import { createWalletClient, createPublicClient, http } from 'viem' import { mainnet } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const walletClient = createWalletClient({ account, chain: mainnet, transport: http(), }) const publicClient = createPublicClient({ chain: mainnet, transport: http(), }) const OFFER_ASSET = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' const OFFER_AMOUNT = '50000000' const TRANSIT_STATION = '0x...' // from route discovery or prior quote // Step 0: Check authorization const authUrl = new URL('https://api.paxoslabs.com/v3/core/authorization') authUrl.searchParams.set('spenderAddress', TRANSIT_STATION) authUrl.searchParams.set('tokenAddress', OFFER_ASSET) authUrl.searchParams.set('amount', OFFER_AMOUNT) authUrl.searchParams.set('userAddress', account.address) authUrl.searchParams.set('chainId', '1') const auth = await fetch(authUrl).then((r) => r.json()) // Step 1: Build quote params const quoteUrl = new URL('https://api.paxoslabs.com/v1/transit/orders/quote') quoteUrl.searchParams.set('userAddress', account.address) quoteUrl.searchParams.set('offerAmount', OFFER_AMOUNT) quoteUrl.searchParams.set('offerAsset', OFFER_ASSET) quoteUrl.searchParams.set('wantAsset', '0x...') quoteUrl.searchParams.set('sourceChainId', '1') quoteUrl.searchParams.set('destinationChainId', '4663') // Handle authorization path if (auth.method === 'permit') { const sig = await walletClient.signTypedData({ account, domain: auth.permitData.domain, types: auth.permitData.types, primaryType: 'Permit', message: auth.permitData.value, }) quoteUrl.searchParams.set('permitSignature', sig) quoteUrl.searchParams.set('permitDeadline', auth.permitData.deadline) } else if (auth.method === 'approval') { const hash = await walletClient.sendTransaction({ to: OFFER_ASSET as `0x${string}`, data: auth.approvalTransaction.encoded as `0x${string}`, chain: mainnet, account, }) await publicClient.waitForTransactionReceipt({ hash }) } // Fetch quote and submit const { transaction } = await fetch(quoteUrl).then((r) => r.json()) const hash = await walletClient.sendTransaction({ to: transaction.to as `0x${string}`, data: transaction.data as `0x${string}`, value: BigInt(transaction.value), }) console.log('Transaction hash:', hash) ``` ## Step 4: Record the Order ID The transaction receipt contains an `OrderSubmitted` event with the order ID (a hex hash). Use this ID to track fulfillment via the [Order Tracking](/v1.0.0/intro/products/transit/developers/guides/order-tracking) endpoints. ## Response Formats Control the transaction response structure with `responseFormat`: | Format | `data` | `abi` / `functionName` / `args` | Use Case | | ------------------- | ------ | ------------------------------- | ------------------------------------------------ | | `encoded` (default) | Yes | No | Direct signing with HSM or `eth_sendTransaction` | | `full` | Yes | Yes | Debugging — see both encoded and decoded | | `structured` | No | Yes | Encode locally with your own tooling | ## Distributor Codes The optional `distributorCode` parameter accepts a 32-byte hex string (0x + 64 chars) for tracking order sources on-chain. If omitted, the quote uses your organization's configured default distributor code. ## Common Errors | Error | Cause | Fix | | ------------------------------ | --------------------------------------- | ----------------------------------- | | `400` — Amount below minimum | Offer amount \< \$35 USD | Increase offer amount | | `400` — Invalid permit | Missing `permitDeadline` with signature | Provide both or neither | | `400` — Invalid integrator fee | Missing `integratorFeeReceiver` | Provide both or neither | | `404` — Route not found | No route for asset pair | Check available routes | | On-chain revert | Insufficient ETH for `value` | Ensure balance covers messaging fee | | On-chain revert | Insufficient allowance | Run authorization check first | ## Next Steps Monitor order status and view execution history. # Order Tracking Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/developers/guides/order-tracking Monitor Transit order status and view execution history. After submitting an order, use these endpoints to track fulfillment progress and view execution details. ## Get Single Order `GET /v1/transit/orders/:orderId` Retrieve status and details for a specific order. ### Parameters | Parameter | Type | Description | | --------- | ------------- | --------------------------------------------------- | | `orderId` | string (path) | Order ID (hex hash) from the `OrderSubmitted` event | ### Example Request ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/orders/0x2680b1256a66f8e762a64b2dda813a071356530715f0084d38f0c67cd8a2068f" \ ``` ### Response ```json theme={null} { "order": { "id": "0x2680b1256a66f8e762a64b2dda813a071356530715f0084d38f0c67cd8a2068f", "offerAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "wantAsset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "amountDue": "49750000", "remainingAmountDue": "0", "offerAmount": "50000000", "receiver": "0x1234567890abcdef1234567890abcdef12345678", "distributorCode": null, "destinationChainId": 4663, "sourceChainId": 1, "receiveTime": 1735689600, "status": "PROCESSED", "user": "0x1234567890abcdef1234567890abcdef12345678", "createdAt": "2026-01-21T15:04:05Z", "updatedAt": "2026-01-21T15:09:05Z", "tokenMetadata": {}, "orderExecuteds": [ { "id": "4663_71768178_6", "amount": "49750000", "remaining": "0", "timestamp": 1735689600, "txHash": "0xbf747f3281c538b42c6e7730007867d1bf6107ea33722b860c05be61e8b9bc57", "chainId": 4663 } ] } } ``` ## Order Status Values | Status | Description | | ---------------- | ------------------------------------ | | `PENDING_BRIDGE` | Order submitted, awaiting processing | | `PROCESSING` | Order is being fulfilled | | `PROCESSED` | Order complete — funds delivered | | `REMOVED` | Order removed from the queue | ## List Orders `GET /v1/transit/orders` Retrieve a paginated list of orders for a user address. ### Parameters | Parameter | Type | Required | Description | | ------------- | ---------- | -------- | ---------------------------------------- | | `userAddress` | hex string | Yes | Wallet address to filter by | | `pageSize` | integer | No | Results per page (default: 10, max: 100) | | `pageToken` | string | No | Cursor for next page | | `filter` | string | No | AIP-160 filter expression | ### Filter Keys | Key | Type | Description | | ------------------ | ---------- | ------------------------------ | | `offerAsset` | hex string | Filter by offer token address | | `wantAsset` | hex string | Filter by want token address | | `sourceChain` | integer | Filter by source chain ID | | `destinationChain` | integer | Filter by destination chain ID | | `status` | enum | Filter by order status | ### Example Request ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/orders?\ userAddress=0x1234567890abcdef1234567890abcdef12345678" \ ``` With filtering: ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/orders?\ userAddress=0x1234...&\ filter=status%3DPROCESSED" \ ``` ### Response ```json theme={null} { "orders": [ { "id": "0x2680b1256a66f8e762a64b2dda813a071356530715f0084d38f0c67cd8a2068f", "status": "PROCESSED", ... } ], "nextPageToken": "eyJvZmZzZXQiOjI1fQ==" } ``` ## Polling for Completion Poll the single-order endpoint until `status` reaches a terminal state (`PROCESSED` or `REMOVED`). ```ts theme={null} async function waitForOrder(orderId: string): Promise { const url = `https://api.paxoslabs.com/v1/transit/orders/${orderId}` while (true) { const { order } = await fetch(url).then((r) => r.json()) if (order.status === 'PROCESSED' || order.status === 'REMOVED') { return order } await new Promise((r) => setTimeout(r, 10_000)) // 10s interval } } ``` ## Response Fields | Field | Type | Description | | -------------------- | -------------- | -------------------------------------------------- | | `id` | string | Order ID (hex hash from on-chain event) | | `offerAsset` | string | Token sent (source chain) | | `wantAsset` | string | Token received (destination chain) | | `amountDue` | string | Initial amount owed (want-asset base units) | | `remainingAmountDue` | string | Outstanding amount (decreases with partial fills) | | `offerAmount` | string | Amount sent by user (offer-asset base units) | | `receiver` | string | Address receiving funds | | `distributorCode` | string \| null | 32-byte hex tracking code, or null if not provided | | `destinationChainId` | number | Destination chain EVM ID | | `sourceChainId` | number | Source chain EVM ID | | `receiveTime` | number | Unix timestamp when order was indexed | | `status` | string | Current order status | | `user` | string | Address that submitted the order | | `createdAt` | string | ISO 8601 creation time | | `updatedAt` | string | ISO 8601 last update time | | `tokenMetadata` | object | Token metadata for offer and want assets | | `orderExecuteds` | array | Execution history with amounts and tx hashes | ## Error Responses | HTTP | Cause | Action | | ----- | --------------- | --------------- | | `404` | Order not found | Verify order ID | # Route Discovery Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/developers/guides/route-discovery Find available Transit routes and check minimum order sizes. Before submitting an order, query the available routes to find asset pairs matching your source and destination chains. ## Step 1: Fetch Available Routes `GET /v1/transit/routes` ### Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | `filter` | string | No | AIP-160 filter expression. Keys: `sourceChainId`, `destinationChainId`, `offerAsset`, `wantAsset` | ### Example Request ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/routes" \ ``` With filtering: ```bash theme={null} curl "https://api.paxoslabs.com/v1/transit/routes?filter=sourceChainId%3D1" \ ``` ### Response ```json theme={null} { "routes": [ { "sourceChainId": 1, "destinationChainId": 4663, "destinationChainEID": 40451, "offerAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "wantAsset": "0x...", "minOrderSize": "35000000", "tokenMetadataMap": { "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "chain_id": "1", "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "name": "USD Coin", "symbol": "USDC", "decimals": "6", "token_standard": "ERC20" } } } ] } ``` ## Step 2: Select a Route From the response, identify a route matching your needs and note these fields: | Field | Usage | | -------------------- | --------------------------------------------------- | | `sourceChainId` | Chain where you'll submit the transaction | | `destinationChainId` | Chain where funds will be delivered | | `offerAsset` | Token address you'll send (on source chain) | | `wantAsset` | Token address you'll receive (on destination chain) | | `minOrderSize` | Minimum order in offer-asset base units | ## Step 3: Verify Minimum Order Size The `minOrderSize` field indicates the minimum amount in the offer asset's base units. For a 6-decimal token like USDC, `35000000` equals \$35. Requests below this threshold return a `400 INVALID_ARGUMENT` error. ## Response Fields | Field | Type | Description | | --------------------- | ------ | -------------------------------------------- | | `sourceChainId` | number | EVM chain ID of source chain | | `destinationChainId` | number | EVM chain ID of destination chain | | `destinationChainEID` | number | Endpoint ID of destination chain | | `offerAsset` | string | Token address on source chain | | `wantAsset` | string | Token address on destination chain | | `minOrderSize` | string | Minimum order size in offer-asset base units | | `tokenMetadataMap` | object | Token metadata keyed by lowercase address | ## Next Steps Get quote calldata and submit your order. # Product Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/transit/product-overview Same-chain and cross-chain stablecoin swaps. Transit enables stablecoin swaps — both same-chain conversions and cross-chain transfers. Users submit orders on a source chain; the protocol fulfills them on the destination chain (or atomically on the same chain for same-chain routes). ## How It Works 1. **Get Quote** — Call the API to get signed calldata for a `submitOrder` transaction 2. **Submit** — Broadcast the transaction on the source chain (includes messaging fee as `value`) 3. **Get Fulfilled** — The protocol processes the order and credits the destination asset on the target chain ## Supported Routes | Direction | Offer Asset | Want Asset | Default Protocol Fee | | ------------------- | ----------- | ---------- | -------------------- | | Ethereum → Ethereum | USDC | USDG | 5 bps (0.05%) | | Ethereum → Ethereum | USDG | USDC | 5 bps (0.05%) | | Ethereum → RH | USDG | USDG | \$0.10 flat | | Ethereum → RH | USDC | USDG | 5 bps (0.05%) | | Ethereum → RH | PYUSD | USDG | 2.5 bps (0.025%) | | RH → Ethereum | USDG | USDG | \$0.15 min + gas | | RH → Ethereum | USDG | USDC | 5 bps (0.05%) | **USDG↔USDG routes** use flat dollar-based fees. RH → Ethereum includes a dynamic gas component based on current Ethereum gas prices, with \$0.15 as the minimum floor. **Same-chain Ethereum routes** (USDC ↔ USDG) settle on Ethereum. **All other routes** use a basis-points fee applied to the offer amount. Organizations may have custom fee configurations. Use `GET /v1/transit/routes` to fetch the current route configuration programmatically. ## Minimum Order Size Orders must be at least **\$35 USD** equivalent in the offer asset. ## Getting Started Step-by-step guides for route discovery, order submission, and tracking. Complete endpoint reference with request/response schemas. Condensed reference for Cursor, Copilot, and Claude. # Accessing USDG0 Source: https://developers.paxoslabs.com/v1.0.0/intro/products/usdg0/access How to access USDG0 liquidity # Accessing USDG0 USDG0 is available across multiple chains and liquidity venues.\ Developers and institutions can access or bridge USDG0 directly, or through ecosystem-specific markets. ## Cross-chain Access General access to USDG0 across supported networks: * [usdg0.paxoslabs.com](https://usdg0.paxoslabs.com) — Primary portal for minting, redeeming, and viewing supply. * [stargate.finance/bridge](https://stargate.finance/bridge) — Bridge USDG0 across supported chains. *** ## Ecosystem Access Below are supported ecosystems where USDG0 can be traded or integrated via local liquidity pools and swap venues. ### Hyperliquid
View available markets * **Spot Pools**: * Hyperliquid USDG0/USDC * Hyperliquid USDG0/ETH * **Perpetual Margin Support**: * USDG0 accepted as collateral for perpetual markets
### Aptos
View available markets * **Liquidity Pools**: * PancakeSwap (Aptos): USDG0/USDC * Aries Markets: USDG0 lending/borrowing pools * **Spot Venues**: * Liquidswap * Econia
### Plume
View available markets * **Liquidity Pools**: * PlumeSwap: USDG0/USDT * Plume Finance Stable Pool * **Spot Venues**: * Plume DEX * Hydra Exchange
*** ## Notes * Availability and liquidity may vary by network. * Always verify contract addresses on official Paxos Labs or ecosystem documentation before interacting. # Addresses Source: https://developers.paxoslabs.com/v1.0.0/intro/products/usdg0/addresses Contract addresses for USDG0 across supported networks | Network | Address | | ---------------- | -------------------------------------------- | | HyperEVM (USDG0) | `0x9522133c663ba8C2a8A7b4F63Dd2aC8DccCe1505` | | Aptos | Coming soon | | Plume | Coming soon | # Analytics Source: https://developers.paxoslabs.com/v1.0.0/intro/products/usdg0/analytics Coming Soon # Developer Guide Source: https://developers.paxoslabs.com/v1.0.0/intro/products/wpaxg/developers Integrate WPAXG swaps into your application via REST API. # WPAXG Swap Integration This guide walks through integrating the WPAXG swap REST API into your application. Currently only available via REST API. ## Prerequisites * A Paxos Labs API key * An Optimism RPC endpoint * A wallet library (viem, ethers, wagmi, etc.) ## Supported Token Pairs WPAXG must be either the `offerAsset` (sell) or `wantAsset` (buy) in every swap request. Supported stablecoins for the other side of the trade: | Token | Address | | ----- | -------------------------------------------- | | USDC | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | ## Getting a Swap Quote Request a quote by calling the `/v1/wpaxg/swapQuotes` endpoint: ```typescript theme={null} const response = await fetch( 'https://api.paxoslabs.com/v1/wpaxg/swapQuotes?' + new URLSearchParams({ chainId: '10', offerAsset: '0x5cb5c4d5e8b184a364534bc688da0553ccf8f484', // WPAXG wantAsset: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC offerAmount: '1000000000000000000', // 1 WPAXG in wei userAddress: '0x...', // User's wallet address }), { headers: { 'x-api-key': 'pxl__', }, } ); const quote = await response.json(); ``` ## Response Structure The quote response includes: ```typescript theme={null} interface SwapQuoteResponse { transaction: { to: string; // Contract to send the transaction to data: string; // Encoded swap calldata value: string; // ETH value (usually "0" for token swaps) gas: string; // Estimated gas limit gasPrice: string; // Suggested gas price }; buyAmount: string; // Expected output amount minBuyAmount: string; // Minimum output (with slippage) allowanceTarget: string; // Address to approve tokens to currentAllowance?: string; // User's current allowance (if any) } ``` ## Handling Token Approvals Before executing the swap, ensure the user has approved sufficient tokens: ```typescript theme={null} import { erc20Abi } from 'viem'; // Check if approval is needed const needsApproval = BigInt(quote.currentAllowance ?? '0') < BigInt(offerAmount); if (needsApproval) { // Request approval const approveTx = await walletClient.writeContract({ address: offerAsset, abi: erc20Abi, functionName: 'approve', args: [quote.allowanceTarget, offerAmount], }); // Wait for approval confirmation await publicClient.waitForTransactionReceipt({ hash: approveTx }); } ``` ## Executing the Swap Once approved, execute the swap using the provided calldata: ```typescript theme={null} const swapTx = await walletClient.sendTransaction({ to: quote.transaction.to, data: quote.transaction.data, value: BigInt(quote.transaction.value), gas: BigInt(quote.transaction.gas), }); const receipt = await publicClient.waitForTransactionReceipt({ hash: swapTx }); ``` ## Using Permit2 Authorization For gasless approvals, use the `permit2` auth method: ```typescript theme={null} const response = await fetch( 'https://api.paxoslabs.com/v1/wpaxg/swapQuotes?' + new URLSearchParams({ chainId: '10', offerAsset: '0x5cb5c4d5e8b184a364534bc688da0553ccf8f484', wantAsset: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', offerAmount: '1000000000000000000', userAddress: '0x...', authMethod: 'permit2', // Use Permit2 }), { headers: { 'x-api-key': 'pxl__', }, } ); const quote = await response.json(); // The response includes EIP-712 typed data for signing if (quote.permit2) { const signature = await walletClient.signTypedData({ domain: quote.permit2.eip712.domain, types: quote.permit2.eip712.types, primaryType: quote.permit2.eip712.primaryType, message: quote.permit2.eip712.message, }); // Include signature when executing the swap // (implementation depends on your setup) } ``` ## Error Handling The API returns standard HTTP error codes: | Status | Description | | ------ | -------------------------------------------- | | 400 | Invalid parameters or insufficient liquidity | | 503 | DEX aggregator temporarily unavailable | Example error response: ```json theme={null} { "error": { "code": 400, "message": "No liquidity available for this swap. Try a smaller amount or different token pair.", "status": "INVALID_ARGUMENT" } } ``` Always validate the `minBuyAmount` before executing to protect against slippage. Consider showing users the expected output and allowing them to set slippage tolerance. # Overview Source: https://developers.paxoslabs.com/v1.0.0/intro/products/wpaxg/overview Swap WPAXG (Wrapped Paxos Gold) on Optimism via DEX aggregators. # WPAXG Swaps The WPAXG Swap API enables buying and selling of WPAXG (Wrapped Paxos Gold) tokens on Optimism. The API returns ready-to-execute transaction calldata for swaps via DEX aggregators, handling liquidity sourcing and routing automatically. Currently only available via REST API. ## Supported Networks | Network | Chain ID | | -------- | -------- | | Optimism | 10 | ## Supported Tokens The following tokens can be used as the offer or want asset: | Token | Address (Optimism) | | ----- | -------------------------------------------- | | WPAXG | `0x5cb5c4d5e8b184a364534bc688da0553ccf8f484` | | USDC | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | ## How It Works 1. **Request a quote** — Call the swap quote endpoint with the offer asset, want asset, amount, and user address 2. **Review the quote** — The response includes the expected output amount, transaction calldata, and any required token approvals 3. **Execute the swap** — Submit the transaction using the provided calldata ## Authorization Methods The API supports two authorization methods for token approvals: * **`allowance_holder`** (default) — Standard ERC-20 approve flow using the AllowanceHolder contract * **`permit2`** — Gasless approval using EIP-712 signed permits For most use cases, the default `allowance_holder` method is recommended as it's simpler to implement. # Common Implementation Patterns Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/common-implementation-patterns Guides to verify webhook signatures, handle events idempotently, and more This page collects the patterns most webhook integrations need: verifying signatures before processing, deduplicating redelivered events, and routing events to the right handler. ## Signature Verification Every webhook request from Paxos Labs includes a cryptographic signature that lets you confirm the request is authentic and hasn't been tampered with. Always verify signatures before processing webhook payloads. ### Headers Each webhook `POST` request includes these headers: | Header | Example | Description | | ------------------------ | -------------------------- | --------------------------------------------------------------------------------------- | | `X-PAXOS-LABS-TIMESTAMP` | `2026-04-07T18:06:40.000Z` | RFC 3339 UTC timestamp of event creation. Concatenated with the payload before signing. | | `X-PAXOS-LABS-SIGNATURE` | `a3f2...9b01` | HMAC-SHA256 hex digest of the timestamp and payload | | `Content-Type` | `application/json` | Always JSON | | `User-Agent` | `PaxosLabs-Webhooks/1.0` | Identifies the sender | ### Signature Scheme `X-PAXOS-LABS-SIGNATURE` contains the raw hex-encoded HMAC-SHA256 digest. The timestamp used in the HMAC computation is sent in the separate `X-PAXOS-LABS-TIMESTAMP` header. ### Verification Algorithm Read `X-PAXOS-LABS-TIMESTAMP` for the RFC 3339 timestamp and `X-PAXOS-LABS-SIGNATURE` for the hex signature. Reject requests where the timestamp is more than **5 minutes** from your server's current time. This protects against replay attacks. Concatenate the timestamp, a literal period (`.`), and the **raw request body** (the exact bytes received — do not parse and re-serialize): ``` {timestamp}.{raw_body} ``` Calculate an HMAC-SHA256 using your endpoint's signing secret as the key and the constructed string as the message. Hex-encode the result. Use a **constant-time comparison** to check that your computed signature matches the `v1` value from the signature header. This prevents timing attacks. ### Implementation Examples ```js theme={null} import { createHmac, timingSafeEqual } from 'node:crypto' const TOLERANCE_SECONDS = 300 function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) { const timestamp = new Date(timestampHeader) if (Number.isNaN(timestamp.getTime())) { throw new Error('Invalid timestamp header') } if (Math.abs(Date.now() - timestamp.getTime()) / 1000 > TOLERANCE_SECONDS) { throw new Error('Timestamp outside tolerance — possible replay attack') } const signedPayload = `${timestampHeader}.${rawBody}` const expected = createHmac('sha256', secret) .update(signedPayload) .digest('hex') const received = Buffer.from(signatureHeader, 'hex') const expectedBuf = Buffer.from(expected, 'hex') if (received.length !== expectedBuf.length || !timingSafeEqual(received, expectedBuf)) { throw new Error('Invalid signature') } } ``` Use `timingSafeEqual` instead of `===` to prevent timing side-channel attacks. ```python theme={null} import hashlib, hmac from datetime import datetime, timezone TOLERANCE_SECONDS = 300 def verify_webhook_signature(raw_body, signature_header, timestamp_header, secret): ts = datetime.fromisoformat(timestamp_header) now = datetime.now(timezone.utc) if abs((now - ts).total_seconds()) > TOLERANCE_SECONDS: raise ValueError("Timestamp outside tolerance — possible replay attack") signed_payload = f"{timestamp_header}.{raw_body}".encode() expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest() if not hmac.compare_digest(signature_header, expected): raise ValueError("Invalid signature") ``` `hmac.compare_digest` provides constant-time comparison in Python. ```go theme={null} package webhook import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "math" "time" ) const ToleranceSeconds = 300 func VerifySignature(body []byte, sigHeader, tsHeader, secret string) error { ts, err := time.Parse(time.RFC3339, tsHeader) if err != nil { return fmt.Errorf("invalid timestamp header: %w", err) } if math.Abs(time.Since(ts).Seconds()) > ToleranceSeconds { return fmt.Errorf("timestamp outside tolerance") } mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "%s.%s", tsHeader, body) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(sigHeader), []byte(expected)) { return fmt.Errorf("invalid signature") } return nil } ``` `hmac.Equal` provides constant-time comparison in Go's standard library. ### Best Practices Never process a webhook payload without verifying the signature first. An unverified payload could be forged by a malicious actor. Standard string comparison (`===`, `==`) leaks timing information that attackers can exploit. Always use `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), or `hmac.Equal` (Go). Reject events where the timestamp is more than 5 minutes from your server's clock. This prevents captured requests from being replayed later. Compute the HMAC over the exact bytes received in the HTTP body. Parsing to JSON and re-serializing can change whitespace or key ordering, producing a different signature. Keep your signing secret in a secrets manager or encrypted environment variable — never hard-code it or commit it to source control. Return a `2xx` status code within **10 seconds**. Move heavy processing to a background queue so the webhook handler returns immediately. Use the event `id` field to deduplicate. Store processed event IDs and skip any that you've already handled. ### Troubleshooting | Symptom | Cause | Fix | | -------------------- | --------------------------------------- | ----------------------------------------------------------------- | | Signature mismatch | Re-serialized body instead of raw bytes | Use the raw HTTP body for HMAC computation | | Timestamp rejection | Server clock drift | Sync your server with NTP; widen tolerance if needed | | `401` on test events | Wrong secret | Verify you're using the correct `pxlwh_` secret for this endpoint | | Secret lost | Secret was not saved at creation | Delete the endpoint and create a new one | *** ## Idempotent Processing Events may be delivered more than once. Use the `id` field to deduplicate: ```js theme={null} const processedEvents = new Set() function handleWebhook(event) { if (processedEvents.has(event.id)) { return // already processed } processedEvents.add(event.id) switch (event.type) { case 'smart_deposit.deposit.confirmed': creditUserAccount(event) break case 'smart_deposit.deposit.finalized': markDepositFinalized(event) break case 'smart_deposit.deposit.failed': notifyUserOfFailure(event) break } } ``` In production, store processed event IDs in a database rather than in-memory to survive restarts. *** ## Correlating Deposit Lifecycle Events `id` deduplicates deliveries of a single event; it changes between events. To connect the events of one deposit — `pending` through `confirmed`/`failed` and `finalized` — key your state on `deposit_id`, which stays constant across the whole sequence. Deliveries can arrive out of order, so guard against a late-arriving earlier stage overwriting a later one: ```js theme={null} // pending always comes first; confirmed/failed are terminal-or-better; finalized is last const STAGE_RANK = { pending: 0, confirmed: 1, failed: 1, finalized: 2 } function handleSmartDepositEvent(event) { const stage = event.type.split('.').pop() // pending | confirmed | finalized | failed const existing = getDeposit(event.deposit_id) if (existing && STAGE_RANK[stage] <= STAGE_RANK[existing.stage]) { return // duplicate or out-of-order delivery — keep the later stage } upsertDeposit(event.deposit_id, { stage, ...event }) } ``` See the [Smart Deposit Routing Events](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#tracking-a-deposit-across-events) reference for the full lifecycle. *** ## Type-Based Routing Route events to different handlers based on the `type` field: ```js theme={null} const handlers = { 'ping': () => console.log('pong'), 'smart_deposit.deposit.pending': (evt) => trackPendingDeposit(evt), 'smart_deposit.deposit.confirmed': (evt) => finalizeDeposit(evt.organization_id, evt.received_amount), 'smart_deposit.deposit.finalized': (evt) => markDepositFinalized(evt), 'smart_deposit.deposit.failed': (evt) => handleFailure(evt.organization_id, evt.remediation, evt.error), } function handleWebhook(event) { const handler = handlers[event.type] if (handler) { handler(event) } else { console.warn(`Unhandled event type: ${event.type}`) } } ``` # Event Catalog Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/event-catalog Complete reference of all webhook event types, payload schemas, and example payloads This page documents every webhook event type available in Paxos Labs. All events use a flat JSON structure with shared common fields and type-specific fields at the top level. ## Event Types * [**Test Events**](/v1.0.0/intro/webhooks/event-catalog/test) * [`ping`](/v1.0.0/intro/webhooks/event-catalog/test#ping) — Lightweight connectivity test. Sent from the Paxos Labs Console with no specific event type selected. * [**Smart Deposit Routing Events**](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing) * [`smart_deposit.deposit.pending`](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#smart_deposit-deposit-pending) — Stablecoins received at the Smart Deposit Address; the deposit into the vault has been initiated. * [`smart_deposit.deposit.confirmed`](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#smart_deposit-deposit-confirmed) — Stablecoins deposited into the vault and shares minted to the destination address. * [`smart_deposit.deposit.finalized`](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#smart_deposit-deposit-finalized) — Forward transaction reached chain-specific finality. * [`smart_deposit.deposit.failed`](/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing#smart_deposit-deposit-failed) — Deposit could not be completed; payload includes the failure reason and a `remediation` value saying where the funds ended up. ## Common Fields Every webhook event includes these fields: ```json theme={null} { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "type": "ping", "timestamp": "2026-04-10T18:32:24.000Z", "version": "v2" } ``` | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | `string` | Unique event identifier (UUID). Remains the same across retry attempts, so consumers can idempotently handle duplicate events. | | `type` | `string` | The event type string (e.g. `ping`). | | `timestamp` | `string` | RFC 3339 UTC timestamp of the webhook delivery creation (e.g., `2026-04-10T18:32:24.000Z`). | | `version` | `string` | API version. Currently `v2`. | *** ## Handling Webhook Events For guides on signature verification, idempotent processing, and more, see [Common Implementation Patterns](/v1.0.0/intro/webhooks/common-implementation-patterns). # Smart Deposit Routing Events Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/event-catalog/smart-deposit-routing Webhook events emitted as stablecoins are routed through the Smart Deposit Address into a vault Every smart deposit webhook includes the [common fields](/v1.0.0/intro/webhooks/event-catalog#common-fields), plus the routing and token context below. Each event also carries `received_amount` — the stablecoin amount detected at the Smart Deposit Address, in the input token's smallest unit. ## Shared Fields | Field | Type | Description | | -------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deposit_id` | `string` | Unique deposit identifier, prefixed with `pxl_dep_` (e.g. `"pxl_dep_8a3c9d2e-…"`). Treat it as an opaque string. Every event for the same deposit — `pending` through `confirmed`/`failed` and `finalized` — carries the same `deposit_id`. See [Tracking a deposit across events](#tracking-a-deposit-across-events). | | `chain_id` | `string` | EIP 155 chain ID (e.g. `"11155111"`). | | `chain_name` | `string` | Human-readable chain name (e.g. `"Ethereum Sepolia"`). | | `organization_id` | `string` | Your organization UUID. | | `customer_id` | `string \| null` | Optional customer identifier passed when creating the Smart Deposit Address. `null` if not provided. | | `smart_deposit_address` | `string` | The Smart Deposit Address that received the stablecoins. | | `user_destination_address` | `string` | End-user address where vault shares are minted. | | `vault_address` | `string` | Target vault (Boring Vault) contract address. | | `input_token_symbol` | `string` | Symbol of the deposited stablecoin (e.g. `"USDC"`). | | `input_token_address` | `string` | Contract address of the deposited stablecoin. | | `input_token_decimals` | `number` | Decimal places of the input token. | | `received_amount` | `string` | Amount received at the Smart Deposit Address, in the input token's smallest unit. String for bigint serialization. Multiple transfers of the same token that arrive at the address in quick succession may be combined into **one** deposit with their total amount — don't assume a 1:1 mapping between on-ramp transactions and deposits. | ## Tracking a Deposit Across Events Use `deposit_id` to tie a webhook to a deposit — it is minted when the deposit is first detected and repeated on every subsequent event for that deposit. Do not use the top-level `id` for this: `id` is unique per *event* (its job is [deduplicating redeliveries](/v1.0.0/intro/webhooks/common-implementation-patterns#idempotent-processing)), so the `pending` and `confirmed` events for one deposit have different `id` values but the same `deposit_id`. A deposit emits one sequence: `pending`, then `confirmed` or `failed`, then — after `confirmed` only — `finalized`. Every sequence begins with `pending`: a `confirmed`, `failed`, or `finalized` event is only ever emitted for a `deposit_id` whose `pending` event was already emitted. As with any webhook system, *deliveries* can still reach your endpoint out of order — order by event semantics, not arrival time. If a deposit fails with `remediation: "retained"`, the funds stay in the Smart Deposit Address and may be retried automatically. A retry is a new sequence with a **new** `deposit_id`, starting again from `pending`. *** ## `smart_deposit.deposit.pending` Paxos Labs has received the stablecoins and initiated the process to deposit them into the vault. ```json theme={null} { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "type": "smart_deposit.deposit.pending", "timestamp": "2026-04-10T18:32:24.000Z", "version": "v2", "deposit_id": "pxl_dep_8a3c9d2e-6f41-4b0a-9c58-2d7e1f0a4b63", "chain_name": "Ethereum Sepolia", "chain_id": "11155111", "organization_id": "700768ae-c71d-42cc-9ff9-13b777d6d379", "customer_id": "user_abc123", "smart_deposit_address": "0xsda0000000000000000000000000000000123", "user_destination_address": "0xuda0000000000000000000000000000000000456", "vault_address": "0xvlt0000000000000000000000000000000999", "input_token_symbol": "USDC", "input_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "input_token_decimals": 6, "received_amount": "10000000" } ``` ### Event-Specific Fields None — `pending` carries only the shared fields above. It is emitted as soon as the stablecoin balance is detected, before any forward transaction exists, so there are no transaction hashes to report yet. *** ## `smart_deposit.deposit.confirmed` Stablecoins have been successfully deposited into the vault and shares have been minted to the destination address. ```json theme={null} { "id": "0d9b7c15-3e82-47a6-b1f4-5c60d8e92a17", "type": "smart_deposit.deposit.confirmed", "timestamp": "2026-04-10T18:33:12.000Z", "version": "v2", "deposit_id": "pxl_dep_8a3c9d2e-6f41-4b0a-9c58-2d7e1f0a4b63", "chain_name": "Ethereum Sepolia", "chain_id": "11155111", "organization_id": "700768ae-c71d-42cc-9ff9-13b777d6d379", "customer_id": "user_abc123", "smart_deposit_address": "0xsda0000000000000000000000000000000123", "user_destination_address": "0xuda0000000000000000000000000000000000456", "vault_address": "0xvlt0000000000000000000000000000000999", "input_token_symbol": "USDC", "input_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "input_token_decimals": 6, "received_amount": "10000000", "forward_tx_hash": "0x1234", "forward_tx_block_hash": "0x4567" } ``` ### Event-Specific Fields | Field | Type | Description | | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `received_amount` | `string` | Amount received in the token's smallest unit. | | `forward_tx_hash` | `string` | Transaction hash of the transaction that forwards funds from the Smart Deposit Address into the vault. | | `forward_tx_block_hash` | `string` | Block hash of the forward transaction. | *** ## `smart_deposit.deposit.finalized` Deposit finalized. ```json theme={null} { "id": "5e2f8a90-1b74-4c3d-a6e9-7f08b5d4c221", "type": "smart_deposit.deposit.finalized", "timestamp": "2026-04-10T18:36:45.000Z", "version": "v2", "deposit_id": "pxl_dep_8a3c9d2e-6f41-4b0a-9c58-2d7e1f0a4b63", "chain_name": "Ethereum Sepolia", "chain_id": "11155111", "organization_id": "700768ae-c71d-42cc-9ff9-13b777d6d379", "customer_id": "user_abc123", "smart_deposit_address": "0xsda0000000000000000000000000000000123", "user_destination_address": "0xuda0000000000000000000000000000000000456", "vault_address": "0xvlt0000000000000000000000000000000999", "input_token_symbol": "USDC", "input_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "input_token_decimals": 6, "received_amount": "10000000", "forward_tx_hash": "0x1234", "forward_tx_block_hash": "0x4567" } ``` ### Event-Specific Fields Same as `smart_deposit.deposit.confirmed`. Sent after the forward transaction reaches chain-specific finality. *** ## `smart_deposit.deposit.failed` Unable to deposit for some reason. Customer notified with reason and remediation actions taken. ```json theme={null} { "id": "b41d6e83-9a20-4f5c-8d17-3e92a0c7f558", "type": "smart_deposit.deposit.failed", "timestamp": "2026-04-10T18:33:12.000Z", "version": "v2", "deposit_id": "pxl_dep_8a3c9d2e-6f41-4b0a-9c58-2d7e1f0a4b63", "chain_name": "Ethereum Sepolia", "chain_id": "11155111", "organization_id": "700768ae-c71d-42cc-9ff9-13b777d6d379", "customer_id": "user_abc123", "smart_deposit_address": "0xsda0000000000000000000000000000000123", "user_destination_address": "0xuda0000000000000000000000000000000000456", "vault_address": "0xvlt0000000000000000000000000000000999", "input_token_symbol": "USDC", "input_token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "input_token_decimals": 6, "received_amount": "10000000", "forward_tx_hash": "0x1234", "forward_tx_block_hash": "0x4567", "error": "Vault cap reached", "remediation": "refunded" } ``` ### Event-Specific Fields | Field | Type | Description | | ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- | | `received_amount` | `string` | Amount received in the token's smallest unit. | | `forward_tx_hash` | `string \| null` | Transaction hash of the forward attempt. `null` if the transaction was never sent. | | `forward_tx_block_hash` | `string \| null` | Block hash of the forward attempt. `null` if the transaction was never sent. | | `error` | `string` | Human-readable error describing why the deposit failed (e.g., vault cap reached, predicate failure, out of gas). | | `remediation` | `string` | Where the funds ended up. One of `refunded`, `recovered`, `retained` — see below. | ### Remediation Values Branch on `remediation` rather than parsing `error`, which is free-form prose intended for humans and may change at any time. | Value | Meaning | Action for you | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `refunded` | The deposit could not proceed (vault cap reached, vault paused, slippage, etc.), so the stablecoins were sent back to the end-user's destination address on-chain. | Tell your end-user their funds were returned. `forward_tx_hash` is the on-chain proof. | | `recovered` | The end-user cannot legally hold the input or share token (sanctions screening failure, blacklisted or frozen address), so the stablecoins were swept to a Paxos Labs recovery wallet. | Do not tell the user their funds were returned — they were not. Escalate through your compliance process. | | `retained` | No on-chain remediation ran. The stablecoins are still sitting in the Smart Deposit Address. | No action required. We are alerted automatically and will investigate; the deposit may be retried automatically. Show this to your end-user as *delayed*, not failed — a successful retry arrives as a new `pending` → `confirmed` sequence with a **new** `deposit_id`. | `remediation` is only present on `smart_deposit.deposit.failed`. It is never sent on `pending`, `confirmed`, or `finalized` events, where there is nothing to remediate. `remediation: "refunded"` and `remediation: "recovered"` always carry a non-null `forward_tx_hash`, because the remediation itself is an on-chain action. `remediation: "retained"` always carries `forward_tx_hash: null`. # Test Events Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/event-catalog/test Test webhook events and how to send them from the Paxos Labs Console Test events let you verify your endpoint connectivity and [signature verification](/v1.0.0/intro/webhooks/common-implementation-patterns#signature-verification) before going live. They share the [common fields](/v1.0.0/intro/webhooks/event-catalog#common-fields) but never represent real transactions. ## `ping` A lightweight connectivity test. Sent when you click **Send Test** in the Paxos Labs Console with no specific event type selected. ```json theme={null} { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "type": "ping", "timestamp": "2026-04-10T18:32:24.000Z", "version": "v2" } ``` The `ping` event contains only the common fields — no additional fields. *** ## Testing with the Paxos Labs Console The Paxos Labs Console lets you send test events for any subscribed event type. Test events: * Carry a normal UUID `id`, indistinguishable from a live event by ID alone * Contain representative sample data (not real transactions) * Are labeled **Test** in delivery history Use test events to verify your endpoint connectivity and signature verification before going live. # Webhooks Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/overview Receive real-time event notifications when activity occurs in your organization Paxos Labs webhooks deliver real-time HTTP notifications to your server when various events occur. Use them notify your users the moment they happen, or keep your infrastructure in sync with Paxos Labs in real-time. ## How It Works Add an HTTPS URL in the [Paxos Labs Console](https://console.paxoslabs.com) under your organization's **Webhooks** tab. You receive a signing secret on creation — store it securely. Choose which event types your endpoint should receive. You can subscribe to all available events or select specific ones. When an event fires, Paxos Labs sends a signed `POST` request to your endpoint. Your server verifies the HMAC signature, then processes the payload. ## Quick Links Register an endpoint, subscribe to events, and handle your first webhook in minutes. Verify webhook authenticity with HMAC-SHA256 signatures. Includes code samples in Node.js, Python, and Go. Complete reference of all webhook event types with payload schemas and examples. *** ## Key Concepts | Concept | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | An HTTPS URL on your server that receives webhook `POST` requests. | | **Subscription** | The set of event types an endpoint listens to. Each endpoint can subscribe to one or more event types. | | **Signing secret** | A unique `pxlwh_` prefixed key generated when you create an endpoint. Used to verify that requests genuinely originate from Paxos Labs. | | **Event** | A JSON payload describing something that happened — e.g., a deposit was confirmed. | | **Delivery** | A single attempt to send an event to your endpoint. The Paxos Labs Console shows delivery history per endpoint. | ## Security Model Every webhook request is signed with your endpoint's unique secret using **HMAC-SHA256**. Two headers accompany each request: | Header | Value | | ------------------------ | --------------------------------------- | | `X-PAXOS-LABS-TIMESTAMP` | Unix seconds when the event was created | | `X-PAXOS-LABS-SIGNATURE` | `v1=` | Your server should always [verify the signature](/v1.0.0/intro/webhooks/common-implementation-patterns#signature-verification) before processing a webhook. This prevents forgery and replay attacks. Your signing secret is shown **once** at creation time. Copy and store it in a secrets manager immediately. If lost, delete the endpoint and create a new one. ## Limits | Limit | Value | | -------------------------- | -------------- | | Endpoints per organization | 10 | | Endpoint URL protocol | HTTPS only | | Endpoint name length | 100 characters | | Delivery timeout | 10 seconds | # Quick Start Source: https://developers.paxoslabs.com/v1.0.0/intro/webhooks/quickstart Register a webhook endpoint, subscribe to events, and verify your first delivery This guide walks through the full webhook setup: creating an endpoint in the Paxos Labs Console, writing a minimal receiver, and verifying the connection with a test event. ## Prerequisites * A Paxos Labs organization with admin access * A publicly reachable HTTPS server (or a tunnel like [ngrok](https://ngrok.com) for local development) ## Step 1: Create a Webhook Endpoint Navigate to your organization in the [Paxos Labs Console](https://console.paxoslabs.com) and select the **Webhooks** tab. Click **Add Endpoint**. Enter your HTTPS URL, an optional display name, and select the event types you want to receive. After creation, a signing secret starting with `pxlwh_` is displayed **once**. Copy it immediately and store it in your secrets manager (e.g., AWS Secrets Manager, Vault, or `.env` for local development). The signing secret is shown only at creation time. If you lose it, delete the endpoint and create a new one. ## Step 2: Build a Receiver In the route for your registered HTTPS handler, verify each request (signature and timestamp) and return a `2xx`-status response within 3 seconds. Slow or failed responses may trigger retries, resulting in your server receiving duplicate events (use the `id` field to process idempotently). ```js theme={null} import { createServer } from 'node:http' import { createHmac, timingSafeEqual } from 'node:crypto' const SECRET = process.env.WEBHOOK_SECRET // pxlwh_... const PORT = process.env.PORT || 4000 const TOLERANCE_SECONDS = 300 // 5-minute replay window function verify(rawBody, signatureHeader, timestampHeader) { const timestamp = new Date(timestampHeader) if (Math.abs(Date.now() - timestamp.getTime()) / 1000 > TOLERANCE_SECONDS) { throw new Error('Timestamp outside tolerance') } const expected = createHmac('sha256', SECRET) .update(`${timestampHeader}.${rawBody}`) .digest('hex') const received = Buffer.from(signatureHeader, 'hex') if (!timingSafeEqual(received, Buffer.from(expected, 'hex'))) { throw new Error('Invalid signature') } } const server = createServer((req, res) => { if (req.method !== 'POST') { res.writeHead(405).end() return } const chunks = [] req.on('data', (c) => chunks.push(c)) req.on('end', () => { const rawBody = Buffer.concat(chunks).toString('utf-8') try { verify( rawBody, req.headers['x-paxos-labs-signature'], req.headers['x-paxos-labs-timestamp'], ) const event = JSON.parse(rawBody) console.log(`Received ${event.type} — ${event.id}`) res.writeHead(200).end('ok') } catch (err) { console.error('Verification failed:', err.message) res.writeHead(401).end('unauthorized') } }) }) server.listen(PORT, () => console.log(`Listening on :${PORT}`)) ``` ```python theme={null} import hashlib, hmac, json, os from datetime import datetime, timezone from http.server import HTTPServer, BaseHTTPRequestHandler SECRET = os.environ["WEBHOOK_SECRET"].encode() # pxlwh_... TOLERANCE = 300 # 5-minute replay window class Handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers.get("Content-Length", 0)) raw_body = self.rfile.read(length).decode() sig_header = self.headers.get("X-PAXOS-LABS-SIGNATURE", "") ts_header = self.headers.get("X-PAXOS-LABS-TIMESTAMP", "") try: self._verify(raw_body, sig_header, ts_header) except Exception as e: self.send_response(401) self.end_headers() self.wfile.write(str(e).encode()) return event = json.loads(raw_body) print(f"Received {event['type']} — {event['id']}") self.send_response(200) self.end_headers() self.wfile.write(b"ok") def _verify(self, raw_body, sig_header, ts_header): ts = datetime.fromisoformat(ts_header) now = datetime.now(timezone.utc) if abs((now - ts).total_seconds()) > TOLERANCE: raise ValueError("Timestamp outside tolerance") expected = hmac.new( SECRET, f"{ts_header}.{raw_body}".encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig_header, expected): raise ValueError("Invalid signature") HTTPServer(("", 4000), Handler).serve_forever() ``` ```go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "math" "net/http" "os" "time" ) const tolerance = 300 // seconds func verify(body []byte, sigHeader, tsHeader, secret string) error { ts, err := time.Parse(time.RFC3339, tsHeader) if err != nil { return fmt.Errorf("bad timestamp: %w", err) } if math.Abs(time.Since(ts).Seconds()) > tolerance { return fmt.Errorf("timestamp outside tolerance") } mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "%s.%s", tsHeader, body) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(sigHeader), []byte(expected)) { return fmt.Errorf("invalid signature") } return nil } func main() { secret := os.Getenv("WEBHOOK_SECRET") http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := verify( body, r.Header.Get("X-Paxos-Labs-Signature"), r.Header.Get("X-Paxos-Labs-Timestamp"), secret, ); err != nil { http.Error(w, err.Error(), 401) return } var evt map[string]any json.Unmarshal(body, &evt) fmt.Printf("Received %s — %s\n", evt["type"], evt["id"]) w.WriteHeader(200) }) fmt.Println("Listening on :4000") http.ListenAndServe(":4000", nil) } ``` ## Step 3: Expose Your Local Server For local development, use a tunnel so Paxos Labs can reach your machine: ```bash theme={null} ngrok http 4000 ``` Copy the generated `https://` URL and use it as your webhook endpoint URL in the Paxos Labs Console. ## Step 4: Send a Test Event Back in the Paxos Labs Console, expand your endpoint row and click **Send Test**. Choose **ping** to fire a lightweight connectivity check. Your server should log output like: ``` Received ping — f47ac10b-58cc-4372-a567-0e02b2c3d479 ``` If the delivery succeeds, the endpoint's delivery history shows a **Success** badge with the HTTP status code. You're ready to subscribe to real events. ## Step 5: Subscribe to Events Edit your endpoint to add event types for the ones you care about. When those events occur in your organization, your server will receive them automatically. ## Next Steps Deep dive into the HMAC-SHA256 signature scheme, replay protection, and production best practices. Full reference of every event type with payload schemas and field descriptions.