Log an expense in about five seconds from your iPhone, Android phone or browser, and it lands in a Google Sheet that works as your database and dashboard. There's no server to host and nothing to pay for.
Try the web form (demo mode): nothing is sent until you add your own Web App URL.
┌───────────┐ POST JSON ┌────────────────────────┐ appendRow ┌─────────────────────┐
│ iPhone │ ───────────▶ │ Google Apps Script │ ──────────▶ │ Google Sheet │
│ Android │ │ Web App (doPost) │ │ Transactions │
│ Web form │ ◀─────────── │ • checks token │ ◀────────── │ Budgets (formulas) │
└───────────┘ JSON reply │ • validates input │ reads │ Summary (charts) │
│ • computes totals │ │ Log (every request)│
└────────────────────────┘ └─────────────────────┘
Logging from a phone: pick a category, then get a reply from the API with the running monthly total.
The sheet: every entry lands as a clean row (emoji stripped, server timestamp, device name), and the Summary tab charts it.
Web form (also the live demo):
- Three ways to log: an iPhone Shortcut (triggered by Back Tap), an Android MacroDroid macro, or a mobile-friendly web form.
- Secret-token auth: the Web App URL alone isn't enough to write to your sheet.
- Input validation: rejects negative, zero or absurd amounts and unknown categories, and neutralises spreadsheet formula injection in text fields.
- Instant feedback: every entry replies with the month's running total.
- Budgets: set a monthly limit per category and get warned at 80% and when you go over.
- Summary endpoint:
GET ?action=summaryreturns this month's spending by category. - Request log: each attempt is written to a Log sheet with the exact error, which makes phone automations easy to debug.
- Concurrency-safe:
LockServiceprevents clashes when two family members log at the same moment. - Auto-built dashboard:
setup()creates the sheets, formulas and charts. - Tested: 19 automated tests run the real
Code.gsagainst an in-memory mock of the Apps Script services, on every push (GitHub Actions).
Google Apps Script (V8 JavaScript) · Google Sheets · HTML/CSS/vanilla JS · iOS Shortcuts · MacroDroid · Node.js test runner · GitHub Actions
apps-script/
Code.gs # Web App: doPost, doGet, validation, setup()
appsscript.json # manifest (runtime, web app access, scopes)
web/
index.html # standalone input form (no build step)
docs/
iphone-shortcut.md # step-by-step Shortcut build
android-macrodroid.md
tests/
gas-mock.js # in-memory stand-ins for SpreadsheetApp, PropertiesService, etc.
code.test.js # validation, auth, end-to-end doPost/doGet, budgets, logging
.github/workflows/
ci.yml # syntax check + tests on every push
index.html # redirects GitHub Pages visitors to the web form
- Create the sheet. Make a new Google Sheet and open Extensions → Apps Script.
- Add the code. Paste
apps-script/Code.gsinto the editor. (Optional: enable Show "appsscript.json" in Project Settings and paste the manifest.) - Run
setup(). Choosesetupin the function dropdown and click Run. Approve the permissions (Google calls unverified personal scripts "unsafe"; click Advanced → Go to project). Open Execution log and copy your secret token. - Deploy. Deploy → New deployment → Web app, Execute as: Me, Who has access: Anyone. Copy the Web App URL (ends in
/exec). - Test. Run
testPost()in the editor. A test row should appear in Transactions. - Connect your devices:
- iPhone → docs/iphone-shortcut.md
- Android → docs/android-macrodroid.md
- Browser → open
web/index.html(or host it on GitHub Pages) and enter the URL and token in Settings.
- Set budgets (optional). Fill in Monthly Limit on the Budgets sheet.
After editing
Code.gs, redeploy with Deploy → Manage deployments → Edit → New version. Otherwise the old code keeps running.
{
"token": "YOUR_TOKEN",
"amount": 120,
"category": "Food & Dining 🍔",
"paymentMode": "UPI 📱",
"remarks": "Lunch",
"deviceName": "Monmeet iPhone"
}amountandcategoryare required.- Category and payment mode match the lists in
Code.gs, ignoring case, emoji and symbols, so"UPI 📱"is stored asUPI. That lets phone menus show emoji labels while the sheet stays clean. deviceanddeviceNameare both accepted.- Send the token in the body (recommended).
/exec?token=...in the URL is also accepted. - Any
date,monthortimethe client sends is ignored. The server clock stamps every row, so a phone with a wrong clock can't misfile expenses.
Response
{
"ok": true,
"row": 42,
"monthTotal": 4350,
"categoryTotal": 1820,
"budget": { "limit": 2000, "spent": 1820, "remaining": 180, "percentUsed": 91, "over": false },
"message": "Logged ₹120 for Food & Dining. September total: ₹4350. 91% of Food & Dining budget used."
}Errors return { "ok": false, "error": "..." }.
{ "ok": true, "month": "2026-09", "total": 4350, "byCategory": { "Food & Dining": 1820, "Transportation": 900 } }Requires Node.js 20 or newer. No dependencies to install.
npm testThe tests load the real apps-script/Code.gs into a Node sandbox with mocked Google services (tests/gas-mock.js), so the same code that runs in Apps Script is what gets tested. They cover emoji label matching, amount validation, formula-injection protection, token checks, budget warnings, and that the token is never written to the Log sheet.
- The token is stored in Script Properties, not in code, so the repo holds no secrets.
- Run
rotateToken()if the token leaks, then update your devices. - Anyone with the URL and token can add rows but can't read the sheet. The summary endpoint exposes totals only.
- Never commit your real URL or token. Use placeholders like
YOUR_WEB_APP_URL. - Prefer the token in the request body: URLs can show up in logs and screenshots.
- Every POST attempt is recorded in a Log sheet (result, error, which fields arrived, but never the token), so failures can be debugged without a phone attached.
- Categories / payment modes: edit
CATEGORIESandPAYMENT_MODESinCode.gs, the lists inweb/index.html, and your phone shortcut. - Currency: set the
CURRENCYscript property (default₹). - Time zone:
timeZoneinappsscript.json.
- A success message isn't proof of success. My phone automations showed "Saved successfully" while nothing was reaching the sheet, because the notification text was hard-coded. Showing the server's real JSON reply on the phone surfaced the actual error straight away.
- Saving code isn't deploying it. In Apps Script, the live web app keeps running the old version until you deploy a new one. I added a
versionfield to the health check so I can confirm which code is actually live. - Where you send a credential matters. Both phones returned
Unauthorizedbecause the token was in the URL while the deployed code only read it from the request body. Moving the token into the body fixed it, and the API now accepts both. - Make failures visible. I added a Log sheet that records every request's result, error and received fields (never the token). It turned blind trial and error into reading one row.
- Spreadsheets have their own type rules. Google Sheets silently turned
2026-09into a date, so my monthly chart showed46266. I fixed it by storing the month as text and building the summary from the timestamp. - Validate at the server, not the client. Phone menus send labels like
UPI 📱. Normalising them on the server (and rejecting anything unknown) keeps the data clean no matter which device sends it. - Testing code that only runs on Google's servers. I wrote a small mock of
SpreadsheetApp,PropertiesServiceand friends so the realCode.gsruns under Node's test runner, and GitHub Actions runs it on every push.
- Recurring expenses (rent, subscriptions) added automatically by a time-based trigger
- Monthly email report
- Undo the last entry from the phone
- Income tracking and savings rate
MIT. See LICENSE.







