Skip to main content
POST /v2/wallets/strategy/optimize computes the highest-APY allocation subject to your constraints and returns an allocations array you can pass straight into POST /v2/wallets or PATCH /v2/wallets/{id}/strategy.

Request

All request body fields are optional. Submit an empty body {} to get the unconstrained highest-APY allocation across all available yield sources.
curl -X POST "$BASE_URL/v2/wallets/strategy/optimize" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "minBlendedApyBps": 400,
    "concentration": { "maxPerProtocolPct": 60 },
    "liquidity": { "maxSettlementHours": 24 }
  }'
FieldTypeDescription
minBlendedApyBpsinteger, 0–10000Preferred minimum blended APY in basis points. If unreachable, the best-achievable allocation is still returned with unmetConstraints: ["minBlendedApyBps"].
liquidity.maxSettlementHoursinteger, ≥0Exclude any source whose maximum processing time exceeds this number of hours. 0 means instant-liquidity only.
concentration.maxPerSourcePctinteger, 1–100Cap on any single source’s allocation percentage.
concentration.maxPerProtocolPctinteger, 1–100Cap on the combined allocation across all sources sharing the same protocol slug. For example, morpho-august-usdc-v2, morpho-gauntlet-usdc, morpho-steakhouse-usdc, and morpho-smokehouse-usdc all count against the morpho protocol cap.
allowedChainsarray of stringsRestrict eligible sources to the listed chain slugs (e.g. ["ethereum", "base"]). Must be a non-empty array if provided. Omit to allow all chains.
excludedSourcesarray of stringsYield source IDs to exclude (e.g. ["syrup-usdc"]).
excludedProtocolsarray of stringsProtocol slugs to exclude entirely (e.g. ["morpho"]). Case-insensitive: "MORPHO", "Morpho", and "morpho" are equivalent.
maxSourcespositive integerCap on the number of sources in the resulting allocation.

Response (200)

{
  "allocations": [
    { "yieldSourceId": "morpho-august-usdc-v2", "pct": 60 },
    { "yieldSourceId": "morpho-gauntlet-usdc", "pct": 40 }
  ],
  "summary": {
    "blendedApyBps": 498,
    "sourceCount": 2,
    "protocolCount": 1,
    "liquidityProfile": {
      "PT0H": 100
    }
  },
  "sources": [
    {
      "yieldSourceId": "morpho-august-usdc-v2",
      "pct": 60,
      "apyBps": 563,
      "protocol": "morpho",
      "chain": "ethereum",
      "maxProcessingTime": "PT0H",
      "expectedProcessingTime": "PT0H"
    },
    {
      "yieldSourceId": "morpho-gauntlet-usdc",
      "pct": 40,
      "apyBps": 472,
      "protocol": "morpho",
      "chain": "ethereum",
      "maxProcessingTime": "PT0H",
      "expectedProcessingTime": "PT0H"
    }
  ]
}

allocations[]

FieldDescription
yieldSourceIdYield source identifier. Pass this directly into strategy.allocations[].yieldSourceId when creating or updating a wallet.
pctInteger percentage (0–100). All entries sum to exactly 100.

summary

FieldDescription
blendedApyBpsWeighted-average APY of the allocation in basis points (conservative floor, never overstated).
sourceCountNumber of sources used in the allocation.
protocolCountNumber of distinct protocols used.
liquidityProfileObject mapping ISO 8601 duration → combined percentage. Each key is a maxProcessingTime bucket; the value is the total percentage allocated to sources with that liquidity tier. Example: { "PT0H": 70, "PT24H": 30 } means 70% is instantly liquid and 30% has up to a 24-hour unwind.

sources[]

Per-source detail mirroring the allocation. Useful for displaying source-level metadata before committing to a strategy.
FieldDescription
yieldSourceIdSame as in allocations[].
pctAllocated percentage for this source.
apyBpsCurrent APY for this source in basis points.
protocolProtocol slug (e.g. morpho, maple, resolv).
chainChain slug (e.g. ethereum, base).
maxProcessingTimeISO 8601 duration representing the worst-case withdrawal completion time (SLA upper bound).
expectedProcessingTimeISO 8601 duration representing the typical withdrawal completion time.

unmetConstraints (optional)

If the best-achievable allocation fails to satisfy one or more of your non-hard constraints, unmetConstraints appears with an array of the failed constraint names. Today the only value that can appear is "minBlendedApyBps" — when your requested APY floor is higher than what’s achievable. The caller can decide whether to accept the returned allocation or loosen the floor. When all constraints are satisfied, unmetConstraints is absent from the response entirely.
{
  "allocations": [
    { "yieldSourceId": "morpho-august-usdc-v2", "pct": 100 }
  ],
  "summary": {
    "blendedApyBps": 563,
    "sourceCount": 1,
    "protocolCount": 1,
    "liquidityProfile": { "PT0H": 100 }
  },
  "sources": [
    {
      "yieldSourceId": "morpho-august-usdc-v2",
      "pct": 100,
      "apyBps": 563,
      "protocol": "morpho",
      "chain": "ethereum",
      "maxProcessingTime": "PT0H",
      "expectedProcessingTime": "PT30M"
    }
  ],
  "unmetConstraints": ["minBlendedApyBps"]
}

Infeasibility (422)

When the optimizer cannot produce a valid allocation, it returns a 422 with a machine-readable reason code.

NO_ELIGIBLE_SOURCES

The eligibility filters (chain, liquidity, excluded sources/protocols) produced an empty candidate set.
{
  "error": "INFEASIBLE",
  "reason": "NO_ELIGIBLE_SOURCES",
  "detail": "No yield sources satisfy the supplied constraints."
}

CAPS_PREVENT_FULL_ALLOCATION

Eligible sources cannot be combined to sum to 100% under the supplied concentration or maxSources caps.
{
  "error": "INFEASIBLE",
  "reason": "CAPS_PREVENT_FULL_ALLOCATION",
  "detail": "Supplied concentration / maxSources caps cannot reach 100% allocation with eligible sources."
}

Validation errors (400)

Malformed request body fields return 400 with a human-readable error message:
{ "error": "minBlendedApyBps must be an integer between 0 and 10000" }
Common validation failures:
FieldExample error
minBlendedApyBps out of range or non-integerminBlendedApyBps must be an integer between 0 and 10000
liquidity.maxSettlementHours negativeliquidity.maxSettlementHours must be a non-negative integer
concentration.maxPerSourcePct out of rangeconcentration.maxPerSourcePct must be an integer between 1 and 100
allowedChains is an empty arrayallowedChains must be a non-empty array if provided
maxSources non-positivemaxSources must be a positive integer

Worked example — chaining into wallet creation

// 1. Ask for an allocation
const optimizeRes = await fetch(`${API_BASE}/v2/wallets/strategy/optimize`, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
        minBlendedApyBps: 400,
        concentration: { maxPerProtocolPct: 60 }
    })
});
const { allocations } = await optimizeRes.json();

// 2. Pass straight into wallet creation
await fetch(`${API_BASE}/v2/wallets`, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
        requestId: crypto.randomUUID(),
        label: 'Customer portfolio',
        strategy: { allocations }
    })
});
The allocations array returned by this endpoint is designed to be passed directly into POST /v2/wallets or PATCH /v2/wallets/{id}/strategy without transformation.
APY rates change continuously. The allocation returned reflects rates at request time. For wallets that will be created minutes or hours later, consider fetching a fresh recommendation closer to creation time.