All projects

Case study · 4 min read

SEFA

An AI budgeting app that proposes, and never acts without your say-so.

Three SEFA app screens: the read-only bank connection prompt, the assistant home and an assistant answer about monthly spending
My role
Designed and built end to end: React Native app, Node/Express API and marketing website.
Timeline
Feb - Sep 2026
Stack
React Native, Expo, TypeScript, Node.js, Express, MongoDB, Redis, Bull, Azure OpenAI, LangGraph, Mono API, WebSockets

SEFA is a mobile-first personal finance app for Nigerian users: budgets, read-only bank sync, bank-statement import and an AI assistant. Every AI write and every imported row waits for the user to confirm. It was my final-year project at Bowen University and is the subject of a published preprint.

The problem

Tracking money in Nigeria usually means scrolling bank statements and typing expenses by hand. Existing tools either ask for too much trust (full banking access) or put an AI chatbot in front of your finances with no guard rails.

I wanted to know whether an AI assistant could be genuinely useful for budgeting while staying trustworthy: it never moves money, never writes data you did not approve, and is never a black box about what it did.

Constraints

  • Money is high-stakes. A wrong category is an annoyance; a wrong write is a breach of trust.
  • Messy input. Statements arrive as PDFs, scans, CSV and Excel, with different layouts.
  • Low-cost hosting and one developer. The API runs on Render, which sleeps when idle, and the scope had to fit a semester.

Approach

SEFA is three pieces: a React Native app (Expo, TanStack Query, Zustand), an Express/MongoDB API with Bull queues on Redis, and a Next.js marketing site. The slow work (statement extraction, bank sync, notifications, assistant replies) runs in background workers, and Azure OpenAI powers extraction and a LangGraph tool-calling assistant.

One idea runs through all of it, a review gate: the system prepares work and a person approves it.

System overview

The app never talks to Mono or the model directly; the API and its workers sit in between.

In the app

Read-only is stated before the user links a bank.
Assistant home with suggested questions.
A spending summary that flags a late-night transfer worth double-checking.

Key decisions

01The AI proposes, the user confirms

Context
A model that writes straight to the database can be wrong with no way back.
What I chose
Write tools only prepare an action. It moves through pending_fields, pending_confirmation, confirmed, then executed (or cancelled / failed), and the user confirms, edits or cancels it in the app. Stale actions expire after 24 hours.
What I passed on
Let the model write and offer an undo. Faster to build, but every mistake becomes a data-repair problem.
Trade-off
One extra tap per write. Read-only questions run without confirmation, so everyday use stays quick.

02Read-only banking, enforced in layers

Context
Linking a bank is where users are most nervous. "We promise not to move money" is not a design.
What I chose
Through Mono the app can read accounts and transactions, and disconnect. Nothing else, enforced four ways: a frozen allow-list of operations, a router guard that returns 403 for anything that looks like a transfer, withdrawal or payment, a connection model whose access mode can only be "read_only", and an append-only audit log. Tokens are encrypted at rest.
What I passed on
One check in the service layer, where a single forgotten route could bypass it.
Trade-off
Plumbing the user never sees, accepted because getting it wrong would cost the whole product.

03Statement import: extract, score, de-duplicate, then ask

Context
Statements range from clean PDFs to blurry scans, and they overlap with transactions already synced from the bank.
What I chose
Text extraction is quality-scored (accepted at 0.7 or above) with OCR fallbacks, and a separate vision path sends page images to an Azure OpenAI vision model that returns strict JSON. Rows start as "needs review" and become "ready" only at category confidence 0.6 or above. Duplicates are caught in three layers: external transaction id, a SHA-256 hash of the row, and a same-day fuzzy match against existing records.
What I passed on
Hand-written parsers per bank, which are precise for one bank and brittle for the next. They still exist and are tested, but the pipeline moved to model-based extraction.
Trade-off
Vision calls are slow and cost money, so they run in a queue with a 40-page cap. The fuzzy layer can flag a genuine repeat purchase, so it marks rows for review instead of deleting them.

04Queues for slow work, a WebSocket for the chat

Context
Extraction, sync and AI replies take seconds to minutes, which is too long for a request on a host that sleeps.
What I chose
Four Bull queues (bank sync, notifications, statement imports, assistant) with three attempts and exponential backoff; cron syncs accounts every two hours and retries failures every thirty minutes. Assistant replies stream over a token-authenticated WebSocket, replacing the earlier SSE route.
What I passed on
Doing the work inline, or keeping SSE only.
Trade-off
More moving parts (Redis, workers) to run, and two streaming transports until SSE is retired.
Statement import: nothing is saved until you approve it

Every route ends at the user. Rows the system is unsure about are flagged instead of guessed.

Lifecycle of an assistant action

A write only happens in the transition from confirmed to executed. A TTL index removes stale actions after 24 hours.

Challenges

  • Latency and crashes. Early assistant replies were slow and one bug failed every response; both were fixed in the backend history.
  • Slow vision calls. They are far slower than a 30-second client default, so they get a longer single attempt with no automatic retry that would double the wait.
  • Messy data. Amount validation went through several commits before real statements stopped producing odd rows.

Results

On a 300-transaction synthetic benchmark, reported in the preprint: 99.49% parser field agreement, 78.40% clear-category accuracy and 1.000 duplicate-detection F1.

The product runs end to end: Android builds ship through Expo's build service, the website is live, and 29 Jest test files cover the assistant, bank security, insights and the statement pipeline. The part I am proudest of is the review gate: in every flow, the worst a model mistake can do is appear as a row waiting for approval.

What I would do next

  • Run the benchmark in CI as accuracy regression tests; today the evaluation lives in the paper, not the build.
  • Retire the SSE route and add queue monitoring and alerts.
  • A Telegram-first rewrite is planned around the same review-first idea, on a Node monorepo with PostgreSQL and Prisma instead of MongoDB.

Contact

Exploring a new product idea? Looking for ways to improve your product? Send a mail to: