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.

9 min read View on GitHub More from Ankit10M

A smartphone on a merchant counter sits inside a swarm of messy notification slips from different apps, with only one clean path leading to a spoken amount bubble. The scene explains how the app converts noisy Android alerts into trustworthy voice announcements.
ShoutPay’s core trick is not reading notifications. It is separating payment signals from everything else.
Key Takeaways

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.

A close-up filter gate splits incoming text into three channels. False positives fall away, valid payment signals pass through, and parser-specific cases are routed into separate tracks. The image explains the repo’s focus on discriminating real transactions from noisy notification text.
The important job is not detection alone. It is rejection with precision.

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.

The service is not the whole product. It is the intake valve that feeds the rest of the pipeline.

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.

StageWhat it protects againstWhy it matters
CleaningFormatting clutter and extra punctuationNormalizes text before any pattern matching starts
Financial signal detectionPromos, chats, OTPs, and non-payment alertsPrevents the parser from wasting time on junk
ClassificationSent versus received ambiguityKeeps spoken output semantically correct
Parser resolutionApp-specific format changesLets the repo evolve without breaking old cases
Validation and dedupeBroken extractions and repeated notificationsStops 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.

ApproachWhat it keys onRisk
Package-name whitelistingWhich app sent the notificationBreaks when one app emits different alert types
Single monolithic parserOne universal message shapeFails as soon as formats drift
Versioned parser resolverText shape and parser capabilityAbsorbs 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.

DimensionShoutPayHardware soundboxNaive TTS reader
Hardware costUses an existing Android phoneRequires dedicated deviceUses an existing phone
Works offlineYes, after setupUsually yesSometimes, depending on app design
Handles multiple payment app formatsYes, through parser versioningOften vendor-managedUsually weak
Local speech customizationHighLimitedLow
Merchant deployment frictionModerateLow once provisionedLow
Upgrade path when formats changeAdd or update parsersVendor firmware or service updateUsually 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.