Document matcher
Source: agent/src/accounting_agent/domain/matching.py (the pure
scorer) + agent/src/accounting_agent/reconcile/document_matcher.py
(the doc-side caller, ingest pipeline).
The bank-side caller is the EB poller.
API
score = score_match(movement, candidate)
# MatchScore(candidate_id, kind, score, signals)
signals carries which signals fired (reference_match,
amount_match, date_proximity_days, counterparty_match,
card_debit_boost) so HITL prompts can show the operator why
the agent thinks they're a pair.
Scoring details are in Matching.
Two callers, same scorer
Bank-side (EB poller)
Every poll tick. For each new or unmatched recon row, walk the
candidate documents in the date+currency window. Promote proposed
when the top score clears HITL.
Doc-side (ingest pipeline)
Every time a new document is indexed. Walk the unmatched recon
rows in the date+currency window. Promote proposed on the same
condition.
Both paths land at the same end state in Turso. The state machine
prevents collisions (the set_reconciliation_proposed write is
gated on status='unmatched', so whichever caller wins, the other
silently no-ops).
The card-debit boost
Card debits never carry the invoice number in their bank description. Without help, they score ≤ 0.45 and stay below HITL forever. The boost adds +0.15 when:
reference_hitis False (substitutes for the missing reference signal)amount_exact(zero-cent diff)counterparty_hit(matches by token overlap)days_off ≤ 2
Tuned for Wise / Revolut / similar card-issuing fintechs. The
boost is gated on not reference_hit so cases where the reference
IS present don't inflate past 1.0.
See commit ad5ac8f for the rationale.
Indexed narrowing
For pools with 50+ candidates the matcher includes a MatchIndex
that buckets candidates by bank_ref_number and amount bucket.
For 30 transactions × 50 candidates this drops score_match calls
from ~1500 to ~150. The narrower's tolerance is intentionally a
hair wider than the scorer's so it never drops a candidate the
scorer would have admitted.
Pattern ported from iseppo/e-arveldaja-mcp's buildInvoiceIndex
+ getIndexedCandidates. See ADR-037.
Calibration
The thresholds (HITL=0.55, AUTO_ACCEPT=0.85) are calibrated for
Estonian OÜ scale — a handful of receipts per month, EUR-
heavy, mostly EU vendors. If you change the signal weights, also
change the thresholds: they're a system, not independent knobs.
Pinned by tests/domain/test_matching.py.