Bank reconciliation is mostly a matching problem, and most bank transactions match Acumatica's cash records on amount, date, and reference number without needing any AI at all. The place an LLM earns its keep is the residual — the handful of transactions each period that don't match cleanly because of a truncated reference, a batched payment, or a timing difference — where a human would otherwise spend an hour scrolling through both ledgers by eye.
Deterministic matching does most of the work
Before any LLM is involved, run a standard matching pass: exact amount + date-window matches against Acumatica's Cash Transactions and the imported bank statement lines. This is the same logic Acumatica's own bank feed reconciliation already does reasonably well, and it should resolve the majority of lines. Only the unmatched remainder — typically a small percentage of total transaction volume — is worth spending model calls on.
unmatched_bank = [t for t in bank_lines if t.id not in matched_ids]
unmatched_gl = acumatica_client.get_unreconciled_cash_transactions(account_id)
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=800,
messages=[{
"role": "user",
"content": f"Bank lines: {json.dumps(unmatched_bank)}\n"
f"GL cash transactions: {json.dumps(unmatched_gl)}\n\n"
f"Propose candidate matches based on amount proximity, date proximity, "
f"and reference text similarity. Return JSON pairs with a confidence score. "
f"Do not invent transactions not present in the input."
}]
)
The model proposes, a human confirms
Treat the model's output strictly as a ranked list of candidate matches with confidence scores, surfaced in a review queue — never as an auto-posted reconciliation. Bank reconciliation errors are exactly the kind of mistake that's expensive to unwind after a period close, so even a high-confidence suggestion should require a click to confirm rather than posting automatically.
A single bank line for a batched payroll or supplier payment run can match the sum of several GL transactions rather than one. An LLM asked to find one-to-one matches will either miss these or force a wrong one-to-one pairing. Explicitly prompt for one-to-many and many-to-one candidates, and have the deterministic layer verify that grouped amounts actually sum correctly before presenting them as a candidate — don't trust the model's arithmetic on the total.
Explaining, not just matching
Where an LLM adds real value beyond fuzzy matching is generating a plain-language explanation for why a match was proposed — "matched on amount and same-day timing; bank reference truncates the invoice number" — which turns the review queue from a wall of numbers into something a controller can approve quickly rather than re-deriving the logic themselves.
Measuring the agent's value
Track two numbers: the percentage of the unmatched residual the agent correctly proposes (measured against what a human ultimately confirms), and the time-to-close for the reconciliation period before and after. If the agent's suggestions are frequently rejected, the fix is usually better context in the prompt (transaction descriptions, vendor aliases) rather than a bigger model.
Wrapping up
Bank reconciliation automation succeeds by staying humble about what it's for — resolving the hard 5% deterministic matching couldn't, with full transparency into why, and leaving the final confirmation to a human before anything is marked reconciled. If you are stuck on something specific, reach out or keep reading through the rest of the Acumatica blog.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.