Skip to main content

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:
1

Enter expense naturally

Type or speak your expense in plain language:
Coffee 200
Uber to office 150
Lunch with team 850
2

Click ADD button

The system sends your input to Google Apps Script which processes it with Gemini AI
3

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

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:
index.html
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

Misc

🛍️ Everything else

Privacy Mode

How It Works

Privacy mode masks income and balance amounts while keeping expense tracking functional:
index.html
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:
index.html
<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:
google_script.js
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:
index.html
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

index.html
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:
ColumnFieldTypeDescription
ADateTimestampISO 8601 format
BDescriptionStringExpense description
CAmountNumberTransaction amount in ₹
DCategoryStringFood, Transport, Tech, etc.
ETypeStringDEBIT 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”
Toggle privacy mode before sharing your screen to mask income amounts
Use the delete function to remove duplicate or incorrect entries

Build docs developers (and LLMs) love