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.mdin 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.mdbefore the first pull. - Mail: Gmail (
jknash@gmail.com) through the Gmail skill, and Justin's personal Outlook mailbox through theoutlook-mailCLI. 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 ascustom.cloudflare) drives deploys through the cloudflare skill. Never ask Justin to paste a key. - Local state: the workbench checkout at
~/workspace/dashboard(registry atscripts/bill_registry.json, generator atscripts/build_finance_snapshot.py, published snapshot atsite/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/dashboardcommits forsite/finance/data/finance.jsonshow 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
bodykey. - 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_exceptionsentry (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 Cardx1-card-manual): flagged"manual": trueand"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:
- What was checked (accounts, window, pull time) and the dashboard state shipped (commit and deploy, or "no change — snapshot identical, nothing to commit").
- What posted since the last run: bills paid, amounts, dates.
- What is due or pending, including anything expected but not yet visible, with the pull time.
- 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.