Skip to main content

Finance daily dashboard update

The complete procedure for the daily refresh of Justin's finance dashboard at dashboard-71j.pages.dev/finance/. It is written to be executed cold: an agent who has never run it — covering for the finance agent (maverick-muse-finance-001), or taking the desk over — can run the whole update from this page alone, plus the two documents it names. The finance role runbook (docs/runbooks/finance.md in jknash/agent-orchestration) carries the mandate; this runbook carries the method.

Cadence: daily, each morning America/Chicago, driven by the job finance-daily-dashboard-update (owned by the budget dashboard goal; reports to the finance desk chat and the main chat). It also runs on demand whenever the chief of staff assigns an update.

Companion documents, fetched fresh before every run:

  • The founding skill, docs/15-skills/finance-receipt-sweep.md in this docsite — mailbox search terms, deduplication rules, the Flex rule, and the manual-bill machinery in full.
  • The finance role runbook (above) — authority, registry discipline, and the escalation path.

The procedure at a glance (the prose below governs):

Before you start​

Access and state a takeover agent needs. Credentials are used through the stored connections and are never handled as values.

  • Bank: the Plaid CLI, with Chase (checking, savings, Disney Visa card) and SoFi (checking, savings) linked. Read /opt/hatch/skills/plaid/SKILL.md before the first pull.
  • Mail: Gmail (jknash@gmail.com) through the Gmail skill, and Justin's personal Outlook mailbox through the outlook-mail CLI. The money mail (Affirm, Apple, Flex, unlinked-card alerts) lives in Outlook; Chase alerts and store receipts land in both.
  • Publishing: the stored GitHub credential (custom.github-docsite) drives the dashboard repo's publish helper; the Cloudflare token (stored as custom.cloudflare) drives deploys through the cloudflare skill. Never ask Justin to paste a key.
  • Local state: the workbench checkout at ~/workspace/dashboard (registry at scripts/bill_registry.json, generator at scripts/build_finance_snapshot.py, published snapshot at site/finance/data/finance.json); domain decisions of record in ~/workspace/goals/budget-dashboard/GOAL.md.
  • Last-run state: the snapshot's own history is the record — the jknash/dashboard commits for site/finance/data/finance.json show when the dashboard last refreshed, and the previous daily report (finance desk chat) shows the last sweep window. The mailbox sweep covers everything since the last run; the founding skill's default lookback is 48 hours, so a missed day widens the window, never skips it.

Procedure​

1. Pull the bank state​

Run the four Plaid reads into a scratch folder for this run (use a durable path, for example ~/workspace/finance-daily-pull/, and keep the files until the run is reported):

plaid accounts
plaid transactions-recurring
plaid liabilities
plaid transactions-get --start-date <120 days before today> --end-date <today>
  • The 120-day transaction window is required: the generator matches bill occurrences and builds history from it. Pending rows are included and stay labeled pending — pending is not posted.
  • If a read is written to an output file instead of stdout, use that file; the payload sits under its body key.
  • Reconcile before proceeding: the checking account's current-minus-available gap must equal the sum of its pending debits. If it does not, a pending item is unexplained — find it now; it belongs in the report.

2. Sweep both mailboxes​

Search Gmail and Outlook for receipts and money alerts since the last run, using the founding skill's search terms (receipt, payment, order, subscription, invoice, charged, renewal, transaction, bill, statement, past due), excluding promotions and social categories. Outlook search is not date-bounded — filter by received date yourself. Then apply the skill's rules:

  • Dedupe: one real-world transaction counts once. The same purchase can leave a receipt in each mailbox plus a pending line in Plaid; match traces on merchant, amount, and date before calling anything new, and label every finding new or already tracked against the registry and the latest snapshot.
  • Card payments: a Chase "payment is scheduled" email states amount and effective date; match it to the bank posting before calling the payment pending.
  • The Zelle to Michele ($2,000, around the 1st, a registered bill): if it is not in the bank data yet, report exactly that, with the pull time — bank data lags same-day sends, and an absent row is not proof a payment failed. Never fabricate a posting.
  • Unlinked accounts: the Apple Card and X1 Card are not Plaid-linked; email is their only signal. Surface their alerts (past-due notices, limit warnings, amounts due) explicitly.
  • Zero-dollar statements (for example Spectrum) are checked, not bills.

3. Apply the registry rules​

Edit scripts/bill_registry.json only where the evidence allows:

  • Flex dates: Flex's emails are authoritative for installment dates. A reschedule email adds a one-off date_exceptions entry (affected month mapped to the new day) and a note naming the source; the standing schedule day never changes for a one-off.
  • Manual bills (Apple Card apple-card-manual, X1 Card x1-card-manual): flagged "manual": true and "source": "email", each a single "once" occurrence. New email evidence (statement, due notice, payment confirmation) replaces the occurrence's date and amount, or records the payment. The generator's matcher never marks them paid; no bank transaction may verify them. Never invent a standing due day, minimum, or recurrence; balances live in the entry's notes as context only.
  • New merchants and charges: classify from the receipt where the evidence allows. Anything unclassifiable is carried in the report as unclassified until it is classified — never dropped.
  • Never in a daily run: new bills, redefined bills, allocation or safe-to-spend changes, buffer changes. Those are stakeholder decisions — escalate to the chief of staff with the evidence.

4. Regenerate the snapshot​

python3 ~/workspace/dashboard/scripts/build_finance_snapshot.py \
--accounts <accounts file> --recurring <recurring file> \
--liabilities <liabilities file> --transactions <transactions file> \
--out ~/workspace/dashboard/site/finance/data/finance.json

Then read the generated snapshot's planning block: note whether planning.unallocated is non-empty and whether forecast.trough.balance is negative (amount and date). Both are report flags in step 7 — they are surfaced, never silently "fixed" by changing allocations.

5. Commit​

Commit the snapshot, plus the registry if step 3 changed it, to jknash/dashboard main with the repo's publish helper:

python3 ~/workspace/dashboard/scripts/gh_file.py \
--path site/finance/data/finance.json \
--file ~/workspace/dashboard/site/finance/data/finance.json \
--message "finance: daily dashboard update"

If the helper refuses on staleness, another change landed first: re-read the remote file, re-run step 4 against the fresh state, and retry once. A second refusal escalates (below) — do not force the publish.

6. Deploy and verify​

cd ~/workspace/skills/cloudflare && .venv/bin/python bin/cf.py deploy

Verify like an outsider, on the production host and on the deployment's preview URL from the deploy output:

  • / returns 200.
  • /finance/ returns the Cloudflare Access 302 (the section is private; a 200 here would mean Access is off, which is itself a finding to escalate).

7. Report​

Post the report to the finance desk chat and the main chat, short and in this order:

  1. What was checked (accounts, window, pull time) and the dashboard state shipped (commit and deploy, or "no change — snapshot identical, nothing to commit").
  2. What posted since the last run: bills paid, amounts, dates.
  3. What is due or pending, including anything expected but not yet visible, with the pull time.
  4. The flags: unallocated bills; a negative forecast trough (amount and date); unclassified charges; unlinked-account alerts; any registry date the run changed, with its source.

A no-change day still reports — one line: dashboard updated, nothing flagged. Silence is a missed run, not a quiet day.

When something fails​

  • Plaid read fails: report the failure and stop. Never ship a snapshot built from a stale or partial pull.
  • One mailbox fails: run the bank side, report which mailbox failed and that the sweep is partial; the next run's window widens to cover the gap.
  • Generator fails: the inputs are the first suspect (a malformed envelope, a wrong window). Fix inputs, not code; a generator defect escalates to the chief of staff — dashboard code changes go through the workbench board, not a daily run.
  • Commit refused twice, or deploy verification fails after one redeploy: escalate to the chief of staff with the step, the error, and what state the dashboard was left in.
  • Any failure is reported the same run, in the report's place — a failed run is never silent, and the next run retries from step 1 with the widened window.

Escalation and never​

Escalate to the chief of staff (maverick-muse-chief_of_staff-001), reporting what was verified, what conflicts, and what was not touched, on: an unreconciled bank read; a charge that cannot be classified from its evidence; a registry change beyond a date exception; any conflict between an email, the bank record, and the registry; or any step above failing twice.

Never: move money or pay a bill (read-only toward the bank, always); fabricate or infer a posting; mark a manual bill paid without email evidence; change an allocation, a buffer, or what counts as a bill; put account numbers, credentials, or full card numbers in any report, commit, or chat; reference divorce case material (the Zelle transfer is a bill line only).


Published by maverick-muse-chief_of_staff-001 · 2026-10-06.