Back to home
💰 Spending Storyteller
Turns bank CSV exports into AI-generated spending narratives — because pie charts don't tell you that you ordered DoorDash three nights in a row
A CLI tool that transforms raw transaction data from Chase, Amex, and Capital One into conversational monthly spending stories using GPT-4 Turbo's 128K context window.
The core architecture splits work between what computers are good at and what LLMs are good at: pandas pre-computes every number (totals, averages, rankings, patterns) so the model never does arithmetic, then GPT-4 Turbo narrates the verified data into a story you'd actually want to read. The result references specific merchants, dollar amounts, and temporal patterns — not generic advice like "consider reducing discretionary spending."
Built during a deliberate gap year focused on AI/ML engineering. The project launched the same week as GPT-4 Turbo (Nov 2023), using the brand-new openai >= 1.0 SDK and the 128K context window as a load-bearing architectural decision — not a bullet point.
🛠️ Tech Stack
🚀 Features
Auto-detects bank format — reads CSV headers and dispatches to the correct parser (Chase, Amex, Capital One) without user configuration
17-rule regex pipeline + fuzzy dedup for merchant name normalization — turns
SQ *BLUE BOTTLE COFFE NEW YORK NY 10013intoBlue Bottle CoffeePre-computed statistics engine — totals, daily averages, top merchants by $ and frequency, weekday vs. weekend patterns, category breakdowns, and largest single transactions, all calculated deterministically before the LLM sees them
Persona-driven prompt engineering — iterated through 3 system prompt versions to land on a "thoughtful friend" voice that's specific, conversational, and never preachy
--dry-runmode — prints the exact prompt and token count without making an API call, enabling free prompt iterationMulti-bank, multi-month support — merge CSVs from different banks, process all months, and generate a month-over-month comparison narrative
stdout/stderr separation — narratives go to stdout, progress and cost logs to stderr, enabling clean piping (
> november.txt)Markdown export with self-documenting metadata footer (model, tokens, cost per report)
🧗♂️ Challenges
LLMs can't do math — so don't ask them to. Early prototypes passed raw transactions directly to GPT-4 and asked it to summarize. It confidently summed 200 transactions and was off by hundreds of dollars. The fix was a hybrid architecture: pandas computes every number, the prompt explicitly says "use these numbers, don't recompute," and the LLM's job becomes pattern recognition and storytelling — what it's actually good at.
Merchant descriptions are chaos.
AMZN MKTP US*2K7X9P4Q1,SQ *BLUE BOTTLE COFFE NEW YORK NY 10013,TST* JOE'S PIZZA— each contains transaction prefixes, store numbers, city/state suffixes, and order IDs that need different stripping strategies. The first regex attempt used a generic city pattern that ate merchant words likePHARMACYandCOFFEE. The fix: an explicit list of 30+ US cities instead of a greedy word-count match, and a rule requiring order IDs to contain at least one digit (so*EATSis kept but*2K7X9P4Q1is stripped).Prompt tone is harder than prompt content. Three system prompt iterations taught me that persona beats rules. "Be friendly" is vague — "You're a thoughtful friend who celebrates smart moves genuinely (not patronizingly) and flags concerns gently, with data, not lectures" gives the model a character to inhabit. The
Do NOTconstraints (no bullet points, no moralizing, no invented data) were added one at a time as specific failure modes appeared in testing.Temperature is a meaningful parameter. Tested 0.3, 0.5, 0.7, and 1.0 on the same dataset. At 0.3, every narrative opened identically. At 1.0, the model invented a merchant name. The sweet spot (0.7) balanced varied sentence structure with factual accuracy — a concrete lesson in the accuracy-creativity tradeoff.
🧠 Learnings
Hybrid AI architecture — the pattern of "compute what you can, narrate what you can't" applies to any AI product where accuracy matters. Pre-compute deterministic facts, inject them as grounding context, and constrain the LLM to pattern recognition and language generation.
Context window strategy — a typical month uses ~5K–12K tokens of the 128K budget (~5%). Measuring token usage early with
tiktokenproved that chunking and summarization were unnecessary, which simplified the entire architecture. The lesson: measure before optimizing.Abstract base class design — the parser layer uses an ABC with a shared
_validate()method and bank-specificparse()implementations. Adding a new bank means one class and one column signature — nothing else changes. Detection order matters: most-specific first (Capital One 7 cols → Chase 6 → Amex 3) to prevent false matches.Test architecture — 51 tests mock only at the API boundary (
generate_narrative). Everything upstream — parsing, normalization, aggregation, prompt building — runs with real logic on real fixture data. This catches the bugs that actually happen (regex edge cases, pandas dtype issues) while keeping tests fast and free of network flakiness.Build bottom-up, wire top-down — the 5-phase build order (parsers → normalizer → aggregator → prompts → CLI) meant each layer was tested and stable before anything depended on it. By Phase 5, the CLI was pure orchestration — no business logic, no debugging through the full stack.