Skip to content

Repository files navigation

SheetSpend

CI Google Apps Script License: MIT

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)│
                            └────────────────────────┘             └─────────────────────┘

Screenshots

Logging from a phone: pick a category, then get a reply from the API with the running monthly total.

iPhone Shortcut category menu   Android MacroDroid amount prompt

Android notification showing the JSON reply with the monthly total

iPhone confirmation notification

The sheet: every entry lands as a clean row (emoji stripped, server timestamp, device name), and the Summary tab charts it.

Transactions sheet with entries from iPhone and Android

Pie chart of this month's spending by category

Web form (also the live demo):

SheetSpend web form SheetSpend web form in dark mode

Features

  • 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=summary returns 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: LockService prevents 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.gs against an in-memory mock of the Apps Script services, on every push (GitHub Actions).

Tech stack

Google Apps Script (V8 JavaScript) · Google Sheets · HTML/CSS/vanilla JS · iOS Shortcuts · MacroDroid · Node.js test runner · GitHub Actions

Repo structure

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

Setup

  1. Create the sheet. Make a new Google Sheet and open Extensions → Apps Script.
  2. Add the code. Paste apps-script/Code.gs into the editor. (Optional: enable Show "appsscript.json" in Project Settings and paste the manifest.)
  3. Run setup(). Choose setup in 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.
  4. Deploy. Deploy → New deployment → Web app, Execute as: Me, Who has access: Anyone. Copy the Web App URL (ends in /exec).
  5. Test. Run testPost() in the editor. A test row should appear in Transactions.
  6. Connect your devices:
  7. 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.

API

POST /exec: log an expense

{
  "token": "YOUR_TOKEN",
  "amount": 120,
  "category": "Food & Dining 🍔",
  "paymentMode": "UPI 📱",
  "remarks": "Lunch",
  "deviceName": "Monmeet iPhone"
}
  • amount and category are required.
  • Category and payment mode match the lists in Code.gs, ignoring case, emoji and symbols, so "UPI 📱" is stored as UPI. That lets phone menus show emoji labels while the sheet stays clean.
  • device and deviceName are both accepted.
  • Send the token in the body (recommended). /exec?token=... in the URL is also accepted.
  • Any date, month or time the 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": "..." }.

GET /exec?action=summary&token=...: this month's totals

{ "ok": true, "month": "2026-09", "total": 4350, "byCategory": { "Food & Dining": 1820, "Transportation": 900 } }

Running the tests

Requires Node.js 20 or newer. No dependencies to install.

npm test

The 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.

Security notes

  • 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.

Customising

  • Categories / payment modes: edit CATEGORIES and PAYMENT_MODES in Code.gs, the lists in web/index.html, and your phone shortcut.
  • Currency: set the CURRENCY script property (default ₹).
  • Time zone: timeZone in appsscript.json.

What I learned

  • 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 version field to the health check so I can confirm which code is actually live.
  • Where you send a credential matters. Both phones returned Unauthorized because 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-09 into a date, so my monthly chart showed 46266. 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, PropertiesService and friends so the real Code.gs runs under Node's test runner, and GitHub Actions runs it on every push.

Roadmap

  • 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

License

MIT. See LICENSE.

About

Log expenses from iPhone/Android in 5 seconds into Google Sheets via an Apps Script API

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages