For Developers ยท 8 min read

Building a Finance Bot on WhatsApp and Telegram With Claude AI

Kiran Namburi April 2026 Hyderabad ๐Ÿ‡ฎ๐Ÿ‡ณ

Last month I shipped Spendly โ€” an AI-powered expense tracker that lives inside WhatsApp and Telegram. No app to download, no forms to fill, just send a message.

This post is about how I built it, the architecture decisions I made, and the things that surprised me along the way.

The Core Idea

Most expense apps fail because of friction. Opening a dedicated app to log โ‚น80 chai feels like too much work. But WhatsApp is already open 40 times a day. So why not use that?

User: coffee 80
Bot: ๐Ÿ’ธ Expense saved ยท โ‚น80 ยท Food ยท Coffee

Behind that simple interaction is Claude AI parsing natural language, a Node.js backend, Supabase storing the data, and the whole thing deployed on Railway.

Tech Stack

AI:        Claude Haiku (parsing) + Claude Sonnet (queries)
Backend:   Node.js + Express โ†’ Railway
Database:  Supabase (PostgreSQL)
Messaging: Telegram Bot API + WhatsApp Business API
Frontend:  React + Vite โ†’ Vercel

The Two-Tier AI Approach

This was the most interesting architectural decision.

Every expense message goes through Claude Haiku โ€” fast and cheap, around $0.0003 per message. It extracts amount, category, date, payment method, and expense type as structured JSON.

When users ask questions โ€” "how much did I spend this month?" or "compare food vs transport" โ€” that goes through Claude Sonnet, which is smarter and handles complex conversational queries with context from previous messages.

Right model for the right job. Haiku for parsing is about 10x cheaper than Sonnet and plenty capable. Sonnet for analysis gives genuinely useful answers.

Pre-LLM Intent Classification

Here is something most AI app tutorials skip โ€” you do not need to call the LLM for every message.

Before touching Claude, I run a regex-based intent classifier:

if (GREETINGS.has(text))              return 'GREETING';
if (/remind me|reminder/.test(text))  return 'REMINDER';
if (/how much|compare/.test(text))    return 'QUERY';
if (/^\d+/.test(text))                return 'RECORD';
Key insight: Around 70% of messages โ€” greetings, commands, obvious expenses like "coffee 80" โ€” never touch the LLM at all. This cuts cost and latency significantly.

The Channel Adapter Pattern

Both Telegram and WhatsApp are separate adapters over a shared core:

telegram.js  โ”€โ”
               โ”œโ”€โ”€ record.js, query.js, command.js
whatsapp.js  โ”€โ”˜

Telegram supports inline keyboard buttons natively. WhatsApp only supports 3 reply buttons maximum โ€” so WhatsApp falls back to a numbered text list for menus with more options.

This pattern means adding a new platform tomorrow only requires writing one adapter file. All business logic stays untouched.

Privacy: Identity Hashing

I did not want to store real Telegram IDs or WhatsApp phone numbers in the database. If the database leaks, no one should be identifiable.

function deriveIdentityHash(id) {
  return crypto
    .createHmac('sha256', process.env.IDENTITY_SECRET)
    .update(String(id))
    .digest('hex');
}

The real phone number or Telegram ID is never written to Supabase. Only the hash. It is irreversible โ€” even I cannot figure out who is who from the database alone. First name is not stored at all โ€” it comes from the message payload on every request and stays in memory only.

Conversational Context

When users ask follow-up questions like "how about entertainment?" the bot needs to remember the previous exchange. I store the last 3 message pairs in memory per user with a 30-minute TTL:

const messages = [
  { role: 'user',      content: dataMessage },
  { role: 'assistant', content: 'Got it...' },
  ...history,   // last 3 exchanges
  { role: 'user',      content: question }
];

No database needed. Simple in-memory Map with auto-expiry.

Magic Link Authentication

The web dashboard needed auth without passwords. Solution: magic links generated by the bot.

User: web
Bot: Here's your dashboard link โ€” expires in 15 minutes

The token is stored in an in-memory Map. Clicking it creates a 7-day session. The Telegram ID bridges identity from bot to web dashboard seamlessly.

What Surprised Me

WhatsApp Business API is harder than Telegram. The Telegram Bot API is a joy to work with. WhatsApp has a lot of moving parts โ€” Meta Developer App, Business Account, System User, permanent token, webhook subscriptions. Took me 2 days just to get the first message through.

The pre-LLM classifier was the biggest performance win. I almost skipped it thinking "Claude is fast enough." Wrong mindset. Regex is microseconds. Claude is 500msโ€“2s. For a chat bot, latency matters a lot.

Prompt engineering is actual engineering. Getting Claude Haiku to reliably extract structured JSON from casual messages like "1088 fuel full take nite first refill in April" took real iteration. The rules in the system prompt matter a lot.

What I Would Do Differently

I would set up staging/production separation from day one. Making fixes directly on production while users are active is stressful.

I would also add more logging earlier. When something breaks in production, "undefined is not a function" in Railway logs is not very helpful.

Open Source

The entire codebase is on GitHub under AGPL-3.0: github.com/knamburi/spendly

Architecture details, design decisions, and privacy notes are all documented at spendly.info/architecture.

Try Spendly

Free to use. No signup. Open source.

Written by Kiran Namburi ยท Hyderabad ๐Ÿ‡ฎ๐Ÿ‡ณ ยท bi.kiran04@gmail.com