SoulZap API
A zap collapses a multi-step DeFi route into a single atomic transaction. You name the destination; SoulZap finds the route, prices it and returns the calldata for your app to sign.
https://api.soulsolidity.com
POST /approve
Returns every approval the zap needs, each flagged. Submit the missing ones.
POST /zap
Returns txData. Send it unchanged from your own wallet or signer.
That is the whole integration. The API never holds funds, never signs and never broadcasts. If any step reverts, the whole transaction reverts and the user keeps their input.
Getting started
Point a coding agent at the integration guide and it has everything it needs.
Integrate the SoulZap API so a user can enter a position in one transaction.
Read https://docs.soulsolidity.com/agents/soulzap-integration.txt and follow it.
Doing it by hand
Two calls. Ask what needs approving, then get the transaction — full field reference under
POST /approve and
POST /zap.
curl -X POST https://api.soulsolidity.com/approve \
-H "Content-Type: application/json" \
-d '{ "chain": "base", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "1000000", "user": "0xYourWallet" }'
curl -X POST https://api.soulsolidity.com/zap \
-H "Content-Type: application/json" \
-d '{ "user": "0xYourWallet", "chain": "base",
"lpData": { "lpType": "single", "toToken": "0x4200000000000000000000000000000000000006",
"fromToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"fromAmount": "1000000" } }'
Submit each unapproved entry from the first call, then send txData from the
second unchanged — to, data and value as returned.
Or run the whole thing in the browser first.
Core concepts
Response shape
A success is the payload itself — no envelope to unwrap. A failure carries a
message and an error code, plus issues when
the request failed schema validation. The HTTP status tells you which you got.
{ "requestId": "0x…", "deadline": 1754500000, "txData": { } }
{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"issues": []
}
Every failure looks like that, including the ones raised before routing — so
code is present on every error and on no success. Branch on the status once
and you are done:
const res = await fetch(url, { method: 'POST', headers, body })
const json = await res.json()
if (!res.ok) throw new Error(json.message)
return json
Amounts
Every amount is a raw integer string in the token's smallest unit — never a decimal,
never a JavaScript number. 1 USDC (6 decimals) is "1000000"; 1 ETH is
"1000000000000000000". Sending 1.5 or 1e18 is a
validation error.
Native input
To zap from the chain's native coin, set fromToken to either
0x0000000000000000000000000000000000000000 or
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. Both are recognised, in any
casing.
Native input needs no approval — /approve returns an empty
approvals array for it. The amount arrives as msg.value
instead, which is why txData.value is populated for native zaps and
"0" otherwise. Sending txData verbatim handles this for you.
recipient vs user
user funds the zap: input tokens are pulled from it, it must hold the
approvals, and it becomes txData.from. recipient receives the
outputs — LP tokens, position NFTs, bond receipts, leftovers — and defaults to
user, so you only send it when zapping on someone else's behalf.
They are usually identical. Two places where the distinction is load-bearing:
-
Depositing into an existing
univ3,algebraorpancakev4tokenId: the position must already be owned byrecipient, otherwise the request is rejected withPosition … is owned by …. -
ApeBond tiers are read against
recipient, notuser, so the discount and any tier purchase belong to the recipient.
integrator is optional and separate: an address, or a partner name known to
the API, emitted on-chain for attribution. An unrecognised value falls back to the zero
address instead of failing the zap.
Slippage
slippage is a percentage, not basis points: 1 means 1%. It
defaults to 1 everywhere it appears, and is capped at 50 in
lpData, 20 in ApeBond's protocolData.
For the concentrated liquidity types (univ3, algebra,
pancakev4) the number bounds the liquidity the position ends up
holding rather than the token amounts, and it is scaled by how narrow the range is: on a
tight range a small price move costs several percent of liquidity, so the tolerance is
widened to keep the number meaning what you set it to. The bound is enforced on-chain and
reverts the whole route if it is missed.
POST /approve
Returns every approval a zap needs, each with its own transaction data and a flag for whether it is already in place. Submit the ones that are missing.
Send the exact lpData object you are about to send to
/zap. Leave it out and only the input-token allowance is checked, which is
complete for every LP type except a pancakev4 deposit into an existing
tokenId — that one also needs an ERC-721 operator approval. Passing it
always is the simplest rule.
Request
| Field | Type | Description | |
|---|---|---|---|
chain | string | number | required | Name, short code or chain ID — see Supported chains. |
token | string | required | The zap's input token. A native sentinel yields an empty approvals array. |
amount | string | required | Raw amount to be spent. The allowance is compared against this. |
user | string | required | Wallet whose allowances are read. |
lpData | object | optional | The same lpData the zap will use. Required to discover non-ERC-20 approvals. |
There is no rpc field here — /approve always reads through the
API's own RPC for that chain.
Response
{
"chain": "bnb",
"approvals": [
{
"kind": "erc20",
"token": "0x55d398326f99059fF775485246999027B3197955",
"spender": "0x372F055470ef14EB79AfA1e1Ae301629FE1E050F",
"description": "Allow the zap to spend this token",
"approved": false,
"txData": {
"to": "0x55d398326f99059fF775485246999027B3197955",
"data": "0x095ea7b3…",
"from": "0xYourWalletAddress"
}
}
]
}
| Field | Type | Description |
|---|---|---|
kind | "erc20" | "erc721" | An allowance to the token manager, or an operator approval on a position manager. |
token | string | Contract the approval is granted on. |
spender | string | Address being approved. |
description | string | Plain-language purpose, safe to put on a button. |
approved | boolean | true if already satisfied — skip it. |
txData | object | { to, data, from? }. Submit as-is. |
For an ERC-721 approval, txData.from is the position's current owner,
which is not necessarily user. Only that address can grant it.
POST /zap
Prices the route and returns the transaction that executes it. A request is a destination
(lpData) plus, optionally, something to do with the result
(protocolData).
Request
| Field | Type | Description | |
|---|---|---|---|
user | string | required | Address the input is pulled from. Becomes txData.from. |
recipient | string | optional | Address the outputs are sent to. Defaults to user. |
chain | string | number | required | Name, short code or chain ID — see Supported chains. |
lpData | object | required | The destination. Discriminated by lpType. |
protocolData | object | optional | What to do with the result. Discriminated by protocol. |
integrator | string | optional | Partner address or known partner name, emitted on-chain for attribution. |
rpc | string | optional | RPC URL to use instead of the API's default for that chain. |
lpData — where the zap ends
Fields every variant takes
| Field | Type | Description | |
|---|---|---|---|
lpType | string | required | Selects the variant. |
fromToken | string | required | Input token, or a native sentinel. |
fromAmount | string | required | Raw input amount. |
slippage | number | optional | Percent, 0–50. Default 1. Not accepted by none. |
none
No routing at all — the input token is handed straight to the protocol step. Takes no
fields beyond fromToken and fromAmount, and is only useful
together with protocolData.
single
Swaps into one token. If fromToken and toToken match (casing
ignored) no swap is built and the input passes through untouched.
| Field | Type | Description | |
|---|---|---|---|
toToken | string | required | Token to swap into. |
{
"lpType": "single",
"fromToken": "0x55d398326f99059fF775485246999027B3197955",
"toToken": "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
"fromAmount": "1000000000000000000",
"slippage": 1
}
univ2
| Field | Type | Description | |
|---|---|---|---|
lpAddress | string | required | The pair contract. |
router | string | required | Router used to add liquidity. |
{
"lpType": "univ2",
"fromToken": "0x55d398326f99059fF775485246999027B3197955",
"fromAmount": "1000000000000000000",
"lpAddress": "0xYourPairAddress",
"router": "0x10ED43C718714eb63d5aA57B78B54704E256024E",
"slippage": 1
}
solidly
| Field | Type | Description | |
|---|---|---|---|
lpAddress | string | required | The pair contract. Stable/volatile is read from it. |
router | string | required | Router used to add liquidity. |
curve
Two-crypto pools. The pool contract is also the LP token.
| Field | Type | Description | |
|---|---|---|---|
lpAddress | string | required | The pool contract. |
gamma
| Field | Type | Description | |
|---|---|---|---|
uniProxy | string | optional | UniProxy contract that quotes the deposit ratio. Defaults to the proxy the hypervisor whitelists. |
lpAddress | string | required | Hypervisor to deposit into. Also the output token. |
ichi
Single-sided deposit. The vault identifies its own deployment, so the two addresses below are only needed to reach one ICHI has not indexed.
| Field | Type | Description | |
|---|---|---|---|
lpAddress | string | required | ICHI vault to deposit into. Also the output token. |
depositGuard | string | optional | Override. Required together with vaultDeployer. |
vaultDeployer | string | optional | Override. Required together with depositGuard. |
{
"lpType": "ichi",
"fromToken": "0x55d398326f99059fF775485246999027B3197955",
"fromAmount": "1000000000000000000",
"lpAddress": "0xYourIchiVault",
"slippage": 1
}
steer
| Field | Type | Description | |
|---|---|---|---|
lpAddress | string | required | Steer vault. |
periphery | string | required | Periphery contract used for the deposit. |
univ3 and algebra
Identical fields. Algebra pools have no fee tier; both read token0,
token1 and tick spacing off the pool.
Mints a new position NFT, or tops up an existing one. Four rules apply:
-
protocolDatais rejected with a400— the NFT is minted straight torecipient, so no protocol step can consume it. -
Omit
tokenIdto mint a new position. Supply it to deposit into an existing one, in which case that position's own range is used andtickLower/tickUpperare ignored. -
An existing
tokenIdmust already be owned byrecipient, and must belong to the pool you named. -
For a new position, both ticks must be exact multiples of the pool's tick spacing,
and
tickLowermust be belowtickUpper.
| Field | Type | Description | |
|---|---|---|---|
positionManager | string | required | NonfungiblePositionManager — the call target. |
pool | string | optional | Pool contract, read for price and tick spacing. Required to mint; with tokenId it defaults to the pool that position is in. |
tickLower | number | required | Lower tick. Ignored when tokenId is set. |
tickUpper | number | required | Upper tick. Ignored when tokenId is set. |
tokenId | string | optional | Existing position to deposit into. |
{
"lpType": "univ3",
"fromToken": "0x55d398326f99059fF775485246999027B3197955",
"fromAmount": "1000000000000000000",
"positionManager": "0xYourPositionManager",
"pool": "0xYourPool",
"tickLower": -60,
"tickUpper": 60,
"slippage": 1
}
pancakev4
Mints a new position NFT, or tops up an existing one. Four rules apply:
-
protocolDatais rejected with a400— the NFT is minted straight torecipient, so no protocol step can consume it. -
Omit
tokenIdto mint a new position. Supply it to deposit into an existing one, in which case that position's own range is used andtickLower/tickUpperare ignored. -
An existing
tokenIdmust already be owned byrecipient, and must belong to the pool you named. -
For a new position, both ticks must be exact multiples of the pool's tick spacing,
and
tickLowermust be belowtickUpper.
| Field | Type | Description | |
|---|---|---|---|
positionManager | string | required | CLPositionManager — the call target. Its pool manager is read off it. |
currency0 | string | required | First currency of the pool key. Must sort strictly below currency1. |
currency1 | string | required | Second currency of the pool key. |
fee | number | required | Pool fee. |
parameters | string | required | bytes32 packing tick spacing and the hook permission bitmap. |
tickLower | number | required | Lower tick. Ignored when tokenId is set. |
tickUpper | number | required | Upper tick. Ignored when tokenId is set. |
hooks | string | optional | Hook contract. Defaults to the zero address. |
hookData | string | optional | Hook calldata. Defaults to "0x". |
tokenId | string | optional | Existing position to deposit into. Needs the extra ERC-721 approval reported by /approve. |
Two pool shapes are rejected outright:
- Pools using the native currency — use the wrapped token's pool instead.
- Pools whose hook returns a liquidity delta.
protocolData — what happens to the result
Optional. Adds a final step that consumes whatever lpData produced, so the
token the zap ends holding has to be the token that step expects.
ApeBond
Buys a bond with the zap output. Pair with none, single,
univ2, solidly, curve, gamma,
ichi or steer, ending in the bond's principal token.
| Field | Type | Description | |
|---|---|---|---|
protocol | "ApeBond" | required | |
bond | string | required | Bond contract to deposit into. |
slippage | number | optional | Percent, 0–20. Default 1. |
tierBoostRate | number | optional | Percent of fromAmount spent on tier points for recipient. Default 1. Send 0 to take nothing. |
enableTierOptimizer | boolean | optional | Default true. Allows the API to buy a higher discount tier when that nets out profitable at this size. |
tierProofSignature | string | optional | A tier proof you obtained yourself, used to price the bond. Replaced if the optimizer buys a tier. |
Money leaves fromAmount before the bond is bought. The
tier boost fee (tierBoostRate, 1% by default) is always taken. On top of
that, if enableTierOptimizer is on and a tier upgrade is profitable at
this size, its cost is taken too and the bond is bought at the better tier. What
remains is tierOptimization.bondInput in the response. Send
tierBoostRate: 0 and enableTierOptimizer: false for a plain
bond purchase with nothing withheld.
Both are best-effort: if the tier service is unreachable the zap is still built, without them.
{
"recipient": "0xYourWalletAddress",
"user": "0xYourWalletAddress",
"chain": "bnb",
"lpData": {
"lpType": "univ2",
"fromToken": "0x55d398326f99059fF775485246999027B3197955",
"fromAmount": "1000000000000000000",
"lpAddress": "0xTheBondsLpToken",
"router": "0x10ED43C718714eb63d5aA57B78B54704E256024E",
"slippage": 1
},
"protocolData": {
"protocol": "ApeBond",
"bond": "0xTheBondContract",
"slippage": 1
}
}
VotingEscrowApeBond
Swaps into ABOND and creates a lock for recipient. Pair with
single whose toToken is ABOND
(0x34294AfABCbaFfc616ac6614F6d2e17260b78BEd) — any other
toToken fails the request.
| Field | Type | Description | |
|---|---|---|---|
protocol | "VotingEscrowApeBond" | required | |
lockDuration | integer | required | Seconds, 0 or greater. Ignored when tokenId is set. |
lockType | 0 | 1 | 2 | required | 0 non-permanent, 1 rolling, 2 permanent. Ignored when tokenId is set. |
tokenId | integer | optional | Add to this existing lock instead of creating one. |
votingEscrow | string | optional | Voting escrow contract. Defaults to 0xDF1dD618f3B564765e3ffc9F229637942ef601B2. |
LynexGauge
Stakes the LP token into a Lynex gauge. Pair with solidly, ending in the
gauge's stake token.
| Field | Type | Description | |
|---|---|---|---|
protocol | "LynexGauge" | required | |
gauge | string | required | Gauge contract to stake into. |
Hydrex
Stakes into a Hydrex gauge. Pair with any lpType that produces an ERC-20.
The gauge's stakeToken() must be exactly the token the zap produces, or the
request is rejected with the token it does expect.
| Field | Type | Description | |
|---|---|---|---|
protocol | "Hydrex" | required | |
gauge | string | required | Gauge contract to stake into. |
Lending gauges take a plain token rather than an LP — they deposit into the ERC-4626
vault themselves, so pair those with lpType: "single" targeting the vault's
underlying. A vault sitting at its supply cap rejects the zap up front.
The stake is an internal balance, not a receipt token, so nothing new lands in the recipient's wallet.
Vex
Buys a listed veNFT from a VeMarket listing and sends it to recipient.
Pair with single or none, ending in the token the listing is
priced in. Listings priced in native currency cannot be zapped into.
| Field | Type | Description | |
|---|---|---|---|
protocol | "Vex" | required | |
veMarket | string | required | VeMarket contract. |
listingIndex | integer | required | Index of the listing to buy. 0 or greater. |
Response
{
"requestId": "0x8f3c…",
"deadline": 1754500000,
"swapQuote1": {},
"swapQuote2": {},
"lpQuote": {
"lpAddress": "0x…",
"token0": {
"address": "0x…",
"fromAmountEstimate": "497512437810945273",
"fromAmountMin": "492537313432835821",
"fromAmountUSD": "0.49"
},
"token1": {
"address": "0x…",
"fromAmountEstimate": "1240000000000000",
"fromAmountMin": "1227600000000000",
"fromAmountUSD": "0.49"
},
"fromAmountUSD": 0.98
},
"protocolQuote": {
"protocol": "ApeBond",
"trueBondPrice": "1024500000000000000"
},
"txData": {
"to": "0x45BEc44bb43555C07940576dD085d01B60c6BCbf",
"data": "0x…",
"from": "0xYourWalletAddress",
"value": "0"
}
}
| Field | Type | Description |
|---|---|---|
requestId | string | Random 32-byte id, emitted on-chain when the order fills. |
deadline | number | Unix seconds. Ten minutes from when the quote was built. |
swapQuote1 | object | Raw LI.FI quote for the first swap leg, when there is one. |
swapQuote2 | object | Raw LI.FI quote for the second leg, on two-sided destinations. |
lpQuote | object | What the destination will receive. Present on every zap. |
protocolQuote | object | Present only when protocolData was sent. |
txData | object | The transaction. Submit unchanged. |
lpQuote
| Field | Type | Description |
|---|---|---|
lpAddress | string |
The destination. The LP token or vault share for pair and vault types;
toToken for single; fromToken for
none; the position manager for univ3,
algebra and pancakev4.
|
token0, token1 | object |
Per-side address, fromAmountEstimate,
fromAmountMin and fromAmountUSD (a string) — the amount
of that token going into the destination. One-sided destinations fill only
token0; a none zap and a single zap whose
toToken equals fromToken fill neither.
|
fromAmountUSD | number | Total USD value being deposited, summed across the sides. Absent when no swap was quoted. |
protocolQuote
Always carries protocol. Vex adds listingIndex.
ApeBond adds trueBondPrice and, when a tier was bought, a
tierOptimization object; the other protocols carry nothing else.
tierOptimization | Type | Description |
|---|---|---|
previousTier | number | Recipient's tier before the zap. |
newTier | number | Tier the bond is priced at. |
tierCost | string | Raw amount of fromToken spent buying the tier. |
bondInput | string | Raw amount that actually reaches the bond after every deduction. |
profit | string | Raw amount retained as the API's margin on the upgrade. |
txData
| Field | Type | Description |
|---|---|---|
to | string | The zap router on that chain — see Contracts. |
data | string | Encoded calldata for the whole route. |
from | string | The user address. |
value | string | Raw native amount to attach. "0" unless the input is native. |
Try zap
Pick a pool, say what you are paying with and how much, and run the whole flow against the real API with your own wallet. The request is shown as it is built, so this doubles as a worked example of every call on this page.
Supported chains
Send chain as a name, a short code or the numeric chain ID. Matching ignores
case, spaces, hyphens and underscores. An unrecognised value returns a 400
listing what is accepted.
| 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 |
Contracts
Same address on every supported chain.
| Contract | Address |
|---|---|
| Zap router | 0x45BEc44bb43555C07940576dD085d01B60c6BCbf |
| Token manager | 0x372F055470ef14EB79AfA1e1Ae301629FE1E050F |
We recommend taking txData.to from the response rather than hardcoding
the router — it is the only value guaranteed to match the chain you asked for.
Contact
Want to integrate zaps into your project? Need a custom zap flow for your protocol? Missing a chain? Or just want to say hi? We are happy to hear from you.
We build integration routes for partners at no cost — tell us where you want a zap to land and we will do the on-chain work.