simulateBestPositions
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.
Signature
simulateBestPositions(params: SimulateBestPositionsParams): Promise<SimulateBestPositionsResponse>
Parameters
params is a single object:
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | ✅ | Amount to deploy, in human units (e.g. 3000 for 3,000 USDC) — not wei. |
token | string | ✅ | Asset symbol, e.g. "USDC", "WETH". |
networks | number | number[] | ✅ | A chain ID, or an array of them, e.g. 8453 or [8453, 42161]. |
strategy | "conservative" | "aggressive" | ✅ | "conservative" favors lower risk; "aggressive" chases higher yield. |
minSplit | number | ❌ | Minimum number of pools to spread the amount across. |
protocols | string[] | ❌ | Restrict candidates to these protocols, e.g. ["aave", "morpho"]. |
pools | string[] | ❌ | Restrict candidates to these exact pool names. |
userPositions | UserPosition[] | ❌ | Positions the user already holds, so the simulation can account for them (see below). |
interface UserPosition {
protocol: string;
pool: string;
tvl: number; // the user's position size in this pool — not the pool's total TVL
}
Returns
Chain-keyed simulated positions, plus calldata to actually enter each one.
Return Type
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.
Good to know
A few behaviors worth knowing before you build against this, confirmed by calling the live API directly:
userPositionsis additive, not just contextual. If the user already has 3,000 more, the returned position is sized for the combined 3,000. Pass it in whenever you know where the user already is.minSplitdivides the amount evenly across that many pools.minSplit: 3on 1,666.67 positions, each with its owncalldata.- 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
datais[]andmessages[chainId]explains why. - An unmatched
poolsfilter isn't a dead end. The call still returnssuccess: truewith an empty array for that chain, andmessages[chainId]lists the pools that actually exist for that token/chain/strategy — handy for a "did you mean…" experience. - Works for any supported chain/token combination — no special-casing needed for less common pairs.
Examples
Simplest call
const result = await sdk.simulateBestPositions({
amount: 3000,
token: "USDC",
networks: 8453,
strategy: "conservative",
});
console.log(result.data["8453"]);
// [{ protocol: "Morpho", pool: "Gauntlet USDC Prime", combined_apy: 4.15, ... }]
Multiple chains, with a minimum split
const result = await sdk.simulateBestPositions({
amount: 5000,
token: "USDC",
networks: [8453, 42161],
strategy: "conservative",
minSplit: 3,
});
const positions = Object.values(result.data).flat();
console.log(positions.map((p) => `${p.protocol}/${p.pool}: $${p.amount}`));
Accounting for an existing position
await sdk.simulateBestPositions({
amount: 3000,
token: "USDC",
networks: 8453,
strategy: "conservative",
userPositions: [
{ protocol: "aave", pool: "USDC", tvl: 1500 }, // the user's current position size
],
});
// Simulated against a $4,500 total, not $3,000.
Blended APY across a split
const positions = Object.values(result.data).flat();
const total = positions.reduce((sum, p) => sum + p.amount, 0);
const blendedApy =
positions.reduce((sum, p) => sum + p.amount * p.combined_apy, 0) / total;