FinSight
Turning a messy bank statement into clean rows, for a market bank-linking APIs do not serve.
- Period
- 2025 — 2026
- Role
- Sole engineer
- Status
- Live
Architecture, backend, AI pipeline, frontend integration, payments and deployment. The marketing site predated the build; everything from the API surface inward is mine.
The problem
Bank statements are one of the richest personal datasets most people own and one of the least usable. They arrive as CSVs with inconsistent column names, or as PDFs where the transaction table is a visual artifact rather than structured data. Getting anything out of them means manual tagging in a spreadsheet, which almost nobody sustains past the first month.
For Nigerian users the existing tools are worse than tedious. Western personal-finance apps depend on bank-linking APIs with poor or no coverage for Nigerian banks, price in USD only, and categorize against merchant vocabularies that do not recognize local transaction descriptors.
Why it mattersThe gap is not analytical sophistication. Once transactions are normalized and labeled, category breakdowns, trend lines and outlier detection are cheap. The whole product hinges on turning a messy document into clean rows without asking the user to do it — for a market the incumbent ingestion method cannot reach at all.
What I built
Upload a statement as CSV or PDF and get back categorized transactions, monthly trend charts, anomaly flags on unusual spending, a chat interface answering natural-language questions against your own history, and an on-demand savings report.
Behind it: authentication, per-user data isolation, a freemium paywall with Paystack billing in NGN and USD, and a documented two-platform deployment.
How it holds together
The ingestion path. The markdown re-serialization step is the one that decides whether the model can reason about the table at all.
A monorepo with backend and frontend as separate deployment targets. Files hit POST /upload and the parser branches on type — CSV through pandas with column normalization, PDF through pdfplumber for structured table extraction with pymupdf as a raw-text fallback when there is no recoverable table structure.
Extracted tables are re-serialized as markdown before reaching the model. It preserves the row and column relationship the model needs in order to know that a number is an amount and not a date.
Isolation lives in the database. Supabase Postgres with row-level security policies, and FastAPI middleware validating Supabase JWTs on every request — the guarantee is not in application code.
Anomaly detection runs z-scores over per-category monthly baselines, so it flags genuine outliers rather than large absolute amounts.
Decisions
- 01
Naive chunking produced plausible garbage
Fixed-size chunking splits transaction tables mid-row. Retrieved context then contains an amount orphaned from its date and description, and the model answers confidently and wrongly. Chunking semantically around section headers with row boundaries as hard splits fixed it. The failure never surfaced as an error — only as answers that looked right.
- 02
LLM-only categorization, no rule table
The obvious design is a rule-based first pass on known merchant strings falling back to the model for ambiguous entries. I went model-only. A rule table is a maintenance liability that needs constant curation to stay useful, and with Groq's inference speed and 40-transaction batching the latency difference did not justify it.
CostA per-upload token cost, permanently, in exchange for zero rule maintenance. A tradeoff, not a free win.
- 03
Currency detection without a geo-IP dependency
Rather than adding a third-party lookup and its latency and failure mode, currency defaults are inferred client-side from navigator.language and an Africa/Lagos timezone check. The part that makes an imperfect heuristic acceptable is the override toggle sitting next to it.
- 04
Authorization has to resolve before the first byte
Gating a streamed SSE endpoint is not the same as gating a request. You cannot retroactively return 402 on a response that has already started streaming, so the entitlement check has to complete before the stream opens.
- 05
Cutting weight the architecture never needed
Deployment broke on ML dependency wheels unbuilt for Python 3.13. I pinned to 3.11 via runtime.txt and, separately, stripped torch and HuggingFace embeddings out of requirements entirely rather than carrying heavyweight local inference for a system that calls a hosted model.
Stack
- FastAPI
- Python 3.11
- Railway
- Groq
- LLaMA 3.3
- JSON mode
- LlamaIndex RAG
- pdfplumber
- pymupdf
- pandas
- Supabase Postgres
- Row-level security
- Google OAuth
- JWT middleware
- Paystack
- HMAC-SHA512 webhooks
- NGN + USD
- Next.js App Router
- Recharts
- SSE
- Vercel
- Structured extraction from genuinely unstructured documents
- RAG over private user data with per-tenant isolation enforced in the database
- Streaming inference behind an entitlement check
- Real payment integration with signature-verified webhooks
- Market-specific engineering where the incumbent approach does not reach