If you charge autonomous agents or bill per tool call, receipts arrive faster than anyone can check them. Most of them are fine. The ones that are not cost real money in a way that is invisible until month end:
- a buyer sends 90% of the amount and the invoice gets closed anyway
- a payment lands in the right asset on the wrong network, or the wrong asset entirely
- the same settlement is submitted twice and you owe a refund you never noticed
- a receipt settles after the invoice expired, at a price you no longer offer
- the buyer paid and got nothing — no service call was ever delivered
This agent reads your invoices, your payment receipts and your service-call log,
matches them by invoice_id, and returns one conservative ledger entry per
invoice. It is deterministic: same input, same verdict, no model required.
What it is not
It is not a payment processor, a wallet, or a chain indexer. It never moves money and never touches a private key. It reads three JSON files and tells you which invoices you may safely close, and which ones need a human.
Install
sudo bash install.sh # deps + venv + systemd service
bash install.sh --no-service # venv only
Debian/Ubuntu. Python 3.9+. Standard library only — no dependencies to audit.
Run
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 status --report out/reconciliation.json
python agent.py explain --invoice-id inv-metering-api --report out/reconciliation.json
python agent.py run --once --deterministic
init-sample writes a working set of the three input files if you just want to
see the shape:
python agent.py init-sample --dir examples
Statuses
One status per invoice, decided in this order. The first one that applies wins, so you always see the root cause rather than a downstream symptom.
| Status | Meaning | Typical fix |
|---|---|---|
invalid |
no invoice_id, or amount_due is missing or not positive |
fix the invoice |
wrong_asset |
a receipt settled with an asset or on a network the invoice does not accept | re-credit or refund |
expired_invoice |
the receipt settled after expires_at |
reissue at current price |
unpaid |
no receipt references this invoice | wait or chase |
underpaid |
credited receipts do not cover amount_due |
collect the remainder |
duplicate_receipt |
the same settlement was submitted more than once | refund the duplicate |
overpaid |
credited receipts exceed amount_due |
refund the difference |
missing_fulfillment |
fully paid, but no service call was delivered | deliver or refund |
reconciled |
amount, asset, network and fulfillment all match | close it |
Everything except reconciled and unpaid sets escalation_required: true.
The conservative rules
These are the rules that make the ledger trustworthy. They are deliberately strict, and all of them are enforced in code, not in a prompt.
- A receipt is credited only if nothing is wrong with it. Wrong asset,
wrong network, duplicate settlement, or post-expiry arrival ⇒ the money is
reported in
amount_uncredited, and never added toamount_paid. An invoice is never marked paid by money that cannot legally settle it. - Duplicates are detected by settlement identity (
tx_hash), not by row id — across the whole batch. The same hash replayed against a second invoice is still a duplicate. - A receipt whose
invoice_idis not in the batch never creates an invoice. It goes toorphan_paymentsverbatim. - Exposure is never summed across assets.
amount_at_risk_by_assetkeeps USDC and ETH apart, because adding them produces a number that means nothing. - Money owed to you and money you are wrongly holding are separate.
amount_at_riskreports one direction per invoice, in the invoice asset.
Bring your own LLM (optional)
The matcher never calls a model. If a Claude Code, Kimi Code, or Codex CLI is
installed and logged in, llm_adapter.py will use it to add a review note —
suggested actions, dunning replies, a standup digest. No API key: your existing
CLI subscription is the billing relationship.
python llm_adapter.py # shows which backend, if any, is available
PACK_LLM=codex python agent.py reconcile # pin a backend
The model can never change a status. It advises; the deterministic core decides.
Input shape
Three files, all optional fields tolerated. Amount fields accept numbers or numeric strings.
// invoices.json
{"invoices": [{"invoice_id": "inv-tool-calls", "buyer": "agent://checkout-bot",
"amount_due": 12.5, "asset": "USDC", "network": "base",
"issued_at": "2026-07-01T09:00:00Z", "expires_at": "2026-07-08T09:00:00Z"}]}
// payments.json
{"payments": [{"payment_id": "pay-001", "invoice_id": "inv-tool-calls",
"amount": 12.5, "asset": "USDC", "network": "base",
"tx_hash": "0xa1b2c3d4", "paid_at": "2026-07-03T10:15:00Z"}]}
// service_calls.json
{"service_calls": [{"call_id": "call-001", "invoice_id": "inv-tool-calls",
"status": "delivered", "completed_at": "2026-07-03T10:20:00Z"}]}
See CUSTOMIZE.md to wire your own billing tables, PSP webhooks or gateway logs
into these three shapes.
Test
bash smoke_test.sh # offline, no keys, no network
python test_agent.py
Files
| File | Role |
|---|---|
agent.py |
CLI + deterministic reconciliation core |
llm_adapter.py |
optional BYO-LLM CLI bridge |
PROMPTS.md |
versioned prompts, loaded by agent.py |
SPEC.md |
data contract and decision order |
CUSTOMIZE.md |
where to plug your own data in |
CHECKLIST.md |
operator runbook |
install.sh / *.service |
one-shot Debian install + systemd unit |
test_agent.py / smoke_test.sh |
offline test suite |
examples/ |
working input files |
License
Personal use per purchase. Resale prohibited.