Overview
SpendWisely George provides flexible expense tracking through both manual entry and AI-powered natural language processing. Every transaction is automatically categorized, stored in Google Sheets, and reflected in your real-time balance.
Adding Expenses
AI-Powered Entry
The primary method uses natural language input with Gemini AI for intelligent parsing:
Enter expense naturally
Type or speak your expense in plain language: Coffee 200
Uber to office 150
Lunch with team 850
Click ADD button
The system sends your input to Google Apps Script which processes it with Gemini AI
AI extracts details
The AI automatically determines:
Description (e.g., “Coffee”)
Amount (e.g., 200)
Category (Food, Transport, Tech, etc.)
Type (DEBIT or CREDIT)
AI Processing Flow
index.html (Frontend)
google_script.js (AI Processing)
async function sendToAI ( mode ) {
const input = document . getElementById ( 'omniInput' );
const txt = input . value ;
if ( ! txt ) return ;
input . value = "" ;
input . placeholder = mode === 'add' ? "Adding..." : "Thinking..." ;
disableInput ( true );
const card = document . getElementById ( 'aiCard' );
const icon = document . getElementById ( 'aiIcon' );
const tip = document . getElementById ( 'aiTipBox' );
icon . innerHTML = '<div class="loader"></div>' ;
if ( mode === 'add' ) {
card . className = "card p-4 bg-blue-50 border-blue-200 min-h-[80px]" ;
tip . innerText = "Processing..." ;
tip . className = "text-[11px] font-bold text-blue-800 italic leading-relaxed flex-1" ;
}
try {
const res = await fetch ( SCRIPT_URL , {
method: 'POST' ,
body: JSON . stringify ({ action: "process_text" , text: txt , mode: mode })
});
const data = await res . json ();
if ( data . status === "Success" ) {
icon . innerText = mode === 'add' ? "✅" : "🧠" ;
tip . innerText = data . ai_response ;
if ( mode === 'add' ) {
appData . balance = parseFloat ( data . balance );
if ( data . parsed ) appData . expenses . push ( data . parsed );
renderUI ();
}
}
} catch ( e ) {
if ( mode === 'add' ) {
handleManualFallback ( txt );
}
}
}
Manual Fallback Mode
If AI processing fails (API limits, network issues), the system automatically falls back to regex-based parsing:
async function handleManualFallback ( text ) {
const amountMatch = text . match ( / \d + ( \. \d + ) ? / );
const amount = amountMatch ? parseFloat ( amountMatch [ 0 ]) : 0 ;
const desc = text . replace ( amountMatch ? amountMatch [ 0 ] : '' , '' ). trim () || "Manual Expense" ;
const type = ( desc . toLowerCase (). includes ( 'salary' ) || desc . toLowerCase (). includes ( 'income' ))
? 'CREDIT' : 'DEBIT' ;
if ( amount > 0 ) {
const manualTx = {
date: new Date (). toISOString (),
description: desc ,
amount: amount ,
category: type === 'CREDIT' ? 'Income' : 'General' ,
type: type
};
appData . balance = ( type === 'CREDIT' )
? ( appData . balance + amount )
: ( appData . balance - amount );
appData . expenses . push ( manualTx );
renderUI ();
document . getElementById ( 'aiIcon' ). innerText = "⚡" ;
document . getElementById ( 'aiTipBox' ). innerText =
`Manual Fallback: Saved " ${ desc } " (₹ ${ amount } )` ;
fetch ( SCRIPT_URL , {
method: 'POST' ,
body: JSON . stringify ({ action: "manual_add" , ... manualTx })
});
}
}
Automatic Categorization
The AI categorizes expenses into predefined categories with custom icons:
Income 💰 Salary, freelance, other income
Food 🍔 Restaurants, groceries, coffee
Transport ⛽ Uber, fuel, parking
Tech ⚡ Software, gadgets, subscriptions
Invest 📈 Mutual funds, stocks, savings
Privacy Mode
How It Works
Privacy mode masks income and balance amounts while keeping expense tracking functional:
function renderUI () {
// Eye icon (showBalance) overrides the general privacy setting
const showBal = appData . showBalance || ! appData . privacy ;
// Update Cards
const updateDisplay = ( id , val ) => {
const el = document . getElementById ( id );
if ( showBal && el ) el . innerText = `₹ ${ Number ( val ). toLocaleString ( 'en-IN' ) } ` ;
else if ( el ) el . innerText = "••••••" ;
};
const totalBal = appData . balance + ( appData . bankBalance || 0 ) + ( appData . portfolioValue || 0 );
updateDisplay ( 'uiBalance' , totalBal );
updateDisplay ( 'uiBankBal' , appData . bankBalance || 0 );
updateDisplay ( 'uiPortfolio' , appData . portfolioValue || 0 );
// Income masking in transaction list
const list = document . getElementById ( 'txList' );
list . innerHTML = '' ;
[ ... appData . expenses ]. reverse (). slice ( 0 , 20 ). forEach ( tx => {
const isIncome = tx . type === 'CREDIT' || tx . category === 'Income' || tx . category === 'Salary' ;
const color = isIncome ? 'text-green-600' : 'text-slate-900' ;
const icon = getIcon ( tx . category );
let amountDisplay = `₹ ${ Number ( tx . amount ). toLocaleString ( 'en-IN' ) } ` ;
// Privacy mode masks income amounts
if ( isIncome && appData . privacy && ! appData . showBalance ) {
amountDisplay = "••••••" ;
}
// Render transaction card...
});
}
Toggle Privacy
Users can toggle privacy mode in Settings:
< div class = "flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100" >
< div >
< p class = "font-bold text-sm text-slate-800" > Privacy Mode </ p >
< p class = "text-[10px] text-slate-400" > Mask Income </ p >
</ div >
< div class = "relative inline-block w-12 align-middle select-none transition duration-200 ease-in flex-shrink-0" >
< input type = "checkbox" name = "toggle" id = "privacyToggle" checked
class = "toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
onclick = " pushSettings ()" />
< label for = "privacyToggle"
class = "toggle-label block overflow-hidden h-6 rounded-full bg-slate-300 cursor-pointer" ></ label >
</ div >
</ div >
Balance Calculation
Balance is calculated server-side in Google Apps Script for accuracy:
function calculateBalance ( sheet ) {
const data = sheet . getDataRange (). getValues ();
let bal = 0 ;
// Start loop from 1 to skip Header row
for ( let i = 1 ; i < data . length ; i ++ ) {
const amount = parseFloat ( data [ i ][ 2 ]); // Column C is Amount
const type = data [ i ][ 4 ]; // Column E is Type
if ( ! isNaN ( amount )) {
if ( type === 'CREDIT' ) {
bal += amount ;
} else {
bal -= amount ; // Assume DEBIT or blank is expense
}
}
}
return bal ;
}
Transaction Management
Viewing Transactions
The UI displays the 20 most recent transactions with full details:
const list = document . getElementById ( 'txList' );
list . innerHTML = '' ;
[ ... appData . expenses ]. reverse (). slice ( 0 , 20 ). forEach ( tx => {
const isIncome = tx . type === 'CREDIT' || tx . category === 'Income' || tx . category === 'Salary' ;
const color = isIncome ? 'text-green-600' : 'text-slate-900' ;
const icon = getIcon ( tx . category );
let amountDisplay = `₹ ${ Number ( tx . amount ). toLocaleString ( 'en-IN' ) } ` ;
if ( isIncome && appData . privacy && ! appData . showBalance ) {
amountDisplay = "••••••" ;
}
list . innerHTML += `
<div class="card p-4 group active:scale-[0.98] transition-transform">
<div class="flex justify-between items-center">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-xl border border-slate-100">
${ icon }
</div>
<div>
<p class="font-bold text-sm text-slate-800 leading-tight"> ${ tx . description } </p>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-wider"> ${ tx . category } </p>
</div>
</div>
<div class="text-right">
<p class="font-black ${ color } text-lg tracking-tight">
${ isIncome ? '+' : '' }${ amountDisplay }
</p>
<button onclick="deleteTx(' ${ tx . description } ', ${ tx . amount } )"
class="text-[10px] text-red-500 font-bold opacity-0 group-hover:opacity-100 transition-opacity uppercase tracking-widest">
Delete 🗑️
</button>
</div>
</div>
</div>
` ;
});
Deleting Transactions
async function deleteTx ( desc , amt ) {
if ( ! confirm ( `Delete " ${ desc } "?` )) return ;
setStatus ( "DELETING..." , "text-orange-500" , "bg-orange-500" );
const res = await fetch ( SCRIPT_URL , {
method: 'POST' ,
body: JSON . stringify ({
action: "delete" ,
description: desc ,
amount: amt
})
});
const data = await res . json ();
if ( data . status === "Deleted" ) {
appData . balance = parseFloat ( data . balance );
appData . expenses = appData . expenses . filter ( tx =>
! ( tx . description === desc && tx . amount == amt )
);
renderUI ();
setStatus ( "ONLINE" , "text-green-500" , "bg-green-500" );
}
}
Data Structure
Expenses are stored in Google Sheets with the following schema:
Column Field Type Description A Date Timestamp ISO 8601 format B Description String Expense description C Amount Number Transaction amount in ₹ D Category String Food, Transport, Tech, etc. E Type String DEBIT or CREDIT
The Google Apps Script automatically calculates balance by summing all CREDIT transactions and subtracting all DEBIT transactions from the sheet data.
Best Practices
The AI works best with conversational input:
✅ “Coffee at Starbucks 250”
✅ “Uber to airport 450”
❌ “250” (too vague)
The AI provides contextual insights by comparing to your history:
“Higher than your usual coffee spend”
“Third Uber ride this week”
Enable privacy mode for screenshots
Toggle privacy mode before sharing your screen to mask income amounts
Review transactions regularly
Use the delete function to remove duplicate or incorrect entries