Skip to content
← All recipes

Build a Portfolio Tracker

≈ half a day

Show your users a live view of their holdings, P&L, and dividend history. Useful for wealth-management apps, neobanks with an investments tab, and B2B reporting dashboards.

The flow

StepEndpoint / EventWhat it does
1GET /users/{id}/portfolioLive holdings: units, avg cost, current USD price, unrealized P&L.
2GET /market/quotes?symbols=A,B,CRefresh prices for all held symbols in one batch request (max 50).
3GET /stocks/{symbol}/chartPre-shaped price-history chart data per holding (1W, 1M, 3M, 1Y).
4GET /users/{id}/dividendsDividend income history with per-share amounts and pay dates.
5GET /dividends/calendarUpcoming declarations for stocks the user holds (partnerEligible).
6GET /report/aumPartner-level aggregate AUM across all sub-accounts (CSV export available).
7GET /report/positionsOpen positions by symbol across all sub-accounts.
8Webhook: dividend.paidNotify users the moment a dividend is credited to their wallet.

Implementation

javascript
const BASE = 'https://mystocks.africa/api/v1/partner';
const h    = { 'x-api-key': process.env.MYSTOCKS_API_KEY };

async function loadPortfolioScreen(subAccountId) {
  // 1. Holdings + summary
  const { holdings, summary } = await fetch(
    `${BASE}/users/${subAccountId}/portfolio`, { headers: h }
  ).then(r => r.json());

  // 2. Refresh prices for all held symbols in one call (chunk past 50)
  const symbols = holdings.map(x => x.symbol).join(',');
  const { data: quotes } = await fetch(
    `${BASE}/market/quotes?symbols=${symbols}`, { headers: h }
  ).then(r => r.json());
  const priceMap = Object.fromEntries(quotes.map(q => [q.symbol, q.usdPrice]));

  // 3. Enrich holdings with live prices
  const enriched = holdings.map(holding => ({
    ...holding,
    livePrice: priceMap[holding.symbol] ?? holding.currentUsdPrice,
    liveValue: (priceMap[holding.symbol] ?? holding.currentUsdPrice) * holding.units,
  }));

  // 4–5. Dividend history + upcoming
  const { dividends } = await fetch(`${BASE}/users/${subAccountId}/dividends?limit=10`, { headers: h }).then(r => r.json());
  const { dividends: upcoming } = await fetch(`${BASE}/dividends/calendar`, { headers: h }).then(r => r.json());

  return { summary, holdings: enriched, recentDividends: dividends,
           upcomingDividends: upcoming.filter(d => d.partnerEligible) };
}

// 6. Export AUM report to CSV
const csv = await fetch(`${BASE}/report/aum?format=csv`, { headers: h }).then(r => r.text());

Common mistakes

  • Polling /market/quotes per holding instead of batching — one request covers 50 symbols.
  • Ignoring priceIsLive on portfolio rows: refresh stale prices with /market/quotes for the latest tick.
  • Rendering every calendar entry — filter on partnerEligible: true to skip stocks none of your users hold.
  • Rebuilding AUM by summing portfolios client-side when /report/aum (with CSV export) already aggregates it.

Prove it in sandbox first

  • 1.Seed a sandbox sub-account with 2–3 instant-fill trades, then build the whole screen against sandbox data.
  • 2.Verify your chunking logic by requesting 51+ symbols and handling the BATCH_LIMIT_EXCEEDED error.