Match every payment to an invoice by an exact reference, credit only the receipts that pass every check (amount, currency or asset, network, expiry date, uniqueness), and confirm that the service was delivered before you close anything. Everything that fails a check goes to a short exception list that a person reviews. Write these rules in code, not in a prompt, so the same input always gives the same verdict, and use a language model only to write notes about the result.
Why "paid" is harder than it looks
Most payments are fine. The ones that are not cost money in ways that stay invisible until month end:
- A buyer sends 90% of the amount and the invoice gets closed anyway.
- The right currency arrives on the wrong network or rail.
- The same settlement is submitted twice, and you now owe a refund nobody noticed.
- A payment lands after the invoice expired, at a price you no longer offer.
- The buyer paid and never received the service.
Two related problems sit on either side of the invoice. Before it, metered usage may be billed without an authorization that covers it. After it, a contract may renew silently because nobody saw the notice window.
A matching approach you can apply today
You can run this pattern with a spreadsheet export and a short script. No purchase needed.
1. Match on a reference, never on a guess
Require an invoice ID on every payment record. Do not match by amount, by customer name, or by "closest open invoice": those heuristics are how a payment from one customer closes another customer's invoice. A payment whose reference is not in your batch goes to an orphan list. It never creates an invoice and is never absorbed into one.
2. Check each receipt before crediting it
A receipt counts toward the invoice only if nothing is wrong with it:
- The amount is present, numeric and positive.
- The currency or asset matches the invoice.
- The network or payment rail matches, when both sides record one.
- The payment date is not after the invoice expiry.
- Its settlement identity (a transaction hash, a processor charge ID) has not already been seen anywhere in the batch.
A receipt that fails any check is recorded as uncredited. It is not partial credit. An invoice should never be marked paid with money that cannot settle it.
3. Give each invoice one status, root cause first
Decide the status in a fixed order and stop at the first match. A sensible order:
- invalid invoice (no ID, no positive amount due)
- wrong asset or network
- expired invoice
- unpaid
- underpaid
- duplicate receipt
- overpaid
- paid but not delivered
- reconciled
Order matters. A payment on the wrong network also leaves the invoice short, but "wrong network" is the cause and "underpaid" is the symptom. Reporting the symptom sends someone to chase a buyer who already paid.
4. Use a small tolerance, and never mix currencies
Allow an absolute rounding tolerance in the invoice currency (for example one cent). Keep exposure per currency: adding USDC and ETH together produces a number that means nothing. Also keep the two directions apart: money owed to you and money you must refund are different actions.
5. Require delivery evidence
"Covered" is not "closable". Before you close an invoice, require a record that the service was delivered: a completed job, a fulfilled order, a successful API call tied to the same invoice ID.
Before the invoice: usage evidence
If you bill per unit or per tool call, apply the same discipline to the usage log. An event is billable only if it has an ID, a customer, a SKU, a numeric quantity and a timestamp; its ID and its content have not been seen before; it falls inside the billing period; a price rule exists for the SKU; and an active authorization for the same customer covers it. Everything else is an exception to resolve before anyone is charged.
After the invoice: renewal notice windows
For contracts, the date that matters is not the end date but the notice date: end date minus the notice period. Flag contracts whose notice date falls inside your watch window, contracts already past it, and contracts with no named owner. Never guess a missing date or owner; report it as missing.
How the packs implement this
Agent Payment Reconciliation applies the rules above almost one to one. It reads three JSON files (invoices, payments, service calls), matches receipts by exact invoice_id, and returns one ledger entry per invoice with a status from invalid, wrong_asset, expired_invoice, unpaid, underpaid, duplicate_receipt, overpaid, missing_fulfillment or reconciled, decided in that order. Duplicates are detected by tx_hash across the whole batch, unmatched receipts go to orphan_payments, rejected money is reported in amount_uncredited, and exposure is grouped per asset in amount_at_risk_by_asset. The default tolerance is 0.01. Every status except reconciled and unpaid sets escalation_required: true. It uses the standard library only.
python agent.py reconcile \
--invoices data/invoices.json \
--payments data/payments.json \
--service-calls data/service_calls.json \
--out out/reconciliation.json
python agent.py explain --invoice-id inv-metering-api --report out/reconciliation.json
Agent Billing Pack covers the invoicing side: a SQLite task queue with retries, a dead-letter queue and idempotency keys, and built-in handlers generate_invoice, send_reminder, record_payment, reconcile_billing and cleanup. Its key rule: an invoice_id comes only from your data, a deterministic generator or the billing system's response. A payment without a real invoice ID is never marked paid; it is parked (for example as awaiting_invoice_creation or manual_review), and any model output that invents an ID is rejected.
Agent Usage Meter turns a usage ledger into billable evidence. It sorts events into billable_totals, over_quota, duplicate_events, missing_authorizations, reconciliation_gaps and manual_review, computes amounts in decimal, and marks every row charge_executed: false.
python agent.py --ledger my_ledger.json --period-start 2026-07-01 --period-end 2026-07-31 meter
Agent Contract Renewal computes notice_date = end_date - notice_days and flags EXPIRED, NOTICE_WINDOW_MISSED, NOTICE_WINDOW, MISSING_OWNER and missing dates. Non-ISO dates go to manual review unchanged.
python agent.py --portfolio my_contracts.json --window-days 90 scan
If payment sits in escrow until delivery is proven, Agent Escrow Release checks delivery proofs against signed webhook receipts with matching artifact hashes, and sorts each escrow into release_ready, hold, refund or manual_review.
All of these run without a model. If a Claude Code, Kimi Code or Codex CLI is connected, or a local Ollama server (PACK_LLM=ollama), it can add a review note or brief; in the reconciliation pack the model can never change a status.
Honest limits
- Nothing moves money. The reconciliation pack never moves money or touches a private key. The usage meter never charges, captures, invoices or refunds. The escrow pack never releases funds and marks every decision
human_approval_required. - You map your data. The packs read JSON in documented shapes. Wiring your billing tables, processor webhooks or exports into those shapes is your job; each pack's
CUSTOMIZE.mddescribes where. - Exact references only. A payment without a usable invoice reference becomes an orphan, and a person resolves it.
- No legal or tax judgment. The usage meter does not decide contractual or tax obligations. The renewal pack does not determine legal obligations: a human verifies the source agreement and authorizes any notice.
Where to start
- Export one month of invoices and payments and check that every payment carries an invoice reference. If many do not, fix that first.
- Apply the receipt checks and the status order above, even in a spreadsheet, and look at what lands outside
reconciled. - If you want it as runnable code, start with Agent Payment Reconciliation ($3, one-time) and its
init-sampleexample files, then add the usage meter or renewal scanner if you bill by usage or manage contracts.
Browse all packs at /packs or read the quickstart.