# Pods MCP Server Source: https://docs.pods.finance/ai-tools/mcp-server Connect AI agents and coding assistants to Pods's DeFi infrastructure with zero-config tool discovery The Pods MCP Server exposes the full Pods API as a set of discoverable, typed tools for any MCP-compatible AI host — Claude Desktop, Cursor, Windsurf, and others. Your AI assistant can quote swaps, generate deposit bytecode, check wallet positions, and scaffold SDK projects without ever constructing a raw HTTP request. **Machine-readable resources:** * [llms.txt](/llms.txt) — Structured overview for LLM consumption * [OpenAPI Spec](/openapi.yaml) — Full API specification ## When to Use What | Use Case | Recommended | Why | | - | - | - | | Execute DeFi operations from a backend | **API** | Direct HTTP calls, full control | | Build a frontend with yield and swap UI | **SDK Widgets** | Handles wallets, signing, execution | | AI agent in an MCP-compatible host | **MCP Server** | Zero-config tool discovery, typed inputs and outputs | | Scaffold a new project inside your AI IDE | **MCP Server** | Generates all files for Next.js, React, Remix, Gatsby | | Decode errors inline while developing | **MCP Server** | `get_error_explanation` returns causes, retryability, and fixes | | Integrate without additional dependencies | **API** | Simple HTTP, no runtime required | For AI agents running inside MCP-compatible hosts like Claude, Cursor, or Windsurf, the MCP Server is the most streamlined path — it exposes every core endpoint as a discoverable tool with typed parameters so the agent never has to construct raw HTTP requests. ## Capabilities The server ships with **25 tools** and **9 prompts** across five categories. Get quotes, generate signed bytecode, and track status for same-chain and cross-chain swaps List strategies, get quotes, deposit and withdraw, check open positions and transaction history Scaffold frontend projects, validate provider configs, check SDK compatibility, and write env files Health checks, API key validation, token listing, and error code explanations Higher-order workflows that chain multiple atomic tools into a single orchestrated call — swap+deposit, best-yield lookup, rebalance, and integration pre-flight ## Minimal Tool Set These five tools cover the most common agent workflows. ### 1. Recommend Integration Path Analyzes your stack and recommends API Direct or SDK Widgets. **Tool:** `recommend_integration_path` **Key inputs:** `frontendFramework`, `walletProvider`, `backendLanguage`, `features` **Returns:** recommended path with rationale. ### 2. Get a Swap Quote Request a ready-to-execute quote for a token swap. **Tool:** `get_swap_quote` **Key inputs:** `fromChain`, `toChain`, `fromToken`, `toToken`, `fromAmount`, `fromAddress` **Returns:** quote object with `transactionRequest` and expiry timestamp. Quotes expire after 5 minutes. Call `execute_swap_bytecode` promptly after receiving one. ### 3. Get Deposit Bytecode Generate transactions for depositing into a yield strategy. **Tool:** `get_deposit_bytecode` **Key inputs:** `strategyId`, `amount`, `wallet`, `action` **Returns:** ordered array of transactions to sign and broadcast. The array may contain multiple transactions (e.g. token approval + deposit). Execute them in order. ### 4. Check Wallet Positions Get all open yield positions for a wallet address. **Tool:** `get_wallet_positions` **Key inputs:** `wallet` **Returns:** positions with current value, principal, profit, and APY per strategy, plus a total balance summary. ### 5. Explain an Error Decode a Pods error code with causes, retry guidance, and resolution steps. **Tool:** `get_error_explanation` **Key inputs:** `errorCode` **Returns:** human-readable description, causes, whether it is retryable, and fix suggestions. ## Skills Skills are higher-order workflows that chain multiple atomic tools into a single orchestrated call — `swap_and_deposit`, `find_best_yield_for_asset`, `rebalance_position`, `diagnose_integration`, plus two analytical prompts (`portfolio_briefing`, `integration_review`). They support `dryRun` preview mode, partial-success returns that preserve already-generated bytecode when a later step fails, and dedicated error codes consumable via `get_error_explanation`. Decision table, per-skill deep dives, dryRun and partial-success walkthroughs, skill-level error code reference, and runtime documentation resources. ## Pre-built Prompts The server ships with 9 conversation starters for common integration scenarios. Invoke them by name from your AI host. | Prompt | What It Does | | - | - | | `choose_integration_path` | Wizard that picks API Direct vs SDK Widgets for your stack | | `troubleshoot` | Diagnoses errors given a code, tx state, and description | | `api_quickstart` | Step-by-step guide for a given feature and language | | `api_integration_wizard` | Full flow: quote → bytecode → signing → status tracking | | `sdk_quickstart` | Fast path: scaffold → env setup → validate config | | `sdk_integration_wizard` | Complete SDK setup: compatibility → scaffold → validate | | `sdk_migration_guide` | Breaking changes and migration steps between SDK versions | | `portfolio_briefing` | Skills prompt: guided analytical briefing of a wallet's yield portfolio — positions, recent activity, underperformers, and recommended rebalances | | `integration_review` | Skills prompt: audit workspace Pods code against a pitfalls checklist (quote TTL, rawQuote forwarding, EIP-1193 value encoding, action names, webhook signature) | ## Decision Rules ### API Direct vs SDK Widgets * **API Direct** — your backend generates transactions and your frontend signs them with any wallet library (ethers.js, viem, wagmi) * **SDK Widgets** — you want pre-built React components that handle the full swap or yield UX with built-in wallet connection Call `recommend_integration_path` if you are unsure which path fits your stack. ### Handling Swap Status ``` Status: PENDING └── Poll every 10–30 seconds Status: DONE ├── substatus: COMPLETED → Transfer successful ├── substatus: PARTIAL → User received a different token (still success) └── substatus: REFUNDED → Tokens returned to sender Status: FAILED └── Check error message, may need user intervention ``` ## Observability In HTTP mode, the server includes a built-in dashboard at `/dashboard` showing tool call counts, latency, error rates, and session quality. Events are logged to NDJSON files and can be forwarded to a central collector for aggregation across multiple instances. ## Next Steps Higher-order workflows with dryRun, partial-success, and skill-level error codes Install and configure the MCP server in your AI host Complete reference for all 25 tools and 9 prompts Make your first API call in 5 minutes # MCP Server Setup Source: https://docs.pods.finance/ai-tools/mcp-setup Connect Claude Desktop, Claude Code, Cursor, Gemini CLI, Codex, and other AI agents to the Pods MCP Server The Pods MCP Server is hosted and ready to use — no installation required. Point your AI host at the server URL and add your API key to start using all 25 tools and 9 prompts. **Server URL:** `http://mcp.pods.finance/mcp` ## Prerequisites * A Pods API key — [get one here](https://www.pods.finance/plg/select-plan) * An MCP-compatible host: Claude Desktop or similar * Node.js installed (required for `npx`) *** ## Claude Desktop Open `~/Library/Application Support/Claude/claude_desktop_config.json` Open `%APPDATA%\Claude\claude_desktop_config.json` Create the file if it does not exist. ```json theme={null} { "mcpServers": { "pods": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.pods.finance/mcp", "--header", "x-api-key:your-api-key" ] } } } ``` Claude Desktop uses `mcp-remote` as a bridge to connect to remote HTTP servers. The `--allow-http` flag is required because `mcp-remote` enforces HTTPS by default. Close and reopen the app. The Pods tools will appear in the tool picker. Ask Claude "what Pods tools are available?" to confirm the connection. *** ## Claude Code ```bash theme={null} claude mcp add --transport http pods http://mcp.pods.finance/mcp --header "x-api-key:your-api-key" ``` Edit `~/.claude/mcp.json` (global) or `.claude/mcp.json` (project): ```json theme={null} { "mcpServers": { "pods": { "type": "http", "url": "http://mcp.pods.finance/mcp", "headers": { "x-api-key": "your-api-key" } } } } ``` Claude Code supports HTTP transport natively — no `mcp-remote` required. ```bash theme={null} claude mcp list ``` Ask Claude "what Pods tools are available?" to confirm the connection. *** ## Cursor Create or edit `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json theme={null} { "mcpServers": { "pods": { "command": "npx", "args": [ "-y", "mcp-remote", "http://mcp.pods.finance/mcp", "--header", "x-api-key:your-api-key", "--allow-http" ] } } } ``` 1. Open **Settings** (`Cmd+Shift+J`) → **Features** → **Model Context Protocol** 2. Click **Add new MCP server** 3. Fill in Name `pods`, Command `npx`, and the args above 4. Toggle the server **on** The `--allow-http` flag is required because `mcp-remote` enforces HTTPS by default. Open Cursor and ask in the chat "what Pods tools are available?" to confirm the connection. The Pods tools will appear in the tool picker. *** ## Google Gemini CLI ```bash theme={null} gemini mcp add --transport http \ --header "x-api-key:your-api-key" \ pods http://mcp.pods.finance/mcp ``` Edit `~/.gemini/settings.json`: ```json theme={null} { "mcpServers": { "pods": { "command": "npx", "args": [ "-y", "mcp-remote", "http://mcp.pods.finance/mcp", "--header", "x-api-key:your-api-key", "--allow-http" ] } } } ``` ```bash theme={null} gemini mcp list ``` Or, inside a Gemini chat session, run `/mcp enable pods`. Ask Gemini "what Pods tools are available?" to confirm the connection. *** ## OpenAI Codex ```bash theme={null} codex mcp add pods -- npx -y mcp-remote http://mcp.pods.finance/mcp --header x-api-key:your-api-key --allow-http ``` Edit `~/.codex/config.toml`: ```toml theme={null} [mcp_servers.pods] command = "npx" args = ["-y", "mcp-remote", "http://mcp.pods.finance/mcp", "--header", "x-api-key:your-api-key", "--allow-http"] ``` ```bash theme={null} codex mcp list ``` Ask Codex "what Pods tools are available?" to confirm the connection. *** ## Other Agents Any MCP-compatible host that accepts a `mcpServers` JSON config can connect to the Pods server using `mcp-remote`: ```json theme={null} { "mcpServers": { "pods": { "command": "npx", "args": [ "-y", "mcp-remote", "http://mcp.pods.finance/mcp", "--header", "x-api-key:your-api-key", "--allow-http" ] } } } ``` Replace `your-api-key` with your Pods API key. The `--allow-http` flag is required because `mcp-remote` enforces HTTPS by default. *** ## Next Steps Browse all 25 tools and 9 prompts with inputs and outputs Learn what the server does and when to use it # Skills Source: https://docs.pods.finance/ai-tools/mcp-skills Higher-order workflows that chain multiple atomic tools into a single orchestrated call Skills are **higher-order workflows** that compose multiple atomic MCP tools into a single orchestrated call. Instead of the agent manually chaining four or five tools (quote → bytecode → quote → bytecode → verify), a skill handles the full flow deterministically and returns all transaction bytecode in execution order, plus warnings and next-step guidance. The Pods MCP ships with **4 skill tools** and **2 skill prompts** — collectively "the skills layer". Skill tools execute code; skill prompts guide the agent through analytical workflows. Every workflow skill also has a dedicated markdown documentation resource accessible at runtime via `ReadMcpResource` — the URIs are listed at the bottom of this page. *** ## When to use a skill vs atomic tools | If you want to... | Use | Why | | - | - | - | | Swap then deposit in one step | `swap_and_deposit` | Deterministic chain; partial-success preserves swap bytecode on deposit failure | | Find the best-APY strategy for an asset | `find_best_yield_for_asset` | Auto-resolves via `get_yield_by_identifier`; returns alternatives | | Move capital between strategies | `rebalance_position` | Orchestrates withdraw + deposit; auto-resolves destination by APY | | Verify an integration works end-to-end | `diagnose_integration` | Pre-flight check of API key, health, and endpoint reachability in one call | | Generate a conversational portfolio summary | `portfolio_briefing` *(prompt)* | LLM synthesizes across positions, history, and yield alternatives | | Audit existing code for common bugs | `integration_review` *(prompt)* | Checklist-driven review with file:line findings | | Preview cost/APY before committing bytecode | any workflow skill with `dryRun: true` | Returns quotes + analysis without generating transaction data | | Chain custom logic between steps | **Don't use a skill** — call atomic tools directly | Skills are opinionated; custom logic needs atomic composition | *** ## Shared features All three workflow skills (`swap_and_deposit`, `find_best_yield_for_asset`, `rebalance_position`) support three capabilities that fundamentally change how agents and integrators interact with them. ### `dryRun: true` — preview mode When set, the skill **skips bytecode generation entirely** and returns only quotes + resolved strategy + estimated output. Useful for: * Cost/APY estimation before committing the user to sign transactions * UX flows that show "you will receive \~X tokens" before the confirm button * Analyzing rebalance viability without consuming a quote window ```json title="Example: swap_and_deposit with dryRun" theme={null} { "fromChain": "ethereum", "toChain": "base", "fromToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "toToken": "0x4200000000000000000000000000000000000006", "amountIn": "100000000", "wallet": "0xAbCd...1234", "depositIdentifier": "eth", "dryRun": true } ``` Response has `dryRun: true` at the top level and omits `swap.bytecode` / `deposit.bytecode`. ### Partial-success returns If a later step fails **after** earlier bytecode was already produced, the skill returns a partial-success payload with the usable work preserved. The result carries `isError: false` deliberately — the MCP host should treat the partial payload as normal structured data and let the caller decide how to proceed. ```json title="Partial-success shape" theme={null} { "status": "partial", "partial": { "swap": { "quoteId": "...", "bytecode": {...} } }, "failedAt": "deposit_chain", "failure": { "httpStatus": 404, "code": "STRATEGY_NOT_FOUND", "resolution": [...] } } ``` Preservation boundaries per skill: | Skill | Preserved on failure | `failedAt` label | | - | - | - | | `swap_and_deposit` | Swap quote + swap bytecode | `deposit_chain` | | `find_best_yield_for_asset` | Best strategy + alternatives + quote | `bytecode_generation` | | `rebalance_position` | Withdraw bytecode + APY delta + amount | `deposit_chain` | Recovery pattern: sign and broadcast the preserved bytecode (e.g. the swap), then call the atomic tool for the failed step separately once the underlying issue is resolved. ### Skill-level error codes Orchestration-layer validation failures raise `SkillValidationError` with dedicated codes — distinct from API errors. These flow through the same `McpErrorResult` shape, so `get_error_explanation` returns resolution steps for them just like for `QUOTE_EXPIRED` or `INSUFFICIENT_LIQUIDITY`. | Code | Meaning | Typical fix | | - | - | - | | `MISSING_DEPOSIT_TARGET` | `swap_and_deposit` received neither `depositStrategyId` nor `depositIdentifier` | Pass one of the two | | `ZERO_SWAP_OUTPUT` | Swap quote returned `tokenOut.amount === "0"` (no liquidity / bad route) | Smaller `amountIn`, different pair, or verify token addresses | | `NO_STRATEGY_FOR_ASSET` | `get_yield_by_identifier` didn't resolve for the given asset | Try a different symbol or browse `list_strategies` | | `SOURCE_POSITION_NOT_FOUND` | `rebalance_position` with `fromStrategyId` the wallet doesn't hold | Call `get_wallet_positions` first to see actual positions | | `ZERO_POSITION_BALANCE` | Could not determine a non-zero source balance, no `amount` given | Pass explicit `amount` in smallest unit | | `SAME_STRATEGY_REBALANCE` | Auto-resolver returned the same strategy as the source | Pass explicit `toStrategyId`, or accept the current strategy is already optimal | *** ## Workflow skills Swaps token A into token B on the destination chain and deposits the proceeds into a yield strategy. Chains `get_swap_quote` → `execute_swap_bytecode` → `get_strategy_quote` → `get_deposit_bytecode` internally. **When to use:** * User wants to earn yield on a token they don't currently hold * You have both a swap target and a deposit target in one user intent * You need both transaction batches returned in guaranteed execution order **Inputs:** | Field | Type | Required | Description | | - | - | - | - | | `fromChain` | string | yes | Source chain name (e.g. `ethereum`, `base`) | | `toChain` | string | yes | Destination chain where the strategy lives | | `fromToken` | string | yes | Source token contract address | | `toToken` | string | yes | Destination token — gets deposited into the strategy | | `amountIn` | string | yes | Amount in smallest unit of `fromToken` | | `wallet` | string | yes | Signs both txs and receives the swap output | | `depositStrategyId` | string | one of | Exact strategy ID | | `depositIdentifier` | string | one of | Asset symbol/category (`"usdc"`, `"eth"`) | | `dryRun` | boolean | no | Preview without generating bytecode. Default `false` | **Example response** (truncated): ```json theme={null} { "swap": { "quoteId": "qx_...", "provider": "teleswap", "tokenIn": { "symbol": "USDC", "amount": "100000000" }, "tokenOut": { "symbol": "ETH", "amount": "28500000000000000" }, "deadline": 1700000000, "bytecode": { "id": "...", "transactionData": [ {"to": "...", "data": "0x...", "value": "0", "chainId": "8453"} ] } }, "deposit": { "strategyId": "aave-eth-base", "resolvedVia": "identifier", "quote": { "estimatedOutput": "28450000000000000", "priceImpact": 0.1 }, "bytecode": { "id": "...", "bytecode": [{"to": "...", "data": "0x...", "value": "0", "chainId": "8453"}] } }, "executionOrder": [ { "step": "swap", "transactionCount": 1 }, { "step": "deposit", "transactionCount": 1 } ], "warnings": [...], "nextSteps": [...] } ``` **Execution order is mandatory** — sign and broadcast all swap transactions and wait for confirmation BEFORE submitting the deposit transactions. The deposit spends the swap output; if you send deposit first it will revert on-chain. The deposit is sized from the swap quote's expected output (`tokenOut.amount`). If real swap output is lower due to slippage, the deposit may revert — warn the user about this before they sign. Given an asset (symbol, name, or category), returns the highest-APY strategy with a deposit quote, ready-to-sign bytecode, and the top 3 alternatives. **When to use:** * User doesn't know a specific `strategyId` and wants "the best yield" for USDC / ETH / etc. * You want to present alternatives so the user can compare before committing * You need a deposit bytecode produced automatically for the winner **Inputs:** | Field | Type | Required | Description | | - | - | - | - | | `asset` | string | yes | Symbol, name, or category (`"usdc"`, `"eth"`) | | `wallet` | string | yes | Wallet that will deposit | | `amount` | string | yes | Amount in smallest unit | | `network` | string | no | Filter for alternatives search (e.g. `"polygon"`) | | `dryRun` | boolean | no | Skip bytecode generation. Default `false` | **Example response** (truncated): ```json theme={null} { "best": { "strategyId": "aave-usdc-polygon", "protocol": "aave", "network": "polygon", "apy": 6.21, "assetName": "usdc" }, "alternatives": [ { "id": "compound-usdc-polygon", "protocol": "compound", "apy": 5.52 }, { "id": "morpho-usdc-polygon", "protocol": "morpho", "apy": 4.81 } ], "quote": { "estimatedOutput": "999850000", "priceImpact": 0.015 }, "bytecode": { "id": "...", "bytecode": [...] }, "warnings": [...], "nextSteps": [...] } ``` Alternatives are filtered to the same `assetName` as the best match and exclude paused strategies. Passing a `network` only narrows alternatives — the best match from `get_yield_by_identifier` may be on a different chain. Moves capital from one yield strategy to another (typically for better APY), returning withdraw + deposit bytecode in execution order. **When to use:** * User has an existing yield position and a better alternative exists * You want the skill to auto-resolve the destination to the best-APY strategy for the same asset * You want the full position balance used when `amount` is omitted **Inputs:** | Field | Type | Required | Description | | - | - | - | - | | `wallet` | string | yes | Owns the source and destination positions | | `fromStrategyId` | string | yes | Current strategy to exit | | `toStrategyId` | string | no | If omitted, auto-resolved to best-APY for same asset | | `amount` | string | no | Defaults to full current position balance | | `dryRun` | boolean | no | Skip bytecode generation. Default `false` | **Example response** (truncated): ```json theme={null} { "from": { "strategyId": "aave-usdc-polygon", "apy": 4.0 }, "to": { "strategyId": "morpho-usdc-polygon", "apy": 7.5, "resolvedVia": "auto-best-apy" }, "apyDelta": 3.5, "amount": "250000000000", "withdraw": { "bytecode": { "id": "...", "bytecode": [...] } }, "deposit": { "quote": {...}, "bytecode": {...} }, "executionOrder": [ { "step": "withdraw", "transactionCount": 1 }, { "step": "deposit", "transactionCount": 2 } ], "warnings": [...], "nextSteps": [...] } ``` **Execution order is mandatory** — broadcast all withdraw transactions and wait for confirmation before the deposit. The deposit spends withdrawn funds. If the auto-resolver returns a destination with worse APY than the source, a high-priority warning is prepended — the skill still produces bytecode so the caller can confirm intent. *** ## Diagnostic skill Runs API key validation, API health, and reachability probes on strategies and tokens endpoints in a single call. Optionally probes `get_wallet_positions` if a `wallet` is passed. **When to use:** * Before shipping an integration to production * When something stopped working downstream and you want a one-shot diagnosis * Onboarding a new developer — faster than running `validate_api_key` + `get_health` + a test call separately **Inputs:** | Field | Type | Required | Description | | - | - | - | - | | `wallet` | string | no | If provided, also probes `get_wallet_positions` | **Example response** (truncated): ```json theme={null} { "overall": "pass", "summary": { "total": 5, "passed": 4, "failed": 0, "skipped": 1 }, "checks": [ { "name": "api_key", "status": "pass", "durationMs": 42.1 }, { "name": "api_health", "status": "pass", "durationMs": 28.9 }, { "name": "strategies_reachable", "status": "pass", "durationMs": 55.3, "details": { "totalDocs": 42 } }, { "name": "tokens_reachable", "status": "pass", "durationMs": 61.2 }, { "name": "wallet_positions", "status": "skipped" } ], "nextSteps": [...] } ``` The skill itself never returns `isError: true` — individual checks fail independently and the overall report captures them. A failed `api_key` check cascades: every subsequent check will fail until the key is fixed, so always resolve the first failing check first. *** ## Skill prompts Prompts guide the agent through analytical workflows where the value is in natural-language synthesis, not deterministic bytecode. Invoke them by name from your AI host. Guides the agent to produce a briefing of a wallet's yield portfolio: positions, recent activity, underperformers, and recommended rebalances. **Inputs:** `wallet` (required), `includeHistoryDays` *(optional, default "7")* **What the prompt instructs the agent to do:** 1. Call `get_wallet_positions` to snapshot current positions and total USD 2. For each position, record strategyId, protocol, network, assetName, APY, balance 3. Call `get_wallet_history` for top 2 positions (by USD value) with the configured window 4. Call `get_yield_by_identifier` for each position's asset and compare APY (\~0.5pp threshold for "underperforming") 5. Produce a summary: total value, top performers, recent activity, underperformers, recommended actions pointing to `rebalance_position` or `find_best_yield_for_asset` Audits the workspace's Pods integration code against a checklist of common pitfalls. Produces a file:line-level report with concrete fixes. **Inputs:** `workspacePath` *(optional)*, `feature` *(optional: `swap`, `yield`, `webhook`, `all`)* **Checklist highlights:** * **Common:** API key from env (not hardcoded), try/catch around API calls, fetch timeout, retry on `QUOTE_EXPIRED`/`SLIPPAGE_EXCEEDED` * **Swap:** quote TTL awareness, `rawQuote` forwarding to `execute_swap_bytecode`, response field is `transactionData` (not `transactions`), EIP-1193 value hex encoding, status polling uses `quoteId` (not bytecode id), cross-chain lifecycle assumptions * **Yield:** `action="lend"` (not `"deposit"`), `wallet` param (not `fromAddress`/`owner`), amount as string in smallest unit, swap→deposit ordering * **Webhook:** signature verification, idempotency via `quoteId`, fast 2xx ack, explicit event-type handling The prompt does NOT auto-edit — it only produces a report. You can then request specific fixes interactively. *** ## Runtime documentation resources Every skill has a markdown documentation resource accessible at runtime via `ReadMcpResource`. Agents can load these on demand when they need fuller context than the tool description provides. | URI | Content | | - | - | | `pods://skills/swap-and-deposit` | Full guide for `swap_and_deposit` | | `pods://skills/find-best-yield` | Full guide for `find_best_yield_for_asset` | | `pods://skills/rebalance-position` | Full guide for `rebalance_position` | | `pods://skills/diagnose-integration` | Full guide for `diagnose_integration` | | `pods://skills/portfolio-briefing` | Full guide for `portfolio_briefing` prompt | | `pods://skills/integration-review` | Full guide for `integration_review` prompt | *** ## Next Steps What the server does, capabilities, and minimal tool set Install and configure in Claude Desktop, Cursor, Windsurf Full accordion reference for all 25 tools and 9 prompts Complete error reference including skill-level codes # MCP Tools Reference Source: https://docs.pods.finance/ai-tools/mcp-tools-reference Complete reference for all 25 tools and 9 prompts exposed by the Pods MCP Server The Pods MCP Server exposes 25 tools and 9 prompts. Tools are callable by the AI agent during a session. Prompts are pre-built conversation starters you can invoke by name from your AI host to begin a guided workflow. *** ## Shared Tools Available regardless of integration path. Analyzes your developer stack and recommends whether to use API Direct or SDK Widgets. **Inputs:** | Field | Type | Description | | - | - | - | | `frontendFramework` | string | e.g. `next`, `react`, `remix`, `gatsby` | | `walletProvider` | string | e.g. `privy`, `dynamic`, `wagmi`, `ethers` | | `backendLanguage` | string | e.g. `typescript`, `javascript`, `python`, `go` | | `features` | string\[] | Features needed: `swap`, `yield`, `wallet-positions` | **Returns:** recommended path (API Direct or SDK Widgets) with rationale and suggested first steps. Checks Pods API health status. Use this to verify connectivity before starting a session or before running diagnostics. **Inputs:** none **Returns:** health status and API version. Validates an API key and returns account information. **Inputs:** | Field | Type | Description | | - | - | - | | `apiKey` | string | The API key to validate | **Returns:** key validity, account name, plan tier, and rate limit details. Lists all tokens supported by Pods with optional filtering by chain. **Inputs:** | Field | Type | Description | | - | - | - | | `chainId` | number *(optional)* | Filter tokens by EVM chain ID | **Returns:** array of token objects with address, symbol, decimals, and chainId. Decodes a Pods error code with causes, HTTP status, retryability flag, and resolution steps. **Inputs:** | Field | Type | Description | | - | - | - | | `errorCode` | string | Error code string (e.g. `QUOTE_EXPIRED`, `INSUFFICIENT_BALANCE`) | **Returns:** description, causes, whether it is retryable, and suggested fixes. *** ## API Direct — Swap Request a quote for a same-chain or cross-chain token swap. **Inputs:** | Field | Type | Description | | - | - | - | | `fromChain` | number | Source chain ID | | `toChain` | number | Destination chain ID | | `fromToken` | string | Token symbol or address | | `toToken` | string | Token symbol or address | | `fromAmount` | string | Amount in smallest unit (e.g. wei) | | `fromAddress` | string | Sender wallet address | **Returns:** quote object with `transactionRequest`, expected output amount, `feeBreakdown`, and expiry timestamp. Quotes expire after 5 minutes. Call `execute_swap_bytecode` promptly after receiving a quote. Generate ready-to-sign transaction bytecode from a confirmed swap quote. **Inputs:** | Field | Type | Description | | - | - | - | | `quoteId` | string | Quote ID returned by `get_swap_quote` | | `fromAddress` | string | Sender wallet address | **Returns:** ordered array of transaction objects (`to`, `data`, `value`, `chainId`) to sign and broadcast. Sign and broadcast each transaction in the returned order. Skipping any transaction will cause the swap to fail. Poll the status of a submitted swap transaction. **Inputs:** | Field | Type | Description | | - | - | - | | `txHash` | string | Transaction hash from the source chain | | `fromChain` | number *(optional)* | Source chain ID — speeds up lookup | | `toChain` | number *(optional)* | Destination chain ID | **Returns:** `status` (`PENDING`, `DONE`, `FAILED`) and `substatus` (`COMPLETED`, `PARTIAL`, `REFUNDED`). Poll every 10–30 seconds while status is `PENDING`. *** ## API Direct — Yield List all available DeFi yield strategies. **Inputs:** | Field | Type | Description | | - | - | - | | `network` | string *(optional)* | Filter by network name (e.g. `polygon`, `arbitrum`) | | `asset` | string *(optional)* | Filter by asset symbol (e.g. `USDC`) | **Returns:** paginated array of strategy objects with id, protocol, APY, fee, and available actions. Retrieve a single strategy with full details. **Inputs:** | Field | Type | Description | | - | - | - | | `strategyId` | string | Strategy identifier (e.g. `Aave-USDC-polygon`) | **Returns:** protocol details, asset, APY, supported actions, fees, and current state. Calculate estimated output and fees for a deposit or withdrawal before generating bytecode. **Inputs:** | Field | Type | Description | | - | - | - | | `strategyId` | string | Strategy identifier | | `amount` | string | Amount in asset's smallest unit | | `action` | string | `lend` or `withdraw` | | `wallet` | string | Wallet address | **Returns:** estimated output, fee charged, and slippage details. Generate transaction bytecode for depositing into a yield strategy. **Inputs:** | Field | Type | Description | | - | - | - | | `strategyId` | string | Strategy identifier | | `amount` | string | Deposit amount in asset's smallest unit | | `wallet` | string | Wallet address | | `action` | string | Typically `lend` | **Returns:** ordered array of transactions to sign and broadcast. May include a token approval transaction before the deposit. Execute all transactions in order. Each transaction depends on the one before it. Generate transaction bytecode for withdrawing from a yield strategy. **Inputs:** | Field | Type | Description | | - | - | - | | `strategyId` | string | Strategy identifier | | `amount` | string | Withdrawal amount in asset's smallest unit | | `wallet` | string | Wallet address | | `action` | string | Typically `withdraw` | **Returns:** ordered array of transactions to sign and broadcast. Retrieve all open yield positions for a wallet address. **Inputs:** | Field | Type | Description | | - | - | - | | `wallet` | string | Wallet address (checksummed or lowercase) | **Returns:** array of positions with current value, principal, profit, and APY per strategy, plus a total balance summary in USD. Retrieve transaction history for a wallet in a specific strategy. **Inputs:** | Field | Type | Description | | - | - | - | | `wallet` | string | Wallet address | | `strategyId` | string | Strategy identifier | **Returns:** paginated transaction history with timestamps, amounts, and status. Find the highest-APY strategy matching an asset name or yield category. **Inputs:** | Field | Type | Description | | - | - | - | | `identifier` | string | Asset name (e.g. `USDC`) or category (e.g. `stablecoin`) | **Returns:** top-performing strategy matching the identifier, with full strategy details. *** ## API Direct — Code Generation Generate a complete, runnable code example for a given feature and programming language. **Inputs:** | Field | Type | Description | | - | - | - | | `feature` | string | `swap`, `yield`, or `wallet-positions` | | `language` | string | `typescript`, `javascript`, `python`, `go`, or `curl` | **Returns:** complete code snippet with comments, ready to copy into a project. Includes environment setup, API call, and response handling. *** ## SDK Widgets Generate all files needed to integrate Pods SDK widgets into a frontend project. **Inputs:** | Field | Type | Description | | - | - | - | | `framework` | string | `next`, `react`, `remix`, or `gatsby` | | `features` | string\[] | `swap`, `yield`, or both | | `walletProvider` | string | Wallet connection library in use | **Returns:** complete file tree — components, providers, and environment templates — ready to copy into your project. Validate a `DeframeProvider` configuration object before using it in production. **Inputs:** | Field | Type | Description | | - | - | - | | `config` | object | The provider config object to validate | **Returns:** validation result with any errors or warnings and suggested fixes for each issue. Check compatibility between `pods-sdk`, React, and your wallet provider before installing or upgrading. **Inputs:** | Field | Type | Description | | - | - | - | | `sdkVersion` | string | Installed version of `pods-sdk` | | `reactVersion` | string | Installed version of React | | `walletProvider` | string | Wallet library name and version | **Returns:** compatibility matrix, known issues, and recommended version combinations. Write Pods environment variables to a `.env` or `.env.local` file. **Inputs:** | Field | Type | Description | | - | - | - | | `apiKey` | string | Your Pods API key | | `file` | string *(optional)* | Target file path. Default: `.env.local` | **Returns:** confirmation of which variables were written and the target file path. Ensure `.env` and `.env.local` are listed in your `.gitignore` before running this tool. *** ## Skills Higher-order workflows that chain multiple atomic tools into a single orchestrated call. Every workflow skill supports `dryRun: true` (preview without generating bytecode) and partial-success returns (preserves already-generated bytecode when a later step fails). Validation failures carry skill-specific error codes like `MISSING_DEPOSIT_TARGET`, `ZERO_POSITION_BALANCE`, and `SOURCE_POSITION_NOT_FOUND`, consumable via `get_error_explanation`. For decision tables, per-skill walkthroughs of `dryRun` and partial-success, and the full skill-level error code reference, see the [Skills Guide](/ai-tools/mcp-skills). This reference page keeps the accordions short. Each skill also has a markdown documentation resource at `pods://skills/` accessible via `ReadMcpResource`. Swap token A into token B on the destination chain and deposit the proceeds into a yield strategy in a single orchestrated call. Chains `get_swap_quote` → `execute_swap_bytecode` → `get_strategy_quote` → `get_deposit_bytecode` internally. **Inputs:** | Field | Type | Description | | - | - | - | | `fromChain` | string | Source chain name (e.g. `ethereum`, `polygon`, `base`) | | `toChain` | string | Destination chain name where the yield strategy lives | | `fromToken` | string | Source token contract address | | `toToken` | string | Destination token — gets deposited into the strategy | | `amountIn` | string | Amount in smallest unit of `fromToken` | | `wallet` | string | Wallet that signs both txs and receives the swap output | | `depositStrategyId` | string *(one of)* | Exact strategy ID. Takes precedence over `depositIdentifier` | | `depositIdentifier` | string *(one of)* | Asset symbol/category (e.g. `usdc`, `eth`) — resolved via `get_yield_by_identifier` | | `dryRun` | boolean *(optional)* | When `true`, skip bytecode generation and return only quotes. Default `false` | **Returns:** structured payload with `swap.bytecode`, `deposit.bytecode`, `executionOrder`, `warnings`, and `nextSteps`. On partial failure, returns `structuredContent.status === "partial"` with the swap bytecode preserved. The swap bytecode MUST be broadcast and confirmed before the deposit bytecode — the deposit spends the swap output. The deposit amount is sized from the swap quote's expected output; slippage can cause on-chain revert. Given an asset, return the highest-APY strategy with a deposit quote, ready-to-sign bytecode, and the top 3 alternatives. Chains `get_yield_by_identifier` → `list_strategies` → `get_strategy_quote` → `get_deposit_bytecode`. **Inputs:** | Field | Type | Description | | - | - | - | | `asset` | string | Symbol, name, or category accepted by `get_yield_by_identifier` (e.g. `usdc`, `eth`) | | `wallet` | string | Wallet that will deposit | | `amount` | string | Amount in smallest unit of the asset | | `network` | string *(optional)* | Network filter for alternatives search (e.g. `polygon`) | | `dryRun` | boolean *(optional)* | When `true`, return best + alternatives + quote without bytecode. Default `false` | **Returns:** `best` strategy, up to 3 `alternatives`, deposit `quote`, deposit `bytecode`, plus warnings and next-step guidance. Move capital from one yield strategy to another — typically for better APY. Returns withdraw + deposit bytecode in execution order. **Inputs:** | Field | Type | Description | | - | - | - | | `wallet` | string | Wallet that holds the source position | | `fromStrategyId` | string | Current strategy to exit | | `toStrategyId` | string *(optional)* | Destination. If omitted, auto-resolved to the best-APY strategy for the same asset | | `amount` | string *(optional)* | Rebalance amount in smallest unit. Defaults to the full current position balance | | `dryRun` | boolean *(optional)* | When `true`, return APY delta + amount without bytecode. Default `false` | **Returns:** `from` and `to` strategy details, `apyDelta`, `withdraw.bytecode`, `deposit.bytecode`, `executionOrder`, and warnings. On partial failure, withdraw bytecode is preserved. The withdraw bytecode MUST be broadcast and confirmed before the deposit — the deposit spends withdrawn funds. If the auto-resolver returns worse APY, a high-priority warning is prepended but bytecode is still generated so the caller can confirm intent. Pre-flight integration health check — runs API key validation, API health, and reachability probes on strategies and tokens endpoints. Run this before shipping to production. **Inputs:** | Field | Type | Description | | - | - | - | | `wallet` | string *(optional)* | If provided, also probes `get_wallet_positions` to verify that endpoint | **Returns:** `overall` (`pass` or `fail`), per-check breakdown with status, timing, and resolution hints. A failed `api_key` check cascades — every subsequent check will fail until the key is fixed. The `wallet_positions` check shows `skipped` when no wallet is passed and does not count against overall status. *** ## Prompts Prompts are pre-built conversation starters. Invoke them by name from your AI host to begin a guided workflow. Walks through a brief stack analysis and recommends API Direct or SDK Widgets for your project. **Inputs:** `feature`, `frontendFramework`, `walletProvider`, `backendLanguage` Use this at the start of a new integration to get a tailored recommendation before writing any code. Diagnoses errors given an error code, transaction state, and description. Returns probable causes and resolution steps. **Inputs:** `errorCode`, `txState`, `description` Use this when a transaction is failing or an API call returns an unexpected error. Step-by-step guided session from first API call to a confirmed on-chain transaction, tailored to a specific feature and language. **Inputs:** `feature`, `language` Use this to get from zero to working code as fast as possible. Full integration session covering the complete flow: quote → bytecode → signing → status tracking. Walks through each step interactively. **Inputs:** none — the wizard collects everything it needs during the session. Use this when building an end-to-end swap or yield integration for the first time. Fast-path SDK session: scaffold a project → write environment variables → validate the provider config. **Inputs:** none — the wizard collects framework and wallet provider during the session. Use this when you want working widget integration in the fewest possible steps. Complete SDK setup session: compatibility check → project scaffolding → provider config validation. **Inputs:** none — the wizard collects all required details interactively. Use this for a thorough SDK setup with checks at each stage before proceeding. Generates a migration guide for moving between `pods-sdk` versions, including all breaking changes and required code updates. **Inputs:** `fromVersion`, `toVersion` Use this before upgrading the SDK in a production project. Skills prompt: guides the agent through producing a conversational briefing of a wallet's yield portfolio — positions, recent activity, underperformers, and recommended rebalances. **Inputs:** `wallet` (required), `includeHistoryDays` *(optional)* The prompt instructs the agent to call `get_wallet_positions`, `get_wallet_history`, and `get_yield_by_identifier`, identify underperformers (\~0.5pp APY threshold), and suggest actions using `rebalance_position` or `find_best_yield_for_asset` where relevant. Skills prompt: audits the workspace's Pods integration code against a checklist of common pitfalls — API key handling, quote lifecycle, bytecode signing formats, action names, webhook signature verification. Produces a file:line-level report with concrete fixes. **Inputs:** `workspacePath` *(optional)*, `feature` *(optional: `swap`, `yield`, `webhook`, `all`)* Checklist includes: quote TTL (5 min), `rawQuote` forwarding to `execute_swap_bytecode`, `transactionData` vs `transactions` field name, EIP-1193 value hex encoding, `action=lend` (not `deposit`), `wallet` param (not `fromAddress`), and webhook idempotency. *** ## Next Steps Install the MCP server in Claude Desktop, Cursor, or Windsurf Make your first API call in 5 minutes without the MCP server # API health check Source: https://docs.pods.finance/api-reference/health/api-health-check /openapi.yaml get /health Check if the API is running # API Reference Source: https://docs.pods.finance/api-reference/introduction Complete reference for the Pods API ## Base URL All API requests should be made to: ``` https://api.pods.finance ``` ## Authentication Most endpoints require authentication via API key in the `x-api-key` header. `GET /health` is public. Customer dashboard endpoints (`GET /customers/me`, token management routes) require a **Bearer** token instead of an API key. See [Authentication](/getting-started/authentication) for details. This reference covers the current public endpoints, request/response formats, and integration patterns. ## Route paths Use each path exactly as shown in this reference. When a route includes `/v2`, that prefix is a required part of the URL. Unified routes such as the strategy catalog, strategy bytecode, and strategy status use their unversioned paths. Cross-resource routes such as `/actions` are also unversioned. For wallet data, choose the view that matches your use case: * `GET /actions?walletAddress={address}` — paginated wallet activity across swaps, transfers, and strategies * `GET /v2/wallets/{address}` — wallet overview with token balances, earn positions, and history * `GET /v2/strategies/{strategyId}?wallet={address}` — one strategy's position, history, PnL, and rate history ## Quick Links 5-minute integration guide Ready-to-use code samples Complete error code reference Real-time API status and uptime # Create BigDataCorp KYC iframe session Source: https://docs.pods.finance/api-reference/kyc/create-bigdatacorp-kyc-iframe-session /openapi.yaml post /api/v1/kyc/bigdatacorp/sessions Starts the BigDataCorp document and liveness capture flow. Pods creates a KYC profile with provider bigdatacorp, stores only safe references and CPF hash/last4, and returns the iframe URL plus the Pods-generated kycUserId. Requests reuse an active capture for up to seven minutes unless restartExistingSession is true, which creates a fresh iframe session in the same KYC profile. If the verified CPF maps to exactly one approved local BigDataCorp KYC profile or approved Avenia identity, Pods creates a customer-scoped approved profile from the existing Avenia IDs without starting a new capture. The response provides a safe completion URL and requiresWebView false for mobile compatibility. Otherwise, Pods can perform a bounded Avenia lookup while the CPF is present in this request. # Get fiat ramp limits Source: https://docs.pods.finance/api-reference/kyc/get-fiat-ramp-limits /openapi.yaml get /api/v1/limits Returns both on-ramp and off-ramp limits in one request. Top-level fields stay the selected rampProvider (default avenia) so existing clients do not break. providers lists remaining limits for every ramp provider with approved KYC on this wallet. UnblockPay remaining uses configurable per-customer USD caps minus committed extract volume (shared onramp/offramp bucket). Each directional object contains the effective remaining amount in the requested currency after applying both the matching local cap and the global USD cap, plus the remaining global cap in USD. The top-level remaining fields are deprecated aliases for the off-ramp values. # Get KYC status Source: https://docs.pods.finance/api-reference/kyc/get-kyc-status /openapi.yaml get /api/v1/kyc/status Returns the normalized Pods KYC status for a Pods-generated kycUserId. This is the canonical status endpoint for every KYC flow: Brazilian Sumsub share-token profiles, foreign Sumsub share-token profiles, BigDataCorp iframe profiles, and external evidence profiles. BigDataCorp profiles are polled while capture is pending; pending KYC attempts are refreshed before returning when possible. When an UnblockPay nest exists and is non-terminal, this call live-refreshes UnblockPay (customer + verification-details). After Path B document uploads, it also POSTs UnblockPay /check once — there is no separate check route. Path A (hosted Sumsub) never triggers /check because no uploads are recorded. Top-level status / brlaEnabled / usdEnabled stay Avenia. Pods also runs a durable background task to update the stored KYC state when clients stop polling. When a profile is rejected, read the reason object rather than rejectReason or providerResult — it is the only field guaranteed to stay stable across providers. For foreign USD flows, poll until status is approved and usdEnabled is true after currency unlock. # Import foreign Sumsub reusable KYC share token Source: https://docs.pods.finance/api-reference/kyc/import-foreign-sumsub-reusable-kyc-share-token /openapi.yaml post /api/v1/kyc/foreign/sumsub-share-token Submit a fresh Sumsub reusable KYC share token for a non-Brazil applicant after Sumsub approves the verification. Pods validates the foreign tax id, stores only a salted hash plus the last four digits, creates or reuses the customer profile and Avenia subaccount, imports the token into Avenia, and returns a Pods-generated kycUserId. Poll GET /api/v1/kyc/status until status is approved, then call POST /api/v1/kyc/currency-unlock to enable USD ramps. # Import Sumsub reusable KYC share token Source: https://docs.pods.finance/api-reference/kyc/import-sumsub-reusable-kyc-share-token /openapi.yaml post /api/v1/kyc/sumsub-share-token Submit a fresh Sumsub reusable KYC share token after Sumsub approves the applicant. Send the CPF whenever it is known; supply taxId instead when the applicant was verified with a document that carries no CPF, such as a passport. Pods validates the identifier, stores only a salted hash plus the last four digits, and revalidates any unique approved Avenia identity already associated with it. An approved match is reused in a new customer-scoped profile without consuming the fresh token. Otherwise, Pods creates or reuses the customer profile and Avenia subaccount, imports the token into Avenia, and returns a Pods-generated kycUserId. A submission without a CPF cannot be matched against identities approved under one, so it may be rejected downstream by Avenia as a duplicate tax id. # Link a wallet to a KYC profile Source: https://docs.pods.finance/api-reference/kyc/link-a-wallet-to-a-kyc-profile /openapi.yaml post /api/v1/kyc/{kycUserId}/wallets Links an extra wallet to an existing KYC profile so it can quote and ticket against the same kycUserId. walletAddress stays the primary address stored when the profile was created. Repeating the call for a wallet that is already linked returns the current list. A wallet can belong to only one kycUserId per customer. # Reset BigDataCorp KYC Source: https://docs.pods.finance/api-reference/kyc/reset-bigdatacorp-kyc /openapi.yaml post /api/v1/kyc/bigdatacorp/reset Archives one unfinished or retryable BigDataCorp profile matching both email and walletAddress within the authenticated customer. Approved, blocked, final-rejected, currency-enabled, and in-progress profiles cannot be reset. This operation only archives the local profile; it does not contact providers or create a new session. The old kycUserId becomes unavailable. A separate sessions request can reuse the same CPF, email, and wallet and follows normal approved Avenia identity reuse rules. Each request targets the currently matching active profile. # Submit completed BigDataCorp KYC to Avenia Source: https://docs.pods.finance/api-reference/kyc/submit-completed-bigdatacorp-kyc-to-avenia /openapi.yaml post /api/v1/kyc/bigdatacorp/submit Finalizes a completed BigDataCorp capture by fetching the provider images and identity data on demand, forwarding them to Avenia, and moving the Pods KYC profile to provider_pending when Avenia accepts the submit. If an exact approved Avenia identity is already associated with this profile, Pods returns approved and enables BRLA without submitting duplicate documents. Repeated requests while the Avenia submission is in progress return the existing provider_pending state and do not duplicate document uploads. A durable Pods background task continues to refresh the Avenia attempt after the response. If Avenia rejects the final submit before a normal pending attempt is created, Pods persists the provider failure on the KYC profile as rejected_retryable so GET /api/v1/kyc/status can return providerResult and providerMessage. Pods does not persist full CPF, address, raw images, or base64 payloads. # Unlink a wallet from a KYC profile Source: https://docs.pods.finance/api-reference/kyc/unlink-a-wallet-from-a-kyc-profile /openapi.yaml delete /api/v1/kyc/{kycUserId}/wallets/{walletAddress} Removes one linked wallet from the KYC profile. The last remaining wallet cannot be removed. If the unlinked address is the primary walletAddress, the next linked wallet becomes primary. # Unlock foreign currency capability Source: https://docs.pods.finance/api-reference/kyc/unlock-foreign-currency-capability /openapi.yaml post /api/v1/kyc/currency-unlock Default rampProvider=avenia: after Avenia approves a foreign KYC profile, submit the USD unlock, refresh account metadata, and return usdEnabled. Idempotent when Avenia USD is already enabled. rampProvider=unblockpay refreshes GET /v1/customers/{id}/rails, persists unblockpay.capabilities, and returns ramps.unblockpay.unlock with the extra bank fields needed to register a wire External Account for offramp. Sending those fields on the same POST creates the External Account. Top-level usdEnabled stays Avenia-only. # GET /ondo/stocks/market-status Source: https://docs.pods.finance/api-reference/ondo/get-ondostocksmarket-status /openapi.yaml get /ondo/stocks/market-status Returns Ondo GM market status with a stable response shape. The market-wide fields (`isOpen`, session fields, optional close reason) are always present. `asset` is `null` when `symbol` is omitted; when `symbol` is provided (GM token ending in lowercase `on`, e.g. `NVDAon`), `asset` nests per-symbol pause/limit status, market data, and a concrete `tradable` / `blockingReason` decision. `asset.tradable` is false for market closed/paused (`ONDO_MARKET_CLOSED` / `ONDO_MARKET_PAUSED`, unless off-hours are open) or an active asset pause (`ONDO_ASSET_PAUSED`). An active `ASSET_LIMITED` restriction only sets `limited: true` and does not block trading. # Create or retrieve smart account Source: https://docs.pods.finance/api-reference/smart-account/create-or-retrieve-smart-account /openapi.yaml post /smart-account Create or retrieve a generic ERC-4337 Gnosis Safe smart account for a given owner address and chain. Deployment is asynchronous; the deterministic CREATE2 address is returned immediately. # Send user operation Source: https://docs.pods.finance/api-reference/smart-account/send-user-operation /openapi.yaml post /aa/send-user-operation Submit a signed ERC-4337 v0.6 user operation to the bundler. Request the user operation payload from GET /v2/swap/quote or GET /strategies/:id/bytecode with output=userOperation, sign it, then send it here. Returns the bundler userOpHash. Only the v0.6 EntryPoint is supported. # Generate strategy transaction bytecode Source: https://docs.pods.finance/api-reference/strategies/generate-strategy-transaction-bytecode /openapi.yaml get /strategies/{strategyId}/bytecode Generate transactions for strategy operations. Supports same-chain deposits and withdrawals, swaps before deposits, cross-chain operations, Fireblocks output, user operations, and Solana instructions where supported. # Get pending strategy status Source: https://docs.pods.finance/api-reference/strategies/get-pending-strategy-status /openapi.yaml get /strategies/{strategyId}/status Returns pending investment actions for a wallet on a specific strategy. Pending cross-chain and CoW actions are refreshed against their providers before responding. # Get strategy analytics Source: https://docs.pods.finance/api-reference/strategies/get-strategy-analytics /openapi.yaml get /strategies/analytics Returns paginated analytics webhook events for the authenticated customer. Requires API key or Bearer token. Pass `period` explicitly (`all-time`, `24h`, `7d`, or `30d`). Minimum `limit` is 10. # Get strategy details Source: https://docs.pods.finance/api-reference/strategies/get-strategy-details /openapi.yaml get /v2/strategies/{strategyId} Returns enriched strategy metadata, spot position metrics, and rate history. Supply wallet to include wallet-scoped history and PnL. # Get strategy rate history Source: https://docs.pods.finance/api-reference/strategies/get-strategy-rate-history /openapi.yaml get /v2/strategies/{strategyId}/rate-history Returns paginated daily rate and APY history. The window ends on the previous UTC day so the response excludes an incomplete current-day snapshot. # Get strategy share-price history Source: https://docs.pods.finance/api-reference/strategies/get-strategy-share-price-history /openapi.yaml get /v2/strategies/{strategyId}/market-data Returns paginated share-price candles for a strategy. Resolution is selected by range: 1d uses 15-minute points, 1w/1m use hourly points, and longer ranges use daily points. # Get wallet PnL history for a strategy Source: https://docs.pods.finance/api-reference/strategies/get-wallet-pnl-history-for-a-strategy /openapi.yaml get /v2/strategies/{strategyId}/pnl Returns a daily unrealized-PnL timeline for a wallet in a strategy, based on the wallet's deposit and withdrawal cost basis and the strategy's daily share price. The `wallet` query parameter is required and must be owned by the authenticated customer, who must also have the strategy enabled. Amounts are underlying-asset raw units serialized as strings. Provide either `days` or an explicit `from`/`to` range. # Get wallet PnL history for a strategy (wallet in path) Source: https://docs.pods.finance/api-reference/strategies/get-wallet-pnl-history-for-a-strategy-wallet-in-path /openapi.yaml get /v2/wallets/{address}/strategies/{strategyId}/pnl Same as GET /v2/strategies/{strategyId}/pnl, but the wallet is supplied as the `address` path parameter instead of the `wallet` query parameter. The wallet must be owned by the authenticated customer, who must also have the strategy enabled. # Get wallet strategy details Source: https://docs.pods.finance/api-reference/strategies/get-wallet-strategy-details /openapi.yaml get /v2/wallets/{address}/strategies/{strategyId} Returns enriched strategy details, position, history, PnL, and rate history for the wallet supplied in the path. # List strategies Source: https://docs.pods.finance/api-reference/strategies/list-strategies /openapi.yaml get /strategies Get the paginated catalog of available DeFi yield strategies for the authenticated customer. # Fulfill a swap quote by transaction hash Source: https://docs.pods.finance/api-reference/swap/fulfill-a-swap-quote-by-transaction-hash /openapi.yaml get /v2/swap/{id}/fulfill Report a mined transaction for a quote so the API processes it immediately instead of waiting for indexer webhooks. The transaction is verified on-chain and replayed through the same detection pipelines a real indexer event would take. Processing is asynchronous; poll GET /v2/swap/status/{id} for the outcome. Safe to retry; resubmissions are idempotent. # Generate transaction bytecode Source: https://docs.pods.finance/api-reference/swap/generate-transaction-bytecode /openapi.yaml post /v2/swap/bytecode Generate executable transaction bytecode from a quote. The quote must not be expired. # Get swap quote Source: https://docs.pods.finance/api-reference/swap/get-swap-quote /openapi.yaml get /v2/swap/quote Returns a persisted quote for swapping tokens on same or different chains, including Pods Ramp fiat rails (`originChain` or `destinationChain` of `fiat`). Brazil Pix supports Pix↔USDC on `base`/`monad`, Pix↔BRLA on `polygon`, Pix↔BRS on `solana`, and Pix↔Morpho hyperUSDCa on `monad`. Crypto BRLA↔BRS inventory rails (no Avenia/fiat leg) are supported on both `polygon` and `base`. Foreign USD supports WIRE/ACH↔USDC on `ethereum`, `base`, and `polygon` (Avenia direct delivery; see Ramp guides). Optional `rampProvider` pins the fiat ramp provider (`avenia` default). Customer `extraRampProviders` is an allowlist only and never auto-selects UnblockPay. UnblockPay quotes today are Pix↔USDC and USD WIRE↔USDC on polygon/base. USD ACH or ethereum on UnblockPay returns `RAMP_PROVIDER_NOT_ELIGIBLE`. Provide either `originChain` + `destinationChain` or `chainIdIn` + `chainIdOut`. Provide exactly one of `amountIn` or `amountOut`. When `originAddress` is provided with `destinationAddress` (crypto / fiat onramp), `pixKey` (Pix offramp), or flat USD bank account fields (USD offramp), the response also includes executable fields (`transactionData`, `paymentInstructions`, `id`) — preferred for new integrations instead of `POST /v2/swap/bytecode`. Optional `usdPaymentMethod` selects WIRE (default) or ACH on USD rails. Quotes expire after 5 minutes; `quote.deadline` is a Unix timestamp in seconds and `quote.deadlineDate` is an ISO 8601 string (UTC). # Get swap status Source: https://docs.pods.finance/api-reference/swap/get-swap-status /openapi.yaml get /v2/swap/status/{id} Check the status of a saved swap quote by quote ID # Update swap status with a transaction hash Source: https://docs.pods.finance/api-reference/swap/update-swap-status-with-a-transaction-hash /openapi.yaml post /v2/swap/status/{id}/update/{txHash} Submit a mined EVM transaction hash for a saved swap quote. The transaction is verified on-chain. Deposit-address quotes are replayed through the swap payment validator synchronously and are only confirmed if payment validation succeeds. # List tokens Source: https://docs.pods.finance/api-reference/tokens/list-tokens /openapi.yaml get /tokens Returns the paginated token catalog available to the authenticated customer. Use includeExtended for coingeckoId and listedIn fields; enhanced responses also include token groups. # Get tracked operation Source: https://docs.pods.finance/api-reference/tracking/get-tracked-operation /openapi.yaml get /actions/{id} Get formatted execution status by ID. Cross-chain swaps and cross-chain strategy bytecode responses return the `id` for this endpoint. # List tracked operations Source: https://docs.pods.finance/api-reference/tracking/list-tracked-operations /openapi.yaml get /actions Lists tracked swap, transfer, and strategy operations for the authenticated customer. Supply walletAddress to retrieve activity for a specific wallet. # Generate transfer transaction bytecode Source: https://docs.pods.finance/api-reference/transfer/generate-transfer-transaction-bytecode /openapi.yaml get /transfer/bytecode Generate ready-to-sign transaction data for token transfers. Supports EVM native/ERC-20 transfers and Solana native/SPL transfers. The response includes an `id` for execution tracking. # Get wallet overview Source: https://docs.pods.finance/api-reference/wallets/get-wallet-overview /openapi.yaml get /v2/wallets/{address} Returns token balances, earn positions, and paginated wallet activity for a customer-owned wallet. History and raw investment actions remain included by default for backwards compatibility and can be omitted independently. # Changelog Source: https://docs.pods.finance/changelog API updates, new features, and documentation changes This page tracks significant changes to the Pods API and documentation. ## API Versioning The API reference shows `/v2` only where the current contract requires it. Unified routes such as `GET /strategies`, strategy bytecode, and strategy status use unversioned paths. Version-specific strategy detail, wallet, and swap routes include `/v2`, and the prefix is a required part of those URLs. Cross-resource routes such as `/actions` are also unversioned. *** ## 2026 ### September 2026 **API** * Ondo BSC CoW (`Ondo-*-bsc`) `GET /strategies/:id/bytecode` accepts independent `priceInUsd` (integer USD cents) and `expireAt` (UTC Unix seconds) on `request-lend` / `request-withdraw`. Client limits use 10 bps slippage; market quotes stay at 100 bps. Other strategies return `PRICE_IN_USD_NOT_SUPPORTED` / `EXPIRE_AT_NOT_SUPPORTED`. * `GET /actions/:id/cancel?wallet=` refunds a funded, unfilled **client-limit** Ondo BSC order. ExpireAt-only orders are not cancellable. HTTP 200 means the refund outbox was queued; `ACTION_CANCEL_REFUND_QUEUE_FAILED` (502) if queueing fails after `REFUNDED`. **Documentation** * [Ondo Global Markets](/guides/ondoglobal-markets): limit price, expiry matrix, quote fields (`clientLimitPriceUsd`, `spotPriceUsd`, `distanceBps`), and cancel * OpenAPI: `priceInUsd` / `expireAt` on strategy bytecode; `GET /actions/{id}/cancel` ### August 2026 **API** * Public Brazil Pix rails via `GET /v2/swap/quote` (+ bytecode): **Pix↔USDC** on Base and Monad; **Pix↔BRLA** on Polygon (mint-to-user onramp / user-send offramp); **Pix BRL ↔ BRS Solana** (1:1 BRLA inventory; user never holds BRLA); **Pix BRL ↔ BRZ Polygon** (1:1 BRLA inventory; offramp is bridge-direct + Track); **Pix↔Morpho hyperUSDCa** on Monad. BRLA, BRS, and BRZ rails charge no Pods/customer swap markup — amounts pass through Avenia / inventory. Deframe-solver Pix rails use two-step `ON_RAMP_FIAT` / `OFF_RAMP_FIAT` Actions with Moralis TRANSFER suppression. * Ramp KYC `walletAddress` accepts a checksummed EVM address **or** a Solana public key on Sumsub share-token and BigDataCorp session routes. * Solana-origin swap bytecode supports `output=instructions` (`{ instructions, lutsByAddress? }`). BRS offramp Memo V1 carries `quote.quoteId` (UUID). **Documentation** * [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp): Sumsub share token for non-Brazil users, USD currency unlock, and USD↔USDC quote patterns (bank fields resolve-or-create beneficiary on offramp quote) * OpenAPI: `POST /api/v1/kyc/foreign/sumsub-share-token`, `POST /api/v1/kyc/currency-unlock`; `KycStatusResponse` adds `usdEnabled` and `eurEnabled` * [Move money with Pods Ramp](/guides/ramp/quotes): supported-route matrix, USDC addresses for Base/Monad, Pix↔BRLA Polygon examples, **Pix↔BRS Solana**, **Pix↔BRZ Polygon**, and Morpho hyperUSDCa Monad onramp/offramp * [Set up Pods KYC](/guides/kyc/setup): KYC `walletAddress` may be a Solana public key * Ramp, KYC money-movement, and Swap guides now treat **`GET /v2/swap/quote` with addresses/`pixKey` as the only recommended path**; `POST /v2/swap/bytecode` is documented as legacy/optional * OpenAPI (`GET /v2/swap/quote` / `POST /v2/swap/bytecode`): one-call quote preference; bytecode marked legacy/optional; `WalletAddress` schema for EVM or Solana ### July 2026 **API — breaking** **`apy` is now the gross rate.** On `GET /strategies`, `GET /v2/strategies/{id}` and `GET /v2/wallets/{id}`, `apy` previously carried a partial fee deduction and now mirrors `grossAPY` — the raw protocol rate with no Pods fee subtracted. For a strategy earning 10% gross with a 5% performance fee and a 2% markup, `apy` changes from `0.095` to `0.10`. **If you display `apy` as a net figure you will now overstate returns.** Read `netAPY` instead. See [Fees](/getting-started/fees#apy-fields). * `netAPY` now deducts the **integrator markup fee** in addition to the performance fee: `grossAPY × (1 − (performanceFeeBps + markupFeeBps) / 10000)`. Previously only the performance fee was subtracted, so customers on a markup agreement were shown a net APY higher than they would actually earn. * `grossAPY` is now consistently the raw protocol rate on every endpoint. It was previously understated on Aave and Morpho strategies. * `apyAfterFees` has been **removed** from position responses. It was undocumented, absent on Aave and Morpho strategies, and duplicated `netAPY`. **Documentation** * [Fees](/getting-started/fees): new APY Fields section documenting which figures include Pods fees and the `netAPY` formula * OpenAPI: gross/net semantics documented on every APY field ### May 2026 **API** * `GET /v2/swap/quote`: optional `preferredProvider` query parameter to pin which swap provider’s quote is persisted when multiple providers support the route; response remains a single `{ quote }` object (best `expectedAmountOut` when omitted) **Documentation** * [Get a Quote](/guides/swaps/get-quote): `preferredProvider`, multi-route aggregation, and related error codes * OpenAPI (`/v2/swap/quote`): `preferredProvider` parameter and updated endpoint description ### April 2026 **Documentation** * Documented canonical chain fields (`chainIdIn`, `chainIdOut`, `chainId`) across OpenAPI, swap quote guide, architecture overview, and Cursor route rules—aligned with enriched swap, strategy, and tracking responses * Documented success-response contract (2xx JSON body, 204, no `success` flag) in OpenAPI intro, Architecture, and Cursor rules * Documented unified error envelope (removed redundant `httpStatus` in body); aligned [Error Codes](/error-codes) examples and rate-limit payload to live behavior * OpenAPI and Architecture: document ISO deadlines and strategy quote timestamps; aligned `scripts/generate-docs.js` strategy quote response schema. **API** * Added `/transfer/bytecode` to API Reference with canonical `originAddress`/`destinationAddress` and deprecated alias fields (`walletOrigin`, `walletDestination`) * Standardized success responses: removed `{ success: true/false }` envelopes; invalid API key on `GET /customers/by-api-key` is now **401**; Supabase signup webhook returns **400/409/500** with `HttpError` shape; member delete returns **204**; health and fulfill endpoints return JSON objects instead of bare strings * Standardized error JSON to `{ error: { code, message, details? } }` across `responser`, auth middlewares, 404/500 handlers, rate limits, and OpenAPI * Standardized date/time fields in JSON responses to **ISO 8601** (UTC): `GET /v2/swap/quote` `quote.deadlineDate`; wallet history DTOs; top-level dates on `GET /actions/:id`. ### March 2026 **Documentation** * Added [Why Pods](/getting-started/why-pods) page with competitive differentiation and build-vs-buy comparison * Added [Fees](/getting-started/fees) page documenting swap fees (25 BPS), yield performance fees (5%), and integrator markup * Added [Error Codes](/error-codes) reference with 20+ error codes organized by category * Added `llms.txt` for AI agent discoverability * Published OpenAPI spec at `docs.pods.finance/openapi.yaml` * Linked status page from Architecture and API Reference pages * Fixed rate limit documentation: aligned to 100 req/min, 1,000 req/hr per API key across all pages * Added `.env` template and `walletAddress` setup to Quickstart * Fixed strategy response shapes in docs to match live API (`data` pagination, flat `asset` field, composite `id`) * Aligned list-pagination docs across OpenAPI, Quickstart, yield guide, and examples: `pagination` now documents `total`, `totalPages`, and `hasMore` (replacing mongoose-style `totalRecords` / `hasNextPage` on public responses) * Updated all swap code examples to use `/v2/` endpoints **API** * Added cross-chain investment support * Added Lido staking strategies (ETH and USD) * Added covered savings xDAI strategy * Added fee administration endpoints for integrators * Fixed Lido addresses and ABIs * Fixed withdraw history tracking * Removed `request-lend` action ### February 2026 **Documentation** * Added iframe integration guides (Privy, wagmi/viem) * Added EarnWidget and SwapWidget documentation * Added LLM playbook for AI tool integration * Added external integration guides (Privy, Fireblocks, Dynamic) ### January 2026 **API** * Launched v2 swap endpoints (`/v2/swap/quote`, `/v2/swap/bytecode`, `/v2/swap/status/{id}`) * Added strategy analytics endpoint * Added wallet transaction history endpoint (`/wallets/{wallet}/history/{strategyId}`) * Added Sepolia testnet support for Aave strategies * Added Base network strategies **Documentation** * Migrated documentation to Mintlify * Added yield guides (check protocol info, deposit, check positions, withdraw) * Added swap guides (get quote, execute swap) * Added JavaScript code examples (strategy deposit, same-chain swap, cross-chain swap) *** ## Deprecation Notices | Endpoint | Status | Replacement | Removal Date | | - | - | - | - | | `GET /swap/quote` | Deprecated | `GET /v2/swap/quote` | TBD | | `POST /swap/bytecode` | Deprecated | `POST /v2/swap/bytecode` | TBD | | `GET /swap/status/{id}` | Deprecated | `GET /v2/swap/status/{id}` | TBD | # Error Codes Source: https://docs.pods.finance/error-codes Complete reference for all Pods API error codes, HTTP status codes, and resolution guidance All Pods API errors use one JSON envelope. The HTTP status line is the source of truth for the status code; the body is always: ```json theme={null} { "error": { "code": "QUOTE_NOT_FOUND", "message": "Quote not found or expired" } } ``` The `details` field is only present when there is extra structured context. For Zod validation failures (**422**), issues are in `details.errors` (path + message per field). Domain-specific data (for example `needsSignUp`) also lives under `details`. ## Quote Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `QUOTE_NOT_FOUND` | 404 | Quote not found or expired | Request a new quote — quotes are valid for 5 minutes | | `QUOTE_EXPIRED` | 400 | Quote has expired | Request a new quote using the same parameters | | `QUOTE_GENERATION_FAILED` | 400 | Could not generate a quote for the specified parameters | Verify token addresses, chain names, and amount format | | `QUOTE_VALIDATION_ERROR` | 400 | Quote validation failed | Check that `quoteId` and `rawQuote` match the original quote response | | `STELLAR_ACCOUNT_NOT_FOUND` | 400 | Destination Stellar account was not found on Horizon | Create/fund the `G…` account before requesting a PIX→USDC Stellar quote | | `TRUSTLINE_REQUIRED` | 400 | Account exists but has no USDC trustline | Establish a USDC trustline (`USDC:`) on the destination `G…` | | `TRUSTLINE_NOT_AUTHORIZED` | 400 | USDC trustline exists but is not authorized | Authorize the USDC trustline, then retry the quote | | `MUXED_DESTINATION_REQUIRED` | 400 | Offramp deposit requires a muxed Stellar destination | Client must submit Payment to the muxed `M…` deposit address (not bare `G…`) | ## Swap Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `NO_ROUTE_FOUND` | 400 | No swap route available between the specified chains or tokens | Try a different token pair or check supported networks | | `INVALID_PREFERRED_PROVIDER` | 400 | Unknown `preferredProvider` name on `GET /v2/swap/quote` | Use a registered provider id (`relay`, `mayan`, `lifi`, `cctp`, `1inch`, etc.) | | `PREFERRED_PROVIDER_NOT_ELIGIBLE` | 400 | `preferredProvider` does not support this chain/token route | Omit the parameter or pick a provider eligible for the route | | `PREFERRED_PROVIDER_QUOTE_UNAVAILABLE` | 400 | Eligible provider did not return a quote in time | Retry or omit `preferredProvider` to use the best available quote | | `INVALID_RAMP_PROVIDER` | 400 | Unknown `rampProvider` name on `GET /v2/swap/quote`, `GET /api/v1/limits`, or `POST /api/v1/kyc/currency-unlock` | Use a registered fiat provider (`avenia`, `unblockpay`). Do not pass a swap provider name here | | `RAMP_PROVIDER_NOT_ELIGIBLE` | 400 | `rampProvider` does not support this fiat rail, or this customer is not opted into that extra ramp provider (`extraRampProviders`) | Omit the parameter (defaults to `avenia`) or use a provider this customer is allowed to pin | | `UNBLOCKPAY_KYC_NOT_PROVISIONED` | 409 | UnblockPay currency-unlock or document upload before the nest exists | Submit UnblockPay PII first (`POST /api/v1/kyc/sessions/:id/submit` with `rampProvider=unblockpay`) | | `UNBLOCKPAY_USD_OFFRAMP_ACCOUNT_MISSING` | 409 | UnblockPay USD offramp quote/bytecode without a saved wire External Account | POST `/api/v1/kyc/currency-unlock` with `rampProvider=unblockpay` and the bank fields listed on `ramps.unblockpay.unlock.requiredBodyFields` | | `UNBLOCKPAY_KYC_REJECTED` | 409 | UnblockPay nest is hard-rejected; submit and document upload will not retry | Open a new identity flow or contact support | | `UNBLOCKPAY_KYC_IN_PROGRESS` | 409 | UnblockPay document upload while nest status is `under_review` | Poll `GET /api/v1/kyc/status` until review finishes or becomes `partially_rejected` | | `PROVIDER_NOT_FOUND` | 404 | Swap provider not found for the requested route | Use a supported chain and token combination | | `BYTECODE_GENERATION_FAILED` | 500 | Failed to generate transaction bytecode | Retry the request — if the issue persists, contact support | | `SWAP_EXECUTION_FAILED` | 500 | Failed to execute the swap transaction | Verify wallet balance, allowance, and gas fees | ## Validation Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `INVALID_AMOUNT_PARAMS` | 400 | Cannot specify both `amountIn` and `amountOut` simultaneously | Use only one of `amountIn` or `amountOut` per request | | `MISSING_AMOUNT_PARAMS` | 400 | Either `amountIn` or `amountOut` is required | Include at least one amount parameter in the request | | `INVALID_SLIPPAGE` | 400 | `slippage` must be between `0.0001` and `0.5` as a fraction of 1 | Use a value like `0.01` for 1% slippage | | `AMOUNT_IN_TOO_LOW_OFFRAMP` | 400 | Off-ramp `amountIn` must be greater than 1 USDC | Increase the input amount | | `AMOUNT_IN_TOO_HIGH_OFFRAMP` | 400 | Off-ramp `amountIn` must be less than 20k USDC | Reduce the input amount or contact support for higher limits | | `AMOUNT_OUT_TOO_LOW_OFFRAMP` | 400 | Off-ramp `amountOut` is below the route minimum (0.5 BRL on Pix BRLA/BRS/BRZ, 5 BRLA on USDC Pix) | Increase the output amount | | `AMOUNT_OUT_TOO_HIGH_OFFRAMP` | 400 | Off-ramp `amountOut` must be less than 110k BRLA | Reduce the output amount or contact support for higher limits | | `AMOUNT_IN_TOO_LOW_ONRAMP` | 400 | On-ramp `amountIn` is below the route minimum (0.5 BRL on Pix BRLA/BRS/BRZ, 1 BRLA on USDC Pix) | Increase the input amount | | `AMOUNT_IN_TOO_HIGH_ONRAMP` | 400 | On-ramp `amountIn` must be at most 100k BRLA | Reduce the input amount or contact support for higher limits | | `AMOUNT_OUT_TOO_LOW_ONRAMP` | 400 | On-ramp `amountOut` must be greater than 1 USDC | Increase the output amount | | `AMOUNT_OUT_TOO_HIGH_ONRAMP` | 400 | On-ramp `amountOut` must be at most 20k USDC | Reduce the output amount or contact support for higher limits | ## Bridge Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `TRANSFER_VALIDATION_FAILED` | 400 | Token transfer validation failed | Verify the token address, chain, and transfer amount | | `INSUFFICIENT_AMOUNT_FOR_REFUND` | 400 | Transferred amount is insufficient to cover the refund fee | Increase the transfer amount to cover bridge fees | | `DEPOSIT_NOT_FOUND` | 404 | Bitcoin deposit not found | Wait for on-chain confirmation or check the deposit address | ## KYC Evidence Errors Returned by `POST /v1/kyc/sessions/{kycUserId}/submit` before anything reaches the ramp provider. See [External Evidence KYC](/guides/kyc/external-evidence) for the flow and for the provider's own rejection reasons, which are separate and arrive on `/kyc/status`. | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `INVALID_EVIDENCE_IMAGE` | 422 | An image is unusable — not valid base64, or over the 8 MB limit | Re-encode or recompress the image below 8 MB | | `UNSUPPORTED_EVIDENCE_IMAGE_FORMAT` | 422 | The image bytes are neither JPEG nor PNG | Convert to JPEG or PNG — the format is read from the bytes, not from `mimeType` | | `EVIDENCE_URL_NOT_ALLOWED` | 422 | The image URL is not HTTPS, its host is not allowlisted, or it resolves to a private address | Use an HTTPS URL on a host Pods has allowlisted for your account | | `EVIDENCE_URL_EXPIRED` | 422 | The pre-signed signature had already expired when the request arrived | Re-sign the URL and submit again — size `X-Amz-Expires` to outlive the call | | `EVIDENCE_URL_FORBIDDEN` | 422 | The storage provider refused the request (403) | Check the signature, the bucket policy, and that the credential that signed it is still valid | | `EVIDENCE_URL_NOT_FOUND` | 422 | The object does not exist (404) | Verify the bucket and key — a deleted or renamed object cannot be fetched | | `EVIDENCE_URL_UNREACHABLE` | 422 | The storage provider could not be reached | Retry — if it persists, check DNS and the host's availability from the public internet | | `EVIDENCE_URL_TIMEOUT` | 422 | Fetching the images exceeded 8 seconds | Serve the objects from a faster region, or send the images as base64 instead | | `DOCUMENT_BACK_REQUIRED` | 422 | `RG` and `CNH` submissions need a back image | Include `documents.back` | | `DOCUMENT_BACK_NOT_ALLOWED` | 422 | A `PASSPORT` is single sided | Omit `documents.back` | | `INVALID_BIRTH_DATE` | 422 | The extracted birth date could not be parsed | Send `extracted.birthDate` as `YYYY-MM-DD` | | `LIVENESS_BELOW_THRESHOLD` | 422 | The attested liveness confidence is below the minimum configured for your account | Re-run the liveness check — no upload happened, so nothing needs cleaning up | | `IDENTITY_CPF_MISMATCH` | 409 | The CPF read from the document is not the CPF the session was created with | Terminal for this session — one person under two records is almost always a capture mistake | | `UNDER_18` | 409 | The applicant is a minor | Terminal — the ramp provider does not onboard minors | ## General Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `INTERNAL_SERVER_ERROR` | 500 | An unexpected error occurred | Retry the request — if the issue persists, contact support | | `SYMBIOSIS_API_ERROR` | 502 | Error communicating with the Symbiosis bridge API | Retry after a short delay — this is usually transient | ## Authentication Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `AUTHENTICATION_REQUIRED` | 401 | No credentials provided, or API key used on a route that requires a Bearer token with a user role | Use `x-api-key` for API routes, or `Authorization: Bearer ` for customer dashboard routes | | `INVALID_API_KEY` | 401 | API key is missing or not recognized | Verify your API key at [pods.finance](https://www.pods.finance/plg/select-plan) | | `INVALID_AUTH_TOKEN` | 401 | Bearer token is invalid or expired | Refresh your Supabase or Magic session token | | `VALIDATION_ERROR` | 422 | Request validation failed (e.g. malformed Authorization header on `GET /customers/me`) | Send `Authorization: Bearer ` in the correct format | | — | 401 | No API key provided | Include `x-api-key` header in your request | | — | 429 | Rate limit exceeded | Wait for the retry window — see `retryAfter` in the response body | ## Transaction Errors | Code | HTTP Status | Description | Resolution | | - | - | - | - | | `TX_NOT_FOUND` | 404 | Transaction hash not found on the specified chain | Verify the hash is correct, mined, and on the expected chain | | `TX_NOT_MINED` | 422 | Transaction exists but is not yet mined | Wait for confirmation and retry | | `TX_REVERTED` | 422 | Transaction was mined but reverted on-chain | Inspect the transaction on a block explorer | ### Rate Limit Response When you hit a rate limit, the body matches the same envelope, for example: ```json theme={null} { "error": { "code": "RATE_LIMITED", "message": "Too many requests" } } ``` Some endpoints also send a **`Retry-After`** response header (seconds). Prefer that header when present. See [Authentication](/getting-started/authentication) for rate limit details. # Cross-Chain Swap Source: https://docs.pods.finance/examples/javascript/cross-chain-swap Complete example of executing a cross-chain token swap with bridge integration This example demonstrates how to swap tokens across different blockchain networks using the Pods API with JavaScript/TypeScript. ## Overview This example covers: * Getting an executable cross-chain quote in **one call** (`GET /v2/swap/quote` with wallet addresses) * Executing the origin-chain legs as one atomic EIP-7702 batch * Monitoring status across chains (`quoteId` vs the Action `id`) ## Prerequisites * Node.js 18+ * Pods API key * Wallet with tokens on the origin chain * Basic understanding of async/await ## Choose Your Library Using ethers.js v6 for blockchain interactions - the most popular Ethereum library. Using viem for blockchain interactions - a modern, TypeScript-first alternative with better performance. ## Installation ```bash theme={null} npm install axios ethers ``` ```bash theme={null} npm install axios viem ``` ## Environment Variables Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... PRIVATE_KEY=0x... ORIGIN_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-key DESTINATION_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-key ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) ``` ### 2. Get an Executable Cross-Chain Quote (one call) Pass `originAddress`, `destinationAddress`, and `refundAddress` so the response already includes executable `transactionData` for the origin chain. New integrations should **not** call the legacy `POST /v2/swap/bytecode`. ```javascript theme={null} const { data } = await pods.get('/v2/swap/quote', { params: { originChain: 'ethereum', destinationChain: 'polygon', tokenIn: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum tokenOut: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon amountIn: '1000000000', // 1000 USDC (6 decimals) originAddress: walletAddress, destinationAddress: walletAddress, refundAddress: walletAddress } }) const { quote, transactionData, id: actionId } = data console.log('Quote ID:', quote.quoteId) // → GET /v2/swap/status/{quoteId} console.log('Action ID:', actionId) // → GET /actions/{id} console.log('Expected output:', quote.tokenOut.amount) console.log('Legs to batch on origin:', transactionData.length) ``` `quote.quoteId` and the top-level `id` are **different identifiers**. Use `quote.quoteId` with `GET /v2/swap/status/{quoteId}` for quote settlement, and the top-level `id` with `GET /actions/{id}` for the on-chain Action execution record. Cross-chain swaps use bridge providers like TeleSwap or Mayan and may take several minutes to complete. ### 3. Execute Origin Transactions (atomic batch) All origin-chain legs must go in **one atomic transaction**. See [Executing Bytecode](/getting-started/executing-bytecode) and [`cross-chain-swap.js`](/examples/javascript/cross-chain-swap.js). ```javascript theme={null} import { http, createWalletClient, publicActions } from 'viem' import { mainnet } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { executeLegsViaEip7702 } from './eip7702-batch.js' const account = privateKeyToAccount(process.env.PRIVATE_KEY) const walletClient = createWalletClient({ account, chain: mainnet, transport: http(process.env.ORIGIN_RPC_URL) }).extend(publicActions) const receipt = await executeLegsViaEip7702(walletClient, transactionData) console.log('✅ Origin batch confirmed in block', receipt.blockNumber) console.log('⏳ Waiting for bridge to process...') ``` ### 4. Monitor Status Poll quote settlement with `quote.quoteId`. When you need the on-chain Action execution record instead, poll `GET /actions/${actionId}` with the top-level `id` from the quote response. ```javascript theme={null} // Poll for status updates const swapId = quote.quoteId let status = 'pending' while (!['fulfilled', 'failed', 'refunded', 'expired'].includes(status)) { await new Promise(resolve => setTimeout(resolve, 10000)) // Wait 10s const { data: statusData } = await pods.get(`/v2/swap/status/${swapId}`) status = statusData.status console.log('Status:', status) if (statusData.originTxHash) { console.log('Origin TX:', statusData.originTxHash) } if (statusData.destinationTxHash) { console.log('Destination TX:', statusData.destinationTxHash) } } if (status === 'fulfilled') { console.log('Cross-chain swap completed successfully!') } else { console.error('Swap did not complete:', status) } ``` ## Webhook Alternative Instead of polling, you can configure webhooks to receive status updates: ```javascript theme={null} // Configure webhook in your Pods dashboard // Webhook payload will include: { "swapId": "550e8400-e29b-41d4-a716-446655440000", "status": "fulfilled", "originTxHash": "0x123...", "destinationTxHash": "0x456...", "timestamp": "2024-01-24T12:35:00.000Z" } ``` ## Error Handling ```javascript theme={null} try { // Your code here } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error console.error('API Error:', apiError.code) console.error('Message:', apiError.message) if (apiError.code === 'QUOTE_EXPIRED') { console.log('Quote expired, please request a new one') } else if (apiError.code === 'NO_ROUTE_FOUND') { console.log('No cross-chain route available') } } else { console.error('Unexpected error:', error.message) } } ``` ## Running the Example **Download both files into the same folder** to run this example: [`cross-chain-swap.js`](/examples/javascript/cross-chain-swap.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} # Install dependencies npm install # Set environment variables cp .env.example .env # Edit .env with your values # Run the example node cross-chain-swap.js ``` ## Expected Output ``` 1️⃣ Requesting cross-chain swap quote (one-call — returns executable transactionData)... Provider: TeleSwap Legs to batch on origin: 2 quoteId: 550e8400-e29b-41d4-a716-446655440000 | actionId: 65f1a2b3c4d5e6f789012345 2️⃣ Executing atomic EIP-7702 batch on origin chain... ✅ Origin batch confirmed in block 12345678 3️⃣ Monitoring bridge status via quoteId... Status: confirmed Status: processing Status: fulfilled 🎉 Cross-chain swap fulfilled ``` ## Next Steps Execute same-chain token swaps Deposit into yield strategies Learn more about swaps View complete API documentation # KYC (Backend) Source: https://docs.pods.finance/examples/javascript/kyc-backend Server-to-server KYC: submit a Sumsub share token or external evidence, then poll status This example demonstrates the Pods KYC flow from your **backend**. KYC is a pure server-to-server flow — no wallet signing. You onboard a user with one of the KYC flows, then poll `GET /api/v1/kyc/status` until the profile reaches a terminal state. Keep `PODS_API_KEY` on your **backend**. Never expose it in a browser or mobile client. ## Overview This example covers: * **Sumsub reusable KYC** (Brazil Pix) — `POST /api/v1/kyc/sumsub-share-token` * **External evidence** — `POST /api/v1/kyc/sessions` then `POST /api/v1/kyc/sessions/{kycUserId}/submit` * **Status polling** — `GET /api/v1/kyc/status` ## Prerequisites * Node.js 18+ * Pods API key * For Sumsub: an approved Sumsub applicant + a freshly generated share token * For external evidence: liveness + document images you captured yourself ## Installation ```bash theme={null} npm install axios ``` ## Environment Variables ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... # Sumsub flow SUMSUB_SHARE_TOKEN=... SUMSUB_APPLICANT_ID=... # External-evidence flow (base64 image bytes) LIVENESS_IMAGE_BASE64=... DOC_FRONT_BASE64=... DOC_BACK_BASE64=... ``` ## Step-by-Step Walkthrough ### 1. Initialize API Client + status poller ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) const TERMINAL = ['approved', 'rejected', 'rejected_retryable', 'blocked'] async function pollKycStatus (kycUserId) { while (true) { const { data: status } = await pods.get('/api/v1/kyc/status', { params: { kycUserId } }) console.log('status:', status.status, 'brlaEnabled:', status.brlaEnabled) if (TERMINAL.includes(status.status)) return status await new Promise(resolve => setTimeout(resolve, 5000)) } } ``` ### 2. Sumsub Reusable KYC (Brazil Pix) Submit the fresh Sumsub share token your backend generated for the approved applicant. Pods returns a `kycUserId` — store it for status checks. ```javascript theme={null} const { data } = await pods.post('/api/v1/kyc/sumsub-share-token', { cpf: '52998224725', shareToken: process.env.SUMSUB_SHARE_TOKEN, sumsubApplicantId: process.env.SUMSUB_APPLICANT_ID, email: 'user@example.com', walletAddress: process.env.WALLET_ADDRESS }) console.log('kycUserId:', data.kycUserId, 'status:', data.status) await pollKycStatus(data.kycUserId) ``` ### 3. External Evidence Create a session, then submit the identity evidence you captured. Each image is either inline `base64` or an HTTPS `url` Pods fetches — pick one per image. ```javascript theme={null} const { data: session } = await pods.post('/api/v1/kyc/sessions', { cpf: '12345678909', email: 'maria@example.com', walletAddress: process.env.WALLET_ADDRESS }) const { data: submit } = await pods.post(`/api/v1/kyc/sessions/${session.kycUserId}/submit`, { applicant: { fullName: 'Maria Silva', dateOfBirth: '1990-05-14', phone: '+5511999999999' }, address: { country: 'BRA', state: 'SP', city: 'São Paulo', zipCode: '01310100', streetAddress: 'Av. Paulista', number: '1000', complement: 'apto 51' }, liveness: { image: { base64: process.env.LIVENESS_IMAGE_BASE64, mimeType: 'image/jpeg' }, attestation: { provider: 'aws_rekognition_face_liveness', status: 'SUCCEEDED', confidence: 95.78, sessionId: '47fa2a26-bb7a-4ca0-85f1-129ad6a6eb0b', challenge: { type: 'FaceMovementAndLightChallenge', version: '2.0.0' }, completedAt: '2026-08-13T14:58:10Z' } }, documents: { documentType: 'RG', front: { base64: process.env.DOC_FRONT_BASE64, mimeType: 'image/jpeg' }, back: { base64: process.env.DOC_BACK_BASE64, mimeType: 'image/jpeg' }, attestation: { provider: 'bigdatacorp_documentoscopy', onboardingId: 'b7d2...', extracted: { fullName: 'MARIA SILVA', birthDate: '1990-05-14', cpf: '12345678909', documentType: 'RG' } } } }) console.log('submit status:', submit.status) await pollKycStatus(session.kycUserId) ``` `/submit` does real work (creates the subaccount + uploads images) and can take \~20s. A timeout or non-JSON body does **not** mean failure — always poll `/status` before concluding. See [External Evidence KYC](/guides/kyc/external-evidence). ## Status values `created` → `provider_pending` → `approved` / `rejected` / `rejected_retryable` / `blocked`. Money movement is enabled once `status` is `approved` and the capability flag you need (`brlaEnabled` for Brazil, `usdEnabled` for foreign USD) is `true`. ## Running the Example ```bash theme={null} node kyc-backend.js sumsub # or: external-evidence ``` See the full runnable script: [`kyc-backend.js`](/examples/javascript/kyc-backend.js). ## Next Steps Move money once the wallet is approved All KYC flows and endpoints Full evidence payload and rejection handling View complete API documentation # Ondo Global Markets (RWA) Source: https://docs.pods.finance/examples/javascript/ondo Buy and sell tokenized stocks & ETFs on BSC, with cross-chain funding and async order tracking This example demonstrates buying and selling Ondo Global Markets tokenized stocks & ETFs with the Pods API. Market-share strategies live on **BSC** (`Ondo--bsc`), but you can fund a buy from — or receive a sell payout on — another EVM chain; Pods handles the bridge. **Settlement is asynchronous.** The on-chain transaction submits the request in seconds; the order reaches `SUCCESS` only after Ondo fills it (minutes later). Track it via `GET /actions/{id}`. ## Overview This example covers: * **Market status** — `GET /ondo/stocks/market-status` * **Buy** (`action=request-lend`) — fund cross-chain from Base USDC * **Sell** (`action=request-withdraw`) — redeem shares, receive USDC cross-chain * **Async order tracking** — `GET /actions/{id}` * **Optional limit / expiry** — `priceInUsd` (USD cents) and `expireAt` (Unix seconds) on Ondo BSC bytecode; cancel via `GET /actions/{id}/cancel?wallet=` ## Prerequisites * Node.js 18+ * Pods API key * Wallet with USDC to buy (or an open Ondo position to sell) ## Installation ```bash theme={null} npm install axios viem ``` ## Environment Variables ```bash theme={null} PODS_API_KEY=your-api-key PRIVATE_KEY=0x... BSC_RPC_URL=https://bsc-dataseed.binance.org BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key ``` ## Step-by-Step Walkthrough ### 1. Check Market Status ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) const { data: market } = await pods.get('/ondo/stocks/market-status', { params: { symbol: 'NVDAon' } }) console.log('Market open:', market.isOpen, market.marketStatus) ``` ### 2. Buy (request-lend) — cross-chain funding Fund the BSC position from Base USDC. `amount` is in the `fromTokenAddress` token's base units (6 decimals for Base USDC; 18 for BSC USDC). The response returns origin-chain `bytecode` legs and an Action `id`. ```javascript theme={null} const { data: buyBytecode } = await pods.get('/strategies/Ondo-NVDA-bsc/bytecode', { params: { action: 'request-lend', wallet: walletAddress, amount: '10000000', // $10 in Base USDC (6 decimals) fromChainId: 8453, fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base } }) console.log('actionId:', buyBytecode.id) console.log('cross-chain:', buyBytecode.crossChain.isCrossChain, buyBytecode.chainIdIn, '→', buyBytecode.chainIdOut) ``` To pin a client CoW limit (cents) and/or `validTo`, add `priceInUsd` and `expireAt`. They are independent and only work on Ondo BSC. See [limit price, expiry, and cancel](/guides/ondoglobal-markets#limit-price-expiry-and-cancel). ```javascript theme={null} const expireAt = Math.floor(Date.now() / 1000) + 3600 const { data: limitBuy } = await pods.get('/strategies/Ondo-NVDA-bsc/bytecode', { params: { action: 'request-lend', wallet: walletAddress, amount: '10000000', fromChainId: 8453, fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', priceInUsd: 32000, // $320.00 expireAt } }) console.log('limit $', limitBuy.quote.clientLimitPriceUsd, 'vs spot', limitBuy.quote.spotPriceUsd) ``` ### 3. Sell (request-withdraw) — cross-chain payout Redeem market shares and receive USDC on Base. `amountInShares` is the share amount (18 decimals). ```javascript theme={null} const { data: sellBytecode } = await pods.get('/strategies/Ondo-NVDA-bsc/bytecode', { params: { action: 'request-withdraw', wallet: walletAddress, amountInShares: '5000000000000000000', // 5 shares (18 decimals) toChainId: 8453, toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base } }) ``` ### 4. Execute the request (atomic batch on the origin chain) Batch all origin-chain `bytecode` legs into **one transaction**. See [Executing Bytecode](/getting-started/executing-bytecode). ```javascript theme={null} import { http, createWalletClient, publicActions } from 'viem' import { bsc, base } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { executeLegsViaEip7702 } from './eip7702-batch.js' const CHAIN_BY_ID = { 56: bsc, 8453: base } const RPC_BY_ID = { 56: process.env.BSC_RPC_URL, 8453: process.env.BASE_RPC_URL } const account = privateKeyToAccount(process.env.PRIVATE_KEY) const originChainId = Number(buyBytecode.chainIdIn) const walletClient = createWalletClient({ account, chain: CHAIN_BY_ID[originChainId], transport: http(RPC_BY_ID[originChainId]) }).extend(publicActions) const receipt = await executeLegsViaEip7702(walletClient, buyBytecode.bytecode) console.log('Request submitted in block:', receipt.blockNumber) ``` ### 5. Track the async order fill The on-chain tx only submits the request. Poll the Action `id` until the order reaches a terminal state. For cross-chain, the bridge leg settles first, then the order. ```javascript theme={null} async function waitForOrder (id) { const terminal = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED'] while (true) { const { data: action } = await pods.get(`/actions/${id}`) console.log('Order status:', action.status) if (terminal.includes(action.status)) return action.status await new Promise(resolve => setTimeout(resolve, 10000)) } } await waitForOrder(buyBytecode.id) ``` An unfilled **client-limit** order (`priceInUsd` set) can be cancelled after funding: ```javascript theme={null} const { data: cancelled } = await pods.get(`/actions/${limitBuy.id}/cancel`, { params: { wallet: walletAddress } }) console.log('cancelled status:', cancelled.status) ``` For a strategy-scoped view of pending orders, use `GET /strategies/{id}/status?wallet={address}` — it returns `hasPending`, per-action `orderBook` eligibility, and the `actions[]` list. ## Running the Example **Download both files into the same folder** to run this example: [`ondo.js`](/examples/javascript/ondo.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} node ondo.js market # or: buy | sell ``` See the full runnable script: [`ondo.js`](/examples/javascript/ondo.js). ## Next Steps Full buy/sell, cross-chain, and accounting details Read positions and track actions Bridge tokens across chains View complete API documentation # Pods Ramp (Fiat On/Off-Ramp) Source: https://docs.pods.finance/examples/javascript/ramp Pix on/off-ramp and foreign USD ACH/WIRE onramp using the one-call quote flow This example demonstrates fiat on/off-ramp with Pods Ramp using the Pods API. Every route uses `GET /v2/swap/quote`: `originChain`/`destinationChain` of `fiat` select the local rail, and `tokenIn`/`tokenOut` select the fiat currency or on-chain asset. Money-movement routes are available only after the wallet's Ramp KYC profile is `approved`. See the [KYC backend example](/examples/javascript/kyc-backend) and [Set up Pods KYC](/guides/kyc/setup). ## Overview This example covers: * **Pix onramp** — Pix BRL → USDC (show the Pix code, then poll) * **Pix offramp** — USDC → Pix BRL (sign `transactionData`, then poll) * **Foreign USD onramp** — USD ACH/WIRE → USDC (show bank details, then poll) ## Prerequisites * Node.js 18+ * Pods API key * A wallet with an `approved` Ramp KYC profile (offramp additionally requires the signer key) ## Installation ```bash theme={null} npm install axios viem ``` ## Environment Variables ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... # user wallet receiving crypto (onramps) PRIVATE_KEY=0x... # signer for offramps (approved Ramp wallet) BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key PIX_KEY=user-pix-key@example.com ``` Send all requests from your **backend**. Never expose `PODS_API_KEY` in a browser or mobile client. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) const TERMINAL = ['fulfilled', 'failed', 'refunded', 'expired'] async function pollSwapStatus (quoteId) { while (true) { await new Promise(resolve => setTimeout(resolve, 10000)) const { data: status } = await pods.get(`/v2/swap/status/${quoteId}`) console.log('Status:', status.status) if (TERMINAL.includes(status.status)) return status.status } } ``` ### 2. Pix Onramp (BRL → USDC) Pass `destinationAddress`; the response includes `paymentInstructions.pix.copyPaste`. There is no on-chain transaction — the user pays the Pix code. ```javascript theme={null} const { data } = await pods.get('/v2/swap/quote', { params: { originChain: 'fiat', destinationChain: 'base', tokenIn: 'BRL', tokenOut: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base amountIn: '1000', // BRL 10.00 (2 decimals) destinationAddress: process.env.WALLET_ADDRESS } }) console.log('Pix code:', data.paymentInstructions.pix.copyPaste) await pollSwapStatus(data.quote.quoteId) ``` ### 3. Pix Offramp (USDC → BRL) Pass `originAddress` (an approved Ramp wallet) and `pixKey`; the response includes executable `transactionData`. Batch and sign it, then poll. ```javascript theme={null} import { http, createWalletClient, publicActions } from 'viem' import { base } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { executeLegsViaEip7702 } from './eip7702-batch.js' const account = privateKeyToAccount(process.env.PRIVATE_KEY) const { data } = await pods.get('/v2/swap/quote', { params: { originChain: 'base', destinationChain: 'fiat', tokenIn: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base tokenOut: 'BRL', amountIn: '1000000', // 1.00 USDC (6 decimals) originAddress: account.address, pixKey: process.env.PIX_KEY } }) const walletClient = createWalletClient({ account, chain: base, transport: http(process.env.BASE_RPC_URL) }).extend(publicActions) const receipt = await executeLegsViaEip7702(walletClient, data.transactionData) console.log('Confirmed in block:', receipt.blockNumber) await pollSwapStatus(data.quote.quoteId) ``` ### 4. Foreign USD Onramp (USD ACH/WIRE → USDC) Pass `destinationAddress` (KYC profile must have `usdEnabled: true`). Send exactly one of `amountIn` (USD cents) or `amountOut` (USDC base units). The response returns bank deposit instructions — no EVM transaction to sign. ```javascript theme={null} const { data } = await pods.get('/v2/swap/quote', { params: { originChain: 'fiat', destinationChain: 'polygon', tokenIn: 'USD', tokenOut: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon amountOut: '4000000', // receive exactly 4 USDC destinationAddress: process.env.WALLET_ADDRESS, usdPaymentMethod: 'ACH' // or 'WIRE' (default) } }) const { usd } = data.paymentInstructions console.log('Routing:', usd.bankRoutingNumber) console.log('Account:', usd.bankAccountNumber) console.log('Memo (required):', usd.depositMessage) await pollSwapStatus(data.quote.quoteId) ``` ## Running the Example **Download both files into the same folder** to run this example: [`ramp.js`](/examples/javascript/ramp.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} node ramp.js pix-onramp # or: pix-offramp | usd-onramp ``` See the full runnable script: [`ramp.js`](/examples/javascript/ramp.js). ## Next Steps Onboard and approve a wallet before ramping All Ramp routes, tokens, and fees Poll KYC approval and swap status View complete API documentation # Same-Chain Swap Source: https://docs.pods.finance/examples/javascript/same-chain-swap Complete example of executing a same-chain token swap This example demonstrates how to swap tokens on the same blockchain network using the Pods API with JavaScript/TypeScript. ## Overview This example covers: * Getting an executable quote in **one call** (`GET /v2/swap/quote` with wallet addresses) * Executing the swap as one atomic EIP-7702 batch * Handling approvals ## Prerequisites * Node.js 18+ * Pods API key * Ethereum wallet with tokens to swap * Basic understanding of async/await ## Choose Your Library Using ethers.js v6 for blockchain interactions - the most popular Ethereum library. Using viem for blockchain interactions - a modern, TypeScript-first alternative with better performance. ## Installation ```bash theme={null} npm install axios ethers ``` ```bash theme={null} npm install axios viem ``` ## Environment Variables Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... PRIVATE_KEY=0x... RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-key ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) ``` ### 2. Get an Executable Quote (one call) Pass `originAddress` and `destinationAddress` on the quote so the response already includes executable `transactionData`. New integrations should **not** call the legacy `POST /v2/swap/bytecode`. ```javascript theme={null} const { data } = await pods.get('/v2/swap/quote', { params: { originChain: 'ethereum', destinationChain: 'ethereum', tokenIn: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH amountIn: '1000000000', // 1000 USDC (6 decimals) originAddress: walletAddress, destinationAddress: walletAddress } }) const { quote, transactionData } = data console.log('Quote ID:', quote.quoteId) console.log('Expected output:', quote.tokenOut.amount) console.log('Provider:', quote.provider) console.log('Legs to batch:', transactionData.length) ``` ### 3. Execute Transactions (atomic batch) Batch all `transactionData` legs into **one transaction** on the origin chain. See [Executing Bytecode](/getting-started/executing-bytecode) and [`same-chain-swap.js`](/examples/javascript/same-chain-swap.js) (EIP-7702 via `eip7702-batch.js`). ```javascript theme={null} import { http, createWalletClient, publicActions } from 'viem' import { mainnet } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { executeLegsViaEip7702 } from './eip7702-batch.js' const account = privateKeyToAccount(process.env.PRIVATE_KEY) const walletClient = createWalletClient({ account, chain: mainnet, transport: http(process.env.RPC_URL) }).extend(publicActions) const receipt = await executeLegsViaEip7702(walletClient, transactionData) console.log('✅ Swap batch confirmed in block', receipt.blockNumber) ``` Smart accounts: request `output=userOperation` on the quote (`GET /v2/swap/quote`) instead. ## Error Handling ```javascript theme={null} try { // Your code here } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error console.error('API Error:', apiError.code) console.error('Message:', apiError.message) if (apiError.code === 'QUOTE_NOT_FOUND') { console.log('Quote not found or expired') } else if (apiError.code === 'NO_ROUTE_FOUND') { console.log('No swap route available') } } else { console.error('Unexpected error:', error.message) } } ``` ## Running the Example **Download both files into the same folder** to run this example: [`same-chain-swap.js`](/examples/javascript/same-chain-swap.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} # Install dependencies npm install # Set environment variables cp .env.example .env # Edit .env with your values # Run the example node same-chain-swap.js ``` ## Expected Output ``` 1️⃣ Requesting swap quote (one-call — returns executable transactionData)... Provider: 1inch Legs to batch: 2 2️⃣ Executing atomic EIP-7702 batch... ✅ Confirmed in block 12345678 🎉 Swap completed — quote status: fulfilled ``` ## Next Steps Execute cross-chain token swaps Deposit into yield strategies Learn more about swaps View complete API documentation # Strategy Deposit Source: https://docs.pods.finance/examples/javascript/strategy-deposit Complete example of depositing funds into a DeFi yield strategy This example demonstrates how to deposit funds into a DeFi yield strategy (e.g., Aave lending) using the Pods API with JavaScript/TypeScript. ## Overview This example covers: * Fetching available strategies * Getting strategy details * Requesting a quote * Generating transaction bytecode * Executing transactions * Verifying the deposit ## Prerequisites * Node.js 18+ * Pods API key * Ethereum wallet with USDC balance * Basic understanding of async/await ## Installation ```bash theme={null} npm install axios ethers ``` ```bash theme={null} npm install axios viem ``` ## Environment Variables Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... PRIVATE_KEY=0x... ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-key POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-key ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) ``` ### 2. Fetch Available Strategies ```javascript theme={null} const { data: strategies } = await pods.get('/strategies', { params: { page: '1', limit: '10' } }) console.log('Available strategies:') strategies.data.forEach(strategy => { console.log(`- ${strategy.id} (${strategy.protocol})`) console.log(` APY: ${(strategy.apy * 100).toFixed(2)}%`) }) ``` ### 3. Get Strategy Details ```javascript theme={null} const strategyId = 'aave-usdc-polygon' const { data: strategyDetails } = await pods.get(`/v2/strategies/${strategyId}`) const { spotPosition, strategy } = strategyDetails console.log('Strategy:', strategy.id) console.log('Protocol:', strategy.protocol) console.log('Network:', strategy.network) console.log('Asset:', strategy.assetName) console.log('APY:', (spotPosition.apy * 100).toFixed(2) + '%') console.log('Status:', strategy.paused ? 'Paused' : 'Active') ``` ### 4. Generate Bytecode ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '1000000000', wallet: walletAddress } }) console.log('ID:', result.id) console.log('Transactions to execute:', result.bytecode.length) ``` #### Same-Chain Swap + Deposit If you hold a different token (e.g., WETH) and want to deposit into a USDC strategy, pass the `fromTokenAddress` parameter. The API will return swap transaction(s) followed by the deposit bytecode: ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '1000000000000000000', // 1 WETH (amount is in fromTokenAddress units) wallet: walletAddress, fromChainId: 137, fromTokenAddress: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619' // WETH on Polygon } }) console.log('Cross-chain:', result.crossChain.isCrossChain) // false console.log('Transactions to execute:', result.bytecode.length) // e.g. 2 (swap + deposit) ``` When `fromTokenAddress` is provided, include `fromChainId` or `chainIdIn`. For same-chain swap + deposit, use the strategy chain. The `amount` parameter refers to the amount of that token to swap, not the strategy's underlying asset. #### Cross-Chain Deposit If the funds start on a different chain, pass `fromChainId` or `chainIdIn` plus `fromTokenAddress`. Store `result.id`; it is used for asynchronous tracking. ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-arbitrum/bytecode', { params: { action: 'lend', amount: '1000000', wallet: walletAddress, fromChainId: 8453, fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base } }) console.log('ID:', result.id) console.log('Bridge:', result.quote.bridge) console.log('Source chain:', result.chainIdIn) console.log('Destination chain:', result.chainIdOut) ``` ### 5. Execute Transactions (atomic batch) **Do not loop `sendTransaction` over `result.bytecode`.** Batch all same-chain legs into one transaction. See [Executing Bytecode](/getting-started/executing-bytecode). **EOA (recommended):** use [EIP-7702 `executeBatch`](/examples/javascript/using-eip7702) or run [`strategy-deposit-eip7702.js`](/examples/javascript/strategy-deposit-eip7702.js) / [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js). **Smart account:** add `output=userOperation` and submit via your bundler — [Smart Account Deposit](/examples/javascript/smart-account-deposit). ```javascript theme={null} import { http, createWalletClient, publicActions } from 'viem' import { polygon } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import { executeLegsViaEip7702 } from './eip7702-batch.js' const account = privateKeyToAccount(process.env.PRIVATE_KEY) const walletClient = createWalletClient({ account, chain: polygon, transport: http(process.env.POLYGON_RPC_URL) }).extend(publicActions) const receipt = await executeLegsViaEip7702(walletClient, result.bytecode) console.log('Confirmed in block:', receipt.blockNumber) ``` ### 6. Verify Deposit ```javascript theme={null} // Wait for indexing await new Promise(resolve => setTimeout(resolve, 5000)) const { data: walletData } = await pods.get(`/v2/wallets/${walletAddress}`, { params: { include: 'earn' } }) console.log('Wallet positions:') walletData.earn.positions.forEach(position => { console.log(`${position.strategy.id}:`) console.log(` Balance: ${position.spotPosition.currentPosition.humanized} ${position.spotPosition.currentPosition.symbol}`) console.log(` Value: $${position.spotPosition.underlyingBalanceUSD}`) console.log(` APY: ${(position.spotPosition.apy * 100).toFixed(2)}%`) }) console.log('Total portfolio value: $' + walletData.earn.summary.totalUnderlyingBalanceUSD) ``` ### 7. Track Cross-Chain Deposit Status (Cross-Chain Only) If you made a cross-chain deposit, the bridge and the destination-chain deposit happen in separate transactions and complete asynchronously. Use the `id` returned in the bytecode response to poll `/actions/:id` until the status reaches a terminal state. ```javascript theme={null} const id = result.id // returned from /strategies/:id/bytecode // (Optional) preview the bridge & expected output before signing if (result.crossChain.isCrossChain) { console.log(`Bridging via ${result.quote.bridge}`) console.log(`Expected out: ${result.quote.outputAmount.humanized} ${result.quote.outputAmount.symbol}`) console.log(`Min out: ${result.quote.minimumOutputAmount.humanized} ${result.quote.minimumOutputAmount.symbol}`) } async function waitForDeposit (id) { const TERMINAL = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED'] while (true) { const { data: statusRecord } = await pods.get(`/actions/${id}`) console.log(`Status: ${statusRecord.status} (${statusRecord.transactions.length} tx)`) if (TERMINAL.includes(statusRecord.status)) return statusRecord await new Promise(resolve => setTimeout(resolve, 5000)) } } const finalStatus = await waitForDeposit(id) console.log('Final transactions:') finalStatus.transactions.forEach(tx => { console.log(` ${tx.chain}: ${tx.txHash} (${tx.status})`) }) ``` ### 8. Track Cross-Chain Deposit Status (Cross-Chain Only) If you made a cross-chain deposit (you passed `fromChainId` in Step 5), the bridge and the destination-chain deposit happen in separate transactions and complete asynchronously. Use the `id` returned in the bytecode response to poll `/actions/:id` until the action reaches a terminal state. ```javascript theme={null} const actionId = result.id // returned from /strategies/:id/bytecode // (Optional) preview the bridge & expected output before signing if (result.crossChain.isCrossChain) { console.log(`Bridging via ${result.quote.bridge}`) console.log(`Expected out: ${result.quote.outputAmount.humanized} ${result.quote.outputAmount.symbol}`) console.log(`Min out: ${result.quote.minimumOutputAmount.humanized} ${result.quote.minimumOutputAmount.symbol}`) } async function waitForDeposit (actionId) { const TERMINAL = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED'] while (true) { const { data: action } = await pods.get(`/actions/${actionId}`) console.log(`Status: ${action.status} (${action.transactions.length} tx)`) if (TERMINAL.includes(action.status)) return action await new Promise(resolve => setTimeout(resolve, 5000)) } } const finalAction = await waitForDeposit(actionId) console.log('Final transactions:') finalAction.transactions.forEach(tx => { console.log(` ${tx.chain}: ${tx.txHash} (${tx.status})`) }) ``` Same-chain deposits do not require polling — the deposit completes inline with the user's signed transaction. You can still call `GET /actions/:id` with the returned `id` for record-keeping. ## Error Handling The example includes comprehensive error handling: ```javascript theme={null} try { // Your code here } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error console.error('API Error:', apiError.code) console.error('Message:', apiError.message) // Handle specific errors if (apiError.code === 'STRATEGY_NOT_FOUND') { console.log('Strategy does not exist or is not available') } else if (apiError.code === 'INSUFFICIENT_BALANCE') { console.log('Wallet does not have enough tokens') } } else { console.error('Unexpected error:', error.message) } } ``` ## Running the Example **Download both files into the same folder** to run this example: [`strategy-deposit.js`](/examples/javascript/strategy-deposit.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} # Install dependencies npm install # Set environment variables cp .env.example .env # Edit .env with your values # Run the example node strategy-deposit.js ``` ## Expected Output ``` 1️⃣ Fetching available strategies... ✅ Available strategies: - Aave-USDC-polygon (Aave) APY: 4.50% - Morpho-BTC-base (Morpho) APY: 3.20% 2️⃣ Getting details for Aave-USDC-polygon... ✅ Strategy details: Strategy: Aave-USDC-polygon Protocol: Aave Network: polygon Asset: USDC APY: 4.50% Status: ✅ Active 4️⃣ Generating deposit bytecode... ✅ Bytecode generated: ID: 65f3a1c8e9b2d4f8a7c1d2e3 Transactions: 2 5️⃣ Executing atomic EIP-7702 batch... ✅ Confirmed in block 12345678 6️⃣ Verifying deposit... ✅ Wallet positions: Aave-USDC-polygon: Balance: 1000.00 USDC Value: $1000.00 APY: 4.17% Total Portfolio Value: $1000.00 🎉 Strategy deposit completed successfully! ``` ## Next Steps Atomic EOA execution (recommended) Output types and wallet decision tree Withdraw from strategies, including cross-chain withdrawals Execute cross-chain token swaps Track wallet positions View complete API documentation # Strategy Withdrawal Source: https://docs.pods.finance/examples/javascript/strategy-withdraw Complete example of withdrawing funds from a DeFi yield strategy, including cross-chain withdrawals This example demonstrates how to withdraw from a DeFi yield strategy using the Pods API with JavaScript/TypeScript. It includes both same-chain withdrawal and cross-chain withdrawal, where withdrawn funds are bridged to a different destination chain. ## Overview This example covers: * Checking existing strategy positions * Generating withdrawal transaction bytecode * Withdrawing to the same chain * Withdrawing cross-chain with `toChainId` and `toTokenAddress` * Executing the returned legs as one atomic EIP-7702 batch * Tracking cross-chain completion by ID ## Prerequisites * Node.js 18+ * Pods API key * Wallet with an existing position in the selected strategy * RPC URL for the strategy chain * Basic understanding of async/await ## Installation ```bash theme={null} npm install axios viem ``` ## Environment Variables Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... PRIVATE_KEY=0x... ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/your-key BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/your-key POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-key ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) ``` ### 2. Check Current Positions ```javascript theme={null} const walletAddress = process.env.WALLET_ADDRESS const { data: walletData } = await pods.get(`/v2/wallets/${walletAddress}`, { params: { include: 'earn' } }) console.log('Open positions:') walletData.earn.positions.forEach(position => { console.log(`${position.strategy.id}:`) console.log(` Balance: ${position.spotPosition.currentPosition.humanized} ${position.spotPosition.currentPosition.symbol}`) console.log(` Value: $${position.spotPosition.underlyingBalanceUSD}`) }) ``` ### 3. Generate Same-Chain Withdrawal Bytecode ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-arbitrum/bytecode', { params: { action: 'withdraw', amount: '1000000', wallet: walletAddress } }) console.log('ID:', result.id) console.log('Cross-chain:', result.crossChain.isCrossChain) // false console.log('Transactions to execute:', result.bytecode.length) ``` ### 4. Generate Cross-Chain Withdrawal Bytecode To withdraw from the strategy chain and bridge the proceeds to another chain, pass `toChainId` or `chainIdOut` plus `toTokenAddress`. ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-arbitrum/bytecode', { params: { action: 'withdraw', amount: '1000000', wallet: walletAddress, toChainId: 8453, toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base } }) console.log('ID:', result.id) console.log('Bridge:', result.quote.bridge) console.log('Source chain:', result.chainIdIn) console.log('Destination chain:', result.chainIdOut) console.log('Expected out:', result.quote.outputAmount.humanized, result.quote.outputAmount.symbol) console.log('Minimum out:', result.quote.minimumOutputAmount.humanized, result.quote.minimumOutputAmount.symbol) ``` The `amount` is the amount to withdraw from the source strategy. For cross-chain withdrawals, Pods returns origin-chain transaction bytecode and a `quote` describing the destination-chain output. ### 5. Execute Transactions (atomic batch) Batch all source-chain `bytecode` legs into one transaction. See [Executing Bytecode](/getting-started/executing-bytecode) and [EIP-7702](/examples/javascript/using-eip7702). ```javascript theme={null} import { executeLegsViaEip7702 } from './eip7702-batch.js' // ... walletClient on source chain (Arbitrum for cross-chain example above) const receipt = await executeLegsViaEip7702(walletClient, result.bytecode) console.log('Confirmed in block:', receipt.blockNumber) ``` ### 6. Track Cross-Chain Withdrawal Status Cross-chain withdrawals complete asynchronously after the origin-chain transactions are confirmed. Use the returned `id` to track completion. ```javascript theme={null} const id = result.id async function waitForWithdrawal (id) { const terminalStatuses = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED'] while (true) { const { data: statusRecord } = await pods.get(`/actions/${id}`) console.log(`Status: ${statusRecord.status}`) if (terminalStatuses.includes(statusRecord.status)) return statusRecord await new Promise(resolve => setTimeout(resolve, 5000)) } } const finalStatus = await waitForWithdrawal(id) console.log('Final status:', finalStatus.status) ``` Same-chain withdrawals do not require polling because the withdrawal completes inline with the user's signed transaction. You can still call `GET /actions/:id` with the returned `id` for record-keeping. ## Running the Example **Download both files into the same folder** to run this example: [`strategy-withdraw.js`](/examples/javascript/strategy-withdraw.js) and the shared EIP-7702 batch helper [`eip7702-batch.js`](/examples/javascript/eip7702-batch.js) that it imports. ```bash theme={null} # Install dependencies npm install # Set environment variables cp .env.example .env # Edit .env with your values # Run the example node strategy-withdraw.js ``` ## Expected Output ```text theme={null} 1️⃣ Checking wallet positions... Open position: aave-usdc-arbitrum Balance: 25.00 USDC Value: $25.00 2️⃣ Generating cross-chain withdrawal bytecode... ID: 65f3a1c8e9b2d4f8a7c1d2e3 Cross-chain: true Source chain: 42161 Destination chain: 8453 Legs to batch: 2 3️⃣ Executing atomic EIP-7702 batch on the source chain... ✅ Confirmed in block 12345678 4️⃣ Tracking cross-chain withdrawal... Status: PENDING Status: SUCCESS Final status: SUCCESS ``` ## Next Steps Deposit into yield strategies Review all withdrawal parameters and response shapes Execute cross-chain token swaps View complete API documentation # Track & Positions Source: https://docs.pods.finance/examples/javascript/track-and-positions Read wallet positions and follow the action lifecycle via polling, webhooks, or WebSocket This example shows the standalone helpers used across every Pods flow: reading a wallet's positions, and following an action to completion by **polling**, **webhook**, or **WebSocket**. ## Overview This example covers: * **Positions** — `GET /v2/wallets/{address}?include=earn` * **Poll an action** — `GET /actions/{id}` (`INITIAL → PENDING → SUCCESS/FAILED/REFUNDED/EXPIRED`) * **Webhook** — receive `ACTION_UPDATE` events (push) * **WebSocket** — subscribe to `wss://api.pods.finance/updates` (push) ## Prerequisites * Node.js 18+ * Pods API key * For webhook/WebSocket modes: `npm install express ws` ## Installation ```bash theme={null} npm install axios # for push modes: npm install express ws ``` ## Environment Variables ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... ACTION_ID=65f1a2b3c4d5e6f789012345 PORT=3000 ``` ## Step-by-Step Walkthrough ### 1. Check Positions ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) const { data } = await pods.get(`/v2/wallets/${process.env.WALLET_ADDRESS}`, { params: { include: 'earn' } }) data.earn.positions.forEach(position => { const p = position.spotPosition console.log(position.strategy.id, '→ $' + p.underlyingBalanceUSD) }) console.log('Total: $' + data.earn.summary.totalUnderlyingBalanceUSD) ``` ### 2. Poll an Action ```javascript theme={null} async function trackAction (id) { const terminal = ['SUCCESS', 'FAILED', 'REFUNDED', 'EXPIRED'] while (true) { const { data: action } = await pods.get(`/actions/${id}`) console.log('status:', action.status) if (terminal.includes(action.status)) return action.status await new Promise(resolve => setTimeout(resolve, 5000)) } } ``` ### 3. Webhook (push alternative) Configure your customer webhook URL with Pods (or pass `webhookURL` per quote). Deframe **signs every delivery** — verify the signature over the raw body before trusting it, then consume the `{ messageType, data }` envelope (this is different from the WebSocket `{ type, action }` shape). Ack quickly and do heavy work asynchronously. ```javascript theme={null} import express from 'express' import { createHmac, timingSafeEqual } from 'node:crypto' const SECRET = process.env.WEBHOOK_SIGNING_SECRET // shared secret from Pods const MAX_AGE_SECONDS = 300 // replay protection const seenWebhookIds = new Set() // dedupe redelivered webhooks // v1 HMAC over `${timestamp}.${webhookId}.${rawBody}`, compared timing-safe. function verifySignature (req) { const webhookId = req.get('X-Deframe-Webhook-Id') const timestamp = req.get('X-Deframe-Webhook-Timestamp') const signatureHeader = req.get('X-Deframe-Webhook-Signature') // "v1=" if (!webhookId || !timestamp || !signatureHeader) return false const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) if (!Number.isFinite(ageSeconds) || ageSeconds > MAX_AGE_SECONDS) return false const signedPayload = `${timestamp}.${webhookId}.${req.body.toString('utf8')}` const expected = createHmac('sha256', SECRET).update(signedPayload, 'utf8').digest('hex') const provided = signatureHeader.replace(/^v1=/, '') const a = Buffer.from(expected, 'hex') const b = Buffer.from(provided, 'hex') return a.length === b.length && timingSafeEqual(a, b) } const app = express() // Raw body so the HMAC matches the exact bytes Deframe signed. app.post('/webhooks/pods', express.raw({ type: '*/*' }), (req, res) => { if (!verifySignature(req)) return res.sendStatus(401) const webhookId = req.get('X-Deframe-Webhook-Id') if (seenWebhookIds.has(webhookId)) return res.sendStatus(200) // duplicate seenWebhookIds.add(webhookId) const event = JSON.parse(req.body.toString('utf8')) if (event.messageType === 'ACTION_UPDATE') { const action = event.data console.log('ACTION_UPDATE', action?.id, action?.status) } res.sendStatus(200) }) app.listen(process.env.PORT ?? 3000) ``` ### 4. WebSocket (push alternative) Connect, subscribe to the wallet channel, and handle `action_update` events. Re-subscribe on reconnect. ```javascript theme={null} import WebSocket from 'ws' const ws = new WebSocket('wss://api.pods.finance/updates') ws.on('open', () => { ws.send(JSON.stringify({ type: 'subscribe', channel: process.env.WALLET_ADDRESS })) }) ws.on('message', raw => { const event = JSON.parse(raw.toString()) if (event.type === 'action_update') { console.log('action_update', event.action?.id, event.action?.status) } }) ``` ## Running the Example ```bash theme={null} node track-and-positions.js positions # or: track | webhook | ws ``` See the full runnable script: [`track-and-positions.js`](/examples/javascript/track-and-positions.js). ## Next Steps Deposit into yield strategies Buy/sell tokenized stocks with async orders Full wallet/position response shape View complete API documentation # EIP-7702 Source: https://docs.pods.finance/examples/javascript/using-eip7702 Batch multiple DeFi transactions into a single atomic operation using EIP-7702 This example demonstrates how to deposit funds into a DeFi yield strategy using the Pods API and EIP-7702. Instead of sending each transaction individually, EIP-7702 collapses the entire sequence into a **single atomic operation** executed directly from your EOA — no smart-contract wallet or contract deployment required. ## Overview This example covers: * Fetching available strategies * Getting strategy details * Generating transaction bytecode * Batching and executing via EIP-7702 * Verifying the deposit ## Prerequisites * Node.js 18+ * Pods API key * Ethereum wallet with USDC balance * Basic understanding of async/await ## Installation ```bash theme={null} npm install axios viem ``` ## Environment Variables Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key PRIVATE_KEY=0x... # RPC for the strategy's network — Polygon here (aave-usdc-polygon) RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/your-key ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step-by-Step Walkthrough ### 1. Initialize API Client ```javascript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' } }) ``` ### 2. Fetch Available Strategies ```javascript theme={null} const { data: strategies } = await pods.get('/strategies', { params: { page: '1', limit: '10' } }) console.log('Available strategies:') strategies.data.forEach(strategy => { console.log(`- ${strategy.id} (${strategy.protocol})`) console.log(` APY: ${(strategy.apy * 100).toFixed(2)}%`) }) ``` ### 3. Get Strategy Details ```javascript theme={null} const strategyId = 'aave-usdc-polygon' const { data: strategyDetails } = await pods.get(`/v2/strategies/${strategyId}`) const { spotPosition, strategy } = strategyDetails console.log('Strategy:', strategy.id) console.log('Protocol:', strategy.protocol) console.log('Network:', strategy.network) console.log('Asset:', strategy.assetName) console.log('APY:', (spotPosition.apy * 100).toFixed(2) + '%') console.log('Status:', strategy.paused ? 'Paused' : 'Active') ``` ### 4. Generate Bytecode The `amount` parameter is in the token's base units (e.g. 6 decimals for USDC: `1000000000` = 1000 USDC). ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '1000000000', wallet: walletAddress } }) console.log('Transactions to execute:', result.bytecode.length) ``` ### 5. Execute Transactions The standard approach sends each transaction in a loop. With EIP-7702 you encode the entire `result.bytecode` array into one `executeBatch` call and send it as a single type-4 transaction. All steps succeed or all revert — no partial failures. ```javascript theme={null} import { http, encodeFunctionData, createWalletClient, publicActions } from 'viem' import { polygon } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' // Same canonical address on every EIP-7702-enabled chain. const SIMPLE_7702_ACCOUNT_ADDRESS = '0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9' const EIP7702AccountAbi = [ { name: 'executeBatch', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'calls', type: 'tuple[]', components: [ { name: 'target', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'data', type: 'bytes' } ]}], outputs: [] } ] const account = privateKeyToAccount(process.env.PRIVATE_KEY) const walletClient = createWalletClient({ account, chain: polygon, // must match the strategy's network transport: http(process.env.RPC_URL) }).extend(publicActions) const walletAddress = account.address // Map Pods bytecode → executeBatch tuple format: [target, value, data] const calls = result.bytecode.map(tx => [ tx.to, BigInt(tx.value ?? '0'), tx.data ]) // Encode all calls into a single executeBatch calldata const calldata = encodeFunctionData({ abi: EIP7702AccountAbi, functionName: 'executeBatch', args: [calls] }) // Sign EIP-7702 authorization — temporarily delegates your EOA to Simple7702Account const authorization = await walletClient.signAuthorization({ contractAddress: SIMPLE_7702_ACCOUNT_ADDRESS, executor: 'self' }) // Send one type-4 transaction containing all the calls const hash = await walletClient.sendTransaction({ to: walletAddress, data: calldata, authorizationList: [authorization] }) console.log('Transaction sent:', hash) const receipt = await walletClient.waitForTransactionReceipt({ hash }) console.log('Confirmed in block:', receipt.blockNumber) ``` **This pattern is chain-agnostic.** To run it on another network, swap the viem `chain` import (`polygon` → `base`, `arbitrum`, `mainnet`, …) and point `RPC_URL` at that chain — match the strategy's network. The `SIMPLE_7702_ACCOUNT_ADDRESS` (`0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9`) is the **same canonical address on every EIP-7702-enabled chain**, so it never changes. ### 6. Verify Deposit ```javascript theme={null} // Wait for indexing await new Promise(resolve => setTimeout(resolve, 5000)) const { data: walletData } = await pods.get(`/v2/wallets/${walletAddress}`, { params: { include: 'earn' } }) console.log('Wallet positions:') walletData.earn.positions.forEach(position => { console.log(`${position.strategy.id}:`) console.log(` Balance: ${position.spotPosition.currentPosition.humanized} ${position.spotPosition.currentPosition.symbol}`) console.log(` Value: $${position.spotPosition.underlyingBalanceUSD}`) console.log(` APY: ${(position.spotPosition.apy * 100).toFixed(2)}%`) }) console.log('Total portfolio value: $' + walletData.earn.summary.totalUnderlyingBalanceUSD) ``` ## Error Handling The example includes comprehensive error handling for both Pods API errors and EIP-7702 wallet errors: ```javascript theme={null} try { // Your code here } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error console.error('API Error:', apiError.code) console.error('Message:', apiError.message) if (apiError.code === 'STRATEGY_NOT_FOUND') { console.log('Strategy does not exist or is not available') } else if (apiError.code === 'INSUFFICIENT_BALANCE') { console.log('Wallet does not have enough tokens') } } else if (error?.code === 4001 || error?.message?.toLowerCase().includes('user rejected')) { console.error('User rejected the signature or transaction.') } else if (error?.code === 4200 || error?.message?.toLowerCase().includes('not supported')) { console.error('Wallet does not support EIP-7702 authorization signing.') } else { console.error('Unexpected error:', error.message) } } ``` ## Running the Example ```bash theme={null} # Install dependencies npm install # Set environment variables cp .env.example .env # Edit .env with your values # Run the example node strategy-deposit-eip7702.js ``` ## Expected Output ``` 1️⃣ Fetching available strategies... ✅ Available strategies: - aave-usdc-polygon (Aave) APY: 4.50% 2️⃣ Getting details for aave-usdc-polygon... ✅ Strategy details: Strategy: aave-usdc-polygon Protocol: Aave Network: polygon Asset: USDC APY: 4.50% Status: ✅ Active 3️⃣ Generating deposit bytecode... ✅ Bytecode generated: Transactions to batch: 2 4️⃣ Signing EIP-7702 authorization... Encoding executeBatch calldata... Sending type-4 transaction... Transaction sent: 0x123... ✅ Transaction confirmed in block 12345678 5️⃣ Verifying deposit... ✅ Wallet positions: aave-usdc-polygon: Balance: 1000.00 USDC Value: $1000.00 APY: 4.17% Total Portfolio Value: $1000.00 🎉 Strategy deposit completed successfully! ``` ## Next Steps Withdraw from strategies, including cross-chain withdrawals Execute cross-chain token swaps Track wallet positions View complete API documentation # Dynamic Integration Source: https://docs.pods.finance/external-integrations/dynamic-integration Multi-chain wallet authentication with Dynamic and Pods yield strategies ## Overview [Dynamic](https://www.dynamic.xyz) provides multi-chain wallet authentication and embedded wallets. This guide explains how to use Dynamic for user authentication and then interact with the Pods API on behalf of authenticated users. Complete setup instructions on Dynamic's documentation *** ## Integration Pattern ### 1. Authenticate the User with Dynamic Use Dynamic's SDK to authenticate the user and obtain their wallet address: ```javascript theme={null} import { useDynamicContext } from '@dynamic-labs/sdk-react-core' const { primaryWallet, user } = useDynamicContext() const walletAddress = primaryWallet?.address ``` ### 2. Query Yield Strategies With the wallet address, query the Pods API for available strategies and the user's current positions: ```javascript theme={null} // List available strategies const { data: strategies } = await axios.get('https://api.pods.finance/strategies', { headers: { 'x-api-key': process.env.PODS_API_KEY } }) // Check user positions const { data: walletOverview } = await axios.get(`https://api.pods.finance/v2/wallets/${walletAddress}`, { params: { include: 'earn' }, headers: { 'x-api-key': process.env.PODS_API_KEY } }) ``` ### 3. Execute Strategy Transactions Fetch the bytecode and sign it using the Dynamic wallet signer. **Batch all same-chain legs into one atomic transaction** — see [Executing Bytecode](/getting-started/executing-bytecode). ```javascript theme={null} // Get deposit bytecode const { data } = await axios.get(`https://api.pods.finance/strategies/${strategyId}/bytecode`, { params: { action: 'lend', amount: '10000000', wallet: walletAddress }, headers: { 'x-api-key': process.env.PODS_API_KEY } }) // EOA: EIP-7702 executeBatch (recommended) — see /examples/javascript/using-eip7702 // Embedded smart account: output=userOperation + bundler submit // Do not: for (const tx of data.bytecode) { await signer.sendTransaction(tx) } console.log('Legs to batch atomically:', data.bytecode.length) ``` The Pods API never takes custody of funds. All transactions are signed and submitted by the user's wallet through Dynamic. *** ## Next Steps Embedded wallet solution with social login Enterprise-grade custody integration # Fireblocks Integration Source: https://docs.pods.finance/external-integrations/fireblocks-integration Enterprise-grade custody integration with Pods using Fireblocks Smart Wallets This guide explains how clients with Fireblocks custody can integrate with the Smart Wallet and Strategy system, including deposits, withdrawals, position tracking, and recommended security best practices. ## Overview The integration leverages Smart Wallets to provide segregated user accounts while keeping your Omnibus wallet isolated from strategy bytecode execution. This architecture ensures maximum security by preventing your main operational wallet from signing potentially untrusted transactions. ## Architecture Flows Depending on how you custody user funds on Fireblocks, the integration has two possible entry points. Both paths converge into the same flow after the owner vault is defined. * **Omnibus Account** — all users share a single Fireblocks vault. You must create a dedicated **Vault Owner** that will act as the signer for every Smart Wallet. This isolates strategy execution risk from the vault that actually holds funds (Omnibus). * **Segregated Vaults** — each user already has their own Fireblocks vault. That vault is used directly as the Smart Wallet owner, so no additional setup is required. Fireblocks integration flow: Omnibus Account vs Segregated Vaults In the **Omnibus** model, the *Vault Owner* created in step 1 is the signer for every Smart Wallet you provision. In the **Segregated** model, each user's own vault plays that role — there is no shared owner vault to create. ## Prerequisites Before starting the integration, ensure you have: * A Fireblocks account with API access * Understanding of Fireblocks Transaction Authorization Policy (TAP) * Familiarity with the [Architecture Overview](/getting-started/architecture) *** ## 1. Fireblocks Setup To safely interact with strategy bytecode while isolating your Omnibus wallet, Fireblocks must be configured with dedicated roles and policies. ### Creating Roles To ensure clean separation of responsibilities and secure execution: | Role | Description | | - | - | | **Initiator** | The *API user* or service responsible for submitting strategy-related transactions to Fireblocks. | | **Vault Owner** | A dedicated vault that acts as the Smart Wallet Owner. This address serves as the signer for smart wallet transactions, providing security segmentation from your main operational wallet. | ### Creating Policies Two policies must be configured in Fireblocks to ensure automated, safe signing of transactions: #### Contract Call Policy | Setting | Value | | - | - | | Initiator | The API user created for calling Fireblocks strategy operations | | Source | Vault Owner | | Signer | Co-signer (for automatic signing) | *** ## 2. Smart Wallet Creation Each user receives their own segregated smart wallet. Smart wallets (Gnosis Safe accounts) can be provisioned via the API: ```http theme={null} POST /fireblocks-smart-account ``` **Request Body:** | Parameter | Type | Required | Description | | - | - | - | - | | `owner` | string | ✅ | The Vault address that will own the Gnosis Safe | | `chainId` | number | ✅ | Chain ID on which to deploy the Safe | **Example Request:** ```bash theme={null} curl --request POST \ --url https://api.pods.finance/fireblocks-smart-account \ --header 'Content-Type: application/json' \ --header 'x-api-key: YOUR_API_KEY' \ --data '{"owner": "0xYourVaultOwnerAddress", "chainId": 137}' ``` **Example Response:** ```json theme={null} { "address": "0xDeployedSafeAddress", "owner": "0xYourVaultOwnerAddress", "chainId": 137 } ``` The Safe address is computed deterministically via CREATE2 and returned immediately. Actual on-chain deployment happens asynchronously in the background. If the Safe is already deployed, the existing address is returned. Once provisioned, store the mapping between your internal user IDs and their smart wallet addresses: ``` userId → smartWalletAddress ``` This mapping enables you to: * Display user balances and yield * Track user performance * Execute operations on behalf of users *** ## 3. Deposit Flow When a user wants to deposit into a strategy (e.g., 100 USDC): Transfer the deposit amount from your Omnibus wallet to the user's `smartWalletAddress`. Fetch the bytecode for the deposit operation: ```http theme={null} GET /strategies/:id/bytecode?action=lend&amount=100000000&wallet=0x...&output=fireblocks&accountId=YOUR_VAULT_ACCOUNT_ID ``` When using Fireblocks, set `output=fireblocks` and provide `accountId` (the Fireblocks vault account ID). The response will include a `transactionData` object ready to submit directly to the Fireblocks API instead of a raw `bytecode` array. See [How to Deposit](/guides/yield/deposit) for detailed parameter information. 1. Use the **Vault Owner** signer (NOT the Omnibus signer) to sign the transaction 2. If using `output=fireblocks`, the response contains a `transactionData.transactionRequest` object ready to submit directly to the Fireblocks Transactions API (`POST /v1/transactions`) 3. If using `output=bytecode` (default), execute the returned bytecode through Fireblocks manually 4. The transaction will deposit funds from the smart wallet into the strategy **Example: submitting the response to the Fireblocks SDK** ```typescript theme={null} import axios from 'axios' import { BasePath, Fireblocks, TransactionOperation, TransferPeerPathType } from '@fireblocks/ts-sdk' const fireblocks = new Fireblocks({ apiKey: process.env.FIREBLOCKS_API_KEY, basePath: BasePath.US, secretKey: Buffer.from(process.env.FIREBLOCKS_SECRET_KEY, 'base64').toString('ascii') }) const podsResponse = await axios.get( 'https://api.pods.finance/strategies/Aave-USDT-Polygon/bytecode', { params: { action: 'lend', amount: '100000000', wallet: '0xYourSmartWalletAddress', output: 'fireblocks', accountId: 'YOUR_VAULT_ACCOUNT_ID' }, headers: { 'x-api-key': process.env.PODS_API_KEY } } ) const transactionData = podsResponse.data.transactionData // Structure example // const transactionData = { // transactionRequest: { // operation: TransactionOperation.ContractCall, // assetId: 'ETH', // source: { // type: TransferPeerPathType.VaultAccount, // id: '' // }, // destination: { // type: TransferPeerPathType.OneTimeAddress, // oneTimeAddress: { // address: '' // } // }, // amount: '0', // extraParameters: { // contractCallData: '' // }, // failOnLowFee: true // } // } const contractCallTransaction = await fireblocks.transactions.createTransaction(transactionData) console.debug('Transaction Result: ', contractCallTransaction.data) ``` *** ## 4. View User Positions and Yield To fetch a user's position, balance, or yield: ```http theme={null} GET /v2/wallets/:smartWalletAddress ``` This endpoint provides all data needed for user-facing dashboards and reporting. See [How to Check Open Positions](/guides/yield/check-positions) for more details. *** ## 5. Withdrawal Flow Withdrawals follow the reverse flow: ```http theme={null} GET /strategies/:id/bytecode?action=withdraw&amount=100000000&wallet=0x...&output=fireblocks&accountId=YOUR_VAULT_ACCOUNT_ID ``` Sign and execute the transaction using the **Vault Owner** signer through Fireblocks. After the withdrawal completes, transfer funds from the smart wallet to the destination: * Back to your **Omnibus wallet**, or * Any other target address *** ## Trade-offs Requires two separate transfers for deposits and withdrawals: Transfer IN (Omnibus to Smart Wallet) and Transfer OUT (Smart Wallet to destination) Your Omnibus signer is never exposed to potentially malicious bytecode, preventing scenarios similar to known custody exploits Each user operates through a fully segregated smart wallet Clean audit trail and easier accounting per user. Aggregated deposit analytics available via the `/analytics` endpoint *** ## Optional: Additional Security Layer For maximum security, you can integrate **Blockaid** to simulate and validate bytecode before signing it on Fireblocks. This adds defense-in-depth by detecting unexpected contract behavior, malicious payloads, and incorrect calldata or parameters. *** ## Next Steps Generate bytecode for depositing into yield positions Learn how to withdraw from positions Monitor your open yield positions Embedded wallet solution with social login # Notus Integration Source: https://docs.pods.finance/external-integrations/notus-integration Account abstraction with Notus smart wallets and Pods strategy bytecodes Use [Notus](https://notus.team/) smart wallets together with Pods so users run yield strategy flows as ERC-4337 user operations: Pods returns the exact calldata sequence for a strategy action; Notus wraps that sequence in a custom user operation, collects an EOA signature, and submits execution asynchronously. This guide follows the same Pods patterns as the other external integrations (list strategies, request `/bytecode`), then adds the Notus-specific steps: resolve or register the smart wallet address, create a custom user operation from the bytecode list, sign `userOperationHash`, and call execute. *** ## Resources | Resource | Description | | - | - | | [Pods Docs](https://docs.pods.finance/) | Strategy listing and bytecode generation | | [Register Smart Wallet](https://docs.notus.team/docs/api-reference/smart-wallets/RegisterWalletController_handle_v1) | Notus `POST /api/v1/wallets/register` | | [Get Smart Wallet](https://docs.notus.team/docs/api-reference/smart-wallets/GetAccountAbstractionAddressController_handle_v1) | Notus `GET /api/v1/wallets/address` | | [Create Custom User Operation](https://docs.notus.team/docs/api-reference/user-operations/CreateCustomUserOperationController_create_v1) | Notus `POST /api/v1/crypto/custom-user-operation` | | [Execute User Operation](https://docs.notus.team/docs/api-reference/user-operations/ExecuteUserOperationController_executeUserOp_v1) | Notus `POST /api/v1/crypto/execute-user-op` | *** ## Prerequisites Configure your backend or secure client with: * **Pods**: base URL and `x-api-key` (same as other integrations). * **Notus**: `X-Api-Key` header on every Notus request, plus the **factory** address and **salt** your project uses so the counterfactual smart wallet is deterministic for each EOA. Notus uses `https://api.notus.team/api/v1` as the API prefix (combine with the paths below). Never expose the Notus API key or Pods API key in public client bundles unless you accept the risk of key abuse. Prefer a backend that proxies these calls. *** ## Reference client The snippets below use `notusPost`, `getSmartWalletFromNotus`, `createSmartWalletFromNotus`, and `signRawHash` as shorthand. They are **not** part of Pods — implement them in your app (for example with `axios` and [viem](https://viem.sh/) `WalletClient`), following the same HTTP paths and headers as a typical Notus integration. The structure below mirrors a small OOP-style client adapted from common Notus + viem setups (request builder, `X-Api-Key`, `factory` / `salt`, raw-hash signing for `userOperationHash`): ```typescript theme={null} // Pseudocode — illustrative only; tighten types, validation, and errors for production. import type { WalletClient } from "viem"; type NotusClientConfig = { /** e.g. https://api.notus.team/api/v1 — include `/api/v1` if your paths omit it */ baseUrl: string; apiKey: string; factory: string; salt: string; }; type NotusWalletDto = { wallet?: { accountAbstraction?: string; registeredAt?: string | null; }; }; export class NotusIntegrationClient { #cfg: NotusClientConfig; constructor(cfg: NotusClientConfig) { this.#cfg = cfg; } /** POST helper used by this guide for `/crypto/*` and `/wallets/register`. */ async notusPost(path: string, body: object): Promise { const url = `${this.#cfg.baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": this.#cfg.apiKey, }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise; } /** GET `/wallets/address` — returns smart wallet address only when already registered. */ async getSmartWalletFromNotus({ address, }: { address: string; }): Promise<{ smartWallet: string } | null> { const q = new URLSearchParams({ externallyOwnedAccount: address, factory: this.#cfg.factory, salt: String(this.#cfg.salt), }); const url = `${this.#cfg.baseUrl.replace(/\/$/, "")}/wallets/address?${q}`; const res = await fetch(url, { headers: { "X-Api-Key": this.#cfg.apiKey }, }); if (!res.ok) return null; const data = (await res.json()) as NotusWalletDto; const w = data.wallet; if (w?.accountAbstraction && w?.registeredAt) return { smartWallet: w.accountAbstraction }; return null; } /** POST `/wallets/register` — links factory + salt wallet to your Notus project. */ async createSmartWalletFromNotus({ address, }: { address: string; }): Promise<{ smartWallet: string } | null> { const data = await this.notusPost("/wallets/register", { externallyOwnedAccount: address, factory: this.#cfg.factory, salt: this.#cfg.salt, }); const w = data.wallet; if (w?.accountAbstraction && w?.registeredAt) return { smartWallet: w.accountAbstraction }; return null; } /** * Sign `userOperationHash` as raw bytes (per Notus execute flow). * Same idea as viem: signMessage({ message: { raw: hash } }). */ async signRawHash( walletClient: WalletClient, userOperationHash: `0x${string}`, ): Promise<`0x${string}`> { return walletClient.signMessage({ account: walletClient.account!, message: { raw: userOperationHash }, }); } } ``` You can extend this class with `buildCustomUserOperation` / `sendUserOperation` methods that call `notusPost("/crypto/custom-user-operation", …)` and `notusPost("/crypto/execute-user-op", …)` when you want a single place for all Notus traffic. *** ## Integration pattern ### 1. Resolve the smart wallet (get or register) The EOA is the address that will sign user operations. The **smart wallet** (account abstraction address) is derived from the EOA, factory, and salt. Recommended flow: 1. **GET** `/wallets/address?externallyOwnedAccount=...&factory=...&salt=...` — [Get Smart Wallet](https://docs.notus.team/docs/api-reference/smart-wallets/GetAccountAbstractionAddressController_handle_v1). 2. If the wallet is not registered yet (`registeredAt` missing or your product rules say so), **POST** `/wallets/register` with the same `externallyOwnedAccount`, `factory`, and optional `salt` — [Register Smart Wallet](https://docs.notus.team/docs/api-reference/smart-wallets/RegisterWalletController_handle_v1). From the JSON response, use `wallet.accountAbstraction` (or `wallet.walletAddress` when they match) as the **`walletAddress`** for Pods bytecode requests and for Notus user operations. Conceptually: ```typescript theme={null} const smartWallet = (await getSmartWalletFromNotus({ address: EOA }))?.smartWallet ?? (await createSmartWalletFromNotus({ address: EOA }))?.smartWallet; ``` Only treat the address as usable for your app when Notus indicates registration succeeded (per your checks on `registeredAt` and error payloads such as `FACTORY_NOT_SUPPORTED`). *** ### 2. Request strategy bytecodes from Pods | Parameter | Value | | - | - | | Method | GET | | Path | `/strategies/:strategy-id/bytecode` | | Query | `action` (required), `wallet` (required — use the **Notus smart wallet** address), `amount` (required, integer string in token decimals) | | Headers | `x-api-key: ` | The response includes `bytecode[]` with `to`, `data`, `value`, and `chainId` per step. All steps in one Notus custom user operation must run in order on a **single** `chainId`; use the first item's `chainId` when building the operation (if Pods returns multiple chains, split into separate Notus flows). ```typescript theme={null} type PodsBytecodeItem = { to: string; data: string; value: string; chainId: string; }; ``` *** ### 3. Create a custom user operation on Notus **POST** `/crypto/custom-user-operation` — [Create Custom User Operation](https://docs.notus.team/docs/api-reference/user-operations/CreateCustomUserOperationController_create_v1). Map each Pods bytecode to a Notus **operation**: | Pods | Notus body field | | - | - | | `to` | `operations[].address` | | `data` | `operations[].data` | | `value` | `operations[].value` (hex string; use `0x0` when empty) | Also send: * `chainId` — number (e.g. `137` for Polygon; see Notus docs for supported networks). * `walletAddress` — the smart wallet address from step 1. * `payGasFeeToken` — ERC-20 contract address used to pay gas (required in the current API; pick the token Notus should debit for fees on that chain). For a **`lend`** action where the bytecode spends **the entire balance** of a token, you usually **cannot** use that same token for `payGasFeeToken`: Notus would try to pull fees from a balance already consumed by the lend flow, and the user operation can **revert**. Prefer paying gas with the strategy **`asset`** (or another token that still has leftover balance), not the **`underlyingAsset`** when that is what you are fully deploying. For **`withdraw`**, paying gas with the **`underlyingAsset`** is typically fine because the flow increases that balance rather than draining it for the core action. Example mapping: ```typescript theme={null} const chainId = Number(bytecodes[0].chainId); const operations = bytecodes.map(({ to, data, value }) => ({ address: to, data, value: value ?? "0x0", })); const { userOperation } = await notusPost("/crypto/custom-user-operation", { chainId, operations, walletAddress: smartWallet, payGasFeeToken: gasTokenAddress, }); ``` The response includes `userOperation.userOperationHash` (and related fields). If Notus returns a **revert reason** preview, fix balances, allowances, or ordering before executing. *** ### 4. Sign and execute the user operation 1. **Sign** the `userOperationHash` with the **EOA** that owns the smart wallet (raw message signature). With viem, sign the hash as raw bytes (not a human-readable EIP-191 message unless Notus specifies otherwise). Notus documents that **`authorization`** may be required on the first transaction for an EIP-7702 wallet: follow their note on viem `signAuthorization()` vs serializing the signature for the execute endpoint — [Execute User Operation](https://docs.notus.team/docs/api-reference/user-operations/ExecuteUserOperationController_executeUserOp_v1). 2. **POST** `/crypto/execute-user-op` with: * `userOperationHash` * `signature` * `authorization` — only when required for that wallet / first operation Execution is **asynchronous** (HTTP 202). Use Notus history or webhooks to track inclusion, as described in their API reference. ```typescript theme={null} const signature = await signRawHash( walletClient, userOperation.userOperationHash, ); await notusPost("/crypto/execute-user-op", { userOperationHash: userOperation.userOperationHash, signature, // authorization: '0x...' // when required }); ``` *** ## Operational notes * **Single chain per custom user operation**: Notus runs `operations` in order on one `chainId`. Multi-chain Pods flows (`crossChain.isCrossChain`) need a different orchestration than a single custom user operation. * **Paymaster / gas token**: Ensure `payGasFeeToken` and balances match Notus expectations for your project tier (errors such as `UNAVAILABLE_COMPUTE_UNITS` are documented on their side). * **Smart wallet deployment**: If the account is not yet deployed on the target chain, resolution or execution may fail until deployment rules for your factory are satisfied (see Notus factory compatibility and your dashboard settings). *** ## Next steps Bytecode flow with embedded smart wallets Wallet auth and Pods API usage End-user deposit guide Custody integration # Privy Integration Source: https://docs.pods.finance/external-integrations/privy-integration Build Pods widget hosts with Privy embedded smart wallets Use Privy when you want Pods widgets to feel like a Web2 product while still keeping wallet custody and transaction execution in your host app. ## Resources | Resource | Description | | - | - | | [Pods Docs](https://docs.pods.finance/) | Official documentation for Pods widgets, strategies, and tx flows | | [Privy Wallets](https://docs.privy.io/wallets) | Privy wallets, embedded wallets, and smart-wallet documentation | | [EarnWidget Integration](/widgets/earn-widget) | Current SDK widget contract for Earn | | [SwapWidget Integration](/widgets/swap-widget) | Current SDK widget contract for Swap | | [LLM Privy Integration Guide](/llms-privy-integration.txt) | Canonical 1-shot prompt for AI-assisted host integration | ## Verified integration contract Install the current verified package set: ```bash theme={null} pnpm add pods-sdk@0.2.87 @deframe-sdk/components@0.1.91 @privy-io/react-auth@3.33.1 permissionless@0.2.57 viem@2.52.0 @reduxjs/toolkit@^2 react-redux@^9 redux@^5 ``` Do not install `@privy-io/react-auth/smart-wallets` as a separate dependency. Import it from the `@privy-io/react-auth` package subpath. ## Environment variables ```bash theme={null} NEXT_PUBLIC_PODS_API_URL=https://api.pods.finance NEXT_PUBLIC_PODS_API_KEY=your_api_key NEXT_PUBLIC_PODS_WEBSOCKET_URL=wss://api.pods.finance/updates NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id ``` ## Styles Import the widget stylesheet before your host CSS. For Next.js App Router: ```tsx theme={null} // app/layout.tsx import '@deframe-sdk/components/styles.css' import './globals.css' ``` For Vite React: ```tsx theme={null} // src/main.tsx import '@deframe-sdk/components/styles.css' import './index.css' ``` For Vite hosts, keep the same names by enabling: ```ts theme={null} import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], envPrefix: ['VITE_', 'NEXT_PUBLIC_'], resolve: { alias: { 'next/link': fileURLToPath(new URL('./src/shims/next-link.tsx', import.meta.url)), }, }, }) ``` For non-Next hosts, add the shim used by that alias: ```tsx theme={null} import type { AnchorHTMLAttributes, PropsWithChildren } from 'react' type NextLinkShimProps = PropsWithChildren< AnchorHTMLAttributes & { href: string } > export default function Link({ href, children, ...props }: NextLinkShimProps) { return ( {children} ) } ``` ## Provider setup ```tsx theme={null} import { PrivyProvider } from '@privy-io/react-auth' import { SmartWalletsProvider } from '@privy-io/react-auth/smart-wallets' import { defineChain, type Chain } from 'viem' import { arbitrum, base, baseSepolia, bsc, gnosis, mainnet, optimism, polygon } from 'viem/chains' import type { ReactNode } from 'react' const hyperEvm = defineChain({ id: 999, name: 'HyperEVM', nativeCurrency: { decimals: 18, name: 'HYPE', symbol: 'HYPE', }, rpcUrls: { default: { http: ['https://rpc.hyperliquid.xyz/evm'], }, }, blockExplorers: { default: { name: 'HyperEVM Explorer', url: 'https://hyperevmscan.io', }, }, }) const PODS_SUPPORTED_CHAINS: Chain[] = [ mainnet, optimism, bsc, polygon, base, baseSepolia, arbitrum, gnosis, hyperEvm, ] const PODS_DEFAULT_CHAIN = PODS_SUPPORTED_CHAINS.find((chain) => chain.id === polygon.id) ?? PODS_SUPPORTED_CHAINS[0] export function PrivyProviders({ children }: { children: ReactNode }) { return ( {children} ) } ``` ## Host integration pattern Inside your widget host component: * Use `usePrivy`, `useWallets`, `useCreateWallet`, `getEmbeddedConnectedWallet`, and `useSmartWallets`. * Reuse the current embedded wallet when it already exists. * If the user is authenticated but still has no smart wallet, show a “Create smart wallet” action before mounting the widget. * Keep Privy `supportedChains` in sync with every EVM chain that Pods bytecode can target; `sendTransaction`, `switchChain`, and `getClientForChain({ id })` fail when the target chain is not supported. * Pass the smart-wallet address into `DeframeProvider.config.walletAddress`. * Use the branded `NEXT_PUBLIC_PODS_API_URL`, `NEXT_PUBLIC_PODS_API_KEY`, and `NEXT_PUBLIC_PODS_WEBSOCKET_URL` env names. * Map those env values into the current `DeframeProvider` API config fields. * Set `enableCrossChainInvestments: true`. ## Complete `EarnWidgetHost` reference implementation If you want a full working host example instead of just the smaller integration fragments, use this as a reference implementation: ```tsx theme={null} 'use client' import { useCallback, useMemo, useState } from 'react' import { PodsSwapFormView, } from '@deframe-sdk/components' import { DeframeProvider, EarnWidget, type DeframeProviderProps, type TxUpdateEvent, } from 'pods-sdk' import { getEmbeddedConnectedWallet, useCreateWallet, usePrivy, useWallets, } from '@privy-io/react-auth' import { useSmartWallets } from '@privy-io/react-auth/smart-wallets' const ROUTE_TO_STATE: Record = { history: 'History', wallet: 'Wallet', swap: 'Swap', earn: 'Earn', overview: 'Overview', details: 'Details', deposit: 'Deposit', withdraw: 'Withdraw', 'investment-details': 'Investment Details', 'history-deposit-details': 'Deposit History Details', 'history-withdraw-details': 'Withdraw History Details', 'history-swap': 'Swap History', 'history-swap-details': 'Swap History Details', } type ProcessBytecode = NonNullable type RawBytecode = { chainId?: number | string to: string data: string value: string gasLimit?: string } type PayloadShape = { clientTxId: string bytecodes: RawBytecode[] simulateError?: boolean } type Eip1193Provider = { request: (args: { method: string; params?: unknown[] }) => Promise } type PodsSmartWalletClient = { sendTransaction: (input: { calls: Array<{ to: string; data: string; value?: bigint }> }) => Promise } type SmartWalletData = { client?: { account?: { address?: string } } & PodsSmartWalletClient getClientForChain?: ( input: { id: number }, ) => PodsSmartWalletClient | Promise | null | undefined } type ActiveWalletShape = { chainId?: string | number address?: string walletClientType?: string switchChain?: (chainId: number) => Promise getEthereumProvider?: () => Promise getProvider?: () => Promise } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } function normalizeWalletChainId(chainId?: string | number): number | undefined { if (typeof chainId === 'number' && Number.isInteger(chainId) && chainId > 0) { return chainId } if (typeof chainId === 'string') { const [scope, raw] = chainId.split(':') if (scope === 'eip155' && raw) { const parsed = Number.parseInt(raw, 10) return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined } const parsed = Number.parseInt(raw ?? chainId, 10) return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined } return undefined } function toBigIntValue(value?: string): bigint | undefined { if (!value) return 0n const normalized = value.trim() if (!normalized || normalized === '0') return 0n try { return BigInt(normalized) } catch { return undefined } } function isSignatureRejected(error: unknown): boolean { const maybeError = error as { code?: number | string; message?: string } const code = maybeError?.code const message = String(maybeError?.message || '').toLowerCase() return code === 4001 || code === 'ACTION_REJECTED' || message.includes('rejected') } function sleep(ms: number): Promise { return new Promise((resolve) => { setTimeout(resolve, ms) }) } async function waitForReceipt( provider: Eip1193Provider, txHash: string, ): Promise<{ status?: string | number }> { const startedAt = Date.now() while (Date.now() - startedAt < 120_000) { const receipt = await provider.request({ method: 'eth_getTransactionReceipt', params: [txHash] }) if (isRecord(receipt) && receipt.status != null) { return receipt as { status?: string | number } } await sleep(1500) } throw new Error('Timed out waiting for transaction confirmation') } async function resolveWalletProvider(wallet: ActiveWalletShape): Promise { if (wallet.getEthereumProvider) { const provider = await wallet.getEthereumProvider() if (provider) return provider } if (wallet.getProvider) { return wallet.getProvider() } return undefined } export function EarnWidgetHost() { const { ready, authenticated, login, logout, user } = usePrivy() const { wallets } = useWallets() const smartWalletData = useSmartWallets() as SmartWalletData const { createWallet } = useCreateWallet() const { client: smartWalletClient, getClientForChain } = smartWalletData const [routeName, setRouteName] = useState('Overview') const [txLogs, setTxLogs] = useState([]) const [isCreatingSmartWallet, setIsCreatingSmartWallet] = useState(false) const embeddedWallet = getEmbeddedConnectedWallet(wallets) const activeWallet = embeddedWallet ?? wallets[0] const smartWalletAddress = smartWalletClient?.account?.address const hasEmbeddedWallet = Boolean(embeddedWallet?.address) const hasSmartWalletSession = Boolean(smartWalletAddress && smartWalletClient) const podsApiUrl = process.env.NEXT_PUBLIC_PODS_API_URL const podsApiKey = process.env.NEXT_PUBLIC_PODS_API_KEY const podsWebSocketUrl = process.env.NEXT_PUBLIC_PODS_WEBSOCKET_URL const routeNavigate = useCallback((route: string) => { setRouteName((current) => { const mapped = ROUTE_TO_STATE[route] ?? route return current === mapped ? current : mapped }) }, []) const logEvent = useCallback((entry: string) => { const timestamp = new Date().toLocaleTimeString() setTxLogs((previous) => [`${timestamp} ${entry}`, ...previous].slice(0, 24)) }, []) const createSmartWallet = useCallback(async () => { if (!createWallet) { logEvent('Create wallet flow unavailable: missing createWallet hook.') return } if (hasEmbeddedWallet) { logEvent('Embedded wallet already exists. Waiting for the smart-wallet session.') return } setIsCreatingSmartWallet(true) try { await createWallet() logEvent('Smart wallet creation requested.') } catch (error) { const msg = String((error as { message?: string })?.message || 'Failed to create smart wallet') logEvent(`Create wallet failed: ${msg}`) } finally { setIsCreatingSmartWallet(false) } }, [createWallet, hasEmbeddedWallet, logEvent]) const processBytecode = useCallback(async (payload: PayloadShape, ctx) => { const rawTransactions: RawBytecode[] = payload.bytecodes const emit = (event: TxUpdateEvent): void => { ctx.updateTxStatus(event) const detail = 'txHash' in event && event.txHash ? ` tx=${String(event.txHash).slice(0, 10)}...` : '' const extra = 'code' in event && event.code ? ` code=${String(event.code)}` : '' const message = 'message' in event && event.message ? ` message=${String(event.message)}` : '' logEvent(`${event.type}${detail}${extra}${message}`) } if (!ready || !authenticated || !activeWallet || !hasSmartWalletSession) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'NO_SMART_WALLET_SESSION', message: 'No Privy smart-wallet session is ready.', }) return } if (activeWallet?.walletClientType !== 'privy') { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'UNSUPPORTED_WALLET', message: 'This integration only accepts Privy embedded smart-wallet sessions.', }) return } if (payload.simulateError) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'SIMULATED_ERROR', message: 'simulateError=true', }) return } let hasSubmittedTx = false try { const provider = await resolveWalletProvider(activeWallet) if (!provider) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'NO_PROVIDER', message: 'No wallet provider available for signatures.', }) return } if (!smartWalletClient && !getClientForChain) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'UNSUPPORTED_WALLET', message: 'No smart wallet client available.', }) return } if (!smartWalletAddress) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'UNSUPPORTED_WALLET', message: 'No smart wallet address available for this session.', }) return } emit({ type: 'HOST_ACK', clientTxId: payload.clientTxId }) const grouped = new Map() const fallbackChainId = normalizeWalletChainId(activeWallet.chainId) for (const tx of rawTransactions) { const chainId = normalizeWalletChainId(tx.chainId) ?? fallbackChainId if (!chainId) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'INVALID_CHAIN', message: 'Missing or invalid chainId in processBytecode payload.', }) return } const current = grouped.get(chainId) ?? [] current.push(tx) grouped.set(chainId, current) } for (const [chainId, transactions] of grouped) { if (activeWallet.switchChain) { const currentChain = normalizeWalletChainId(activeWallet.chainId) if (currentChain !== chainId) { await activeWallet.switchChain(chainId) } } const client = await Promise.resolve( typeof getClientForChain === 'function' ? getClientForChain({ id: chainId }) : smartWalletClient, ) if (!client) { emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: 'UNSUPPORTED_WALLET', message: `Smart wallet client is not initialized for chainId=${chainId}.`, }) return } const calls = transactions.map((tx) => { const normalizedValue = toBigIntValue(tx.value) return { to: tx.to, data: tx.data, ...(normalizedValue && normalizedValue > 0n ? { value: normalizedValue } : {}), } }) for (const _tx of transactions) { emit({ type: 'SIGNATURE_PROMPTED', clientTxId: payload.clientTxId, }) } const txHash = await client.sendTransaction({ calls }) hasSubmittedTx = true for (const _tx of transactions) { emit({ type: 'TX_SUBMITTED', clientTxId: payload.clientTxId, chainId, txHash, }) } const receipt = await waitForReceipt(provider, txHash) if (receipt.status === '0x0' || receipt.status === 0) { for (const _tx of transactions) { emit({ type: 'TX_REVERTED', clientTxId: payload.clientTxId }) } return } for (const _tx of transactions) { emit({ type: 'TX_CONFIRMED', clientTxId: payload.clientTxId }) } } emit({ type: 'TX_FINALIZED', clientTxId: payload.clientTxId }) } catch (error) { if (isSignatureRejected(error)) { emit({ type: 'SIGNATURE_DECLINED', clientTxId: payload.clientTxId }) return } const maybeError = error as { code?: number | string; message?: string } if (hasSubmittedTx) { emit({ type: 'TX_FAILED', clientTxId: payload.clientTxId, code: String(maybeError.code ?? 'UNKNOWN_ERROR'), message: String(maybeError.message ?? 'Failed to execute smart-wallet transaction'), }) return } emit({ type: 'SIGNATURE_ERROR', clientTxId: payload.clientTxId, code: String(maybeError.code ?? 'UNKNOWN_ERROR'), message: String(maybeError.message ?? 'Failed to execute smart-wallet transaction'), }) } }, [activeWallet, authenticated, getClientForChain, hasSmartWalletSession, logEvent, ready, smartWalletAddress, smartWalletClient]) const config = useMemo( () => ({ // Next.js client example. In Vite, replace process.env with import.meta.env. // pods-sdk@0.2.87 still expects these provider field names. DEFRAME_API_URL: podsApiUrl, DEFRAME_API_KEY: podsApiKey, DEFRAME_WEBSOCKET_URL: podsWebSocketUrl, walletAddress: smartWalletAddress, userId: user?.id, globalCurrency: 'USD' as const, globalCurrencyExchangeRate: 1, enableCrossChainInvestments: true, components: { SwapFormView: PodsSwapFormView, }, theme: { mode: 'dark' as const, preset: 'default' as const, }, debug: process.env.NODE_ENV === 'development', }), [podsApiKey, podsApiUrl, podsWebSocketUrl, smartWalletAddress, user?.id], ) const missingConfig = !podsApiUrl || !podsApiKey const showWalletAction = ready && authenticated && !hasSmartWalletSession let walletActionLabel = 'Create smart wallet' if (isCreatingSmartWallet) { walletActionLabel = 'Creating smart wallet...' } if (hasEmbeddedWallet && !hasSmartWalletSession) { walletActionLabel = 'Smart wallet session loading...' } const walletActionDisabled = isCreatingSmartWallet || hasEmbeddedWallet let widgetGateMessage: string | null = null if (!ready) { widgetGateMessage = 'Loading Privy...' } else if (!authenticated) { widgetGateMessage = 'Connect with Privy to load the widget.' } else if (missingConfig) { widgetGateMessage = 'Please provide NEXT_PUBLIC_PODS_API_URL and NEXT_PUBLIC_PODS_API_KEY.' } else if (!hasSmartWalletSession) { widgetGateMessage = hasEmbeddedWallet ? 'Waiting for the Privy smart-wallet session...' : 'Create a smart wallet before loading the widget.' } return (

EarnWidget integration

{ready && authenticated ? ( ) : null} {ready && !authenticated ? ( ) : null} {showWalletAction ? ( ) : null}
{widgetGateMessage ? (
{widgetGateMessage}
) : (
)}
{txLogs.length === 0 ? (

No tx status updates yet.

) : ( txLogs.map((entry, index) =>

{entry}

) )}
Current route: {routeName}
) } ``` Suggested host CSS for that example: ```css theme={null} .pods-host-shell { width: min(1180px, 100%); max-width: 100%; margin: 0 auto; overflow-x: hidden; } .deframe-widget-card { width: min(760px, 100%); max-width: 100%; margin: 0 auto; overflow-x: hidden; } .deframe-widget-card > * { width: min(760px, 100%); max-width: 100%; min-width: 0; } ``` ## Add Swap to the Same Privy Host When your app exposes both Invest and Swap, keep the Privy wallet logic, `config`, and `processBytecode` in one shared provider component, register the Pods Wallet-style swap view in that provider config, then mount either widget inside it. ```tsx theme={null} import { PodsSwapFormView, } from '@deframe-sdk/components' import { DeframeProvider, EarnWidget, SwapWidget, type DeframeProviderProps, } from 'pods-sdk' import { useMemo, type ReactNode } from 'react' type ProcessBytecode = NonNullable type PrivyDeframeProviderProps = { children: ReactNode config: DeframeProviderProps['config'] processBytecode: ProcessBytecode } function PrivyDeframeProvider({ children, config, processBytecode, }: PrivyDeframeProviderProps) { const providerConfig = useMemo(() => ({ ...config, components: { ...(config.components ?? {}), SwapFormView: PodsSwapFormView, }, }), [config]) return ( {children} ) } type SharedWidgetRouteProps = { config: DeframeProviderProps['config'] processBytecode: ProcessBytecode } export function InvestRoute({ config, processBytecode, onRouteChange, }: SharedWidgetRouteProps & { onRouteChange: (route: string) => void }) { return ( ) } export function SwapRoute({ config, processBytecode, fromTokenAddress, fromChainId, toTokenAddress, toChainId, }: SharedWidgetRouteProps & { fromTokenAddress?: string fromChainId?: number toTokenAddress?: string toChainId?: number }) { return ( ) } ``` Keep the provider mounted only after the Privy smart-wallet session is ready. Use the [SwapWidget guide](/widgets/swap-widget) for URL prefill and remount-key handling. ## `processBytecode` expectations The host still owns execution. Your `processBytecode` bridge must: * **group legs by `chainId` and submit one batched transaction per chain** (the example below uses `client.sendTransaction({ calls })` — do not send legs individually) * emit `HOST_ACK` * emit `SIGNATURE_PROMPTED` * submit transactions through the Privy smart-wallet session * emit `TX_SUBMITTED` * wait for confirmation through the wallet provider * emit `TX_CONFIRMED` * emit `TX_FINALIZED` * map user cancellations to `SIGNATURE_DECLINED` * map other pre-submit failures to `SIGNATURE_ERROR` For the full transaction lifecycle contract, see [processBytecode Contract](/widgets/process-bytecode). ## Branding / theme customization Use `DeframeProvider.config.theme` as the main theme API: ```tsx theme={null} theme: { mode: 'dark', preset: 'default', overrides: { dark: { colors: { brandPrimary: '#2DD881', brandSecondary: '#8EF0B0', bgDefault: '#0B0F14', bgSubtle: '#121820', bgMuted: '#1B2330', bgRaised: '#161E29', textPrimary: '#F6F8FB', textSecondary: '#AAB6C5', textDisabled: '#64748B', }, }, }, } ``` You can also apply widget-scoped CSS variables from the host shell: ```css theme={null} .pods-host-shell .deframe-widget { --deframe-widget-color-brand-primary: #2dd881; --deframe-widget-color-bg-primary: #0b0f14; --deframe-widget-color-bg-raised: #161e29; --deframe-widget-color-text-primary: #f6f8fb; --deframe-widget-color-text-secondary: #aab6c5; --deframe-widget-size-radius-lg: 18px; } ``` Keep those overrides scoped to `.deframe-widget`, never global on `body` or `:root`. ## Authentication flow Before creating wallets or rendering an authenticated Earn host, users must be logged in. At a minimum, your app should provide: * a way to start login * a way to complete login * a boolean gate for authenticated vs unauthenticated UI * a logout path Helpful hooks from `@privy-io/react-auth`: ### `usePrivy()` | Property | Description | | - | - | | `ready` | SDK is ready to be used | | `authenticated` | Whether the user is authenticated | | `user` | User object with linked accounts | | `login()` | Starts login flow | | `logout()` | Ends the session | ### `useLoginWithEmail()` | Property | Description | | - | - | | `sendCode({ email })` | Sends an OTP to the email | | `loginWithCode({ code })` | Completes login with the OTP | | `state.status` | Current login state | | `state.error` | Error details when login fails | ```tsx theme={null} import { useState } from 'react' import { usePrivy, useLoginWithEmail } from '@privy-io/react-auth' export function EmailLoginForm() { const { ready, authenticated } = usePrivy() const { sendCode, loginWithCode, state } = useLoginWithEmail() const [email, setEmail] = useState('') const [code, setCode] = useState('') const handleSendCode = async () => { await sendCode({ email }) } const handleLoginWithCode = async () => { await loginWithCode({ code }) } if (!ready) return null if (authenticated) return null return (
setEmail(event.target.value)} /> setCode(event.target.value)} /> {state.error ?

{state.error.message}

: null}
) } ``` ## Embedded wallet + smart wallet flow The practical flow is: 1. authenticate the user 2. create or reuse the embedded Privy wallet 3. let Privy expose the smart-wallet client for transaction submission 4. pass the smart-wallet address to Pods ### Create or reuse the embedded wallet #### `useWallets()` | Property | Description | | - | - | | `ready` | Wallet list is ready | | `wallets` | Linked/created wallets for the user | #### `useCreateWallet()` | Property | Description | | - | - | | `createWallet()` | Creates the embedded wallet for the user when needed | ```tsx theme={null} import { useCreateWallet, useWallets } from '@privy-io/react-auth' const { ready, wallets } = useWallets() const { createWallet } = useCreateWallet() if (ready && wallets.length === 0) { return } ``` ### Access the smart-wallet client #### `useSmartWallets()` | Property | Description | | - | - | | `client` | Smart-wallet client instance when available | | `client.account.address` | Smart-wallet address | | `getClientForChain({ id })` | Returns a chain-specific smart-wallet client | ```tsx theme={null} import { useSmartWallets } from '@privy-io/react-auth/smart-wallets' const { client, getClientForChain } = useSmartWallets() const smartWalletAddress = client?.account.address ``` Smart wallets are commonly counterfactual. You may have an address before the contract is deployed on a specific chain. Before depending on a smart wallet on a target network: * check whether the contract is deployed on that chain * if needed, trigger deployment with a minimal transaction * ensure your paymaster or sponsorship setup has enough balance ### Verify whether the smart wallet is deployed ```tsx theme={null} import { createPublicClient, http } from 'viem' import { polygon } from 'viem/chains' const smartWalletAddress = '0xYOUR_SMART_WALLET_ADDRESS' const publicClient = createPublicClient({ chain: polygon, transport: http(), }) const code = await publicClient.getCode({ address: smartWalletAddress as `0x${string}` }) const isDeployed = !!code && code !== '0x' console.log({ isDeployed, code }) ``` ### Trigger deployment with a minimal transaction ```tsx theme={null} import { useSmartWallets } from '@privy-io/react-auth/smart-wallets' const { client, getClientForChain } = useSmartWallets() const smartWalletAddress = client?.account.address const chainClient = await getClientForChain({ id: 137 }) if (!chainClient || !smartWalletAddress) throw new Error('Missing chain client or smart wallet') await chainClient.sendTransaction({ calls: [ { to: smartWalletAddress, value: 0n, data: '0x', }, ], }) ``` In Pods integrations, the smart-wallet address is the address you normally pass into `DeframeProvider.config.walletAddress`. ## Fetching strategies and bytecode directly from Pods API Even when you use the widget, it can still help to understand the underlying API calls for debugging or custom host flows. All examples below use: * `NEXT_PUBLIC_PODS_API_URL` as base URL * `NEXT_PUBLIC_PODS_API_KEY` in `x-api-key` ### List strategies ```tsx theme={null} const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY const url = new URL('/strategies', baseUrl) url.searchParams.set('limit', '100') const res = await fetch(url.toString(), { method: 'GET', headers: { 'x-api-key': apiKey, }, }) const json = await res.json() console.log(json) ``` ### Get strategy details for a wallet ```tsx theme={null} const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY const strategyId = 'Aave-USDT-polygon' const wallet = '0xYOUR_WALLET' const url = new URL(`/v2/strategies/${strategyId}`, baseUrl) url.searchParams.set('wallet', wallet) const res = await fetch(url.toString(), { method: 'GET', headers: { 'x-api-key': apiKey, }, }) const json = await res.json() console.log(json) ``` ### Generate strategy bytecode ```tsx theme={null} const baseUrl = import.meta.env.NEXT_PUBLIC_PODS_API_URL const apiKey = import.meta.env.NEXT_PUBLIC_PODS_API_KEY const strategyId = 'Aave-USDT-polygon' const action = 'lend' const wallet = '0xYOUR_WALLET' const amount = '1500000' const url = new URL(`/strategies/${strategyId}/bytecode`, baseUrl) url.searchParams.set('action', action) url.searchParams.set('wallet', wallet) url.searchParams.set('amount', amount) const res = await fetch(url.toString(), { method: 'GET', headers: { 'x-api-key': apiKey, }, }) const json = await res.json() console.log(json) ``` Typical response shape: ```ts theme={null} type PodsBytecodeResponse = { feeCharged: string bytecode: Array<{ to: string value: string data: string chainId: string }> } ``` ## Executing strategy bytecodes with Privy After you receive strategy bytecodes, execute them with the smart-wallet client for the target chain. ```tsx theme={null} import { useSmartWallets } from '@privy-io/react-auth/smart-wallets' const { getClientForChain } = useSmartWallets() async function executePodsBytecodes(resp: PodsBytecodeResponse) { const first = resp.bytecode[0] if (!first) throw new Error('Empty bytecode array') const chainId = Number(first.chainId) const chainClient = await getClientForChain({ id: chainId }) if (!chainClient) throw new Error('Chain client not found') const calls = resp.bytecode.map((b) => ({ to: b.to as `0x${string}`, data: b.data as `0x${string}`, value: BigInt(b.value), })) const txHash = await chainClient.sendTransaction({ calls }) console.log({ txHash }) } ``` If you are using sponsorship or a paymaster, make sure it has enough balance before testing the flow. ## Recommended references * [EarnWidget Integration](/widgets/earn-widget) * [LLM Privy Integration Guide](/llms-privy-integration.txt) * [Widget Integration Overview](/widget-integration) # Architecture Overview Source: https://docs.pods.finance/getting-started/architecture Understanding the Pods API system architecture and trustless operation flow Pods API operates as a trustless DeFi aggregation platform. We do not hold, manage, or pass any funds through our smart contracts. Transaction signing and propagation are handled entirely on your side, ensuring full custody remains with you. ## System Architecture Pods API Architecture - System flow showing API interaction, protocol data, transaction execution, and monitoring The diagram above illustrates the complete flow of interactions between your application, Pods API, and various DeFi protocols across multiple blockchain networks. ## API Flow Patterns ### Canonical chain identifiers Swap quotes, strategy bytecode payloads, and related responses expose **`chainIdIn`** (source network) and **`chainIdOut`** (destination) as numeric EVM chain IDs. Use them for routing and display instead of string network names where possible. Single-network flows (for example `GET /transfer/bytecode`) use top-level **`chainId`** as a number. Listing actions supports optional query **`chainId`** to filter by step network. Each item in `bytecode[]` / `transactionData[]` carries its own **`chainId` as a decimal string** (JSON serialization of `bigint`) — for example `"8453"`, not `8453`. Coerce with `Number(tx.chainId)` before comparing to numeric route fields or wallet APIs. See the OpenAPI `TransactionData` schema and [Get a Quote](/guides/swaps/get-quote) for request examples. ### 1. Protocol Data Retrieve quotes and protocol information using the API. **Endpoints:** ```http theme={null} GET /v2/strategies/:id GET /strategies GET /v2/swap/quote ``` **Purpose:** * Fetch available protocols and their current rates * Get quotes for swaps or yield positions * Compare rates across different protocols and chains ### 2. Transaction Data Request transaction data (bytecode) needed for execution. **Endpoints:** ```http theme={null} POST /v2/swap/bytecode GET /strategies/{strategyId}/bytecode GET /transfer/bytecode ``` **Purpose:** * Provides the bytecode for executing transactions * Supports multiple transaction types: * Cross-chain swaps * Same-chain swaps * Yield positions (deposit/withdraw) * Token and native token transfers ### 3. Sign & Propagate Bytecode **Atomic execution required.** `bytecode[]` is a bundle — execute all legs on the same `chainId` in **one transaction** (EIP-7702 batch, smart-account userOp, or Safe/Fireblocks batch). Sending legs as separate transactions can leave funds stuck in `PENDING` actions. See [Executing Bytecode](/getting-started/executing-bytecode) for the `output` decision tree. **User Responsibility:** Transaction signature and propagation are handled **entirely on your side**. Pods does not hold, manage, or pass any funds through its smart contracts. **Key Points:** * Users sign transactions themselves using their own wallets * Choose `output` (`bytecode`, `userOperation`, `fireblocks`) to match your wallet stack * Transaction propagation is handled by the user * Full custody remains with the user at all times * Pods never has access to your private keys or funds ### 4. Position Monitoring After transaction execution, Pods monitors positions and provides updates. **Endpoints:** ```http theme={null} GET /actions/{id} GET /actions?walletAddress=:address GET /v2/wallets/:address GET /v2/strategies/:strategyId?wallet=:address ``` **Purpose:** * Track execution status after `POST /v2/swap/bytecode` or cross-chain strategy bytecode returns an `id` * Track wallet activity across chains with `GET /actions?walletAddress=:address` * Retrieve a wallet overview with positions and history from `GET /v2/wallets/:address` * Retrieve one strategy's wallet-scoped position and history from `GET /v2/strategies/:strategyId?wallet=:address` * Monitor yield earnings in real-time * View swap history and transaction status * Aggregate positions across multiple protocols ### 5. Alerts & Notifications Configure alerts and receive notifications for position monitoring. **Features:** * **Webhook notifications** - Real-time callbacks to your server * **Transaction status updates** - Track cross-chain transaction progress * **Position changes** - Monitor yield earnings and balances * **APY change notifications** - Get alerted when rates change significantly Configure webhook URLs in your customer dashboard to receive automatic notifications for transaction status changes. ### 6. Integration Features Additional features to enhance user protection and experience. **Available Features:** * **Multi-provider routing** - Automatic selection of best swap provider * **Cross-chain optimization** - Best route for bridge transactions * **Real-time APY tracking** - Updated protocol rates * **Gas estimation** - Accurate gas cost predictions * **Transaction simulation** - Pre-execution validation ## Rate Limiting API rate limits ensure fair usage and system stability: - **100 requests per minute** per API key - **1,000 requests per hour** per API key If you need higher limits, contact our support team. Check real-time API uptime and incident history ## Success responses Successful calls return **HTTP 2xx** with a JSON body that is the resource or a small outcome object (for example `{ "status": "ok" }` for health checks). Responses are **not** wrapped in `{ "success": true }`—use the status code to detect success. Some deletes return **204 No Content** with an empty body. Errors always use **4xx/5xx**; the API does not return `success: false` with HTTP 200. ## Dates and times Response bodies expose instants as **ISO 8601** strings in UTC (RFC 3339), for example `2026-04-08T14:30:00.000Z`. This applies to fields such as swap `quote.deadlineDate`, strategy quote `timestamp` / `validUntil`, tracking `createdAt` / `updatedAt` / `finishedAt`, and wallet history timestamps. Swap `quote.deadline` is a Unix timestamp in seconds; use `quote.deadlineDate` for a human-readable expiration instant. Query parameters that filter by date (for example on `GET /v2/strategies/:strategyId`) use the same string format; `z.string().datetime()` accepts values like `2026-01-01T00:00:00Z`. ## Error Handling Failed requests return JSON in a single shape: `{ "error": { "code", "message", "details?" } }` (the numeric status is only in the HTTP status line). Validation errors (**422**) include `details.errors` with field paths and messages. The API uses standard HTTP response codes: | Code | Meaning | Description | | - | - | - | | 200 | Success | Request completed successfully | | 204 | No Content | Success with no response body (e.g. some deletes) | | 400 | Bad Request | Invalid parameters or request format | | 401 | Unauthorized | Invalid or missing API key | | 404 | Not Found | Resource does not exist | | 409 | Conflict | Resource already exists or conflicting state | | 422 | Unprocessable Entity | Validation failed (e.g. Zod) | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Server-side error occurred | ## Key Guarantees No fund custody - you maintain full control of your assets at all times Real-time tracking of your DeFi positions across chains and protocols Webhook alerts for transaction status and position updates Seamless operations across Ethereum, Base, Optimism, Arbitrum, Solana, Polygon, Gnosis, and Bitcoin ## Next Steps Atomic batching and output format (`bytecode` vs `userOperation` vs `fireblocks`) Add yield to your app in 5 minutes Deposit, withdraw, and monitor positions Copy-paste end-to-end strategy deposit Same-chain & cross-chain swap quotes Read more about [Why Pods](/getting-started/why-pods) and explore our [complete code examples](/examples/javascript/strategy-deposit). # Authentication Source: https://docs.pods.finance/getting-started/authentication Learn how to authenticate with the Pods API Pods API uses API keys to authenticate most requests. Include your API key in the `x-api-key` header for swap, strategy, wallet, and tracking endpoints. `GET /health` is public and does not require authentication. Customer dashboard endpoints (`GET /customers/me`, `/customers/:id/tokens`, `/customers/:id/token-groups`) require a **Bearer** token (Supabase or Magic JWT) — API key alone is not sufficient because those routes enforce user role checks. ## Getting an API Key Create an account at [pods.finance](https://pods.finance) Go to the **API Keys** section in your dashboard Click "Generate API Key" and give it a descriptive name Copy your API key immediately - you won't be able to see it again! **Keep your API keys secure!** Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, etc. ## Using Your API Key Include your API key in the `x-api-key` header with every request: ```javascript JavaScript/TypeScript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY } }) // Make authenticated request const response = await pods.get('/strategies') ``` ```python Python theme={null} import requests import os headers = { 'x-api-key': os.getenv('PODS_API_KEY') } response = requests.get( 'https://api.pods.finance/strategies', headers=headers ) ``` ```bash cURL theme={null} curl -X GET "https://api.pods.finance/strategies" \ -H "x-api-key: $PODS_API_KEY" ``` ```go Go theme={null} package main import ( "net/http" "os" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.pods.finance/strategies", nil) req.Header.Add("x-api-key", os.Getenv("PODS_API_KEY")) resp, _ := client.Do(req) } ``` Store your API key in a secure server-side environment variable (e.g. `PODS_API_KEY`). Do not expose it in frontend code or commit it to git. ## API Key Authentication **Recommended for all integrations** Use the `x-api-key` header for all API requests: ```javascript theme={null} headers: { 'x-api-key': 'your-api-key' } ``` **Best for:** * Backend services * Server-to-server communication * Automated scripts and bots * CI/CD pipelines * All application types API keys are tied to your customer account and can be rotated or revoked at any time from your dashboard. ## Bearer Token Authentication **Required for customer dashboard endpoints** Use a Supabase or Magic JWT in the `Authorization` header for routes such as `GET /customers/me`, `/customers/:id/tokens`, and `/customers/:id/token-groups`: ```javascript theme={null} headers: { Authorization: 'Bearer your-jwt-token' } ``` These routes enforce user role checks (`admin`, `owner`, `developer`, `viewer`). API key authentication alone does not satisfy those checks even when the key is valid. **Best for:** * Customer dashboard and admin UI * Token group and wallet token management * Profile and account settings (`GET /customers/me`) ## Rate Limits API keys are subject to rate limits to ensure fair usage: * **100 requests per minute** per API key * **1,000 requests per hour** per API key ### Handling Rate Limits When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```json theme={null} { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Please wait before making more requests.", "details": { "retryAfter": 60 } } } ``` ```javascript JavaScript theme={null} import axios from 'axios' const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY } }) // Add rate limit handling pods.interceptors.response.use( response => response, async error => { if (error.response?.status === 429) { const retryAfter = error.response.data.error.details.retryAfter console.log(`Rate limited. Retrying after ${retryAfter}s...`) await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)) // Retry the request return pods(error.config) } return Promise.reject(error) } ) ``` ```python Python theme={null} import requests import time def make_request_with_retry(url, headers, max_retries=3): for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code == 429: retry_after = response.json()['error']['details']['retryAfter'] print(f"Rate limited. Waiting {retry_after}s...") time.sleep(retry_after) continue return response raise Exception("Max retries exceeded") ``` ## Security Best Practices Never hardcode API keys in your source code. **Do:** * Use environment variables * Use secret management services (AWS Secrets Manager, HashiCorp Vault) * Use encrypted configuration files **Don't:** * Commit keys to version control * Include keys in client-side code * Share keys via email or messaging apps Rotate your API keys periodically to minimize security risks. **Recommended rotation schedule:** * Production keys: Every 90 days * Development keys: Every 180 days * Immediately if compromised **How to rotate:** 1. Generate a new API key in your dashboard 2. Update your application with the new key 3. Test that the new key works 4. Delete the old key Create separate API keys for each environment to isolate security risks. ``` Development → dev_key_xxx Staging → stg_key_xxx Production → prod_key_xxx ``` This way, if a development key is compromised, your production environment remains secure. Regularly check your API key usage in the dashboard: * Track request volume * Monitor for unusual patterns * Review error rates * Check which endpoints are being accessed Set up alerts for: * Unusually high request volume * Requests from unexpected IP addresses * High error rates ## Troubleshooting **Problem**: Your request returns a 401 Unauthorized error. **Solutions**: * Verify your API key is correct * Check that the `x-api-key` header is included * Ensure no extra spaces or characters in the key * Verify the key hasn't been deleted or revoked ```javascript theme={null} // ✅ Correct headers: { 'x-api-key': 'your-api-key' } // ❌ Wrong - don't use "Bearer" prefix headers: { 'x-api-key': 'Bearer your-api-key' } // ❌ Wrong - incorrect header name headers: { 'Authorization': 'your-api-key' } ``` **Problem**: Your request returns a 403 Forbidden error. **Possible causes**: * API key doesn't have permission for this endpoint * Account has been suspended * Request from blocked IP address **Solution**: Contact support at [support@pods.finance](mailto:support@pods.finance) **Problem**: New API key doesn't work immediately. **Solution**: API keys may take up to 30 seconds to propagate. Wait a moment and try again. If the issue persists: 1. Verify you copied the entire key 2. Check for hidden characters or spaces 3. Try generating a new key ## Next Steps Make your first API call Explore available endpoints Deposit, withdraw, and monitor positions Integrate token swaps into your app # Executing Bytecode Source: https://docs.pods.finance/getting-started/executing-bytecode How to sign and broadcast Pods transaction bundles atomically, and which output format to use for your wallet stack Strategy deposits, withdrawals, and swaps return one or more transaction legs in `bytecode[]` (or `transactionData[]` for swaps). **These legs are a single logical operation** — not independent transactions you can pick and choose. **Do not send each item in `bytecode[]` as a separate wallet transaction.** Partial execution can move funds into Pods escrow while the action stays `PENDING` indefinitely (until SLA refund or support). This is especially common on cross-chain flows (e.g. Ondo Stocks) where the origin chain bundle may include approve + bridge legs. Execute every leg that shares the same `chainId` in **one atomic transaction**. ## Atomic execution Filter legs by `chainId` (or `chainIdIn` on strategy responses), then batch them: | Wallet setup | How to batch | | - | - | | **EOA** (MetaMask, wagmi, viem) | [EIP-7702 `executeBatch`](/examples/javascript/using-eip7702) — one type-4 tx from the EOA | | **ERC-4337 smart account** (Privy, Alchemy AA, Notus) | One user operation with internal batch — use `output=userOperation` | | **Gnosis Safe / Fireblocks smart wallet** | Safe `execTransaction` with MultiSend, or `output=fireblocks` | | **Custom executor** | Your own multicall / account-abstraction batch over the returned calldata | All legs in the batch succeed or all revert — no half-executed funding. Cross-chain strategy flows may require signatures on **more than one chain** (e.g. fund on Base, sell on BSC). Each chain's legs are still one atomic bundle per chain — never split legs within the same `chainId`. ## Choosing `output` Bytecode endpoints accept `output` to match your signing infrastructure: | `output` | When to use | Response field | Signing flow | | - | - | - | - | | `bytecode` (default) | EOA with EIP-7702, Solana wallet (default), or you batch calldata yourself | `bytecode[]` / `transactionData[]` (EVM) or `transactionData.rawTransaction` (Solana base64) | Batch all same-chain legs → one tx (EVM); sign and submit `rawTransaction` (Solana) | | `userOperation` | ERC-4337 smart account (Privy embedded wallet, Alchemy AA, Notus) — EVM-origin only | `userOperation` | Submit one userOp via your bundler | | `fireblocks` | Fireblocks custody + smart wallet — EVM-origin only | `transactionData` (Fireblocks shape) | `POST /v1/transactions` with vault `accountId` | | `instructions` | Solana-origin only; wallets that assemble instructions themselves | `{ instructions, lutsByAddress? }` | Assemble and sign via your Solana wallet (or `/svm/convert-instructions-to-base64`) | ### `output=bytecode` (default) Use when the **signer is an EOA** or you control a contract that batches calls. ```http theme={null} GET /strategies/:id/bytecode?action=lend&amount=...&wallet=0x... ``` Recommended path for EOAs: [EIP-7702 batch deposit](/examples/javascript/using-eip7702). ### `output=userOperation` Use when the **`wallet` param is a smart account** and you submit via EntryPoint / bundler. ```http theme={null} GET /strategies/:id/bytecode?action=lend&amount=...&wallet=0xSafe...&output=userOperation ``` `output=userOperation` targets an **already-deployed** smart account at `wallet`. If the account is not on-chain yet, building the userOp fails with `Could not create a new SmartAccount without an owner`. Provision the account first — see [Smart Account Deposit](/examples/javascript/smart-account-deposit) or your AA provider's deploy flow. Pods does **not** auto-provision a smart account for a plain EOA address. Use `output=bytecode` and batch with EIP-7702, or create the smart account explicitly before requesting `userOperation`. ### `output=fireblocks` Use with Fireblocks custody. Requires `accountId` (vault account ID) and a smart wallet previously provisioned via `POST /fireblocks-smart-account`. ```http theme={null} GET /strategies/:id/bytecode?action=lend&amount=...&wallet=0xSafe...&output=fireblocks&accountId=12 ``` See [Fireblocks Integration](/external-integrations/fireblocks-integration). ## Decision tree ``` What signs the transaction? │ ├─ Plain EOA (browser wallet, server key) │ └─ output=bytecode → EIP-7702 executeBatch (recommended) │ ├─ ERC-4337 smart account (Privy, Dynamic, Notus, …) │ └─ output=userOperation → submit via bundler │ ├─ Fireblocks smart wallet │ └─ output=fireblocks + accountId → Fireblocks Transactions API │ ├─ Solana wallet │ ├─ output=bytecode → sign transactionData.rawTransaction (base64) │ └─ output=instructions → assemble { instructions, lutsByAddress? } │ └─ Custom / iframe widget └─ output=bytecode → your batch executor (must be atomic) ``` ## After execution 1. Save the `id` from the bytecode response for tracking. 2. Poll `GET /actions/{id}`, WebSocket `/updates`, or customer webhooks for `ACTION_UPDATE`. 3. For async strategies (Ondo `request-lend` / `request-withdraw`), also poll position or `GET /strategies/:id/status?wallet=` until pending arrays clear. ## Common mistakes | Mistake | Symptom | Fix | | - | - | - | | `for (tx of bytecode) sendTransaction(tx)` | Funds stuck, `PENDING` forever | Batch into one tx (7702 / userOp / Safe) | | `output=userOperation` with EOA `wallet` | `Could not create a new SmartAccount without an owner` | Use `bytecode` + 7702, or deploy smart account first | | Sell before forward completes (Ondo) | On-chain `transfer amount exceeds balance` | Gate on `suw.phase !== 'awaiting_forward'` and wallet position | | Wrong `chainId` filter | Tx reverts or wrong network | Sign only legs where `Number(chainId) === Number(chainIdIn)` (buy) or BSC (Ondo sell). Leg `chainId` is a decimal string. | ## Next steps Atomic EOA batching with viem ERC-4337 flow with output=userOperation Custody integration with output=fireblocks Cross-chain stocks — high-risk partial-execution flow # Fees Source: https://docs.pods.finance/getting-started/fees Understand the fee structure for swaps and yield strategies on Pods Pods charges transparent fees on swaps and yield strategies. This page covers the platform fees and how you can add your own markup fees as an integrator. ## Platform Fees **25 BPS** (0.25%) applied on every swap transaction, both same-chain and cross-chain. **5% performance fee** charged on exit from yield positions. This fee is only applied to the profits earned, not the principal. ## Fee Fields in API Responses When you query strategies via `GET /strategies` or `GET /v2/strategies/:id`, each strategy object includes fee-related fields: | Field | Type | Description | | - | - | - | | `fee` | string | Entry fee in **basis points** (e.g., `"0"` = 0.00%, `"25"` = 0.25%). Configured per customer. | | `performanceFeeBps` | string | Performance fee in **basis points**, charged on profits at withdrawal (e.g., `"500"` = 5%). | | `markupFeeBps` | string | Your integrator markup in **basis points**, charged on the same profit base as the performance fee. | ## APY Fields Strategy and wallet responses expose several APY figures. Only **`netAPY`** accounts for Pods fees: | Field | Fees deducted? | Meaning | | - | - | - | | `grossAPY` | No | Raw protocol APY, before any Pods fee. | | `netAPY` | **Yes** | `grossAPY × (1 − (performanceFeeBps + markupFeeBps) / 10000)` | | `apy` | No | Mirrors `grossAPY`. Kept for backward compatibility. | | `avgApy` | No | Annualized average since the strategy start date. | | `inceptionApy` | No | Cumulative growth since inception, not annualized. | | `rateHistory[].apy` | No | Daily gross snapshot. Not customer-specific — one value shared across all integrators. | | `rateHistory[].conversionRateApy` | No | Annualized from on-chain conversion-rate movement. | Wallet position responses (`GET /v2/wallets/:address` and `GET /v2/strategies/:id` with `wallet`) also include **realized APY** fields derived from actual on-chain conversion-rate snapshots: | Field | Description | | - | - | | `realizedApy7d` | Strategy-level realized yield over the trailing 7 days, annualized, as a decimal ratio gross of performance fees. Null when rate history does not cover the window. | | `realizedApy30d` | Strategy-level realized yield over the trailing 30 days, annualized, as a decimal ratio gross of performance fees. Null when rate history does not cover the window. | > **Note:** Realized APY is only computed for yield-accumulating strategies. Tokenized equity strategies (e.g. Ondo stock strategies) opt out because their conversion rate is a market price, not a yield accumulator. Read **`netAPY`** when displaying what a user will actually earn. `apy` is the gross rate — if you render it as a net figure you will overstate returns by your combined fee. For example, a strategy earning 10% gross with a 5% performance fee and a 2% markup returns `grossAPY: 0.10` and `netAPY: 0.093`. ### Gross vs. Net APY The `apy` field returned in strategy listings and wallet position responses (`GET /v2/wallets/:address`, `GET /v2/strategies/:strategyId`) is the **gross protocol APY** — before any performance or markup fees are deducted. The net APY (after fees) is computed by the client or displayed in the Pods dashboard. To calculate the net APY yourself: ``` netApy = grossApy × (1 - (performanceFeeBps + markupFeeBps) / 10000) ``` ### Gross vs. Net Profit Wallet-scoped position responses (`GET /v2/wallets/:address`, `GET /v2/strategies/:strategyId?wallet=`) report realized profit both before and after Pods fees, on the `profit` object: | Field | Fees deducted? | Meaning | | - | - | - | | `profit.value` | No | Gross profit on the capital still deployed, in underlying base units. | | `profit.afterFees` | **Yes** | `profit.value × (1 − (performanceFeeBps + markupFeeBps) / 10000)`, truncated. | Read **`profit.afterFees`** when displaying what a user has actually earned. `profit.value` is the gross figure — the same relationship `apy` has to `netAPY`. Two rules the formula alone does not show: * **Fees are charged on profit, never on a loss.** When `profit.value` is negative, `afterFees` equals it exactly. A reported loss is never reduced. * **The fee is truncated, not rounded.** Sub-unit remainders fall in the user's favour. For example, a position 50 USDC in profit with a 10% performance fee and a 10% markup returns `profit.value: "50000000"` and `profit.afterFees: "40000000"`. `afterFees` appears on `profit` only. Every other amount is gross, including `cumulativeProfit`, `principal`, `currentPosition`, **`profitInUSD`**, and the wallet summary's `totalProfitInUSD`. There is no net USD figure — to show profit in USD net of fees, scale `profitInUSD` by `afterFees / value` yourself. ### Swap quotes (`GET /v2/swap/quote`) Every persisted quote includes **`feeBreakdown`** — the canonical source for all quote-time fees (Pods platform fee, customer markup, and bridge/spread impact). See [Get a Quote](/guides/swaps/get-quote) for the response shape and field reference. Both fields are strings representing basis points (1 BPS = 0.01%). These values may vary per customer based on your integrator agreement. ## Fee Stacking Pods fees are charged **on top of** any underlying protocol fees (e.g., Aave's own protocol fees). The total cost to the end user is: > **Protocol fees** + **Pods platform fees** + **Your integrator markup** (if configured) ## Markup Fees As an integrator, you can add your own markup fees on top of the platform fees to monetize your integration. | Fee Type | Description | | - | - | | **Swap Markup** | Additional BPS added on top of the 25 BPS platform fee for swap transactions | | **Yield Performance Markup** | Additional performance fee percentage added on top of the 5% platform fee on yield exits. This markup is tracked separately from the base performance fee at the contract level — the smart contract returns `performanceFee` and `markupFee` as distinct values. | Markup fees are currently configured through the admin panel. A self-service API endpoint for managing markup fees is planned for a future release. Contact us at **[support@pods.finance](mailto:support@pods.finance)** to set up your custom markup fees. ## Next Steps Learn how to integrate token swaps Explore yield strategy integrations # Add Yield to Your App in 5 Minutes Source: https://docs.pods.finance/getting-started/quickstart Integrate DeFi yield strategies into your application in 4 steps This quickstart walks you through integrating a DeFi yield strategy into your application. You'll browse available strategies, generate deposit transaction data, and execute it with your user's wallet. Get your API key at [Pods](https://www.pods.finance/plg/select-plan) Have an Ethereum/Polygon wallet address and sufficient USDC balance Ensure your wallet has sufficient USDC on Polygon and MATIC for gas fees. Create a `.env` file: ```bash theme={null} PODS_API_KEY=your-api-key WALLET_ADDRESS=0x... PRIVATE_KEY=0x... POLYGON_RPC_URL=https://polygon-rpc.com ``` Never commit your `.env` file to version control. Add it to `.gitignore`. ## Step 1: Browse Available Strategies List all available yield strategies to find one for your use case. ```javascript JavaScript theme={null} import axios from 'axios' const walletAddress = process.env.WALLET_ADDRESS // Your wallet address const pods = axios.create({ baseURL: 'https://api.pods.finance', headers: { 'x-api-key': process.env.PODS_API_KEY } }) const { data: strategies } = await pods.get('/strategies') strategies.data.forEach(s => { console.log(`${s.id} — ${s.protocol} on ${s.network} — APY: ${(s.apy * 100).toFixed(2)}%`) }) ``` ```json theme={null} { "data": [ { "id": "Aave-USDC-polygon", "protocol": "Aave", "assetName": "USDC", "network": "polygon", "asset": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "assetDecimals": 6, "apy": 0.0482, "paused": false, "fee": "0", "availableActions": ["lend", "withdraw"] } ], "pagination": { "page": 1, "limit": 100, "total": 5, "totalPages": 1, "hasMore": false } } ``` Use `id` (e.g. `Aave-USDC-polygon`) as the strategy identifier in the next steps. ## Step 2: Generate Deposit Bytecode Request ready-to-execute transaction data for depositing into the strategy. ```javascript JavaScript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '10000000', // 10 USDC (6 decimals) wallet: walletAddress } }) console.log('Transactions to execute:', result.bytecode.length) console.log('ID:', result.id) ``` ```bash cURL theme={null} curl -X GET "https://api.pods.finance/strategies/aave-usdc-polygon/bytecode" \ -H "x-api-key: $PODS_API_KEY" \ -G \ --data-urlencode "action=lend" \ --data-urlencode "amount=10000000" \ --data-urlencode "wallet=$WALLET_ADDRESS" ``` ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "bytecode": [ { "to": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "data": "0x095ea7b3...", "value": "0", "chainId": "137" }, { "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD", "data": "0x617ba037...", "value": "0", "chainId": "137" } ] } ``` The response may contain multiple legs (e.g. token approval + deposit). Execute **all legs on the same `chainId` in one atomic transaction** — not as separate sends. See [Executing Bytecode](/getting-started/executing-bytecode). ## Step 3: Execute Transactions **Do not loop over `bytecode` and call `sendTransaction` for each item.** Batch every leg into a single transaction. For EOAs, use [EIP-7702](/examples/javascript/using-eip7702). For smart accounts, request `output=userOperation`. See [Executing Bytecode](/getting-started/executing-bytecode). **EOA (recommended):** use the [EIP-7702 example](/examples/javascript/using-eip7702) — it maps `result.bytecode` into one `executeBatch` type-4 transaction. **Smart account:** add `output=userOperation` to the bytecode request and submit the returned `userOperation` via your bundler. See [Smart Account Deposit](/examples/javascript/smart-account-deposit). ```javascript JavaScript theme={null} // EOA path — see examples/javascript/strategy-deposit-eip7702.js for the full flow const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '10000000', wallet: walletAddress } }) // Batch result.bytecode into ONE transaction (EIP-7702 executeBatch or your AA batch). // Do not: for (const tx of result.bytecode) { await sendTransaction(tx) } console.log('Legs to batch:', result.bytecode.length) ``` ## Step 4: Check Your Position After depositing, use the wallet overview endpoint to check the current position and profit for your strategy. ```javascript JavaScript theme={null} const { data: wallet } = await pods.get(`/v2/wallets/${walletAddress}`, { params: { include: 'earn', includeHistory: false, withActions: false } }) wallet.earn.positions.forEach(p => { console.log(`Strategy: ${p.strategy.protocol} ${p.strategy.assetName} on ${p.strategy.network}`) console.log(` Current Position: ${p.spotPosition.currentPosition.humanized} ${p.spotPosition.currentPosition.symbol}`) console.log(` Principal: ${p.spotPosition.principal.humanized} ${p.spotPosition.principal.symbol}`) console.log(` Profit: ${p.spotPosition.profit.humanized} ${p.spotPosition.profit.symbol}`) console.log(` APY (gross): ${(p.spotPosition.apy * 100).toFixed(2)}%`) // gross protocol APY, before performance/markup fees }) console.log(`Total Balance (USD): $${wallet.earn.summary.totalUnderlyingBalanceUSD}`) ``` ```bash cURL theme={null} curl -X GET "https://api.pods.finance/v2/wallets/$WALLET_ADDRESS?include=earn&includeHistory=false&withActions=false" \ -H "x-api-key: $PODS_API_KEY" ``` ```jsonc theme={null} { "earn": { "positions": [{ "spotPosition": { "currentPosition": { "value": "10048700", "decimals": 6, "humanized": "10.0487", "symbol": "USDC" }, "profit": { "value": "48700", // gross profit — before Pods fees "afterFees": "43830", // net of performance + markup fees — display this one "decimals": 6, "humanized": "0.0487", "symbol": "USDC" }, "principal": { "value": "10000000", "decimals": 6, "humanized": "10.00", "symbol": "USDC" }, "underlyingBalanceUSD": 10.05, "apy": 0.0482 // gross APY — before performance and markup fees }, "strategy": { "id": "Aave-USDC-polygon", "assetName": "USDC", "protocol": "Aave", "network": "polygon", "asset": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "assetDecimals": 6 } }], "summary": { "totalProfitInUSD": 0.05, "totalUnderlyingBalanceUSD": 10.05 } } } ``` This slim request keeps current position, principal, and profit while omitting raw actions and history from the response. Call it periodically to track how your user's position grows over time. ## Next Steps Atomic batching and output format for your wallet stack Verify the deposit and monitor your user's yield position Generate withdrawal transaction data Browse strategies, deposit, check positions, and withdraw Also integrate token swaps into your app # Why Pods Source: https://docs.pods.finance/getting-started/why-pods Ship on-chain yield and swaps in weeks, not months **Ship on-chain yield and swaps in weeks, not months** Pods is a B2B infrastructure API that enables fintechs, neobanks, wallets, custodians, and exchanges to integrate DeFi yield strategies and cross-chain swaps without building or maintaining an internal blockchain engineering team. Launch DeFi products in weeks instead of months of in-house development Get treated, individualized position data across all protocols via one API Your users keep custody — Pods never touches private keys or funds ## By the Numbers DeFi protocols integrated Blockchain networks supported API calls to deploy yield Typical integration time **Your integration:** `GET /strategies` → `GET /strategies/:id/bytecode` → user signs. Three API calls to go from zero to deployed yield. **Direct protocol integration:** Research protocol docs → write Solidity adapters → deploy contracts → build indexer → handle chain reorgs → monitor. Months of work per protocol. ## Who Is Pods For? Pods serves **B2B clients** building crypto financial products: * **Fintechs & Neobanks** — Offer yield products to end users without building DeFi infrastructure * **Wallets** — Add earn features and cross-chain swaps directly in your wallet interface * **Custodians** — Integrate institutional-grade yield strategies for clients * **Exchanges** — Provide seamless on-chain yield and swap products * **Investment Platforms** — Deploy client funds into curated DeFi strategies If you're evaluating whether to build DeFi integrations in-house or use Pods, this page is for you. ## What Pods Abstracts Building DeFi integrations in-house requires far more than calling a few smart contract functions. Pods handles the entire complexity stack: * Protocol-specific adapters for Aave, Morpho, Lido, Compound, 1inch, Jupiter, and more * Transaction bytecode generation for deposits, withdrawals, and swaps * Multi-step transaction orchestration (approve → deposit → track) * Gas estimation and optimization * Transaction simulation and validation * Real-time indexing of positions across multiple chains * Chain reorganization detection and reconciliation * Historical transaction data and realized PnL tracking * Accounting-ready data exports * Multi-protocol position aggregation * Protocol upgrades and versioned migrations * New protocol integrations (without requiring client updates) * Security audits and monitoring * Oracle validation for price feeds * Protocol-specific quirks and edge cases * RPC node management with automatic failover * Job queues and background workers * Alerting, monitoring, and incident response * Rate limiting and request management * Webhook delivery and retry logic **Without Pods:** You need a dedicated blockchain engineering team, ongoing protocol maintenance, and infrastructure ops. **With Pods:** You call a few HTTP endpoints and focus on your product. ## The Integration Experience Integrating DeFi yield with Pods follows a simple 3-step pattern: ### 1. Choose a Strategy Fetch available yield strategies with current APY rates: ```javascript theme={null} const { data: strategies } = await pods.get('/strategies') strategies.data.forEach(s => { console.log(`${s.id} — ${(s.apy * 100).toFixed(2)}% APY on ${s.network}`) }) ``` ### 2. Generate Bytecode Request ready-to-execute transaction data: ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '10000000', // 10 USDC wallet: userWalletAddress } }) ``` ### 3. User Signs & Broadcasts Your user signs **one atomic transaction** bundling all bytecode legs — **you keep custody**: ```javascript theme={null} // Batch result.bytecode into ONE tx — see /getting-started/executing-bytecode // EOA: EIP-7702 executeBatch | Smart account: output=userOperation | Fireblocks: output=fireblocks console.log('Legs to batch:', result.bytecode.length) ``` See [Executing Bytecode](/getting-started/executing-bytecode) and [EIP-7702](/examples/javascript/using-eip7702). **That's it.** Funds are deployed, accruing yield, and tracked automatically. Build your first integration in 5 minutes Full end-to-end code example ## Non-Custodial + No Balance-Sheet Risk Pods is **strictly infrastructure** — not a financial product. **Pods never:** * Holds your users' private keys * Takes custody of funds * Passes funds through our smart contracts * Creates balance-sheet risk for your business **Your users:** * Sign transactions with their own wallets * Maintain full custody at all times * Interact directly with underlying DeFi protocols Transaction signing and propagation happen entirely on your side. Pods provides the data and transaction bytecode — you execute it. ## Why Not Integrate Protocols Directly? Many teams start by building direct integrations with protocols like Aave or Morpho. Here's why that becomes complex fast: ### Direct Protocol Integration **Requirements:** * Solidity expertise to understand contract interfaces * Protocol-specific adapters for each integration (Aave, Morpho, Lido, Compound, etc.) * Indexing infrastructure to track user positions * Chain reorg handling and reconciliation * Monitoring, alerting, and incident response * Ongoing maintenance for protocol upgrades * Security audits for every new integration **Time to market:** 3-6 months per protocol **Team required:** 2-3 blockchain engineers + 1 DevOps **Ongoing cost:** High (maintenance, monitoring, upgrades) ### Pods Integration **Requirements:** * API key * Basic HTTP client (axios, fetch, requests) * Wallet library for transaction signing (ethers.js, viem) **Time to market:** 1-2 weeks for full integration **Team required:** Any backend engineer **Ongoing cost:** Low (pay-per-use API fees) ### Side-by-Side Comparison | | Direct Integration | Pods | | - | - | - | | **API calls to deposit** | 15+ (approve, encode, simulate, send, index, reconcile per protocol) | 3 (`GET /strategies` → `GET /:id/bytecode` → user signs) | | **Protocols supported** | 1 per integration | 13+ via one API | | **Chains supported** | 1 per RPC setup | 8 networks | | **Team required** | 2-3 blockchain engineers + DevOps | Any backend engineer | | **Time to first deposit** | 3-6 months | 1-2 weeks | | **New protocol added** | Months of development | Available instantly, no code changes | | **Position tracking** | Build custom indexer | Built-in (`GET /v2/wallets/:address`) | Pods provides a **normalized API surface** across all protocols. Add a new protocol without changing your codebase — we handle the complexity. ## Position Tracking & Data Beyond transaction generation, Pods provides real-time position monitoring and accounting-ready data: **Available via API:** * Current position balances across all protocols * Realized and unrealized PnL * Yield earned over time (with APY breakdowns) * Historical transaction data * Aggregated portfolio value across chains * Webhook notifications for status changes **Example:** ```javascript theme={null} const { data: walletOverview } = await pods.get(`/v2/wallets/${userAddress}`, { params: { include: 'earn' } }) console.log('Total Portfolio Value:', walletOverview.earn.summary.totalUnderlyingBalanceUSD) walletOverview.earn.positions.forEach(p => { console.log(`${p.strategy.id}: $${p.spotPosition.underlyingBalanceUSD}`) console.log(` Current APY: ${(p.spotPosition.apy * 100).toFixed(2)}%`) }) ``` No need to index events, handle chain reorgs, or reconcile balances — Pods provides normalized, treated data. ## FAQ **No.** Pods is non-custodial infrastructure. Your users sign transactions with their own wallets and maintain full custody. Pods never holds private keys or funds. **No.** Pods abstracts all smart contract interactions. Any backend engineer familiar with REST APIs can integrate Pods — no blockchain expertise required. User funds remain safe — they're deposited directly into underlying protocols (Aave, Morpho, etc.) and are never held by Pods. Your users can always withdraw directly via protocol UIs. Check our live uptime: [Pods API Status](https://pods.betteruptime.com/) **Yes.** Pods continuously adds new protocol integrations. When we add a new protocol, it becomes immediately available via the API — no client-side changes required. **Yes.** Pods provides testnet endpoints for development and testing. Contact support to enable testnet access for your API key. Pods monitors protocol upgrades and handles migrations automatically. Versioned adapters ensure backward compatibility. You'll be notified of any breaking changes well in advance. ## Next Steps Add yield to your app in 5 minutes Understand how Pods works Explore all available endpoints Sign up and start building **Questions?** Reach out to our team at [support@pods.finance](mailto:support@pods.finance) # External Evidence KYC Source: https://docs.pods.finance/guides/kyc/external-evidence Route contract for onboarding applicants you captured yourself Use this flow when your application already performs identity capture and only wants Pods to handle the ramp provider side and expose one normalized status for money movement. You keep your own liveness and documentoscopy providers. You send Pods the evidence you already hold; Pods creates the ramp subaccount, uploads the images, submits the KYC, and tracks it to a verdict. **Pods stores no images.** They are held in memory for the duration of your request, forwarded to the ramp provider, and discarded. This holds for both transports: an image sent as base64 and an image Pods fetched from a URL you provided follow the same path — into memory, to the provider, gone. Pods retains the ramp provider's document ids, a SHA-256 digest, the liveness score, and timestamps. The rest of the submitted payload is kept for **up to 7 days** so a failure on the provider's side can be retried without asking the applicant to capture again. **On the `url` transport that includes the URL itself, signature and all** — that is what makes the retry able to re-fetch the bytes rather than just name them. Size `X-Amz-Expires` knowing Pods holds the link for that window. See [Retention](#retention). ## What you need before you start | | | | - | - | | API key | Server-to-server, sent as `x-api-key`. Never expose it in a browser. | | Liveness | A liveness check you run yourself, producing a reference image and a result. | | Documentoscopy | Front and back images of the identity document, plus the data read from it. | | Address | Collected from the user. Brazil only for now. | | Wallet address | The wallet the user's funds will move from. Required at session creation. More can be linked later — see [Linking more wallets](#linking-more-wallets). | ## The flow Send CPF, email and wallet address. You get back a `kycUserId` — use it for every later call and for status polling. Liveness and document capture happen entirely on your side, with your providers. One call with everything: applicant data, address, the liveness image plus its attestation, and the document images plus the data extracted from them. The ramp provider does not decide synchronously. Poll the status endpoint until the session reaches a terminal state. ## 1. Create the session ```bash theme={null} curl -X POST https://api.pods.finance/api/v1/kyc/sessions \ -H "x-api-key: $PODS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "cpf": "12345678909", "email": "maria@example.com", "walletAddress": "0x6efa29a060C784075188C43dB94cF203f0D54611" }' ``` ```json theme={null} { "kycUserId": "3f2b1c4a-…", "status": "created", "existing": false } ``` This call is safe to repeat. A CPF that already has a session under your account returns the existing `kycUserId` rather than creating a second one, with `"existing": true`. If that CPF is already bound to a different email you get `409 KYC_PROFILE_EMAIL_MISMATCH` — worth surfacing rather than working around, since one person under two records is almost always a mistake. ## 2. Submit the evidence One call. Every image is either inline base64 — as most capture providers return it — or an HTTPS URL that Pods fetches for you. Pick per image; see [Image requirements](#image-requirements). ```bash theme={null} curl -X POST https://api.pods.finance/api/v1/kyc/sessions/3f2b1c4a-…/submit \ -H "x-api-key: $PODS_API_KEY" \ -H "Content-Type: application/json" \ -d @evidence.json ``` ```json theme={null} { "applicant": { "fullName": "Maria Silva", "dateOfBirth": "1990-05-14", "phone": "+5511999999999" }, "address": { "country": "BRA", "state": "SP", "city": "São Paulo", "zipCode": "01310100", "streetAddress": "Av. Paulista", "number": "1000", "complement": "apto 51" }, "liveness": { "image": { "base64": "/9j/4AAQSkZJRg…", "mimeType": "image/jpeg" }, "attestation": { "provider": "aws_rekognition_face_liveness", "status": "SUCCEEDED", "confidence": 95.78811645507812, "sessionId": "47fa2a26-bb7a-4ca0-85f1-129ad6a6eb0b", "challenge": { "type": "FaceMovementAndLightChallenge", "version": "2.0.0" }, "completedAt": "2026-08-13T14:58:10Z" } }, "documents": { "documentType": "RG", "front": { "base64": "…", "mimeType": "image/jpeg" }, "back": { "base64": "…", "mimeType": "image/jpeg" }, "attestation": { "provider": "bigdatacorp_documentoscopy", "onboardingId": "b7d2…", "extracted": { "fullName": "MARIA SILVA", "birthDate": "1990-05-14", "cpf": "12345678909", "documentType": "RG" } } } } ``` ```json theme={null} { "applicant": { "fullName": "Maria Silva", "dateOfBirth": "1990-05-14", "phone": "+5511999999999" }, "address": { "country": "BRA", "state": "SP", "city": "São Paulo", "zipCode": "01310100", "streetAddress": "Av. Paulista", "number": "1000", "complement": "apto 51" }, "liveness": { "image": { "url": "https://s3.us-east-1.amazonaws.com/your-bucket/liveness/abc123/reference.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=…" }, "attestation": { "provider": "aws_rekognition_face_liveness", "status": "SUCCEEDED", "confidence": 95.78811645507812, "sessionId": "47fa2a26-bb7a-4ca0-85f1-129ad6a6eb0b", "challenge": { "type": "FaceMovementAndLightChallenge", "version": "2.0.0" }, "completedAt": "2026-08-13T14:58:10Z" } }, "documents": { "documentType": "RG", "front": { "url": "https://s3.us-east-1.amazonaws.com/your-bucket/documents/abc123?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=…" }, "back": { "url": "https://s3.us-east-1.amazonaws.com/your-bucket/documents/def456?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=…" }, "attestation": { "provider": "bigdatacorp_documentoscopy", "onboardingId": "b7d2…", "extracted": { "fullName": "MARIA SILVA", "birthDate": "1990-05-14", "cpf": "12345678909", "documentType": "RG" } } } } ``` ```json theme={null} { "kycUserId": "3f2b1c4a-…", "status": "provider_pending", "timings": { "validate": 8, "upload": 22182, "level1": 376, "totalMs": 22578 } } ``` **A failed submit does not mean a failed KYC.** This call does real work before responding — it creates the ramp subaccount and uploads all three images — and takes roughly **20 seconds**, almost entirely image upload. The platform edge cuts the connection at **30 seconds** and answers with its own HTML error page while the submission carries on and succeeds server-side, so a timeout, a `503` or a non-JSON body tells you nothing about the outcome. On any error or timeout, poll [`/status`](#3-poll-the-status) before concluding anything. Never re-`POST /submit` to "make sure" — while the session is `provider_pending` it answers with that state rather than doing the work twice, so it cannot help you and it cannot tell you more than `/status` already does. Resubmitting is only meaningful once `/status` reports `rejected_retryable`; see [Handling rejections](#handling-rejections). `timings` reports each phase in milliseconds and is there to make a slow submission diagnosable without a support ticket. It is informational; do not branch on it. ### Rejections arrive here too The ramp provider can refuse the submission during this call. When it does, `/submit` answers **`400`** with an error body, and the session moves to `rejected` or `rejected_retryable`: ```json theme={null} { "error": { "code": "AVENIA_KYC_LEVEL_1_API_FAILED", "message": "cannot repeat taxId across multiple users" } } ``` Do not read that `message`. Call the status endpoint and read the normalized `reason` instead — see [Handling rejections](#handling-rejections). The provider returns free-form text that changes without notice; `reason.code` does not. ### About the liveness attestation The `attestation` is your assertion about the check you ran. Pods records it and compares `confidence` against the threshold configured for your account. Pods does **not** call your provider to verify it — the record is stored as `customer_attested`, which is the honest description of the arrangement. Only two fields are load-bearing: `status` must be `SUCCEEDED` or `PASSED`, and `confidence` must clear your threshold. Everything else is recorded for audit correlation and is not validated. A `confidence` below the threshold is rejected with `422 LIVENESS_BELOW_THRESHOLD` before any upload happens, so you save the round trip and the provider call. Amazon Rekognition returns the reference image as **bytes in the API response** — `ReferenceImage.Bytes` on `GetFaceLivenessSessionResults` — not as a durable URL. So the transport you use depends on whether capture and submission are the same process. `AuditImages` carries the other frames in the same shape; `ReferenceImage` is the one to submit. **A — no storage needed.** Read the session result and submit in one execution. The result is readable only for a short window after the check, so fetch and submit together; there is nothing to persist and no bucket to allowlist. ```js theme={null} import { RekognitionClient, GetFaceLivenessSessionResultsCommand } from '@aws-sdk/client-rekognition' const rekognition = new RekognitionClient({ region: 'us-east-1' }) const result = await rekognition.send( new GetFaceLivenessSessionResultsCommand({ SessionId: sessionId }) ) if (result.Status !== 'SUCCEEDED' || !result.ReferenceImage?.Bytes) { throw new Error(`liveness not usable: ${result.Status}`) } await fetch(`https://api.pods.finance/api/v1/kyc/sessions/${kycUserId}/submit`, { method: 'POST', headers: { 'x-api-key': process.env.PODS_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ applicant, address, documents, liveness: { image: { base64: Buffer.from(result.ReferenceImage.Bytes).toString('base64') }, attestation: { provider: 'aws_rekognition_face_liveness', status: result.Status, confidence: result.Confidence, sessionId: result.SessionId, challenge: result.Challenge && { type: result.Challenge.Type, version: result.Challenge.Version } } } }) }) ``` **B — bucket and pre-sign.** Same bytes, stored first and submitted later. This is the right shape when capture and submission are separate processes — a mobile capture that hands off to a backend job, or a retry that must not depend on the Rekognition session still being readable. ```js theme={null} import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' const s3 = new S3Client({ region: 'us-east-1' }) const Bucket = 'your-bucket' const Key = `liveness/${result.SessionId}/reference.jpg` await s3.send(new PutObjectCommand({ Bucket, Key, Body: Buffer.from(result.ReferenceImage.Bytes), ContentType: 'image/jpeg' })) // Size the signature to outlive the submit — the call itself takes ~20 seconds. const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key }), { expiresIn: 3600 }) // Then send liveness.image as { url } instead of { base64 }. ``` Do not bother setting `response-content-type` on the signed URL to match the object: Pods reads the format from the bytes and ignores the header either way. The AWS calls above are illustrative. The field names were read from the published `@aws-sdk/client-rekognition` type declarations. ### About the extracted document data `documents.attestation.extracted` is the source of truth for the name, birth date and CPF sent to the ramp provider — because that is what the provider compares the images against. Data typed by the user tends to produce name and birth-date mismatches downstream. `applicant.fullName` and `applicant.dateOfBirth` are optional and only fill gaps. One rule is absolute: if `extracted.cpf` differs from the CPF the session was created with, the request is refused with `409 IDENTITY_CPF_MISMATCH`. Nothing is uploaded. ### Image requirements | | | | - | - | | Transport | Exactly one of `base64` or `url` per image. Sending both, or neither, is a `422`. Images in the same request may use different transports — a client running AWS Face Liveness gets the selfie as bytes from the API and pulls the documents from its own bucket, and that mix is a normal submission, not a workaround. | | Encoding (`base64`) | Base64. A `data:` URI prefix is accepted and stripped. | | Scheme (`url`) | HTTPS only. `http://` is refused without a request being made. | | Allowed hosts (`url`) | Configured per environment, as an exact host list. Ask Pods to allowlist your bucket host if your bucket is not public. | | Signature lifetime (`url`) | The pre-signed signature must still be valid when Pods fetches, which is *during* the submit — not when you built the payload. | | Format | JPEG or PNG. Detected from the bytes, not from `mimeType` and not from the response's `Content-Type`. | | Size | 8 MB maximum, decoded — or fetched. The transfer is aborted at the limit rather than buffered. | | Back side | Required for `RG` and `CNH`. Must be omitted for `PASSPORT`. Either transport satisfies it. | ## 3. Poll the status ```bash theme={null} curl "https://api.pods.finance/api/v1/kyc/status?kycUserId=3f2b1c4a-…" \ -H "x-api-key: $PODS_API_KEY" ``` ```json theme={null} { "kycUserId": "3f2b1c4a-…", "provider": "external_evidence", "status": "provider_pending", "brlaEnabled": false, "reason": null, "documentType": "RG", "documentEvidenceSource": "customer_attested", "livenessEvidenceSource": "customer_attested", "updatedAt": "2026-08-13T15:01:22Z" } ``` Poll every 10 seconds until `status` is terminal. | Status | Meaning | | - | - | | `created` | Session exists, no evidence submitted yet. | | `provider_pending` | Submitted; awaiting the ramp provider's decision. | | `approved` | Verified. `brlaEnabled` is now true and the user can move money. | | `rejected` | Terminal. Do not resubmit the same evidence. | | `rejected_retryable` | Failed, but a new attempt may succeed. See `reason.retryFrom`. | The response carries the full profile; the fields above are the ones this flow adds. The rest is shared with the other Pods KYC flows and documented in the API reference. ## Handling rejections When a session is rejected, `reason` is populated: ```json theme={null} { "kycUserId": "3f2b1c4a-…", "status": "rejected_retryable", "brlaEnabled": false, "reason": { "code": "LIVENESS_FACE_MISMATCH", "message": "The selfie does not match the submitted document.", "retryable": true, "retryFrom": "liveness" } } ``` These are Pods codes, not the upstream provider's. They stay stable if Pods changes ramp provider, so you can program against them safely — which `rejectReason`, `providerResult` and `providerMessage` on the same response cannot promise, since they carry raw provider values. | Code | Retryable | Redo | What happened | | - | - | - | - | | `LIVENESS_FACE_MISMATCH` | yes | `liveness` | The selfie and the document are not the same person. | | `LIVENESS_QUALITY_INSUFFICIENT` | yes | `liveness` | The face in the selfie was not clear enough. | | `LIVENESS_FRAUD_SUSPECTED` | **no** | — | The liveness evidence was flagged as fraudulent. | | `IMAGE_QUALITY_INSUFFICIENT` | yes | `documents` | Blurry, too dark, too bright, cropped. | | `DOCUMENT_INVALID` | yes | `documents` | Not a valid document, or irregularities detected. | | `IDENTITY_DATA_MISMATCH` | yes | `applicant` | Name or birth date do not match official records. | | `IDENTITY_CPF_MISMATCH` | **no** | — | The document belongs to a different CPF than the session. | | `TAX_ID_INVALID` | **no** | — | CPF not found or irregular at Receita Federal. | | `TAX_ID_ALREADY_VERIFIED` | **no** | — | This person is already verified under another record. | | `IDENTITY_DECEASED` | **no** | — | Death record found. | | `COMPLIANCE_BLOCKED` | **no** | — | Sanctions, PEP, adverse media or similar. | | `UNDER_18` | **no** | — | The applicant is a minor. | | `PROVIDER_ERROR` | yes | — | The provider could not complete the request. Retry shortly. | To retry, call `/submit` again on the **same** `kycUserId` with corrected evidence for the block named in `retryFrom`. Do not create a new session — deduplication is per CPF, so it will return the same one anyway. A retry is a full new attempt: Pods re-uploads all three images and asks the ramp provider for a fresh verdict, reusing the record it already created for this person rather than opening a second one. The session returns to `provider_pending` and you poll it exactly as you did the first time, so expect the same \~20 seconds and the same `timings`. Only `rejected_retryable` accepts a retry — `rejected`, `blocked` and `approved` answer with the state they are already in, and so does a session still in `provider_pending`. `TAX_ID_ALREADY_VERIFIED` means the person exists at the ramp provider under a different record. A tax ID can only be verified once across the whole platform, so this is terminal for a new record; reach out to Pods to have the existing identity attached to your account. ## Rejections before anything is uploaded These are validation failures, returned in under a second. They never reach the ramp provider and never create a subaccount. | Status | Code | | | - | - | - | | `422` | `LIVENESS_NOT_SUCCEEDED` | The attestation status is not a pass. | | `422` | `LIVENESS_CONFIDENCE_MISSING` | A threshold is configured but no score was sent. | | `422` | `LIVENESS_BELOW_THRESHOLD` | The score is below your configured minimum. | | `422` | `INVALID_EVIDENCE_IMAGE` | The image is unusable: not valid base64, or over 8 MB. | | `422` | `UNSUPPORTED_EVIDENCE_IMAGE_FORMAT` | The bytes are neither JPEG nor PNG. | | `422` | `EVIDENCE_URL_NOT_ALLOWED` | Not HTTPS, the host is not allowlisted, or it resolves to a private address. | | `422` | `EVIDENCE_URL_EXPIRED` | The pre-signed signature had already expired. Re-sign and submit again. | | `422` | `EVIDENCE_URL_FORBIDDEN` | The storage provider refused the request (`403`). | | `422` | `EVIDENCE_URL_NOT_FOUND` | The object does not exist (`404`). | | `422` | `EVIDENCE_URL_UNREACHABLE` | The storage provider could not be reached. | | `422` | `EVIDENCE_URL_TIMEOUT` | Fetching the images exceeded 8 seconds. | | `422` | `DOCUMENT_BACK_REQUIRED` | `RG` and `CNH` need a back image. | | `422` | `DOCUMENT_BACK_NOT_ALLOWED` | A passport is single sided. | | `422` | `INVALID_BIRTH_DATE` | The extracted birth date could not be parsed. | | `409` | `IDENTITY_CPF_MISMATCH` | The document CPF is not the session CPF. | | `409` | `UNDER_18` | The applicant is a minor. | | `409` | `INVALID_KYC_PROVIDER` | This session was not opened for external evidence. | ## Linking more wallets A person is verified once, but they may move money from more than one wallet. The status response carries two wallet fields: `walletAddress` is the primary wallet, the one sent at session creation; `walletAddresses` lists every wallet allowed to quote and ticket against this `kycUserId`, the primary included. To add a wallet, link it to the existing verification instead of creating a second KYC profile: ```bash theme={null} curl -X POST https://api.pods.finance/api/v1/kyc/3f2b1c4a-…/wallets \ -H "x-api-key: $PODS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "walletAddress": "0x0000000000000000000000000000000000000002" }' ``` ```json theme={null} { "kycUserId": "3f2b1c4a-…", "walletAddress": "0x6efa29a060C784075188C43dB94cF203f0D54611", "walletAddresses": [ "0x6efa29a060C784075188C43dB94cF203f0D54611", "0x0000000000000000000000000000000000000002" ] } ``` Linking does not depend on the verdict — you can link before or after the session is `approved`. Linking a wallet that is already on this `kycUserId` changes nothing and returns the same list. To unlink, send a `DELETE` with the wallet in the path. The response has the same shape: ```bash theme={null} curl -X DELETE https://api.pods.finance/api/v1/kyc/3f2b1c4a-…/wallets/0x0000000000000000000000000000000000000002 \ -H "x-api-key: $PODS_API_KEY" ``` The unlinked wallet stops being able to quote or ticket against this `kycUserId` immediately. If you unlink the primary, the first remaining wallet becomes the new `walletAddress`. | Status | Code | | | - | - | - | | `409` | `KYC_WALLET_ALREADY_LINKED` | The wallet belongs to another `kycUserId` under your account. A wallet can belong to only one. | | `409` | `KYC_LAST_WALLET` | The last wallet cannot be removed. Link the replacement first, then unlink. | | `404` | `KYC_WALLET_NOT_LINKED` | The wallet is not linked to this `kycUserId`. | | `404` | `KYC_PROFILE_NOT_FOUND` | No session with this `kycUserId` exists under your account. | **Repeating session creation does not add a wallet.** For a CPF that already has a session, `POST /sessions` returns the existing `kycUserId` and ignores the `walletAddress` you sent, so nothing is linked. Use `/wallets` instead. Session creation with a wallet that already belongs to another `kycUserId` is refused with the same `409 KYC_WALLET_ALREADY_LINKED`. ## Idempotency There is no idempotency header. Deduplication is structural: one session per CPF per account. A repeated `POST /sessions` returns the existing session. A repeated `/submit` while an upload is in flight, or after one completed, returns the current state instead of uploading the documents again. ## Retention | | Kept | | - | - | | Document and selfie images | **Never.** In memory for the duration of your request, then discarded. Only a SHA-256 digest and the provider's document ids survive. | | The rest of the submitted payload — address, applicant data, both attestations, the extracted document data including the CPF | **Up to 7 days**, then deleted automatically | | Session record: CPF hash, last four digits, status, provider ids | For the life of the verification | The 7-day window exists for one reason: when the ramp provider fails on their side, Pods can retry the submission instead of sending your applicant back through capture. Expiry is automatic and needs no call from you. Expiry is a floor, not an exact instant — records are swept periodically, so a payload may live slightly past the 7-day mark. Nothing in the retry buffer is ever returned by an endpoint. ## What Pods does not do * Pods does not call your liveness or documentoscopy provider. * Pods does not verify your attestation against your provider. * Pods does not store images, ever. * Pods does not keep the address or the full CPF beyond the retention window above. * Pods does not return the liveness score. Only pass or fail — showing a biometric score to an end user is discouraged by the providers themselves. # Overview Source: https://docs.pods.finance/guides/kyc/overview How Pods KYC works end to end: providers and flows, onboarding endpoints, one status model, and retention Pods KYC normalizes several identity and verification flows behind a single status model so your application can enable fiat-to-crypto money movement without integrating each local KYC/AML provider yourself. You pick the flow that fits your users, onboard them, and poll one status endpoint until a capability is unlocked. Whatever flow you use, approval is asynchronous and every profile is customer-scoped. Poll `GET /api/v1/kyc/status` until the profile reaches a terminal state and the capability flag you need (`brlaEnabled` for Brazil, `usdEnabled` for foreign USD) is `true`. ## Introduction | Item | Value | | - | - | | **Flows / providers** | Sumsub reusable KYC (Brazil Pix), external evidence (bring-your-own capture), foreign USD (Sumsub → Avenia), age verification (BigID hosted iframe) | | **Onboarding endpoints** | `POST /api/v1/kyc/sumsub-share-token` · `POST /api/v1/kyc/foreign/sumsub-share-token` · `POST /api/v1/kyc/sessions` + `/submit` · `POST /api/v1/kyc/sessions/:id/documents` (UnblockPay Path B) · `POST /api/v1/kyc/currency-unlock` | | **Status endpoint** | `GET /api/v1/kyc/status?kycUserId=…` | | **Terminal statuses** | `approved`, `rejected`, `rejected_retryable`, `blocked` | | **Capability flags** | `brlaEnabled` (Brazil), `usdEnabled` (foreign USD), `eurEnabled` (planned) | | **Retention** | Document/selfie images are never stored; the rest of an external-evidence payload is kept up to 7 days for retry | | **Auth** | Header `x-api-key: ` | ## Authentication Every request requires: ``` x-api-key: ``` * The key identifies your **customer**. Fee configuration and strategy catalog are scoped to it. * Success responses are the bare JSON body (HTTP 200). Errors use `{ "error": { "code", "message", "details?" } }`. * Keep the API key on your **backend** — never expose it in a browser app. Don't have a key yet? [Get an API key](https://www.pods.finance/plg/select-plan). ## Integration flow Pick the flow that matches your users: bring-your-own capture (external evidence), foreign USD onboarding, or Brazil Pix Sumsub reusable KYC. Create a session and submit the evidence you captured, or submit a Sumsub reusable share token — depending on the flow. Each call returns a `kycUserId` — store it for status polling. **External evidence** ```http theme={null} POST /api/v1/kyc/sessions # then POST /submit ``` Full guide: [External evidence KYC](/guides/kyc/external-evidence) **Brazil Pix reusable KYC** ```http theme={null} POST /api/v1/kyc/sumsub-share-token ``` Full guide: [Set up Pods KYC](/guides/kyc/setup) **Foreign USD** ```http theme={null} POST /api/v1/kyc/foreign/sumsub-share-token ``` Full guide: [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp) Verification is asynchronous. Poll until the profile reaches a terminal state: ```http theme={null} GET /api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000 ``` Full guide: [Track Pods Ramp status](/guides/ramp/status). Level 1 approval does not enable USD ramps by itself. Request the currency unlock and poll status until `usdEnabled` is `true`: ```http theme={null} POST /api/v1/kyc/currency-unlock ``` Full guide: [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp). Once the capability flag is `true`, use the same Swap v2 quote routes to onramp, offramp, or ramp to yield: ```http theme={null} GET /v2/swap/quote ``` Full guide: [Move money with Pods Ramp](/guides/ramp/quotes). # Set Up Pods KYC Source: https://docs.pods.finance/guides/kyc/setup Configure Sumsub reusable KYC Use Pods KYC when your application owns the user experience, but wants Pods to handle local KYC/AML provider integration and expose one normalized approval status for fiat-to-crypto money movement. Pods KYC uses Sumsub reusable KYC: you own a Sumsub account and generate reusable share tokens. Pods receives a tax id, a short-lived Sumsub share token, the Sumsub applicant id, email, and wallet address. Pods creates or reuses an internal subaccount with the local KYC/AML provider, tracks the attempt, and exposes one normalized status back to you. Pods does not store Sumsub share tokens, full tax ids, raw document images, base64 payloads, or address data. Pods stores only a salted tax-id hash and its last four digits, plus the status fields and provider ids needed for audit and retry. ## Supported countries Pods KYC is live today for Brazil and expanding to more countries in Latin America. | Country | Status | On-ramp methods | Off-ramp methods | | - | - | - | - | | Brazil (BRL) | Live | Pix | Pix | | Argentina (ARS) | Live | Bank transfer (CVU), Mercado Pago | Bank transfer (CBU / CVU / alias) | | Mexico (MXN) | Live | Bank transfer (CLABE / SPEI) | Bank transfer (CLABE) | | Colombia (COP) | Live | Bank transfer (PSE), BRE-B, Request-to-Pay (Bancolombia, Nequi) | Bank transfer (PSE), BRE-B | Each country integrates with its own local KYC/AML provider behind the scenes (Avenia for Brazil) and its own Sumsub reusable-identity partner. Confirm with Pods which partner and required verification fields apply to your target country before configuring the Sumsub level below. ## KYC requirements Every Pods KYC identity flow verifies the same four things before a user can be approved. Liveness, document, and address are enforced entirely inside Sumsub as part of the level configured in [step 1](#1-configure-the-sumsub-level); the tax id is the only one of the four that your backend also sends directly to Pods. | Requirement | Enforced by | Sent to Pods | | - | - | - | | Tax id (CPF for Brazil) | Sumsub applicant data (`TIN` field) | Yes -- as `cpf` on `POST /api/v1/kyc/sumsub-share-token` (see [step 6](#6-submit-the-share-token-to-pods)) | | Liveness check | Sumsub level's `Selfie` step, advanced liveness | No -- folds into Sumsub's `reviewAnswer` | | Document check | Sumsub level's `Identity document` step (ID card, passport, driver's license, or residence permit) | No -- folds into Sumsub's `reviewAnswer` | | Home address | Sumsub level's `Applicant data` step (street, street line 2, country, city, state, building number, postcode) | No -- held by Sumsub and the local KYC/AML provider, never received by Pods | Pods only receives the tax id, the share token, the Sumsub applicant id, email, and wallet address when you call `POST /api/v1/kyc/sumsub-share-token`. Liveness, document, and address data stay inside Sumsub and the local provider -- your backend must confirm Sumsub reached `reviewStatus: completed` and `reviewResult.reviewAnswer: GREEN` (all four checks passed) before generating the share token and submitting it to Pods. ## Before you start You need to understand these integration values: | Value | Source | Used for | | - | - | - | | `PODS_API_BASE_URL` | Pods | Base URL for Pods API requests (`https://api.pods.finance`). | | `PODS_API_KEY` | Pods | Server-to-server authentication from your backend. [Get an API key](https://www.pods.finance/plg/select-plan). | | `PROVIDER_PARTNER_TOKEN` | Pods | Paste once in Sumsub Dashboard to add the local KYC/AML provider as a reusable identity recipient. See [Step 2](#2-add-the-local-provider-as-a-reusable-identity-recipient). | | `PROVIDER_FOR_CLIENT_ID` | Pods | Send as `forClientId` when generating Sumsub share tokens for that provider. See [Step 5](#5-generate-the-sumsub-share-token). | The local KYC/AML provider (Avenia for Brazil) has no direct relationship with your company in this flow. Pods manages that relationship internally and gives you the values needed for Sumsub reusable identity sharing. ## 1. Configure the Sumsub level Create an individual Sumsub verification level named exactly: ```text theme={null} pods-ramp-level ``` The fixed name keeps setup, monitoring, support, and runbooks consistent across integrations. Open [Sumsub Individual levels](https://cockpit.sumsub.com/checkus/sdkIntegrations/levels/individual?limit=100), create a **Standard** level, and configure the required steps. Create an individual Sumsub level Select Standard level type ### Required Sumsub steps | Step | Configuration | | - | - | | Identity document | Allow ID card, passport, driver's license, and residence permit. The applicant submits one accepted document. | | Selfie | Use advanced liveness check. | | Applicant data | Require first name, last name, TIN, tax residence country, email, date of birth, street, street line 2, country, city, state, building number, and postcode. | Add the identity document and selfie steps first. Identity document and selfie steps Then add the applicant data step. Select Applicant data as a new step Add each required applicant data field as a separate required field. Add applicant data fields Create the level. Create the Sumsub level ### Country-specific requirements The required tax id, applicant fields, and Sumsub reusable-identity partner differ per country. For Brazil, Avenia requires: * CPF in the Sumsub `TIN` field. * Country of Tax ID set to Brazil. * Full name. * Date of birth. * One identity document: `ID_CARD`, `PASSPORT`, `DRIVERS`, or `RESIDENCE_PERMIT`. * Sumsub review answer `GREEN`. For Mexico, Colombia, and Argentina, confirm the equivalent tax-id field, required documents, and Sumsub partner with Pods before creating the level, since those routes are not live yet. Reference: * [Sumsub verification levels](https://docs.sumsub.com/docs/verification-levels) * [Sumsub configure verification levels](https://docs.sumsub.com/docs/configure-verification-levels) * [Avenia Sumsub shared token flow](https://integration-guide.avenia.io/docs/KYC/kycSumsubSharedToken/) ## 2. Add the local provider as a reusable identity recipient Before generating reusable KYC share tokens, add the local KYC/AML provider as a recipient in Sumsub. 1. Go to `Reusable identity` -> `Partners`. 2. Open the `Recipients` tab. 3. Click `Add recipient`. 4. Enter the `PROVIDER_PARTNER_TOKEN` provided by Pods. 5. Save the recipient setup. Open Reusable identity partners Open the Recipients tab and add a recipient Enter the provider partner token After the recipient is available, use the `PROVIDER_FOR_CLIENT_ID` from Pods when generating share tokens. This is the provider's Sumsub recipient/client id. It is separate from the partner token and from your Pods API key. Reference: [Sumsub partner management](https://docs.sumsub.com/docs/manage-sharing-partners). ## 3. Create a Sumsub app token Create the Sumsub app token after Reusable Identity is enabled and after the local provider is added as a recipient. This order matters because the token must include permission to share applicant data. 1. Open Sumsub Dashboard. 2. Go to `Dev space` -> `App tokens`. 3. Create a new app token. 4. Ensure `Reusable Identity` -> `Share applicants data` is selected. 5. Save the token and secret key securely. Sumsub only shows the secret once. Generate a Sumsub app token Enable Reusable Identity share applicants data permission Save the generated Sumsub app token and secret key Store them in your backend: ```bash theme={null} SUMSUB_API_BASE_URL="https://api.sumsub.com" SUMSUB_APP_TOKEN="..." SUMSUB_SECRET_KEY="..." SUMSUB_LEVEL_NAME="pods-ramp-level" PROVIDER_FOR_CLIENT_ID="..." ``` If Sumsub enables Reusable Identity after you already created an app token, create a new app token and make sure `Reusable Identity` -> `Share applicants data` is selected. A valid app token still requires active Sumsub credits/license. If Sumsub returns `license-key-exhausted` or `Not enough credit`, ask Sumsub to enable credits for the target environment. Reference: [Sumsub app tokens](https://docs.sumsub.com/reference/authentication). ## 4. Run WebSDK and confirm approval Use your backend to create a Sumsub WebSDK access token for the applicant and mount WebSDK in your application. Use the same `pods-ramp-level` level name configured above. Your backend must only continue to Pods after Sumsub reaches: ```text theme={null} reviewStatus = completed reviewResult.reviewAnswer = GREEN ``` You can detect that result by receiving Sumsub webhooks or by polling Sumsub from your backend. If you use webhooks, the target must point to your backend, not to Pods. Required signal: | Signal | Required? | Purpose | | - | - | - | | `applicantReviewed` with `completed + GREEN` | Yes | Generate the share token and submit it to Pods. | | `applicantReviewed` with `completed + RED` | Yes | Do not submit to Pods. Keep the user blocked or route them to retry/support. | | `applicantPending` | Recommended | Useful for monitoring pending reviews. | | `applicantAwaitingUser` | Recommended | Useful to prompt the user to complete missing steps. | | `applicantAwaitingService` or `applicantOnHold` | Recommended | Useful for operations visibility. | | `applicantCreated` | Optional | Logging only. Do not call Pods from this event. | Reference: * [Sumsub user verification webhooks](https://docs.sumsub.com/docs/user-verification-webhooks) * [Sumsub receive verification results](https://docs.sumsub.com/docs/receive-and-interpret-results-via-api) * [Sumsub WebSDK integration](https://docs.sumsub.com/docs/get-started-with-web-sdk) ## 5. Generate the Sumsub share token After Sumsub returns `completed + GREEN`, your backend calls Sumsub to generate a fresh share token for that applicant and the local provider's recipient. ```http theme={null} POST https://api.sumsub.com/resources/accessTokens/shareToken ``` Request: ```json theme={null} { "applicantId": "SUMSUB_APPLICANT_ID", "forClientId": "PROVIDER_FOR_CLIENT_ID", "ttlInSecs": 600 } ``` Use the approved Sumsub applicant id and reuse the same `PROVIDER_FOR_CLIENT_ID` provided by Pods. Generate a fresh `shareToken` for each approved applicant import. Sumsub share tokens are short-lived and invalidated after usage. Response: ```json theme={null} { "token": "SUMSUB_SHARE_TOKEN", "forClientId": "PROVIDER_FOR_CLIENT_ID" } ``` Reference: * [Sumsub generate share token API](https://docs.sumsub.com/reference/generate-share-token) * [Sumsub reusable KYC via API](https://docs.sumsub.com/docs/reusable-kyc-via-api) * [Avenia shared token flow](https://integration-guide.avenia.io/docs/KYC/kycSumsubSharedToken/) ## 6. Submit the share token to Pods Send the fresh token from your backend to Pods. Pods does not store the share token. Pods validates the submitted tax id and searches its profiles for an approved identity with the same salted tax-id hash. When the local provider still reports the identity as approved, Pods creates a separate profile for your customer, reuses the approved identity, and returns `approved` without consuming the fresh token. Pods stores only the salted hash and the last four digits, never the raw tax id. If no approved identity can be revalidated, Pods creates or reuses the customer profile and internal subaccount with the local provider, then imports the fresh share token normally. If the provider reports that the token was already imported into that subaccount, Pods checks that subaccount for an existing approved attempt before adopting it. ```http theme={null} POST /api/v1/kyc/sumsub-share-token ``` Request: ```json theme={null} { "cpf": "52998224725", "shareToken": "SUMSUB_SHARE_TOKEN", "sumsubApplicantId": "SUMSUB_APPLICANT_ID", "email": "user@example.com", "walletAddress": "UserWalletAddress" } ``` | Field | Required | Description | | - | - | - | | `cpf` | Yes | Valid tax id used to match a previously approved identity. For Brazil this is a CPF. Send digits only or formatted; Pods does not store the raw value. | | `shareToken` | Yes | The fresh Sumsub token generated for the local provider. | | `sumsubApplicantId` | Yes | Unique Sumsub applicant id from the approved applicant. Pods uses it for customer-scoped idempotency. | | `email` | Yes | User email stored on the Pods KYC profile. | | `walletAddress` | Yes | User wallet stored on the Pods KYC profile and used by money movement flows. Accepts a checksummed EVM address **or** a Solana public key. Pix BRL ↔ BRS Solana and Pix BRL ↔ BRZ Polygon quotes must use this same address as `destinationAddress` (onramp) or `originAddress` (offramp). | Pods generates the public `kycUserId` and returns it in the response. Store this id in your backend and use it for future status checks. Successful response: ```json theme={null} { "kycUserId": "550e8400-e29b-41d4-a716-446655440000", "status": "provider_pending" } ``` If this request is retried with the same `sumsubApplicantId`, Pods returns the same customer-scoped profile instead of creating a duplicate. The tax id must remain the same; a conflicting tax id returns `409 SUMSUB_CPF_MISMATCH`. An immediate `approved` response means the local provider revalidated an existing approved identity for the tax id. A `provider_pending` response means the provider accepted a new import for processing; approval is only final after the status endpoint returns `approved`. See [Track Pods KYC status](/guides/ramp/status) to poll for that result, and [Move money with Pods KYC](/guides/ramp/quotes) to use the resulting approval. # Ondo Global Markets Source: https://docs.pods.finance/guides/ondoglobal-markets API integration guide for buying and selling tokenized stocks & ETFs on BSC via Ondo Global Markets strategies, including cross-chain funding and payout Ondo Global Markets brings tokenized **stocks & ETFs** on-chain as **DeFi strategies** (`protocol: "Ondo"`, `network: bsc`). The market-share position always lives on **BSC**, but clients can fund a buy from — or receive a sell payout on — another EVM chain. Pods handles the bridge and the async order fill. ## Introduction | Item | Value | | - | - | | **Strategy ID format** | `Ondo--bsc` (e.g. `Ondo-AAPL-bsc`) | | **Position chain** | BSC (`56`) | | **Strategy asset** | Market-share token on BSC (symbol ends in `ON`, e.g. `AAPLON`) | | **Underlying reference** | Strategy accounting settles in **BSC USDC** (18 decimals) internally. Funding/payout may use [another chain's USDC](#8-supported-chains-%26-usdc-addresses) — on cross-chain calls, `quote.tokenIn` (buy) / `quote.tokenOut` (sell) are already expressed in that chain's token, decimals and `chainId`. | | **Buy / Sell actions** | `buy` or `request-lend` / `sell` or `request-withdraw` | | **Optional limit / expiry** | `priceInUsd` (USD cents) and `expireAt` (Unix seconds) on BSC CoW bytecode — [details](#limit-price-expiry-and-cancel) | | **Minimum buy** | `$10` USDC (`strategy.metadata.minRequestLendUsd`) | | **Auth** | Header `x-api-key: ` | **Same-chain (BSC)** ``` Buy: USDC (BSC) → [client signs on BSC] → market shares (BSC) Sell: market share (BSC) → [client signs on BSC] → USDC (BSC) ``` **Crosschain** ``` Buy: USDC (origin) → [client signs on origin chain] → market shares (BSC) Sell: market share (BSC) → [client signs on BSC] → USDC (destination chain) ``` Position: always queried with the wallet on **BSC**. > **Settlement is asynchronous.** The on-chain transaction submits the request in seconds; the order > reaches `SUCCESS` only after Ondo fills it (minutes later). For crosschain, the bridge leg settles > first, then the order. Track both the on-chain receipt and the action status via WebSocket or > history polling. > **Quote and bytecode come from the same call.** `GET /strategies/:id/bytecode` returns a priced > `quote` (for your UI) and `bytecode[]` (to execute on-chain). **Preview quotes without a connected wallet.** `wallet` also accepts the zero address (`0x000…0`) or a well-known burn address (e.g. `0x000…dEaD`, `0x111…`, `0x222…`). The response is priced exactly like a real request but includes `previewOnly: true` and omits `id`, `singleUseAddress`, and `orderUid` — no `Action` is created, and the address is never persisted or synced to Moralis. A `previewOnly` response has no bytecode to sign. Use the user's real, owned wallet address for any request you intend to execute. *** ## Authentication Every request requires: ``` x-api-key: ``` * The key identifies your **customer**. Fee configuration and strategy catalog are scoped to it. * Success responses are the bare JSON body (HTTP 200). Errors use `{ "error": { "code", "message", "details?" } }`. * Keep the API key on your **backend** — never expose it in a browser app. Don't have a key yet? [Get an API key](https://www.pods.finance/plg/select-plan). ## Integration flow Buy and sell share **one path** — only the bytecode call (step 4) differs by direction. Check [tradability](#1-market-status) — prefer passing a symbol. ```http theme={null} GET /ondo/stocks/market-status?symbol=NVDAon ``` ```http theme={null} GET /tokens?category=stock,etf,portfolio&chainId=56 ``` ```http theme={null} GET /strategies?protocol=Ondo&network=bsc&category=stock,etf,portfolio ``` ```http theme={null} GET /strategies/:id/bytecode ``` | | BUY — `buy` or `request-lend` | SELL — `sell` or `request-withdraw` | | - | - | - | | `action` | `buy` or `request-lend` | `sell` or `request-withdraw` | | Amount param | `amount=` | `amountInShares=` | | Direction params | `fromChainId` / `fromTokenAddress` | `toChainId` / `toTokenAddress` | | Optional | `priceInUsd`, `expireAt` — [limit & expiry](#limit-price-expiry-and-cancel) | same | **Buy only:** enforce the `$10` minimum. Execute `bytecode[]` on the correct chain. Poll until `SUCCESS` — via WebSocket, `GET /strategies/:id/status`, or position/history polling. Re-read the wallet position to reflect the settled result. *** ## 1. Market status Gate buy/sell when the market is closed. Optionally pass a Global Markets token `symbol` to enrich the same response with per-asset tradability under `asset`. ```http theme={null} GET /ondo/stocks/market-status GET /ondo/stocks/market-status?symbol=NVDAon ``` The response shape is always the Ondo Global Markets market-wide payload plus an `asset` field: * without `symbol`: `asset` is `null` — returns the default market status: ```json theme={null} { "isOpen": true, "marketStatus": "regular", "nextOpen": "2025-08-21T13:31:00Z", "nextClose": "2025-08-20T19:59:00Z", "nextOpenSession": "regular", "reason": null, "timestamp": "2025-08-20T21:03:02Z", "asset": null } ``` * with `symbol`: `asset` contains the per-symbol tradability snapshot Key market fields: `isOpen` — block new submissions when `false`. The payload may also include session details (`marketStatus`, `nextOpen`, `nextClose`, `offhours`). `symbol` is the Ondo Global Markets token symbol and must end in lowercase `on` (for example `NVDAon`, `AAPLon`). Invalid symbols return **422**. When present, `asset` includes: ```jsonc theme={null} "asset": { "symbol": "NVDAon", "tradable": true, "limited": false, "blockingReason": null, // { code, message } when not tradable "paused": false, "status": null, // matching pause/limit entry when present "marketData": { /* raw Ondo per-asset market data (primary / underlying) */ } } ``` | Field | Meaning | | - | - | | `symbol` | Echo of the requested Global Markets token symbol | | `tradable` | `true` when Deframe considers the symbol safe to trade right now | | `limited` | `true` when an active `ASSET_LIMITED` restriction exists (trade still allowed) | | `blockingReason` | `null` when tradable; otherwise `{ code, message }` explaining why not | | `paused` | Convenience flag for an active `ASSET_PAUSED` restriction | | `status` | Matching asset pause/limit entry when present | | `marketData` | Raw per-asset market data (primary/underlying), when available | `asset.tradable` is `false` when: 1. The market is closed/paused (`ONDO_MARKET_CLOSED` / `ONDO_MARKET_PAUSED`), unless `offhours.isOpen` is `true` 2. The specific asset has an active `ASSET_PAUSED` restriction (`ONDO_ASSET_PAUSED`) An active `ASSET_LIMITED` restriction does **not** flip `tradable` to `false`; it only sets `limited: true`. Bytecode generation for Ondo `request-lend` / `request-withdraw` uses the same tradability rules and returns **409** with those `ONDO_*` codes when a trade is blocked. Per-asset eligibility during off-hours can still fail later at CoW quote time with errors such as `COW_ASSET_SESSION_CLOSED` — see [Ondo Global Markets docs](https://docs.ondo.finance). *** ## 2. List stocks, ETFs, and portfolios ```http theme={null} GET /tokens?category=stock,etf,portfolio&chainId=56&limit=300 ``` * `category` accepts comma-separated values. Ondo Global Markets uses `stock`, `etf`, and `portfolio`. * `portfolio` is the Ondo model portfolios (for example `BLKHIon`, `MAG7Xon`). They are not stocks or ETFs. * Each token: `{ symbol, name, address, decimals, chainId, category, priceInUSD, logoURI? }`. * Ondo symbols end in `ON` (e.g. `AAPLON`). Strip `ON` for display if desired. *** ## 3. Resolve strategy ID A market-share token address alone is not enough — you need the **strategy id**. ```http theme={null} GET /strategies?protocol=Ondo&network=bsc&category=stock,etf,portfolio ``` `category` filters the same Ondo tags as the token catalog: `stock`, `etf`, and `portfolio`. Pass one tag or a comma-separated list. Omit it to return every Ondo strategy on that network. Returns `{ data: [...], pagination }`. Each item's **`id`** is the strategy id you use — a slug in the form `{protocol}-{assetName}-{network}`: ```json theme={null} { "data": [ { "id": "Ondo-NVDA-bsc", // ← the strategy id you pass to the bytecode call "protocol": "Ondo", "network": "bsc", "networkId": "56", // string, not number "asset": "0x…", // ← market-share token address (match this) "assetName": "NVDA", "assetToken": { "address": "0x…", "symbol": "NVDAon" }, "availableActions": ["request-lend", "request-withdraw", "buy", "sell"] } ], "pagination": { "page": 1, "limit": 100, "total": 12 } } ``` Pick the item where: * `asset` matches the market-share token address * `networkId` is `"56"` (a string) Then pass its `id` (e.g. `Ondo-NVDA-bsc`) to `buy` / `request-lend` or `sell` / `request-withdraw` from `availableActions`. `buy` and `sell` do the same thing as `request-lend` and `request-withdraw`. *** ## 4. Buy — `buy` or `request-lend` ```http theme={null} GET /strategies/:strategyId/bytecode ``` | Parameter | Required | Description | | - | - | - | | `action` | yes | `buy` or `request-lend` | | `wallet` | yes | Address that signs on the **funding chain** | | `amount` | yes | Funding token in **base units** on `fromChainId`. Use the **token's decimals** (from `fromTokenAddress`), not a fixed 6 — e.g. `10000000` = $10 USDC on Base (6 dec); `10000000000000000000` = $10 USDC on BSC (18 dec). Read `quote.tokenIn.decimals` to confirm. | | `fromChainId` | yes | Funding chain (`56` for same-chain BSC, or another supported chain) | | `fromTokenAddress` | yes | USDC (or supported token) contract on `fromChainId`. **Required** whenever `fromChainId` is set. | | `priceInUsd` | no | Client CoW limit as **integer USD cents** (`17050` = \$170.50). See [limit price, expiry, and cancel](#limit-price-expiry-and-cancel). | | `expireAt` | no | CoW `validTo` as a **UTC Unix timestamp in seconds** (not ms). Independent of `priceInUsd`. | **Same-chain (BSC):** `fromChainId=56`, `fromTokenAddress=0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` (18 decimals). The client signs on BSC. Buy \$10 (USDC has 18 decimals on BSC → `amount=10e18`): ```http theme={null} GET /strategies/Ondo-NVDA-bsc/bytecode?action=request-lend&wallet=0xYourWalletAddress&amount=10000000000000000000&fromChainId=56&fromTokenAddress=0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d ``` **Crosschain:** `fromChainId=` origin chain (e.g. `8453` Base). The client signs **only on the origin chain**; Pods bridges funds to BSC and fills the order asynchronously. Buy \$10 from Base (USDC has 6 decimals on Base → `amount=10e6`): ```http theme={null} GET /strategies/Ondo-NVDA-bsc/bytecode?action=request-lend&wallet=0xYourWalletAddress&amount=10000000&fromChainId=8453&fromTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` **Response (relevant fields):** ```jsonc theme={null} { "id": "", // correlate status & WebSocket "chainIdIn": 8453, // number — funding chain (where the client signs) "chainIdOut": 56, // number — position chain (BSC) "crossChain": { "isCrossChain": true, "chainIdIn": 8453, "chainIdOut": 56 }, "singleUseAddress": "0x…", // ephemeral wallet for async settlement "orderUid": "0x…", // settlement order id "quote": { // Crosschain: tokenIn is the funding leg (origin token/chain), already merged with the bridge // quote — decimals and chainId always match what you actually send on fromChainId. "tokenIn": { "amount", "amountInUSD", "symbol", "decimals", "chainId": 8453, ... }, // tokenOut stays the settlement leg (market shares on BSC) — unaffected by the funding bridge. "tokenOut": { "amount", "minAmountOut", "amountInUSD", "chainId": 56, ... }, // charges[] adds a bridgeAndSlippage line for the funding bridge alongside the settlement fees. "feeBreakdown": { "summary": { "platformFeesTotalInUSD", "bridgeAndSlippageImpactOutUSD" } } }, // Each bytecode leg `chainId` is a decimal string (JSON bigint). Coerce with Number() vs chainIdIn. "bytecode": [ { "to": "0x…", "value": "0", "data": "0x…", "chainId": "8453" } ] } ``` **Client execution** **Atomic execution is mandatory.** Cross-chain buys may return multiple legs on the funding chain (approve + bridge). Execute every leg where `Number(chainId) === Number(chainIdIn)` in **one transaction** — never as separate sends. Partial execution parks funds in escrow with `PENDING` status and no pipeline progress. See [Executing Bytecode](/getting-started/executing-bytecode) and [EIP-7702](/examples/javascript/using-eip7702). 1. Filter `bytecode` where `Number(chainId) === Number(chainIdIn)` (same-chain: `56`; crosschain: the origin chain). 2. Batch all filtered legs into **one** signed transaction (`output=bytecode` + EIP-7702, or `output=userOperation` / `output=fireblocks` per your wallet stack). 3. Wait for async completion (crosschain: bridge → BSC → market-share fill). * **Minimum:** \$10 USDC (enforced server-side, error `REQUEST_LEND_AMOUNT_TOO_LOW`). For crosschain the minimum is checked on the **bridged USDC amount** that lands on BSC. * Use `quote` for the full pricing UI (funding amount, expected shares, fees). `quote.tokenIn` always reflects `fromChainId` — read its `decimals`/`chainId` directly, no need to cross-reference §8. > Crosschain: the client only signs on the origin chain. Bridge + market-share purchase are handled by Pods. *** ## 5. Sell — `sell` or `request-withdraw` ```http theme={null} GET /strategies/:strategyId/bytecode ``` | Parameter | Required | Description | | - | - | - | | `action` | yes | `sell` or `request-withdraw` | | `wallet` | yes | Address on **BSC** (where market shares are held) | | `amountInShares` | yes | Share amount in **wei** (18 decimals) | | `toChainId` | yes | Payout chain (`56` for same-chain BSC, or another supported chain) | | `toTokenAddress` | yes | USDC (or supported token) on `toChainId`. **Required** whenever `toChainId` is set. | | `priceInUsd` | no | Client CoW limit as **integer USD cents** (`17050` = \$170.50). See [limit price, expiry, and cancel](#limit-price-expiry-and-cancel). | | `expireAt` | no | CoW `validTo` as a **UTC Unix timestamp in seconds** (not ms). Independent of `priceInUsd`. | **Same-chain (BSC):** `toChainId=56`, `toTokenAddress=0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d`. The client signs on BSC. Sell 5 shares (shares are 18-decimal wei → `amountInShares=5e18`), receive USDC on BSC: ```http theme={null} GET /strategies/Ondo-NVDA-bsc/bytecode?action=request-withdraw&wallet=0xYourWalletAddress&amountInShares=5000000000000000000&toChainId=56&toTokenAddress=0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d ``` * Use `amountInShares` (not `amount`) for Ondo sells. No minimum sell amount. * Response shape matches buy (`id`, `chainIdIn`, `chainIdOut`, `crossChain`, `singleUseAddress`, `orderUid`, `quote`, `bytecode`). **Sell legs are a direct `transfer` of the market-share token to `singleUseAddress`, not an `approve` + pull.** If you run a calldata allowlist (e.g. a policy-signer-sponsored paymaster), the sell leg's call target is the market-share token contract itself — whitelist each ticker token you support as a call target, not just a shared router/spender address. **Crosschain:** `toChainId=` destination chain. The client signs **only on BSC** (shares live there); Pods sells the market share for USDC on BSC, then bridges the proceeds to the destination chain. Sell 5 shares (`amountInShares` is still 18-decimal shares → `5e18`), receive USDC on Base: ```http theme={null} GET /strategies/Ondo-NVDA-bsc/bytecode?action=request-withdraw&wallet=0xYourWalletAddress&amountInShares=5000000000000000000&toChainId=8453&toTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` * **`quote.tokenIn`** is always shares on BSC (`decimals: 18`, `chainId: 56`) — what the client signs away. * **`quote.tokenOut`** reflects the **payout leg**: on same-chain sells this is BSC USDC (`decimals: 18`); on cross-chain sells it is already `toTokenAddress` on `toChainId` (e.g. Base USDC = `decimals: 6`, `chainId: 8453`). Read `quote.tokenOut.decimals` / `quote.tokenOut.chainId` directly — never assume BSC decimals for a cross-chain sell, and never cross-reference §8 to guess the payout scale. * **`quote.feeBreakdown.charges[]`** includes a `bridgeAndSlippage` line for the exit and, on cross-chain sells, a second one for the payout bridge — both self-describe their own `decimals`/`symbol`, so render each line as-is rather than assuming a shared scale. * The payout bridge is requoted at forward time with the realized settlement balance, so treat `quote.tokenOut` on cross-chain sells as the estimate at bytecode time, not the final amount. * **`bytecode[].chainId`** is a **decimal string** (e.g. `"56"`). Compare with `Number(leg.chainId) === 56` (or `Number(chainIdIn)`), never strict-equal to a numeric route field. **Client execution** Batch all BSC legs (`Number(chainId) === 56`) into **one atomic transaction**. See [Executing Bytecode](/getting-started/executing-bytecode). 1. Filter `bytecode` where `Number(chainId) === 56` (always BSC — shares are on BSC). 2. Batch and sign on BSC in a single transaction. 3. Wait for async completion (market-share fill → crosschain: bridge USDC to `toChainId`). > Crosschain: the client only signs on BSC. Market share → USDC redemption and the bridge payout are handled async by Pods. *** ## Limit price, expiry, and cancel `priceInUsd` and `expireAt` are optional on `GET /strategies/:id/bytecode` for **Ondo BSC CoW** (`Ondo-*-bsc` / `OndoStockBsc`) `request-lend` and `request-withdraw`. Ethereum primary Ondo strategies and every other strategy reject them with `400` `PRICE_IN_USD_NOT_SUPPORTED` / `EXPIRE_AT_NOT_SUPPORTED`. The two params are **independent**: | `priceInUsd` | `expireAt` | Price / slippage | CoW `validTo` | | - | - | - | - | | no | no | GM / CoinGecko + **100 bps** | SLA (1.5× same-chain, 3× cross-chain) | | no | yes | GM / CoinGecko + **100 bps** | `expireAt` | | yes | no | client limit + **10 bps** | now + 24h | | yes | yes | client limit + **10 bps** | `expireAt` | **`priceInUsd`** is integer **USD cents**, not dollars. `17050` = \$170.50. When set, the bytecode `quote` also includes: | Field | Meaning | | - | - | | `quote.clientLimitPriceUsd` | Client limit in **dollars** (`170.5`) | | `quote.spotPriceUsd` | Reference spot used to compute distance | | `quote.distanceBps` | Signed distance of the client limit vs spot, in basis points | **`expireAt`** is a UTC Unix timestamp in **seconds** (CoW `validTo` / uint32). Milliseconds are rejected (`INVALID_EXPIRE_AT`). The timestamp must be in the future and at most **168 hours** from now. Buy $10 of NVDA on BSC with a $320.00 limit that expires in one hour: ```http theme={null} GET /strategies/Ondo-NVDA-bsc/bytecode?action=request-lend&wallet=0xYourWalletAddress&amount=10000000000000000000&fromChainId=56&fromTokenAddress=0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d&priceInUsd=32000&expireAt=1778000000 ``` ### Cancel a client-limit order Only **funded, unfilled client-limit** actions (`quote` / PRESIGN `clientLimitPriceUsd > 0`) can be cancelled. An `expireAt`-only market order is **not** cancellable — it expires and refunds at `orderValidTo`. ```http theme={null} GET /actions/:id/cancel?wallet=0xYourWalletAddress ``` Auth is `x-api-key` or a Bearer token. `wallet` must match the action's requesting wallet. A `200` means the action is `REFUNDED` and the SUW refund outbox was **queued** (or already existed). Poll `GET /actions/:id` until the refund step / outbox confirms on-chain. The reason `ONDO_CLIENT_LIMIT_CANCELLED` is stored on the **settlement step** (`steps[].failureReason`), not as a top-level Action field. | Code | HTTP | When | | - | - | - | | `ACTION_CANCEL_WALLET_MISMATCH` | 403 | `wallet` is not the requesting wallet | | `ACTION_CANCEL_NOT_SUPPORTED` | 409 | Not an Ondo BSC client-limit investment action | | `ACTION_CANCEL_NOT_OPEN` | 409 | Already `SUCCESS`, `REFUNDED`, or `EXPIRED` | | `ACTION_CANCEL_NOT_FUNDED` | 409 | Funding transfer has not confirmed yet | | `ACTION_ALREADY_FILLED` | 409 | CoW already filled (including a fill that races the cancel) | | `ACTION_CANCEL_REFUND_QUEUE_FAILED` | 502 | Cancel recorded `REFUNDED` but the refund outbox could not be queued | *** ## 6. Positions & pending requests The `wallet` is always normalized to **BSC** (same EVM address regardless of funding/payout chain). ### Wallet snapshot (all positions) ```http theme={null} GET /v2/wallets/:address?include=all ``` Stocks & ETFs appear under `earn.positions` where `strategy.protocol === "Ondo"` — **not** at the top level of the response: ```jsonc theme={null} { "tokens": { "positions": [ /* ERC-20 balances, unrelated to Ondo positions */ ], "summary": { "totalUsdValue": 128.40 } }, "earn": { "positions": [ { "spotPosition": { "currentPositionInShares": { "value": "", "decimals": 18, "symbol": "AAPLON" }, "underlyingBalanceUSD": 14.98, "requestedToLend": [], "requestedToLendInShares": [], "requestedToWithdraw": [], "requestedToWithdrawInShares": [], "profitInUSD": 0.12 }, "strategy": { "protocol": "Ondo", "id": "Ondo-AAPL-bsc", "asset": "0x…", "networkId": 56, "metadata": { "ondoSymbol": "AAPL", "minRequestLendUsd": 10 } } } ], "summary": { "totalUnderlyingBalanceUSD": 14.98, "totalProfitInUSD": 0.12 } }, "history": { "items": [ /* ... */ ], "pagination": { /* ... */ } } } ``` Each entry in `earn.positions` has the same `{ spotPosition, strategy }` shape as the single-strategy endpoint below — read `earn.positions[].strategy.protocol === "Ondo"` to filter for stocks & ETFs. ### Single strategy position ```http theme={null} GET /v2/wallets/:address/strategies/:strategyId ``` or ```http theme={null} GET /v2/strategies/:strategyId?wallet=
``` Both forms return the same response. It includes Actions-backed accounting for principal, profit, and [in-flight operations](#in-flight-operations). **Example response** — a settled 5-share AAPLON holding, nothing pending: ```jsonc theme={null} { "spotPosition": { "currentPositionInShares": { "value": "5000000000000000000", "decimals": 18, "symbol": "AAPLON" }, "underlyingBalanceUSD": 14.98, "requestedToLend": [], // pending buy, USDC "requestedToLendInShares": [], // pending buy, shares "requestedToWithdraw": [], // pending sell, USDC "requestedToWithdrawInShares": [], // pending sell, shares "profitInUSD": 0.12 }, "strategy": { "protocol": "Ondo", "id": "Ondo-AAPL-bsc", "asset": "0x…", "networkId": 56, "metadata": { "ondoSymbol": "AAPL", "minRequestLendUsd": 10 } } } ``` ### In-flight operations The `requestedTo*` arrays represent **pending async orders** not yet settled (same on same-chain and crosschain): | Field | Meaning | | - | - | | `requestedToLend` | USDC committed to a pending buy | | `requestedToLendInShares` | Equivalent shares for pending buy | | `requestedToWithdraw` | USDC value of a pending sell | | `requestedToWithdrawInShares` | Shares committed to a pending sell | Empty arrays mean nothing is pending. **Expected states** * Buy submitted, shares not yet credited → `requestedToLend` / `requestedToLendInShares` populated. * Sell submitted, USDC not yet paid out → `requestedToWithdraw` / `requestedToWithdrawInShares` populated. * Settled → all pending arrays empty; `currentPositionInShares` / `underlyingBalanceUSD` reflect the real balance. Suggested polling: every 5–15 s until the pending arrays clear (or use WebSocket/webhook below). ### Available balance for sell | Value | How to compute | | - | - | | `availableShares` | `currentPositionInShares.value` − sum of `requestedToWithdrawInShares` | | `availableUSD` | `underlyingBalanceUSD` − sum of `requestedToWithdraw` (clamp to ≥ 0) | Do **not** subtract pending buys from available shares. Use `availableShares` as `amountInShares` when selling the full position. *** ## 7. Track order status After submitting `bytecode[]` on-chain, the action is **not** complete until status is `SUCCESS`. ### HTTP — pending actions for one strategy Poll when you want a strategy-scoped snapshot without loading the full wallet or strategy position. The handler refreshes in-flight operations before responding. ```http theme={null} GET /strategies/:strategyId/status?wallet=
``` | Parameter | Required | Description | | - | - | - | | `strategyId` | yes (path) | Strategy slug or id (e.g. `Ondo-AAPL-bsc`) | | `wallet` | yes | Same wallet used in the bytecode call | #### Root response ```jsonc theme={null} { "strategyId": "Ondo-AAPL-bsc", "wallet": "0xUserWalletAddress", "hasPending": true, "message": "1 pending action(s) for this strategy.", "orderBook": { /* Ondo Global Markets only — explained below */ }, "actions": [] } ``` | Field | Type | Description | | - | - | - | | `strategyId` | string | Strategy slug | | `wallet` | string | Checksummed address from query | | `hasPending` | boolean | `true` when `actions.length > 0` | | `message` | string | Human-readable summary | | `actions` | array | Pending actions only (see below) | | `orderBook` | object | CoW tradability probe — BSC Ondo stock strategies only | | `gmMarket` | object | Ondo GM market/asset tradability — Ethereum primary strategies only | #### Orderbook `orderBook` (**BSC Ondo Global Markets strategies only**) is a pre-flight tradability probe — it tells you whether a new request is likely to succeed right now, before you build any bytecode. `requestLend` covers **buys** and `requestWithdraw` covers **sells** (present only when sell is supported): ```jsonc theme={null} "orderBook": { "requestLend": { "canOpenOrder": true, "code": null, "errorType": null, "message": null, "retryable": false, "probeSellAmount": "100000000000000000000", "probeBuyAmount": "…" }, "requestWithdraw": { /* same shape when sell is supported */ } } ``` | `orderBook` field | Meaning | | - | - | | `canOpenOrder` | Whether a new buy/sell request is likely to succeed right now | | `code` | Stable error code when `canOpenOrder` is `false` | | `errorType` | Additional machine-readable reason (nullable) | | `message` | Human-readable reason (nullable) | | `retryable` | Client may retry later (e.g. market closed) | | `probeSellAmount` | Amount used for the availability probe (raw units) | | `probeBuyAmount` | Expected output from probe when `canOpenOrder` is `true` (optional) | Present even when `actions` is empty. #### GM market (Ethereum primary) `gmMarket` is the equivalent pre-flight probe for **Ethereum primary** Ondo stock strategies. It comes from the memoized Ondo GM market/asset status, not CoW: ```jsonc theme={null} "gmMarket": { "source": "ondo-gm", "available": true, "tradable": true, "code": null, "message": null, "symbol": "NVDAon", "marketStatus": "regular", "isOpen": true, "asset": { "tradable": true, "paused": false, "limited": false } } ``` `available: false` and `code: "ONDO_GM_UNAVAILABLE"` mean the GM API could not be reached. Closed/paused markets keep `available: true` and set `tradable: false` with `ONDO_MARKET_CLOSED`, `ONDO_MARKET_PAUSED`, or `ONDO_ASSET_PAUSED`. #### Actions **What appears in `actions[]`** | Action state | Listed? | Where to track instead | | - | - | - | | `PENDING` | yes | This endpoint | | `INITIAL` (user has not sent the funding tx yet) | no | `GET /actions/:id`, WebSocket | | `SUCCESS`, `REFUNDED`, `FAILED`, etc. | no | History, wallet position, WebSocket | **Each item in `actions[]` — base fields (every pending action)** ```jsonc theme={null} { "id": "674a1b2c3d4e5f6789012345", "type": "SAMECHAIN_INVESTMENT_DEPOSIT", "status": "PENDING", "createdAt": "2026-05-29T12:00:00.000Z", "updatedAt": "2026-05-29T12:10:00.000Z", "currentStep": { "stepIndex": 2, "status": "PENDING", "legs": [{ "kind": "SWAP", "status": "PENDING", "operator": null }] }, "steps": [ { "stepIndex": 0, "status": "SUCCESS", "legs": [] } ], "suw": { /* Ondo Global Markets same-chain only — explained below */ } } ``` | Field | Notes | | - | - | | `type` | Buy: `SAMECHAIN_INVESTMENT_DEPOSIT` or `CROSSCHAIN_INVESTMENT_DEPOSIT`. Sell: `SAMECHAIN_INVESTMENT_WITHDRAW` or `CROSSCHAIN_INVESTMENT_WITHDRAW` | | `currentStep` | First step still `INITIAL` or `PENDING`. `null` when no step is waiting — the on-chain steps are done but the action is still settling async (track `suw.phase` instead), not a sign it's finished | | `steps` | Full pipeline snapshot | | `steps[].legs[]` | `{ kind, status, operator }` — leg kind examples: `SWAP`, `PRESIGN`, `MINT`, `REDEEM`, `TRANSFER`, `BRIDGE` | Cross-chain pending actions include **only** the base block (no `suw`). #### Same-chain suw **`suw` block (same-chain only):** ```jsonc theme={null} "suw": { "singleUseAddress": "0x…", "phase": "order_in_progress", "fundsReturnedToWallet": false, "order": { /* posted and unposted shapes explained below */ }, "balances": { "underlying": { "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", "symbol": "USDC", "decimals": 18, "amountRaw": "10000000000000000000", "amountHumanized": "10.0" }, "strategy": { "address": "0x…", "symbol": "AAPLON", "decimals": 18, "amountRaw": "0", "amountHumanized": "0.0" } }, "timing": { "requestedAt": "2026-05-29T12:00:00.000Z", "ageSeconds": 1800, "deadlineSeconds": 7200, "refundAt": "2026-05-29T14:00:00.000Z", "secondsUntilRefund": 120 }, "pricing": { "side": "BUY", "referencePriceUsd": 200, "livePriceUsd": 214, "priceSource": "ondo", "driftBps": 700, "slippageBps": 100 }, "suwSteps": [ { "stepIndex": 1, "label": "presign", "status": "SUCCESS", "txHash": "0x…", "legs": [] } ] } ``` | `suw` field | Meaning | | - | - | | `singleUseAddress` | Ephemeral wallet executing the async settlement | | `phase` | High-level lifecycle — see table below | | `fundsReturnedToWallet` | Final transfer to the user wallet completed | | `order` | Settlement order metadata; `null` if not created yet | | `balances` | Tokens on the single-use wallet during settlement; `null` after funds returned | | `timing` | Request age, SLA, refund countdown | | `pricing` | Reference vs live market price; `null` if unavailable | | `suwSteps` | Internal settlement substeps (`presign`, `swap_and_settlement`, `forward_to_wallet`) | **`suw.phase` lifecycle** | Phase | UI meaning | | - | - | | `awaiting_transfer` | User has not completed the funding transfer | | `awaiting_presign` | Funds received; Pods preparing settlement | | `order_in_progress` | Settlement order open, awaiting fill | | `awaiting_forward` | Fill complete; final transfer to user wallet pending | | `completed` | Fully settled | | `refunded` / `expired` / `failed` / `cancelled` | Terminal — drops out of `actions[]` on next poll | **`suw.order` shapes** All orders share a core, and `posted` decides the rest of the shape: ```jsonc theme={null} { "orderUid": "0x…", "posted": true, "quoteId": "…", "action": "request-lend", "sellAmount": "…", "buyAmount": "…", "status": "…" } ``` **Not posted yet** (`posted: false`) — shows `lastPostError`, `lastPostAttemptAt`, `canOpenOrder`, `code`, `message`, `retryable`: ```jsonc theme={null} { "orderUid": "0x…", "posted": false, "quoteId": "…", "action": "request-lend", "sellAmount": "…", "buyAmount": "…", "status": "not_posted", "lastPostError": null, "lastPostAttemptAt": null, "canOpenOrder": true, "code": null, "message": null, "retryable": true } ``` **Posted / live** (`posted: true`) — shows `validTo`, `creationDate`, `executionDate`, `executedSellAmount`, `executedBuyAmount`, `txHash`: ```jsonc theme={null} { "orderUid": "0x…", "posted": true, "quoteId": "…", "action": "request-lend", "sellAmount": "10000000000000000000", "buyAmount": "…", "status": "open", "validTo": 1710000000, "creationDate": "…", "executionDate": null, "executedSellAmount": null, "executedBuyAmount": null, "txHash": null } ``` Non-Ondo strategies return the same root envelope without `orderBook` or `suw`. #### Empty pending example ```jsonc theme={null} { "strategyId": "Ondo-AAPL-bsc", "wallet": "0x…", "hasPending": false, "orderBook": { "requestLend": { "canOpenOrder": true, "code": null, "errorType": null, "message": null, "retryable": false, "probeSellAmount": "100000000000000000000" }, "requestWithdraw": { "canOpenOrder": true, "code": null, "errorType": null, "message": null, "retryable": false, "probeSellAmount": "1000000000000000000" } }, "message": "No pending actions for this strategy. You are all caught up.", "actions": [] } ``` Suggested polling: every 5–15 s while `hasPending` is `true`, or until `suw.phase` reaches a terminal value. Example: ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/Ondo-AAPL-bsc/status?wallet=0xYOUR_WALLET" ``` ### WebSocket (recommended) ``` wss://api.pods.finance/updates ``` ```json theme={null} { "type": "subscribe", "channel": "" } ``` Listen for `action_update` events where `action.id` matches the bytecode response `id`: | Status | Meaning | | - | - | | `PENDING` | Awaiting settlement | | `SUCCESS` | Order filled | | `FAILED` | Order failed | | `REFUNDED` | Order refunded | Re-subscribe on reconnect. Alternatively, use your **customer webhook** (`ACTION_UPDATE` events). ### History Wallet-wide history is embedded in `GET /v2/wallets/:address?include=all`. For this strategy's position and history, use: ```http theme={null} GET /v2/strategies/:strategyId?wallet=
``` Market-share buys appear as `SAMECHAIN_INVESTMENT_DEPOSIT`; sells as `SAMECHAIN_INVESTMENT_WITHDRAW`. Ignore `INITIAL` status (quote artifact). ### Refresh positions Refetch wallet/strategy endpoints after on-chain confirmation **and** after WebSocket `SUCCESS`. *** ## 8. Supported chains & USDC addresses Stocks & ETFs settle on BSC; funding (buy) and payout (sell) can use any supported USDC chain. | Network | Chain ID | USDC decimals | USDC address | | - | - | - | - | | BSC | `56` | **18** | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | | Ethereum | `1` | 6 | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | Base | `8453` | 6 | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Arbitrum | `42161` | 6 | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | | Polygon | `137` | 6 | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | When `fromChainId` / `toChainId` are set, **`fromTokenAddress` / `toTokenAddress` are required** — the API does not default to USDC. Omission returns **422** `VALIDATION_ERROR`. A non-USDC funding or payout token may add an extra same-chain swap leg. *** ## 9. Errors | Code | HTTP | When | | - | - | - | | `ONDO_MARKET_CLOSED` | 409 | Market closed (off-hours) — buy/sell blocked | | `ONDO_MARKET_PAUSED` | 409 | Market paused by Ondo | | `ONDO_ASSET_PAUSED` | 409 | This ticker is paused (`ASSET_PAUSED`) | | `REQUEST_LEND_AMOUNT_TOO_LOW` | 422 | Buy below \$10 USDC | | `REQUEST_LEND_AMOUNT_TOO_HIGH` | 422 | Buy above the allowed max | | `REQUEST_WITHDRAW_AMOUNT_TOO_HIGH` | 422 | Sell more than available shares | | `LEND_AMOUNT_TOO_SMALL` | 422 | Amount too small for quote | | `WITHDRAW_AMOUNT_TOO_SMALL` | 422 | Sell amount too small | | `WITHDRAW_AMOUNT_REQUIRED` | 400 | Missing `amount`/`amountInShares` | | `FROM_TOKEN_NOT_SUPPORTED` | 400 | Funding token not supported | | `FROM_TOKEN_REQUIRED` | 400 | Crosschain buy without funding token | | `TO_TOKEN_REQUIRED` | 400 | Crosschain sell without payout token | | `CROSSCHAIN_BRIDGE_NO_OUTPUT` | 422 | Bridge returned no USDC for the buy | | `USDC_NOT_SUPPORTED` | 400 | USDC not configured for the chain | | `UNSUPPORTED_CHAIN` | 400 | Unknown `fromChainId`/`toChainId` | | `QUOTE_NOT_FOUND` | 404 | Re-quote | | `QUOTE_EXPIRED` | 400 | Re-quote | | `BRIDGE_QUOTE_NOT_FOUND` | 404 | Crosschain bridge quote expired — re-quote | | `VALIDATION_ERROR` | 422 | Missing `fromTokenAddress`/`toTokenAddress` when chain ID set; other schema failures | | `INVALID_PRICE_IN_USD` | 400 | `priceInUsd` is not a positive integer in USD cents | | `INVALID_EXPIRE_AT` | 400 | `expireAt` is not future UTC seconds, is milliseconds, or exceeds 168h | | `PRICE_IN_USD_NOT_SUPPORTED` | 400 | `priceInUsd` on a strategy that is not Ondo BSC CoW | | `EXPIRE_AT_NOT_SUPPORTED` | 400 | `expireAt` on a strategy that is not Ondo BSC CoW | | `ACTION_CANCEL_WALLET_MISMATCH` | 403 | Cancel `wallet` does not match the action | | `ACTION_CANCEL_NOT_SUPPORTED` | 409 | Cancel on a non-client-limit action | | `ACTION_CANCEL_NOT_OPEN` | 409 | Cancel on a terminal action | | `ACTION_CANCEL_NOT_FUNDED` | 409 | Cancel before the funding transfer confirms | | `ACTION_ALREADY_FILLED` | 409 | Cancel after (or racing) a CoW fill | | `ACTION_CANCEL_REFUND_QUEUE_FAILED` | 502 | Cancel recorded but refund outbox could not be queued | | `NO_API_KEY` / `INVALID_API_KEY` | 401 | Auth failure | Preserve `error.code` in your UI for user-friendly messages. *** ## 10. Integration notes 1. **Use the correct actions:** `buy` or `request-lend`, and `sell` or `request-withdraw`. Do not use the generic `lend`/`withdraw`. 2. **Sign on the right chain:** buy = funding chain (`chainIdIn`); sell = always BSC; position = BSC wallet. 3. **Atomic bytecode:** never send `bytecode[]` legs as separate transactions — see [Executing Bytecode](/getting-started/executing-bytecode). 4. **Async completion:** after the origin tx, Pods completes bridge (if crosschain) + order fill; monitor via the action status, position, or `history`. 5. **Sells use shares:** pass `amountInShares` (18 decimals), not `amount`. Gate sells on wallet position (`currentPositionInShares`) and `suw.phase !== 'awaiting_forward'`. 6. **Wallet output:** EOA → `output=bytecode` + [EIP-7702](/examples/javascript/using-eip7702); smart account → `output=userOperation`; Fireblocks → `output=fireblocks` + `accountId`. 7. **Same EVM wallet** works across funding/payout chains; the market-share position always lives on BSC. 8. **Limit vs market:** omit both params for a market CoW (100 bps + SLA `validTo`). Pass `priceInUsd` for a client limit (10 bps). `expireAt` only changes `validTo` and does **not** make the order cancellable — cancel requires `priceInUsd`. *** ## Minimal example **Buy \$10 of AAPL from Base (crosschain):** ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/Ondo-AAPL-bsc/bytecode\ ?action=request-lend&amount=10000000&wallet=0xYOUR_WALLET\ &fromChainId=8453&fromTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" ``` **Buy with a \$320.00 client limit** (cents + optional expiry): ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/Ondo-AAPL-bsc/bytecode\ ?action=request-lend&amount=10000000&wallet=0xYOUR_WALLET\ &fromChainId=8453&fromTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\ &priceInUsd=32000&expireAt=1778000000" ``` Cancel that order after funding, if it is still unfilled: ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/actions//cancel?wallet=0xYOUR_WALLET" ``` Sign `response.bytecode` (legs where `Number(chainId) === 8453`) on Base, then poll: ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/v2/strategies/Ondo-AAPL-bsc?wallet=0xYOUR_WALLET" ``` **Sell shares, receive USDC on Arbitrum (crosschain):** ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/Ondo-AAPL-bsc/bytecode\ ?action=request-withdraw&amountInShares=1000000000000000000&wallet=0xYOUR_WALLET\ &toChainId=42161&toTokenAddress=0xaf88d065e77c8cC2239327C5EDb3A432268e5831" ``` Sign `response.bytecode` (legs where `Number(chainId) === 56`) on BSC, then poll the position until the pending arrays clear. *** ## Endpoint reference | Concern | Method & Path | | - | - | | Market open / symbol tradable? | `GET /ondo/stocks/market-status` (`?symbol=` optional) | | List stocks, ETFs, and portfolios | `GET /tokens?category=stock,etf,portfolio&chainId=56` | | List strategies | `GET /strategies?protocol=Ondo&network=bsc&category=stock,etf,portfolio` | | Strategy + position | `GET /v2/strategies/:id?wallet=` | | Position per wallet | `GET /v2/wallets/:addr/strategies/:id` | | Quote + bytecode | `GET /strategies/:id/bytecode` | | Cancel client-limit | `GET /actions/:id/cancel?wallet=` | | Pending actions | `GET /strategies/:id/status?wallet=` | | Wallet snapshot | `GET /v2/wallets/:addr?include=all` | | Wallet activity | `GET /actions?walletAddress=` | | Status updates | `wss:///updates` | All HTTP endpoints require `x-api-key`. # Pods Ramp Overview Source: https://docs.pods.finance/guides/ramp/overview How Pods Ramp works end to end: KYC, fiat-to-crypto money movement, and status tracking Pods Ramp lets your application own the user experience while Pods handles local KYC/AML integration and fiat-to-crypto money movement. A user completes reusable KYC once, then moves money across Pix and foreign USD rails through a single set of Swap v2 quote routes. Pods Ramp is live today for Brazil (Pix) and foreign USD (WIRE/ACH), and is expanding to more countries in Latin America. Onramp, offramp, and ramp-to-yield all run through `GET /v2/swap/quote` — `originChain`/`destinationChain` select the rail and `tokenIn`/`tokenOut` select the currency or on-chain asset. ## Introduction | Item | Value | | - | - | | **Countries / rails** | Brazil Pix (live) and foreign USD WIRE/ACH (live); more LatAm rails expanding | | **Routes** | Onramp (fiat → crypto), offramp (crypto → fiat), ramp-to-yield (Pix → Morpho hyperUSDCa on Monad) | | **Pix pairs** | USDC, BRLA, BRS, BRZ, and hyperUSDCa (Monad) against Pix BRL | | **Foreign USD pairs** | USD WIRE/ACH ↔ USDC on `ethereum`, `base`, `polygon` | | **KYC submit** | `POST /api/v1/kyc/sumsub-share-token` | | **Quote endpoint** | `GET /v2/swap/quote` (onramp, offramp, and yield) | | **Status endpoints** | `GET /api/v1/kyc/status` (approval) · `GET /v2/swap/status/{quoteId}` (money movement) | | **Limits endpoint** | `GET /api/v1/limits` (remaining on-ramp/off-ramp allowance) | | **Auth** | Header `x-api-key: ` | ## Authentication Every request requires: ``` x-api-key: ``` * The key identifies your **customer**. Fee configuration and strategy catalog are scoped to it. * Success responses are the bare JSON body (HTTP 200). Errors use `{ "error": { "code", "message", "details?" } }`. * Keep the API key on your **backend** — never expose it in a browser app. Don't have a key yet? [Get an API key](https://www.pods.finance/plg/select-plan). ## Integration flow Add the local KYC/AML provider as a reusable identity recipient in Sumsub and configure the `pods-ramp-level` verification level. Your user completes identity verification through Sumsub WebSDK in your app. Full guide: [Set up Pods KYC](/guides/kyc/setup). After Sumsub returns `completed + GREEN`, your backend generates a fresh Sumsub share token and submits it to Pods with the user's tax id, applicant id, email, and wallet address: ```http theme={null} POST /api/v1/kyc/sumsub-share-token ``` The response returns a `kycUserId` — store it for status checks. Full guide: [Set up Pods KYC](/guides/kyc/setup). Local provider validation is asynchronous. Poll until the Ramp profile reaches `approved`, which enables money movement: ```http theme={null} GET /api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000 ``` Full guide: [Track Pods Ramp status](/guides/ramp/status). Before requesting a quote, check how much of the customer's on-ramp and off-ramp allowance remains for the requested currency: ```http theme={null} GET /api/v1/limits?currency=BRL&originAddress=0x1234... ``` The response reports, for each direction, the remaining amount in the requested currency and the remaining global USD cap, plus a `blocked` flag if the KYC provider has suspended the account. The top-level `remaining` and `remainingGlobalUsd` fields are deprecated aliases for the off-ramp values — read `onramp`/`offramp` directly instead. If no ramp provider applies to this customer, the response is `{ "provider": null }`. Once approved, request an onramp, offramp, or ramp-to-yield quote. `originChain`/`destinationChain` select the rail; `tokenIn`/`tokenOut` select the currency or asset: ```http theme={null} GET /v2/swap/quote ``` The response includes payment instructions (Pix copy-paste or bank details) on onramps, or `transactionData` to sign on offramps. Full guide: [Move money with Pods Ramp](/guides/ramp/quotes). The user pays via the local rail (onramp has no on-chain transaction to sign), or your app signs the returned `transactionData` from the approved wallet (offramp): ```http theme={null} GET /v2/swap/quote ``` Full guide: [Move money with Pods Ramp](/guides/ramp/quotes). Track the onramp, offramp, or yield flow to `fulfilled` with the quote's `quoteId`: ```http theme={null} GET /v2/swap/status/{quoteId} ``` Full guide: [Track Pods Ramp status](/guides/ramp/status). # Move Money with Pods Ramp Source: https://docs.pods.finance/guides/ramp/quotes Onramp, offramp, and yield quotes for Pods Ramp Money movement routes are available only after Ramp status is `approved`. See [Set up Pods KYC](/guides/kyc/setup) to get a user to that state, and [Track Pods Ramp status](/guides/ramp/status) to poll both KYC approval and quote status. Pods Ramp uses the same Swap v2 quote routes across every supported country: `originChain`/`destinationChain` of `fiat` select the local payment rail, and `tokenIn`/`tokenOut` select the fiat currency or on-chain asset. Brazil Pix and foreign USD (WIRE/ACH) are live today; Mexico, Colombia, and Argentina will use their own local rail and currency through the same routes once live. Send all requests from your backend with `x-api-key`; never expose `PODS_API_KEY` to a browser or mobile client. ## Token addresses | Asset | Network | Address | | - | - | - | | USDC | Ethereum (`1`) | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | USDC | Base (`8453`) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | USDC | Polygon (`137`) | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | | USDC | Monad (`143`) | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603` | | BRLA | Polygon (`137`) | `0xE6A537a407488807F0bbeb0038B79004f19DDDFb` | | BRS (Nora) | Solana (`0`) | `BRSxQRUaGswjLs7ewcH7uXj3r7SgmfKSSLLXyCKHZtUo` | | BRZ (Transfero) | Polygon (`137`) | `0x4eD141110F6EeeAbA9A1df36d8c26f684d2475Dc` | | hyperUSDCa (Morpho) | Monad (`143`) | `0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371` | ## Supported Brazil Pix routes ### KYC — Quick Summary Before requesting Pix quotes, ensure the user has an approved Ramp KYC profile and currency unlocked: * **Obtain partner token:** The server exchanges Sumsub responses for a short-lived `PROVIDER_PARTNER_TOKEN` via the share-token endpoint; keep it server-side only. * **Submit share token / start review:** Use the Sumsub share token flow to submit identity documents (see the KYC setup guide for integration details). * **Poll status:** Check `GET /api/v1/kyc/status` (or your backend status endpoint) until the profile reaches `approved`. * **Unlock currency:** Call the currency-unlock endpoint (`POST /api/v1/kyc/currency-unlock`) to enable onramp/offramp quotes for the approved profile and wallet. * **Sumsub app client id:** `PROVIDER_FOR_CLIENT_ID` (Sumsub `forClientId`) comes from your Sumsub application settings — use it in client integrations as documented in the KYC setup guide. Notes: share tokens expire quickly and must be handled server-side; the wallet `destinationAddress` used in Pix onramps must match an `approved` Ramp KYC profile for that wallet. Full KYC guide: [KYC setup guide](/guides/kyc/setup) | Direction | Public pair | Chains (v1) | Fees | | - | - | - | - | | Onramp | Pix BRL → **USDC** | `base`, `monad` | Pix rail + Pods/customer swap fees (when configured) + bridge/slippage as shown in `feeBreakdown` | | Offramp | **USDC** → Pix BRL | `base`, `monad` | Same commercial model as USDC onramp | | Onramp | Pix BRL → **BRLA** | `polygon` only | **No Pods/customer markup** — amounts pass through Avenia; optional Avenia Pix fee as `paymentRail` | | Offramp | **BRLA** → Pix BRL | `polygon` only | **No Pods/customer markup** — amounts pass through Avenia; optional Avenia Pix fee as `paymentRail` | | Onramp | Pix BRL → **BRS** | `solana` | **No Pods/customer markup** — 1:1 BRLA inventory after decimal scale; optional Avenia Pix fee as `paymentRail` | | Offramp | **BRS** → Pix BRL | `solana` | **No Pods/customer markup** — 1:1 BRLA inventory after decimal scale; optional Avenia Pix fee as `paymentRail` | | Onramp | Pix BRL → **BRZ** | `polygon` | **No Pods/customer markup** — 1:1 BRLA inventory (both 18 decimals); optional Avenia Pix fee as `paymentRail` | | Offramp | **BRZ** → Pix BRL | `polygon` | **No Pods/customer markup** — 1:1 BRLA inventory (both 18 decimals); optional Avenia Pix fee as `paymentRail` | | Onramp / offramp | Pix BRL ↔ **Morpho hyperUSDCa (Monad)** | `monad` | Shown in `feeBreakdown` on the quote | ### Onramp: Pix BRL to USDC Use this when a verified user pays Pix and receives USDC on Base or Monad. When you include `destinationAddress`, the quote response includes payment instructions directly. ```http theme={null} GET /v2/swap/quote ``` Example (Brazil, Pix BRL to USDC on Base): ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=fiat&destinationChain=base&tokenIn=BRL&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amountIn=1000&destinationAddress=0x0000000000000000000000000000000000000001" ``` For Monad, change `destinationChain` to `monad` and `tokenOut` to Monad USDC from the table above. | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `base` or `monad` | | `tokenIn` | Yes | `BRL` | | `tokenOut` | Yes | USDC contract on the destination chain | | `amountIn` | Yes | Fiat amount in the currency's smallest unit. Example: `1000` is BRL 10.00. | | `destinationAddress` | Yes | User wallet address that will receive USDC. Must match an `approved` Ramp KYC profile for that wallet. | Successful response: ```json theme={null} { "quote": { "quoteId": "9be08433-3338-4881-9517-31faa22489c9", "originChain": "fiat", "destinationChain": "base", "tokenIn": { "symbol": "BRL", "decimals": 2, "amount": "1000" }, "tokenOut": { "contract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6, "expectedAmountOut": "1769602", "chainId": 8453 }, "status": "pending", "deadlineDate": "2026-06-25T15:38:38.670Z", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "Pix fee", "amountRaw": "77", "decimals": 2, "symbol": "BRL", "asset": "tokenIn" }, { "kind": "bridgeAndSlippage", "label": "Bridge fee + slippage", "amountRaw": "8848", "decimals": 6, "symbol": "USDC", "asset": "tokenOut" } ] } }, "paymentInstructions": { "method": "PIX", "amount": { "currency": "BRL", "amountRaw": "1000", "decimals": 2 }, "expiresAt": "2026-06-25T15:43:39.433Z", "pix": { "copyPaste": "000201010212..." } }, "chainId": null, "transactionData": [], "id": "65f1a2b3c4d5e6f789012345" } ``` Show `paymentInstructions` to the user in the shape of the local rail -- for Pix, `paymentInstructions.pix.copyPaste` is the copy-paste code. Use `quote.feeBreakdown` to display payment-rail, Pods/customer, and bridge or slippage charges. There is no EVM transaction to sign on onramp (`transactionData` is empty; `chainId` is `null`). Poll `GET /v2/swap/status/{quote.quoteId}` until `fulfilled`. ### Offramp: USDC to Pix BRL Use this when a verified user sends USDC on-chain and receives Pix. The user must have an `approved` Ramp profile for the **same** `originAddress`, and that wallet must belong to your customer on Pods. Pass `originAddress` and `pixKey` on the quote — the response includes `transactionData` ready to sign. Sign on the selected origin chain (`base` → `8453`, `monad` → `143`). For crypto-to-BRL PIX offramps, an explicitly supplied `pixKey` on `GET /v2/swap/quote` or `POST /v2/swap/bytecode` is trimmed and checked with Avenia. Invalid or unknown keys return `400 INVALID_PIX_KEY`. Empty keys are rejected; existing request-schema checks may return `422 VALIDATION_ERROR` first when required destination inputs are missing. The bytecode endpoint uses the saved quote to identify the rail. Keys supplied only through `destinationAddress` or saved quote data do not trigger this new check; onramps, USD offramps, and crypto swaps are outside its scope. Pricing-only quotes can continue when the ordinary-key lookup is unavailable. A quote with both an origin wallet and a destination can generate funding instructions, even with the default `output=bytecode`, and follows the execution policy: an explicit Avenia HTTP 5xx response allows the request to continue; timeouts, authentication/rate-limit errors, and incomplete responses return `502 PIX_KEY_LOOKUP_FAILED`. This fallback does not bypass existing KYC, ownership, third-party, or payout checks, and does not guarantee the pricing or payout APIs are available. Key existence does not prove the recipient is the intended person. BR Codes retain their existing format, checksum, metadata, and expiration checks; the ordinary-key outage fallback does not apply to BR Codes. A successful lookup confirms that the key resolves to a recipient. It does not confirm that this is the recipient the user intended; `thirdParty` and the existing payout rules still apply. Recipient lookup details are not added to the swap response. ```http theme={null} GET /v2/swap/quote ``` Example (Brazil, USDC on Base to Pix BRL): ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=base&destinationChain=fiat&tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=BRL&amountIn=1000000&originAddress=0x0000000000000000000000000000000000000001&pixKey=user-pix-key@example.com" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `base` or `monad` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | USDC contract on the origin chain | | `tokenOut` | Yes | `BRL` | | `amountIn` | Yes | USDC amount in smallest units. Example: `1000000` is 1.00 USDC. | | `originAddress` | Yes | Approved wallet address that will send USDC. | | `pixKey` | Yes | Pix key that will receive the BRL payout. | Successful response: ```json theme={null} { "quote": { "quoteId": "1ed3ff63-58b8-4eb4-9152-723bbdfec724", "originChain": "base", "destinationChain": "fiat", "tokenIn": { "contract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6, "amount": "1000000", "chainId": 8453 }, "tokenOut": { "contract": "BRL", "symbol": "BRL", "decimals": 2, "amount": "520", "expectedAmountOut": "520", "chainId": 0 }, "provider": "deframe", "status": "pending", "deadlineDate": "2026-06-29T22:21:12.230Z", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "Pix fee", "amountRaw": "20", "decimals": 2, "symbol": "BRL", "asset": "tokenOut" } ] } }, "chainId": 8453, "depositAddress": "0x4444444444444444444444444444444444444444", "transactionData": [ { "chainId": "8453", "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x...", "value": "0" }, { "chainId": "8453", "to": "0x...", "data": "0x...", "value": "0" } ], "id": "65f1a2b3c4d5e6f789012345" } ``` Execute every transaction in `transactionData`, in order, from `originAddress` on the selected origin chain (batch same-chain legs into one atomic tx when your wallet stack supports it — see [Execute a swap](/guides/swaps/execute-swap)). The first transaction transfers USDC to the returned `depositAddress`; the next transaction records the transfer for tracking. After Pods detects and collects the USDC payment, Pods creates the local-rail payout to `pixKey`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Onramp: Pix BRL to BRLA (Polygon) Use this when a verified user pays Pix and receives **BRLA on Polygon** directly in their wallet. Avenia mints BRLA to `destinationAddress` — there is no Deframe inventory hop and **no Pods/customer swap markup**. Quote amounts mirror Avenia's fixed-rate response (Avenia rail fees may still appear as `paymentRail` in `feeBreakdown`). Prefer this rail when the product needs BRLA itself. Prefer Pix→USDC when the product needs USDC on Base or Monad. Do not call Avenia's APIs directly for either public pair — use `GET /v2/swap/quote`. ```http theme={null} GET /v2/swap/quote ``` Example (Brazil, Pix BRL to BRLA on Polygon): ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=fiat&destinationChain=polygon&tokenIn=BRL&tokenOut=0xE6A537a407488807F0bbeb0038B79004f19DDDFb&amountIn=10000&destinationAddress=0x0000000000000000000000000000000000000001" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `polygon` (v1) | | `tokenIn` | Yes | `BRL` | | `tokenOut` | Yes | Polygon BRLA: `0xE6A537a407488807F0bbeb0038B79004f19DDDFb` | | `amountIn` | Yes | Fiat amount in smallest units. Example: `10000` is BRL 100.00. | | `destinationAddress` | Yes | User wallet that will receive BRLA on Polygon. Must match an `approved` Ramp KYC profile for that wallet. | Successful response shape matches other onramps: `paymentInstructions.pix.copyPaste` for the user to pay, `quote.tokenOut` with BRLA (`decimals` 18, `chainId` 137), `feeBreakdown` without Pods/customer platform charges, empty `transactionData`, and `chainId: null`. After Pix settles, Avenia mints BRLA to `destinationAddress`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Offramp: BRLA (Polygon) to Pix BRL Use this when a verified user sends **BRLA on Polygon** and receives Pix. Amounts pass through Avenia with no Pods/customer markup. Pass `originAddress` and `pixKey` on the quote; the response includes `transactionData` (BRLA transfer + track) for Polygon (`chainId` 137). ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=polygon&destinationChain=fiat&tokenIn=0xE6A537a407488807F0bbeb0038B79004f19DDDFb&tokenOut=BRL&amountIn=98500000000000000000&originAddress=0x0000000000000000000000000000000000000001&pixKey=user-pix-key@example.com" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `polygon` (v1) | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | Polygon BRLA: `0xE6A537a407488807F0bbeb0038B79004f19DDDFb` | | `tokenOut` | Yes | `BRL` | | `amountIn` | Yes | BRLA amount in smallest units (18 decimals). | | `originAddress` | Yes | Approved wallet that will send BRLA on Polygon. | | `pixKey` | Yes | Pix key that will receive the BRL payout. | Execute `transactionData` from `originAddress` on Polygon (`chainId` 137). After Pods validates the transfer, Avenia pays Pix to `pixKey`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Onramp: Pix BRL to BRS (Solana) Use this when a verified user pays Pix and receives **BRS on Solana**. Pricing is 1:1 after decimal scale (BRLA 18 decimals ↔ BRS 6 decimals). There is **no Pods/customer swap markup**. The user never holds or sends BRLA — Pods settles that inventory internally. The Ramp KYC `walletAddress` for this pair must be the user's **Solana** public key, and `destinationAddress` must match it. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=fiat&destinationChain=solana&tokenIn=BRL&tokenOut=BRSxQRUaGswjLs7ewcH7uXj3r7SgmfKSSLLXyCKHZtUo&amountIn=10000&destinationAddress=EXYhCNamLPFfSMekSn7Lzc2cP32aZpRa7ok5gErdJzcj" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `solana` | | `tokenIn` | Yes | `BRL` | | `tokenOut` | Yes | Solana BRS mint: `BRSxQRUaGswjLs7ewcH7uXj3r7SgmfKSSLLXyCKHZtUo` | | `amountIn` | Yes | Fiat amount in smallest units. Example: `10000` is BRL 100.00. | | `destinationAddress` | Yes | User Solana wallet that will receive BRS. Must match an `approved` Ramp KYC profile for that wallet. | Successful response shape matches other Pix onramps: `paymentInstructions.pix.copyPaste` for the user to pay, `quote.tokenOut` with BRS (`decimals` 6, `chainId` 0), `feeBreakdown` without Pods/customer platform charges, empty `transactionData`, and `chainId: null`. After Pix settles, Pods sends BRS to `destinationAddress`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Offramp: BRS (Solana) to Pix BRL Use this when a verified user sends **BRS on Solana** and receives Pix. Amounts pass through 1:1 inventory with no Pods/customer markup. Pass `originAddress` (the approved Solana wallet) and `pixKey` on the quote. The origin payload is Solana, not an EVM transaction array. Default `output=bytecode` returns `type: intra-chain-solana` with `transactionData.rawTransaction` (base64). That transaction transfers BRS to the Pods inventory ATA and includes Memo V1 `{ version: 1, actionId }` where `actionId` is the **quote UUID** (`quote.quoteId`), not the Action ObjectId. Sign and submit that transaction from `originAddress`. Solana wallets that assemble instructions themselves can request `output=instructions` (Solana-origin only). EVM and fiat origins reject `instructions` with `OUTPUT_NOT_SUPPORTED`. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=solana&destinationChain=fiat&tokenIn=BRSxQRUaGswjLs7ewcH7uXj3r7SgmfKSSLLXyCKHZtUo&tokenOut=BRL&amountIn=98500000&originAddress=EXYhCNamLPFfSMekSn7Lzc2cP32aZpRa7ok5gErdJzcj&pixKey=user-pix-key@example.com" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `solana` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | Solana BRS mint: `BRSxQRUaGswjLs7ewcH7uXj3r7SgmfKSSLLXyCKHZtUo` | | `tokenOut` | Yes | `BRL` | | `amountIn` | Yes | BRS amount in smallest units (6 decimals). Example: `98500000` is 98.50 BRS. | | `originAddress` | Yes | Approved Solana wallet that will send BRS. | | `pixKey` | Yes | Pix key that will receive the BRL payout. | | `output` | No | `bytecode` (default, base64 `rawTransaction`) or `instructions` (Solana-origin only). | Sign `transactionData.rawTransaction` from `originAddress` on Solana. After Pods confirms the inbound BRS transfer (Memo V1 `quoteId`), Avenia pays Pix to `pixKey`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Onramp: Pix BRL to BRZ (Polygon) Use this when a verified user pays Pix and receives **BRZ on Polygon**. Pricing is 1:1 (BRLA 18 decimals ↔ BRZ 18 decimals). There is **no Pods/customer swap markup**. The user never holds or sends BRLA — Pods settles that inventory internally. The Ramp KYC `walletAddress` for this pair must be the user's **Polygon** address, and `destinationAddress` must match it. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=fiat&destinationChain=polygon&tokenIn=BRL&tokenOut=0x4eD141110F6EeeAbA9A1df36d8c26f684d2475Dc&amountIn=10000&destinationAddress=0x0000000000000000000000000000000000000001" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `polygon` | | `tokenIn` | Yes | `BRL` | | `tokenOut` | Yes | Polygon BRZ: `0x4eD141110F6EeeAbA9A1df36d8c26f684d2475Dc` | | `amountIn` | Yes | Fiat amount in smallest units. Example: `10000` is BRL 100.00. | | `destinationAddress` | Yes | User Polygon wallet that will receive BRZ. Must match an `approved` Ramp KYC profile for that wallet. | Successful response shape matches other Pix onramps: `paymentInstructions.pix.copyPaste` for the user to pay, `quote.tokenOut` with BRZ (`decimals` 18, `chainId` 137), `feeBreakdown` without Pods/customer platform charges, empty `transactionData`, and `chainId: null`. After Pix settles, Pods sends BRZ to `destinationAddress`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Offramp: BRZ (Polygon) to Pix BRL Use this when a verified user sends **BRZ on Polygon** and receives Pix. Amounts pass through 1:1 inventory with no Pods/customer markup. Pass `originAddress` (the approved Polygon wallet) and `pixKey` on the quote. BRZ has no EIP-2612 permit. The origin payload is an EVM transaction array: an ERC-20 `transfer` of BRZ to the Pods bridge address plus a Deframe Track event whose extra data is version `0`, action `2`, and `quote.quoteId`. A transfer to the bridge without that matching Track does not confirm the quote. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=polygon&destinationChain=fiat&tokenIn=0x4eD141110F6EeeAbA9A1df36d8c26f684d2475Dc&tokenOut=BRL&amountIn=98500000000000000000&originAddress=0x0000000000000000000000000000000000000001&pixKey=user-pix-key@example.com" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `polygon` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | Polygon BRZ: `0x4eD141110F6EeeAbA9A1df36d8c26f684d2475Dc` | | `tokenOut` | Yes | `BRL` | | `amountIn` | Yes | BRZ amount in smallest units (18 decimals). Example: `98500000000000000000` is 98.50 BRZ. | | `originAddress` | Yes | Approved Polygon wallet that will send BRZ. | | `pixKey` | Yes | Pix key that will receive the BRL payout. | Execute `transactionData` from `originAddress` on Polygon (`chainId` 137). After Pods confirms the inbound BRZ transfer plus matching Track, Avenia pays Pix to `pixKey`. Track with `GET /v2/swap/status/{quote.quoteId}`. ### Onramp to yield: Pix BRL to Morpho hyperUSDCa (Monad) Deposit Pix BRL directly into Morpho Hyperithm USDC Apex on Monad. The flow matches the plain USDC onramp — only `tokenOut` is the vault share token (`hyperUSDCa`) instead of USDC. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=fiat&destinationChain=monad&tokenIn=BRL&tokenOut=0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371&amountIn=1000&destinationAddress=0x0000000000000000000000000000000000000001" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `monad` | | `tokenIn` | Yes | `BRL` | | `tokenOut` | Yes | Morpho hyperUSDCa on Monad: `0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371` | | `amountIn` | Yes | Fiat amount in the currency's smallest unit. | | `destinationAddress` | Yes | User wallet that will receive hyperUSDCa shares. | Successful response: ```json theme={null} { "quote": { "quoteId": "9be08433-3338-4881-9517-31faa22489c9", "originChain": "fiat", "destinationChain": "monad", "tokenIn": { "symbol": "BRL", "decimals": 2, "amount": "1000" }, "tokenOut": { "contract": "0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371", "symbol": "hyperUSDCa", "decimals": 18, "expectedAmountOut": "...", "chainId": 143 }, "status": "pending", "deadlineDate": "2026-06-25T15:38:38.670Z", "feeBreakdown": { "charges": [] } }, "paymentInstructions": { "method": "PIX", "amount": { "currency": "BRL", "amountRaw": "1000", "decimals": 2 }, "expiresAt": "2026-06-25T15:43:39.433Z", "pix": { "copyPaste": "000201010212..." } }, "id": "9be08433-3338-4881-9517-31faa22489c9" } ``` `quote.tokenOut.decimals` is the vault share decimals (`18`). `expectedAmountOut` is a raw **share** amount of hyperUSDCa, not USDC. Store it so you can redeem the exact deposited shares later on the offramp. There is no on-chain transaction for the user to sign on this side. After Pix settles, Pods converts to USDC on Monad, mints hyperUSDCa, and sends the shares to `destinationAddress`. ### Offramp from yield: Morpho hyperUSDCa (Monad) to Pix BRL Redeem Morpho hyperUSDCa shares and receive Pix. `amountIn` must be a raw share amount of hyperUSDCa — not a USDC amount and not a `principal` / `currentPosition` value from the wallet endpoint (see [Check yield position and profit](#check-yield-position-and-profit) below). Do not pass a USDC- or BRL-denominated position value as `amountIn`. Use the wallet's on-chain hyperUSDCa share balance, or the share amount stored from the onramp quote above. ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=monad&destinationChain=fiat&tokenIn=0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371&tokenOut=BRL&amountIn=1000000000000000000&originAddress=0x0000000000000000000000000000000000000001&pixKey=user-pix-key@example.com" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `monad` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | Morpho hyperUSDCa on Monad: `0x78999cc96d2Ba0341588C60CcB0E91c6C33CF371` | | `tokenOut` | Yes | `BRL` | | `amountIn` | Yes | Raw hyperUSDCa share amount to redeem. | | `originAddress` | Yes | Approved wallet that holds the hyperUSDCa position. | | `pixKey` | Yes | Pix key that will receive the BRL payout. | Successful response includes `quote` plus `transactionData` / `chainId` / `id` (same shape as the USDC offramp). Execute every transaction in `transactionData`, in order, from `originAddress` on Monad (`chainId` 143). The first transaction redeems hyperUSDCa to USDC and sends USDC to `depositAddress`; the next records the transfer for tracking. Pods validates the redeem before creating the Pix payout to `pixKey`. ### Check yield position and profit For Morpho hyperUSDCa positions, use `GET /v2/wallets/{wallet}` and find the entry where `strategy` matches Morpho Hyperithm on Monad. `spotPosition.currentPosition`, `principal`, `profit`, and `cumulativeProfit` are reported in **USDC** terms, even though the wallet holds hyperUSDCa shares on-chain. Do not use those values as `amountIn` for a yield-to-Pix quote — use the on-chain share balance or the share amount stored from the onramp quote. If the user wants to exit into USDC on Monad without Pix, use that strategy's withdraw bytecode endpoint (underlying-token amount) — see the yield strategy guides. ## Supported USD routes | Direction | Public pair | Chains | Fees | | - | - | - | - | | Onramp | USD WIRE/ACH → **USDC** | `ethereum`, `base`, `polygon` | Avenia mints USDC directly to the user wallet; no Pods/customer swap markup; ACH/WIRE In Fee is USD and appears as `paymentRail` | | Offramp | **USDC** → USD WIRE/ACH | same EVM chains | Pass-through; bank fields on quote resolve-or-create the Avenia beneficiary. Out Fee is **USDC** (live ACH \~1.01 USDC, WIRE \~10.04 USDC) and appears as `kind: paymentRail` on `tokenIn` | See [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp) for KYC, currency unlock, and quote patterns. ### How to present deposit instructions | Field | UI use | | - | - | | `paymentInstructions.usd.bankName` / `bankAddress` | Bank receiving the ACH/WIRE | | `paymentInstructions.usd.bankRoutingNumber` | ABA routing | | `paymentInstructions.usd.bankAccountNumber` | Account number | | `paymentInstructions.usd.accountType` | e.g. `CHECKING` | | `paymentInstructions.usd.accountHolderName` | Beneficiary name | | `paymentInstructions.usd.depositMessage` | **Required memo / reference** — without it, Avenia cannot match the payment | | `paymentInstructions.amount` | Exact USD to send (`amountRaw` / `10^decimals`) | | `paymentInstructions.expiresAt` | Instruct the user before this time | There is no EVM transaction to sign (`transactionData` is `[]`, `chainId` is `null`). Top-level `id` is the **Action** id; `quote.quoteId` is for `GET /v2/swap/status/{quoteId}`. ### Foreign USD KYC The USD onramp/offramp flows require a foreign KYC profile with `usdEnabled: true`. The full KYC walkthrough lives in [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp); include these backend steps before requesting USD quotes. 1. Submit the Sumsub share token to Pods (after Sumsub `completed + GREEN`): ```bash theme={null} curl -sS -X POST \ -H "x-api-key: $PODS_API_KEY" \ -H "Content-Type: application/json" \ "$PODS_API_BASE_URL/api/v1/kyc/foreign/sumsub-share-token" \ -d '{ "email": "user@example.com", "shareToken": "SUMSUB_SHARE_TOKEN", "sumsubApplicantId": "SUMSUB_APPLICANT_ID", "taxId": "123456789", "taxIdCountry": "USA", "walletAddress": "0x..." }' ``` Response returns `kycUserId` and an initial `status` (often `provider_pending`). Store `kycUserId` for status polling and idempotency. 2. Poll KYC status until approved (Level 1 may still have USD locked): ```bash theme={null} curl -sS -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/api/v1/kyc/status?kycUserId=..." ``` Wait until `status: approved` and then unlock USD. 3. Unlock USD capability for that `kycUserId`: ```bash theme={null} curl -sS -X POST \ -H "x-api-key: $PODS_API_KEY" \ -H "Content-Type: application/json" \ "$PODS_API_BASE_URL/api/v1/kyc/currency-unlock" \ -d '{ "currency": "usd", "kycUserId": "..." }' ``` After a successful submit, `usdEnabled` can remain `false` briefly while the provider finalizes USD KYC — poll the status endpoint until `usdEnabled: true` before requesting USD quotes. Currency-unlock errors you may encounter: | HTTP | `error.code` | When | | - | - | - | | `400` | `AVENIA_CURRENCY_KYC_SUBMIT_FAILED` | Avenia rejected the USD KYC (missing identification document on the subaccount) | | `500` | `INTERNAL_SERVER_ERROR` | Avenia credentials not configured on the API | Keep the detailed foreign KYC guide for onboarding and troubleshooting; this summary ensures `quotes` contains the minimal actionable API steps required before requesting USD onramp/offramp quotes. Choose the pair with `tokenOut` / `tokenIn` (USDC, BRLA, BRS, or BRZ). Pods will not route a Pix→USDC or Pix→BRZ request to the BRLA mint-to-user rail, or the reverse. **Integrate with `GET /v2/swap/quote` only.** Pass `destinationAddress` on onramps (Pix QR or ACH/WIRE deposit instructions in the response). On offramps pass `originAddress` plus `pixKey` (Pix) or the USD bank fields (ACH/WIRE) so `transactionData` is ready to sign. New integrations should not call `POST /v2/swap/bytecode`. The quote response top-level `id` is the **Action** id (use with `GET /actions/{id}`). Track money-movement progress with `GET /v2/swap/status/{quoteId}` using `quote.quoteId` — these are different identifiers. ### Onramp: USD ACH/WIRE to USDC Use this when a foreign user (see [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp)) pays USD via ACH or WIRE and receives USDC on `ethereum`, `base`, or `polygon`. The KYC profile for `destinationAddress` must have `usdEnabled: true`. With `destinationAddress`, the quote response includes bank deposit instructions — no EVM transaction to sign. ```http theme={null} GET /v2/swap/quote ``` Pass **exactly one** of `amountIn` (USD cents to send) or `amountOut` (USDC raw to receive). Both are supported. | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `fiat` | | `destinationChain` | Yes | `ethereum`, `base`, or `polygon` | | `tokenIn` | Yes | `USD` | | `tokenOut` | Yes | USDC contract on the destination chain | | `amountIn` **or** `amountOut` | Yes (exactly one) | `amountIn`: USD cents (`500` = USD 5.00). `amountOut`: USDC raw (`4000000` = 4 USDC). | | `destinationAddress` | Yes | Wallet that receives USDC (unlocked USD KYC). | | `usdPaymentMethod` | No | `WIRE` (default) or `ACH` | #### `amountIn` — exact USD send ```bash theme={null} curl -sS -G \ -H "x-api-key: $PODS_API_KEY" \ --data-urlencode "originChain=fiat" \ --data-urlencode "destinationChain=polygon" \ --data-urlencode "tokenIn=USD" \ --data-urlencode "tokenOut=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" \ --data-urlencode "amountIn=500" \ --data-urlencode "destinationAddress=0xb0b6234dCbB885409eF23fD5FD5E63eE33b1377f" \ --data-urlencode "usdPaymentMethod=ACH" \ "$PODS_API_BASE_URL/v2/swap/quote" ``` Live capture: send **USD 5.00** → receive \~**3.983876 USDC** (ACH fee \~USD 1.02 deducted from the input). ```json theme={null} { "quote": { "quoteId": "06c35a13-aa53-4de0-ae5b-f81c13f10cc8", "originChain": "fiat", "destinationChain": "polygon", "tokenIn": { "symbol": "USD", "decimals": 2, "amount": "500", "amountInUSD": 5 }, "tokenOut": { "contract": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "symbol": "USDC", "decimals": 6, "amount": "3983876", "expectedAmountOut": "3983876", "minAmountOut": "3983876" }, "provider": "deframe", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "ACH fee", "amountRaw": "102", "decimals": 2, "symbol": "USD", "asset": "tokenIn", "amountInUSD": 1.02 } ] }, "chainIdOut": 137 }, "paymentInstructions": { "method": "ACH", "amount": { "currency": "USD", "amountRaw": "500", "decimals": 2 }, "usd": { "bankRoutingNumber": "101019644", "bankAccountNumber": "218939188578", "depositMessage": "BRGEGVRDDCX4JE6ZKEBK", "paymentRail": "ACH" } }, "chainId": null, "transactionData": [], "id": "6a9215c94649656acd52ea13" } ``` #### `amountOut` — exact USDC receive ```bash theme={null} curl -sS -G \ -H "x-api-key: $PODS_API_KEY" \ --data-urlencode "originChain=fiat" \ --data-urlencode "destinationChain=polygon" \ --data-urlencode "tokenIn=USD" \ --data-urlencode "tokenOut=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" \ --data-urlencode "amountOut=4000000" \ --data-urlencode "destinationAddress=0xb0b6234dCbB885409eF23fD5FD5E63eE33b1377f" \ --data-urlencode "usdPaymentMethod=ACH" \ "$PODS_API_BASE_URL/v2/swap/quote" ``` Live capture: receive **exactly 4 USDC** → send **USD 5.02** (`paymentInstructions.amount.amountRaw=502`). Same call with `usdPaymentMethod=WIRE` kept `tokenOut.amount=4000000` and returned \~USD 14.04 to send (WIRE fee \~USD 10). ```json theme={null} { "quote": { "quoteId": "afb84845-5347-4d26-9546-4b22eed465f1", "originChain": "fiat", "destinationChain": "polygon", "tokenIn": { "symbol": "USD", "decimals": 2, "amount": "502", "amountInUSD": 5.02 }, "tokenOut": { "contract": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "symbol": "USDC", "decimals": 6, "amount": "4000000", "expectedAmountOut": "4000000", "minAmountOut": "4000000" }, "provider": "deframe", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "ACH fee", "amountRaw": "102", "decimals": 2, "symbol": "USD", "asset": "tokenIn", "amountInUSD": 1.02 } ] }, "chainIdOut": 137 }, "paymentInstructions": { "method": "ACH", "amount": { "currency": "USD", "amountRaw": "502", "decimals": 2 }, "usd": { "bankRoutingNumber": "101019644", "bankAccountNumber": "218939188578", "depositMessage": "BRGZEPGAWBJTUEWYMBMG", "paymentRail": "ACH" } }, "chainId": null, "transactionData": [], "id": "6a932e6e8e3c3ec415c10819" } ``` Show `paymentInstructions.usd` to the user (routing, account, **deposit message / memo**, amount, expiry). ACH/WIRE rail fees appear as `kind: paymentRail` in `feeBreakdown` — there is no Pods/customer platform markup on this rail. Prefer `amountOut` when the product promise is “receive N USDC”. Poll `GET /v2/swap/status/{quote.quoteId}` until `fulfilled`. Full walkthrough: [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp). ### Offramp: USDC to USD ACH/WIRE Use this when a foreign user (see [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp)) sends USDC on `ethereum`, `base`, or `polygon` and receives USD via ACH or WIRE. The KYC profile for `originAddress` must have `usdEnabled: true`. Pass `originAddress` plus the USD bank fields — Pods resolve-or-creates the Avenia beneficiary (account number + routing match reuses the existing `id`; otherwise it registers one). The `id` stays internal on `rawQuote`; do not send `beneficiaryUsdBankAccountId`. With those fields present, the quote response includes `transactionData` ready to sign. ```http theme={null} GET /v2/swap/quote ``` Pass **exactly one** of `amountIn` (USDC raw to send) or `amountOut` (USD cents to receive). Both are supported — this is the onramp pair inverted. | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `ethereum`, `base`, or `polygon` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | USDC contract on the origin chain | | `tokenOut` | Yes | `USD` | | `amountIn` **or** `amountOut` | Yes (exactly one) | `amountIn`: USDC raw (`5000000` = 5 USDC). `amountOut`: USD cents (`400` = USD 4.00). | | `originAddress` | Yes | Wallet that sends USDC (unlocked USD KYC). | | `usdPaymentMethod` | No | `WIRE` (default) or `ACH` | | `bankAccountNumber` | Yes | Destination account number | | `bankRoutingNumber` | Yes | ABA routing | | `bankBeneficiaryName` | Yes | Legal name on the account. **ASCII only** — Avenia rejects accents (`ã`, `é`, …) with `bankBeneficiaryName is invalid` → `QUOTE_GENERATION_FAILED`. | | `bankName` | Yes | Bank name | | `beneficiaryStreetLine1` | Yes | Address line 1 | | `beneficiaryCity` / `beneficiaryState` / `beneficiaryPostalCode` / `beneficiaryCountry` | Yes | Address (`beneficiaryCountry` e.g. `USA`) | | `alias` | No | Defaults to `bankBeneficiaryName` | | `beneficiaryStreetLine2` / `description` | No | Extra address line / Avenia note | Avenia prices the offramp rail as an **Out Fee in USDC** (live ACH \~1.0125 USDC, WIRE \~10.035 USDC). The fee is already in the spread between `tokenIn.amount` and `tokenOut.amount`, and also appears as `kind: paymentRail` on `tokenIn` (USDC). There is no Pods/customer swap markup. With `amountIn`, USD out is net of that USDC Out Fee. With `amountOut`, Pods asks Avenia once (`outputAmount`); `tokenIn.amount` is the USDC the wallet must transfer (fee included). WIRE Out Fee is ~~10 USDC — a `5` USDC `amountIn` covers ACH (~~\$3.98 out) but not WIRE. Prefer `amountOut` (or a larger `amountIn`) for WIRE. #### `amountIn` — exact USDC send ```bash theme={null} curl -sS -G \ -H "x-api-key: $PODS_API_KEY" \ --data-urlencode "originChain=polygon" \ --data-urlencode "destinationChain=fiat" \ --data-urlencode "tokenIn=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" \ --data-urlencode "tokenOut=USD" \ --data-urlencode "amountIn=5000000" \ --data-urlencode "originAddress=0xb0b6234dCbB885409eF23fD5FD5E63eE33b1377f" \ --data-urlencode "usdPaymentMethod=ACH" \ --data-urlencode "bankAccountNumber=581984177591" \ --data-urlencode "bankRoutingNumber=721738952" \ --data-urlencode "bankBeneficiaryName=John Smith" \ --data-urlencode "bankName=JPMorgan Chase Bank N.A." \ --data-urlencode "beneficiaryStreetLine1=123 Main Street" \ --data-urlencode "beneficiaryCity=New York" \ --data-urlencode "beneficiaryState=NY" \ --data-urlencode "beneficiaryPostalCode=10001" \ --data-urlencode "beneficiaryCountry=USA" \ "$PODS_API_BASE_URL/v2/swap/quote" ``` Live capture (2026-09-10, local API, Picnic `usdEnabled` wallet): send **5 USDC** → receive **USD 3.98** (`tokenOut.amount=398`). Avenia Out Fee \~**1.0125 USDC**. After the route fix, that fee is a `paymentRail` charge on `tokenIn` and `tokenIn.chainId` / top-level `chainId` are **137**. ```json theme={null} { "quote": { "quoteId": "7a6c2c6c-96b3-4fe0-866a-8ab32d076a22", "originChain": "polygon", "destinationChain": "fiat", "tokenIn": { "contract": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "symbol": "USDC", "decimals": 6, "amount": "5000000", "chainId": 137 }, "tokenOut": { "symbol": "USD", "decimals": 2, "amount": "398", "expectedAmountOut": "398", "minAmountOut": "398", "chainId": 0 }, "provider": "deframe", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "ACH fee", "amountRaw": "1012500", "decimals": 6, "symbol": "USDC", "asset": "tokenIn" } ] } }, "chainId": 137, "depositAddress": "0x…", "transactionData": [ { "chainId": "137", "to": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "data": "0x...", "value": "0" }, { "chainId": "137", "to": "0x...", "data": "0x...", "value": "0" } ] } ``` #### `amountOut` — exact USD receive ```bash theme={null} curl -sS -G \ -H "x-api-key: $PODS_API_KEY" \ --data-urlencode "originChain=polygon" \ --data-urlencode "destinationChain=fiat" \ --data-urlencode "tokenIn=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" \ --data-urlencode "tokenOut=USD" \ --data-urlencode "amountOut=400" \ --data-urlencode "originAddress=0xb0b6234dCbB885409eF23fD5FD5E63eE33b1377f" \ --data-urlencode "usdPaymentMethod=ACH" \ --data-urlencode "bankAccountNumber=581984177591" \ --data-urlencode "bankRoutingNumber=721738952" \ --data-urlencode "bankBeneficiaryName=John Smith" \ --data-urlencode "bankName=JPMorgan Chase Bank N.A." \ --data-urlencode "beneficiaryStreetLine1=123 Main Street" \ --data-urlencode "beneficiaryCity=New York" \ --data-urlencode "beneficiaryState=NY" \ --data-urlencode "beneficiaryPostalCode=10001" \ --data-urlencode "beneficiaryCountry=USA" \ "$PODS_API_BASE_URL/v2/swap/quote" ``` Live capture: receive **exactly USD 4.00** → send **5.012532 USDC** (`tokenIn.amount=5012532`, quote `77fef6a2-358e-462a-8396-b9e2469f03d0`). Same call with `usdPaymentMethod=WIRE` kept `tokenOut.amount=400` and asked for **14.035088 USDC** (`d14a8e40-8689-4505-9f5e-7da67e329cc8`, Out Fee \~10.035 USDC). ```json theme={null} { "quote": { "quoteId": "77fef6a2-358e-462a-8396-b9e2469f03d0", "originChain": "polygon", "destinationChain": "fiat", "tokenIn": { "contract": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "symbol": "USDC", "decimals": 6, "amount": "5012532", "chainId": 137 }, "tokenOut": { "symbol": "USD", "decimals": 2, "amount": "400", "expectedAmountOut": "400", "minAmountOut": "400", "chainId": 0 }, "provider": "deframe", "feeBreakdown": { "charges": [ { "kind": "paymentRail", "label": "ACH fee", "amountRaw": "1012532", "decimals": 6, "symbol": "USDC", "asset": "tokenIn" } ] } }, "chainId": 137, "depositAddress": "0x…", "transactionData": [ { "chainId": "137", "to": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "data": "0x...", "value": "0" }, { "chainId": "137", "to": "0x...", "data": "0x...", "value": "0" } ], "id": "6aa322f8fd43d7a2324e0c48" } ``` There is no `paymentInstructions` block on offramp — the user pays on-chain. Execute every transaction in `transactionData`, in order, from `originAddress`. Sign using each item's `chainId` (`polygon` → `137`, `base` → `8453`, `ethereum` → `1`; see [Execute a swap](/guides/swaps/execute-swap)). Top-level `chainId` and `quote.tokenIn.chainId` match the origin after the route bind fix. The first transaction transfers USDC to `depositAddress`; the next records the transfer for tracking. After Pods detects the USDC, Avenia pays ACH/WIRE to the resolved bank account. Prefer `amountOut` when the product promise is “receive \$N”. Poll `GET /v2/swap/status/{quote.quoteId}` until `fulfilled`. KYC unlock and beneficiary field notes: [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp). ### Offramp to a third party Use this when a verified user wants to convert USDC (`base` or `monad`) into a fiat payment sent to someone else, instead of paying themselves back. This is the same offramp flow as above, with two differences: set `thirdParty=true` on the quote, and pass the **recipient's** `pixKey` (normal offramp assumes the Pix key belongs to the same person as `originAddress`; third-party offramp does not). ```http theme={null} GET /v2/swap/quote ``` Example: ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/quote?originChain=base&destinationChain=fiat&tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=BRL&amountIn=1000000&originAddress=0x0000000000000000000000000000000000000001&pixKey=recipient-pix-key@example.com&thirdParty=true" ``` | Query parameter | Required | Value | | - | - | - | | `originChain` | Yes | `base` or `monad` | | `destinationChain` | Yes | `fiat` | | `tokenIn` | Yes | USDC contract on the origin chain | | `tokenOut` | Yes | Local fiat currency, e.g. `BRL` | | `amountIn` | Yes | USDC amount in smallest units. | | `originAddress` | Yes | Approved wallet address that will send USDC on the selected origin chain. | | `pixKey` | Yes | Pix key of the **recipient** receiving the payment — not the origin wallet owner's own key. | | `thirdParty` | Yes | `true`. Marks this offramp as paying a key that does not belong to `originAddress`'s own Ramp profile. | The response includes `transactionData` like a normal offramp. Execute and track status the same way; only the recipient of the resulting payout differs. Reference: * [Get a quote](/guides/swaps/get-quote) * [Execute a swap](/guides/swaps/execute-swap) # Track Pods Ramp Status Source: https://docs.pods.finance/guides/ramp/status Poll KYC approval and swap status for Pods Ramp Pods Ramp has two independent statuses to track: the KYC/approval status of the user's Ramp profile (from [Set up Pods KYC](/guides/kyc/setup)), and the status of an individual onramp, offramp, or yield quote (from [Move money with Pods Ramp](/guides/ramp/quotes)). ## Poll Ramp approval status Local KYC/AML provider validation is asynchronous. Pods schedules a durable background monitor, so the stored status keeps progressing even when your client is offline. Poll Pods until the status reaches a terminal state to update your own UI. ```http theme={null} GET /api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000 ``` Successful response: ```json theme={null} { "kycUserId": "550e8400-e29b-41d4-a716-446655440000", "provider": "sumsub", "email": "user@example.com", "walletAddress": "0x0000000000000000000000000000000000000001", "status": "approved", "brlaEnabled": true, "rejectReason": null, "providerStatus": "COMPLETED", "providerResult": "APPROVED", "providerMessage": null, "updatedAt": "2026-06-15T12:01:00.000Z" } ``` | Status | Meaning | | - | - | | `created` | Local user exists, but no attempt with the local provider exists yet. | | `provider_pending` | An attempt with the local provider exists and is processing. | | `approved` | Money movement is enabled. | | `rejected` | Money movement is blocked. | | `rejected_retryable` | User can retry KYC depending on your UX. | | `blocked` | The provider import was blocked before a normal pending attempt was created. | ## Track swap status Track onramp, offramp, and yield quote flows with the Swap v2 status endpoint. ```http theme={null} GET /v2/swap/status/9be08433-3338-4881-9517-31faa22489c9 ``` | Status | Meaning | | - | - | | `pending` | Payment has not been detected yet, or the quote is still waiting for confirmation. | | `confirmed` | Payment was detected and output settlement is not final yet. | | `processing` | Output settlement is in progress. | | `fulfilled` | Final success state. | | `expired` | Quote or fiat payment expired before payment was detected. | | `failed` | Operational failure. Show an error and route to support or retry. | | `refunded` | Refund flow finished, when applicable. | Reference: * [Get a quote](/guides/swaps/get-quote) * [Execute a swap](/guides/swaps/execute-swap) # Execute Swap Source: https://docs.pods.finance/guides/swaps/execute-swap Sign and submit the transaction data returned by GET /v2/swap/quote In this section, you'll learn how to execute a token swap using the transaction data returned from `GET /v2/swap/quote`. **Prerequisites:** Complete the [Get a Quote](/guides/swaps/get-quote) guide first. New integrations should request the quote with wallet addresses (and `pixKey` for fiat offramps) so executable fields are already in the quote response. ## Introduction **Recommended path:** call `GET /v2/swap/quote` with `originAddress` and `destinationAddress` (crypto) or `originAddress` + `pixKey` (fiat offramp). The response includes `id`, `chainId`, and `transactionData` (and `paymentInstructions` for fiat onramps). Sign and submit those transactions — no second API call. **Trustless Operation:** You sign and submit all transactions yourself. Pods never has custody of your funds. ## Response fields from the quote When addresses are supplied on the quote, the API returns transaction data ready to be signed and executed (alongside the nested `quote` object): ```json theme={null} { "quote": { "...": "..." }, "id": "65f1a2b3c4d5e6f789012345", "chainId": 137, "transactionData": [ { "to": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "data": "0x095ea7b3000000000000000000000000111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000000000000000000000000000000000000f4240", "value": "0", "chainId": "137" }, { "to": "0x111111125421ca6dc452d289314280a0f8842a65", "data": "0x07ed2379...", "value": "0", "chainId": "137" } ] } ``` ## Response Fields ID created for execution tracking. Use this ID with `GET /actions/{id}` when you need execution status after transactions are submitted. Top-level numeric chain ID for the origin execution network. Each item in `transactionData` also has a per-leg `chainId` as a **decimal string** (for example `"137"`), matching the OpenAPI `TransactionData` schema — coerce with `Number()` when comparing to this top-level field or wallet APIs. EVM routes return an array of transaction legs when `output=bytecode` (default). **Batch all legs on the same `chainId` into one atomic transaction** — see [Executing Bytecode](/getting-started/executing-bytecode). Solana-origin routes return `type: intra-chain-solana` with `transactionData.rawTransaction` (base64). Request `output=instructions` on Solana-origin only to receive `{ instructions, lutsByAddress? }` instead of a serialized transaction — EVM and fiat origins reject that output with `OUTPUT_NOT_SUPPORTED`. When `output=fireblocks`, returns a Fireblocks contract call payload instead of a transaction array. Typical EVM legs: * **Approval**: Grants permission to the swap contract * **Swap**: Executes the actual token swap * **Tracking**: Records the swap in Pods's system Present on fiat onramps (for example Pix). Show `paymentInstructions.pix.copyPaste` to the user. There is no EVM `transactionData` to sign on that path. ERC-4337 user operation object. Only present when `output=userOperation` is requested on the quote. The contract address to send the transaction to Encoded transaction data for the contract call Amount of native token to send (in wei). Usually "0" unless swapping native tokens. `quote.quoteId` and top-level `id` identify different resources. `quoteId` tracks the saved swap quote and is the `{id}` for `GET /v2/swap/status/{id}`. The top-level `id` is used for Action execution tracking. ## Transaction Execution Sign and submit the transaction data on your end — Pods never has custody of your funds. **Execute atomically.** Do not send each item in `transactionData` as a separate transaction. Batch all same-chain legs into one tx (EIP-7702, smart-account userOp, or Fireblocks batch). See [Executing Bytecode](/getting-started/executing-bytecode). | Your stack | `output` on quote | Guide | | - | - | - | | EOA (MetaMask, wagmi, viem) | `bytecode` (default) | [EIP-7702](/examples/javascript/using-eip7702) | | ERC-4337 smart account | `userOperation` | [Smart Account Deposit](/examples/javascript/smart-account-deposit) | | Fireblocks custody | `fireblocks` + `accountId` | [Fireblocks Integration](/external-integrations/fireblocks-integration) | | Solana wallet | `bytecode` (default) or `instructions` | Sign `transactionData.rawTransaction`, or assemble `{ instructions, lutsByAddress? }` via `/svm/convert-instructions-to-base64` | ### EOA — EIP-7702 (recommended) Use [EIP-7702 `executeBatch`](/examples/javascript/using-eip7702) to encode all `transactionData` legs into a single type-4 transaction. ### Smart account — `output=userOperation` Request `output=userOperation` on `GET /v2/swap/quote` together with the wallet addresses. Submit the returned `userOperation` through your bundler — one userOp, not a loop of separate sends. ### Fireblocks — `output=fireblocks` Pass `accountId` on the quote and submit `transactionData` to the Fireblocks Transactions API. See [Fireblocks Integration](/external-integrations/fireblocks-integration). ## Swap Types The quote path automatically handles different swap types: **Same-chain swaps** (e.g., USDT to USDC on Ethereum): * Usually 2-3 transactions: approve + swap (+ tracking) * Executes immediately on the same blockchain * Lower fees, faster execution **Cross-chain swaps** (e.g., USDT on Ethereum to USDC on Polygon): * Involves bridge operations * May take several minutes to complete * Webhooks notify you of status changes * Use `/v2/swap/status/{quoteId}` for quote status and `GET /actions/{id}` for execution status ## Complete Workflow Call `GET /v2/swap/quote` with amount, tokens, chains, and `originAddress` + `destinationAddress` (or `pixKey` for fiat offramp). Save `quote.quoteId` and the top-level `id`. For crypto routes, sign and send `transactionData`. For fiat onramps, show `paymentInstructions` to the user. Use `GET /v2/swap/status/{quoteId}` to monitor quote progress. Use `GET /actions/{id}` with the top-level `id` when you need Action execution status. ## Advanced: `POST /v2/swap/bytecode` New integrations should not need this endpoint. It remains available for older clients that obtained a price-only quote first. Prefer the one-call quote flow above. ```http theme={null} POST /v2/swap/bytecode ``` | Parameter | Type | Required | Description | | - | - | - | - | | `quoteId` | string | ✅ | The quote ID returned by `GET /v2/swap/quote` | | `originAddress` | string | Optional | The wallet address that will sign and send the transaction. Required for EVM/Solana routes. | | `destinationAddress` | string | Optional | The destination wallet address. Required unless `pixKey` is provided. | | `pixKey` | string | Optional | PIX key for fiat off-ramp routes. | | `thirdParty` | boolean | Optional | Must match the `thirdParty` value used when requesting the quote when both are set; a mismatch returns `PIX_THIRD_PARTY_QUOTE_MISMATCH` (400). | | `rawQuote` | any | Optional | Provider-specific quote data from the quote response when present. | | `webhookURL` | string (URI) | Optional | Per-quote webhook endpoint override. | | `output` | string | Optional | `bytecode` (default), `userOperation`, or `fireblocks`. | | `accountId` | number | Optional | Fireblocks vault account ID. Required when `output` is `fireblocks`. | ## Error Handling The API returns appropriate error messages for: * Missing required parameters * Invalid or expired quote * Quote not found or already used * Provider-specific errors during transaction preparation **Example Error Response:** ```json theme={null} { "error": { "code": "QUOTE_NOT_FOUND", "message": "Quote not found or expired" } } ``` Always check that the quote hasn't expired before executing. Quotes are typically valid for 5 minutes. ## Next Steps Start a new swap Track cross-chain swap progress View complete implementation examples # Get a Quote Source: https://docs.pods.finance/guides/swaps/get-quote Learn how to request quotes for swapping tokens on same-chain or cross-chain In this section, you'll learn how to request quotes for swapping between tokens using the Pods API. ## Prerequisites Get your API key for authentication at [Pods](https://www.pods.finance/plg/select-plan) Contract addresses for the tokens you want to swap Chain IDs for source and destination chains Include your API key in the `x-api-key` header ## Getting a Quote ### Endpoint ```http theme={null} GET /v2/swap/quote ``` This is a **v2** endpoint. Always use `/v2/swap/quote` for new integrations. Pass wallet addresses (and `pixKey` for fiat offramps) on the quote so the response includes executable fields — you do not need `POST /v2/swap/bytecode`. ### Required Parameters | Parameter | Type | Description | Example | | - | - | - | - | | `originChain` or `chainIdIn` | string or number | Source chain name, or source chain ID. Use one of these fields. | `ethereum`, `1` | | `destinationChain` or `chainIdOut` | string or number | Destination chain name, or destination chain ID. Use one of these fields. | `polygon`, `137` | | `tokenIn` | string | Contract address of input token | `0xdAC17F958D2ee523a2206206994597C13D831ec7` (USDT) | | `tokenOut` | string | Contract address of output token | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` (USDC) | | `amountIn` or `amountOut` | string | Amount in the token's smallest unit. Provide exactly one of these fields. | `1000000` | ### Optional Parameters | Parameter | Type | Description | Example | | - | - | - | - | | `preferredProvider` | string | Force a specific **crypto swap/bridge** provider for the returned quote when multiple providers support the route. Case-insensitive. Must be a registered provider name (`relay`, `mayan`, `lifi`, `cctp`, `1inch`, `jupiter`, `teleswap`, `pods`, `symbiosis-bitcoin`). The provider must be eligible for the token pair and chains. When omitted, the API returns and persists the quote with the highest `tokenOut.expectedAmountOut`. Returns `INVALID_PREFERRED_PROVIDER` (400) if the name is not registered, `PREFERRED_PROVIDER_NOT_ELIGIBLE` (400) if the provider does not support the route, or `PREFERRED_PROVIDER_QUOTE_UNAVAILABLE` (400) if the provider returned no quote. Do **not** use this to pin a fiat rail — use `rampProvider`. | `relay` | | `rampProvider` | string | Pin the **fiat rail provider** on onramp/offramp quotes (`originChain` or `destinationChain` is `fiat`). Case-insensitive. Omitted or empty defaults to `avenia` even when the customer has `extraRampProviders` — that field is an allowlist, not a ranking preference. Registered names: `avenia`, `unblockpay`. UnblockPay v1 quotes are Pix↔USDC and USD WIRE↔USDC on polygon and base (direct mint/redeem, not the Avenia BRLA inventory hop). UnblockPay USD is WIRE only — `usdPaymentMethod=ACH` or `ethereum` returns `RAMP_PROVIDER_NOT_ELIGIBLE`. Public quotes keep `provider: "deframe"` and add additive `rampProvider`. Ignored on crypto-only quotes. Bytecode uses the persisted quote and does not accept a conflicting `rampProvider`. Returns `INVALID_RAMP_PROVIDER` (400) if the name is not registered, or `RAMP_PROVIDER_NOT_ELIGIBLE` (400) if the provider does not support that fiat rail or this customer is not opted in. Distinct from `preferredProvider`. | `avenia` | | `slippage` | number | Max slippage as a **fraction of 1** (e.g. `0.01` = 1%). Range `0.0001`–`0.5`. Passed to the selected provider; omit to use each provider’s default. Returns `INVALID_SLIPPAGE` (400) when out of range. | `0.01` | | `originAddress` | string | Sender on the origin chain (checksummed EVM or Solana public key). When provided together with `destinationAddress`, `pixKey` (Pix offramp), or USD bank fields (USD offramp), the response includes executable fields (`id` + `transactionData`, and for fiat onramps `paymentInstructions`) — the recommended one-call flow. | `0x…` or Solana pubkey | | `destinationAddress` | string | Recipient on the destination chain (checksummed EVM or Solana public key). Provide this for crypto destinations and fiat onramps. Must be a real wallet address — omit the parameter entirely if you only want a price quote. Any address value passed here is used as-is as the destination. | `0x…` or Solana pubkey | | `pixKey` | string | PIX key for **BRL** fiat off-ramp destinations. Use instead of `destinationAddress` when `destinationChain` is `fiat` and `tokenOut` is `BRL`. Required together with `originAddress` for Pix offramp. For **USD** offramp (`tokenOut=USD`), pass the bank account fields instead — see [Foreign USD KYC and ramps](/guides/kyc/usd-foreign-ramp). | `user@bank.com` | | `usdPaymentMethod` | string | `ACH` or `WIRE` (default `WIRE`) on foreign USD ramps. | `ACH` | | `bankBeneficiaryName` | string | Legal name on the USD offramp bank account. **ASCII only** — Avenia rejects accents. Required with the other bank fields when `tokenOut=USD`. | `John Smith` | | `refundAddress` | string | Refund address if the swap fails | `0x…` | | `feeSponsorship` | boolean | Request fee sponsorship on eligible Pods bridge routes (`pods` provider). Defaults to `false`. | `false` | | `transferSpeed` | string | Transfer speed for CCTP cross-chain swaps. `fast` (default) or `standard`. Ignored for non-CCTP routes. | `fast` | | `thirdParty` | boolean | When true, the fiat off-ramp quote is a third-party payment (`outputThirdParty=true` sent to the provider). Only relevant for PIX/fiat off-ramp routes. Defaults to `false`. Set this on the quote together with the recipient `pixKey`. | `false` | | `output` | string | Executable payload format when addresses are supplied. Defaults to `bytecode`. `userOperation` and `fireblocks` are EVM-origin only. `instructions` is Solana-origin only (`OUTPUT_NOT_SUPPORTED` otherwise). | `bytecode` | | `webhookURL` | string (URI) | Optional per-quote webhook endpoint. When set, quote lifecycle webhooks are delivered to this URL instead of the customer default webhook URL. | `https://example.com/webhooks/swap-quote` | Either `amountIn` OR `amountOut` is required (not both). Use `amountOut` for exact output swaps ("I want to receive exactly X tokens"). Provide **either** named chains (`originChain` + `destinationChain`) **or** numeric IDs (`chainIdIn` + `chainIdOut`). Responses always include canonical **`chainIdIn`** / **`chainIdOut`** on the quote when the API can resolve them; prefer these for storage and UI over string names or legacy `fromChainId` / `toChainId`. ## Example Requests ### Same-Chain Swap (Ethereum USDT to USDC) ```bash theme={null} curl --request GET \ --url 'https://api.pods.finance/v2/swap/quote?originChain=ethereum&destinationChain=ethereum&tokenIn=0xdAC17F958D2ee523a2206206994597C13D831ec7&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amountIn=1000000' \ --header 'x-api-key: YOUR_API_KEY' ``` ### Cross-Chain Swap (Ethereum to Polygon) ```bash theme={null} curl --request GET \ --url 'https://api.pods.finance/v2/swap/quote?originChain=ethereum&destinationChain=polygon&tokenIn=0xdAC17F958D2ee523a2206206994597C13D831ec7&tokenOut=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174&amountIn=1000000' \ --header 'x-api-key: YOUR_API_KEY' ``` ### Same route using chain IDs ```bash theme={null} curl --request GET \ --url 'https://api.pods.finance/v2/swap/quote?chainIdIn=1&chainIdOut=137&tokenIn=0xdAC17F958D2ee523a2206206994597C13D831ec7&tokenOut=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174&amountIn=1000000' \ --header 'x-api-key: YOUR_API_KEY' ``` ### Cross-chain with a preferred provider When several providers can quote the same route, pass `preferredProvider` to persist and return that provider’s quote (for example, to keep routing consistent with your UI or compliance rules): ```bash theme={null} curl --request GET \ --url 'https://api.pods.finance/v2/swap/quote?originChain=polygon&destinationChain=arbitrum&tokenIn=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359&tokenOut=0xaf88d065e77c8cc2239327c5edb3a432268e5831&amountIn=1000000&preferredProvider=relay' \ --header 'x-api-key: YOUR_API_KEY' ``` ## Multi-route aggregation For routes supported by more than one provider, the API requests quotes in parallel, ranks them by `tokenOut.expectedAmountOut`, and **returns a single `quote` object** — the best route by default, or the `preferredProvider` when set. * Only that quote is saved to the database. Prefer requesting the quote with `originAddress` + `destinationAddress` (or `pixKey`) so execution fields are returned in the same response. * Fixed-quote Pods bridge routes (`pods` provider) still use a single provider; `preferredProvider` must be `pods` or omitted on those paths. ## Response Format The API returns a JSON object with one persisted quote. When both `originAddress` and `destinationAddress` are supplied (or `originAddress` + `pixKey` for fiat offramps), the response also includes executable fields (`id`, `chainId`, `transactionData`, and for fiat onramps `paymentInstructions`). That is the recommended integration path. The quote always includes a `feeBreakdown` object that itemizes platform fees (Pods fee, customer markup fee) and the estimated bridge/slippage impact: ```json theme={null} { "quote": { "quoteId": "550e8400-e29b-41d4-a716-446655440000", "chainIdIn": 1, "chainIdOut": 137, "originChain": "ethereum", "destinationChain": "polygon", "tokenIn": { "contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "symbol": "USDT", "decimals": 6, "amount": "1000000", "chainId": 1 }, "tokenOut": { "contract": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "symbol": "USDC", "decimals": 6, "amount": "998500", "chainId": 137 }, "provider": "relay", "deadline": 1706098500, "deadlineDate": "2024-01-24T12:55:00.000Z", "status": "pending", "feeSponsorship": false, "feeBreakdown": { "chargeableAmountInRaw": "1000000", "amountInAfterPlatformFeesRaw": "985000", "reference": { "amountOutRaw": "998600", "source": "spotUsdAtQuoteTime", "pricedAt": "2026-05-28T14:32:10.000Z", "tokenInPriceInUSD": 1, "tokenOutPriceInUSD": 1, "amountInAfterPlatformFeesRaw": "985000" }, "charges": [ { "kind": "pods", "label": "Pods fee", "amountRaw": "5000", "amountInUSD": 0.005, "asset": "tokenIn", "decimals": 6, "symbol": "USDT" }, { "kind": "customer", "label": "Customer fee", "amountRaw": "10000", "amountInUSD": 0.01, "asset": "tokenIn", "decimals": 6, "symbol": "USDT" }, { "kind": "bridgeAndSlippage", "label": "Bridge fee + slippage", "amountRaw": "100", "amountInUSD": 0.0001, "asset": "tokenOut", "decimals": 6, "symbol": "USDC", "signed": true } ], "summary": { "platformFeesTotalInRaw": "15000", "platformFeesTotalInUSD": 0.015, "bridgeAndSlippageImpactOutRaw": "100", "bridgeAndSlippageImpactOutUSD": 0.0001 } } } } ``` **Fee breakdown** (`feeBreakdown`) is the **canonical source** for all quote fees on every route — platform fees (Pods, customer markup) and bridge/spread impact. It is returned on **`GET /v2/swap/quote` only** (not on `GET /v2/swap/status/:id`). Charge labels are in English; localize in your UI. The `bridgeAndSlippage` charge is **signed** — a negative `amountRaw` means favorable price impact vs spot. Platform fees (`pods`, `customer`) are deducted from `tokenIn` before the provider quote. ## Response Fields Unique identifier for this quote. Use this when executing the swap. Canonical numeric chain IDs for the swap route (source → destination). Prefer these for new code; string `originChain` / `destinationChain` and legacy `fromChainId` / `toChainId` remain for compatibility. Source and destination chain names (e.g., `ethereum`, `polygon`, `arbitrum`). These remain supported; numeric `chainIdIn` / `chainIdOut` are preferred for new integrations. Complete token information including contract address, symbol, decimals, amount, and chain ID. `tokenOut.expectedAmountOut` and `tokenOut.minAmountOut` are the provider guarantees after platform fees are taken from the input. Itemized costs: **Pods fee**, **Customer fee**, and **Bridge fee + slippage** (combined, signed on `tokenOut`). `reference` explains the spot baseline used for the bridge charge. Zero-value charge lines are omitted; use `summary` for aggregate totals that may be zero. Name of the swap provider used (e.g., `1inch`, `Jupiter`, `TeleSwap`, `Mayan`, `relay`, `lifi`) Quote expiration as a Unix timestamp in **seconds**. Execute the swap before this time. Quote expiration as an **ISO 8601 UTC string** (e.g. `2024-01-20T12:30:00.000Z`). Human-readable alternative to `deadline`. Provider-specific quote data (stringified JSON). Some bridge providers require this when building transactions. This field is omitted from the response when null — when present, the one-call quote path already consumes it for you. ## Supported Chains `ethereum` `base` `optimism` `arbitrum` `solana` `polygon` `gnosis` `bitcoin` `hyperevm` `monad` `fiat` (off-ramp/on-ramp routes) ## Error Handling The API returns appropriate error messages for: * Missing required parameters * Invalid chain names or token addresses * No available swap provider for the requested route * Invalid or ineligible `preferredProvider` or `rampProvider` (see [Error Codes](/error-codes)) * Provider-specific errors during quote generation | Code | When | | - | - | | `INVALID_PREFERRED_PROVIDER` | `preferredProvider` is not a registered provider name | | `PREFERRED_PROVIDER_NOT_ELIGIBLE` | Provider exists but does not support this chain/token route | | `PREFERRED_PROVIDER_QUOTE_UNAVAILABLE` | Provider is eligible but did not return a quote (timeout or upstream failure) | | `INVALID_RAMP_PROVIDER` | `rampProvider` is not a registered fiat rail provider | | `RAMP_PROVIDER_NOT_ELIGIBLE` | Provider exists but does not support this fiat currency / payment method | | `AMOUNT_IN_TOO_LOW` | AmountIn is below the minimum for this route | | `AMOUNT_IN_TOO_HIGH` | AmountIn exceeds available liquidity for this route. Try a smaller amount. | **Example Error Response:** ```json theme={null} { "error": { "code": "NO_ROUTE_FOUND", "message": "No swap route available between specified chains" } } ``` Quotes have an expiration time (typically 5 minutes). Make sure to execute the swap before the `deadline` timestamp. ## Next Steps Sign the transactions returned by the quote and track status View detailed API documentation # Overview Source: https://docs.pods.finance/guides/swaps/overview How Pods Swap v2 works end to end: one quote endpoint for same-chain, cross-chain, and fiat routes, then trustless execution and tracking Pods Swap v2 prices and executes token swaps through a single endpoint. It automatically detects the route type — same-chain, cross-chain, or fiat ramp — aggregates across providers for the best output, and returns an executable payload you sign yourself. Pods never takes custody of funds. Request the quote with wallet addresses (and `pixKey` for fiat offramps) and the response already includes the executable fields (`id`, `transactionData`, and `paymentInstructions` for fiat onramps) — you do not need a second `POST /v2/swap/bytecode` call. ## Introduction | Item | Value | | - | - | | **Endpoint** | `GET /v2/swap/quote` (quote + executable payload in one call) | | **Route types** | Same-chain (`originChain` == `destinationChain`), cross-chain (they differ), fiat ramp (`originChain`/`destinationChain` = `fiat`) | | **Amounts** | Token's smallest unit; provide exactly one of `amountIn` or `amountOut` | | **Providers** | Aggregated across `relay`, `mayan`, `lifi`, `cctp`, `1inch`, `jupiter`, `teleswap`, `pods`, `symbiosis-bitcoin` — highest `expectedAmountOut` wins unless you set `preferredProvider`. Fiat ramps stay on `pods`/`deframe` and pin the rail provider with `rampProvider` (default `avenia`). | | **Execution** | Trustless — you sign and submit `transactionData` yourself; Pods never custodies funds | | **Status endpoint** | `GET /v2/swap/status/{quoteId}` | | **Auth** | Header `x-api-key: ` | The system automatically detects the route type from the chains you pass and routes to the appropriate provider: **Same-chain swap**: `originChain` equals `destinationChain` **Cross-chain swap**: `originChain` differs from `destinationChain` **Fiat ramp**: `originChain` or `destinationChain` is `fiat` ## Authentication Every request requires: ``` x-api-key: ``` * The key identifies your **customer**. Fee configuration and strategy catalog are scoped to it. * Success responses are the bare JSON body (HTTP 200). Errors use `{ "error": { "code", "message", "details?" } }`. * Keep the API key on your **backend** — never expose it in a browser app. Don't have a key yet? [Get an API key](https://www.pods.finance/plg/select-plan). ## Integration flow Request a same-chain, cross-chain, or fiat quote. Pass `originAddress` and `destinationAddress` (or `pixKey` for fiat offramps) so the response includes executable fields. Read `tokenOut.expectedAmountOut` / `minAmountOut` and `feeBreakdown` before executing, and store the canonical `chainIdIn` / `chainIdOut` and `quote.quoteId`: ```http theme={null} GET /v2/swap/quote ``` Full guide: [Get a quote](/guides/swaps/get-quote). Execute all origin-chain `transactionData` legs as **one atomic batch** (EIP-7702 or a smart-account userOp) from `originAddress` — never submit them one by one, or a later failure can leave approvals/transfers committed and strand funds. Fiat onramps have nothing to sign — show `paymentInstructions` instead: ```http theme={null} GET /v2/swap/quote ``` Full guide: [Execute a swap](/guides/swaps/execute-swap) (atomic batching via [EIP-7702](/examples/javascript/using-eip7702) or [Executing Bytecode](/getting-started/executing-bytecode)). Poll the swap to `fulfilled` with the quote's `quoteId`, or subscribe to live WebSocket updates: ```http theme={null} GET /v2/swap/status/{quoteId} ``` Full guide: [Track a swap](/guides/swaps/track-status). # Track a Swap Source: https://docs.pods.finance/guides/swaps/track-status Poll swap status or subscribe to live WebSocket updates until a swap is fulfilled After you execute a swap, track it to a terminal state. You can **poll** the swap status endpoint with the quote's `quoteId`, or **subscribe** to live WebSocket updates keyed by the wallet address. Both reach the same outcome — poll when a request/response loop is enough, subscribe when you want push updates without polling. `quote.quoteId` and the top-level `id` identify different resources. `quoteId` tracks the saved swap **quote** — it is the `{id}` for `GET /v2/swap/status/{id}`. The top-level `id` is the **Action** id — use it with `GET /actions/{id}` and to correlate WebSocket `action_update` events. ## Poll swap status Track onramp, offramp, swap, and yield quote flows with the Swap v2 status endpoint, using the `quote.quoteId` from the quote response. ```http theme={null} GET /v2/swap/status/{quoteId} ``` ```bash theme={null} curl -sS \ -H "x-api-key: $PODS_API_KEY" \ "$PODS_API_BASE_URL/v2/swap/status/8ef5685b-280d-4b01-8a02-97cd05fe1d35" ``` Successful response (trimmed to the fields you branch on): ```json theme={null} { "quoteId": "8ef5685b-280d-4b01-8a02-97cd05fe1d35", "provider": "uniswap", "status": "pending", "originChain": "ethereum", "destinationChain": "ethereum", "originAddress": "0xb794F5eA0ba39494cE839613fffBA74279579268", "destinationAddress": "0xb794F5eA0ba39494cE839613fffBA74279579268" } ``` Poll until `status` is terminal (`fulfilled`, `expired`, `failed`, or `refunded`). | Status | Meaning | Terminal | | - | - | - | | `pending` | Payment has not been detected yet, or the quote is still waiting for confirmation. | No | | `confirmed` | Payment was detected and output settlement is not final yet. | No | | `processing` | Output settlement is in progress. | No | | `fulfilled` | Final success state. | Yes | | `expired` | Quote or fiat payment expired before payment was detected. | Yes | | `failed` | Operational failure. Show an error and route to support or retry. | Yes | | `refunded` | Refund flow finished, when applicable. | Yes | Already have the mined transaction hash? Report it with `POST /v2/swap/status/{id}/update/{txHash}` so the API processes it immediately instead of waiting for indexer webhooks, then poll `GET /v2/swap/status/{id}` for the outcome. Resubmissions are idempotent. ## WebSocket updates (recommended) Subscribe to live updates instead of polling. Connect, subscribe to the wallet channel, and listen for `action_update` events. ``` wss://api.pods.finance/updates ``` ```json theme={null} { "type": "subscribe", "channel": "" } ``` Listen for `action_update` events where `action.id` matches the **top-level `id`** from the quote/execution response: | Status | Meaning | | - | - | | `PENDING` | Awaiting settlement | | `SUCCESS` | Settlement completed | | `FAILED` | Settlement failed | | `REFUNDED` | Refund completed | Re-subscribe on reconnect. Alternatively, use your **customer webhook** (`ACTION_UPDATE` events) — set a per-quote `webhookURL` on the quote to override the default endpoint. ## Action execution status For on-chain execution progress (as opposed to quote settlement), poll the Action by the top-level `id`: ```http theme={null} GET /actions/{id} ``` Use this when you need the execution/Action state; use `GET /v2/swap/status/{quoteId}` for quote settlement. Reference: * [Get a quote](/guides/swaps/get-quote) * [Execute a swap](/guides/swaps/execute-swap) # Check Open Positions Source: https://docs.pods.finance/guides/yield/check-positions Learn how to retrieve and monitor current yield positions for a wallet address In this section, we'll learn how to retrieve the current yield positions for an address. **Prerequisites:** * [How to get available protocols](/guides/yield/get-available-protocols) * [How to deposit](/guides/yield/deposit) ## Retrieve Current Positions Use the following endpoint to retrieve all the current yield positions of an address: ```http theme={null} GET /v2/wallets/:address?include=earn ``` **Required Parameters:** | Parameter | Description | Example | | - | - | - | | address | Wallet address | `0x1234567890123456789012345678901234567890` | **Optional Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `includeHistory` | boolean | `true` | Include paginated transaction history. | | `withActions` | boolean | `true` | Include raw investment actions inside each earn position. | **Slim Request:** ```http theme={null} GET /v2/wallets/0x1234567890123456789012345678901234567890?include=earn&includeHistory=false&withActions=false ``` Use the slim request when the client needs the calculated position, principal, and profit but does not consume raw actions or wallet history. ## Response Format The response returns yield positions under `earn.positions` and an aggregate under `earn.summary`. By default it also includes raw actions and paginated wallet activity under `history`; the slim request omits both without changing the position accounting. **Example Response:** ```json theme={null} { "earn": { "positions": [{ "spotPosition": { "currentPosition": { "value": "10048700", "decimals": 6, "humanized": "10.0487", "symbol": "USDC" }, "profit": { "value": "48700", "afterFees": "43830", "decimals": 6, "humanized": "0.0487", "symbol": "USDC" }, "cumulativeProfit": { "value": "48700", "decimals": 6, "humanized": "0.0487", "symbol": "USDC" }, "principal": { "value": "10000000", "decimals": 6, "humanized": "10.00", "symbol": "USDC" }, "underlyingBalanceUSD": 10.05, "apy": 0.0417, "grossAPY": 0.0417, "netAPY": 0.0396, "avgApy": 0.0412, "inceptionApy": 0.0385 }, "strategy": { "id": "Aave-USDC-polygon", "protocol": "Aave", "assetName": "USDC", "network": "polygon", "networkId": "137", "asset": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "assetDecimals": 6, "apy": 0.0417, "paused": false, "fee": "0", "availableActions": ["lend", "withdraw"] } }], "summary": { "totalProfitInUSD": 0.05, "totalUnderlyingBalanceUSD": 10.05 } } } ``` ## Understanding the Response Raw balance as a BigInt string in the token's smallest unit (e.g. `"10000000"` = 10 USDC) USD value of the position Current gain or loss of the position relative to the cost basis still invested, taking into account adjustments such as partial withdrawals. This reflects the true economic result of the open position — what the client has effectively earned on the capital that remains deployed. * `value`: the **gross** figure, before Pods fees. * `afterFees`: the same figure net of the performance and markup fees — `value × (1 - (performanceFeeBps + markupFeeBps) / 10000)`. Display this one. Fees apply to profit only, so when `value` is negative `afterFees` equals it exactly. See [Fees](/getting-started/fees) for the full rules. A simpler accumulated view of the overall result: the current position value minus the total net amount moved by the client (deposits minus withdrawals). This does not adjust for the changing cost basis and gives a straightforward picture of how much value has been generated since inception. * `apy`: Current APY as a decimal (e.g. `0.0417` = 4.17%) * `grossAPY`: APY before performance fees are deducted (decimal). May be `null` when not applicable. * `netAPY`: APY after performance fees are deducted (decimal). May be `null` when not applicable. * `avgApy`: Time-weighted average APY since inception * `inceptionApy`: APY since the strategy launched For strategies with queued (request-based) investment flows, these arrays contain pending amounts not yet settled on-chain: * `requestedToLend`: Array of amounts pending deposit (e.g. a `request-lend` action that is PENDING) * `requestedToWithdraw`: Array of amounts pending withdrawal (e.g. a `request-withdraw` action that is PENDING) Each entry has the same shape as `currentPosition` (`value`, `decimals`, `humanized`, `symbol`). These fields are only present on strategies that support the `request-lend` / `request-withdraw` action flow. Full strategy object (same metadata exposed by `GET /v2/strategies/:strategyId`) including protocol, network, asset details, and fee Total portfolio value in USD across all positions ## UI Integration Example Active yield positions in UI *Example UI showing active yield positions* ## Next Steps Learn how to withdraw from a position Open a new yield position # Protocol Information Source: https://docs.pods.finance/guides/yield/check-protocol-info Learn how to retrieve yield protocol information including APY rates and terms Before implementing yield features in your application, you'll need to retrieve essential information such as APY rates, minimum deposit requirements, and other terms. This guide explains how to fetch this data using our API endpoints. In the yield category, we encompass various passive income-generating activities in DeFi. This includes lending (where users earn interest by providing liquidity), investing in yield-generating vaults (which automatically compound returns), and staking (where users lock tokens to earn rewards). ## Introduction | Item | Value | | - | - | | **Strategy ID format** | `{protocol}-{assetName}-{network}` (e.g. `Aave-USDC-polygon`) | | **Category** | Yield — lending, yield vaults, and staking | | **Deposit / Withdraw actions** | `lend` (deposit) / `withdraw` | | **Amounts** | Token's smallest unit (base units), using the token's own decimals | | **Cross-chain** | Fund a deposit from — or receive a withdrawal on — another EVM chain; Pods handles the bridge | | **APY fields** | `spotPosition.apy`, `grossAPY`, `netAPY`, `avgApy`, `inceptionApy` | | **Auth** | Header `x-api-key: ` | ## Authentication Every request requires: ``` x-api-key: ``` * The key identifies your **customer**. Fee configuration and strategy catalog are scoped to it. * Success responses are the bare JSON body (HTTP 200). Errors use `{ "error": { "code", "message", "details?" } }`. * Keep the API key on your **backend** — never expose it in a browser app. Don't have a key yet? [Get an API key](https://www.pods.finance/plg/select-plan). ## Integration flow First, retrieve the list of all supported protocols: ```http theme={null} GET /strategies ``` The JSON body is `{ "data": [ …strategies ], "pagination": { … } }`. Use `pagination.hasMore`, `pagination.total`, and `pagination.totalPages` to page through results. Query parameters include `page`, `limit` (default limit is 100), `network`, `protocol`, and `category` to filter results. See the OpenAPI description for the full `ListPagination` shape. Then, fetch specific protocol information using: ```http theme={null} GET /v2/strategies/:id ``` **Required Parameters:** | Parameter | Description | Example | | - | - | - | | :id | Strategy ID | `Aave-USDC-polygon` | **Example Request:** ```http theme={null} GET /v2/strategies/Aave-USDC-polygon ``` Full guide: [Get available protocols](/guides/yield/get-available-protocols). Generate deposit bytecode, then execute the returned `bytecode[]` on-chain (batch legs on the same chain into one atomic transaction): ```http theme={null} GET /strategies/:id/bytecode?action=lend&amount=&wallet=
``` Same-chain deposits confirm with the on-chain receipt. Cross-chain deposits settle asynchronously — poll with the `id` from the deposit bytecode response until it completes: ```http theme={null} GET /actions/:id ``` Full guide: [Deposit to Yield](/guides/yield/deposit). Read the wallet's live positions — principal, profit, and any pending requests: ```http theme={null} GET /v2/wallets/:address?include=earn ``` Full guide: [Check Positions](/guides/yield/check-positions). Generate withdraw bytecode, then execute it on-chain. Cross-chain withdrawals are async — track them via `GET /actions/:id`: ```http theme={null} GET /strategies/:id/bytecode?action=withdraw&amount=&wallet=
``` Full guide: [Withdraw from Yield](/guides/yield/withdraw). ## UI Integration Example Protocol Selection UI *Example UI implementation showing protocol selection* ## Next Steps List strategies and read a strategy's details Execute a deposit into a yield protocol # Deposit to Yield Source: https://docs.pods.finance/guides/yield/deposit Learn how to generate transaction bytecode for depositing into yield positions In this section, you'll learn how to request ready-to-use transaction data (bytecode) for depositing into a yield position. **Prerequisites:** * [How to get available protocols](/guides/yield/get-available-protocols) ## Request Transaction Data Fetch ready-to-use transaction data using: ```http theme={null} GET /strategies/:id/bytecode ``` **Required Parameters:** | Parameter | Description | Example | | - | - | - | | action | Type of operation | `lend` or `request-lend` | | amount | Amount in token's smallest unit | `10000000` | | wallet | Wallet that will own the position | `0xUserWalletAddress` | The `request-lend` action is only for [Ondo Global Markets](/guides/ondoglobal-markets) strategies (tokenized stocks & ETFs), whose orders settle asynchronously. For every other yield strategy, use `lend`. **Preview quotes without a connected wallet.** `wallet` also accepts the zero address (`0x000…0`) or a well-known burn address (e.g. `0x000…dEaD`, `0x111…`, `0x222…`). The response is priced exactly like a real request but includes `previewOnly: true` and omits `id`, `bytecode`, `userOperation`, `singleUseAddress`, and `orderUid` — no `Action` is created, and the address is never persisted or synced to Moralis/Helius. A `previewOnly` response has no bytecode to sign. Use the user's real, owned wallet address for any request you intend to execute. Pick the section that matches how the user is funding the deposit. Each shows the extra parameters, an example request, and the response you get back. ### Simple deposit Deposit the strategy's underlying token directly on the strategy's chain — no swap or bridge. Only the required parameters above are needed, and `crossChain.isCrossChain` stays `false`. **Example Request:** ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=lend&amount=10000000&wallet=0xUserWalletAddress ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "bytecode": [ { "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD", "value": "0", "data": "0xe8eda9df...", "chainId": "137" } ] } ``` ### Same-chain swap deposit Hold a different token on the strategy's chain? Pass `fromTokenAddress` (with a `fromChainId`/`chainIdIn` matching the strategy chain) to swap into the underlying asset before depositing. The response prepends the swap transaction(s) before the deposit bytecode, and `crossChain.isCrossChain` stays `false`. **Additional Parameters:** | Parameter | Description | Example | | - | - | - | | fromChainId or chainIdIn | Source chain ID. Use the strategy chain for same-chain swaps. Required when `fromTokenAddress` is provided. | `137` (Polygon) | | fromTokenAddress | Token address to swap from before depositing (same-chain). When provided, the response includes swap transaction(s) before the deposit bytecode. | `0x7ceB2...f619` (WETH) | When `fromTokenAddress` is provided with a `fromChainId` matching the strategy chain, the API automatically prepends a same-chain swap from `fromTokenAddress` to the strategy's underlying asset. The `amount` parameter refers to the amount of `fromTokenAddress` to swap, and `crossChain.isCrossChain` remains `false`. **Example Request:** If you hold WETH but want to deposit into a USDC strategy, pass `fromTokenAddress` to swap first: ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=lend&amount=1000000000000000000&wallet=0xUserWalletAddress&fromChainId=137&fromTokenAddress=0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "bytecode": [ { "to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", "value": "0", "data": "0x54e3f31b...", "chainId": "137" }, { "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD", "value": "0", "data": "0xe8eda9df...", "chainId": "137" } ] } ``` ### Cross-chain swap deposit Funding from another chain? Pass a `fromChainId`/`chainIdIn` that differs from the strategy's chain. The response adds a `quote` object describing the bridge selection, the resolved tokens, and the expected/minimum output amount, and sets `crossChain.isCrossChain` to `true`. **Additional Parameters:** | Parameter | Description | Example | | - | - | - | | fromChainId or chainIdIn | Source chain ID for the funds being bridged before deposit | `8453` (Base) | | fromTokenAddress | Source token address. Required when `fromChainId` or `chainIdIn` is provided. Must be a valid ERC-20 token on the source chain; returns `FROM_TOKEN_NOT_SUPPORTED` (400) if the address is not a valid ERC-20 contract on the specified chain. | `0x833589...2913` (USDC on Base) | **Example Request:** To bridge USDC on Base into a USDC yield strategy on Arbitrum, pass the Base `fromChainId` and `fromTokenAddress`: ```http theme={null} GET /strategies/KPK-podsUSDCy-arbitrum/bytecode?action=lend&amount=1000000&wallet=0xUserWalletAddress&fromChainId=8453&fromTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 8453, "chainIdOut": 42161, "id": "6a04f2f75bf7233c6220b3ca", "crossChain": { "isCrossChain": true, "chainIdIn": 8453, "chainIdOut": 42161 }, "quote": { "bridge": "CCTP", "sender": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "fromChainId": 8453, "fromTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "toChainId": 42161, "toTokenAddress": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "inputAmount": { "value": "1000000", "decimals": 6, "humanized": "1.0", "symbol": "USDC" }, "outputAmount": { "value": "999870", "decimals": 18, "humanized": "0.00000000000099987", "symbol": "KPK-podsUSDCy" }, "minimumOutputAmount": { "value": "999870", "decimals": 18, "humanized": "0.00000000000099987", "symbol": "KPK-podsUSDCy" }, "chainIdIn": 8453, "chainIdOut": 42161 }, "bytecode": [ { "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "value": "0", "data": "0x095ea7b3...", "chainId": "8453" }, { "to": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", "value": "0", "data": "0x8e0250ee...", "chainId": "8453" } ] } ``` The top-level `id` is used for execution tracking — store it and pass it to `GET /actions/:id` to track the cross-chain deposit through completion (see [Checking Cross-Chain Deposit Status](#checking-cross-chain-deposit-status) below). The `quote` object describes which bridge was selected (`bridge`), the expected `outputAmount`, and the worst-case `minimumOutputAmount` (slippage floor). Display these to the user before they sign. ### Output format options By default the endpoint returns raw `bytecode`. These optional parameters change the response format and apply to any of the deposit types above. | Parameter | Description | Example | | - | - | - | | output | Response format: `bytecode` (default), `userOperation`, `fireblocks`, or `instructions` (Solana) | `fireblocks` | | accountId | Fireblocks vault account ID. **Required when `output=fireblocks`** | `12` | | webhookURL | Optional per-action webhook endpoint (URI). When set, action update webhooks are delivered to this URL instead of the customer default webhook URL. | `https://example.com/webhooks/strategy-action` | **Example Request (Fireblocks output):** If you are using Fireblocks custody, set `output=fireblocks` and pass your vault `accountId`. The `wallet` parameter must be a smart wallet previously provisioned via `POST /fireblocks-smart-account` (see [Fireblocks Integration](/external-integrations/fireblocks-integration)). ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=lend&amount=10000000&wallet=0xSafeAddress&output=fireblocks&accountId=YOUR_VAULT_ACCOUNT_ID ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "transactionData": { "transactionRequest": { "operation": "CONTRACT_CALL", "assetId": "MATIC_POLYGON", "source": { "type": "VAULT_ACCOUNT", "id": "YOUR_VAULT_ACCOUNT_ID" }, "destination": { "type": "ONE_TIME_ADDRESS", "oneTimeAddress": { "address": "0xSafeAddress" } }, "amount": "0", "extraParameters": { "contractCallData": "0x6a761202..." }, "note": "lend aave-usdc-polygon", "failOnLowFee": true } } } ``` The `transactionData.transactionRequest` payload can be submitted directly to the Fireblocks Transactions API (`POST /v1/transactions`) — no further encoding required. The Earn Owner vault must be configured as the signer (see [Fireblocks Integration](/external-integrations/fireblocks-integration#1-fireblocks-setup)). Use `GET /strategies` to list available protocols, then pass the chosen strategy ID into the bytecode endpoint above. ## Executing the Transaction Batch all `bytecode` legs on the same `chainId` into **one atomic transaction**. Do not loop `sendTransaction` per leg. See [Executing Bytecode](/getting-started/executing-bytecode). | Wallet stack | Approach | | - | - | | EOA | [EIP-7702 `executeBatch`](/examples/javascript/using-eip7702) | | Smart account | `output=userOperation` → [Smart Account Deposit](/examples/javascript/smart-account-deposit) | | Fireblocks | `output=fireblocks` → [Fireblocks Integration](/external-integrations/fireblocks-integration) | ```javascript theme={null} // EOA — batch result.bytecode into ONE tx (see strategy-deposit-eip7702.js) const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'lend', amount: '10000000', wallet: walletAddress } }) // Do not: for (const tx of result.bytecode) { await signer.sendTransaction(...) } console.log('Legs to batch atomically:', result.bytecode.length) ``` ## Checking Cross-Chain Deposit Status Cross-chain deposits are asynchronous: the source-chain bridge transaction is signed first, then the bridged funds are delivered and deposited on the destination chain. To track progress to completion, use the `id` returned in the bytecode response: ```http theme={null} GET /actions/:id ``` **Path Parameters:** | Parameter | Description | Example | | - | - | - | | id | The `id` returned by `GET /strategies/:id/bytecode` | `6a04f2f75bf7233c6220b3ca` | **Example Request:** ```http theme={null} GET /actions/6a04f2f75bf7233c6220b3ca ``` **Example Response (deposit in flight):** ```json theme={null} { "id": "6a04f2f75bf7233c6220b3ca", "type": "CROSSCHAIN_INVESTMENT_DEPOSIT", "status": "PENDING", "strategyId": "65a1b2c3d4e5f6a7b8c9d0e1", "strategySlug": "aave-usdc-polygon", "initialDate": "2026-05-19T14:30:15Z", "createdAt": "2026-05-19T14:30:00Z", "originAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "receiverAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "amounts": { "assetIn": { "token": { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 }, "chain": { "id": 8453, "name": "base" }, "amountRaw": "1000000", "amountHumanized": "1.0", "amountInUSD": "1.00" }, "assetOut": { "token": { "address": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "symbol": "KPK-podsUSDCy", "decimals": 18 }, "chain": { "id": 42161, "name": "arbitrum" }, "amountRaw": "999870", "amountHumanized": "0.00000000000099987", "amountInUSD": "1.00" } }, "transactions": [ { "chain": "base", "txHash": "0xabc...", "blockNumber": 19234567, "timestamp": "2026-05-19T14:30:15Z", "status": "SUCCESS", "underlyingAssetFeeValue": "0" }, { "chain": "arbitrum", "txHash": null, "blockNumber": null, "timestamp": null, "status": "PENDING", "underlyingAssetFeeValue": null } ], "finishedAt": null, "underlyingAssetFeeValue": "0" } ``` **Status values:** | Status | Meaning | | - | - | | `INITIAL` | Created, awaiting on-chain execution | | `PENDING` | At least one transaction broadcast; awaiting confirmations | | `SUCCESS` | All steps completed; funds are deposited | | `FAILED` | A step failed | | `REFUND_REQUIRED` | Bridged funds arrived but the deposit step failed; refund is needed | | `REFUNDED` | Refund completed | | `EXPIRED` | Quote expired before execution | Poll this endpoint until `status` reaches a terminal value (`SUCCESS`, `FAILED`, `REFUNDED`, or `EXPIRED`). The `transactions` array lists each on-chain hash as it is confirmed, so you can surface progress to the user as the bridge and the destination-chain deposit complete in sequence. Same-chain deposits do not need this polling — the deposit completes inline with the user's signed transaction. The `id` is still returned for record-keeping and can be queried the same way. ## Webhook Notifications As an alternative to polling, Pods can push every status transition to a URL you control. The payload is identical to `GET /actions/:id`, so you can drive the same UI off either source. Default webhook URL configuration is currently provisioned manually — contact the Pods team to register your default `webhookURL` (and any optional custom headers, e.g. an HMAC signature header). For a single deposit request, pass `webhookURL` on the bytecode endpoint to override the customer-level default. Self-service configuration via the admin panel is coming soon. **Transport:** | Field | Value | | - | - | | Method | `POST` | | URL | Your `webhookURL` | | `Content-Type` | `application/json` | | Custom headers | Any headers you configure with the team are sent on every request | **Body shape:** ```json theme={null} { "messageType": "ACTION_UPDATE", "data": { /* same object returned by GET /actions/:id */ } } ``` **Example payload (cross-chain deposit reaching `SUCCESS`):** ```json theme={null} { "messageType": "ACTION_UPDATE", "data": { "id": "6a04f2f75bf7233c6220b3ca", "type": "CROSSCHAIN_INVESTMENT_DEPOSIT", "status": "SUCCESS", "strategyId": "65a1b2c3d4e5f6a7b8c9d0e1", "strategySlug": "aave-usdc-polygon", "initialDate": "2026-05-19T14:30:15Z", "createdAt": "2026-05-19T14:30:00Z", "originAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "receiverAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "amounts": { "assetIn": { "token": { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 }, "chain": { "id": 8453, "name": "base" }, "amountRaw": "1000000", "amountHumanized": "1.0", "amountInUSD": "1.00" }, "assetOut": { "token": { "address": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "symbol": "KPK-podsUSDCy", "decimals": 18 }, "chain": { "id": 42161, "name": "arbitrum" }, "amountRaw": "999870", "amountHumanized": "0.00000000000099987", "amountInUSD": "1.00" } }, "transactions": [ { "chain": "base", "txHash": "0xabc...", "blockNumber": 19234567, "timestamp": "2026-05-19T14:30:15Z", "status": "SUCCESS" }, { "chain": "arbitrum", "txHash": "0xdef...", "blockNumber": 248123456, "timestamp": "2026-05-19T14:33:02Z", "status": "SUCCESS" } ], "finishedAt": "2026-05-19T14:33:02Z" } } ``` **Delivery semantics:** * A request fires for **every** status change, so you may receive multiple deliveries per deposit (`INITIAL` → `PENDING` → `SUCCESS`). * Pods expects a `2xx` response. Non-`2xx` responses are recorded but not automatically retried; reconcile with `GET /actions/:id` if you need at-least-once guarantees. * Respond as quickly as possible; do the heavy work asynchronously on your side. ## Next Steps Monitor your open yield positions Learn how to withdraw from positions # Get available protocols Source: https://docs.pods.finance/guides/yield/get-available-protocols List yield strategies and read a strategy's metadata, current rates, and rate history Retrieve the catalog of yield strategies, then read a specific strategy's metadata, APY, and rate history. Every request is authenticated with the `x-api-key` header. ## List strategies ```http theme={null} GET /strategies ``` Returns `{ "data": [ …strategies ], "pagination": { … } }`. **Example Response:** ```json theme={null} { "data": [ { "id": "Aave-USDC-polygon", "protocol": "Aave", "network": "polygon", "networkId": "137", "asset": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "assetName": "USDC", "assetDecimals": 6, "paused": false, "fee": "0", "performanceFeeBps": "0", "availableActions": ["lend", "withdraw"], "apy": 0.0417, "grossAPY": 0.0417, "netAPY": 0.0417, "avgApy": 0.0412, "inceptionApy": 0.0385 }, { ... }, ], "pagination": { "page": 1, "limit": 100, "total": 128, "totalPages": 2, "hasMore": true } } ``` **Query parameters:** | Parameter | Description | Default | | - | - | - | | `page` | Page number | `1` | | `limit` | Results per page | `100` | | `network` | Filter by network (e.g. `polygon`) | — | | `protocol` | Filter by protocol (e.g. `Aave`) | — | | `category` | Comma-separated category tags (e.g. `yield`, or Ondo `stock`, `etf`, `portfolio`) | — | **Example:** ```http theme={null} GET /strategies?protocol=Aave&network=polygon ``` Use `pagination.hasMore`, `pagination.total`, and `pagination.totalPages` to page through results. See the OpenAPI description for the full `ListPagination` shape. **Example:** ```http theme={null} GET /strategies?page=2&limit=50 ``` **Example Response:** ```json theme={null} { "data": [ { ... }, ], "pagination": { "page": 2, "limit": 50, "total": 128, "totalPages": 3, "hasMore": true } } ``` ## Get a strategy's details ```http theme={null} GET /v2/strategies/:id ``` | Parameter | Description | Example | | - | - | - | | `:id` | Strategy ID | `Aave-USDC-polygon` | **Example request:** ```http theme={null} GET /v2/strategies/Aave-USDC-polygon ``` ## Response The response groups metadata under `strategy`, current rate metrics under `spotPosition`, and historical rates under `rateHistory`. **Response includes:** * Composite strategy ID (`strategy.id`) in the format `{protocol}-{assetName}-{network}` * Protocol branding (`strategy.protocolInfo`) — `{ name, logo }` stored on each `Strategy` document in MongoDB * Network (`strategy.chain`) — `{ name, id, explorer, logo }` * Tokens (`strategy.assetToken`, `strategy.underlyingToken`) — `{ name, address, symbol, decimals, logo? }` * Current APY rate (`spotPosition.apy`), fee-adjusted rates (`grossAPY`, `netAPY`), and historical APY (`avgApy`, `inceptionApy`) * Asset info (`strategy.asset` contract address, `assetName`, `assetDecimals`) * Protocol fee in basis points (`strategy.fee`) * Available actions (`strategy.availableActions`) * Pause status (`strategy.paused`) **Example Response:** ```json theme={null} { "strategy": { "id": "Aave-USDC-polygon", "protocol": "Aave", "network": "polygon", "networkId": "137", "asset": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "assetName": "USDC", "assetDecimals": 6, "paused": false, "fee": "0", "performanceFeeBps": "0", "availableActions": ["lend", "withdraw"] }, "spotPosition": { "apy": 0.0417, "grossAPY": 0.0417, "netAPY": 0.0417, "avgApy": 0.0412, "inceptionApy": 0.0385 }, "rateHistory": { "items": [], "pagination": { "page": 1, "limit": 100, "total": 0, "totalPages": 0, "hasMore": false } } } ``` The `fee` and `performanceFeeBps` fields are in basis points (1 BPS = 0.01%). See [Fees](/getting-started/fees) for the full fee structure and how markup fees work. ## Next steps Generate and execute deposit bytecode Monitor your yield positions # Testnet: Aave (Base Sepolia) Source: https://docs.pods.finance/guides/yield/testnet-aave-base-sepolia Test the full lend / withdraw integration end-to-end on the Base Sepolia testnet using the Aave strategy and faucet USDC This strategy is a standard **DeFi lending position** (`protocol: "Aave"`, `network: base-sepolia`) running entirely on the **Base Sepolia testnet** (chain `84532`). Use it to integrate and test the Pods lend / withdraw flow end-to-end **before** pointing your client at a mainnet Aave strategy. You lend faucet USDC into the Aave v3 pool and receive an interest-bearing aToken position; withdrawing redeems it back to USDC. Everything is **same-chain and synchronous** — there is no bridge leg and no async order fill. **Testnet only.** Funds have no real value. USD / TVL figures for this strategy are **not** meaningful — the faucet USDC has no price feed. Drive your test assertions off raw underlying balances and the on-chain aToken balance, not USD values. ## Introduction | Item | Value | | - | - | | **Strategy ID** | `aave-aBasSepUSDC-base-sepolia` | | **Protocol** | `Aave` | | **Network** | Base Sepolia (`84532`) | | **Strategy asset** | `aBasSepUSDC` aToken — `0x10F1A9D11CDf50041f3f8cB7191CBE2f31750ACC` (6 decimals) | | **Underlying asset** | Faucet USDC — `0xba50Cd2A20f6DA35D788639E581bca8d0B5d4D5f` (6 decimals) | | **Actions** | `lend` (deposit) / `withdraw` (redeem) | | **Settlement** | Synchronous, single chain (no bridge, no async fill) | ``` Lend: USDC (Base Sepolia) → [client signs on Base Sepolia] → aBasSepUSDC shares Withdraw: aBasSepUSDC shares → [client signs on Base Sepolia] → USDC (Base Sepolia) Position: queried with the wallet on Base Sepolia ``` `GET /strategies/:id/bytecode` returns `bytecode[]` (to execute on-chain) plus tracking fields (`id`, `feeCharged`, `chainIdIn`, `chainIdOut`, `crossChain`). For a direct same-chain lend/withdraw the `quote` field is **`null`** — it is only populated when an extra swap or bridge leg is involved. aTokens are **1:1** with the underlying at any instant — 1 USDC lent ≈ 1 aBasSepUSDC share. ## Initial setup Before your first request, the signing wallet must be funded **on Base Sepolia**: There is no point buying it — it has no value. **Mint it from the Aave faucet** (see [Get the faucet](#1-get-the-faucet-usdc) below). Your wallet signs and broadcasts the `lend` / `withdraw` transaction, so it needs native ETH to pay gas. Grab some from any Base Sepolia faucet. Every request is authenticated with the `x-api-key` header. See [Authentication](/getting-started/authentication). Once the wallet holds faucet USDC **and** a little Base Sepolia ETH, you can run the full lend → position → withdraw cycle. ## 1. Get the faucet (USDC) The underlying is a **mock USDC** deployed by Aave for Base Sepolia. You acquire it by minting from the Aave testnet faucet — you cannot buy it and it carries no value. | Token | Address | Decimals | Chain | | - | - | - | - | | Faucet USDC | `0xba50Cd2A20f6DA35D788639E581bca8d0B5d4D5f` | 6 | `84532` | | aBasSepUSDC | `0x10F1A9D11CDf50041f3f8cB7191CBE2f31750ACC` | 6 | `84532` | You only ever mint **USDC** (the underlying). The `aBasSepUSDC` aToken is minted **by the Aave pool** when you `lend` — never mint or transfer it directly. ### Option A — Aave faucet UI (recommended) Go to [app.aave.com/faucet](https://app.aave.com/faucet/). Switch the network selector to **testnet** mode and pick **Base Sepolia**. Connect the wallet you will use to sign Pods transactions. Click **Faucet** on **USDC** — it mints a fixed batch of test USDC to your wallet (repeatable). Confirm the transaction. After it confirms, the faucet USDC (`0xba50Cd2A20f6DA35D788639E581bca8d0B5d4D5f`) shows up in your wallet balance. Import the token address if it doesn't appear automatically. ### Option B — mint directly via BaseScan The Aave faucet is an on-chain contract; you can mint without the UI: Get the **Faucet** address for the Aave v3 Base Sepolia market from the Aave [`address-book`](https://github.com/bgd-labs/aave-address-book) (testnet markets), or read it from the Aave UI transaction in Option A. Open it on [sepolia.basescan.org](https://sepolia.basescan.org) → **Contract → Write Contract → Connect**. Call `mint(address token, address to, uint256 amount)` with `token` = the faucet USDC, `to` = your wallet, `amount` = base units (6 decimals — `100000000` = 100 USDC). ### Base Sepolia ETH for gas You also need a small amount of native ETH on Base Sepolia to pay gas. A fraction of an ETH covers many test transactions. Public faucets (these are the ones we used): * [Alchemy — Base Sepolia faucet](https://www.alchemy.com/faucets/base-sepolia) * [Chainlink — Base Sepolia faucet](https://faucets.chain.link/base-sepolia) * Coinbase Developer Platform / QuickNode Base Sepolia faucets * Or bridge Sepolia ETH → Base Sepolia via the official Base bridge These faucets dispense **ETH for gas**, not USDC. Get the underlying USDC from the Aave faucet ([Option A](#option-a-aave-faucet-ui-recommended) / [Option B](#option-b-mint-directly-via-basescan)) above. ## 2. Integration flow ``` LEND (deposit) WITHDRAW (redeem) ────────────── ───────────────── 1. GET /strategies?protocol=Aave&network=base-sepolia → strategyId (same) 2. Confirm wallet has faucet USDC + ETH Confirm wallet holds the position 3. GET /strategies/:id/bytecode GET /strategies/:id/bytecode action=lend action=withdraw amount= amount= wallet=
wallet=
4. Review feeCharged / amounts Review feeCharged / amounts 5. Sign & broadcast bytecode[] on Base Sepolia (1 tx, synchronous) (same) 6. Wait for the on-chain receipt (same) 7. Refresh position (same) ``` ## 3. Resolve the strategy ID The strategy ID is fixed (`aave-aBasSepUSDC-base-sepolia`), but you can also discover it: ```http theme={null} GET /strategies?protocol=Aave&network=base-sepolia ``` Returns `{ data: [...], pagination }`. Pick the strategy where `asset` matches the aToken (`0x10F1A9D11CDf50041f3f8cB7191CBE2f31750ACC`) and `networkId === 84532`. Use actions `lend` and `withdraw` from `availableActions`. ## 4. Lend — `lend` ```http theme={null} GET /strategies/:strategyId/bytecode ``` | Parameter | Required | Description | | - | - | - | | `action` | yes | `lend` | | `wallet` | yes | Address that signs on Base Sepolia and holds the position | | `amount` | yes | Faucet USDC in **base units** (6 decimals — `1000000` = 1 USDC) | | `output` | no | `bytecode` (default) / `userOperation` / `fireblocks` / `instructions` | **Example request:** ```http theme={null} GET /strategies/aave-aBasSepUSDC-base-sepolia/bytecode?action=lend&amount=10000000&wallet=0x1234... ``` **Example response:** ```jsonc theme={null} { "feeCharged": "0", // entry fee deducted from the deposit, in USDC base units "chainIdIn": 84532, "chainIdOut": 84532, "id": "65f3a1c8e9b2d4f8a7c1d2e3", // action id (record-keeping; queryable via GET /actions/:id) "crossChain": { "isCrossChain": false, "chainIdIn": 84532, "chainIdOut": 84532 }, "quote": null, // null for a direct same-chain lend "bytecode": [ // typically: entry-fee transfer (if any) → ERC20 approve(USDC, AavePool) → AavePool.supply → tracking { "to": "0x…", "value": "0", "data": "0x…", "chainId": "84532" } ] } ``` All `bytecode` legs are on `chainId 84532` (Base Sepolia). Sign and broadcast them on Base Sepolia; the position is live as soon as the transaction confirms. An **entry fee** may be deducted from the deposited amount (configured per customer). The amount charged is reported in the top-level `feeCharged` field, and the aToken shares you receive correspond to the amount **after** the fee. ## 5. Withdraw — `withdraw` ```http theme={null} GET /strategies/:strategyId/bytecode ``` | Parameter | Required | Description | | - | - | - | | `action` | yes | `withdraw` | | `wallet` | yes | Address on Base Sepolia that holds the position | | `amount` | yes | Underlying USDC to redeem, in **base units** (6 decimals) | | `output` | no | `bytecode` (default) / `userOperation` / `fireblocks` / `instructions` | Because aTokens are 1:1 with the underlying, `amount` is denominated in **USDC base units** (not a separate share unit). To exit the full position, use the current position value (see [Position](#6-position)). The response shape matches lend (`feeCharged`, `chainIdIn`, `chainIdOut`, `id`, `crossChain`, `quote: null`, `bytecode`); all legs are on `chainId 84532`. A **performance fee** may be charged on profit at withdraw time and transferred out as part of the bytecode — the redeemed amount reflects the amount after that fee. ## 6. Position The `wallet` is the same EVM address used for signing (always on Base Sepolia for this strategy). Pass `wallet` to get position fields; omit it to get only APY fields. **Single strategy position:** ```http theme={null} GET /v2/strategies/:strategyId?wallet=
``` or `GET /v2/wallets/:address/strategies/:strategyId` (same shape). **Wallet snapshot (all positions):** `GET /v2/wallets/:address?include=all` — Aave positions appear under `earn.positions` where `strategy.protocol === "Aave"`. **Position shape (with `wallet`).** Balance fields are `AssetAmount` objects (`{ value, decimals, symbol, displaySymbol }`), where `value` is the raw integer string: ```jsonc theme={null} { "spotPosition": { "currentPosition": { "value": "", "decimals": 6, "symbol": "USDC", "displaySymbol": "USDC" }, "principal": { "value": "", "decimals": 6, "symbol": "USDC", "displaySymbol": "USDC" }, "profit": { "value": "", "decimals": 6, "symbol": "USDC", "displaySymbol": "USDC" }, "cumulativeProfit": { "value": "", "decimals": 6, "symbol": "USDC", "displaySymbol": "USDC" }, "profitInUSD": 0, // null/0 on testnet — no USDC price feed "underlyingBalanceUSD": 0, // null/0 on testnet — no USDC price feed "apy": 0.0, // net APY (after performance fee) "grossAPY": 0.0, "netAPY": 0.0, "apyAfterFees": 0.0, "inceptionApy": 0.0, "avgApy": 0.0, "performanceFee": "" // optional }, "strategy": { "protocol": "Aave", "id": "aave-aBasSepUSDC-base-sepolia", "asset": "0x10F1A9D11CDf50041f3f8cB7191CBE2f31750ACC", "networkId": 84532 } } ``` Without `wallet`, `spotPosition` contains only `apy`, `grossAPY`, `netAPY`, `inceptionApy`, `avgApy` (and optionally `performanceFee`) — no balance fields. `underlyingBalanceUSD`, `profitInUSD` and any TVL/USD figure are **not meaningful** on testnet — the mock USDC has no USD rate. Assert on `currentPosition.value` (raw underlying) and the on-chain aToken balance. ## 7. Track the transaction Settlement is **synchronous**: once the Base Sepolia transaction confirms, the lend / withdraw is complete — there is no separate async order to await. * Wait for the on-chain receipt (`status === 1`) on `chainId 84532`. * Refresh the position endpoints after confirmation. * Use `GET /actions?walletAddress=
` for wallet activity, or `GET /v2/strategies/:strategyId?wallet=
` for this strategy's position and history. Ignore `INITIAL` status (a quote artifact). Optionally subscribe to `wss:///updates` and the wallet channel for `action_update` events. ## 8. Network & addresses | Item | Value | | - | - | | Network | Base Sepolia | | Chain ID | `84532` | | Explorer | [https://sepolia.basescan.org](https://sepolia.basescan.org) | | Native gas asset | ETH | | Aave Pool | `0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27` | | Aave DataProvider | `0x3cB7B00B6C09B71998124196691e8bF2694De863` | | Faucet USDC (underlying) | `0xba50Cd2A20f6DA35D788639E581bca8d0B5d4D5f` (6 decimals) | | aBasSepUSDC (aToken) | `0x10F1A9D11CDf50041f3f8cB7191CBE2f31750ACC` (6 decimals) | ## 9. End-to-end testnet run A full smoke test from a clean wallet: Mint faucet USDC (§1) and grab a little Base Sepolia ETH for gas. Verify both balances on BaseScan. Confirm `availableActions` includes `lend` and `withdraw` (§3). ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/aave-aBasSepUSDC-base-sepolia/bytecode\ ?action=lend&amount=10000000&wallet=0xYOUR_WALLET" ``` Sign every `bytecode` leg (`chainId 84532`) on Base Sepolia. `currentPosition.value` should reflect ≈ 10 USDC in base units (minus entry fee, if configured): ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/v2/strategies/aave-aBasSepUSDC-base-sepolia?wallet=0xYOUR_WALLET" ``` ```bash theme={null} curl -H "x-api-key: $API_KEY" \ "https://api.pods.finance/strategies/aave-aBasSepUSDC-base-sepolia/bytecode\ ?action=withdraw&amount=10000000&wallet=0xYOUR_WALLET" ``` Sign the `bytecode` on Base Sepolia. Refresh the position — `currentPosition.value` drops back toward 0 and the USDC returns to the wallet (minus any performance fee). Keep amounts small, re-run the faucet whenever you run low, and assert on **raw underlying** balances / on-chain aToken balance rather than USD values (no testnet price feed). ## 10. Errors The amount-specific errors (`LEND_AMOUNT_TOO_SMALL`, `WITHDRAW_AMOUNT_TOO_SMALL`, `WITHDRAW_AMOUNT_REQUIRED`, `REQUEST_LEND_AMOUNT_TOO_LOW`) are **Ondo-stock specific** and do **not** apply to this Aave strategy. The ones you can actually hit here: | Code | HTTP | When | | - | - | - | | `NO_API_KEY` / `INVALID_API_KEY` | 401 | Missing or invalid `x-api-key` | | `QUOTE_NOT_FOUND` | 404 | Quote no longer available — re-request the bytecode | | `QUOTE_EXPIRED` | 400 | Quote expired — re-request the bytecode | | `BYTECODE_GENERATION_FAILED` | 500 | Bytecode could not be built (e.g. wallet has no USDC / no position to withdraw) | | `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error | There is **no** dedicated insufficient-balance error code: lending more USDC than the wallet holds (or withdrawing more than the position) surfaces as `BYTECODE_GENERATION_FAILED`, or the on-chain transaction reverts. See [Error Codes](/error-codes) for the full list, and preserve `error.code` in your UI for user-friendly messages. ## Integration notes 1. **Use the generic actions:** `lend` (deposit) and `withdraw` (redeem). There is **no** `request-lend` / `request-withdraw` here — settlement is synchronous and same-chain. 2. **Sign on Base Sepolia (`84532`)** for every leg; the position lives on the same wallet/chain. 3. **Amounts are in USDC base units (6 decimals)** for both lend and withdraw — aTokens are 1:1 with the underlying, so there is no separate share unit to convert. 4. **Fees:** an entry fee may reduce the lent amount (reported as top-level `feeCharged`); a performance fee may be taken on profit at withdraw. 5. **Testnet caveats:** USD / TVL values are unreliable (no price feed) — assert on raw balances. 6. **Smart accounts:** pass `output=userOperation`, or `output=fireblocks` with `accountId`. ## Next Steps The full deposit reference (same-chain & cross-chain) for mainnet strategies Monitor your open yield positions # Withdraw from Yield Source: https://docs.pods.finance/guides/yield/withdraw Learn how to generate transaction bytecode for withdrawing from yield positions In this section, you'll learn how to request ready-to-use transaction data (bytecode) for withdrawing from a yield position. **Prerequisites:** * [How to Deposit](/guides/yield/deposit) * [How to Check Open Positions](/guides/yield/check-positions) ## Protocol-Specific Approach Use this when you want to work with a specific yield protocol. **Request Transaction Data** Fetch specific protocol information using: ```http theme={null} GET /strategies/:id/bytecode ``` **Required Parameters:** | Parameter | Description | Example | | - | - | - | | action | Type of operation | `withdraw` or `request-withdraw` | | amount | Amount in token's smallest unit | `10000000` | | wallet | Wallet that owns the position | `0x1234...` | The `request-withdraw` action is only for [Ondo Global Markets](/guides/ondoglobal-markets) strategies (tokenized stocks & ETFs), whose orders settle asynchronously. For every other yield strategy, use `withdraw`. **Preview quotes without a connected wallet.** `wallet` also accepts the zero address (`0x000…0`) or a well-known burn address (e.g. `0x000…dEaD`, `0x111…`, `0x222…`). The response is priced exactly like a real request but includes `previewOnly: true` and omits `id`, `bytecode`, `userOperation`, `singleUseAddress`, and `orderUid` — no `Action` is created, and the address is never persisted or synced to Moralis/Helius. A `previewOnly` response has no bytecode to sign. Use the user's real, owned wallet address for any request you intend to execute. Pick the section that matches where the user wants to receive the withdrawn funds. Each shows the extra parameters, an example request, and the response you get back. ### Simple withdraw Withdraw to the strategy's underlying token on the strategy's own chain — no bridge. Only the required parameters above are needed, and `crossChain.isCrossChain` stays `false`. For supported ERC-4626 strategies, add `withdrawAll=true` when the user intends to redeem the wallet's complete vault-share balance. Continue sending `amount` as the expected underlying amount; partial withdrawals must omit `withdrawAll` and remain asset-denominated. **Example Request:** ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=withdraw&amount=10000000&wallet=0x1234... ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "bytecode": [ { "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD", "value": "0", "data": "0xe8eda9df...", "chainId": "137" } ] } ``` ### Same-chain swap withdraw Want to receive a different token on the strategy's own chain? Pass `toTokenAddress` (with a `toChainId`/`chainIdOut` matching the strategy chain) to swap the withdrawn underlying into that token. The response appends the swap transaction(s) after the withdraw bytecode, and `crossChain.isCrossChain` stays `false`. **Additional Parameters:** | Parameter | Description | Example | | - | - | - | | toChainId or chainIdOut | Destination chain ID. Use the strategy chain for same-chain swaps. Required when `toTokenAddress` is provided. | `137` (Polygon) | | toTokenAddress | Token address to receive after withdrawing (same-chain). When it differs from the underlying asset, the response appends swap transaction(s) after the withdraw. | `0x7ceB2...f619` (WETH) | When `toTokenAddress` is provided with a `toChainId` matching the strategy chain, the API automatically appends a same-chain swap from the strategy's underlying asset to `toTokenAddress` after the withdraw. The `amount` parameter refers to the underlying amount withdrawn (before the swap), and `crossChain.isCrossChain` remains `false`. **Example Request:** If you hold a position in a USDC strategy but want to receive WETH on the same chain, pass `toTokenAddress` to swap after withdrawing: ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=withdraw&amount=10000000&wallet=0x1234...&toChainId=137&toTokenAddress=0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619 ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": { "bridge": "1INCH", "sender": "0x1234...", "fromChainId": 137, "fromTokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "toChainId": 137, "toTokenAddress": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", "inputAmount": { "value": "10000000", "decimals": 6, "humanized": "10.0", "symbol": "USDC" }, "outputAmount": { "value": "2500000000000000", "decimals": 18, "humanized": "0.0025", "symbol": "WETH" }, "minimumOutputAmount": { "value": "2487500000000000", "decimals": 18, "humanized": "0.0024875", "symbol": "WETH" } }, "bytecode": [ { "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD", "value": "0", "data": "0x69328dec...", "chainId": "137" }, { "to": "0x1111111254EEB25477B68fb85Ed929f73A960582", "value": "0", "data": "0x12aa3caf...", "chainId": "137" } ] } ``` Unlike a bridge quote, a same-chain swap quote has `fromChainId === toChainId` (both the strategy chain), and `crossChain.isCrossChain` stays `false` — no cross-chain tracking is needed. ### Cross-chain swap withdraw Want the funds on another chain? Pass a `toChainId`/`chainIdOut` (with a `toTokenAddress`) that differs from the strategy's chain. The response adds a `quote` object describing the bridge selection, the resolved tokens, and the expected/minimum output amount, and sets `crossChain.isCrossChain` to `true`. **Additional Parameters:** | Parameter | Description | Example | | - | - | - | | toChainId or chainIdOut | Destination chain ID for the withdrawn funds | `8453` (Base) | | toTokenAddress | Destination token address. **Required when `toChainId` or `chainIdOut` is provided.** If `toChainId`/`chainIdOut` is omitted, `toTokenAddress` is also not required. | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` (USDC on Base) | **Example Request:** ```http theme={null} GET /strategies/aave-usdc-arbitrum/bytecode?action=withdraw&amount=10000000&wallet=0x1234...&toChainId=8453&toTokenAddress=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 42161, "chainIdOut": 8453, "id": "6a04f2f75bf7233c6220b3ca", "crossChain": { "isCrossChain": true, "chainIdIn": 42161, "chainIdOut": 8453 }, "quote": { "bridge": "CCTP", "sender": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "fromChainId": 42161, "fromTokenAddress": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "toChainId": 8453, "toTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "inputAmount": { "value": "999870", "decimals": 18, "humanized": "0.00000000000099987", "symbol": "KPK-podsUSDCy" }, "outputAmount": { "value": "999000", "decimals": 6, "humanized": "0.999", "symbol": "USDC" }, "minimumOutputAmount": { "value": "999000", "decimals": 6, "humanized": "0.999", "symbol": "USDC" }, "chainIdIn": 42161, "chainIdOut": 8453 }, "bytecode": [ { "to": "0x...", "value": "0", "data": "0x...", "chainId": "42161" } ] } ``` The top-level `id` is used for execution tracking — store it and pass it to `GET /actions/:id` to track the cross-chain withdrawal through completion (see [Checking Cross-Chain Withdrawal Status](#checking-cross-chain-withdrawal-status) below). The `quote` object describes which bridge was selected (`bridge`), the expected `outputAmount`, and the worst-case `minimumOutputAmount` (slippage floor). Display these to the user before they sign. ### Output format options By default the endpoint returns raw `bytecode`. These optional parameters change the response format and apply to either withdraw type above. | Parameter | Description | Example | | - | - | - | | output | Response format: `bytecode` (default), `userOperation`, `fireblocks`, or `instructions` (Solana) | `fireblocks` | | accountId | Fireblocks vault account ID. **Required when `output=fireblocks`** | `12` | | webhookURL | Optional per-action webhook endpoint (URI). When set, action update webhooks are delivered to this URL instead of the customer default webhook URL. | `https://example.com/webhooks/strategy-action` | **Example Request (Fireblocks output):** If you are using Fireblocks custody, set `output=fireblocks` and pass your vault `accountId`. The `wallet` parameter must be a smart wallet previously provisioned via `POST /fireblocks-smart-account` (see [Fireblocks Integration](/external-integrations/fireblocks-integration)). ```http theme={null} GET /strategies/aave-usdc-polygon/bytecode?action=withdraw&amount=10000000&wallet=0xSafeAddress&output=fireblocks&accountId=YOUR_VAULT_ACCOUNT_ID ``` **Example Response:** ```json theme={null} { "feeCharged": "0", "chainIdIn": 137, "chainIdOut": 137, "id": "65f3a1c8e9b2d4f8a7c1d2e3", "crossChain": { "isCrossChain": false, "chainIdIn": 137, "chainIdOut": 137 }, "quote": null, "transactionData": { "transactionRequest": { "operation": "CONTRACT_CALL", "assetId": "MATIC_POLYGON", "source": { "type": "VAULT_ACCOUNT", "id": "YOUR_VAULT_ACCOUNT_ID" }, "destination": { "type": "ONE_TIME_ADDRESS", "oneTimeAddress": { "address": "0xSafeAddress" } }, "amount": "0", "extraParameters": { "contractCallData": "0x6a761202..." }, "note": "withdraw aave-usdc-polygon", "failOnLowFee": true } } } ``` The `transactionData.transactionRequest` payload can be submitted directly to the Fireblocks Transactions API (`POST /v1/transactions`) — no further encoding required. The Earn Owner vault must be configured as the signer (see [Fireblocks Integration](/external-integrations/fireblocks-integration#1-fireblocks-setup)). After the withdrawal completes, transfer funds out of the smart wallet to your Omnibus wallet or any other destination. Use `GET /strategies` to list available protocols, then pass the chosen strategy ID into the protocol-specific approach above. ## Executing the Transaction Batch all `bytecode` legs on the same `chainId` into **one atomic transaction**. See [Executing Bytecode](/getting-started/executing-bytecode). ```javascript theme={null} const { data: result } = await pods.get('/strategies/aave-usdc-polygon/bytecode', { params: { action: 'withdraw', amount: '10000000', wallet: walletAddress } }) // Batch result.bytecode into ONE tx — do not send each leg separately console.log('Legs to batch atomically:', result.bytecode.length) ``` Make sure you have sufficient balance in your position before attempting to withdraw. Check your open positions first using the [check positions endpoint](/guides/yield/check-positions). ## Checking Cross-Chain Withdrawal Status Cross-chain withdrawals are asynchronous: the source-chain withdraw + bridge transaction is signed first, then the bridged funds are delivered on the destination chain. To track progress to completion, use the `id` returned in the bytecode response: ```http theme={null} GET /actions/:id ``` **Path Parameters:** | Parameter | Description | Example | | - | - | - | | id | The `id` returned by `GET /strategies/:id/bytecode` | `6a04f2f75bf7233c6220b3ca` | **Example Request:** ```http theme={null} GET /actions/6a04f2f75bf7233c6220b3ca ``` **Example Response (withdrawal in flight):** ```json theme={null} { "id": "6a04f2f75bf7233c6220b3ca", "type": "CROSSCHAIN_INVESTMENT_WITHDRAW", "status": "PENDING", "strategyId": "65a1b2c3d4e5f6a7b8c9d0e1", "strategySlug": "aave-usdc-polygon", "initialDate": "2026-05-19T14:30:15Z", "createdAt": "2026-05-19T14:30:00Z", "originAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "receiverAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "amounts": { "assetIn": { "token": { "address": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "symbol": "KPK-podsUSDCy", "decimals": 18 }, "chain": { "id": 42161, "name": "arbitrum" }, "amountRaw": "999870", "amountHumanized": "0.00000000000099987", "amountInUSD": "1.00" }, "assetOut": { "token": { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 }, "chain": { "id": 8453, "name": "base" }, "amountRaw": "999000", "amountHumanized": "0.999", "amountInUSD": "1.00" } }, "transactions": [ { "chain": "arbitrum", "txHash": "0xabc...", "blockNumber": 19234567, "timestamp": "2026-05-19T14:30:15Z", "status": "SUCCESS", "underlyingAssetFeeValue": "0" }, { "chain": "base", "txHash": null, "blockNumber": null, "timestamp": null, "status": "PENDING", "underlyingAssetFeeValue": null } ], "finishedAt": null, "underlyingAssetFeeValue": "0" } ``` **Status values:** | Status | Meaning | | - | - | | `INITIAL` | Created, awaiting on-chain execution | | `PENDING` | At least one transaction broadcast; awaiting confirmations | | `SUCCESS` | All steps completed; funds arrived on the destination chain | | `FAILED` | A step failed | | `REFUND_REQUIRED` | Funds withdrawn but the bridge step failed; refund is needed | | `REFUNDED` | Refund completed | | `EXPIRED` | Quote expired before execution | Poll this endpoint until `status` reaches a terminal value (`SUCCESS`, `FAILED`, `REFUNDED`, or `EXPIRED`). The `transactions` array lists each on-chain hash as it is confirmed. Same-chain withdrawals do not need this polling — the withdraw completes inline with the user's signed transaction. The `id` is still returned for record-keeping and can be queried the same way. ## Webhook Notifications As an alternative to polling, Pods can push every status transition to a URL you control. The payload is identical to `GET /actions/:id`, so you can drive the same UI off either source. Default webhook URL configuration is currently provisioned manually — contact the Pods team to register your default `webhookURL` (and any optional custom headers, e.g. an HMAC signature header). For a single withdrawal request, pass `webhookURL` on the bytecode endpoint to override the customer-level default. Self-service configuration via the admin panel is coming soon. **Transport:** | Field | Value | | - | - | | Method | `POST` | | URL | Your `webhookURL` | | `Content-Type` | `application/json` | | Custom headers | Any headers you configure with the team are sent on every request | **Body shape:** ```json theme={null} { "messageType": "ACTION_UPDATE", "data": { /* same object returned by GET /actions/:id */ } } ``` **Example payload (cross-chain withdrawal reaching `SUCCESS`):** ```json theme={null} { "messageType": "ACTION_UPDATE", "data": { "id": "6a04f2f75bf7233c6220b3ca", "type": "CROSSCHAIN_INVESTMENT_WITHDRAW", "status": "SUCCESS", "strategyId": "65a1b2c3d4e5f6a7b8c9d0e1", "strategySlug": "aave-usdc-polygon", "initialDate": "2026-05-19T14:30:15Z", "createdAt": "2026-05-19T14:30:00Z", "originAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "receiverAddress": "0x49A54d4d925868475Eb8d2cb2c784481ca755e8A", "amounts": { "assetIn": { "token": { "address": "0x4F38e7a76b9C32Ab68FBf500E4d52806B15AB93b", "symbol": "KPK-podsUSDCy", "decimals": 18 }, "chain": { "id": 42161, "name": "arbitrum" }, "amountRaw": "999870", "amountHumanized": "0.00000000000099987", "amountInUSD": "1.00" }, "assetOut": { "token": { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 }, "chain": { "id": 8453, "name": "base" }, "amountRaw": "999000", "amountHumanized": "0.999", "amountInUSD": "1.00" } }, "transactions": [ { "chain": "arbitrum", "txHash": "0xabc...", "blockNumber": 248123456, "timestamp": "2026-05-19T14:30:15Z", "status": "SUCCESS" }, { "chain": "base", "txHash": "0xdef...", "blockNumber": 19234999, "timestamp": "2026-05-19T14:33:02Z", "status": "SUCCESS" } ], "finishedAt": "2026-05-19T14:33:02Z" } } ``` **Delivery semantics:** * A request fires for **every** status change, so you may receive multiple deliveries per withdrawal (`INITIAL` → `PENDING` → `SUCCESS`). * Pods expects a `2xx` response. Non-`2xx` responses are recorded but not automatically retried; reconcile with `GET /actions/:id` if you need at-least-once guarantees. * Respond as quickly as possible; do the heavy work asynchronously on your side. ## Next Steps View your remaining positions Make a new deposit Check protocol information # Iframe Integration Source: https://docs.pods.finance/iframe/overview Embed Pods DeFi widgets in any stack using a hosted iframe and postMessage protocol ## Webview Base URL ``` https://iframe.pods.finance ``` All iframe integrations point to this hosted webview. You append query parameters to configure which widget to display and how to connect to the Pods API. *** ## Iframe URL Query Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `smartAccountAddress` | `0x${string}` | Yes | The user's smart account address | | `chainId` | number | Yes | Target chain ID (e.g., `137` for Polygon) | | `theme` | string | Yes | UI theme (`"dark"` or `"light"`) | | `preset` | string | Yes | Widget preset assigned to your integration | | `podsApiUrl` | string | Yes | Pods API base URL | | `podsApiKey` | string | Yes | Pods API key | | `alchemyPolicyId` | string | Yes | Alchemy gas policy ID (for paymaster) | | `alchemyRpcId` | string | Yes | Alchemy RPC/API key | | `widget` | `"earn"` \| `"swap"` | No | Which widget to display (defaults to `"earn"`) | ### URL Builder Example ```typescript theme={null} function buildIframeUrl(params: { webviewBaseUrl: string smartAccountAddress: string chainId: number theme?: string preset?: string podsApiUrl: string podsApiKey: string alchemyPolicyId: string alchemyRpcId: string widget?: 'earn' | 'swap' }): string { const url = new URL(params.webviewBaseUrl) url.searchParams.set('smartAccountAddress', params.smartAccountAddress) url.searchParams.set('chainId', String(params.chainId)) if (params.theme) url.searchParams.set('theme', params.theme) if (params.preset) url.searchParams.set('preset', params.preset) url.searchParams.set('podsApiUrl', params.podsApiUrl) url.searchParams.set('podsApiKey', params.podsApiKey) url.searchParams.set('alchemyPolicyId', params.alchemyPolicyId) url.searchParams.set('alchemyRpcId', params.alchemyRpcId) if (params.widget) url.searchParams.set('widget', params.widget) return url.toString() } ``` *** ## postMessage Protocol The iframe and wrapper communicate via `window.postMessage`. The protocol is typed in TypeScript below — copy this into your project as `src/types/postmessage-protocol.ts`. ### Shared Types ```typescript theme={null} export interface BytecodeTransaction { to: string data: string value: string chainId?: number gasLimit?: string } export type TxStatusPayload = | { type: 'HOST_ACK'; clientTxId: string } | { type: 'SIGNATURE_PROMPTED'; clientTxId: string } | { type: 'SIGNED'; clientTxId: string } | { type: 'TX_SUBMITTED'; clientTxId: string; chainId: number; txHash?: string; userOperationHash?: string } | { type: 'TX_CONFIRMED'; clientTxId: string; txHash: string; blockNumber?: number; confirmations?: number } | { type: 'TX_FINALIZED'; clientTxId: string; txHash?: string } | { type: 'SIGNATURE_DECLINED'; clientTxId: string } | { type: 'SIGNATURE_ERROR'; clientTxId: string; code: string; message: string } | { type: 'TX_REVERTED'; clientTxId: string; txHash: string; reason?: string } export type UpdateTxStatus = (event: TxStatusPayload) => void ``` ### Message Directions ```typescript theme={null} // Webview → Wrapper export type WebviewToWrapperMessage = | { kind: 'READY' } | { kind: 'PROCESS_BYTECODE_REQUEST'; clientTxId: string; bytecodes: BytecodeTransaction[]; simulateError?: boolean } // Wrapper → Webview export type WrapperToWebviewMessage = | { kind: 'TX_STATUS_UPDATE'; payload: TxStatusPayload } // Union export type PostMessage = WebviewToWrapperMessage | WrapperToWebviewMessage ``` ### Type Guards ```typescript theme={null} export function isWebviewMessage(msg: unknown): msg is WebviewToWrapperMessage { if (typeof msg !== 'object' || msg === null) return false const m = msg as Record return m.kind === 'READY' || m.kind === 'PROCESS_BYTECODE_REQUEST' } export function isWrapperMessage(msg: unknown): msg is WrapperToWebviewMessage { if (typeof msg !== 'object' || msg === null) return false const m = msg as Record return m.kind === 'TX_STATUS_UPDATE' } ``` *** ## processBytecode Flow via postMessage The iframe version of `processBytecode` works over `postMessage` instead of in-process callbacks. The transaction lifecycle statuses and error handling are the same as the [widget processBytecode](/widgets/process-bytecode) — the only difference is the transport: messages are wrapped in `{ kind: 'TX_STATUS_UPDATE', payload }` and sent via `postMessage` instead of calling `ctx.updateTxStatus()` directly. ### Happy Path ``` Webview → Wrapper: { kind: 'PROCESS_BYTECODE_REQUEST', clientTxId, bytecodes } Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'HOST_ACK', clientTxId } } Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'SIGNATURE_PROMPTED', clientTxId } } ... user signs in wallet / smart wallet signs automatically ... Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'SIGNED', clientTxId } } Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_SUBMITTED', clientTxId, chainId, txHash } } ... wait for confirmation ... Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_CONFIRMED', clientTxId, txHash, blockNumber } } Wrapper → Webview: { kind: 'TX_STATUS_UPDATE', payload: { type: 'TX_FINALIZED', clientTxId, txHash } } ``` ### Error Paths See [processBytecode — Error Handling](/widgets/process-bytecode) for the full list of error statuses (`SIGNATURE_DECLINED`, `SIGNATURE_ERROR`, `TX_REVERTED`). *** ## Listening for postMessage Events The wrapper must listen for `message` events on `window` and filter using the type guards from the protocol types: ```typescript theme={null} useEffect(() => { const handler = (event: MessageEvent) => { if (!isWebviewMessage(event.data)) return if (event.data.kind === 'READY') { console.log('Iframe is ready') } if (event.data.kind === 'PROCESS_BYTECODE_REQUEST') { const { clientTxId, bytecodes } = event.data processBytecode(clientTxId, bytecodes) } } window.addEventListener('message', handler) return () => window.removeEventListener('message', handler) }, [processBytecode]) ``` *** ## Iframe HTML Element ```html theme={null}