Skip to main content

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.

No wallet required

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:

FieldTypeRequiredDescription
amountnumberAmount to deploy, in human units (e.g. 3000 for 3,000 USDC) — not wei.
tokenstringAsset symbol, e.g. "USDC", "WETH".
networksnumber | 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.
minSplitnumberMinimum number of pools to spread the amount across.
protocolsstring[]Restrict candidates to these protocols, e.g. ["aave", "morpho"].
poolsstring[]Restrict candidates to these exact pool names.
userPositionsUserPosition[]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"
}
Replace the receiver before sending

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:

  • userPositions is additive, not just contextual. If the user already has 1,500inapoolandyousimulatedepositing1,500 in a pool and you simulate depositing 3,000 more, the returned position is sized for the combined 4,500not4,500 — not 3,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,000returnsthree 5,000 returns three ~1,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 — 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;