Bookkeeping sorter for bank, Stripe and PayPal exports
From the Subzero Labs video "Your AI Has Amnesia. This Free Code Makes It Remember".
Free, no sign-up. Copy it, use it, share it.
For: Anyone self-employed
The job: Every month: hundreds of bank and payment lines to sort into categories before your accountant or tax time.
You give it: bank CSV, Stripe payments export, PayPal activity CSV, your own rules (my_rules.csv) and your category list in config.json.
It is one agent of the free AI OS starter (https://subzero.bodymindbeing.me/free/ai-os-starter/) - download the whole starter, it runs on sample data out of the box. Nothing is sent anywhere: replies come out as drafts.
Download the AI OS starter (zip)
MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.
Run this one
- Set up the starter (the steps on the starter page).
- python -m aios run bookkeeping
- Edit config.json: point "bookkeeping" at your own files (paths relative to config.json).
Check it yourself
- Open data/bookkeeping.csv: the 'Decided by' column says your rule, the AI, or nobody.
- Add up one category in a spreadsheet - it must match the home screen total.
- Money coming in that the AI filed as an expense, an answer that is not on your list, or a low-confidence answer goes to 'Needs review'.
- A draft for your records, not tax advice.
"""EVERY SELF-EMPLOYED PERSON - bookkeeping sorter. Reads your bank, Stripe and PayPal CSV exports and puts every line
in a category from YOUR list (config.json "categories"):
1. your own rules first (my_rules.csv: "description contains" -> category), plain code;
2. what is left goes to the AI, which may only answer with a category from your list - anything else, a low
confidence, or a category that does not fit the direction of the money (money IN filed as an expense, or
money OUT filed as income) becomes "Needs review";
3. the totals per category are added up by the code.
Output: data/bookkeeping.csv (every line + category + how it was decided) for you or your accountant to check.
This is a draft for your records, not tax advice."""
import csv
from collections import defaultdict
from aios import llm
from ._common import col, money, table, to_date
TITLE = "Bookkeeping sorter"
REVIEW = "Needs review"
# categories that money coming IN may have; anything else on money in (or "Income" on money out) is sent to you
MONEY_IN = ["Income", "Transfer (not income or expense)", "Personal (not business)"]
def load(cfg, s):
"""Every export -> [(date, source, description, amount)] (money in = positive, out = negative)."""
out = []
for src in s["exports"]:
for r in table(cfg, src["file"]):
d = to_date(col(r, "date", "created date (utc)", "created", "posting date", "transaction date"))
desc = " ".join(x for x in (col(r, "description", "name", "payee", "memo"), col(r, "type")) if x)
if src.get("kind") == "stripe": # a Stripe payment = the gross amount in, its fee out
out.append((d, src["name"], desc or "Stripe payment", money(col(r, "amount"))))
fee = money(col(r, "fee"))
if fee:
out.append((d, src["name"], "Stripe fee", -abs(fee)))
continue
amount = money(col(r, "amount", "gross", "net"))
if src.get("kind") == "paypal":
fee = money(col(r, "fee"))
if fee:
out.append((d, src["name"], "PayPal fee", -abs(fee)))
out.append((d, src["name"], desc, amount))
return [x for x in out if x[0]]
def by_rules(desc, rules):
low = desc.lower()
return next((cat for key, cat in rules if key and key in low), None)
def run(cfg, data, s):
cats = s["categories"]
rules = [(col(r, "description contains", "contains").lower().strip(), col(r, "category"))
for r in table(cfg, s["rules"])] if s.get("rules") else []
rules += [("stripe fee", "Commissions and fees"), ("paypal fee", "Commissions and fees")]
lines = load(cfg, s)
done, todo = [], []
for i, (d, src, desc, amt) in enumerate(lines):
cat = by_rules(desc, rules)
(done if cat else todo).append([i, d, src, desc, amt, cat, "your rule" if cat else ""])
if todo:
ask = "\n".join(f"{n}. {t[3]} | {'+' if t[4] > 0 else ''}{t[4]:.2f}" for n, t in enumerate(todo, 1))
r = llm.ask_json("Categories (use these exact words, nothing else):\n- " + "\n- ".join(cats)
+ f"\n\nBank / payment lines of a self-employed person (+ = money in):\n{ask}\n\nGive each line "
"the best category and your confidence. If unsure, use 'Needs review'. "
'JSON list: [{"n": 1, "category": "...", "confidence": "high|medium|low"}]')
r = r if isinstance(r, list) else next((v for v in r.values() if isinstance(v, list)), [])
got = {int(x.get("n", 0)): x for x in r if isinstance(x, dict) and str(x.get("n", "")).isdigit()}
money_in = set(s.get("money_in_categories", MONEY_IN))
for n, t in enumerate(todo, 1):
x = got.get(n, {})
cat, conf = x.get("category"), str(x.get("confidence", "low")).lower()
if cat not in cats: # not on your list -> you decide
t[5], t[6] = REVIEW, "AI answered off-list" if cat else "no answer"
elif cat != REVIEW and ((t[4] > 0 and cat not in money_in) or (t[4] < 0 and cat == "Income")):
# money in filed as an expense, or money out filed as income
t[5], t[6] = REVIEW, f"AI said '{cat}' but the money goes {'in' if t[4] > 0 else 'out'}"
elif conf not in ("high", "medium"):
t[5], t[6] = REVIEW, "AI unsure"
else:
t[5], t[6] = cat, f"AI ({conf})"
done += todo
done.sort(key=lambda t: t[0])
totals = defaultdict(float)
for t in done:
totals[t[5]] += t[4]
with open(data / "bookkeeping.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["Date", "Source", "Description", "Amount", "Category", "Decided by"])
w.writerows([t[1], t[2], t[3], f"{t[4]:.2f}", t[5], t[6]] for t in done)
review = [t for t in done if t[5] == REVIEW]
items = [f"{c}: {v:,.2f}" for c, v in sorted(totals.items(), key=lambda kv: kv[1])]
attention = [f"{len(review)} line(s) need your decision (e.g. '{review[0][3]}') - see data/bookkeeping.csv"] if review else []
n_rule = sum(1 for t in done if t[6] == "your rule")
return {"summary": f"{len(done)} lines sorted: {n_rule} by your rules, {len(done) - n_rule - len(review)} by the AI, "
f"{len(review)} for you. Totals by category below (a draft, not tax advice).",
"items": items, "attention": attention}
Shared as shown in the video. Replace any <PLACEHOLDERS> with your own values.