System Design ยท v1

How Spendly
actually works.

A complete walkthrough of the architecture โ€” from a WhatsApp message to a saved transaction.


Overview

System Architecture

User
โœˆ
Telegram
@SpendlyFinBot
๐Ÿ’ฌ
WhatsApp
+91 70933 81983
๐ŸŒ
Web Dashboard
spendly.info/app
Adapters
telegram.js
Webhook handler
whatsapp.js
Meta Cloud API
REST API
Express routes
Core
intent.js
Pre-LLM classifier
record.js
Save transactions
query.js
NL questions
command.js
Bot commands
AI
๐Ÿค–
Claude Haiku
Expense parsing
๐Ÿง 
Claude Sonnet
NL queries + context
Data
๐Ÿ—„๏ธ
Supabase
Postgres DB
๐Ÿ”
Identity Hash
HMAC-SHA256
๐Ÿ’พ
Context Store
In-memory ยท 30min TTL

Data Flow

A message, step by step.

01

Message received

User sends coffee 80 upi on Telegram or WhatsApp. Webhook fires to the Railway backend instantly.

02

Identity hashed

The Telegram ID or WhatsApp phone number is passed through HMAC-SHA256 with a secret key. Only the hash is stored โ€” the real identifier never touches the database.

03

Intent classified

intent.js runs a pre-LLM regex classifier. ~70% of messages (greetings, commands, clear expenses) are handled without calling Claude at all โ€” reducing cost and latency.

04

AI parses the transaction

Claude Haiku extracts amount, category, description, date, payment method, and expense type. Returns a structured JSON array โ€” even for multi-entry messages.

05

Saved to Supabase

Transaction saved under the hashed user ID. If a default payment method is set, it's applied before insert. A batch_id groups multi-entry messages for atomic undo.

06

Confirmation sent

User sees: ๐Ÿ’ธ Expense saved ยท โ‚น80 ยท Food ยท Coffee ยท UPI. Budget alert fires asynchronously if they're approaching a limit.


Design Decisions

Why we built it this way.

01

Channel adapter pattern

Telegram and WhatsApp are separate adapters over a shared core. Adding a new platform means writing one adapter โ€” all business logic stays unchanged.

02

Two-tier AI

Haiku handles fast, cheap parsing. Sonnet handles smarter conversational queries. Right model for the right job โ€” ~10x cost difference between the two.

03

Pre-LLM intent classifier

A regex-based classifier runs before Claude. Greetings, slash commands, and bare numbers are handled instantly โ€” no API call, no latency, no cost.

04

Conversational context

Last 3 exchanges stored in memory with a 30-minute TTL per user. Enables natural follow-ups like "how about entertainment?" without re-stating context.

05

Identity hashing

Real Telegram IDs and phone numbers are never stored. HMAC-SHA256 with a server secret produces an irreversible hash โ€” we literally cannot identify users from our own DB.

06

Magic link auth

No passwords. A one-time token (15 min TTL) is issued via the bot. Clicking it creates a 7-day session. Identity bridges seamlessly from bot to web dashboard.

07

Multi-entry + batch undo

The parser always returns an array. Multiple expenses in one message get a shared batch_id โ€” so /undo removes the entire batch atomically.

08

Custom categories

The AI receives a suggestion list but isn't constrained by it. Users can specify any category โ€” "laptop under Electronics" โ€” and the AI honours it exactly.

09

Reminders as a first-class feature

Natural language reminder setting โ€” "electricity bill due 10th remind me" โ€” parsed by Claude, stored separately from transactions. One-time and recurring reminders supported. Freemium gate built in from day one via a plan column on the users table.


Tech Stack

What it's built with.

AI
Anthropic Claude (Haiku + Sonnet)
Haiku for parsing speed/cost, Sonnet for conversational intelligence
Backend
Node.js + Express
Lightweight, webhook-friendly, async-first
Database
Supabase (PostgreSQL)
Managed Postgres with instant REST API, free tier generous
Messaging
Telegram Bot API + Meta WhatsApp Cloud API
Direct webhook integration, no third-party middleware
Reminders
Supabase (reminders + recurring_reminders tables)
Natural language parsing via Haiku, daily cron job for delivery, freemium gate via users.plan
Frontend
React + Vite + Tailwind CSS
Fast builds, component-driven dashboard
Hosting
Railway (backend) + Vercel (frontend)
Git-push deploys, generous free tiers, zero DevOps
Domain
spendly.info via Hostinger
A record โ†’ Vercel, CNAME for www subdomain

Privacy

What we store โ€” and what we don't.

๐Ÿ” Identity hashing

Your Telegram ID and WhatsApp phone number are passed through HMAC-SHA256(id, SECRET) before storage. The hash is one-way and irreversible โ€” even Spendly's own database cannot be used to identify you.

First name is used only during the session for personalization ("Hey Kiran!") โ€” it is never stored in the database. Every message from Telegram or WhatsApp includes the user's name in the payload, so it's always available in memory without persisting it.

All transactions are stored under the hashed ID. You can export your data anytime with /export and permanently delete everything with /deleteaccount.