Skip to content
← All recipes

Nightly Reconciliation

≈ 1 day

A partner that moves client money needs an independent nightly check that its internal ledger agrees with the platform. The reconciliation report gives you a dated snapshot of every balance and position; tax lots and realized gains feed CGT statements from the same run.

The flow

StepEndpoint / EventWhat it does
1GET /report/reconciliation?asOf=YYYY-MM-DDDated snapshot: accounts, cash ledger, securities, unsettled trades, fees, dividends, corporate actions, custody positions.
2Compare cashSum your internal per-user balances and match against the cash section (master + every sub-account).
3Compare positionsMatch units per symbol per sub-account against the securities section; pooled custody vs the sum of holdings is in the custody section.
4Explain the gapsThe unsettled section is the legitimate difference between wallet cash and bank cash until T+N — everything else is an exception.
5GET /users/{id}/tax-lots · /gainsFIFO open lots and realized disposals for capital-gains statements, from the same nightly run.
6?format=csv&section=cash|securities|…Pull any section as CSV straight into your warehouse or finance tooling.
7GET /client-activity · GET /auditAPI-level activity trail to investigate any exception you flagged.

Implementation

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

async function nightlyRecon(asOf /* '2026-07-09' — yesterday, UTC */) {
  const report = await fetch(
    `${BASE}/report/reconciliation?asOf=${asOf}`, { headers: h }
  ).then(r => r.json());

  const exceptions = [];

  // 2. Cash: our ledger vs platform ledger, per sub-account
  for (const row of report.cash.entries) {
    const internal = await myLedger.balanceUsd(row.subAccountId, asOf);
    if (Math.abs(internal - row.balanceUsd) > 0.01) {
      exceptions.push({ kind: 'CASH', subAccountId: row.subAccountId, internal, platform: row.balanceUsd });
    }
  }

  // 3. Positions: units per symbol per sub-account
  for (const pos of report.securities.positions) {
    const internal = await myLedger.units(pos.subAccountId, pos.symbol, asOf);
    if (Math.abs(internal - pos.units) > 1e-6) {
      exceptions.push({ kind: 'POSITION', ...pos, internal });
    }
  }

  // 4. Unsettled trades explain cash-vs-bank timing — record, don't alert.
  const unsettledUsd = report.summary.unsettledSellAmount + report.summary.unsettledBuyAmount;

  if (exceptions.length) await alertFinance(asOf, exceptions);
  await warehouse.store('mystocks_recon', { asOf, summary: report.summary, exceptions, unsettledUsd });
}

Common mistakes

  • Reconciling against live endpoints instead of the asOf snapshot — balances move while you iterate; the report is internally consistent for its date.
  • Alerting on unsettled amounts. Unsettled SELL proceeds are a timing difference by design (T+N), not a break — record them, expect them to clear by settlement date.
  • Comparing floats for equality. Use a cent threshold for cash and a dust threshold (1e-6 units) for fractional positions.
  • Running at local midnight. Use UTC dates consistently — asOf is a UTC business date.
  • Only reconciling cash. Position breaks (missed corporate action, unapplied fill) are rarer but far more expensive to unwind late.

Prove it in sandbox first

  • 1.The full reconciliation report works in sandbox — trade a few times, then pull ?asOf=today and check the trades appear in securities + fees sections.
  • 2.Pull ?format=csv&section=cash and load it into a spreadsheet to design your matching logic before writing code.