← Hub
← Back to Feature Versions

Safe to Spend — Version History

Markdown companion: docs/FEATURE_VERSIONS_SAFE_TO_SPEND.md

Twenty-one documented iterations from a single hero number to a behaviorally-tuned runway with savings deliberately set aside, drill-downs on every aggregate, and per-tab projections. Each version shipped on real user data, each change was earned.

→ Safe to Spend math explorer — click-to-expand component breakdown of the whole formula → Typical Spending Forecast spec — day-by-day worked example

Foundation Evolve Rework Correction Simplify Current Reverted

Thematic arc

Three big shifts across five weeks of iteration:

v5.7
2026-10-04
CURRENT
A confirmed paycheck re-detected as a second income stream no longer counts twice — found by the round-2 evals

What changed (code)

  • The bug: the paycheck is three user-confirmed semimonthly slices, but the detector also kept an unconfirmed “biweekly” stream for one of the same deposits, so the ledger counted that money twice and This Month (and the pace behind every window) was inflated by about a quarter.
  • Fix: pure dropShadowIncome() — once paycheck slices are confirmed on an account, an unconfirmed income stream on the same account within 5% of a confirmed slice is the same deposit and is dropped, in the ledger and in the Coach’s payday tool.

Trigger

  • Round-2 grading: two independent graders flagged the same paycheck counted twice; a read-only data check confirmed it.
v5.6
2026-10-04
CORRECTION
Payday no longer collapses the runway to one day — found by the real-data evals (finding #3)

What changed (code)

  • The bug: the horizon was [today, max(payday−1, month end)] and on payday the runway was “just today”. On 30 Sep 2026 (payday and month end) the pace became “cash + today’s paycheck ÷ 1 day”, and This Week multiplied it by 5 days the engine never looked at — a weekly figure larger than all cash plus all income.
  • Fix 1 — the horizon covers every window it prices: globalIdx = max(paydayIdx, monthEndIdx, weekEndIdx).
  • Fix 2 — on payday the runway rolls to the FOLLOWING paycheck (pure runwayPayday(); new runwayPayday field). Home: “payday today 🎉 · safe until next payday (Thu Oct 15)”.
  • Owner decision 5f — “this pay cycle”: hold back what is due before the next paycheck; what is due after it is that paycheck’s job. Rejected: “cover each window only” (ignores mid-month card dues) and “this cycle + the next” (over-conservative).

Trigger

  • The round-1 real-data eval run: 6 failing conversations (failure type 1) quoted the impossible weekly figure; after the 5a prompt fix the Coach even defended it.

Lessons

  • An index that is “usually” larger isn’t a horizon. Make the invariant explicit (every priced window ⊆ horizon) and test the degenerate day.
  • A prompt can’t fix a confidently wrong engine — the prompt sanity rule was ignored in 5 of 5 cases.
v5.5
2026-06-13
REWORK
Every tab is the sustainable daily pace × the days in its window, reaching across paychecks

What changed (code)

  • The global floor no longer caps the tabs: the pace x = min L(d)/(d+1) already clears every dip. This Month legitimately exceeds cash when it crosses a paycheck; the floor survives as the dip footnote and shortfall amount.
v5.4
2026-06-12
EVOLVE
Spending habits become a Money Out term — the hero is what’s free even if your habits repeat

What changed (code)

  • The typical-discretionary forecast (“if your habits repeat”) is PROMOTED from context-only projection to a subtracted Money Out bucket: moneyOut.habits, spread per-weekday across every ledger day exactly like essentials. New “Spending habits” SubRowCard tagged “estimated”; the projection line + methodology sheet are retired — the hero already IS the habits-repeat number.
  • Scope flip 1 — checking/debit-funded only: reverses the v4.7 credit-card broadening AND tightens to the checking subtype like essentials — a card swipe never leaves checking before payday (the ccDue ladder carries the statement), and set-aside depository subtypes (savings/prepaid/PayPal) never draw on the runway either. The subtype gate was a pre-ship adversarial-review finding: the first cut stopped at account_type = 'depository'.
  • Scope flip 2 — essentials-whitelist exclusion (bug fix): the old sample included groceries/gas, so the retired projection line subtracted essentials AGAIN after the runway already had. The habits sample now excludes ESSENTIALS_PFC_PREFIXES (NULL categories kept — real purchases), making essentials + habits MECE by construction. Methodology: habits_excl_essentials_robust_mean_dow_p90.
  • Cold start: <60d history → term omitted (typicalDiscretionary = null, row hidden, “doesn’t include your spending habits yet” footnote; the still-learning footnote now covers whichever estimated terms are inactive). Coach tool description + system prompt flipped; projection_accuracy eval ground truths flipped both directions.
  • Pre-ship adversarial review (5 dimensions → refute-verify, 29 confirmed findings) also caught: the untilPayday metadata window included payday itself while the subtracted term stops at payday−1 (fixed — metadata windows now share the tab’s exact bounds); stale pre-habits floor.values in three rewritten eval mocks; doc drift across six spec surfaces. Locked conventions (SAFE_TO_SPEND_MATH.md): the day-60 cliff and the full-day-today charge.

User trigger

  • Vik’s product call (2026-06-12): “if your habits repeat should come under money out… and this should be accounted for the safe2spend calculation” — the headline should be what’s truly free assuming usual behavior, not a budget ceiling with a side projection.

Lessons

  • Promoting a CONTEXT metric into MONEY MATH is never just “subtract it.” The account scope (net-position vs single-account runway) and the MECE boundary against existing formula terms both have to be re-derived, or the promotion imports double-counts that were harmless as context.

Timeline gap: v5.3 (2026-06-12, PRs #207–#211) was the paycheck/detection hardening wave — multi-account direct-deposit splits, semimonthly two-anchor expansion, confirmed-stream ownership contract, 30-day sync lookback, poison-row hardening. Detailed entry pending.

v5.2
2026-06-12
EVOLVE
Depository stream scoping — detection stays account-agnostic, the money math becomes account-scoped

What changed (code)

  • New exported pure partitionStreams() in safe_to_spend_service.ts (DB-free unit-testable, the deriveLedgerTabs precedent): outflow, income, and cc_payment streams enter the ledger ONLY when sourced from a depository account, or legacy rows with no account linkage — dropping an unlinked paycheck would be a worse trust failure than the bugs this fixes.
  • Bug 1 — CC-sub double-count: a subscription billed to a credit card (claude.ai on the card) was also subtracted from checking, while it already lived inside the card’s ccDue statement repayment — a double charge. Card-billed subscriptions/bills are now display-only (Recurring Expenses credit view).
  • Bug 2 — card-credit income leak: the card’s “Automatic Payment - Thank You” credit mirror was classified as income and entered Expected income at p25 — the user-visible $260.51. Credit-side payment-received mirrors are never income; that credit is money LEAVING checking, not arriving.
  • Excluded streams go to excludedNonDepository and deliberately do NOT feed possibleBills — a card-billed subscription must never resurface as a checking “possible bill.”

User triggers

  • Expected income showed a $260.51 “paycheck” that was actually the card’s own autopay confirmation.
  • A card-billed subscription was charged against the checking runway twice — once directly, once inside the card payment.

Lessons

  • Detection can be account-agnostic; money math must be account-scoped. A detector that fans out into multiple consumers (STS ledger, Recurring Expenses, Coach) needs an explicit eligibility contract per consumer, or the consumers inherit each other’s bugs silently.

Timeline gap: v5.0–v5.1 (2026-06-10 → 06-12, PRs #188–#193) were the daily cash-flow ledger rework — MECE Money Out, the Plaid Liabilities CC due ladder, payday/income v2, the global-floor invariant, and the ledger UI. Full current-state spec: the math explorer / SAFE_TO_SPEND_MATH.md (includes the v4.9 → v5 deltas table). Detailed timeline entries for v5.0/v5.1 are pending.

v4.9
2026-04-25
EVOLVE
Savings exclusion as a behavioral choice + open-question dump from on-device review

What changed (code)

  • Spendable balance filter changed from account_type = 'depository' to account_type = 'depository' AND account_subtype = 'checking'. Savings, money-market, HSA, and CD balances no longer count toward Safe to Spend.
  • New setAsideAccounts[] field on the API response — savings/HSA/MM/CD accounts surfaced separately for transparency (mirror of the existing loansExcluded[] pattern).
  • New “Set aside in savings” row in the SafeToSpend accordion (only renders when the user has any setAside accounts) — drill-down shows per-account balance with a subtitle of "savings, HSA, money market, etc."

Why this is the right call (deliberate behavioral choice)

Vik: "Yes, I think we need to help people save more. I would like the safe to spend component to reflect that in the math and UX. Yes, it's a deliberate choice, not a hacky label choice."

Including savings in “safe to spend” would silently encourage users to dip into their savings the moment runway feels tight. Excluding it surfaces saving as separate, protected money — the “set aside” framing reinforces that it’s not for spending. Same psychological pattern Apple, Wealthfront, and behavioral finance research consistently endorse.

What got documented (no code change)

Long thread of on-device review with Vik produced a list of follow-on questions, simplifications, and planned features. All captured in the math explorer’s expanded "Open Questions" section:

  • Plaid Liabilities-based CC due-date splitting — split the CC line into "Due [date]" + "Accumulating for next cycle" using statement balance + due date from Plaid Liabilities (we have production access). Same total subtracted, much better clarity.
  • Apple Card-style payment simulator — slider/wheel UX showing 25%/50%/75%/100% payment scenarios with interest impact.
  • Plaid Signal for live spend tracking — close the post-swipe latency gap (today: pending charges in minutes-to-hours, posted in 1-3 days).
  • Detector coverage gap — recurring stuff with thin history gets misclassified. User 53’s BILT rent ($4,200/mo) is the canonical example: 2 occurrences in 102 days, not detected as recurring → lands in typical-discretionary → inflates Thursday average from ~$15 to $666. Fix: parallel "Confirm a recurring bill" UX.
  • Lookback bump — 365 → 730 days when users approach 1 year of history.

What also got fixed in docs (no code, but real)

  • Typical-spending spec: clarified the misleading "zero-spend days are NOT in this map" wording (they ARE included as zeros in the per-weekday samples; just not in the intermediate Map).
  • Typical-spending spec: noted that excludeCreditCardSideOfCcPayment is defense-in-depth, not a primary inclusion path — CC payments are already excluded by other filters.
  • Math explorer: Spendable Balance card updated to reflect checking-only filter and savings exclusion rationale.
  • Typical-spending spec: new "Why is your Thursday so high?" section walking through the BILT story.

Lessons

  • Filter mismatches are silent until they aren’t. The label said "Checking" for weeks; the filter included savings. We just got lucky that user 53 had no savings accounts.
  • Behavioral defaults > "be neutral." Showing savings as part of safe-to-spend looks neutral but quietly nudges users toward worse outcomes. Excluding it is opinionated, but the right opinion.
  • Detector coverage gap is the single biggest distortion in typical-discretionary today. Real recurring stuff misclassified as discretionary inflates averages by orders of magnitude. Confirm-a-recurring-bill is the unblocking feature.
v4.8
2026-04-23
EVOLVE
Scroll gesture fix + per-window-day cards + “you’d likely have” copy

What changed

  • Horizontal scroll fixed. v4.7 card strips wouldn’t swipe — the outer <Pressable onPress={toggleExpanded}> that wrapped the entire SafeToSpend card was swallowing ScrollView gestures. Restructured so only the hero header (amount + subtitle + chevron) lives inside the toggle Pressable; the drill-down section lives outside it.
  • Per-window-day cards. The Typical Spending drill-down now shows ONE CARD PER DAY in the selected tab’s window (not 7 weekday summaries). For Until Payday’s 9-day window, you see 9 cards: Thu Apr 23, Fri Apr 24, Sat Apr 25, Sun Apr 26, Mon Apr 27, Tue Apr 28, Wed Apr 29, Thu Apr 30, Fri May 1. Each card shows weekday name + date + that day’s typical spend. Users can now see exactly WHY the sum is what it is — in user 53’s case, Thursday and Friday appear twice in the Until Payday window, which is what drives the total past This Month.
  • Projection total label reworded. "If habits repeat, at payday" → "You’d likely have $X left at payday (May 1)". Names the outcome (remaining balance) instead of forcing the user to infer it. Per-tab variants: "by tonight", "by Sun Apr 27", "by Apr 30", "at payday (May 1)".

User triggers

  • “I’m not able to scroll through any of those square boxes to the left or right.”
  • “I don’t know how typical spending for Until Payday for 9 days is $2,658, whereas Today it’s $666 something.”
  • “If habits repeat by payday ... like you have another certain amount, like is that what’s remaining? I think we need to make that a little bit more clear.”

Lessons

  • Don’t wrap a scroll container inside a tap-Pressable. React Native’s gesture system gives the outer Pressable first dibs on touch; horizontal scroll gestures get interpreted as taps.
  • Show the addends, not the group’s averages — when a sum spans multiple instances of the same group (two Thursdays in one window), per-group summaries hide the explanation.
  • Projection copy should name the outcome, not the hypothesis. "If habits repeat" makes the user do the inference work; "You’d likely have $X left" just tells them.
v4.7
2026-04-23
EVOLVE
Horizontal-scroll drill-downs + CC swipes in the forecast + per-tab projections

What changed

  • Aggregated rows (Checking / Bills / CC balances / Typical spending) now carry a chevron affordance and reveal a horizontal-scrolling strip of compact cards when tapped. Previous inline-list drill-down felt visually weak and the tappability wasn't obvious.
  • typicalDiscretionary now spans both checking and credit card outflows. A future $50 Uber on CC is still a real outflow — excluding CC swipes was conservative but dishonest. User 53's sample size jumped 16 → 160 transactions (10×) just by lifting the depository-only filter.
  • Projection block appears on every tab (Today / This Week / This Month / Until Payday), not just Until Payday. Backend returns a typicalDiscretionary.windows map; each tab reads its own window's forecastAmount. perDayOfWeek is user-level (computed once); per-window projections are just different day-range sums of the same weekday averages.
  • Tab-aware labels: “If habits repeat, by tonight / by week end / by month end / at payday.”
  • Methodology modal gains a projections-per-window table showing all four scenarios side-by-side.
  • Deleted ExpandableBreakdownRow + ChildLine; replaced by the new DrillDownRow component.

User triggers

  • “It's hard to know that it can be clickable. The breakdown of the points of rows for what's in Credit Card Balance and what is Bills before payday is not great. Maybe we make this a small square box that can be scrollable to the left and right.”
  • “Shouldn't you include credit card transactions that are not included in the balance as of today? Things that you would predict as expenses in credit card until next payday, similar to how you have calculated for checking?”
  • “Shouldn't we bring this thinking into all of the tabs? Why just until payday?”

Lesson

Three tightly-related lessons in one release. (1) Conservative scope choices read as hedging. Depository-only was “safe” but invisibly cut sample size 10× and pretended CC spending wasn't real. Own the scope or explain why you scoped down. (2) When the same math conceptually applies across UI slices (tabs), compute it once at the user level and project per-slice — don't force per-slice repeated compute or per-slice feature gaps. (3) Tappable = chevron. Typography-only affordance (underline) was too subtle; iOS users look for the right-chevron.

v4.6
2026-04-23
EVOLVE
Drill-downs on every aggregated row + typical discretionary spending as CONTEXT

What changed

  • Aggregated accordion rows are now tap-to-expand: Checking → per-account balances, Bills before payday → per-merchant list with dates, Credit card balances → per-card balances. Single-row-open accordion (tapping a new row closes the previous) caps card height.
  • New "Projection at payday" block on the Until Payday tab, placed AFTER the per-tab math (NOT inserted between Runway and Daily Budget, which would break the arithmetic identity). Shows typical non-recurring spending and = if habits repeat, at payday.
  • New transactions.recurring_stream_id FK, populated by the recurrence detector. Lets downstream services filter recurring vs discretionary via WHERE recurring_stream_id IS NULL — no more error-prone normalized-name lookups.
  • New typical_discretionary_service.ts: up to 365 days of history, robust-mean-per-weekday capped at the 90th percentile, excludes non-discretionary primaries (INCOME, TRANSFER_IN/OUT, LOAN_PAYMENTS, BANK_FEES) and the CC-account side of CC bill payments. Adaptive lookback: min(365, daysOfHistoryAvailable), null when <60 days.
  • Methodology modal (tap the "How?" link on the projection header): sample size, observed-days coverage percentage, per-weekday bar chart, window dates. Honest about scope: day-of-week seasonality only; month-of-year needs multi-year data.
  • AI Coach's calculate_safe_to_spend tool now surfaces typicalDiscretionary so the model can answer "what if I go out this weekend?" with a grounded baseline.

User trigger

Vik noticed two gaps. (1) "If I want to drill down into what those specifically are, I think I need a way to know that" — Credit card balances $7,021 and Bills before payday $14 were opaque; the data was on the response but not surfaced. (2) "What if you are buying things on your debit card that's not recurring? Do you think we have actually taken into account that at all?" — future impulse spending isn't in the runway formula. Industry norm says don't try to predict it; Vik said close the gap as honest context, using "at least one year of data to predict seasonality."

What the formula captures now

Category Captured? How
Past debit / checking spending (cleared)✓spendableBalance reads availableBalance from Plaid
Past debit / checking spending (pending)✓Plaid availableBalance subtracts pending debits automatically
Past credit-card spending✓creditCardDebt = sum of current CC balances
Future recurring bills (rent, Netflix, gym)✓upcomingBills from recurring_streams (conf ≥ 0.3)
Future recurring income (paycheck)✓expectedIncome from income streams (conf ≥ 0.75)
Future non-recurring discretionaryContext (v4.6)typicalDiscretionary forecast, shown as “Projection at payday” — NOT subtracted from runway

Lesson

When one threshold is doing two jobs, split it. v4.5 split confidence into anchor-reliability vs amount-reliability. v4.6 splits recurrence-match into "exclude from discretionary forecast" (FK-based, precise) vs "stream-level membership" (name-based, lenient). Both times the split produced a better answer. Also: typical spend stays as CONTEXT, not a formula input — runway is the user's contract with the number; don't bury a prediction inside it.

v4.5
2026-04-23
EVOLVE
Confirm your paycheck + semi-monthly variant + split confidence bars

What changed

  • “Confirm your paycheck” Settings row — user taps a specific income transaction, sets user_confirmed=true on its recurring_streams row
  • Split the confidence bar by decision: user_confirmed bypasses the ≥0.6 ANCHOR bar (dates trusted), but NOT the ≥0.75 AMOUNT bar (one observation isn’t a forecast)
  • Semi-monthly variant picker: “1st & 15th” vs “15th & Last day” — reuses existing payday_day_of_month column (1 or 15)
  • Normalizer extended to strip ACH metadata tags and mixed-alphanumeric transaction codes; 4-word cap on merchant keys
  • Bills deactivate-pass excludes user-confirmed streams so detect-and-replace cycle doesn’t wipe them
  • New endpoints: GET /paycheck-confirmations/candidates + POST /paycheck-confirmations

User trigger

Real-data reveal: the single confidence_score was gating TWO independent questions — “are the DATES reliable?” and “are the AMOUNTS reliable?” — and blended-low blocked the reliable-dates half. User: recent job change (1 paycheck, partial period) + paternity leave ending ($1,278–$3,197 variance). Both paths failed → 14-day fallback → negative runway on every tab. “Resolve it for me like I’m any other customer.”

Lesson

When auto-detection is uncertain, let the user tap ground truth. Recognition tasks (pick from a list) reliably beat recall tasks (remember a date). Confidence scores are a signal, not a verdict — one user tap beats six noisy observations.

Before / after on real user data

Before v4.5 (user 53)

source = inferred (fallback)
nextPayday = May 7 (14-day default)
bills = $11,821.83 (14-day window, mortgage included)
runway = −$5,011

After v4.5 (user 53)

source = user_setting
nextPayday = May 1 (1st & 15th variant)
bills = $13.85 (just LA Fitness + Amazon Prime)
runway = +$6,796

v4.4
2026-04-23
SIMPLIFY
Payday v2: 2 cases, DB single source of truth, date hydration fix

What changed

  • Payday precedence collapsed from 5 cases to 2: user_setting or inferred
  • DB profiles row is the ONLY source of truth — mobile writes via PUT /users/me/profile, no query-param overrides on SafeToSpend
  • Deleted: PaydayDivergenceBanner, isStale, divergenceWarning, legacyPaydaySource, thisMonth alias, legacy_data AI tool block, backfill ace command, “Last Paycheck” Settings row
  • Semi-monthly hardcoded to 15th + last-day of month (standard US contract)
  • Biweekly/weekly anchor derives silently from the highest-confidence income stream’s last_observed_date
  • Fixed Postgres date hydration off-by-one: May 2 was sliding into May 1 windows because DateTime.fromJSDate(d, { zone: tz }) converted UTC midnight to the user’s previous evening. New helper toUserLocalDate() normalizes everywhere

User trigger

“I think we’re complicating this a lot. Do you agree?” — after discovering the Settings UI showed Biweekly while the API quietly received monthly/1 via stale query params.

Lesson

Complexity compounds silently. Every “edge case” branch added pre-PMF is a bug factory. Solve for the W2-salaried regular-paycheck user first; defer multiple income sources, job changes, thin Plaid history, and divergence detection.

Scope of the delete

Net −500+ LoC across backend + mobile. Zero eval fixture churn (distinguished-engineer review confirmed no fixture referenced any dropped field).

v4.3
2026-04-23
EVOLVE
Runway decomposition in the accordion

What changed

  • Expanded accordion gains a new top section on every tab: Checking − Bills before payday − Credit card balances + Income before payday = Runway
  • Rows conditionally rendered (CC debt / income / bills hidden when zero)
  • Values pulled from the until-payday window regardless of which tab is active — same runway drives daily budget everywhere
  • “Also coming up in this window” hidden on Until Payday tab (would duplicate the runway rows)
  • Operators (− + = ÷) live in the label column; value column shows positive magnitudes color-coded by role

User trigger

“I know we’re starting with runway until payday. I think people are going to want to know how that was arrived at.” — Vik, 2026-04-23

Lesson

Transparent derivation goes one layer deeper each iteration. v4 showed the math, v4.2 showed where $/day came from, v4.3 shows where runway came from. If you write = X anywhere in a financial UI, someone eventually asks = how?

v4.2
2026-04-22
CORRECTION
Drop the cap, uniform $/day across all tabs

What changed

  • Removed the min(..., untilPayday.amount) cap from v4.1
  • All 4 tabs now show amount = dailyBudget × daysInWindow with uniform perDayInWindow = dailyBudget
  • When This Month extends past next payday, a footnote explains: “Extends N days past your next payday — this projection assumes the same daily rate continues.”
  • Breakdown UX reworked: every tab now shows how we derived the daily budget (runway ÷ days = $/day), then the per-tab math ($/day × days)
  • Components moved to a secondary “Also coming up in this window” section — context, not arithmetic inputs

Why

v4.1’s cap had a side effect: perDayInWindow = amount / daysInWindow varied across tabs. User feedback: “This Month is taking $777/day for 9 days whereas Until Payday is taking $874/day times 8 days — doesn’t make sense.” Dropping the cap preserved a single daily rate everywhere.

v4.1
2026-04-22
CORRECTION
Per-tab amount = dailyBudget × daysInWindow, capped at runway

What changed

  • Per-tab amount formula: min(dailyBudget × daysInWindow, untilPayday.amount) for Today / This Week / This Month
  • Breakdown components (bills / income in window) preserved as CONTEXT but no longer arithmetically sum to amount

Why

On-device test revealed the v4 formula collapsed amount to spendable − ccDebt whenever no bills/income fell inside the window. Wednesday-to-Sunday “This Week” returned the same number as Until Payday. User’s mental model: “$777/day × 5 days left in week = $3,887.” Fixed by switching to the daily-budget-allocation model.

Lesson

“Mathematically honest” lost to “user-intuitive” on this one. Pay-cycle-daily-budget allocation > window-scoped sum.

v4.0
2026-04-20
REWORK
4 always-visible tabs, ISO week, 0.75 income threshold

What changed

  • All 4 tabs always rendered — no auto-hide based on redundancy (same number across tabs is fine)
  • Week became ISO Mon-Sun in the user’s timezone (not rolling 7 days)
  • Income math-inclusion threshold lowered from 0.85 to 0.75 — scoped ONLY to SafeToSpendService; Smart Pulse / Morning Brief narratives kept the stricter 0.85 bar
  • Per-window math added: daysInWindow + perDayInWindow computed server-side
  • Color legend with “seen” state via SecureStore
  • Breakdown shows total then derivation line: "= $perDayInWindow/day × daysInWindow days"
  • Removed the min(payCycle, dailyBudget×7) cap on This Week

What we learned

“If you want intuitive math, don’t lie about the cap.” Showing the honest per-window sum with a legible derivation beat a capped-but-opaque number. Consistency across tabs — even when redundant — felt more trustworthy than hidden logic.

v3.2 (reverted)
2026-04-19
REVERTED
Removed user-configured payday — and put it right back

What we tried

Attempted to remove user-facing payday configuration entirely and rely only on stream detection. The thinking: “we can tell when you get paid from your bank data — why ask?”

Why it was reverted

For new users with thin Plaid history, stream detection needs weeks of data before it’s reliable. Until then, payday predictions were wrong — and “wrong by design” for new users is a dealbreaker. User control had to stay.

v3.1
2026-04-18
REWORK
Shared service + Rest of Month + payday-today

What changed

  • Extracted safe_to_spend_service.ts — AI Coach and Home now both delegate to it. The v1.3 duplication finally killed.
  • Added a 4th timeframe: Rest of Month (today → endOfMonth)
  • Fixed the payday-today edge case: on your payday morning, daysUntilPayday = 0 correctly, no forward-roll to the next cycle

What we learned

Consolidation > abstraction. One implementation of the math with two callers is simpler than two implementations with a shared interface.

v3.0
2026-04-17
EVOLVE
Until Payday is default + visually primary

What changed

  • Until Payday became the default tab (was This Week)
  • Visual primacy: slightly larger type + accent color on Until Payday

Why

Feedback that the emotionally-relevant question is always “how much can I spend before the next paycheck lands?” — not “how much this calendar week?”. Paycheck cycle is the real rhythm of cash management.

v2.2
2026-04-17
EVOLVE
Loan accounts surfaced as context, not math input

What changed

  • Added loansExcluded[] to the response — mortgage / auto loan shown to the user but NOT subtracted from runway

Why

Plaid doesn’t return loan transactions reliably, and a mortgage isn’t the kind of debt you pay off next month anyway. Surfacing it as context (“here are the big liabilities we’re aware of”) without contaminating the formula was the right line.

v2.1
2026-04-16
CORRECTION
Server-persisted payday settings

What changed

  • Payday frequency + day-of-month moved from client-only state to the profiles row
  • AI Coach read the same DB row so Home and Coach now agreed on when payday was

The seed of a future mistake

Mobile also still sent payday values as query params on every request — two sources of truth that would silently drift until v4.4. This version half-fixed the consistency problem.

v2.0
2026-04-15
REWORK
Credit card debt in the formula

What changed

  • Formula now: spendableBalance − upcomingBills − creditCardDebt + expectedIncome
  • Bills attributed to specific accounts
  • New response field: creditCards[] with per-card balance

Why

Users with rolling CC balances saw “Safe to Spend = $X” when they actually owed $X+ to their cards and were negative in net worth. Subtracting CC debt made the number honest at the cost of looking smaller — a deliberate tradeoff for financial accuracy over optimism.

v1.3
2026-04-14
CORRECTION
AI Coach daily budget matches Home

Why

AI Coach was computing its own daily budget; Home was computing a different one. Users asked the Coach “what can I spend today?” and got a different number than the hero card on Home. Hot-patched the AI Coach to match.

What we learned

Two systems independently implementing the same math will drift. This became the motivation for extracting SafeToSpendService as the shared source of truth in v3.1.

v1.2
2026-04-13
EVOLVE
Payday-aware daily budget

What changed

  • Introduced the concept of dailyBudget = runway ÷ daysUntilPayday
  • “Today” stopped showing full balance and started showing a per-day budget
  • User payday setting (frequency + day-of-month) began to drive the math

Why this was big

The shift from “here’s your runway” to “here’s what you can spend today and have runway left.” Debt-by-day, not windowed sum.

v1.1
2026-04-02
CORRECTION
Rolling windows + date parsing fix

What changed

  • Timeframes clamped to calendar boundaries (endOf('week'), endOf('month'))
  • Date string parsing on the mobile side fixed — dates were off-by-one in some zones

What we learned

First lesson in what would become a recurring theme: timezone and date semantics are the hardest part of a cashflow app. A date is not an instant.

v1
2026-04-01
EVOLVE
Three timeframes + confidence-weighted bills

What changed

  • Added three timeframe tabs: Today, This Week, This Month
  • Bills subtracted only when confidence ≥ some threshold — the formula was born
  • Formula: amount = spendableBalance − upcomingBills + expectedIncome
Kept from v0

The hero-card format — users recognize “a big number with context underneath”.

New problem

Timeframes were rolling windows from the moment of request — “This Week” on a Thursday was Thu→Wed, which matched nobody’s mental model of a week.

v0
2026-03-24
FOUNDATION
Safe-to-Spend hero card on the dashboard

What shipped

  • A single number: current account balance shown as “safe to spend”
  • Static balance, no payday awareness, no bills subtracted
  • Lived inside the broader dashboard-with-real-API-data effort

Why this was the starting point

Minimum viable affordance: give the user one honest number they can glance at. Most cashflow apps stop here. Quickly became clear that “your balance” is not the same as “what you can safely spend” — pending bills and upcoming rent change the answer dramatically.