Build Your Own Free, No-Code Expense Tracker: Part 3The Frictionless Upgrade
How to strip down your custom AppSheet expense tracker, build a visual dashboard, and let automated emails do the nagging for you.
The Inspiration: That One Friend We all know that one friend who needs multiple reminders to pay off their share of the trip bills. Yeah, we have one in our group too, and that person is exactly what motivated me to build this upgrade and write Part 2.
TL;DR After publishing Part 1 of this guide, the feedback was clear: authentication and email management are a hassle for quick weekend trips. In Part 2, we are stripping down our AppSheet expense tracker to make it frictionless. We are removing login screens, locking pre-filled rosters, polishing the UI, adding a visual donut chart for expense tracking, and writing a smart Apps Script that batches email notifications so you never spam your friends. The result is a plug-and-play web app you can drop in a WhatsApp group and start using instantly.
The Friction of The Ghost User A few months ago, I shared a guide on how to build a completely free expense-splitting app. Testing it in the wild during a recent group trip to Lonavala revealed a major bottleneck: setup friction.
In our original build, the app relied strictly on Google account authentication. This meant users had to log in, and if they used a different email than the one you typed in the database, the app threw errors. Adding members took too much time. When traveling with a pre-defined group of friends, you do not need enterprise-level security. You just need a fast, shared scratchpad, and a way to automatically remind people what they owe without ruining the group chat vibe.
Here are the complete steps to upgrade your tracker into a lean, mean, frictionless machine.
Step 1: Upgrading the Google Sheet Database We are going to adjust our database to support our new public approach and our smart email system.
- Open your Google Sheet database and go to the Members tab. Ensure your first column is Name and contains the names of everyone on the trip. Add a second column named Email and enter their addresses. If someone does not want emails, leave their cell blank.
- Go to the Expenses tab. Scroll to the first empty column on the far right and name it Notification_Sent. Leave all the rows beneath it completely blank.
Step 2: Ditching the Login Screen Instead of forcing everyone to authenticate via Google, we are changing the AppSheet security settings to allow public access.
- Open your AppSheet editor.
- Navigate to the Security tab on the left-hand menu.
- Find the setting labeled Require user sign-in and toggle it OFF. Your app is now a public web link. You can drop the URL into WhatsApp, and friends can instantly open it.
Step 3: Locking Down the Roster To prevent typos that create duplicate balances, we need to lock the pre-filled roster.
- In AppSheet, go to the Data menu and select your Members table.
- Look for the Table permissions setting.
- Change this setting to Read-Only. AppSheet will automatically remove the New button from the dropdown menus. Users can now only select from your verified list.
Step 4: Polishing the UI Let us make the app feel like a consumer fintech product by fixing the money fields and labels.
- Go to Data, select your Expenses table, and change the Amount column type to Price.
- Click the pencil icon next to the column, scroll down to Type Details, and type your currency symbol.
- To remove the plus and minus stepper buttons, change the Display mode to Standard and clear any number in the Increase/decrease step field.
- Scroll down to the Display section and change the Display name using quotation marks, such as =”Total Amount”, =”What for?”, and =”Shared With”.
Step 5: Adding the Expense Graph and Home Dashboard Next, we will add a donut chart to show exactly how much of the trip expenses each person has consumed.
- Go to Data and select your Expenses table. Add a Virtual Column named Per Person Share. Use the formula: [Amount] / COUNT([Split With]) and set the type to Price.
- Select your Members table. Add a Virtual Column named Total Share. Use the formula: SUM(SELECT(Expenses[Per Person Share], IN([_THISROW].[Name], [Split With]))) and set the type to Price.
- Go to the Views menu on the left sidebar and create a New View named Expense Breakdown. Select your Members table, set the View Type to Chart, choose Donut chart, and select your new Total Share column.
- Create one more New View named Home. Set the View Type to Dashboard. Under View entries, add Expense Breakdown and Settlements.
- To clean up the bottom navigation bar, click on your old standalone Settlements view and your Expense Breakdown view, and change their Position settings to ref. This hides them from the bottom menu while keeping them visible on your new Home dashboard.
Step 6: The Smart Notification Engine (Apps Script) Here is where we solve the problem of that one forgetful friend. We want to notify people when they are added to a split, but we do not want to spam their inbox if five expenses are logged in a row. This script sends an immediate email if they haven’t received one in 30 minutes; otherwise, it queues the message and sends a neat summary later.
Get Amey Nerkar’s stories in your inbox
Join Medium for free to get updates from this writer.
Click Extensions in Google Sheets, select Apps Script, delete the existing code, and paste this:
function processNewExpenses() { var ss = SpreadsheetApp.getActiveSpreadsheet(); var expensesSheet = ss.getSheetByName(“Expenses”); var membersSheet = ss.getSheetByName(“Members”); var queueSheet = ss.getSheetByName(“Email_Queue”);
if (!queueSheet) { queueSheet = ss.insertSheet(“Email_Queue”); queueSheet.appendRow([“Email”, “Message”, “Name”]); }
var membersData = membersSheet.getDataRange().getValues(); var emailMap = {};
for (var i = 1; i < membersData.length; i++) { var memberName = String(membersData[i][0]).trim(); var memberEmail = String(membersData[i][1]).trim(); if (memberName) { emailMap[memberName] = memberEmail; } }
var data = expensesSheet.getDataRange().getValues(); if (data.length <= 1) return;
var headers = data[0]; var notifyIndex = headers.indexOf(“Notification_Sent”);
if (notifyIndex === -1) { expensesSheet.getRange(1, headers.length + 1).setValue(“Notification_Sent”); notifyIndex = headers.length; }
var descIndex = headers.indexOf(“What for?”) !== -1 ? headers.indexOf(“What for?”) : headers.indexOf(“Description”); var amountIndex = headers.indexOf(“Total Amount”) !== -1 ? headers.indexOf(“Total Amount”) : headers.indexOf(“Amount”); var splitIndex = headers.indexOf(“Shared With”) !== -1 ? headers.indexOf(“Shared With”) : headers.indexOf(“Split With”); var paidByIndex = headers.indexOf(“Paid By”) !== -1 ? headers.indexOf(“Paid By”) : headers.indexOf(“Paid_By”);
var props = PropertiesService.getScriptProperties(); var now = new Date().getTime();
for (var r = 1; r < data.length; r++) { var row = data[r]; if (row[notifyIndex] !== “Processed”) { var splitWithRaw = String(row[splitIndex]); var description = String(row[descIndex]); var amount = row[amountIndex]; var paidBy = String(row[paidByIndex]);
var splitMembers = splitWithRaw.split(",").map(function(s) { return s.trim(); }).filter(Boolean);for (var m = 0; m < splitMembers.length; m++) {
var person = splitMembers[m];
if (person === paidBy) continue; var email = emailMap[person];
if (!email || email === "") {
console.error("Error: No email ID found for " + person + ". Mail not triggered.");
continue;
} var message = paidBy + " added an expense: " + description + " for " + amount + ". You have been added to the split.";
var lastSent = props.getProperty("lastSent_" + email); if (!lastSent || (now - parseInt(lastSent)) > (30 * 60 * 1000)) {
MailApp.sendEmail(email, "New Trip Expense Added", message);
props.setProperty("lastSent_" + email, now.toString());
} else {
queueSheet.appendRow([email, message, person]);
}
}
expensesSheet.getRange(r + 1, notifyIndex + 1).setValue("Processed");
}
} }
function flushEmailQueue() { var ss = SpreadsheetApp.getActiveSpreadsheet(); var queueSheet = ss.getSheetByName(“Email_Queue”); if (!queueSheet) return;
var data = queueSheet.getDataRange().getValues(); if (data.length <= 1) return;
var queueMap = {}; for (var i = 1; i < data.length; i++) { var email = data[i][0]; var message = data[i][1]; if (!queueMap[email]) { queueMap[email] = []; } queueMap[email].push(message); }
var props = PropertiesService.getScriptProperties(); var now = new Date().getTime();
for (var userEmail in queueMap) { var messages = queueMap[userEmail]; var emailBody = “Here is a summary of recent trip expenses you were added to:\n\n”; for (var j = 0; j < messages.length; j++) { emailBody += “- “ + messages[j] + “\n”; }
MailApp.sendEmail(userEmail, "Trip Expenses Summary", emailBody);
props.setProperty("lastSent_" + userEmail, now.toString());}
queueSheet.getRange(2, 1, data.length — 1, queueSheet.getLastColumn()).clearContent(); }
Step 7: Setting the Triggers Finally, we need to automate the script so it runs quietly in the background and takes over the job of reminding your friends.
- Click the clock icon on the left side of your Apps Script screen to open the Triggers menu.
- Click Add Trigger. Choose processNewExpenses, set event source to From spreadsheet, and event type to On change. Save it. This catches new bills instantly.
- Click Add Trigger again. Choose flushEmailQueue, set event source to Time-driven, select Minutes timer, and set it to Every 30 minutes. Save it. This acts as your throttle.
The Result With these adjustments, your app goes from a rigid spreadsheet tool to a frictionless mobile experience. Your friends open the web link, view the beautiful home dashboard, log expenses from a pre-filled list, and the system handles the gentle email reminders automatically. At the end of the trip, you just check the final balances and settle up via any UPI app. No more nagging required.









