FinSight
Turning a messy bank statement into clean rows, for a market that bank-linking APIs do not serve.
Architecture, backend, AI pipeline, frontend integration, payments and deployment. The marketing site predated the build; everything from the API surface inward is mine.
How it holds together
Step 1 of 7
- UploadPOST /upload
- CSV parsepandas · normalize
- Markdown tablerow/column kept
- Batchgroups of 40
- CategorizeGroq · LLaMA 3.3 · json
- PersistSupabase Postgres · RLS
- Chat + analysisRAG · SSE stream
The parser branches on file type here. Everything downstream depends on which branch recovers usable structure.
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.
The 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.

The architecture
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
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.
Stack
- Backend
- FastAPI · Python 3.11 · Railway
- Model
- Groq · LLaMA 3.3 · JSON mode · LlamaIndex RAG
- Parsing
- pdfplumber · pymupdf · pandas
- Data & auth
- Supabase Postgres · Row-level security · Google OAuth · JWT middleware
- Payments
- Paystack · HMAC-SHA512 webhooks · NGN + USD
- Interface
- Next.js App Router · Recharts · SSE · Vercel
What it demonstrates
- 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