<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aditya Thandra</title>
    <description>The latest articles on DEV Community by Aditya Thandra (@shadowmonarch9).</description>
    <link>https://dev.to/shadowmonarch9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049811%2F14cba8da-2eee-4993-a54d-1afe2688075d.png</url>
      <title>DEV Community: Aditya Thandra</title>
      <link>https://dev.to/shadowmonarch9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shadowmonarch9"/>
    <language>en</language>
    <item>
      <title>I Built an SMS-Reading Expense Tracker. Then I Learned Why Nobody Else Does That.</title>
      <dc:creator>Aditya Thandra</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:11:33 +0000</pubDate>
      <link>https://dev.to/shadowmonarch9/i-built-an-sms-reading-expense-tracker-then-i-learned-why-nobody-else-does-that-309a</link>
      <guid>https://dev.to/shadowmonarch9/i-built-an-sms-reading-expense-tracker-then-i-learned-why-nobody-else-does-that-309a</guid>
      <description>&lt;p&gt;&lt;em&gt;Cross-posted from the &lt;a href="https://github.com/shadow-monarch9/Spendlee" rel="noopener noreferrer"&gt;Spendlee&lt;/a&gt; repo, where the full technical postmortem lives in &lt;a href="https://github.com/shadow-monarch9/Spendlee/blob/main/docs/PIVOT-FROM-SMS-TO-VOICE-AI.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/PIVOT-FROM-SMS-TO-VOICE-AI.md&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch that seemed obvious
&lt;/h2&gt;

&lt;p&gt;Every UPI transaction in India comes with an SMS receipt. So the first version of what's now &lt;strong&gt;Spendlee&lt;/strong&gt; skipped manual expense entry entirely — a &lt;code&gt;BroadcastReceiver&lt;/code&gt; would catch the bank text, regex out the amount and merchant, and fire a notification: &lt;em&gt;"New transaction detected — log it?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Zero typing. Zero friction. The single biggest reason expense trackers get abandoned, solved before the user even opened the app.&lt;/p&gt;

&lt;p&gt;It worked. I built the whole thing — SMS receiver, bank-specific and generic UPI parsers, the notification flow, confirm/categorize screens, Room storage. Then I tried to actually ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fell apart
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google Play Protect doesn't care that you're not malware.&lt;/strong&gt; The moment a signed APK requesting &lt;code&gt;RECEIVE_SMS&lt;/code&gt;/&lt;code&gt;READ_SMS&lt;/code&gt; was installed outside the Play Store, Play Protect blocked it on sight — the same heuristic that catches SMS-stealing malware doesn't stop to ask whether this particular app is a personal finance tool you wrote yourself. Getting around it meant disabling a security feature and enabling "install from unknown sources," which is a fine ask for a developer testing their own build and a hard no for handing the APK to a friend or spouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribution was harder than the app.&lt;/strong&gt; You can't even email the thing to yourself — Gmail strips &lt;code&gt;.apk&lt;/code&gt; attachments outright. Every distribution path led back to the same trade: disable one protection, then another, for an app whose entire value proposition was supposed to be &lt;em&gt;effortless&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Play Store isn't a clean escape hatch either.&lt;/strong&gt; Declaring &lt;code&gt;RECEIVE_SMS&lt;/code&gt;/&lt;code&gt;READ_SMS&lt;/code&gt; there requires a separate Permissions Declaration Form with strict, inconsistent review outcomes, plus a mandatory privacy policy — a reasonable cost for a company shipping an official banking app, a lot of process risk for a side project whose core feature is exactly the permission pair Google scrutinizes hardest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then the wall that actually mattered: iOS.&lt;/strong&gt; No third-party iOS app has ever been allowed to read SMS content — not behind a permission dialog, not through a special entitlement, not at any App Store review tier. This isn't a policy form to fill out; it's OS-level sandboxing Apple enforces uniformly. Which meant the SMS-reading architecture could &lt;em&gt;never&lt;/em&gt; have an iOS counterpart, full stop — any plan to eventually port this to iPhone was dead the moment SMS reading became the core mechanism, regardless of how much more Android engineering went into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pivot
&lt;/h2&gt;

&lt;p&gt;I replaced automatic detection with &lt;strong&gt;user-initiated voice/text logging&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tap-to-speak using Android's on-device &lt;code&gt;SpeechRecognizer&lt;/code&gt; — no cloud speech API, no extra permission risk.&lt;/li&gt;
&lt;li&gt;A "Type" mode for the same flow when talking out loud isn't practical.&lt;/li&gt;
&lt;li&gt;The transcribed or typed sentence ("spent 250 on groceries") goes to a Supabase Edge Function, which calls an LLM to extract amount, merchant, and category. The LLM API key lives only as an Edge Function secret — the Android app never holds it.&lt;/li&gt;
&lt;li&gt;A review step shows the recognized text before it's parsed, so a speech-recognition slip is a two-second edit instead of a silently wrong transaction.&lt;/li&gt;
&lt;li&gt;Everything downstream — confirm, categorize, Room storage, reports — was already built for the SMS flow and needed only cosmetic changes to plug into the new input source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this turned out to be the better design, not just the available one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No sensitive permissions.&lt;/strong&gt; &lt;code&gt;RECORD_AUDIO&lt;/code&gt; isn't in Android's Restricted Permissions bucket — no Play Protect flag, no special declaration form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actually portable to iOS.&lt;/strong&gt; Speech-to-text and typed text aren't platform-gated the way SMS content is. A future iOS build could use Apple's own on-device speech APIs and call the &lt;em&gt;same&lt;/em&gt; Edge Function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Covers cash, not just UPI.&lt;/strong&gt; SMS-based detection only ever saw electronic payments. Voice/text logging works for anything, spoken in plain language.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check platform distribution policy before the architecture locks around a permission.&lt;/strong&gt; Play Protect's behavior and Apple's SMS policy were both knowable in advance — finding out after the feature was fully built meant rebuilding the core interaction model, not tweaking it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Works on my dev device" isn't "shippable."&lt;/strong&gt; Sideloading friction is invisible right up until you try to hand the APK to someone who isn't you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A platform-level restriction has no engineering workaround.&lt;/strong&gt; When the OS vendor enforces a constraint uniformly, no amount of extra dev time recovers the original plan — only a different design does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve reusable investment when you pivot.&lt;/strong&gt; Because the confirm/categorize/report UI never cared &lt;em&gt;how&lt;/em&gt; a transaction entered the queue, swapping the input mechanism was all the pivot cost.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Full writeup with more detail: &lt;a href="https://github.com/shadow-monarch9/Spendlee/blob/main/docs/PIVOT-FROM-SMS-TO-VOICE-AI.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/PIVOT-FROM-SMS-TO-VOICE-AI.md&lt;/code&gt;&lt;/a&gt;. Repo: &lt;a href="https://github.com/shadow-monarch9/Spendlee" rel="noopener noreferrer"&gt;github.com/shadow-monarch9/Spendlee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobileapp</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
