# Portfolio tracker

Read balances and equity history, scope requests to subaccounts, and stream live balance updates for an authenticated user.

This tutorial builds a read-only portfolio view for a signed-in Polyester user: current balances, equity over time, and live balance updates. It also shows how account scoping works and how a server hands its work off to the browser so the client starts warm.

Plan on about fifteen minutes. It assumes you already have an authenticated client. If you do not, start with [API Keys](https://testnet.polyester.com/docs/sdk/typescript/guides/authentication/api-keys).

1. Authenticate

   Any authenticated client works. A bot or backend uses an API key; a web app uses a wallet-backed browser session. This example uses an API key so it can run as a script.

   ```ts
   import { PolyesterClient, POLYESTER_DEVNET_ENVIRONMENT, evmHexToBytes } from "@polyester/sdk";

   const client = new PolyesterClient({
   	environment: POLYESTER_DEVNET_ENVIRONMENT,
   	auth: {
   		kind: "api-key-ed25519",
   		getKeyId: () => process.env.POLYESTER_API_KEY_ID ?? null,
   		getSecretKey: () => evmHexToBytes(process.env.POLYESTER_API_SECRET_HEX ?? "0x"),
   	},
   });

   const me = await client.auth.me();
   console.log("tracking portfolio for", me.username);
   ```

   A key with no policy can call `auth.me` and then fail on `balances.list` with `PermissionError: API key policy is required for ledger reads.` Attach a policy that includes `read-balances` (see [API Keys](https://testnet.polyester.com/docs/sdk/typescript/guides/authentication/api-keys)).

2. Read current balances

   `balances.list` returns one row per asset, with trading, funding, reserved, and available amounts as decimal strings.

   ```ts
   const balances = await client.balances.list();
   for (const b of balances) {
   	console.log(b); // per-asset balances as exact decimal strings
   }
   ```

   Because balances are decimal strings, feed them straight into a decimal library for totals rather than parsing to `number`.

3. Scope to a subaccount

   Almost every authenticated read accepts an optional `account` field. Omit it to use the active account, or target a specific one.

   ```ts
   await client.balances.list(); // active account (main, unless a resolver says otherwise)
   await client.balances.list({ account: "main" }); // force the main account
   await client.balances.list({ account: { subaccountId } }); // a specific subaccount
   ```

   List the user's subaccounts to build a switcher:

   ```ts
   import { PermissionError } from "@polyester/sdk";

   const { subaccounts } = await client.subaccounts.list();
   for (const sub of subaccounts) {
   	try {
   		const subBalances = await client.balances.list({ account: { subaccountId: sub.id } });
   		console.log(sub.label, subBalances.length, "assets");
   	} catch (err) {
   		if (err instanceof PermissionError) {
   			console.log(sub.label, "skipped (this key cannot read subaccounts)");
   			continue;
   		}
   		throw err;
   	}
   }
   ```

   A root-scoped API key can list subaccounts but cannot read their balances. Use a browser session, or a key created on that subaccount.

   > **Authorization stays server-side**
   >
   > The `account` field expresses intent. The backend independently verifies that the authenticated caller may access that account, so a read never leaks another user's data.

4. Chart equity over time

   Equity and balance history come back columnar (parallel arrays), which is what charting libraries want. Both take a fixed range rather than custom timestamps.

   ```ts
   const equity = await client.balances.getEquityHistory({
   	range: "7d",
   	groupBy: "asset",
   });

   const balanceHistory = await client.balances.getBalanceHistory({
   	range: "7d",
   	accountCodes: ["trading", "funding"],
   });
   ```

   Use one of `"1d"`, `"7d"`, `"30d"`, `"90d"`, `"180d"`, or `"365d"`. The responses report the selected `startTsSec` and `endTsSec`.

5. Stream live balance updates

   Subscribe to keep the view current. Every balance change arrives as a parsed row.

   ```ts
   const unsubscribe = client.balances.subscribe({
   	accountId: me.accountId,
   	onEvent: (balance) => updateRow(balance),
   	onError: (ctx) => console.error("balance stream error", ctx.error),
   });

   // later
   unsubscribe();
   ```

6. Hydrate the browser from the server (web apps)

   In a web app you can render the portfolio without a flash of loading state by handing two things from the server to the browser: the auth display state and the catalog snapshot.

   ```ts
   // server: build a per-request client from cookies, then bake the catalog into the page
   import { createPolyesterServerClientFromRequest } from "@polyester/sdk";

   const serverClient = createPolyesterServerClientFromRequest({ environment, request });
   await serverClient.catalog.ensureReady();
   const snapshot = serverClient.catalog.snapshot();

   // browser: start the client warm, then restore the session
   const client = new PolyesterBrowserClient({ environment, catalogSnapshot: snapshot });
   client.auth.hydrateAuthState({ mainAccountId, username, activeAccountId });
   await client.auth.restoreSession();
   ```

   See [Server-side usage](https://testnet.polyester.com/docs/sdk/typescript/guides/server-side) for the full handoff.

## Where to go next

- [Accounts & balances guide](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances) for scoping, subaccounts, and policies.
- [Balances reference](https://testnet.polyester.com/docs/sdk/typescript/reference/balances) for every field and result shape.
- [Transfers](https://testnet.polyester.com/docs/sdk/typescript/reference/transfers) to show where each balance change came from.
