nudgey.
Engine · 04

The architecture
that protects the user.

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.

Hexagonal layers

Three concentric rings.

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.

RING 1

presentation/

UI screens — Flutter widgets in mobile, React Server Components in backoffice.

  • No data imports allowed
  • Goes through domain only
  • Renders from typed view models
RING 0 · CORE

domain/

Interfaces, pure logic, the privacy-bucket types. The whole product is testable without a phone.

  • No SDK imports allowed
  • Defines all 21 V1 interfaces
  • Owns RawSms, Transaction, AnonymizedInsight types
RING 2

data/

Firebase, the on-device extraction engine, geofencing, the cloud LLM gateway. Adapters that satisfy domain interfaces.

  • No domain logic allowed
  • One adapter per interface
  • SDK code is constrained here
Privacy boundary

Six buckets, type-system enforced.

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.

Local-only
type-system enforced
The strongest claim. Lint rule blocks any write of these types to RemoteRepository. The single anonymize() function is the only conversion path.
RawSms, Transaction, Merchant, precise location, Conversation, IntentToken
Anonymized cloud-bounded
de-linkable
Truly de-linkable — no key on device or in cloud reconstructs the user. Produced exclusively by anonymize(); allowlisted method signatures on RemoteRepository.
AnonymizedInsight (category bucket, amount band, weekday/weekend, no merchant names), anonymized aggregate counters
Pseudonymous cloud-bounded
treated as PII
FCM tokens are stable per-device identifiers. Treated as personal data under ODPC/POPIA/GDPR; minimization rules apply. Type-wrapped, scope-limited Firestore writes.
PseudonymousFcmToken, Firebase Auth UID, install ID
Auth-stored PII
isolated
Phone number in E.164 — required for sign-in. Isolated to Firebase Auth's storage; never copied to Firestore documents. Backoffice DSAR flow includes Auth user deletion.
+254… in Firebase Auth only
Stateless cloud transit (no PII)
request/response
LLM call for the period-story narrative — assembled exclusively from AnonymizedInsight, category buckets, and the user's first name. Request/response only — no cloud persistence.
LLM bundle for monthly recap (premium)
Stateless cloud transit (consented free-text)
user-authored
The carve-out. Talk-to-Nudgey messages — user's typed input goes to the LLM verbatim; can contain merchant names, amounts, life context. Explicit consent surface in onboarding + on first use of the conversation. No sample-and-audit retention. Quality monitored via on-device aggregate counters reported as AnonymizedInsight only.
User-typed conversation messages

What the lint catches and what it doesn't

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.

At-rest: StrongBox-backed encryption

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.

On-device engine

Nudgey learns how your money moves,
on-device.

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.

1 · Regex-first parser

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.

2 · LiteRT-LM fallback

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.

3 · Background isolate

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.

Day-one truth

Interface roster

A small, named interface roster.

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.

#InterfacePurposeV1 implementation
1SmsSourceOn-device live + historical financial-message intakeAndroidSmsSource
2TransactionParserRaw message → Transaction (regex-first, on-device LLM fallback)Regex template parsers → LiteRT-LM (Gemma 3 1B int4) tail
3LocalRepositoryOn-device persistenceDriftLocalRepository
4RemoteRepositoryAnonymized cloud sync onlyFirestoreRemoteRepository
5AuthServicePhone-based authFirebaseAuthService
6LocationServiceLocation + geofenceAndroidLocationService
7NotificationServiceLocal + pushFlutterFcmNotificationService
8PaymentServicePremium subscriptionMpesaStkPaymentService
9AnalyticsServiceAnonymized usageFirebaseAnalyticsService
10PatternDetectorRecurring vs habitual classificationRulesPatternDetector
11NudgeGeneratorDaily swipe-card nudge textTemplateNudgeGenerator (V1.5: LlmNudgeGenerator)
12RemoteConfigServiceFeature flags + thresholdsFirebaseRemoteConfigService
13RetrospectiveAnalyzerInsight candidate generationRulesRetrospectiveAnalyzer
14InsightRankerTop-k insight selectionWeightedInsightRanker
15InsightSchedulerCadence orchestrationRulesInsightScheduler
16ConversationStoreLocal chat history + intent tokensDriftConversationStore
17LlmGatewayStateless cloud LLM call (conversation + period story) — distinct from the on-device LiteRT-LM engineCloud LlmGateway (Gemini Flash / Haiku class)
18JourneyOrchestratorUI flow selection from registryRulesJourneyOrchestrator
19InstalledFinanceAppsDetectorKnown finance app presenceAndroidInstalledFinanceAppsDetector
20InstitutionRegistryTyped registry of finance institutionsRemoteConfigInstitutionRegistry
21InstitutionResolverRawSms → 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.

Around the engine

What landed alongside it.

Three capabilities that ride on the same on-device data and privacy boundary.

Account recovery + encrypted backup

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.

The savings ledger

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.

Bill radar + discreet reminders

Recurring bills (SB15) detected from cadence, with a discreet reminder channel that respects quiet hours and a daily cap. Nudgey never nags — it nudges.

.
Institution coverage

58 institutions — and resilient to the ones it's never seen.

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.

Categories

BANK · MOBILE_MONEY · FINTECH_LENDER · MICROFINANCE · SACCO · INSURANCE

Coverage tiers

TIER 1 · FULL PARSER

Templated, corpus-gated extraction

18 institutions
M-Pesa (incl. Fuliza, M-Shwari, KCB-M-Pesa) · KCB · Equity (incl. Equitel) · Co-op · NCBA · Stanbic · StanChart · Absa · I&M · Family · NIC · Prime · DTB · T-Kash · Tala · Branch · Hustler Fund
TIER 2 · CLASSIFIED

Catalogued · category-aware

Saccos · MFIs · lenders · insurers
Faulu · KWFT · Stima Sacco · Mwalimu Sacco · Hazina Sacco · Zenka · Okolea · Saida · and the rest of the 58
TIER 3 · UNKNOWN-PROVIDER

Recognized, extracted, then learned

Open-ended · resilient
Any unrecognized provider with a real transaction is still recognized as financial and extracted into a placeholder account → Nudgey asks the user "what's this account?" → the answer promotes it to a named institution. Surfaced in backoffice triage; Remote Config adds it for everyone, no app release.

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.

Insights orchestration

The "AI feel" comes from when, not from models.

V1's agentic feel is mostly orchestration. The insights engine is the orchestration core.

Three components

RetrospectiveAnalyzer

Runs a battery of pure-function analyzers over a transaction window. Each produces an InsightCandidate with novelty + strength scores.

InsightRanker

Picks the top-k candidates by combined score, deduplicates, applies fairness rules — don't surface the same insight twice in a month.

InsightScheduler

Owns cadence: weekly Sunday Drop (one insight, 9–10 AM Sunday), spontaneous (when a fresh pattern triggers), monthly (period story).

Where insights surface

The Onboarding Reveal + Money Map

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.

Continuous insights

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

The period story

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.

Cloud LLM gateway

The cloud LLM. Two use cases, both stateless.

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.

USE CASE 01

Talk to Nudgey

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.

USE CASE 02

Period-story narrative

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.

The roadmap