Forecasts

GET /{user_id}/forecasts builds a cashflow forecast from the user’s last 93 days of transactions on every request; nothing is cached. The response schema (Forecasts) is in API Specification.

Request Flow

  1. Read the user’s SAVINGS_MONTH rows for the current and previous two months (missing rows are not an error).

  2. Fetch active items with accounts from the Transactions Service; sum available (else current) balances into checking and savings totals per account. Failure returns 500.

  3. Fetch 93 days of transactions and run Recurring Detection.

  4. Keep only expenses and income on currently active accounts.

  5. Build the response: Cash Runway, Savings Progress, and Cash Calendar, each gated by an API env var (INCLUDE_CASH_RUNWAY, INCLUDE_SAVINGS_PROGRESS, INCLUDE_CASH_CALENDAR, all true), then income, recurring, and ritual (always empty).

Source: pkg/api/forecasts.go, pkg/api/forecasts_builder.go

Recurring Detection

Transactions are grouped per account, then split into debits (expenses) and credits (income) before name grouping.

Step Rule

Group by name

Cleaned transaction name; expense groups are further split by amount variance, income groups are not

Minimum size

More than 2 transactions per group, after merging closely dated transactions (retries) into one

Cadence

Average gap in days between consecutive transactions; every gap must fall within 0.8x to 1.2x the average (0.4x to 1.6x when retries were merged), else the group is not recurring

Frequency and next date

Average gap rounded and mapped to a frequency name (weekly, biweekly, semimonthly, monthly, and so on); next_date is the last transaction plus the average gap

Income check

Credit groups must also pass a Plaid-category income check

Source: pkg/api/fm_forecasts.go

Cash Runway

Starting from today’s checking and savings totals, walk forward one day at a time, subtracting each recurring expense on its due date and rescheduling it by its frequency, until either total reaches zero or two years pass. cash_runway is the walk reduced to 12 evenly spaced steps (date, checking, savings); cash_runway_days is the number of days covered. Income is not added. Source: pkg/api/runway.go

Cash Calendar

cash_calendar lists days with activity: past days in the current month (from the detected groups' transactions) and projected days up to six months out (each expense and income projected forward by its frequency). For projected days, income is added and expenses subtracted from a copy of today’s per-account balances; a day on which an account goes negative is marked is_overdraft with the affected overdraft_accounts. Source: pkg/api/calendar.go

Savings Progress

average_income and average_expenses are monthly totals of the detected upcoming income and expenses (frequency converted to occurrences per month). potential_savings is six monthly steps: one actual step per stored SAVINGS_MONTH row (up to three), each with potential equal to the previous actual plus max(income - expenses, 0), then projected steps one month apart until six exist. With no rows the first step is today with actual: 0. Source: pkg/api/savings_progress.go

Savings Snapshot

site-insight-savings-snapshot consumes user_new_txns_batch_completed (payload: the bare user ID) tapped from EventBridge — see Events. It lists the user’s active items with accounts, keeps checking and savings subtypes, sums available (else current) per account, and writes the SAVINGS_MONTH#<YYYY-MM> row (schema), overwriting the current month. A user with no checking or savings accounts gets no row. Metric insight.savings.snapshot tagged result:saved|no_accounts|failed. Source: pkg/savings-snapshot/handler.go