erin.dev

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

Source Code

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

Python 3.11GPT-4 Turbo (128K context)OpenAI SDK v1.0+pandastiktokendifflib (fuzzy matching)argparse

🚀 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 10013 into Blue Bottle Coffee

  • Pre-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-run mode — prints the exact prompt and token count without making an API call, enabling free prompt iteration

  • Multi-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 like PHARMACY and COFFEE. 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 *EATS is kept but *2K7X9P4Q1 is 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 NOT constraints (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 tiktoken proved 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-specific parse() 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.