Skip to main content
Get a detailed and accurate portfolio view for a user, including positions, balances by asset type, and session key status. This method provides more accurate data than getPositions, use it to display portfolio value in your UI. Balances are enriched with net-of-pending-fee fields (balanceWithFee, underlyingAmountWithFee). The pending fee is derived from onchain current earnings × the Zyfi fee rate (10%). Gross fields (balance, underlyingAmount) are unchanged. If earnings cannot be fetched, *WithFee equals the gross value so the response shape stays stable. Each position also includes pool_apy_withFee (pool_apy × 0.9). Gross pool_apy is unchanged.
Show balanceWithFee / underlyingAmountWithFee / pool_apy_withFee as the primary numbers in your product UI (parity with the Zyfai app). Gross fields remain available for debugging or advanced transparency.

Signature

Parameters

Returns

Detailed portfolio data including positions and balances by asset type, plus fee-adjusted balance fields.

Return Type

Computing the total balance

portfolioByAssetType is not the user’s total balance. The backend builds it from exactly two sources: the underlying amount of every deployed position and the idle balances sitting in the Safe. Anything in neither place is missing from it. That gap is real under the yieldmaxxing strategy. When the agent requests a redemption from a delayed-withdrawal protocol, the position leaves the snapshot immediately while the funds stay in the vault for a day or three. During that window they appear in no balance field, only in pendingAsyncWithdrawals. So an integration needs two different numbers, and showing one where the other belongs is the most common mistake: An in-flight amount is no longer withdrawable. A withdrawFunds call only reaches positions in the current snapshot and idle Safe balances, and the in-flight amount is in neither. Calling it again will not pull those funds out any faster. If every remaining position sits in a pool that already has a REQUESTED or CLAIMABLE entry, the call throws — async pools allow only one redemption at a time. They land on the user’s EOA on their own once the protocol releases them, so the right UI is to show them as pending with their estimatedClaimAt and disable withdraw until the entry reaches CLAIMED. See withdrawFunds.
Note the ?? "0x0" below: when every position of an asset is in flight, the backend drops the asset key from portfolioByAssetType altogether rather than reporting a zero balance, and portfolioByAssetType itself can be {}.
requestable is what a withdrawal can still be asked on, not what arrives immediately. It also covers async positions the user currently holds, and withdrawing those turns them into a new in-flight redemption that settles days later.Cap the amount a user can enter at requestable, never at total. Asking for more does not fail loudly: the backend transfers what it can right away and queues a redemption for the shortfall, so the user gets a partial transfer now and the rest days later. This is rarely what they expected when they typed the number.
To tell apart the part of requestable that settles immediately, cross-check each entry of positions against getAsyncOpportunities: a position whose protocol_name and pool match an async opportunity will settle through a delayed redemption, while idle balances and every other position settle in the withdrawal transaction itself.
Do not add staleBalances. They are the same idle Safe balances that portfolioByAssetType already counts, exposed as a per-chain view so you can surface funds waiting to be deployed. Adding them double-counts.Do not add every pendingAsyncWithdrawals entry. The array also keeps CLAIMED requests for 24 hours so you can show a recent history. Those funds are back in the Safe and already counted. Filter on REQUESTED and CLAIMABLE.

Displaying an in-flight redemption

Each entry carries what a progress UI needs: status, amount, protocol.name, pool, and estimatedClaimAt. When the protocol is temporarily refusing claims, statusMessage holds copy ready to display. amount is hex-encoded least units, like every other balance in this payload. For example, "0x98967f" is 9999999, so 9.999999 USDC at 6 decimals. Read it with BigInt, never with parseInt or Number.
A FAILED status is not a loss of funds: the protocol did not allow the claim in time, so the position is restored and the request is retried on the next cycle. pauseMessageByToken is a companion field holding user-facing copy when deposits are paused for a token (tokenized stocks over the weekend, for example), keyed by token symbol.

How *WithFee is calculated

  • Fee source: current_earnings_by_chain only (fetched in parallel with the portfolio)
  • Multiple positions on the same chain + token: fee is split proportionally by underlyingAmount
  • Portfolio balances are live, but earnings used for the fee may be from a snapshot. Small mismatches are possible.

Example

Notes

  • This method automatically resolves the smart wallet address from the EOA
  • If no smart wallet exists for the user, returns an empty portfolio
  • Auth is not required for this read path
  • portfolioByAssetType provides balances grouped by token (e.g., USDC, WETH, EURC): positions plus idle Safe balances, and nothing else
  • portfolioByChain provides the same balance structure grouped by chain, then by asset type
  • For the true total under the yieldmaxxing strategy, see Computing the total balance
  • Decode hex / wei amounts with the correct decimals (USDC/EURC = 6, WETH = 18, …)
  • For earnings net of fee, see getOnchainEarnings
  • For legacy position data, see getPositions