Preview the best yield split for an amount before anyone signs anything. Give it an amount, a token, and a chain (or several), and it comes back with the pools it would route into, the blended APY, and ready-to-run transaction calldata for each position.
This is a pure simulation: read-only, no signing, no gas. It only needs your API key, so it’s a great fit for calculators, previews, and “what would I earn” dashboards. Call it as often as you like as the user tweaks their inputs.
interface SimulateBestPositionsResponse { success: boolean; data: Record<string, SimulatedPosition[]>; // keyed by chain ID messages: Record<string, string>; // per-chain notes, e.g. why a chain came back empty}interface SimulatedPosition { protocol: string; pool: string; simulated_apy: number; // base APY, no rewards combined_apy: number; // Headline APY including rewards. Use this one. amount: number; // how much goes into this pool, human units amount_raw: string; // the same amount, in the token's raw decimal units tvl: number; // pool's total TVL liquidity: number; // pool's available liquidity averageCombinedApy30Days: number; // 30-day average, useful to confirm the APY isn't a spike url: string; // link to the pool calldata: SimulateCalldataItem[];}interface SimulateCalldataItem { contract_address: string; function_name: string; // e.g. "approve", "deposit" parameters: string[]; value: string; // ETH value, usually "0" description: string; // human-readable, e.g. "Approve 3000 USDC for Morpho"}
The deposit calldata contains a "<RECEIVER>" placeholder among its parameters. Swap it for the real receiving address (usually the user’s Safe / smart wallet) before you send the transaction.
A few behaviors worth knowing before you build against this, confirmed by calling the live API directly:
userPositions is additive, not just contextual. If the user already has 1,500inapoolandyousimulatedepositing3,000 more, the returned position is sized for the combined4,500,not3,000. Pass it in whenever you know where the user already is.
minSplit divides the amount evenly across that many pools. minSplit: 3 on 5,000returnsthree1,666.67 positions, each with its own calldata.
A chain can come back empty on purpose. If every candidate pool for a chain gets excluded (unstable APY, doesn’t clear the strategy’s risk bar, etc.), that chain’s entry in data is [] and messages[chainId] explains why.
An unmatched pools filter isn’t a dead end. The call still returns success: true with an empty array for that chain, and messages[chainId] lists the pools that actually exist for that token/chain/strategy. This is handy for a “did you mean…” experience.
Works for any supported chain/token combination, with no special-casing needed for less common pairs.