UPI_Transaction_Alert: ShoutPay: The UPI Alert Engine That Turns Android Noise Into Spoken Money
A deep look at the parser pipeline, signal detection, and voice layer behind an open-source soundbox alternative for Android merchants.
- ShoutPay treats UPI alerts as a noisy signal problem, then speaks only the messages that survive a multi-stage validation pipeline.
- The repo’s real differentiator is parser versioning, which lets it adapt to changing notification formats without hard-wiring itself to one app shape.
- Its voice layer is product design, not garnish, because Indian numbering and local languages make the output feel native and usable.
- The project is a software soundbox alternative that keeps parsing, storage, and speech on device instead of handing the job to the cloud.
Most apps that read notifications aloud stop at text-to-speech. ShoutPay starts earlier. It assumes Android notifications are messy, app-specific, and full of false positives, then builds a local pipeline that decides what counts as money before it ever speaks.
Why a Soundbox Needs a Parser
The product problem is simple: merchants want to hear when a payment lands. The technical problem is not simple at all. A UPI alert might look like a transfer, a security notice, a promo, or a genuine transaction depending on the app, the bank, the language, and even the phrasing of a single line.
That is why this repo feels closer to a streaming compiler than a notification reader. Raw text comes in, the noise gets stripped away, financial signals get tested, parsers are chosen by message shape, and only then does the voice layer get involved.
The Heartbeat: NotificationListenerService
At the edge of the system sits UpiNotificationListenerService. It listens to every notification on the device, which is both powerful and dangerous. In debug mode, it can capture unfiltered messages so the project can learn from new payment formats instead of pretending the world is stable.
class UpiNotificationListenerService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
val extras = sbn.notification.extras
val rawText = listOfNotNull(
extras.getCharSequence(Notification.EXTRA_TEXT),
extras.getCharSequence(Notification.EXTRA_BIG_TEXT)
).joinToString(" ")
serviceScope.launch {
processTransactionUseCase(rawText, sbn.packageName)
}
}
}
From Noise to Payment Candidate
The center of gravity is ProcessTransactionUseCase. It does not trust the first signal it sees. It cleans the text, tests whether the notification looks financial, classifies the direction of money, resolves the right parser version, validates the result, and dedupes repeats before anything reaches speech.
That sequence matters. Each stage removes a different failure mode. Cleaning removes formatting noise. Filtering blocks irrelevant notifications. Classification separates sent from received. Parser resolution handles changing app formats. Deduping prevents the same alert from being announced twice.
| Stage | What it protects against | Why it matters |
|---|---|---|
| Cleaning | Formatting clutter and extra punctuation | Normalizes text before any pattern matching starts |
| Financial signal detection | Promos, chats, OTPs, and non-payment alerts | Prevents the parser from wasting time on junk |
| Classification | Sent versus received ambiguity | Keeps spoken output semantically correct |
| Parser resolution | App-specific format changes | Lets the repo evolve without breaking old cases |
| Validation and dedupe | Broken extractions and repeated notifications | Stops duplicate or malformed announcements |
The Regex War Is the Real Product
The cleverness shows up in NotificationFilter.kt. The repo does not just search for the word rs and call it money. It uses a pattern like RS_PATTERN to make sure currency markers sit next to numbers, so a word like transfers does not trigger a false payment signal.
That sounds small, but it is the difference between a useful merchant tool and a noisy toy. The project is defending against language traps, bank-specific phrasing, and notification formats that only seem consistent until you look closely.
private val RS_PATTERN = Regex("\\brs\\.?\\s*\\d+")
fun looksFinancial(text: String): Boolean {
return RS_PATTERN.containsMatchIn(text.lowercase())
}
Parsers as a Versioned Contract
The parser layer is where the repo stops being a one-off script and starts behaving like a system. TransactionParser defines the contract, ParserVersionResolver picks the right implementation, and each parser decides whether it can handle the current text shape.
That versioned approach is the right answer to a moving target. If GPay changes its phrasing, the app does not need a rewrite. A new parser can arrive alongside the old one, and the resolver can keep routing messages to the best fit.
| Approach | What it keys on | Risk |
|---|---|---|
| Package-name whitelisting | Which app sent the notification | Breaks when one app emits different alert types |
| Single monolithic parser | One universal message shape | Fails as soon as formats drift |
| Versioned parser resolver | Text shape and parser capability | Absorbs app updates without collapsing the whole system |
Why the Voice Layer Matters More Than It Looks
The voice stack is not a garnish. VoiceAnnouncementEngine and AmountToWordsConverter turn numbers into something a merchant can trust at a glance, and in a noisy shop that trust is everything. The converter also handles the Indian numbering system, so amounts sound natural in lakhs and crores, not awkwardly imported from another market.
That local detail is a serious product decision. English, Hindi, and Marathi support are not just language toggles. They turn a generic TTS feature into something that fits the real rhythm of Indian commerce.
Privacy, Permissions, and Trust
The app’s permission story is part of the architecture. Notification listener access is sensitive, so the onboarding flow forces a privacy explanation before the main experience starts. That is the right move for a local-only system that depends on deep access to the user’s notifications.
Because the processing stays on device, trust does not have to be outsourced to a server. The trade-off is that the repo has to earn reliability through parsing discipline, local persistence, and clear onboarding instead of marketing claims.
What Replaces a Hardware Soundbox?
Compared with commercial soundboxes, ShoutPay trades hardware simplicity for software flexibility. Compared with a naive notification reader, it trades convenience for much stronger filtering, parser selection, and localization. That makes it most compelling for merchants who can tolerate setup in exchange for control.
| Dimension | ShoutPay | Hardware soundbox | Naive TTS reader |
|---|---|---|---|
| Hardware cost | Uses an existing Android phone | Requires dedicated device | Uses an existing phone |
| Works offline | Yes, after setup | Usually yes | Sometimes, depending on app design |
| Handles multiple payment app formats | Yes, through parser versioning | Often vendor-managed | Usually weak |
| Local speech customization | High | Limited | Low |
| Merchant deployment friction | Moderate | Low once provisioned | Low |
| Upgrade path when formats change | Add or update parsers | Vendor firmware or service update | Usually manual cleanup or breakage |
That is the real pitch. ShoutPay is not trying to beat hardware by being simpler. It is trying to beat it by being adaptable, inspectable, and local.