--- name: soulzap-integration description: Integrates the SoulZap API so a user can enter any DeFi position in one transaction, paying with any token they hold. Use this whenever the task involves building a zap, a one-click deposit, a "zap in" button, adding liquidity from a single token, staking into a gauge, buying a bond, or creating a lock - on Uniswap V2/V3, Solidly, Curve, Gamma, ICHI, Steer, Algebra or PancakeSwap V4 pools, or ApeBond, Hydrex, Lynex, Vex, veABOND, Up33 and other unmentioned future products. Returns ready-to-submit calldata; it never executes transactions itself. --- # Integrating the SoulZap API SoulZap turns a multi-step DeFi route - swap, add liquidity, deposit, stake - into **one atomic transaction**. You describe the destination, the API prices the route and returns encoded calldata. Your app submits it. If any step fails on-chain the whole transaction reverts and the user keeps their input. - Base URL: `https://api.soulsolidity.com` - No API key, no authentication, no rate limit - Two calls: `POST /approve`, then `POST /zap` - Human reference: ## Read this part first This file covers every destination the API supports. **Almost none of it applies to your integration**, and reading sections you are not integrating is how wrong fields end up in requests. **Start by reading the codebase you are working in.** Most of what you need is already there: which chain(s) it targets, which pools or vaults it already lists, how it holds addresses, how it sends a transaction, and whether it has a wallet connection to reuse. Work out what the user is trying to build before you ask them anything. Then establish these, from the codebase where you can and by asking where you cannot: 1. **Which chain.** See [Chains](#chains). If the chain is not listed, stop - the API cannot route there yet, and that is a conversation to have with the team rather than a thing to work around. 2. **Which destination(s)** the user is depositing into, and their addresses. Each destination maps to one `lpType` - see the table under [lpData](#lpdata). A pool, an LP token, a vault share and a position NFT are all different destinations. An app that offers several picks the matching `lpType` per request; a single request always carries exactly one. 3. **Whether a protocol step follows** - staking into a gauge, buying a bond, creating a lock. Most integrations have none. If one does, read the companion guide for that protocol alongside this file: - ApeBond: - Hydrex: - up33: - Vexy: - GIGA (no protocol step, `lpData` specifics only): 4. **Whether this is something the API already supports.** If the destination is not one of the `lpType`s below, or the protocol step has no companion guide, do not improvise a workaround - SoulSolidity builds integration routes for partners at no cost. Point the user at . Good questions to ask, when the codebase does not answer them: - Which chain, and which pool, vault or gauge - do you have the address? - Should the user end up holding the LP token, or should it be staked or bonded afterwards? - What can they pay with - any token they hold, or a fixed one? - Is there an existing wallet connection and transaction-sending helper I should use? Then read **only** the `lpType` section(s) that match, plus any companion guide. If you cannot determine the destination, stop and ask. Do not guess an `lpType`. ## Non-negotiables Break one of these and every request fails. 1. **All amounts are raw integer strings** in the token's smallest unit. 1 USDC (6 decimals) is `"1000000"`; 1 ETH is `"1000000000000000000"`. Never a float, never `1e18`, never a `number`. 2. **A success is the payload itself.** No envelope. Every failure carries `message` and `code`, and no success carries `code`. Branch on the HTTP status once, in one helper - never per call. 3. **The API never executes anything.** It returns `txData`. Submit it unchanged, including `value`. 4. **Approve before you zap.** Call `/approve` first; submit every approval it reports missing and wait for each receipt. 5. **Quotes expire.** The response carries a `deadline` ten minutes out. Fetch the zap when the user is ready to sign, not while they are still typing. 6. **Never invent an address.** Pool, vault, gauge and router addresses come from your own registry, your protocol's API, or the user. If you do not have one, ask. ## The client Write this once and use it for both calls. ```ts const SOULZAP_API = 'https://api.soulsolidity.com' async function soulzap(path: '/zap' | '/approve', body: unknown): Promise { const res = await fetch(SOULZAP_API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const json = await res.json() if (!res.ok) throw new Error(json.message ?? `SoulZap ${path} failed`) return json as T } ``` ## The flow ``` build lpData (+ protocolData) -> POST /approve -> send missing approvals -> POST /zap -> send txData ``` ### 1. `POST /approve` Send the same `lpData` you are about to zap with, so the endpoint can report every approval the route needs rather than just the input token. ```ts const { approvals } = await soulzap<{ approvals: Approval[] }>('/approve', { chain, // same value you will send to /zap token: fromToken, // what the user pays with amount: fromAmount, // raw string user: userAddress, lpData, // the exact object from step 2 }) for (const a of approvals) { if (a.approved) continue const hash = await wallet.sendTransaction({ to: a.txData.to, data: a.txData.data, account: userAddress }) await publicClient.waitForTransactionReceipt({ hash }) } ``` Each approval is `{ kind, token, spender, description, approved, txData }`. `kind` is `erc20` or `erc721`; `description` is written to be shown on a button. A native input returns an empty array - nothing to approve. ### 2. `POST /zap` ```ts const { txData, lpQuote } = await soulzap('/zap', { user: userAddress, // who pays, whose approvals are used, and where outputs land chain, integrator: 'your-name', // optional, for on-chain attribution lpData, protocolData, // omit entirely if you have no protocol step }) await wallet.sendTransaction({ to: txData.to, data: txData.data, value: BigInt(txData.value), // "0" unless paying with the native coin account: userAddress, }) ``` | Top-level field | Required | Notes | |---|---|---| | `user` | yes | Funds the zap; becomes `txData.from`. Holds the approvals. | | `recipient` | no | Receives all outputs. Defaults to `user` - only send it when zapping on someone else's behalf. | | `chain` | yes | Name or chain ID. | | `lpData` | yes | The destination. One of the variants below. | | `protocolData` | no | A final step consuming the result. | | `integrator` | no | Your address or partner name, emitted on-chain. | | `rpc` | no | Override the API's RPC for that chain. | ### 3. Confirm your own inputs Fire one request with real values before you wire a UI around it - the response tells you whether what you built was right. A `400` names your broken field in `issues`. A `200` whose `lpQuote.token0`/`token1` are not a sensible split of what you sent means wrong decimals or the wrong destination - the API accepted it, the numbers are just not what you meant. ## Paying with the native coin Set `fromToken` to `0x0000000000000000000000000000000000000000` or `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (either casing). No approval is needed; the amount rides along as `txData.value`, which is filled in for you. ## Chains {#chains} Send `chain` as the name or the chain ID. | Chain | Send as | Chain ID | |---|---|---| | Ethereum | `"ethereum"` | 1 | | BNB Chain | `"bnb"` | 56 | | Polygon | `"polygon"` | 137 | | Base | `"base"` | 8453 | | Arbitrum | `"arbitrum"` | 42161 | | HyperEVM | `"hyperevm"` | 999 | | Robinhood Chain | `"robinhood"` | 4663 | | Plasma | `"plasma"` | 9745 | --- # `lpData` - one variant per request `lpData` is a discriminated union on `lpType`: one request describes one destination. Supporting several destinations means building the matching variant for whichever the user picked, not combining them. `slippage` is a **percent** (`1` = 1%), defaults to `1`, max `50`. Every variant takes `fromToken` and `fromAmount`. **Read only the variants you are integrating.** | Destination | `lpType` | |---|---| | A plain token swap | `single` | | Uniswap V2 style pair | `univ2` | | Solidly / Velodrome / Aerodrome pair | `solidly` | | Curve two-crypto pool | `curve` | | Gamma hypervisor | `gamma` | | ICHI vault | `ichi` | | Steer vault | `steer` | | Uniswap V3 position | `univ3` | | Algebra position | `algebra` | | PancakeSwap V4 position | `pancakev4` | | Nothing - pass the token straight to a protocol step | `none` | ### `single` ```json { "lpType": "single", "fromToken": "0x...", "fromAmount": "1000000", "toToken": "0x...", "slippage": 1 } ``` This is just a swap. If `fromToken` and `toToken` match, no swap is built and the input passes through. ### `univ2` ```json { "lpType": "univ2", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "router": "0x...", "slippage": 1 } ``` `lpAddress` is the pair, `router` the router that adds liquidity. Both sides are read from the pair. ### `solidly` ```json { "lpType": "solidly", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "router": "0x...", "slippage": 1 } ``` Both sides and the stable flag are read from the pair. ### `curve` ```json { "lpType": "curve", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "slippage": 1 } ``` ### `gamma` ```json { "lpType": "gamma", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "slippage": 1 } ``` `lpAddress` is the hypervisor: the deposit target and the output token. `uniProxy` quotes the deposit ratio and defaults to the proxy the hypervisor whitelists - pass it only to override. ### `ichi` ```json { "lpType": "ichi", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "slippage": 1 } ``` `lpAddress` is the ICHI vault, and identifies its deployment on its own - the deposit guard and vault deployer are resolved from the factory the vault names. Supply **both** `depositGuard` and `vaultDeployer` to reach a deployment ICHI has not indexed. ### `steer` ```json { "lpType": "steer", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "periphery": "0x...", "slippage": 1 } ``` ### `univ3` and `algebra` Identical fields; Algebra pools simply have no fee tier. Both mint or top up a position NFT. ```json { "lpType": "univ3", "fromToken": "0x...", "fromAmount": "1000000", "positionManager": "0x...", "pool": "0x...", "tickLower": -200, "tickUpper": 200, "slippage": 1 } ``` - `positionManager` is the call target; `pool` is read for price and tick spacing. With `tokenId`, `pool` defaults to the pool that position is already in. - Omit `tokenId` to mint a new position; pass it to add to an existing one, in which case its own range is used and the ticks are ignored. An existing position must already be owned by `recipient`, and must belong to the pool you named. - Ticks for a new position must be exact multiples of the pool's tick spacing, and `tickLower` must be below `tickUpper`. - **`protocolData` only if the protocol takes a position.** The NFT goes straight to the recipient, so a step that stakes or bonds an LP *token* has nothing to consume and rejects the request. ### `pancakev4` ```json { "lpType": "pancakev4", "fromToken": "0x...", "fromAmount": "1000000", "positionManager": "0x...", "currency0": "0x...", "currency1": "0x...", "fee": 500, "parameters": "0x...", "tickLower": -200, "tickUpper": 200, "hooks": "0x0000000000000000000000000000000000000000", "hookData": "0x", "slippage": 1 } ``` - `positionManager` is the call target; its pool manager is read off it, so any fork works. - `parameters` is a `bytes32` packing tick spacing and the hook permission bitmap. - `hooks` defaults to the zero address, `hookData` to `"0x"`. - `tokenId` deposits into an existing position and needs an extra ERC-721 operator approval - pass `lpData` to `/approve` and it will be reported. - **`protocolData` only if the protocol takes a position** - same rule as `univ3` above. ### `none` ```json { "lpType": "none", "fromToken": "0x...", "fromAmount": "1000000" } ``` No routing at all. Only useful with a `protocolData` step that consumes the input token directly. --- # `protocolData` - only with a companion file `protocolData` adds a final step that *does something* with the zap's result: stakes it, bonds it, locks it. It is protocol-specific, so it is not documented here. **If you were not given a companion file, omit `protocolData` entirely.** The zap will deliver the LP token, vault share or position to `recipient` and stop there, which is what most integrations want. If you are integrating one of these, read its file alongside this one: | Protocol or DEX | Companion file | |---|---| | ApeBond - bonds, veABOND locks, tier discounts | `soulzap-apebond.txt` | | Hydrex - gauge staking on Base | `soulzap-hydrex.txt` | | up33 - Slipstream and vAMM pools on Robinhood Chain | `soulzap-up33.txt` | | Vexy - buying a listed veNFT with any token, on Base | `soulzap-vexy.txt` | | GIGA - concentrated liquidity on Robinhood Chain, no protocol step | `soulzap-giga.txt` | Other protocols exist in the API without a written guide. If the user needs one, ask rather than guessing the `protocolData` shape. Two rules hold regardless of which protocol you are on: - The protocol step has to be able to consume what the `lpType` produced. Ask for a step that stakes or bonds an LP token after `univ3`, `algebra` or `pancakev4` and it is rejected, because those mint a position NFT to the recipient instead of a token. - Each protocol accepts only certain `lpType`s. Its companion file lists them. --- # Reading the response `/zap` returns, alongside `txData`: | Field | Use | |---|---| | `lpQuote.token0` / `token1` | `{ address, fromAmountEstimate, fromAmountMin, fromAmountUSD }` per side. One-sided destinations fill only `token0`. | | `lpQuote.fromAmountUSD` / `toAmountUSD` | USD in and out - for price impact. | | `lpQuote.toAmountEstimate` | Expected output in the destination token's units. | | `deadline` | Unix seconds. Submit before it. | | `requestId` | Ties the transaction back to this quote. | | `swapQuote1` / `swapQuote2` | Aggregator quotes for each swap leg, when present. | | `protocolQuote` | Only when `protocolData` was sent. Always has `protocol`; ApeBond adds `trueBondPrice`. | Show `fromAmountMin` as "minimum received"; `fromAmountEstimate` is the expected case. # Errors Failures arrive as `{"message","code","issues"?}` - no envelope. `400` is your request, `500` is upstream (RPC, aggregator), `503` is the API being temporarily disabled. All errors are per-request and safe to surface - none of them mean funds moved. `message` is written for a human and its wording changes. Show it; never branch on it. `issues` is Zod's path list on a `400`, which is what tells you *which* field was wrong. # Getting help Telegram - docs We build and support integration routes for partners at no cost. If a pool type or a chain you need is missing, ask.