Subzero Labs

Free code · Subzero Labs

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) 65 KB

MIT licence. Python 3.10+, no other installs. Unzip, then follow README.md.

Run this one

  1. Set up the starter (the steps on the starter page).
  2. python -m aios run bookkeeping
  3. Edit config.json: point "bookkeeping" at your own files (paths relative to config.json).

Check it yourself

  1. Open data/bookkeeping.csv: the 'Decided by' column says your rule, the AI, or nobody.
  2. Add up one category in a spreadsheet - it must match the home screen total.
  3. 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'.
  4. A draft for your records, not tax advice.

agents/bookkeeping.py

"""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.