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 }
}'
| Field | Type | Description |
|---|
minBlendedApyBps | integer, 0–10000 | Preferred minimum blended APY in basis points. If unreachable, the best-achievable allocation is still returned with unmetConstraints: ["minBlendedApyBps"]. |
liquidity.maxSettlementHours | integer, ≥0 | Exclude any source whose maximum processing time exceeds this number of hours. 0 means instant-liquidity only. |
concentration.maxPerSourcePct | integer, 1–100 | Cap on any single source’s allocation percentage. |
concentration.maxPerProtocolPct | integer, 1–100 | Cap 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. |
allowedChains | array of strings | Restrict eligible sources to the listed chain slugs (e.g. ["ethereum", "base"]). Must be a non-empty array if provided. Omit to allow all chains. |
excludedSources | array of strings | Yield source IDs to exclude (e.g. ["syrup-usdc"]). |
excludedProtocols | array of strings | Protocol slugs to exclude entirely (e.g. ["morpho"]). Case-insensitive: "MORPHO", "Morpho", and "morpho" are equivalent. |
maxSources | positive integer | Cap 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[]
| Field | Description |
|---|
yieldSourceId | Yield source identifier. Pass this directly into strategy.allocations[].yieldSourceId when creating or updating a wallet. |
pct | Integer percentage (0–100). All entries sum to exactly 100. |
summary
| Field | Description |
|---|
blendedApyBps | Weighted-average APY of the allocation in basis points (conservative floor, never overstated). |
sourceCount | Number of sources used in the allocation. |
protocolCount | Number of distinct protocols used. |
liquidityProfile | Object 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.
| Field | Description |
|---|
yieldSourceId | Same as in allocations[]. |
pct | Allocated percentage for this source. |
apyBps | Current APY for this source in basis points. |
protocol | Protocol slug (e.g. morpho, maple, resolv). |
chain | Chain slug (e.g. ethereum, base). |
maxProcessingTime | ISO 8601 duration representing the worst-case withdrawal completion time (SLA upper bound). |
expectedProcessingTime | ISO 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:
| Field | Example error |
|---|
minBlendedApyBps out of range or non-integer | minBlendedApyBps must be an integer between 0 and 10000 |
liquidity.maxSettlementHours negative | liquidity.maxSettlementHours must be a non-negative integer |
concentration.maxPerSourcePct out of range | concentration.maxPerSourcePct must be an integer between 1 and 100 |
allowedChains is an empty array | allowedChains must be a non-empty array if provided |
maxSources non-positive | maxSources 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.