Plate IV · Prototype

TasdeeqPay

Zero false positives, by design.

An OCR + LLM pipeline that verifies manual bank transfers automatically — built around one rule: never confirm a payment that didn't happen.

Vite · React 19 · Express · Supabase · Redis · Gemini OCR · Android

3
Match sources
0
False-positive target
LLM-based
OCR
Outbox pattern
Mobile delivery
Overview

Pakistani merchants reconcile manual bank transfers by hand: a customer pays, screenshots the receipt, and someone eyeballs it against the order and the bank alert. It's slow, and the errors are asymmetric — a wrong 'paid' means you shipped for free.

TasdeeqPay automates that reconciliation with a deterministic engine and a hardened mobile pipe, and it's tuned so the expensive mistake can't happen. It's a working prototype of the core; productionisation is in progress.

The system in motion

Animated architecture breakdown — nodes and data paths resolve in sequence.

Full architecture blueprint
TasdeeqPay detailed architecture blueprint
Three-source cross-match — order data, the OCR'd screenshot and the bank alert must all agree before a payment is confirmed; anything less is held. · open full-size ↗
The stack — and what each part does
Vite + React 19Merchant dashboard
ExpressVerification API + matching engine
SupabaseOrders, payments and merchant data
RedisQueueing / fast lookups in the match path
Gemini OCR (LLM)Reads payment screenshots into structured fields — generalises across bank formats
Android companion (Kotlin/Flutter)Forwards bank alerts with an outbox pattern
Under the hood

Three sources, one agreement

The engine trusts no single input. It cross-matches three independent signals — the order (what's owed, to whom), the customer's payment screenshot read via OCR, and the merchant's own bank alert. It confirms only when all three agree. Partial agreement isn't a 'probably'; it's a hold for human review.

Bias toward the cheaper error

Every classifier makes two kinds of mistake, and in payments they aren't equal. A false positive — confirming a payment that didn't clear — costs the merchant the value of the goods. A false negative — missing a real payment — costs one manual check. So the system is tuned to eliminate the first at the expense of tolerating the second.

Let the model read, not a brittle parser

Every bank formats statements differently. A hardcoded parser per bank breaks the moment a layout changes; an LLM extracts the fields and generalises to new formats instead of throwing. The determinism lives in the matching logic; the flexibility lives in the reading.

Getting the alert off the phone

Bank alerts arrive on an Android phone, and Android OEMs aggressively kill background apps. The companion app uses an outbox pattern hardened against exactly that: the alert is persisted the instant it arrives and forwarded reliably, not best-effort. Correctness at the core is worthless if the pipe feeding it drops messages.

Decisions worth defending
Why zero-false-positive over catching everything?
In payments a false 'confirmed' loses the merchant real money; a missed match just needs a manual check. You bias the system toward the cheaper error every time.
Why an LLM for OCR instead of a parser?
A parser-per-bank is brittle and breaks on layout changes. An LLM reads the fields and generalises to formats it hasn't seen, so onboarding a new bank isn't a code change.

Proof. Working prototype of the OCR + three-source cross-match core; productionisation in progress.