--- name: portals-defi-api description: Use this skill when the user asks to integrate the Portals API for DeFi token data, multichain balances, swaps, zaps, cross-chain operations, vault entry, historical token data, or anything involving the api.portals.fi REST endpoints. Apply it whenever the user mentions Portals, build.portals.fi, /v2/portal, /v2/tokens, /v2/account, zaps, or DEX-aggregator routing across EVM chains. --- # Portals DeFi API A unified REST API for token data and DeFi execution across EVM chains. Two surfaces: - **Data API** — token prices, metadata, balances, history, transactions, holders, volume metrics across 400+ DeFi protocols and 10+ EVM networks. - **Execution API** — DEX-aggregator quotes ("swaps") and intent-based zaps that bundle bridges + swaps + protocol entries (Yearn, Curve, Morpho, Aave, Balancer, etc.) into a single transaction. Base URL: `https://api.portals.fi`. Auth: `Authorization: Bearer ` from [build.portals.fi](https://build.portals.fi). Execution endpoints (`/v2/portal`, `/v2/portal/estimate`, `/v2/approval`, `/v2/approval/cross-chain`) accept the header but do not require it. ## Language scope The data endpoints (`/v2/tokens`, `/v2/account`, `/v2/tokens/history`, `/v2/tokens/holders`, `/v3/tokens/transactions`, `/v3/tokens/metrics`, `/v2/platforms`, `/v1/networks`) are pure HTTP — every example in `examples/` shows `curl` first, so they're directly reproducible from Python (`requests`), Go (`net/http`), shell, or any HTTP client. The TypeScript snippets that follow assume the `axios` instance from `templates/auth-helper.ts`; Python translation is one line: `requests.get(url, headers={"Authorization": f"Bearer {key}"}, params={...}).json()`. The execution endpoints return the same JSON regardless of caller, but signing + broadcasting the returned `tx` payload requires a wallet library. `templates/viem-submit.ts` is the canonical Node/viem helper. For Python, hand `tx.data` / `int(tx.value)` / `int(tx.gasLimit)` to `web3.py`'s `Account.sign_transaction` + `eth.send_raw_transaction` — same payload, different signing library. See `examples/04-simulate-and-execute.md` for both side-by-side. Pure curl can read every endpoint and even fetch the executable `tx`, but cannot sign it. ## When to use this skill Use it when the user wants to: - Fetch token prices, metadata, APY, liquidity, or platform-tagged tokens - Read an address's balances across multiple EVM chains - Generate a swap or zap transaction (single-chain or cross-chain) - Simulate an output amount before executing - Check or build an ERC-20 approval (including EIP-2612 permit) - Pull historical price/liquidity series, transaction feeds, holder lists, or volume metrics - Replace per-protocol SDKs with one unified router ## When NOT to use this skill - The user wants on-chain reads not exposed by the API (raw `eth_call`, mempool watching, event log subscriptions) — use viem/ethers directly. - The user is on a non-EVM chain (Solana, Cosmos, Bitcoin) — Portals is EVM-only as of writing. - The user wants to *execute* the on-chain transaction itself — Portals returns calldata; the user signs and broadcasts with their own wallet client. This skill points to `templates/viem-submit.ts` for that. ## Token identifier format (read this first) Every token is identified by `[network]:[address]` — both lowercased. This format is universal across all endpoints. ``` ethereum:0x0000000000000000000000000000000000000000 ← native ETH ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 ← USDC arbitrum:0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8 ← Aave WETH base:0x04c0599ae5a44757c0af6f9ec3b93da8976c150a ← Morpho Re7 WETH ``` Native tokens use the zero address on every chain. Live networks (verified): `ethereum`, `arbitrum`, `optimism`, `base`, `polygon`, `bsc`, `avalanche`, `sonic`, `hyperevm`. Hit `GET /v1/networks` on startup for the canonical list rather than hardcoding. ## Endpoint decision tree ``` What does the user want? ├─ Token info (price, APY, liquidity, search) ............. GET /v2/tokens ├─ A specific token by ID ................................. GET /v2/tokens?ids=… ├─ Address balances across chains ......................... GET /v2/account?owner=… ├─ Historical price/liquidity series ...................... GET /v2/tokens/history ├─ Recent transactions for a token ........................ GET /v3/tokens/transactions ├─ Rolling-window volume metrics .......................... GET /v3/tokens/metrics ├─ Token holders list ..................................... GET /v2/tokens/holders ├─ Supported protocols list ............................... GET /v2/platforms ├─ Supported chains list .................................. GET /v1/networks │ ├─ Swap / zap quote (same chain or cross-chain) ........... GET /v2/portal ├─ Simulate output amount, no calldata .................... GET /v2/portal/estimate ├─ Check ERC-20 approval (single chain) ................... GET /v2/approval └─ Check ERC-20 approval (cross-chain) .................... GET /v2/approval/cross-chain ``` Same-chain and cross-chain quotes share the same execute endpoint (`/v2/portal`) — the API auto-detects by inspecting whether `inputToken` and `outputToken` networks match. The only branch is approval: cross-chain swaps go through Squid router and need a different approval target. ## The canonical execution flow Most integrations follow this exact sequence. Follow it; don't invent variations. ``` 1. Get a quote GET /v2/portal/estimate?inputToken=…&outputToken=…&inputAmount=… → expectedOutputAmount, gasEstimate (no calldata, fast) 2. Check approval GET /v2/approval?sender=…&inputToken=…&inputAmount=… (skip for native tokens) → if context.shouldApprove === true, sign & submit response.approve (for the tx) or response.permit (for an EIP-2612 typed-data signature) 3. Get executable order GET /v2/portal?inputToken=…&outputToken=…&inputAmount=…&sender=… → tx { to, data, value, gasLimit } + context 4. Submit on-chain walletClient.sendTransaction({ to, data, value, gas }) → on-chain hash. Re-fetch if the quote is older than ~30s (no public expiry field — quotes drift with market state). ``` Cross-chain replaces step 2 with `/v2/approval/cross-chain`; everything else is identical. ## Quick reference (request shapes) ```typescript // Quote / execute (single or cross-chain) GET /v2/portal { inputToken: string // "[network]:[address]" inputAmount: string // base units, e.g. "1000000000000000000" for 1 ETH outputToken: string // "[network]:[address]" sender: string // EOA holding the input slippageTolerancePercentage?: number // 0.1–10. Omit for auto-slippage (requires validate=true) partner?: string // your fee-share address feePercentage?: number // 0–1.00. Default 0.3 permitSignature?: string // 132-char EIP-2612 signature permitDeadline?: string // uint256 timestamp validate?: boolean // simulate before responding. Default true } // Simulate-only (faster, no calldata) GET /v2/portal/estimate { inputToken: string inputAmount: string outputToken: string } // Approval check (single chain) GET /v2/approval { sender: string inputToken: string inputAmount: string permitDeadline?: string } // Approval check (cross-chain — different target router) GET /v2/approval/cross-chain { same shape as /v2/approval } // Token data (search & filter) GET /v2/tokens { ids?: string[] // comma-separated "[network]:[address]" search?: string // free text networks?: string // repeat: networks=ethereum&networks=base platforms?: string // repeat: platforms=yearn-v3&platforms=morpho minApy?: number minLiquidity?: number sortBy?: "price" | "liquidity" | "apy" | "volumeUsd1d" sortDirection?: "asc" | "desc" page?: number limit?: number // default 25, max 250 } // Address balances (multichain) GET /v2/account { owner: string // single address networks?: string // repeat: networks=ethereum&networks=optimism // limit/page are ignored — endpoint always returns the full set } // Historical (1-year cap, paginated for >1000 items) GET /v2/tokens/history { id: string // single token from?: number // unix seconds; default 1d ago; max 1 year ago resolution?: "15m" | "30m" | "1h" | "4h" | "1d" page?: number } // Cursor-paginated transaction feed GET /v3/tokens/transactions { id: string limit?: number // default 25, max 50 cursor?: string // opaque, from previous response.pageInfo.endCursor } // Rolling volume metrics across 8 windows GET /v3/tokens/metrics { id: string } // Holder distribution (BETA) GET /v2/tokens/holders { id: string limit?: number // default 100, max 1000 page?: number // 0-indexed } ``` ## Critical gotchas (read every time) These are the failure modes that bite first-time integrators. Each has burned someone in production. 1. **Always simulate before executing.** Hit `/v2/portal/estimate` (or call `/v2/portal` with `validate=true`) to confirm the route exists and the output is in range. Routes can disappear (low liquidity, paused vaults) and quotes can drift between fetch and submit. Fail loudly if `outputAmountUsd / inputAmountUsd < 0.95` unless slippage explains the gap. 2. **Quote drift is real.** Quotes don't carry a public expiry field, but they age fast. Re-fetch `/v2/portal` right before submission if more than ~30 seconds have passed — the on-chain call may revert on `minOutputAmount` if the route shifted. 3. **`tx.value` must be cast to BigInt before sending.** The API returns `value` as a string. Wallet clients expect bigint. `viem`'s `sendTransaction` will throw `TypeError: Cannot mix BigInt and other types` if you pass the raw string. 4. **Cross-chain uses a different approval endpoint.** `/v2/approval/cross-chain`, not `/v2/approval`. The Squid router has a different spender than the same-chain Portals router. Using the wrong one will return "approval set" but the cross-chain swap will revert with `TRANSFER_FROM_FAILED`. 5. **Token ID format**: `network:address`, both segments. The API normalizes case server-side (`Ethereum:0xA0B…` works), but responses always come back lowercased. Compare with `.toLowerCase()` on both sides if matching keys round-trip — naive equality on user input may break. 6. **`/v2/account` ignores `limit` and `page`.** The endpoint returns all balances in one response. Don't paginate; don't pass `limit`. Pass `networks` to scope. 7. **Tier-restricted endpoints.** `/v2/tokens/history`, `/v3/tokens/transactions`, `/v3/tokens/metrics`, and `/v2/tokens/holders` require **Pioneer** tier or higher. Hitting them on a Free tier returns 403 with `{ "message": "Plan does not allow access" }`. Catch this; degrade gracefully (cache, hide UI) rather than throwing. 8. **`/v3/tokens/transactions` uses cursor pagination, not page numbers.** The response carries `pageInfo: { hasNextPage, endCursor }`. Pass `cursor=` on subsequent requests. Don't try to compute offsets from the cursor — it's opaque. 9. **History has a 1-year hard cap.** `from` cannot be older than 1 year ago. Earlier values return 400 with `"Timestamp is not valid (newer or equal than 1 year ago)"`. History numeric fields are string-encoded, including the live `totalItems` value. For longer series, store your own snapshots. 10. **Never put the API key client-side.** Browser-exposed Bearer keys can be exfiltrated and rate-limited maliciously. Proxy through your own backend that injects the header. The execution endpoints (`/v2/portal`, `/v2/portal/estimate`, `/v2/approval`) are key-optional precisely so you can call them from the browser without leaking anything; the *data* endpoints should always be proxied. 11. **Approval response uses `approve`, not `tx`.** The `/v2/approval` and `/v2/approval/cross-chain` responses are shaped `{ context, approve, permit }`. The transaction object is at `response.approve` (`{ to, data, from, gasLimit }`). Reaching for `response.tx` returns undefined and crashes the broadcast. The `permit` block carries the EIP-2612 typed-data when `context.canPermit === true`. 12. **`/v3/tokens/metrics` and `/v3/tokens/transactions` only support platform tokens.** Calling them with native (`0x000…`) or basic ERC-20 tokens (USDC, WETH, etc.) returns 400 with `"Transactions are not supported for basic and native tokens"`. Filter by `platform` first if you're uncertain. 13. **Activity and history timestamps are ISO strings, not unix seconds.** `/v3/tokens/transactions` and `/v2/tokens/history` return `time: "2026-05-01T20:24:23.000Z"` — pass to `new Date(time)` directly. Don't multiply by 1000. ## Where to look for more - **Endpoint reference**: see `reference/endpoints.md` for the full per-endpoint table (auth, tier, query params, response shape pointer). - **Response shapes**: see `reference/token-shapes.md` for TypeScript types covering Token, Balance, PortalOrder, Approval, History, Transaction, Metrics, Holder. - **Errors**: see `reference/errors.md` for the catalog of error codes, root causes, and fixes. - **Worked examples**: `examples/01-fetch-token-data.md` through `examples/06-historical-and-metrics.md` cover the most common flows end-to-end. - **Submit helper**: `templates/viem-submit.ts` is a paste-ready function for broadcasting a Portal order via viem. `templates/auth-helper.ts` is a tiny axios wrapper that injects the Bearer header and handles 429s. - **Live console**: [api.portals.fi/docs](https://api.portals.fi/docs) is the Swagger UI — the source of truth for query params and responses. - **Public docs**: [build.portals.fi/docs](https://build.portals.fi/docs) — the human-readable equivalent of this skill. ## Worked-example index (pick the closest match) ``` "I want a price" → examples/01-fetch-token-data.md "Find tokens by APY/TVL" → examples/01-fetch-token-data.md (filter section) "Portfolio across chains" → examples/02-multichain-balances.md "Just a swap quote" → examples/03-quote-swap.md "Full swap with broadcast" → examples/04-simulate-and-execute.md "Bridge + swap + zap" → examples/05-cross-chain-zap.md "Charts and volume" → examples/06-historical-and-metrics.md ``` Read the matching example, then come back here for gotchas before submitting.