The privacy boundary is a type-system rule, not a promise. The interface roster is small and named. Orchestration is the moat — not the model.
Domain at the center is pure logic — no SDK imports allowed. Data plugs in from the outside. Presentation reads from domain only. Crossings are interface-shaped, never direct.
UI screens — Flutter widgets in mobile, React Server Components in backoffice.
Interfaces, pure logic, the privacy-bucket types. The whole product is testable without a phone.
RawSms, Transaction, AnonymizedInsight typesFirebase, the on-device extraction engine, geofencing, the cloud LLM gateway. Adapters that satisfy domain interfaces.
Be honest about what's actually de-linkable vs what's only pseudonymous vs what's user-authored. Claiming more anonymity than the data has fails ODPC review.
RemoteRepository. The single anonymize() function is the only conversion path.
RawSms, Transaction, Merchant, precise location, Conversation, IntentTokenanonymize(); allowlisted method signatures on RemoteRepository.
AnonymizedInsight (category bucket, amount band, weekday/weekend, no merchant names), anonymized aggregate countersPseudonymousFcmToken, Firebase Auth UID, install IDAnonymizedInsight, category buckets, and the user's first name. Request/response only — no cloud persistence.
AnonymizedInsight only.
The lint rule blocks any attempt to write a Local-only type to RemoteRepository. It does not catch user free-text in the conversation — that path is governed by the consent surface and bundling rules in ADR-0006, not by the type system. Location is consumed locally only; there is no RemoteRepository method that accepts a location type — enforced by absence.
The on-device database is encrypted with a key held in hardware where available — StrongBox + the Trusted Execution Environment, on top of Android file-based encryption (ADR-0017). Presence-gating the key was rejected because headless background analytics need it with no user present; the accepted residual is a rooted, unlocked, live device. The point stands: the data lives on the phone, encrypted, and the privacy claim is carried by architecture, not copy.
Nudgey learns how your money moves from your phone — quietly, on-device. Nothing leaves it. The extraction engine turns the financial activity already on the phone into structured transactions, fees, balances, and counterparties without a network round-trip. Two stages, tuned for a mid-tier Kenyan Android.
Deterministic template parsers handle the rigidly-formatted providers (M-Pesa, and the templated parts of major banks) — fast, exact, zero model cost. A sender-based pre-filter means promotional and non-financial messages never reach the model at all.
The messy, unrecognized tail falls back to an on-device LLM — Gemma 3 1B int4 on LiteRT-LM, running off the UI thread with greedy decoding. Migrated off the deprecated MediaPipe tasks-genai runtime; validated end-to-end on a mid-tier A56 and a flagship Fold 7.
A battery-safe WorkManager isolate keeps draining the backlog and new activity after the app is closed — chunked, throttled with backoff, requiresBatteryNotLow, charging/idle-preferred. The regex stage keeps model load to the rare tail, so background work stays cheap.
~N% complete · still building). If real candidates aren't ready at the reveal, an honest holding card shows — placeholder/fictitious data is forbidden.~N% sure position figure climbs as evidence accumulates.Every concrete dependency in the product implements one of these. Every screen in mobile or backoffice consumes through these. Adding one is a spec amendment.
| # | Interface | Purpose | V1 implementation |
|---|---|---|---|
| 1 | SmsSource | On-device live + historical financial-message intake | AndroidSmsSource |
| 2 | TransactionParser | Raw message → Transaction (regex-first, on-device LLM fallback) | Regex template parsers → LiteRT-LM (Gemma 3 1B int4) tail |
| 3 | LocalRepository | On-device persistence | DriftLocalRepository |
| 4 | RemoteRepository | Anonymized cloud sync only | FirestoreRemoteRepository |
| 5 | AuthService | Phone-based auth | FirebaseAuthService |
| 6 | LocationService | Location + geofence | AndroidLocationService |
| 7 | NotificationService | Local + push | FlutterFcmNotificationService |
| 8 | PaymentService | Premium subscription | MpesaStkPaymentService |
| 9 | AnalyticsService | Anonymized usage | FirebaseAnalyticsService |
| 10 | PatternDetector | Recurring vs habitual classification | RulesPatternDetector |
| 11 | NudgeGenerator | Daily swipe-card nudge text | TemplateNudgeGenerator (V1.5: LlmNudgeGenerator) |
| 12 | RemoteConfigService | Feature flags + thresholds | FirebaseRemoteConfigService |
| 13 | RetrospectiveAnalyzer | Insight candidate generation | RulesRetrospectiveAnalyzer |
| 14 | InsightRanker | Top-k insight selection | WeightedInsightRanker |
| 15 | InsightScheduler | Cadence orchestration | RulesInsightScheduler |
| 16 | ConversationStore | Local chat history + intent tokens | DriftConversationStore |
| 17 | LlmGateway | Stateless cloud LLM call (conversation + period story) — distinct from the on-device LiteRT-LM engine | Cloud LlmGateway (Gemini Flash / Haiku class) |
| 18 | JourneyOrchestrator | UI flow selection from registry | RulesJourneyOrchestrator |
| 19 | InstalledFinanceAppsDetector | Known finance app presence | AndroidInstalledFinanceAppsDetector |
| 20 | InstitutionRegistry | Typed registry of finance institutions | RemoteConfigInstitutionRegistry |
| 21 | InstitutionResolver | RawSms → FinanceInstitution? | HybridInstitutionResolver |
The roster grew as V1 filled out — account recovery + encrypted backup (SB16), the savings ledger (SB14), and bill radar + discreet reminders (SB15) each register their own interfaces against the same discipline. V2 adds BankApiAdapter and BankApiRegistry for authenticated bank-API integration (KCB Buni, Equity Jenga). Architecture forward-builds for it; no V1 work.
Three capabilities that ride on the same on-device data and privacy boundary.
An encrypted on-device backup plus lost-number email recovery (SB16), so a user who changes SIM or phone doesn't lose their history. Keys stay in the user's control; the backup is opt-in, not auto-sync.
A compounding, honest record of money saved (SB14) — the keystone of the long-term moat. Built from the same on-device transaction stream the Money Map reads.
Recurring bills (SB15) detected from cadence, with a discreet reminder channel that respects quiet hours and a daily cap. Nudgey never nags — it nudges.
A typed registry replaces any hardcoded bank list. Sender-ID-first resolution, heuristic fallback, ops-triage for the long tail. The catalogue covers 58 Kenyan institutions: M-Pesa, all major banks (including Co-op / MCo-opCash and NCBA / Loop), saccos, lenders, and insurers. New institutions are added without an app release via Remote Config.
BANK · MOBILE_MONEY · FINTECH_LENDER · MICROFINANCE · SACCO · INSURANCE
Category-awareness propagates through the product: voice templates have category variants (the warning copy for a fintech-lender debt cycle differs from a sacco contribution), insights reason about debt vs savings discipline, and the Money Map spans every institution at once — including the silent providers whose deposits are inferred from balance gaps.
V1's agentic feel is mostly orchestration. The insights engine is the orchestration core.
Runs a battery of pure-function analyzers over a transaction window. Each produces an InsightCandidate with novelty + strength scores.
Picks the top-k candidates by combined score, deduplicates, applies fairness rules — don't surface the same insight twice in a month.
Owns cadence: weekly Sunday Drop (one insight, 9–10 AM Sunday), spontaneous (when a fresh pattern triggers), monthly (period story).
Once Nudgey has learned enough from the phone, it paints a real day-one reveal — top merchants, money-out network, recurring bills, fees, day-of-week patterns — and seeds the living Money Map. Real, never fake; honest about what it's still building. The viral moment.
Interesting findings surface as the map grows — not only on Sundays — into the daily stack, respecting quiet hours and a daily cap. Plus the Sunday Drop: one weekly insight, 9–10 AM Sunday (free: 1/week; premium: full archive).
Monthly recap. Premium tier: LLM-generated narrative via the cloud LlmGateway, prompt assembled exclusively from AnonymizedInsight-class inputs and the user's first name (no merchant names, no exact amounts). Free tier: templated via NudgeGenerator. Routing in PeriodStoryBuilder via a PremiumStatus check.
Separate from the on-device LiteRT-LM extraction engine: the cloud LLM is reached only through one interface — LlmGateway — and only for two V1 use cases. The on-device engine never leaves the phone; this is the only path that calls out.
5 turns/day on free tier after a 30-day full-access trial. Premium: unlimited. User free-text crosses to cloud (consent gate); response streamed back. No persistence on the cloud side. On-device memory carries history.
Premium tier only. Assembled bundle is AnonymizedInsight-class only — category buckets, amount bands, the user's first name. No merchant names, no exact amounts. Free tier gets a templated narrative via NudgeGenerator.
NudgeGenerator from TemplateNudgeGenerator to LlmNudgeGenerator so daily swipe nudges become LLM-rendered. Same interface, same templates, just better copy.BankApiAdapter and BankApiRegistry for authenticated bank integrations (KCB Buni, Equity Jenga).