AndroidKotlinEngineering

Parsing India's DLT sender IDs on-device

How Logic Inbox sorts Indian SMS into Personal, Transactions, Orders and Promotions using TRAI's DLT header format, a layered rules engine and a lot of hard-won regex — with no server and no ML model.

Sorting Indian SMS is a different problem from sorting SMS anywhere else. A typical Indian inbox on a Tuesday afternoon holds a UPI debit alert, an OTP that expires in ten minutes, a courier update, a railway PNR, three offers from a telecom operator, and one message from your mother. The categorisation has to be right, instant, and — in our case — computed entirely on the phone, because Logic Inbox has no internet permission.

Here’s what that engine actually looks like.

The gift TRAI gave us: the DLT header

Since India’s DLT (Distributed Ledger Technology) registration regime, commercial SMS in India arrives from a structured sender ID rather than a phone number:

AD-AIRTEL-P     VM-HDFCBK-S     AX-SBIINB-T

The format is ACCESS-ENTITY-CATEGORY. The trailing letter is the useful part:

SuffixMeaning
PPromotional
TTransactional
SService
GGovernment

In Kotlin that is two regexes:

private val dltHeader = Regex("^[A-Z]{1,3}-[A-Z0-9]{2,10}(-[A-Z])?$", RegexOption.IGNORE_CASE)
private val categorySuffix = Regex("-([PTSG])$", RegexOption.IGNORE_CASE)

If a message carries a P suffix, the sender itself has declared it as marketing. That is a stronger signal than anything we could infer from the text — and it is why an app built for India can do this well without a machine-learning model, while a generic international SMS app cannot.

Not every automated sender uses a clean DLT header, though, so we also treat a 4–7 digit numeric short code, or any short alphabetic sender, as “not a human”.

Rules in priority order, because the categories overlap

The classifier is a single pass that returns on the first confident answer. The order matters more than any individual rule.

1. OTP wins over everything. A one-time password is the most time-critical thing in the inbox, and OTP messages frequently also look like order updates (“Your Swiggy order OTP is 4821”). We require both a standalone 4–8 digit code and OTP language:

private val codeRegex = Regex("(?<![\\dA-Za-z])(\\d{4,8})(?![\\dA-Za-z])")
private val otpKeywords = Regex("(\\botp\\b|one[- ]?time\\s+(pass|pin|code)|verification code|security code)", IGNORE_CASE)
private val otpVerb = Regex("(do not share|don'?t share|never share|valid for|expires|use\\s+\\d|is your)", IGNORE_CASE)

The negative lookarounds on the code regex matter: without them, Rs.4500 and an order ID like SR12345678 both read as OTPs. The keyword requirement is what stops every four-digit number in a promotional message from being treated as a code.

2. Promotional, unless money moved. A P suffix routes to Promotions — but only if the message isn’t clearly transactional. Banks do send offers from promotional headers, and a “₹5,000 credited” message that happens to arrive with a P suffix belongs in Transactions regardless of what the sender declared.

3. Transactions: money language, not just an amount. An amount alone is not a transaction — a promo shouts amounts too. We require an amount and movement language:

private val creditWords = Regex("(credited|received|deposited|refund|cashback|salary)", IGNORE_CASE)
private val debitWords  = Regex("(debited|spent|paid|withdrawn|purchase|sent|txn|payment of|deducted)", IGNORE_CASE)

The same pass extracts the amount, the account tail (the XX1234 in “A/c XX1234”), and whether it was a credit or a debit — which is exactly what the finance passbook needs, so we never parse the message twice. Amount parsing has to survive Indian bank formatting quirks like Rs:7312.00 with a colon and lakh-style grouping.

4. Orders and deliveries: status words, not intent words. This was the hardest category to get right, because “Order now and get 40% off” and “Your order has been dispatched” share a keyword. The rule keys on status, never on the imperative:

private val orderWords = Regex("(out for delivery|has been (shipped|dispatched|placed|confirmed)|" +
    "arriving (today|tomorrow|on|by)|track (your )?(order|shipment|package|parcel)|" +
    "booking (is )?confirmed|\\bPNR\\b|e-?ticket|order id)", IGNORE_CASE)

…and it only fires when the sender is an automated short code or the message carries a tracking link, so a friend texting “your order arrived” stays in Personal.

5. Everything else from a short code goes to Promotions; everything else at all is Personal. That last line is the one that makes the app feel right day to day: the Personal tab stays clean of automated senders, which is the entire reason people miss SMS Organizer.

Details that only show up in real inboxes

  • Merchant names need a sentence-boundary stop. at Apollo Pharmacy. Click here to view must yield “Apollo Pharmacy”, not “Apollo Pharmacy Click here to view”. Trailing punctuation and separators get trimmed too.
  • Pretty sender labels. AD-AIRTEL-P is shown as AIRTEL — split on -, take the entity segment. It sounds trivial and it is one of the most-noticed touches in the app.
  • Order-like OTPs. Because OTP wins the classification, a delivery OTP would otherwise vanish from Orders. A separate isOrderLike() check mirrors the Orders gate so the message can be shown in both places without weakening the OTP rule.

Why rules and not a model — for now

An on-device ML classifier was the obvious alternative, and the code is deliberately layered so one can slot in later. But for the first release, rules won on every axis that mattered: they run in microseconds on a mid-range phone, they need no bundled model or training data, they are debuggable when a user tells us one message landed in the wrong tab, and — most importantly — their behaviour is explainable. When a user asks “why did you file this under Promotions?”, we can answer precisely.

The trade-off is honest: rules are brittle at the edges, and every new sender format is maintenance. That maintenance is the product. India’s SMS ecosystem is specific enough that a tuned rules engine built by people who read these messages every day beats a generic model that has never seen a DLT header.

Logic Inbox is free on Google Play; the full feature story lives at logicinbox.app.

Logic Inbox
Logic Inbox

Private SMS organizer — sorted on your device, with no internet permission.

More from the studio

All posts