Skip to main content

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​

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​

FeatureDescription
πŸ“ Expense FormMobile-first interface, record in ~8 seconds
πŸ“Š Dashboard5 tabs: Overview Β· Budget Β· Goals Β· Assets Β· AI
🏦 Asset ManagementTrack gold, savings, stocks, real estate β€” P&L, maturity dates, net worth
πŸ€– Telegram BotHTML-formatted messages, inline keyboard buttons β€” instant alerts, daily/weekly summaries
🎯 Goals & DebtsCRUD savings goals + debt management with progress tracking
πŸ’° Budget TrackingIncome, savings, bills, category budgets β€” spending velocity monitoring
πŸ“‚ Category ManagementAdd/edit/delete spending categories from the app
βš™οΈ SettingsToggle Telegram, manage system configuration
πŸ“ˆ Auto ReportsMonthly / Quarterly / Yearly β€” auto-generated on the 1st of each month
πŸ“± PWA-likeAdd to Home Screen, dark/light theme, mobile-first design
🧠 AI AnalysisGemini-powered: monthly summary, savings tips, trend forecast, financial health score

πŸ—οΈ System Architecture​

πŸ—οΈ System Architecture β€” Hover to Explore

πŸ“± User
⚑ Google Apps Script
πŸ“Š Google Sheets
πŸ€– Telegram Bot
🧠 Gemini AI
⚑ Vite + React
Flow: User β†’ React PWA β†’ google.script.run β†’ GAS Backend β†’ Google Sheets
+ GAS Triggers β†’ Telegram Bot notifications + Gemini AI analysis

Technology Stack​

βš›οΈ React 18
Frontend UI
⚑ Vite
Build tooling
πŸ“Š Recharts
Charts & graphs
πŸ”§ Google Apps Script
Backend (V8)
πŸ“‹ Google Sheets
Database
πŸ€– Telegram Bot API
Notifications
🧠 Gemini 2.5 Flash
AI analysis
πŸ“¦ Clasp
GAS deployment

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​

1
Setup Tools
2
Create GAS
3
Google Sheet
4
Build & Deploy
5
Telegram Bot
6
Done!

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​

# 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
Enable Apps Script API

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:

vite.config.js
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 },
},
},
});
Why Single File?

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​

  1. Go to Google Sheets β†’ Create a new spreadsheet
  2. Copy the Sheet ID from the URL: https://docs.google.com/spreadsheets/d/{SHEET_ID}/edit
  3. Open Extensions β†’ Apps Script β†’ copy the Script ID from the URL
  4. Create the Clasp config:
gas/.clasp.json
{
"scriptId": "PASTE_YOUR_SCRIPT_ID_HERE",
"rootDir": "."
}

Step 4: GAS Configuration​

gas/appsscript.json
{
"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"
}
}
ScopePurpose
spreadsheetsRead/write Google Sheets data
script.external_requestCall Telegram & Gemini APIs
script.scriptappManage time-based triggers
calendarReserved 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​

gas/Code.js
// ════════════════════════════════════════════════════════════════
// 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) || '';
}
Security First

Never hardcode API tokens or secrets in your source code! Use GAS Script Properties to store:

  • TELEGRAM_BOT_TOKEN
  • TELEGRAM_CHAT_ID
  • GEMINI_API_KEY

Go to GAS Editor β†’ Project Settings βš™οΈ β†’ Script Properties β†’ Add each key/value pair.

Web App Entry Point​

gas/Code.js β€” doGet()
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​

gas/Code.js β€” addEntry()
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​

gas/Code.js β€” getOrCreateSheet_()
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:

gas/Code.js β€” SETUP_HE_THONG()
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​

gas/Code.js β€” setupAllTriggers()
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

TriggerScheduleFunction
Monthly/Quarterly/Yearly reports1st of month, 8 AMgenerateAllReports()
Update goals progressSunday, 8 PMupdateGoalsProgress()
Telegram daily summaryEvery day, 9 PMtelegramDailyReminder()
Telegram weekly summarySunday, 8 PMtelegramWeeklySummary()

πŸ€– 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)​

  1. Open Telegram β†’ search for @BotFather β†’ send /newbot
  2. Choose a name (e.g., Family Expense Bot)
  3. Choose a username (e.g., family_expense_bot)
  4. Copy the token that BotFather gives you

Step 2: Get Your Chat ID (~1 minute)​

  1. Open Telegram β†’ search for @userinfobot or @RawDataBot β†’ send /start
  2. Copy your Chat ID (a number)

Step 3: Store Credentials Securely​

  1. Open GAS Editor β†’ Project Settings (βš™οΈ icon on the left sidebar)
  2. Scroll to Script Properties β†’ Edit script properties
  3. Add these properties:
PropertyValue
TELEGRAM_BOT_TOKENToken from BotFather
TELEGRAM_CHAT_IDYour Chat ID
GEMINI_API_KEYAPI key from Google AI Studio
  1. Click Save script properties

Sending Messages​

gas/Code.js β€” sendTelegram_()
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​

gas/Code.js β€” buildInlineKeyboard_()
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​

gas/Code.js β€” doPost() + handleTelegramWebhook_()
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​

gas/Code.js β€” setupTelegramWebhook()
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

TriggerWhenContentInline KB
New expenseReal-timeπŸ’Έ 150K Β· πŸ›’ Food Β· πŸ‘€ Thanh + budget progress[πŸ“Š This Month]
telegramDailyReminder9 PM dailyToday's total + budget alerts + remaining[πŸ“Š Sheet] [πŸ“‹ Report]
telegramWeeklySummarySunday 8 PMWeek total + vs last week + top 3 + debts due[πŸ“Š Sheet]
telegramBudgetAlertManualCategories at β‰₯80% / 100% budget[πŸ“Š Sheet] [πŸ“‹ Details]
telegramMonthlyReport1st of monthLast month summary + pie chart[πŸ“Š Sheet]

Telegram Helper Functions​

gas/Code.js β€” Utility 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, '&amp;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Testing the Bot​

gas/Code.js β€” telegramTest()
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:

src/utils/gas.js
// 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);
});
}
Local Development

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​

package.json β€” 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​

CommandDescription
npm run devVite dev server with hot reload (uses mock data)
npm run buildBuild single HTML file into gas/
npm run deployBuild + push to Google Apps Script
npm run openOpen 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:

AnalysisDescription
🏠 Simple SummaryPlain-language explanation anyone can understand
πŸ“Š Monthly AnalysisIncome/expenses, budget adherence, reduction suggestions
πŸ‘Ά Baby ExpensesTrack baby-related spending: formula, diapers, checkups
πŸ’‘ Savings TipsSpecific, actionable suggestions tailored to your data
πŸ“ˆ 6-Month TrendTrend analysis, forecasting, early warnings
❀️ Financial HealthScore your financial health + priority actions
πŸ’¬ Free Q&AChat with AI about your personal finances
src/utils/gemini.js β€” Simplified
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';
}
Free Tier Limits

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:

SheetPurposeAuto-Created
RawDataRaw transactions (1 row per entry)βœ…
BaoCao_ThangMonthly reportsβœ…
BaoCao_QuyQuarterly reportsβœ…
BaoCao_NamYearly reportsβœ…
MucTieu_TietKiemSavings goalsβœ…
QuanLy_NoDebt managementβœ…
TaiSanAssets (gold, savings, stocks, real estate)βœ…
NganSachBudget configuration (versioned by month)βœ…
CaiDatSystem settings (Telegram on/off, etc.)βœ…
CauHinhCategories & configurationβœ…

RawData Schema​

Each expense/income entry becomes a single row:

ColumnFieldExample
ATimestamp2026-02-10T14:30:00
BDate10/02/2026
CMonth2
DYear2026
EQuarterQ1
FPersonThanh
GTypeExpense / Income
HGroupEssential / Wants / Growth
ICategoryFood
JFull CategoryFood (groceries, supermarket, snacks)
KAmount (K)150
LAmount (VND)150000
MNoteWeekly groceries

🚒 Build & Deploy​

1
Build
2
Push
3
Deploy
4
Setup
5
Test

One-Command Deploy​

# Build React app into single HTML + push to GAS
npm run deploy

This runs:

  1. vite build β€” bundles React into gas/index.html
  2. clasp push β€” pushes Code.js, index.html, appsscript.json to GAS

Deploy as Web App​

  1. Open GAS Editor: cd gas && clasp open
  2. Click Deploy β†’ New deployment
  3. Select Web app
  4. Execute as: Me | Access: Anyone
  5. Click Deploy β†’ Copy the URL

First-Time Setup​

In the GAS Editor, run the SETUP_HE_THONG function:

  1. Open GAS Editor
  2. Select SETUP_HE_THONG from the function dropdown
  3. Click Run ▢️
  4. 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

Version Management

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​

1
πŸ“±

Open App

Tap the Home Screen icon (PWA-like)

2
πŸ’Έ

Enter Expense

Amount β†’ Category β†’ Person β†’ Save (~8 sec)

3
πŸ””

Get Notified

Instant Telegram confirmation with budget bar

4
πŸŒ™

9 PM Summary

Automatic daily recap with alerts

5
πŸ“Š

Weekly Review

Sunday summary with trends and goals

6
🧠

AI Insights

Ask Gemini to analyze your spending anytime

πŸ”§ Customization​

Categories​

Dashboard β†’ βš™οΈ Management β†’ Categories tab β†’ Add / Edit / Delete

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​

EntityGetAddUpdateDelete
GoalsgetGoals()addGoal(goal)updateGoal(goal)deleteGoal(rowIndex)
DebtsgetDebts()addDebt(debt)updateDebt(debt)deleteDebt(rowIndex)
AssetsgetAssets()addAsset(asset)updateAsset(asset)deleteAsset(rowIndex)
CategoriesgetCategories()addCategory(cat)updateCategory(cat)deleteCategory(rowIndex)
BudgetgetBudget()β€”updateBudget(budget)β€”
SettingsgetSettings()β€”updateSettings(settings)β€”

πŸ› Troubleshooting​

clasp push fails
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_TOKEN and TELEGRAM_CHAT_ID in 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() then setupTelegramWebhook(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_KEY in 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 dev for hot reload with mock data
  • Deploy only when changes are ready: npm run deploy
  • Always create a new version after deploying
  • Keep CATEGORY_MAP in 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​

πŸ“– Learning Resources​

πŸŽ‰ 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