--- name: soulzap-apebond description: Adds ApeBond support to a SoulZap integration - buying a bond in one transaction with any token, creating or topping up a veABOND lock, and the tier discount system. Use this whenever the task involves an ApeBond bond purchase, a "bond with any token" button, a tier boost or tier optimizer, or a veABOND lock. Companion to soulzap-integration.txt, which must be read first. --- # ApeBond with the SoulZap API > **Read `soulzap-integration.txt` first** - it carries the flow, the rules and every `lpData` > variant. This file only adds what is specific to ApeBond. Two products live here, and they use different `protocolData`: - **Bonds** - `protocol: "ApeBond"`. Swap any token into the bond's principal and buy the bond. - **veABOND locks** - `protocol: "VotingEscrowApeBond"`. Swap any token into ABOND and lock it. ## Where bonds come from Bonds are redeployed constantly, so a hardcoded list goes stale. Read the live one: ``` GET https://realtime-api.ape.bond/bonds -> { bonds: [ ... ] } ``` Fields that matter per entry: | Field | Use | |---|---| | `billAddress` | The `bond` address in `protocolData`. | | `principalToken` | What the bond accepts. Your `lpData` must end up holding exactly this. | | `chainId` | Which chain to send. | | `billType` | `"liquidity"` means the principal is an LP token; anything else means a plain token. | | `lpToken.liquidityDex` | Which DEX the LP lives on - decides `univ2` vs `solidly`. | | `tierBoostRate` | The bond's own boost rate, if it sets one. | | `soldOut`, `hide` | Filter these out of a UI. | Two things to guard against: the endpoint also returns **Solana** bonds, which have no EVM address - keep only entries whose `billAddress` matches `/^0x[0-9a-fA-F]{40}$/`. And never invent a bond address; if it is not in this feed, ask. ## Choosing the `lpType` The whole `lpData` exists to end up holding the bond's `principalToken`. | Bond | `lpType` | How to fill it | |---|---|---| | `billType` is not `"liquidity"` | `single` | `toToken` = `principalToken` | | `billType` is `"liquidity"`, on a Solidly-style DEX | `solidly` | `lpAddress` = `principalToken`, plus that DEX's `router` | | `billType` is `"liquidity"`, on a V2-style DEX | `univ2` | `lpAddress` = `principalToken`, plus that DEX's `router` | Solidly-style DEXes include Aerodrome, Velodrome, THENA, Ramses, Equalizer, Solidly, Lynex, Nile, Cleo, Fenix, Pharaoh and Hydrex. `lpToken.liquidityDex` names the DEX; match on it case-insensitively. If the two are ambiguous, the pair itself is authoritative - a Solidly pair exposes `stable()`, a plain V2 pair does not. The API also accepts `none`, `gamma`, `ichi`, `curve` and `steer` for bonds, if the principal is one of those. It does **not** accept `univ3`, `algebra` or `pancakev4`. ## `protocolData` - buying a bond ```json { "protocol": "ApeBond", "bond": "0x...", "slippage": 1 } ``` | Field | Required | Notes | |---|---|---| | `bond` | yes | `billAddress` from the feed. | | `slippage` | no | Percent, **max 20** here (not 50). Default `1`. | | `enableTierOptimizer` | no | Default `true`. See below. | | `tierBoostRate` | no | Percent, 0-100. Overrides the default boost rate. | | `tierProofSignature` | no | Normally resolved for you. Only send one you obtained yourself. | ### Worked example - bond with USDC into an LP bond Only the request objects; approve and zap exactly as the base guide does. ```ts const bond = bonds.find(b => b.billAddress === selectedBondAddress) const lpData = { lpType: 'solidly', // because bond.billType === 'liquidity' on a solidly dex fromToken: usdcAddress, fromAmount: '10000000', // 10 USDC, raw lpAddress: bond.principalToken, // the LP the bond accepts router: dexRouterForThatLp, slippage: 1, } const protocolData = { protocol: 'ApeBond', bond: bond.billAddress, slippage: 1 } ``` `protocolQuote` comes back as `{ protocol: "ApeBond", trueBondPrice, tierOptimization? }`. `trueBondPrice` is the price actually paid, useful for showing the realised discount. ## Tiers A user's tier sets their bond discount. Two mechanisms run automatically inside `/zap`, both before the swap, and both take their cut out of `fromAmount` - so every quote you get back is already net of them. **Tier boost fee.** A small percentage of the input buys boost points toward the user's tier. The rate comes from `tierBoostRate` if you send it, otherwise from the bond's own rate, otherwise from a default. Send `tierBoostRate: 0` to switch it off. **Tier optimizer.** On by default. If jumping the user a full tier is mathematically profitable - the extra discount exceeds the cost of the tier - the zap buys the jump and applies the discount immediately via a generated proof. If it is not profitable, nothing happens. Set `enableTierOptimizer: false` to disable it. When the optimizer acts, `protocolQuote.tierOptimization` is populated: ```json { "previousTier": 1, "newTier": 3, "tierCost": "...", "bondInput": "...", "profit": "..." } ``` All three amounts are raw strings in the input token. `profit` is the net gain after `tierCost` - show it if you want to justify the jump to the user. ### Previewing tiers before zapping To show a tier ladder in your UI without building a transaction: ``` POST https://api.soulsolidity.com/apebond/tierOptimizationPreview { "bond": "0x...", "chainId": 56, "fromToken": "0x...", "fromAmount": "10000000", "user": "0x...", "tierBoostRate": 1 } ``` Returns `currentTier`, `optimalTier`, `abondPriceUsd`, `fromTokenPriceUsd`, `tierBoostPoints`, and a `tiers[]` array where each entry has `tier`, `tierName`, `bonus`, `abondRequired`, `tierCostInFromToken`, `bondInputInFromToken`, `profitInFromToken`, `isProfitable` and `minFromAmount`. This is read-only. It changes nothing and costs nothing; the actual decision still happens inside `/zap`. ## `protocolData` - creating a veABOND lock Swaps into ABOND and locks it in one transaction. Pair it with `lpType: "single"`, where `toToken` is ABOND. ```json { "protocol": "VotingEscrowApeBond", "lockDuration": 31536000, "lockType": 2 } ``` | Field | Required | Notes | |---|---|---| | `lockDuration` | yes | Seconds, integer, `0` or more. | | `lockType` | yes | `0` non-permanent, `1` rolling, `2` permanent. | | `votingEscrow` | no | The escrow contract. | | `tokenId` | no | Add to an existing lock instead of creating one. | ```ts const lpData = { lpType: 'single', fromToken: usdcAddress, fromAmount: '10000000', toToken: abondAddress, slippage: 1, } const protocolData = { protocol: 'VotingEscrowApeBond', lockDuration: 31536000, lockType: 2 } ``` The escrow contract takes its own treasury fee out of the lock. That is internal to it - it is not part of anything the zap quotes, so do not try to reconcile the two. ## Errors you will hit here | Message | Fix | |---|---| | `Bond ... takes ..., but this zap produces ...` | The `lpData` destination is not the bond's principal. With `univ3`/`algebra`/`pancakev4` it never can be - bonds cannot take a position NFT. | | `Input validation failed` with `path: ["protocolData","slippage"]` | ApeBond caps slippage at 20, not 50. | | `Multicall: Something went wrong` | Usually a stale bond address. Re-read the feed. |