Offline-First, MoMo-Centric Budgeting Platform with Gemini SMS Parsing
Most personal finance applications assume a Western economic reality: bank-account-first transactions, constant high-speed mobile data, email addresses for identity, and free bank feeds. In Ghana, MTN MoMo and Vodafone/Telecel Cash dominate financial activity; users are on prepaid data where every kilobyte counts; network coverage is unstable; and identity is tied directly to phone numbers.
CediSmart is a monorepo platform built from first principles for emerging markets. It pairs a high-performance FastAPI backend with a React Native (Expo) app, featuring native phone-number SMS auth, an offline SQLite caching layer with batch sync, and a Gemini 2.5 Flash-powered SMS transaction parser that turns plain-text Mobile Money notifications into structured ledger entries.
How raw SMS notifications are parsed into structured ledger entries using Gemini 2.5 Flash, with a regex fallback engine.
Entity-Relationship diagram showing the budgeting and transaction models.
Raw transactional SMS alerts from Mobile Money are highly unstructured. Attempting to parse these with general LLM prompts can result in malformed JSON or slow response times. Furthermore, the system must remain functional even when offline or when rate limits are exceeded.
We leverage Gemini 2.5 Flash's native Structured Outputs via the FastAPI backend. By providing a strict JSON schema via responseSchema, we guarantee parseable results. If the API is offline or rate-limited, the system seamlessly falls back to a rule-based regex compiler.
# Enforcing structured schema with Gemini 2.5 Flash
generation_config = {
"responseMimeType": "application/json",
"responseSchema": {
"type": "OBJECT",
"properties": {
"amount": {"type": "NUMBER"},
"transaction_type": {
"type": "STRING",
"enum": ["income", "expense"]
},
"description": {"type": "STRING"},
"category_suggestion": {"type": "STRING"},
"notes": {"type": "STRING"}
},
"required": ["amount", "transaction_type", "description"]
}
}
To survive low-connectivity areas, the client writes data locally to SQLite and queues it. During network recovery, sending a batch of local updates could cause double-write race conditions or duplicate transactions if the client retries a failed HTTP request.
Implemented client-side UUID generation (`client_id`) on the mobile app. The backend processes batches using a `BulkCreateRequest` schema. It preloads known `client_id`s in a single database query, processes only the non-duplicate entries, and returns a granular summary of created vs skipped items.
# Idempotent bulk check in service.py
async def bulk_create_transactions(user_id, payload, db, redis):
incoming_ids = {
item.client_id for item in payload.transactions
if item.client_id is not None
}
# Fetch existing client_ids in one query
existing_result = await db.execute(
select(Transaction.client_id).where(
Transaction.user_id == user_id,
Transaction.client_id.in_(incoming_ids)
)
)
existing_ids = {row[0] for row in existing_result.all()}
# Process batch...
for item in payload.transactions:
if item.client_id in existing_ids:
skipped += 1
continue
# ... add transaction to database
created += 1
Email registration is uncommon in the mass Ghanaian market, and hardcoded API keys in mobile apps can be reverse-engineered from APK decompilations.
Configured passwordless mobile login using Termii SMS Gateway. All communications are securely routed through the FastAPI backend. Verification codes are validated using Redis-backed TTL caches (5 min), and successful verification generates an RS256-signed JWT token, keeping the keys safe on the server.
# SMS OTP creation route using Redis cache
async def generate_otp(phone_number: str, redis: RedisConn):
otp_code = generate_secure_otp() # 4-digit code
cache_key = f"otp:{phone_number}"
# Set verification code with 5-minute TTL
await redis.set(cache_key, otp_code, ex=300)
# Trigger Termii API call (server-side ONLY)
await send_termii_sms(phone_number, f"Your CediSmart code: {otp_code}")
Never place AI studio keys in front-end client environments. Proxying all requests through your own API guarantees rate-limiting, credential protection, and allows for clean fallback rules.
Synchronization loops will inevitably retry requests under unstable connections. Without matching unique client-side UUIDs to database keys, you risk duplicating ledger transactions.
Building applications for emerging markets means understanding user behaviors. Relying on phone numbers and mobile money is much more valuable to a local audience than a generic banking API.
Check out the full source code or reach out to discuss the architecture.