Building a Family Expense Tracker with GAS & Telegram π°
πΈ Zero-Cost Full-Stack Finance App
Google Apps Script + Sheets + Telegram + Gemini AI β Hosted free, forever
π Table of Contents
- Overview
- π― Key Features
- ποΈ System Architecture
- π Getting Started
- β‘ Backend: Google Apps Script
- π€ Telegram Bot Integration
- βοΈ Frontend: React + Vite
- π§ AI Analysis with Gemini
- π Google Sheets Structure
- π’ Build & Deploy
- π± Daily Usage
- π§ Customization
- π Troubleshooting
- π Best Practices
- π Next Steps & Enhancements
- π Resources & References
- π Conclusion
Overviewβ
Managing family finances shouldn't require expensive software or complex infrastructure. This guide walks you through building a complete expense tracking system that's entirely free to host, using Google's ecosystem and Telegram for notifications.
The app lets you record expenses in ~8 seconds from your phone, get real-time Telegram alerts, view a rich dashboard with charts, and even leverage Gemini AI to analyze your spending habits β all powered by Google Apps Script with Google Sheets as the database.
Quick Entry
Record expenses in ~8 seconds β amount, category, person, done!
Rich Dashboard
5-tab dashboard: Overview, Budget, Goals, Assets, AI Analysis
Telegram Bot
Real-time notifications, daily/weekly summaries, inline keyboards
AI Insights
Gemini 2.5 Flash Lite β 6 quick analyses + free-form Q&A
Asset Tracking
Gold, savings, stocks, real estate β net worth at a glance
100% Free
No servers, no bills β Google Apps Script + Sheets + Telegram
π― Key Featuresβ
| Feature | Description |
|---|---|
| π Expense Form | Mobile-first interface, record in ~8 seconds |
| π Dashboard | 5 tabs: Overview Β· Budget Β· Goals Β· Assets Β· AI |
| π¦ Asset Management | Track gold, savings, stocks, real estate β P&L, maturity dates, net worth |
| π€ Telegram Bot | HTML-formatted messages, inline keyboard buttons β instant alerts, daily/weekly summaries |
| π― Goals & Debts | CRUD savings goals + debt management with progress tracking |
| π° Budget Tracking | Income, savings, bills, category budgets β spending velocity monitoring |
| π Category Management | Add/edit/delete spending categories from the app |
| βοΈ Settings | Toggle Telegram, manage system configuration |
| π Auto Reports | Monthly / Quarterly / Yearly β auto-generated on the 1st of each month |
| π± PWA-like | Add to Home Screen, dark/light theme, mobile-first design |
| π§ AI Analysis | Gemini-powered: monthly summary, savings tips, trend forecast, financial health score |
ποΈ System Architectureβ
ποΈ System Architecture β Hover to Explore
google.script.run β GAS Backend β Google Sheets+ GAS Triggers β Telegram Bot notifications + Gemini AI analysis
Technology Stackβ
Project Structureβ
family-expense/
βββ src/ β React source (local dev)
β βββ main.jsx β Entry point
β βββ App.jsx β Router (hash-based: #form | #dashboard)
β βββ styles/globals.css β CSS variables & base
β βββ utils/
β β βββ constants.js β Categories, budget, colors, sample data
β β βββ formatters.js β fmt(), fmtK(), pct()
β β βββ gas.js β Bridge GAS β local dev (mock in dev)
β β βββ gemini.js β Gemini AI client (cache, rate-limit)
β βββ components/
β βββ shared.jsx β Card, ProgressBar, StatCard, Tooltip
β βββ ExpenseForm/ β Daily expense entry form
β βββ Dashboard/ β Dashboard with 5 tabs
β β βββ OverviewTab.jsx β Tab 1: Overview (trend, pie, recent)
β β βββ BudgetTab.jsx β Tab 2: Budget (cashflow, budget vs actual)
β β βββ GoalsTab.jsx β Tab 3: Savings goals & debts
β β βββ AssetsTab.jsx β Tab 4: Assets (gold, savings, stocks)
β β βββ AITab.jsx β Tab 5: AI analysis (Gemini)
β βββ Management/ β CRUD panel (goals, debts, categories, etc.)
β
βββ gas/ β Pushed to Google Apps Script
β βββ Code.js β Backend: API + reports + triggers + Telegram
β βββ appsscript.json β GAS config + OAuth scopes
β βββ .clasp.json β CLASP config (scriptId)
β βββ index.html β β Built file from Vite (all-in-one)
β
βββ index.html β Vite entry
βββ vite.config.js β Vite + singlefile plugin
βββ package.json
How It Worksβ
Data Flowβ
π Getting Startedβ
Prerequisitesβ
πBefore You Begin
Make sure you have the following installed:
- Node.js β₯ 18 (for Vite and Clasp)
- npm (comes with Node.js)
- A Google Account (for Sheets and Apps Script)
- A Telegram Account (for the bot)
- Optionally: a Gemini API key from Google AI Studio for AI features
Step 1: Install Dependenciesβ
- npm
- yarn
# Clone or create the project
mkdir family-expense && cd family-expense
npm init -y
# Install frontend dependencies
npm install react react-dom recharts
# Install dev dependencies
npm install -D vite @vitejs/plugin-react vite-plugin-singlefile
# Install Clasp globally (Google Apps Script CLI)
npm install -g @google/clasp
# Login to Clasp
clasp login
mkdir family-expense && cd family-expense
yarn init -y
yarn add react react-dom recharts
yarn add -D vite @vitejs/plugin-react vite-plugin-singlefile
npm install -g @google/clasp
clasp login
Before using Clasp, you must enable the Apps Script API at: https://script.google.com/home/usersettings
Step 2: Configure Viteβ
The key innovation here is using vite-plugin-singlefile to bundle the entire React app into a single HTML file that Google Apps Script can serve:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
export default defineConfig({
plugins: [react(), viteSingleFile()],
build: {
outDir: 'gas', // Build directly into the gas/ folder
emptyOutDir: false, // Don't delete Code.js & appsscript.json!
assetsInlineLimit: 100000000, // Inline everything
cssCodeSplit: false,
rollupOptions: {
output: { manualChunks: undefined },
},
},
});
Google Apps Script can only serve HTML files via HtmlService. By bundling React + CSS + JS into one index.html, we can serve a full SPA from GAS with zero external hosting.
Step 3: Create Google Sheet & Apps Script Projectβ
- From Existing Sheet
- Create New with Clasp
- Go to Google Sheets β Create a new spreadsheet
- Copy the Sheet ID from the URL:
https://docs.google.com/spreadsheets/d/{SHEET_ID}/edit - Open Extensions β Apps Script β copy the Script ID from the URL
- Create the Clasp config:
{
"scriptId": "PASTE_YOUR_SCRIPT_ID_HERE",
"rootDir": "."
}
# Enable Apps Script API first: https://script.google.com/home/usersettings
mkdir gas && cd gas
clasp create --type standalone --title "Family Expense Tracker"
Then create a Google Sheet separately and note its Sheet ID.
Step 4: GAS Configurationβ
{
"timeZone": "Asia/Ho_Chi_Minh",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.scriptapp",
"https://www.googleapis.com/auth/calendar"
],
"webapp": {
"access": "ANYONE_ANONYMOUS",
"executeAs": "USER_DEPLOYING"
}
}
| Scope | Purpose |
|---|---|
spreadsheets | Read/write Google Sheets data |
script.external_request | Call Telegram & Gemini APIs |
script.scriptapp | Manage time-based triggers |
calendar | Reserved for future calendar integration |
β‘ Backend: Google Apps Scriptβ
This is the heart of the application β a single Code.js file that handles everything.
Configuration & Constantsβ
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// FAMILY EXPENSE TRACKER β Google Apps Script Backend
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const CONFIG = {
// β οΈ Paste your Google Sheet ID here
SPREADSHEET_ID: 'YOUR_SHEET_ID_HERE',
SHEETS: {
RAW_DATA: 'RawData', // Raw transactions
MONTHLY: 'BaoCao_Thang', // Monthly reports
QUARTERLY: 'BaoCao_Quy', // Quarterly reports
YEARLY: 'BaoCao_Nam', // Yearly reports
GOALS: 'MucTieu_TietKiem', // Savings goals
DEBTS: 'QuanLy_No', // Debt management
ASSETS: 'TaiSan', // Assets tracking
BUDGET: 'NganSach', // Budget configuration
SETTINGS: 'CaiDat', // System settings
CONFIG: 'CauHinh', // Categories & config
},
TIMEZONE: 'Asia/Ho_Chi_Minh',
};
// Helper: open spreadsheet by ID
function getSpreadsheet_() {
return SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
}
// Helper: read secrets from Script Properties (never hardcode!)
function getSecret_(key) {
return PropertiesService.getScriptProperties().getProperty(key) || '';
}
Never hardcode API tokens or secrets in your source code! Use GAS Script Properties to store:
TELEGRAM_BOT_TOKENTELEGRAM_CHAT_IDGEMINI_API_KEY
Go to GAS Editor β Project Settings βοΈ β Script Properties β Add each key/value pair.
Web App Entry Pointβ
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Family Expense Tracker')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1, user-scalable=no');
}
This single function serves your entire React app. When Vite builds the project, it outputs gas/index.html which is then served by GAS.
Adding Expensesβ
function addEntry(entry) {
const ss = getSpreadsheet_();
const sheet = getOrCreateSheet_(ss, CONFIG.SHEETS.RAW_DATA, [
'Timestamp', 'Date', 'Month', 'Year', 'Quarter',
'Person', 'Type', 'Group', 'Category', 'Full Category',
'Amount (K)', 'Amount (VND)', 'Note',
]);
// Support custom timestamp from frontend
var ts = entry.timestamp ? new Date(entry.timestamp) : new Date();
if (isNaN(ts.getTime())) ts = new Date();
var month = parseInt(Utilities.formatDate(ts, CONFIG.TIMEZONE, 'M'), 10);
var year = parseInt(Utilities.formatDate(ts, CONFIG.TIMEZONE, 'yyyy'), 10);
var quarter = 'Q' + Math.ceil(month / 3);
sheet.appendRow([
ts,
Utilities.formatDate(ts, CONFIG.TIMEZONE, 'dd/MM/yyyy'),
month, year, quarter,
entry.person,
entry.type === 'expense' ? 'Expense' : 'Income',
catInfo.group,
entry.category,
catInfo.fullName,
entry.amount,
entry.amount * 1000,
entry.note || '',
]);
// Send Telegram notification
notifyTelegram_(entry, ts);
return { success: true };
}
Helper: Get or Create Sheetβ
function getOrCreateSheet_(ss, name, headers) {
var sheet = ss.getSheetByName(name);
if (!sheet) {
sheet = ss.insertSheet(name);
if (headers && headers.length) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(1, 1, 1, headers.length)
.setFontWeight('bold')
.setBackground('#4CAF50')
.setFontColor('white');
sheet.setFrozenRows(1);
}
}
return sheet;
}
System Setup Functionβ
Run this once after deploying to create all sheets and triggers:
function SETUP_HE_THONG() {
setupGoals(); // Create savings goals sheet
setupAssets(); // Create assets sheet
setupBudget(); // Create budget sheet
setupSettings(); // Create settings sheet
setupAllTriggers(); // Install time-based triggers
Logger.log('β
SYSTEM READY!');
Logger.log('π RawData β Raw transactions');
Logger.log('π BaoCao_Thang β Monthly reports');
Logger.log('π BaoCao_Quy β Quarterly reports');
Logger.log('π BaoCao_Nam β Yearly reports');
Logger.log('π― MucTieu_TietKiem β Savings goals');
Logger.log('π QuanLy_No β Debt management');
Logger.log('π¦ TaiSan β Assets');
Logger.log('π° NganSach β Budget config');
Logger.log('βοΈ CaiDat β Settings');
}
Time-Based Triggersβ
function setupAllTriggers() {
// Remove existing triggers to avoid duplicates
ScriptApp.getProjectTriggers().forEach(function(t) {
ScriptApp.deleteTrigger(t);
});
// Monthly/Quarterly/Yearly reports β 1st of each month, 8 AM
ScriptApp.newTrigger('generateAllReports')
.timeBased().onMonthDay(1).atHour(8).create();
// Update savings goals progress β Sundays, 8 PM
ScriptApp.newTrigger('updateGoalsProgress')
.timeBased().onWeekDay(ScriptApp.WeekDay.SUNDAY).atHour(20).create();
// Telegram daily summary β Every day, 9 PM
ScriptApp.newTrigger('telegramDailyReminder')
.timeBased().everyDays(1).atHour(21).create();
// Telegram weekly summary β Sundays, 8 PM
ScriptApp.newTrigger('telegramWeeklySummary')
.timeBased().onWeekDay(ScriptApp.WeekDay.SUNDAY).atHour(20).create();
Logger.log('β
All triggers installed');
}
β°Automatic Triggers
| Trigger | Schedule | Function |
|---|---|---|
| Monthly/Quarterly/Yearly reports | 1st of month, 8 AM | generateAllReports() |
| Update goals progress | Sunday, 8 PM | updateGoalsProgress() |
| Telegram daily summary | Every day, 9 PM | telegramDailyReminder() |
| Telegram weekly summary | Sunday, 8 PM | telegramWeeklySummary() |
π€ Telegram Bot Integrationβ
This is one of the most powerful features β a fully interactive Telegram bot that sends rich notifications with inline keyboard buttons.
Step 1: Create Your Bot (~2 minutes)β
- Open Telegram β search for @BotFather β send
/newbot - Choose a name (e.g.,
Family Expense Bot) - Choose a username (e.g.,
family_expense_bot) - Copy the token that BotFather gives you
Step 2: Get Your Chat ID (~1 minute)β
- Open Telegram β search for @userinfobot or @RawDataBot β send
/start - Copy your Chat ID (a number)
Step 3: Store Credentials Securelyβ
- Via GAS Editor (Recommended)
- Via Code (One-time)
- Open GAS Editor β Project Settings (βοΈ icon on the left sidebar)
- Scroll to Script Properties β Edit script properties
- Add these properties:
| Property | Value |
|---|---|
TELEGRAM_BOT_TOKEN | Token from BotFather |
TELEGRAM_CHAT_ID | Your Chat ID |
GEMINI_API_KEY | API key from Google AI Studio |
- Click Save script properties
Create a temporary function in GAS Editor, run it once, then delete it:
function _initSecrets() {
setupSecrets({
TELEGRAM_BOT_TOKEN: '123456:ABC-DEF1234...',
TELEGRAM_CHAT_ID: '123456789',
GEMINI_API_KEY: 'AIza...'
});
}
function setupSecrets(secrets) {
var props = PropertiesService.getScriptProperties();
if (secrets.TELEGRAM_BOT_TOKEN) props.setProperty('TELEGRAM_BOT_TOKEN', secrets.TELEGRAM_BOT_TOKEN);
if (secrets.TELEGRAM_CHAT_ID) props.setProperty('TELEGRAM_CHAT_ID', secrets.TELEGRAM_CHAT_ID);
if (secrets.GEMINI_API_KEY) props.setProperty('GEMINI_API_KEY', secrets.GEMINI_API_KEY);
Logger.log('β
Secrets saved to Script Properties');
}
Sending Messagesβ
function sendTelegram_(text, opts) {
var token = getSecret_('TELEGRAM_BOT_TOKEN');
var chatIds = getAllChatIds_();
if (!token || chatIds.length === 0) return false;
var anySuccess = false;
chatIds.forEach(function(chatId) {
try {
var url = 'https://api.telegram.org/bot' + token + '/sendMessage';
var payload = {
chat_id: chatId,
text: text,
parse_mode: 'HTML',
disable_web_page_preview: true,
};
if (opts && opts.reply_markup) {
payload.reply_markup = opts.reply_markup;
}
var res = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
if (res.getResponseCode() === 200) anySuccess = true;
// Auto-remove blocked chats
if (res.getResponseCode() === 403) removeChatId_(chatId);
} catch (e) {
Logger.log('β Telegram error: ' + e.message);
}
});
return anySuccess;
}
Inline Keyboard Buttonsβ
function buildInlineKeyboard_(buttons) {
return { inline_keyboard: buttons };
}
// Usage example:
sendTelegram_('πΈ <b>Expense:</b> 150K\nπ Food Β· π€ Thanh', {
reply_markup: buildInlineKeyboard_([[
{ text: 'π This Month', callback_data: '/status' },
{ text: 'π Report', callback_data: '/report' }
]])
});
Webhook Handler for Bot Commandsβ
function doPost(e) {
try {
var body = JSON.parse(e.postData.contents);
if (body.update_id !== undefined) {
return handleTelegramWebhook_(body);
}
} catch(err) {}
return ContentService.createTextOutput('OK');
}
function handleTelegramWebhook_(update) {
// Handle inline button presses
if (update.callback_query) {
return handleCallbackQuery_(update.callback_query);
}
var msg = update.message;
if (!msg || !msg.text) return ContentService.createTextOutput('OK');
var chatId = String(msg.chat.id);
var text = msg.text.trim();
var firstName = msg.from.first_name || '';
var reply = '', replyMarkup = null;
if (text === '/start') {
addChatId_(chatId); // Auto-register for notifications
reply = 'π <b>Hello ' + escapeHtml_(firstName) + '!</b>\n\n'
+ 'β
Subscribed to expense notifications.\n\n'
+ 'π <b>Commands:</b>\n'
+ '/status β Today\'s summary\n'
+ '/report β Monthly report\n'
+ '/stop β Unsubscribe\n';
replyMarkup = buildInlineKeyboard_([[
{ text: 'π Status', callback_data: '/status' },
{ text: 'π Report', callback_data: '/report' }
]]);
} else if (text === '/status') {
reply = buildQuickStatus_();
replyMarkup = buildInlineKeyboard_([[
{ text: 'π Report', callback_data: '/report' },
{ text: 'π Sheet', url: getSheetUrl_() }
]]);
} else if (text === '/report') {
reply = buildQuickMonthSummary_();
}
// Send reply to the user who sent the command
if (reply) {
sendDirectReply_(chatId, reply, replyMarkup);
}
return ContentService.createTextOutput('OK');
}
Setting Up the Webhookβ
function setupTelegramWebhook(webAppUrl) {
var token = getSecret_('TELEGRAM_BOT_TOKEN');
if (!token) { Logger.log('β No bot token'); return; }
var url = 'https://api.telegram.org/bot' + token + '/setWebhook';
var res = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ url: webAppUrl }),
muteHttpExceptions: true,
});
Logger.log('Webhook setup: ' + res.getContentText());
}
After deploying your web app, run:
setupTelegramWebhook('https://script.google.com/macros/s/YOUR_DEPLOY_ID/exec');
Notification Typesβ
πTelegram Message Types
| Trigger | When | Content | Inline KB |
|---|---|---|---|
| New expense | Real-time | πΈ 150K Β· π Food Β· π€ Thanh + budget progress | [π This Month] |
telegramDailyReminder | 9 PM daily | Today's total + budget alerts + remaining | [π Sheet] [π Report] |
telegramWeeklySummary | Sunday 8 PM | Week total + vs last week + top 3 + debts due | [π Sheet] |
telegramBudgetAlert | Manual | Categories at β₯80% / 100% budget | [π Sheet] [π Details] |
telegramMonthlyReport | 1st of month | Last month summary + pie chart | [π Sheet] |
Telegram Helper Functionsβ
// Format amount (K) for Telegram: 1500 β "1.5M", 150 β "150K"
function formatK_(val) {
var n = Number(val) || 0;
if (Math.abs(n) >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'M';
return n.toLocaleString('vi-VN') + 'K';
}
// Unicode progress bar for budget tracking
function progressBar_(pct, len) {
len = len || 10;
var p = Math.min(100, Math.max(0, pct));
var filled = Math.round(p / 100 * len);
var bar = '';
for (var i = 0; i < filled; i++) bar += 'β°';
for (var i = 0; i < len - filled; i++) bar += 'β±';
return bar + ' ' + Math.round(p) + '%';
}
// Escape HTML for Telegram parse_mode: HTML
function escapeHtml_(text) {
return String(text).replace(/&/g, '&')
.replace(/</g, '<').replace(/>/g, '>');
}
Testing the Botβ
function telegramTest() {
var ok = sendTelegram_(
'π <b>Test successful!</b>\n\n'
+ 'Family Expense Bot is connected.\n'
+ 'π
' + Utilities.formatDate(new Date(), CONFIG.TIMEZONE, 'dd/MM/yyyy HH:mm')
+ '\n\nπ§ͺ ' + progressBar_(75, 10),
{ reply_markup: buildInlineKeyboard_([[
{ text: 'π Status', callback_data: '/status' },
{ text: 'π Report', callback_data: '/report' }
]]) }
);
Logger.log(ok ? 'β
Telegram test OK' : 'β FAILED β Check BOT_TOKEN and CHAT_ID');
}
βοΈ Frontend: React + Viteβ
GAS β Frontend Bridgeβ
The magic that connects the React frontend to GAS backend:
// Bridge: GAS calls in production, mock data in local dev
const isDev = typeof google === 'undefined' || !google.script;
export function callGas(functionName, ...args) {
if (isDev) {
console.log(`[DEV] Mock GAS call: ${functionName}`, args);
return Promise.resolve(getMockData(functionName));
}
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
[functionName](...args);
});
}
When running npm run dev, the app uses mock/sample data instead of real GAS calls. This gives you hot reload and fast iteration without deploying to GAS every time.
Package Scriptsβ
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"push": "cd gas && npx @google/clasp push",
"deploy": "npm run build && npm run push",
"open": "cd gas && npx @google/clasp open --webapp"
}
}
Development Workflowβ
| Command | Description |
|---|---|
npm run dev | Vite dev server with hot reload (uses mock data) |
npm run build | Build single HTML file into gas/ |
npm run deploy | Build + push to Google Apps Script |
npm run open | Open the deployed web app |
π§ AI Analysis with Geminiβ
Gemini API Setupβ
Get a free API key from Google AI Studio and store it in Script Properties as GEMINI_API_KEY.
The AI tab provides 6 quick analyses + free-form Q&A:
| Analysis | Description |
|---|---|
| π Simple Summary | Plain-language explanation anyone can understand |
| π Monthly Analysis | Income/expenses, budget adherence, reduction suggestions |
| πΆ Baby Expenses | Track baby-related spending: formula, diapers, checkups |
| π‘ Savings Tips | Specific, actionable suggestions tailored to your data |
| π 6-Month Trend | Trend analysis, forecasting, early warnings |
| β€οΈ Financial Health | Score your financial health + priority actions |
| π¬ Free Q&A | Chat with AI about your personal finances |
const GEMINI_MODEL = 'gemini-2.5-flash-lite';
const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
export async function askGemini(apiKey, prompt, data) {
const url = `${GEMINI_BASE_URL}/${GEMINI_MODEL}:generateContent?key=${apiKey}`;
const systemPrompt = `You are a family financial advisor.
Analyze the following expense data and provide insights.
Data: ${JSON.stringify(data)}`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: systemPrompt + '\n\n' + prompt }] }],
}),
});
const result = await response.json();
return result.candidates?.[0]?.content?.parts?.[0]?.text || 'No response';
}
Gemini 2.5 Flash Lite free tier: ~30 requests/minute, ~1500 requests/day β more than enough for personal use.
π Google Sheets Structureβ
The app uses 10 sheets as its database:
| Sheet | Purpose | Auto-Created |
|---|---|---|
RawData | Raw transactions (1 row per entry) | β |
BaoCao_Thang | Monthly reports | β |
BaoCao_Quy | Quarterly reports | β |
BaoCao_Nam | Yearly reports | β |
MucTieu_TietKiem | Savings goals | β |
QuanLy_No | Debt management | β |
TaiSan | Assets (gold, savings, stocks, real estate) | β |
NganSach | Budget configuration (versioned by month) | β |
CaiDat | System settings (Telegram on/off, etc.) | β |
CauHinh | Categories & configuration | β |
RawData Schemaβ
Each expense/income entry becomes a single row:
| Column | Field | Example |
|---|---|---|
| A | Timestamp | 2026-02-10T14:30:00 |
| B | Date | 10/02/2026 |
| C | Month | 2 |
| D | Year | 2026 |
| E | Quarter | Q1 |
| F | Person | Thanh |
| G | Type | Expense / Income |
| H | Group | Essential / Wants / Growth |
| I | Category | Food |
| J | Full Category | Food (groceries, supermarket, snacks) |
| K | Amount (K) | 150 |
| L | Amount (VND) | 150000 |
| M | Note | Weekly groceries |
π’ Build & Deployβ
One-Command Deployβ
# Build React app into single HTML + push to GAS
npm run deploy
This runs:
vite buildβ bundles React intogas/index.htmlclasp pushβ pushesCode.js,index.html,appsscript.jsonto GAS
Deploy as Web Appβ
- Open GAS Editor:
cd gas && clasp open - Click Deploy β New deployment
- Select Web app
- Execute as: Me | Access: Anyone
- Click Deploy β Copy the URL
First-Time Setupβ
In the GAS Editor, run the SETUP_HE_THONG function:
- Open GAS Editor
- Select
SETUP_HE_THONGfrom the function dropdown - Click Run βΆοΈ
- Grant permissions when prompted
This creates all 10 sheets and installs all time-based triggers automatically.
After Each Code Changeβ
npm run deploy
Then in GAS: Deploy β Manage deployments β Edit β select New version β Deploy
Each clasp push updates the code, but the deployed version doesn't change automatically. You must create a new version in the deployment settings after each push.
π± Daily Usageβ
Open App
Tap the Home Screen icon (PWA-like)
Enter Expense
Amount β Category β Person β Save (~8 sec)
Get Notified
Instant Telegram confirmation with budget bar
9 PM Summary
Automatic daily recap with alerts
Weekly Review
Sunday summary with trends and goals
AI Insights
Ask Gemini to analyze your spending anytime
π§ Customizationβ
Categoriesβ
- From App
- From Code
Dashboard β βοΈ Management β Categories tab β Add / Edit / Delete
Edit CATEGORY_MAP in both src/utils/constants.js and gas/Code.js:
const CATEGORY_MAP = {
'Food': { group: 'Essential', fullName: 'Food (groceries, supermarket)' },
'Transport': { group: 'Essential', fullName: 'Transport (gas, grab, repairs)' },
'Healthcare': { group: 'Essential', fullName: 'Healthcare (checkups, medicine)' },
'Dining Out': { group: 'Wants', fullName: 'Dining Out (cafe, restaurants)' },
'Shopping': { group: 'Wants', fullName: 'Shopping (clothes, tech, cosmetics)' },
'Entertainment':{ group: 'Wants', fullName: 'Entertainment (movies, travel)' },
'Education': { group: 'Growth', fullName: 'Education (books, courses, apps)' },
'Salary': { group: 'Income', fullName: 'Salary' },
'Freelance': { group: 'Income', fullName: 'Freelance' },
};
Budget Configurationβ
Edit DEFAULT_BUDGET in constants.js and Code.js:
const DEFAULT_BUDGET = {
income: { 'Salary': 30000, 'Freelance': 5000 },
savings: { 'Gold': 2000, 'Emergency Fund': 3000 },
bills: { 'Rent': 8000, 'Internet': 250, 'Phone': 200 },
expenses: {
'Food': 3000, 'Transport': 1200, 'Healthcare': 800,
'Dining Out': 3000, 'Shopping': 700, 'Entertainment': 800,
'Education': 1000,
},
debt: 2000,
};
CRUD API Referenceβ
| Entity | Get | Add | Update | Delete |
|---|---|---|---|---|
| Goals | getGoals() | addGoal(goal) | updateGoal(goal) | deleteGoal(rowIndex) |
| Debts | getDebts() | addDebt(debt) | updateDebt(debt) | deleteDebt(rowIndex) |
| Assets | getAssets() | addAsset(asset) | updateAsset(asset) | deleteAsset(rowIndex) |
| Categories | getCategories() | addCategory(cat) | updateCategory(cat) | deleteCategory(rowIndex) |
| Budget | getBudget() | β | updateBudget(budget) | β |
| Settings | getSettings() | β | updateSettings(settings) | β |
π Troubleshootingβ
clasp push fails
- Verify
.clasp.jsonhas the correctscriptIdandrootDir: "." - Make sure Apps Script API is enabled: https://script.google.com/home/usersettings
- Run
clasp loginagain if your session expired
App shows loading forever
- In GAS deployment settings, make sure access is set to Anyone
- Try creating a new deployment instead of editing an existing one
Telegram bot not responding
- Verify
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDin Script Properties - Run
telegramTest()in GAS Editor β grant permissions if prompted - Check the Execution Log for errors
Telegram webhook not working
- Make sure you called
setupTelegramWebhook()with the correct Web App URL - The URL should be the deployed web app URL, not the editor URL
- Run
removeTelegramWebhook()thensetupTelegramWebhook(url)to reset
Local dev shows no data
This is normal! npm run dev uses mock/sample data. Real data only appears in the deployed version on GAS.
AI analysis not working
- Verify
GEMINI_API_KEYin Script Properties - Ensure you're using a valid key from Google AI Studio
- Check the browser console for API errors
After deploy, old version still shows
In GAS Editor: Deploy β Manage deployments β Edit β select New version β Deploy. Each clasp push updates code but the deployed version is pinned.
π Best Practicesβ
πSecurity
- Never hardcode secrets in source files
- Use Script Properties for all tokens/keys
- Set web app access to "Anyone" only if needed
- Blocked chats are auto-removed from notification list
β‘Performance
- GAS has a 6-minute execution limit per function
- Batch sheet reads: get full range, filter in memory
- Use
Utilities.formatDate()with timezone for consistency - Cache Gemini results to reduce API calls
π οΈDevelopment
- Use
npm run devfor hot reload with mock data - Deploy only when changes are ready:
npm run deploy - Always create a new version after deploying
- Keep
CATEGORY_MAPin sync between frontend and backend
πData
- Google Sheets has a 10 million cell limit
- 1 year β 3,000β5,000 rows β plenty of room
- Use sheet-based versioning for budget (snapshot per month)
- Back up your Sheet periodically
π Next Steps & Enhancementsβ
Receipt OCR
Scan receipts with Google Vision API and auto-fill expenses
Multi-User
Family members can register via /start and get their own notifications
Chart Images
Send pie/bar charts via Telegram using quickchart.io
Multi-Language
Add English/Vietnamese toggle for the entire app
Recurring Expenses
Auto-add monthly bills (rent, subscriptions) on schedule
Investment Tracking
Auto-fetch gold prices, stock quotes for real-time portfolio value
π Resources & Referencesβ
π Official Documentationβ
- Google Apps Script Reference
- Clasp β Command Line Apps Script
- Telegram Bot API
- Gemini API β Google AI Studio
- Vite Documentation
- vite-plugin-singlefile
- Recharts β React Charting Library
π Learning Resourcesβ
- Google Apps Script β Getting Started
- Building Web Apps with GAS
- Telegram Bot Tutorial
- React 18 Documentation
π Conclusionβ
Your Free Finance App is Ready!
You've built a complete expense tracking system with zero hosting costs. Record expenses, track budgets, get AI insights, and receive Telegram notifications β all from your phone.
Key Takeawaysβ
Zero Cost
GAS + Sheets + Telegram = completely free hosting
Single File
Vite bundles entire React app into one HTML file for GAS
Smart Notifications
Telegram bot with inline keyboards and rich formatting
AI-Powered
Gemini analyzes your spending and gives actionable advice
Auto Reports
Time-based triggers generate daily/weekly/monthly summaries
Full CRUD
Manage goals, debts, assets, categories β all from the app
Built with β€οΈ using Google Apps Script, React, Vite, Telegram & Gemini AI
Last updated: February 2026 | Version 2.7.0