Unpaid-invoice chaser
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: Freelancers and creators
The job: Late invoices mean awkward emails, every week, each one a bit firmer than the last.
You give it: your invoice list as CSV or .xlsx (Invoice, Client, Email, Amount, Currency, Due, Paid) and your payment terms in Markdown.
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 invoice_chaser
- Edit config.json: point "invoice_chaser" at your own files (paths relative to config.json).
Check it yourself
- In a spreadsheet, work out today minus the due date for each unpaid invoice - the days late and the total owed must match.
- Every reminder names the invoice number and the exact amount; if the AI's draft misses either, a plain template is used.
- Run it twice: each reminder step is drafted only once per invoice (data/invoice_chaser_state.json).
"""FREELANCERS + CREATORS - unpaid-invoice chaser. Reads your invoice list (a CSV/.xlsx export from your invoicing tool
or your own spreadsheet: invoice, client, email, amount, currency, due date, paid date). Plain code works out what is
overdue and by how many days, and which reminder step it has reached:
step 1 1-6 days late friendly nudge
step 2 7-13 days late firm reminder
step 3 14-29 days late second reminder, mention your late-fee terms
step 4 30+ days late final notice -> also goes to your attention list
The AI drafts each reminder in your tone from your own payment terms (memory). The code then checks the draft names
the exact invoice number and amount - if not, a plain template is used instead. Each step is drafted once per
invoice (data/invoice_chaser_state.json). Nothing is sent: drafts are in data/drafts/invoices/."""
import json
from collections import defaultdict
from aios import llm, memory
from aios.config import path
from ._common import col, money, save_draft, table, to_date, today
TITLE = "Unpaid invoices"
STEPS = [(30, 4, "final notice"), (14, 3, "second reminder"), (7, 2, "firm reminder"), (1, 1, "friendly nudge")]
def step_for(days_late):
return next(((n, name) for start, n, name in STEPS if days_late >= start), (0, ""))
def amount_text(amount, cur):
return f"{amount:,.2f} {cur}".strip()
def draft_ok(text, inv, amount):
"""The reminder must name the exact invoice number and amount (e.g. 1,200.00 or 1200.00)."""
t = text.replace(",", "")
return inv.lower() in text.lower() and f"{amount:.2f}" in t
def template(client, inv, amount, cur, due, late, step_name):
return (f"Hi {client},\n\nA quick note that invoice {inv} for {amount_text(amount, cur)} was due on {due} and is now "
f"{late} days overdue. Could you let me know when it will be paid?\n\nThank you.")
def run(cfg, data, s):
now = today(cfg)
memory.sync_folder(data, s["business_project"], path(cfg, s["business_notes"]))
terms = memory.notes_text(data, s["business_project"])
st_f = data / "invoice_chaser_state.json"
state = json.loads(st_f.read_text()) if st_f.exists() else {}
owed, items, attention, drafted = defaultdict(float), [], [], 0
rows = table(cfg, s["invoices"])
for r in rows:
inv, client = col(r, "invoice", "invoice number", "number"), col(r, "client", "customer", "brand")
amount, cur = money(col(r, "amount", "total", "amount due")), col(r, "currency") or ""
due, paid = to_date(col(r, "due", "due date")), to_date(col(r, "paid", "paid date", "date paid"))
if paid or not due:
continue
late = (now - due).days
if late < 1:
items.append(f"{inv} {client}: {amount_text(amount, cur)} due {due} (in {-late} days)")
continue
owed[cur] += amount
n, name = step_for(late)
line = f"{inv} {client}: {amount_text(amount, cur)}, {late} days late -> step {n} ({name})"
if state.get(inv, 0) < n:
r_ = llm.ask(f"MY PAYMENT TERMS AND TONE:\n{terms}\n\nWrite a {name} email (step {n} of 4) to {client} "
f"about invoice {inv} for {amount_text(amount, cur)}, due {due}, now {late} days overdue. "
"Use only my terms; mention the late fee only if my terms have one and the step is 3 or 4. "
"Say the invoice number and the exact amount. Under 120 words. Just the email body.")
if not draft_ok(r_, inv, amount):
r_ = template(client, inv, amount, cur, due, late, name)
line += " (AI draft missed the number/amount - plain template used)"
f = save_draft(data, "invoices", len(state) + 1, col(r, "email") or client,
f"Invoice {inv} - {name}", r_)
state[inv] = n
drafted += 1
line += f" - draft {f.name}"
else:
line += " - reminder for this step already drafted"
items.append(line)
if n == 4:
attention.append(f"{client} {inv}: {late} days overdue ({amount_text(amount, cur)}) - final notice drafted")
st_f.write_text(json.dumps(state, indent=1))
items.sort(key=lambda x: ("late" not in x, x))
total = ", ".join(amount_text(v, c) for c, v in sorted(owed.items())) or "nothing"
return {"summary": f"Overdue: {total} across {sum(1 for i in items if 'late' in i)} invoices. "
f"{drafted} new reminder draft(s) - nothing sent.", "items": items, "attention": attention}
Shared as shown in the video. Replace any <PLACEHOLDERS> with your own values.