This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
Every bank statement is haunted a little. The gym you stopped going to in March. The free trial that quietly turned into $14.99 a month. The streaming service that raised its price two dollars and hoped you wouldn't notice. The $2.99 charge that hit twice in one week. None of them make noise. They just keep showing up, month after month, like a ghost that's forgotten it's dead.
Ghost Charges finds them. Export a CSV from your bank, run one command, and get a table like this:
7 recurring charges still active, $1,510.52 a year.
| Merchant | Every | Last charge | Per year | Next | Why look |
| Ironworks Gym | monthly | 2026-10-07 $49.00 | $588.00 | 2026-11-06 | |
| Netflix | monthly | 2026-10-03 $17.99 | $215.88 | 2026-11-02 | price went up $15.49 → $17.99 |
| New York Times| weekly | 2026-10-26 $4.00 | $208.00 | 2026-11-02 | |
| Canva | monthly | 2026-10-15 $14.99 | $179.88 | 2026-11-14 | free trial turned paid |
| Spotify | monthly | 2026-10-12 $11.99 | $143.88 | 2026-11-11 | |
| Amazon Prime | annual | 2025-11-02 $139.00 | $139.00 | 2026-11-02 | renews within 30 days |
| Apple | monthly | 2026-10-22 $2.99 | $35.88 | 2026-11-21 | charged twice |
| Hulu | monthly | 2026-04-20 $9.99 | - | - | stopped |
It's the small ghosts that add up: $1,510 a year in this (synthetic) statement, most of it from charges nobody looks at anymore.
Demo
The repo has the full output of a real run on the synthetic sample: the findings table and the audit trail, which shows every name the model read and whether the rules accepted it. On an 8-core CPU with no GPU, Gemma 4 E2B reads about one statement line every 2 seconds, so 28 unique lines took under a minute. Run it again with --readings and it finishes instantly without the model.
Code
onyxaholguin-cyber
/
ghost-charges
Find the subscriptions still haunting your bank statement. A local Gemma 4 reads statement lines; plain tested Python finds what repeats. Nothing leaves your machine.
Ghost Charges 👻
Find the subscriptions still haunting your bank statement, on your own machine.
Export a CSV from your bank. An open-weight model (Gemma 4 E2B, 2.8 GB, a CPU is enough) reads each messy statement line (PAYPAL *SPOTIFYUSA 4029357733, SQ *IRONWORKS GYM 2841 AUSTIN TX) and names the merchant. Plain, tested Python checks every name it gives, groups the charges, works out what repeats, and flags the ones worth a look
Your bank history never leaves your computer.
7 recurring charges still active, $1,510.52 a year
| Merchant | Every | Last charge | Per year | Next | Why look |
| Ironworks Gym | monthly | 2026-10-07 $49.00 | $588.00 | 2026-11-06 | |
| Netflix | monthly | 2026-10-03 $17.99 | $215.88 | 2026-11-02 | price went up $15.49 → $17.99 |
| Canva | monthly | 2026-10-15 $14.99 | $179.88 |…How I Built It
Bank statement lines are hard to read for code and easy for a language model:
PAYPAL *SPOTIFYUSA 4029357733
SQ *IRONWORKS GYM 2841 AUSTIN TX
NYTIMES*NYTDIGITAL 800-698-4637 NY
NETFLIX.COM 866-579-7172 CA
NETFLIX COM LOS GATOS CA
A regex can strip PAYPAL * and the phone numbers, but it will never know that NYTIMES*NYTDIGITAL is the New York Times. Gemma knows. What Gemma can't be trusted with is arithmetic over a year of dates. So the split is the same one I've used before: the model only reads, plain code does everything else.
-
Read. Each unique line goes to a local
llama-server(llama.cpp b11617) running Gemma 4 E2B Q4_0, at temperature 0, with a JSON Schema forcing{merchant, quote, kind}. -
Check the reading with rules. The quote must really be in the line, and at least one word of the merchant name has to appear in it. For
AMZN Mktp US*2K45L7the model said "Amazon". That word isn't written there, so the rule falls back to "Amzn Mktp" and marks the lineFIXEDin the audit. The model doesn't get to invent a brand. - Group. Names merge when one is the other plus a suffix ("Adobe Inc." → "Adobe", "Dropbox Plus" → "Dropbox").
- Find what repeats. Weekly, monthly or annual by the median gap between charges, at least 3 charges (2 for annual), steady amounts (so the grocery store doesn't count), and then the flags: price went up, charged twice, free trial turned paid, renews within 30 days, stopped.
It's Python's standard library only. No pip installs, no accounts, and no network calls except to the model server on localhost.
Did the model earn its place? I scored both versions against hand-made answer keys:
| Set | Gemma + rules | Rules alone |
|---|---|---|
| Dev statement (148 rows, 8 subscriptions) | 8/8 exactly right | 7/8 (can't name NYTIMES*NYTDIGITAL) |
| Held-out statement (48 rows, 4 subscriptions), first run | 1/4 | 0/4 |
| Same held-out, after the fix | 4/4 | 3/4 (splits the two Dropbox descriptors and misses the annual renewal) |
The held-out run is the honest part. I wrote it after the rules were done, and it broke things: Gemma named the same merchant two different ways ("Adobe" / "Adobe Inc.", "Youtube Premium" / "YouTube Premium"), which split each subscription into pieces too small to look recurring. The merge rule in step 3 fixed it. Because I fixed it on those same rows, the last line isn't a clean held-out score, and the README says so. It has 14 unit tests, and 6 of 6 deliberate code breakages get caught. CI re-scores the recorded Gemma replies on every push.
Why Does Open Innovation Matter?
A bank export is a year of your life: where you live, where you eat, who you pay, what you're embarrassed to be subscribed to. Uploading it to a hosted model to find a $9.99 charge is a bad trade, and "we don't train on your data" is a promise you can't check.
With an open-weight model, the trade disappears. Gemma 4 E2B is 2.8 GB and runs on a CPU. You can pull the network cable and Ghost Charges still works. There's no API key to leak, no per-token bill, and no service that can shut down and take your tool with it. Because the model is open and pinned, the recorded replies in the repo are reproducible: anyone can rerun the exact same model and get the exact same readings.
The open part also changes how you build. When the model is a file on your disk, it stops being an oracle and becomes a component, one that you test, check with rules, and replace if a better small model comes out next month.
The Spirit of It
Halloween is about the things that linger after they should be gone. Most of us are carrying a few ghost charges right now. The first time I ran this on the sample, the line that landed was the Hulu row: stopped, last seen in April. That's the one you want to see. The ones still marked active are the ones to go cancel tonight.
Prize Categories
- Best Use of Gemma
Top comments (0)