> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zyf.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# getPortfolio

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.

<Tip title="User-facing display">
  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.
</Tip>

## Signature

```typescript theme={null}
getPortfolio(userAddress: string): Promise<PortfolioDetailedResponse>
```

## Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `userAddress` | `string` | ✅ | User's EOA address |

## Returns

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

## Return Type

```typescript theme={null}
interface PortfolioDetailedResponse {
  success: boolean;
  userAddress: string;
  portfolio: PortfolioDetailed;
}

interface PortfolioAssetBalance {
  balance: Hex;           // gross (live)
  balanceWithFee?: Hex;   // live − pending fee (SHOW THIS)
  decimals: number;
}

type PortfolioByAssetType = Record<string, PortfolioAssetBalance>;

type PortfolioByChain = Record<string, PortfolioByAssetType>;

interface PortfolioDetailed {
  hasBalance?: boolean;
  staleBalances?: staleBalances[];
  hasActiveSessionKey?: boolean;
  positions?: PositionSlot[];
  pendingAsyncWithdrawals?: AsyncWithdrawal[]; // NOT counted in the balances below
  pauseMessageByToken?: Record<string, string>;
  portfolioByAssetType?: PortfolioByAssetType;
  portfolioByChain?: PortfolioByChain;
}

interface staleBalances {
  chainId: number;
  tokenSymbol: string;
  balance: string;
  isPending: boolean;
  updatedAt?: string;
}

type AsyncWithdrawalStatus = "REQUESTED" | "CLAIMABLE" | "CLAIMED" | "FAILED";

interface AsyncWithdrawal {
  id: string;
  status: AsyncWithdrawalStatus;
  chainId: number;
  pool: string;              // vault identifier, e.g. "NVDAC"
  amount: string;            // Hex least units (e.g. "0x98967f"). Read with BigInt.
  token?: { symbol?: string; decimals?: number; address?: string };
  protocol?: {
    id?: string;
    name?: string;
    metadata?: { asyncWithdrawalDays?: number }; // nominal delay, in days
  };
  withdrawalTarget?: string | null;   // "user_eoa" when the user initiated it
  estimatedClaimAt?: string | null;   // ISO timestamp
  claimedAt?: string | null;
  requestTxHash?: string | null;
  claimTxHash?: string | null;
  retryCount?: number;
  statusMessage?: string | null;      // protocol pause copy, ready to display
}

interface PositionSlot {
  chain?: string;
  protocol_id?: string;
  protocol_name?: string;
  protocol_icon?: string;
  pool?: string;
  token_id?: string;
  token_symbol?: string;
  token_icon?: string;
  amount?: string;
  underlyingAmount?: string;        // gross
  underlyingAmountWithFee?: string; // SHOW THIS
  pool_apy?: number;
  pool_apy_withFee?: number; // SHOW THIS (pool_apy × 0.9)
  pool_tvl?: number;
  liquidity?: number;
  decimals?: number;
}
```

## 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`](/docs/sdk/technical-reference#strategies)
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:

| Number | Formula | Use it for |
| :- | :- | :- |
| **Total balance** | `portfolioByAssetType` + in-flight | "You have X" (the user still owns the in-flight funds) |
| **Requestable** | `portfolioByAssetType` only | Any withdraw form, max button, or amount validation |

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](/docs/sdk/api/withdraw-funds#delayed-withdrawals-yieldmaxxing-strategy).

```text theme={null}
total       = portfolioByAssetType[assetType].balance
            + sum(pendingAsyncWithdrawals where status is REQUESTED or CLAIMABLE)

requestable = portfolioByAssetType[assetType].balance
```

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 `{}`.

```typescript theme={null}
const { portfolio } = await sdk.getPortfolio("0xUser...");

const IN_FLIGHT = ["REQUESTED", "CLAIMABLE"];

const balancesFor = (assetType: string, tokenSymbol: string) => {
  // Everything a withdrawal can still act on.
  const requestable = BigInt(
    portfolio.portfolioByAssetType?.[assetType]?.balance ?? "0x0"
  );

  // Owned, but locked in a redemption until the protocol releases it.
  const inFlight = (portfolio.pendingAsyncWithdrawals ?? [])
    .filter((w) => IN_FLIGHT.includes(w.status) && w.token?.symbol === tokenSymbol)
    .reduce((sum, w) => sum + BigInt(w.amount), 0n);

  return { requestable, inFlight, total: requestable + inFlight };
};
```

<Warning title="Requestable does not mean instant">
  `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.
</Warning>

To tell apart the part of `requestable` that settles immediately, cross-check
each entry of `positions` against
[getAsyncOpportunities](/docs/sdk/api/get-async-opportunities): 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.

<Warning title="Two ways to get this wrong">
  **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`.
</Warning>

### 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`.

```typescript theme={null}
portfolio.pendingAsyncWithdrawals
  ?.filter((w) => w.status === "REQUESTED" || w.status === "CLAIMABLE")
  .forEach((w) => {
    console.log(
      `${formatUnits(BigInt(w.amount), w.token?.decimals ?? 18)} ${w.token?.symbol}`,
      `from ${w.protocol?.name}: claimable around ${w.estimatedClaimAt}`
    );
    if (w.statusMessage) console.log(w.statusMessage);
  });
```

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

```text theme={null}
pendingFee(token[, chain]) = current_earnings × 0.1
balanceWithFee             = liveBalance − pendingFee
```

* 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

```typescript theme={null}
import { formatUnits } from "viem";

const { portfolio } = await sdk.getPortfolio("0xUser...");

// Check session key status
console.log("Has active session key:", portfolio.hasActiveSessionKey);

// User-facing balances by asset type (prefer balanceWithFee)
if (portfolio.portfolioByAssetType) {
  Object.entries(portfolio.portfolioByAssetType).forEach(([asset, data]) => {
    const hex = data.balanceWithFee ?? data.balance;
    console.log(`${asset}: ${formatUnits(BigInt(hex), data.decimals)}`);
  });
}

// Per-chain (same fields)
const usdcOnBase = portfolio.portfolioByChain?.["8453"]?.usdc?.balanceWithFee;

// Positions: prefer underlyingAmountWithFee
portfolio.positions?.forEach((p) => {
  console.log(
    `${p.protocol_name} (${p.chain}): ${p.underlyingAmountWithFee ?? p.underlyingAmount} ${p.token_symbol} @ ${p.pool_apy_withFee ?? p.pool_apy}% APY`
  );
});
```

## 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](#computing-the-total-balance)
* Decode hex / wei amounts with the correct `decimals` (USDC/EURC = 6, WETH = 18, …)
* For earnings net of fee, see [getOnchainEarnings](/docs/sdk/api/get-onchain-earnings)
* For legacy position data, see [getPositions](/docs/sdk/api/get-positions)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.