Skip to content
Last updated: Sep 25, 2026

Cash Application Intelligence — CID & Matching Engine Reference ​

OwnerClassificationVersionEffectiveNext reviewStatus
Platform EngineeringInternal1.12026-09-042026-12-04Draft

Purpose. The single reference for how cash application decides — customer identification (CID) and the four matching legs — across both execution substrates (batch/bulk and online/live). It documents the shared configuration contract, every rule, the confidence model, the operator vocabulary, and — honestly — where the two engines agree and where they do not. This supersedes the scattered CASHAPP-INTELLIGENCE.md / CASHAPP-RULE-PARITY.md working notes.

For operating the engine (how to trigger a run, prerequisites, confirming a run), see Run the Cash Application Engine. This document is the rule-level companion that guide defers to.


1. Two engines, one config ​

Cash application identifies and matches on two substrates, by design, for performance — not two products, but two implementations of one decision model:

Batch / BulkOnline / Live
TriggerRun Engine action / schedule (cashapplication workflow)Smart ID on a single payment
SubstrateSet-based DuckDB SQL over the whole eligible populationPer-item Postgres, EntityResolutionService.suggest()
LatencyMinutes (full dataset)Sub-second
CID implementationHand-written DuckDB SQL stages (activities.ts)TypeScript rule handlers (entityresolution.service.ts)
Matching implementationGenerated SQL (matching-sql-generator.ts)MatchingEngineService (shared predicates)
Learns aliases✅ writes back confirmed aliases❌ suggest-only
Invokes Layer-2 judge✅ on ambiguity❌ caller decides

The shared contract. Both engines read the same source of truth:

  • finbase.matching_config — one row per engine leg (CASH-CID, CASH-CL-BANK, CASH-RA-BANK, CASH-BANK-RA, CASH-RL-INV). Holds auto_match_threshold, suggest_threshold, source_entity/target_entity, max_candidates.
  • finbase.matching_rule — one row per rule (code, tier, confidence_score, cardinality, conditions/metadata, execution_mode).
  • cid-config.ts — the CID tuning constants, every one env-overridable.

Because the config is shared, thresholds and rule catalogs are common to both paths. But the implementations are two separate codebases, and their coverage of that catalog is not identical — see §6 Parity. Treat that section as the authoritative answer to "why did live and bulk disagree?"

Engine inventory (as configured) ​

Config codeFlowRulesAutoSuggest
CASH-CIDbank item → customer188550
CASH-CL-BANKbank item → company ledger (invoice match, "MR")299550
CASH-RA-BANKbank item → remittance advice (Smart Link, "RR")129550
CASH-BANK-RAremittance advice → bank item (reverse link, "BR")89550
CASH-RL-INVremittance line → company ledger (line match, "LR")129550

79 rules total. (The old "24 MR + 12 RR" note is stale — the invoice-match leg is now 29 rules.)


2. The CID engine ​

CID answers "whose money is this?" — resolving a bank item to a customer. 18 rules across 4 tiers, evaluated tier-by-tier; within a tier, rules run in parallel and their scores combine.

The 18 CID rules ​

TierCodeConfRuleexec_mode
1CID-00195Exact Name Matchalways
1CID-00298Bank Account Matchalways
1CID-00395Customer Number in Referencealways
1CID-00490Known Alias Lookupalways
1CID-01497Tax ID Matchalways
1CID-01892Lockbox: invoice # → invoice → customeralways ¹
1CID-02196Amount disambig: subsidiary with $-matching open ARalways ¹
2CID-00588Invoice Number in Referencealways
2CID-00690Remittance Advice Matchalways
2CID-00785PO Number in Referencealways
3CID-00875Amount to Open Invoicealways
3CID-00980Amount + Date Patternalways
3CID-01082Recent Payer Historyalways
3CID-01570Address Matchalways
3CID-01678ZIP + Amount Comboalways
4CID-01180Normalized Name Matchalways
4CID-01272Token Overlap Match (fuzzy)bulk_only ²
4CID-01370Email Domain Matchalways

¹ always in config, but the live engine ships no handler for CID-018 / CID-021 — by design, not oversight: both are set-based / join-heavy (lockbox invoice→invoice→customer resolution; scanning open AR across subsidiaries to disambiguate) — exactly the full-table scans the live path is engineered to avoid. They run batch-side only (see §6). ² bulk_only: the live handler exists but is skipped at runtime — token-overlap fuzzy is ~40 s/batch, too slow for a per-click path (entityresolution.service.ts:852).

Bulk-only extras (code-only, not in the rule catalog): the DuckDB engine adds CID-019 / CID-019-LIKE (match against Razor-enriched customername) and CID-022 (remittance-driven CID — resolve the payer from a linked remittance's invoice references). These have no matching_rule row and no live equivalent.

CID confidence model ​

Governed by cid-config.ts (all env-overridable):

ConstantDefaultMeaning
SHORT_CIRCUIT_CONFIDENCE95A candidate ≥ this stops tier evaluation — no lower tier runs
AMBIGUITY_GAP_THRESHOLD15Top-two candidates within this gap → ambiguous → route (Layer-2 in bulk)
AMBIGUITY_CONFIDENCE_FLOOR50Below this, don't even treat as a suggestion
AMOUNT_MATCH_MAX_CUSTOMERS3Amount-only rules abstain if > N customers share the amount (anti-noise)
MULTI_RULE_BONUS10Per additional rule that independently hits the same candidate
TOKEN_OVERLAP_MIN0.6Min Jaccard-style token overlap for CID-012 to fire
FUZZY_MAX_DISTANCE3Max edit distance for fuzzy name rules
CONTEXT_ENRICHMENT_LIMIT3Max context rows pulled to enrich a candidate

Scoring flow: run tier 1 → if any candidate ≥ SHORT_CIRCUIT_CONFIDENCE, stop; else tier 2, 3, 4. A candidate hit by multiple rules gets base + (n−1) × MULTI_RULE_BONUS. Decision against the CASH-CID thresholds: ≥ 85 → auto-identify, ≥ 50 → suggest, < 50 → no call. If the top two are within AMBIGUITY_GAP_THRESHOLD, the result is ambiguous: bulk hands it to the Layer-2 judge; live returns the ranked suggestions and lets the caller decide.


3. The matching legs ​

Once the payer is known (or a remittance is in hand), matching answers "which open items does this money clear?" Four legs, all config-driven — both engines build their predicates from the same matching_rule rows, so these legs are genuinely unified.

CASH-CL-BANK — invoice matching ("MR", 29 rules) ​

Bank item → company ledger. The core clearing leg.

TierCodesCharacter
1 (90–95)MR-001…009, MR-028, MR-029Reference + amount — invoice/PO/cheque/receipt ref found in a narrative field with the amount agreeing. Highest trust.
2 (70–95)MR-010…016, MR-026, MR-027Customer + amount, incl. tolerance bands and adjusted amounts (bank charges, withholding tax, payment-terms discount).
3 (50–60)MR-017…020Fuzzy / weak — levenshtein ref, amount-only + date proximity, token overlap.
4 (60–85)MR-021…025Many-to-one / one-to-many — multiple refs in one narrative, invoice-sum = payment, one invoice across several payments.

Link a remittance advice to the bank item that paid it (and the reverse). Same shape as MR but keyed on payment/document number, payer account, and customer identity, decaying to amount-only + date proximity at tier 3.

CASH-RL-INV — line matching ("LR", 12 rules) ​

Remittance advice line → company ledger. Resolves each remitted line to a specific open invoice: exact/ERP ref (tier 1) → normalized ref (tier 2) → fuzzy ref (tier 3) → amount+date (tier 4). Rules come in with-amount and no-amount-check variants so a reference-certain line still matches when the remitted amount is a partial or grouped figure.

Cardinality ​

matching_rule.cardinality drives join shape: 1:1 (hash join, one item ↔ one target), N:1 (many open items sum to one payment — sliding-window aggregation via sum_equals), 1:N (one invoice across several payments).


4. Operator vocabulary ​

Every matching rule is a set of conditions over these operators (matching-sql-generator.ts). The same vocabulary compiles to DuckDB SQL (bulk) and to MatchingEngineService predicates (live).

OperatorMeaning
equalsexact equality (after transforms)
containssubstring (e.g. invoice ref inside a narrative)
abs_equalsabsolute-value equality (sign-agnostic amounts)
within_tolerancenumeric within a band — percent, absolute, or mixed
levenshteinedit distance ≤ threshold (fuzzy reference)
token_overlapJaccard-style token overlap ≥ threshold (fuzzy name)
date_withindates within N days
field_adjustedcompare after adjusting one side (bank charges, WHT)
terms_discountamount agrees after applying a payment-terms discount
sum_equalsaggregate of grouped rows equals target (the N:1 engine)
is_null / is_not_nullpresence checks

Field transforms applied before comparison: lowercase, uppercase, trim, normalize_ref (strip -, ., spaces + lowercase), abs, strip_alpha (digits only), ltrim_zero / ltrim_zero_lower (drop leading zeros).


5. Decision, clearing & trace ​

  • Thresholds are per-leg on matching_config: auto_match_threshold, suggest_threshold. Above auto → apply (STP); between suggest and auto → propose for review; below suggest → no action.
  • STP / clearing. Clean, unambiguous, above-auto matches auto-clear. Ambiguous or below-auto route for approval. Conflicts — one open item claimed by multiple payments — are downgraded to suggestions in a dedicated conflict-detection pass so two payments never auto-clear the same invoice.
  • Match states — Auto-Matched is not Cleared. The engine drives a payment to Matched / Auto-Matched (a confident, ready-to-apply match). Posting the clearing entry (clearing transaction + journal + AR reduction) is a separate apply step — the bulk engine does not auto-post it. So "straight-through" in the KPI means matched (matchstatus ∈ {Matched, Cleared}), and a zero-touch payment typically rests at Auto-Matched until applied.
  • N:1 and the auto threshold. Individual N:1 rules are low-confidence on their own (MR-021 multi-ref = 85, MR-022/MR-025 = 70/60 — all below the 95 auto threshold). A genuine N:1 reaches auto by stacking: several N:1 rules hit the same combination and the MULTI_RULE_BONUS lifts the combined score to ~99. A bare amount-only N:1 (no reference) tops out at 85 → suggestion, not auto.
  • Remittance confirmation gates N:1 clearing. A reference/amount N:1 match with no remittance parks at wf = Awaiting Remittance (conservative: don't straight-through a multi-invoice payment on amount alone). Linking a confirming remittance moves it to Auto-Matched. So a "zero-touch STP" N:1 needs both the invoice references and a remittance.
  • Remittance candidate filter + ambiguity. The payment→remittance matcher (CASH-RA-BANK) only considers remittances with paymentstatus ≠ 'Matched' (a consumed remittance silently disappears from candidates — check paymentstatus, not the status column). When two candidates both score ≥ 95 within the ambiguity gap, the result is a multi-suggestion — the engine presents both and the analyst picks (e.g. the same payment remitted via two channels).
  • Trace. Every decision writes to finbase.intelligencelog (per-rule hits, scores, the winning candidate, and comboAlternatives for N:1). This is what the UI match/CID chips read; it is the audit trail for "why this outcome". A DuckDB preload of intelligencelog is required for line-level intel to populate.
  • Idempotency. Re-running is safe: bulk write-back is keyed so a second run reconciles rather than duplicates. Extraction ≠ matching — re-running one does not refresh the other.

6. Bulk vs live parity — the honest picture ​

The config layer is unified; the implementations are two codebases with deliberately different coverage — the live path is tuned for sub-second latency and therefore omits the set-based, scan-heavy rules that only make sense in a batch pass. This is the authoritative answer to "why did the queue and the UI disagree?" — and the answer is usually "as designed", not "a bug".

AreaStatusDetail
Config (thresholds, catalogs, cid-config)✅ UnifiedSingle source of truth; both engines read it
Matching legs (MR / RR / BR / LR)✅ UnifiedBoth compile the same matching_rule rows through the shared operator vocabulary
CID tiers 1–3 (minus 018/021) + CID-011, 013✅ AlignedImplemented in both, same thresholds
CID-018 (Lockbox), CID-021 (Amount-disambig)✅ Batch-side by designSet-based / join-heavy (invoice→customer resolution; cross-subsidiary AR scan) — too expensive for the sub-second live path, so run batch-only. Config label is the only mismatch (says always, behaves bulk-only).
CID-012 (Token Overlap)✅ Batch-side by designexecution_mode = bulk_only; live handler exists but is skipped — fuzzy token overlap is ~40 s/batch, too slow per-click
CID-019 / 019-LIKE / 022✅ Bulk-only extrasRazor-enriched name + remittance-driven CID; DuckDB stages with no catalog row and no live equivalent (batch-scoped by nature)
CID-012 confidence formula🟡 LatentLive 70 + score×15, bulk 70 + score×30 — inconsistent, but moot because live never runs CID-012
Alias learning⚠️ Bulk-onlyBulk writes back confirmed aliases; live is suggest-only
Layer-2 judge⚠️ Bulk-onlyBulk invokes it on ambiguity; live returns suggestions for the caller to judge

Net: a bank item with a strong tier-1–3 signal resolves the same on both paths. Bulk resolves more — it also runs lockbox, amount-disambiguation, fuzzy token overlap, the remittance-driven and Razor-enriched rules, and it learns. So the correct expectation is not "identical", but "live is a fast, high-precision subset; bulk is the complete engine."

Validated against the demo scenarios (2026-09-04) ​

Both paths were run over the 6 CID demo scenarios (bank items 9001–9006). CID behaved as documented on 6 of 6, and the live-vs-bulk differences matched this model exactly:

ItemSignalLive (per-click)Bulk (batch)
9001name embedded in a wire narrative ("…METRO DISTRIBUTION LLC…")suggest @75 (CID-004 alias-substring)auto-identify (CID-001)
9002alias "ACME CORP" in narrativesuggest @75 (CID-004)suggest (CID-004) — agree
9003customer # CUST005 in referenceauto @95 (CID-003)identify + match (CID-003)
9004amount $7,777 matches two subsidiariesambiguous → route (CID-008)route, pending ID (CID-008) — agree
9005–9006no usable signalnoneno candidate — agree

The 9001 divergence is instructive: even CID-001, a rule both engines implement, is not semantically identical — bulk's SQL matches the customer name embedded in a longer narrative, while live's handler requires the narrative to equal the name and so falls through to CID-004 (suggest). This is the same "bulk resolves more" principle at the rule-implementation level, not just the rule-catalog level. It is worth aligning if per-click auto-identification of embedded names is desired online.

Open follow-ups (housekeeping, not correctness) ​

  • Truth-in-config: mark CID-018 / CID-021 as execution_mode = bulk_only so the catalog matches their intentional batch-only behaviour (they read always today, which misleads).
  • Bulk hardcodes the CID-012 token-overlap floor at 0.6; live reads the env-overridable TOKEN_OVERLAP_MIN. Align bulk to the constant for config parity.
  • Reconcile the CID-012 confidence formula (15 vs 30) even though it is currently moot, to avoid a surprise if CID-012 is ever promoted to always.

7. Config knobs (where to tune) ​

To change…Edit
A rule's confidence, tier, or predicatefinbase.matching_rule (that leg's rows)
Auto / suggest thresholds for a legfinbase.matching_config.{auto,suggest}_match_threshold
Whether a rule runs live, bulk, or offmatching_rule.metadata.execution_mode (always / bulk_only / off)
CID short-circuit, ambiguity gap, multi-rule bonus, token floor, etc.cid-config.ts constants or their CID_* env vars

Revision history ​

VersionDateAuthorChange
1.02026-09-04Platform EngineeringInitial canonical reference — CID (18 rules + bulk-only extras), 4 matching legs (MR/RR/BR/LR, 79 rules total), operator vocabulary, confidence model, and a code-grounded bulk-vs-live parity matrix. Supersedes the CASHAPP-INTELLIGENCE.md / CASHAPP-RULE-PARITY.md working notes.
1.12026-09-04Platform Engineering§5 extended with match-state and remittance behaviors surfaced during demo-scenario validation: Auto-Matched ≠ Cleared (clearing is a separate apply step), N:1 reaches the auto threshold only by multi-rule stacking, remittance confirmation gates N:1 clearing (Awaiting Remittance → Auto-Matched), and the paymentstatus remittance-candidate filter + ambiguity → multi-suggestion.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.