← Back

FinFlow

Every break has a reason. FinFlow finds it automatically.

Five-pass reconciliation engine over Kafka-streamed transactions. Deterministic passes handle 90%+ of breaks at high confidence. Ollama resolves the rest in plain language. pgvector remembers every anomaly so patterns don't slip through twice.

PythonApache KafkaPostgreSQLpgvectorOllamallama3.2FastAPIStrawberry GraphQLStreamlitDocker Composesentence-transformers
GitHub →

// recon_run_2024-09-01.log

The problem

[BREAK] TXN-8821 — no ledger match

amount: $4,217.50   entity: PARTNER_C   date: 2024-08-29

analyst: is this a timing issue or a real break?

analyst: pulling ledger export...

analyst: checking VLOOKUP... sorting by date...

analyst: found it — ledger says 2024-09-01

46 more breaks in the queue.

Reconciliation breaks are inevitable. Timing drift, reference typos, rounding — they happen every day across every payment system. The cost isn't the break. It's the analyst spending 20 minutes per break ruling out the obvious cases before they can see the real problem.

I built FinFlow to push the deterministic work into code. Exact matches go instantly. Timing gaps get a 3-day tolerance window. Fuzzy references run Levenshtein distance. Amount deltas get a 1% threshold pass. Only the truly ambiguous cases ever reach the LLM — and the LLM is running locally via Ollama, zero API cost.

The second layer is memory. pgvector stores a 384-dimension embedding of every anomaly explanation. When a new anomaly fires, cosine similarity finds past anomalies that looked the same. Patterns that recur get caught faster each time.

What this solves

  • Manual break triage — analysts spending hours on timing and reference noise
  • No audit trail — match decisions were undocumented and unreplayable
  • Recurring anomalies going unnoticed — no cross-run pattern memory
  • AI-or-nothing tradeoffs — LLM only runs when the deterministic stack fails

Key metrics

Match Rate94%+
Passes5
LLM Cost$0 (local)
Latency60s cycle

Architecture

Four layers, each responsible for one concern, communicating via Kafka and PostgreSQL.

01 — Ingestion
  • ·Producer generates multi-source mock transactions with realistic drift
  • ·Consumer reads Kafka, writes to PostgreSQL with checkpoint-based replay
  • ·ON CONFLICT DO NOTHING — idempotent, safe to restart anywhere
Kafkaasyncpgconfluent-kafka
02 — Reconciliation
  • ·Engine polls every 60s, pulls unreconciled IDs from last 48h
  • ·Runs five ordered passes, each narrowing the unmatched set
  • ·Writes results and audit trail — every decision is traceable
PythonPostgreSQLLevenshtein
03 — Agent
  • ·Polls completed runs every 90s for four anomaly types
  • ·Ollama llama3.2 explains each anomaly in 2–3 sentences
  • ·sentence-transformers embeds explanation → pgvector stores it
Ollamapgvectorsentence-transformers
04 — API + Dashboard
  • ·FastAPI + Strawberry GraphQL — typed queries with playground UI
  • ·REST endpoints for stats, runs, breakdowns, anomaly resolution
  • ·Streamlit dashboard: match rate trend, anomaly cards, resolve button
FastAPIGraphQLStreamlit

// live output

Streamlit dashboard

Auto-refreshes every 30s. Four panels: KPIs, run history, pass breakdown, anomaly queue.

◆ FinFlow — Reconciliation Dashboard↻ refreshed 3s ago

Match Rate

94.2%

last run

Matched

1,884

transactions

Breaks

116

unmatched

Open Anomaly

3

needs review

Total Runs

48

since deploy

// match rate trend — last 10 runs

98%95%90%85%Run 3941434547Run 48

// run #48 pass breakdown

Exact Match1243
Timing Tol.389
Fuzzy Ref.168
Amt Threshold71
AI Resolution13

AI used

0.7%

Token cost

$0.00

// open anomalies (3)

sorted by severity
HIGHvolume_spikeENTITY_A2 similar in history

Unmatched count 2.3× above 7-day rolling avg (116 vs avg 50). Possible feed interruption from SYSTEM_2 after 14:00 UTC.

🧠 Ollama llama3.2 — embedded to pgvector
MEDIUMentity_concentrationENTITY_B1 similar in history

ENTITY_B accounts for 38% of all breaks this run. Reference format change detected — "REF-" prefix dropped in 3 consecutive runs.

🧠 Ollama llama3.2 — embedded to pgvector
LOWsystematic_mismatchSYSTEM_33 similar in history

SYSTEM_3 contributing 17% of unmatched across last 4 runs. Consistent amount delta of $0.01–$0.03 suggests rounding mode change.

🧠 Ollama llama3.2 — embedded to pgvector

The five-pass engine

Each pass takes the transactions left unmatched by the previous one. Deterministic before probabilistic — the LLM only sees what nothing else could handle.

Pass 1 — Exact Matchconf 1.00

Entity + currency + amount + (transaction ID or reference). Fastest possible match, highest confidence. No tolerance applied.

100%
Pass 2 — Timing Toleranceconf 0.95

Same fields, but allows up to 3-day date difference. Catches settlement timing drift between source and ledger.

95%
Pass 3 — Fuzzy Referenceconf 0.85

Entity + currency + amount + Levenshtein distance ≤ 4 on reference strings. Catches typos, truncation, and format variants. Confidence decreases 0.02 per distance unit.

85%
Pass 4 — Amount Thresholdconf 0.80

Entity + currency + reference + amount within 1%. Catches rounding errors, FX conversion deltas, and small fee variances.

80%
Pass 5 — AI Resolutionconf 0.60–0.75

Batches of 10 unmatched transactions sent to Ollama llama3.2. Structured JSON response. Used only for true residual breaks that no deterministic pass could handle.

68%

AI anomaly detection

The agent runs every 90s after reconciliation completes. It detects four structural anomaly types, explains each one in plain language, embeds the explanation, and uses pgvector to find similar past events.

volume_spike

Unmatched count > 2× 7-day rolling average

Sudden surge in unmatched transactions — usually signals a feed interruption or upstream schema change.

Ollama explains in 2–3 sentences → sentence-transformers embeds → pgvector finds similar past anomalies (cosine sim > 0.85)

unusual_break_pattern

Break rate > 20% of run volume

The overall ratio of breaks to matched is unusually high for this run — not a volume spike, but a quality signal.

Ollama explains in 2–3 sentences → sentence-transformers embeds → pgvector finds similar past anomalies (cosine sim > 0.85)

entity_concentration

One entity > 40% of breaks

Breaks are clustering around a single counterparty — likely a formatting change on their side or a new data contract.

Ollama explains in 2–3 sentences → sentence-transformers embeds → pgvector finds similar past anomalies (cosine sim > 0.85)

systematic_mismatch

One source system > 15% of run unmatched

A specific source system is consistently failing to match — typically a field mapping regression or deployment artifact.

Ollama explains in 2–3 sentences → sentence-transformers embeds → pgvector finds similar past anomalies (cosine sim > 0.85)

// agent output

Anomaly resolution in practice

From raw unmatched transactions to Ollama explanation to pgvector similarity search to analyst resolution — the full loop.

step 1detection

# anomaly_agent.py polling run #48

unmatched_count = 116

rolling_7d_avg = 50.3

ratio = 2.31 # > 2.0 threshold

✔ anomaly detected

type: volume_spike

entity: ENTITY_A

severity: HIGH

step 2ollama explain

POST http://ollama:11434/api/generate

prompt →

Run #48 has 116 unmatched vs avg 50. Entity ENTITY_A is concentrated. Explain in 2-3 sentences.

llama3.2 →

ENTITY_A's transaction volume spiked 2.3× above the 7-day average in run #48, concentrated after 14:00 UTC. This pattern is consistent with a feed interruption or a schema change in the upstream SYSTEM_2 connector. Recommend checking SYSTEM_2 delivery logs for the 13:45–15:00 UTC window.

step 3pgvector + resolve

similar anomalies (cosine > 0.85)

#310.94SYSTEM_2 outage18d ago
#190.88Schema migration41d ago

analyst resolves

resolution_note:

Confirmed SYSTEM_2 connector restart at 13:52 UTC. Replay from offset 41820 initiated.

✔ Mark Resolved
Snooze 1h

Design decisions

The choices that shaped the architecture.

Deterministic before probabilistic

Passes 1–4 use exact logic — no LLM involved. This means 90%+ of matches carry confidence ≥ 0.80 with zero token cost. Ollama is reserved for true residual ambiguity, keeping costs at zero and latency predictable.

Kafka checkpoint replay

The consumer stores (topic, partition, offset) in PostgreSQL. On restart, it replays from the last committed offset. Combined with ON CONFLICT DO NOTHING inserts, the pipeline is fully idempotent — safe to kill and restart at any point.

pgvector as anomaly memory

Every anomaly explanation is embedded to 384 dimensions via sentence-transformers and indexed with an IVFFlat cosine index. This lets the agent ask 'have we seen something like this before?' across all history in a single SQL query.

GraphQL API

Strawberry GraphQL at /graphql — playground UI included. Typed queries over reconciliation runs, breaks, and anomalies.

query

# open anomalies + pass breakdown for run #48

query {

anomalies(status: "open") {

id anomalyType severity

explanation similarCount resolvedAt

}

matchBreakdown(runId: 48) {

passType matchedCount confidence

}

}

response — 200 OK — 42ms

{"data": {

"anomalies": [

{

"id": "ANO-0041",

"anomalyType": "volume_spike",

"severity": "HIGH",

"explanation": "ENTITY_A volume 2.3× above 7d avg...",

"similarCount": 2,

"resolvedAt": null

} ...

],

"matchBreakdown": [

{ "passType": "exact_match", "matchedCount": 1243, "confidence": 1.0 },

{ "passType": "timing_tolerance", "matchedCount": 389, "confidence": 0.95 },

{ "passType": "ai_resolution", "matchedCount": 13, "confidence": 0.71 }

]

}}