01/Apps

Mooney

Mooney is a voice-first Android expense tracker built and operated by OCTYN, live on Google Play. You say one sentence and a categorised transaction is saved, end to end in under five seconds on a live network. Speech runs through Deepgram, intent through Groq, and every third-party call goes through a server rather than the phone.

Liveusemooney.app

Mooney in use

The Mooney capture screen on Android, mid recording.
capture
A logged transaction in the Mooney ledger.
ledger
Mooney's spending breakdown for the month.
breakdown
Mooney's receipt scan result before it is saved.
receipt scan
Mooney's Google Play listing.
google play
usemooney.app, the product site with the self-editing blog.
usemooney.app

Deepgram Nova-3 STT, Groq intent parsing, Hive local store, passive transaction capture, on-device receipt OCR. Cow mascot as accountability coach.

Mooney is OCTYN's own product. We built it, shipped it to Google Play production, and we operate it.

1. The situation

First commit 2026-04-10. Logging an expense by hand costs more than the expense is worth, so people stop. Say one sentence, get a categorised transaction, and the habit survives. The constraint was the phone: Android, where the microphone has to work from a home screen widget without opening the app, and every provider sits on a free tier with a hard ceiling.

2. Why it was hard

Android will not let one app hold two Dart VMs in one process, so the widget microphone runs in a separate :overlay process. It cannot open the local database, because the main app holds the lock, and it has no auth session. A capture from the widget has nowhere to write and nobody to write as. Naive versions work in development and fail on a device, because Android wipes the cache directory under memory pressure, which is exactly when widgets fire.

3. What was built

Flutter, Riverpod, Hive. Local-first, so every feature works with no network. Voice records AAC, goes to Deepgram Nova-3 pinned to en-IN with numeral output and keyterm boosting on currency words, then to Groq llama-3.1-8b in JSON mode, escalating to llama-3.3-70b only on UNKNOWN.

Every third-party call goes through a Supabase edge function instead of straight from the device. No API key ships inside the APK, keys rotate without a release, and rate limiting sits server side where it sees every user instead of one phone. Sixteen functions sit on that seam.

4. What it does under load

Repo version 1.0.1+11 on 2026-06-20, about 87 days from first commit to the week-one install reading of roughly 50. Audited against provider ceilings on 2026-04-25: three Groq keys capped at 28 requests a minute each against a ceiling of 30, and two Deepgram keys carrying roughly 1.4 million voice turns a month, which puts the ceiling at 1,500 to 2,000 daily active users before requests queue.

End of speech to saved row lands under five seconds. That figure comes from screen recordings on a Samsung S23 over a live network, not from instrumentation, so it is a demonstrated wall-clock time rather than a p95. We publish it as what it is. Per-stage timing is not yet recorded in the app, which is the first thing section 8 says we would change.

5. What happens when it breaks

Every provider has a documented failure path, audited in docs/API_FALLBACK_AUDIT.md rather than assumed.

Key rotation is one shared module: a rolling 60 second window per key, a 60 second cooldown on any 429, 5xx or throw, round-robin to the least loaded healthy key, and a log line per hop naming the key and the reason. Intent degrades through four layers, Groq 8b to Groq 70b to Cerebras to OpenRouter, so all three Groq keys can die at once and users see nothing. Receipts degrade to Groq text-only, then Gemini, then a 502 the client renders as "couldn't parse, tap to retry". Speech has no second provider. If both Deepgram keys fail, voice stops. Known, deliberate, written down.

Errors land in five places, split by what each can see: Sentry for Dart stack traces and release tags, PostHog for a parallel client_error event showing what the user was doing before the throw, Supabase for server-side failures, Play Console for native crashes and ANRs the Flutter engine never sees, RevenueCat for billing. A decision table names which surface to open first per symptom, and after every release a ten minute smoke test fires a deliberate crash to confirm it still arrives. Rollback is the Play staged rollout, so a bad build halts at a percentage rather than being recalled.

6. Who maintains it, and what that costs in attention

Two people, in practice one per incident.

Steady state is quiet by construction. Local-first means a Supabase outage pages nobody, key exhaustion self-heals, provider outages route around themselves, and the rotator writes down what it did, so the question afterwards is answerable without reproducing anything.

What costs attention is Android, not the AI. OEM behaviour changes break the notification listener and the widget host silently. They arrive as a Play vitals number, never as an exception, and they need a device on a cable rather than a laptop. That cost is seasonal, tied to OS releases. The other standing commitment is the post-release smoke test, because skipping it is how monitoring dies unnoticed.

7. What changes when requirements change in six months

Configuration, no deploy: API keys, per-tier daily OCR caps, free and paid limits, which provider is primary. Adding a fourth key or moving one to a paid tier changes no code, because the least-loaded logic routes to the higher ceiling on its own.

A small code change: a new expense field, a new category set, another speech language, a fifth fallback. Edge functions deploy independently of the app, so a server-side change reaches every installed phone immediately without a Play review. That is why the seam is where it is.

A larger code change: anything altering what the model returns, because the prompt, the sanitiser and the expense model are coupled, or anything touching the overlay, because two processes is structural.

A rebuild: team accounts, because storage is a local JSON blob with no schema and no migrations, right for single-user local-first and wrong for shared data. Also iOS, because the widget, listener and overlay are Kotlin.

A new provider is an afternoon. A new field is a day. Shared accounts is a storage rewrite, and we would say so before taking the work.

8. What we would do differently

Instrument latency on day one, because the product claim is about speed and nothing measures it. Give speech a second provider, since every other leg has one. And build the on-device path first: the ML Kit short-circuit arrived after the cloud path existed, and built first it would have shaped the whole receipt flow.

More apps

what OCTYN ships for people to use2

Want something like Mooney for your operation?

Book a call →