← all builds

Payments Matcher

financereconciliationautomation
Download .md

A sanitised reference implementation. Drop it into Claude Code, point it at your own bank feed and invoice table, adapt the reference rules. Generic on purpose, with no business specifics, no real payees and no real amounts.

The problem

A bank feed and an invoice list both hold the same fact, that something got paid, but nothing ties them together automatically. Someone ends up reading a statement line by line, working out by eye which invoice it must be, and marking it paid by hand. It is slow, and a short payment or an odd reference is easy to miss.

The idea

Read your own already stored copy of the bank feed, never the bank itself live, and try each payment against your invoices two ways in order. First, pull a likely invoice number out of the payment's own reference text and check the amount agrees exactly. If the reference is no help, fall back to matching on amount alone, but only when exactly one open invoice is that amount. Anything that still does not land cleanly, a short payment, more than one invoice at that amount, a reference that matches nothing, goes on a short list for a person to glance at. Nothing is ever guessed into place.

bank feed ──> [reference match] ──> exact amount? ──> write the match
                    │ no match
                    v
             [amount match] ──> exactly one open invoice that size? ──> write the match
                    │ no
                    v
              add to the needs you list, nothing written

Minimal reference implementation

# payments_matcher.py, reference only. Swap in your own bank rows and invoice table.
import re

def invoice_from_reference(reference):
    """The longest run of digits in the reference, read as the invoice number.
    Handles a reference typed oddly (INV0123, 1NV-0456, INV-00789) without
    needing an exact format. None when there is no such run."""
    runs = re.findall(r"\d+", reference or "")
    if not runs:
        return None
    return max(runs, key=len)

def match_payment(payment, open_invoices):
    """One payment, in whole pennies, against open invoices. Returns (invoice,
    rule) or (None, reason) when a person needs to look at it."""
    number = invoice_from_reference(payment["reference"])
    by_number = {inv["number"]: inv for inv in open_invoices}
    if number and number in by_number:
        inv = by_number[number]
        if inv["amount"] == payment["amount"]:
            return inv, "reference_and_amount"
        return None, f"names invoice {number} but the amount does not agree"

    same_amount = [inv for inv in open_invoices if inv["amount"] == payment["amount"]]
    if len(same_amount) == 1:
        return same_amount[0], "amount_only"
    if len(same_amount) > 1:
        return None, f"{len(same_amount)} open invoices are that exact amount"
    return None, "no open invoice matches this payment"

def run(payments, open_invoices, write_match, needs_you):
    for payment in payments:
        inv, result = match_payment(payment, open_invoices)
        if inv:
            write_match(payment, inv, result)     # your own write, never the bank
        else:
            needs_you(payment, result)             # a short list, not a guess

Why it works

Adapt it

Built for my own group's accounts, runs every morning, and this is the generalised version. Take it, point it at your own bank feed, tell me what you would add.