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.

Base URL https://api.soulsolidity.com
AuthNone. No key, no rate limit.
Coverage8 chains · 11 LP types · 5 protocols
1
POST /approve

Returns every approval the zap needs, each flagged. Submit the missing ones.

2
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.

Start here → or run a real zap in the browser

Getting started

Point a coding agent at the integration guide and it has everything it needs.

paste into your agent
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.

bash
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.

200 OK
{ "requestId": "0x…", "deadline": 1754500000, "txData": { } }
4xx / 5xx
{
  "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:

javascript
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, algebra or pancakev4 tokenId: the position must already be owned by recipient, otherwise the request is rejected with Position … is owned by ….
  • ApeBond tiers are read against recipient, not user, 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

POST https://api.soulsolidity.com/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

FieldTypeDescription
chainstring | numberrequiredName, short code or chain ID — see Supported chains.
tokenstringrequiredThe zap's input token. A native sentinel yields an empty approvals array.
amountstringrequiredRaw amount to be spent. The allowance is compared against this.
userstringrequiredWallet whose allowances are read.
lpDataobjectoptionalThe 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

json
{
  "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"
      }
    }
  ]
}
FieldTypeDescription
kind"erc20" | "erc721"An allowance to the token manager, or an operator approval on a position manager.
tokenstringContract the approval is granted on.
spenderstringAddress being approved.
descriptionstringPlain-language purpose, safe to put on a button.
approvedbooleantrue if already satisfied — skip it.
txDataobject{ to, data, from? }. Submit as-is.
Caution

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

POST https://api.soulsolidity.com/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

FieldTypeDescription
userstringrequiredAddress the input is pulled from. Becomes txData.from.
recipientstringoptionalAddress the outputs are sent to. Defaults to user.
chainstring | numberrequiredName, short code or chain ID — see Supported chains.
lpDataobjectrequiredThe destination. Discriminated by lpType.
protocolDataobjectoptionalWhat to do with the result. Discriminated by protocol.
integratorstringoptionalPartner address or known partner name, emitted on-chain for attribution.
rpcstringoptionalRPC URL to use instead of the API's default for that chain.

lpData — where the zap ends

Fields every variant takes

FieldTypeDescription
lpTypestringrequiredSelects the variant.
fromTokenstringrequiredInput token, or a native sentinel.
fromAmountstringrequiredRaw input amount.
slippagenumberoptionalPercent, 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.

FieldTypeDescription
toTokenstringrequiredToken to swap into.
json
{
  "lpType": "single",
  "fromToken": "0x55d398326f99059fF775485246999027B3197955",
  "toToken": "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
  "fromAmount": "1000000000000000000",
  "slippage": 1
}

univ2

FieldTypeDescription
lpAddressstringrequiredThe pair contract.
routerstringrequiredRouter used to add liquidity.
json
{
  "lpType": "univ2",
  "fromToken": "0x55d398326f99059fF775485246999027B3197955",
  "fromAmount": "1000000000000000000",
  "lpAddress": "0xYourPairAddress",
  "router": "0x10ED43C718714eb63d5aA57B78B54704E256024E",
  "slippage": 1
}

solidly

FieldTypeDescription
lpAddressstringrequiredThe pair contract. Stable/volatile is read from it.
routerstringrequiredRouter used to add liquidity.

curve

Two-crypto pools. The pool contract is also the LP token.

FieldTypeDescription
lpAddressstringrequiredThe pool contract.

gamma

FieldTypeDescription
uniProxystringoptionalUniProxy contract that quotes the deposit ratio. Defaults to the proxy the hypervisor whitelists.
lpAddressstringrequiredHypervisor 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.

FieldTypeDescription
lpAddressstringrequiredICHI vault to deposit into. Also the output token.
depositGuardstringoptionalOverride. Required together with vaultDeployer.
vaultDeployerstringoptionalOverride. Required together with depositGuard.
json
{
  "lpType": "ichi",
  "fromToken": "0x55d398326f99059fF775485246999027B3197955",
  "fromAmount": "1000000000000000000",
  "lpAddress": "0xYourIchiVault",
  "slippage": 1
}

steer

FieldTypeDescription
lpAddressstringrequiredSteer vault.
peripherystringrequiredPeriphery 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:

  • protocolData is rejected with a 400 — the NFT is minted straight to recipient, so no protocol step can consume it.
  • Omit tokenId to mint a new position. Supply it to deposit into an existing one, in which case that position's own range is used and tickLower/tickUpper are ignored.
  • An existing tokenId must already be owned by recipient, 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 tickLower must be below tickUpper.
FieldTypeDescription
positionManagerstringrequiredNonfungiblePositionManager — the call target.
poolstringoptionalPool contract, read for price and tick spacing. Required to mint; with tokenId it defaults to the pool that position is in.
tickLowernumberrequiredLower tick. Ignored when tokenId is set.
tickUppernumberrequiredUpper tick. Ignored when tokenId is set.
tokenIdstringoptionalExisting position to deposit into.
json
{
  "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:

  • protocolData is rejected with a 400 — the NFT is minted straight to recipient, so no protocol step can consume it.
  • Omit tokenId to mint a new position. Supply it to deposit into an existing one, in which case that position's own range is used and tickLower/tickUpper are ignored.
  • An existing tokenId must already be owned by recipient, 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 tickLower must be below tickUpper.
FieldTypeDescription
positionManagerstringrequiredCLPositionManager — the call target. Its pool manager is read off it.
currency0stringrequiredFirst currency of the pool key. Must sort strictly below currency1.
currency1stringrequiredSecond currency of the pool key.
feenumberrequiredPool fee.
parametersstringrequiredbytes32 packing tick spacing and the hook permission bitmap.
tickLowernumberrequiredLower tick. Ignored when tokenId is set.
tickUppernumberrequiredUpper tick. Ignored when tokenId is set.
hooksstringoptionalHook contract. Defaults to the zero address.
hookDatastringoptionalHook calldata. Defaults to "0x".
tokenIdstringoptionalExisting 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.

FieldTypeDescription
protocol"ApeBond"required
bondstringrequiredBond contract to deposit into.
slippagenumberoptionalPercent, 0–20. Default 1.
tierBoostRatenumberoptionalPercent of fromAmount spent on tier points for recipient. Default 1. Send 0 to take nothing.
enableTierOptimizerbooleanoptionalDefault true. Allows the API to buy a higher discount tier when that nets out profitable at this size.
tierProofSignaturestringoptionalA tier proof you obtained yourself, used to price the bond. Replaced if the optimizer buys a tier.
Caution

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.

json
{
  "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.

FieldTypeDescription
protocol"VotingEscrowApeBond"required
lockDurationintegerrequiredSeconds, 0 or greater. Ignored when tokenId is set.
lockType0 | 1 | 2required0 non-permanent, 1 rolling, 2 permanent. Ignored when tokenId is set.
tokenIdintegeroptionalAdd to this existing lock instead of creating one.
votingEscrowstringoptionalVoting escrow contract. Defaults to 0xDF1dD618f3B564765e3ffc9F229637942ef601B2.

LynexGauge

Stakes the LP token into a Lynex gauge. Pair with solidly, ending in the gauge's stake token.

FieldTypeDescription
protocol"LynexGauge"required
gaugestringrequiredGauge 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.

FieldTypeDescription
protocol"Hydrex"required
gaugestringrequiredGauge 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.

FieldTypeDescription
protocol"Vex"required
veMarketstringrequiredVeMarket contract.
listingIndexintegerrequiredIndex of the listing to buy. 0 or greater.

Response

json · 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"
  }
}
FieldTypeDescription
requestIdstringRandom 32-byte id, emitted on-chain when the order fills.
deadlinenumberUnix seconds. Ten minutes from when the quote was built.
swapQuote1objectRaw LI.FI quote for the first swap leg, when there is one.
swapQuote2objectRaw LI.FI quote for the second leg, on two-sided destinations.
lpQuoteobjectWhat the destination will receive. Present on every zap.
protocolQuoteobjectPresent only when protocolData was sent.
txDataobjectThe transaction. Submit unchanged.

lpQuote

FieldTypeDescription
lpAddressstring 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, token1object 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.
fromAmountUSDnumberTotal 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.

tierOptimizationTypeDescription
previousTiernumberRecipient's tier before the zap.
newTiernumberTier the bond is priced at.
tierCoststringRaw amount of fromToken spent buying the tier.
bondInputstringRaw amount that actually reaches the bond after every deduction.
profitstringRaw amount retained as the API's margin on the upgrade.

txData

FieldTypeDescription
tostringThe zap router on that chain — see Contracts.
datastringEncoded calldata for the whole route.
fromstringThe user address.
valuestringRaw 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.

ChainSend asChain ID
Ethereumethereum1
BNB Chainbnb56
Polygonpolygon137
Basebase8453
Arbitrumarbitrum42161
HyperEVMhyperevm999
Robinhood Chainrobinhood4663
Plasmaplasma9745

Contracts

Same address on every supported chain.

ContractAddress
Zap router0x45BEc44bb43555C07940576dD085d01B60c6BCbf
Token manager0x372F055470ef14EB79AfA1e1Ae301629FE1E050F

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.