<?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: Ankith M</title>
    <description>The latest articles on DEV Community by Ankith M (@ankithm1006).</description>
    <link>https://dev.to/ankithm1006</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%2F4161494%2F8f1002ef-d04c-4f14-b098-1a3a7919960e.png</url>
      <title>DEV Community: Ankith M</title>
      <link>https://dev.to/ankithm1006</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ankithm1006"/>
    <language>en</language>
    <item>
      <title>A failed UPI payment owes you ₹100 a day. I built the sloth that waits to collect it.</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:34:41 +0000</pubDate>
      <link>https://dev.to/ankithm1006/a-failed-upi-payment-owes-you-100-a-day-i-built-the-sloth-that-waits-to-collect-it-2bdg</link>
      <guid>https://dev.to/ankithm1006/a-failed-upi-payment-owes-you-100-a-day-i-built-the-sloth-that-waits-to-collect-it-2bdg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Indian phone has this SMS somewhere: &lt;em&gt;"Rs.1250.00 debited from A/c XX4521 ... Txn status: PENDING. If not credited to beneficiary, amount will be reversed."&lt;/em&gt; The money left. It didn't arrive. And then you wait, check the balance twice a day, maybe call the bank, and eventually forget about it. If it comes back in a week, you're relieved.&lt;/p&gt;

&lt;p&gt;Here's what almost nobody knows: &lt;strong&gt;since 15 October 2019, the bank has to pay you ₹100 for every day that reversal is late.&lt;/strong&gt; Not if you complain. Automatically. It's in RBI's circular on failed transactions, paragraph 5: compensation "shall be effected to the customer's account suo moto, without waiting for a complaint or claim from the customer."&lt;/p&gt;

&lt;p&gt;So the money is owed. Getting it just needs someone patient: someone who remembers the date, counts the days, writes the letter, waits 30 days, writes the next letter. That's the most sloth-shaped job I could think of, so I built the sloth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Paisa Wapas&lt;/strong&gt; ("money back"). You forward the failed-payment SMS once. Then you do nothing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma reads the SMS&lt;/strong&gt;: is this a UPI payment to a person or to a shop, an ATM that didn't give cash, a card, IMPS, Aadhaar Pay, a wallet, a reversal, or just a normal payment? It pulls out the amount, date, reference, bank and payee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code checks every value Gemma gives against the SMS text.&lt;/strong&gt; An amount, date or reference that isn't in the message is dropped and listed. The model never decides a deadline or a rupee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code applies the RBI rule&lt;/strong&gt;: UPI to a person must be reversed by T+1, UPI to a shop by T+5, an ATM by T+5, and so on (ten rows from the circular's annex). It works out the deadline, how late it is, and what's owed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Temporal workflow does the waiting.&lt;/strong&gt; A durable timer runs to the deadline. If the money isn't back, Gemma drafts a letter to the bank, from facts code worked out. When you send it, a 30-day timer starts, and if nothing happens, Gemma drafts the complaint to the RBI Ombudsman (free, at cms.rbi.org.in). When the money finally comes back, it tells you the compensation the bank owes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is sent for you.&lt;/strong&gt; Every letter is a draft you copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who gets to be lazier:&lt;/strong&gt; anyone who's ever had a payment fail. Especially the people at home who would never write to a bank's nodal officer, or know that there's a nodal officer at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live (no sign-up):&lt;/strong&gt; &lt;a href="https://paisa-wapas-1037019152152.asia-south1.run.app" rel="noopener noreferrer"&gt;https://paisa-wapas-1037019152152.asia-south1.run.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tap a sample SMS, then pick &lt;em&gt;"It takes 45 days"&lt;/em&gt; and &lt;strong&gt;Fast-forward&lt;/strong&gt;. You're watching the real workflow run on Temporal's time-skipping test server: 45 days of patient waiting in about 15 seconds, with two real Gemma-written letters along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Freuisbrytomqkx24xyqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Freuisbrytomqkx24xyqv.png" alt="Reading an SMS: the deadline and what's owed" width="780" height="3962"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnkith-m1006%2Fpaisa-wapas%2Fmain%2Fdocs%2F3-fast-forward.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnkith-m1006%2Fpaisa-wapas%2Fmain%2Fdocs%2F3-fast-forward.png" alt="45 days in 15 seconds" width="780" height="7300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ankith-m1006" rel="noopener noreferrer"&gt;
        Ankith-m1006
      &lt;/a&gt; / &lt;a href="https://github.com/Ankith-m1006/paisa-wapas" rel="noopener noreferrer"&gt;
        paisa-wapas
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Forward a failed UPI/bank SMS once: Gemma reads it, code applies the RBI deadline and Rs 100/day, a Temporal workflow does the waiting.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Paisa Wapas&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Money cut, payment failed? Forward the SMS once. Paisa Wapas knows the RBI deadline, counts the ₹100 a day the bank owes you, and does the waiting for you.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://paisa-wapas-1037019152152.asia-south1.run.app" rel="nofollow noopener noreferrer"&gt;https://paisa-wapas-1037019152152.asia-south1.run.app&lt;/a&gt; (no sign-up)&lt;/p&gt;
&lt;p&gt;Built for the DEV &lt;em&gt;Hacktoberfest Open-Source AI Challenge&lt;/em&gt;, Week 3: &lt;em&gt;Slow and Steady&lt;/em&gt;.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read the SMS&lt;/th&gt;
&lt;th&gt;The deadline and what's owed&lt;/th&gt;
&lt;th&gt;Fast-forward 45 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Ankith-m1006/paisa-wapas/docs/1-home.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnkith-m1006%2Fpaisa-wapas%2FHEAD%2Fdocs%2F1-home.png" alt="Home"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Ankith-m1006/paisa-wapas/docs/2-read-light.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnkith-m1006%2Fpaisa-wapas%2FHEAD%2Fdocs%2F2-read-light.png" alt="Read"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Ankith-m1006/paisa-wapas/docs/3-fast-forward.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnkith-m1006%2Fpaisa-wapas%2FHEAD%2Fdocs%2F3-fast-forward.png" alt="Timeline"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The rule almost nobody uses&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Since 15 Oct 2019, RBI's circular on failed transactions (DPSS.CO.PD No.629/02.01.014/2019-20) sets a deadline for every kind of failed payment: a UPI transfer that left your account but never reached the other person must be reversed by &lt;strong&gt;T + 1&lt;/strong&gt;, an ATM that didn't give cash by &lt;strong&gt;T + 5&lt;/strong&gt;, and so on. After that the bank owes you &lt;strong&gt;₹100 for every day of delay&lt;/strong&gt;, and paragraph 5 says it must credit that &lt;strong&gt;without waiting for a complaint&lt;/strong&gt;. If the…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ankith-m1006/paisa-wapas" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The rule first, from the source
&lt;/h3&gt;

&lt;p&gt;Before any model, I read the circular itself (RBI/2019-20/67, DPSS.CO.PD No.629/02.01.014/2019-20, 20 September 2019) and turned its annex into a table in &lt;code&gt;rules.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failed payment&lt;/th&gt;
&lt;th&gt;Back by&lt;/th&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UPI to a person, IMPS, card to card, NACH, wallet&lt;/td&gt;
&lt;td&gt;T + 1&lt;/td&gt;
&lt;td&gt;₹100/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPI to a shop, card at a shop or online, ATM, Aadhaar Pay&lt;/td&gt;
&lt;td&gt;T + 5&lt;/td&gt;
&lt;td&gt;₹100/day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;T&lt;/code&gt; is the day of the transaction. Ten rows, one &lt;code&gt;verdict()&lt;/code&gt; function, no model involved. Then the RBI Integrated Ombudsman Scheme, 2021 for the escalation: complain to the bank in writing first, and if there's no reply in 30 days, you can go to RBI, within a year and 30 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The model reads, code decides
&lt;/h3&gt;

&lt;p&gt;Gemma 4 (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;) gets one SMS and returns a small JSON object: &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;bank&lt;/code&gt;, &lt;code&gt;payee&lt;/code&gt;, each copied exactly as written. Then &lt;code&gt;check()&lt;/code&gt; treats every field as a claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the amount must match a number in the SMS,&lt;/li&gt;
&lt;li&gt;the reference must appear in the SMS,&lt;/li&gt;
&lt;li&gt;the date must appear in the SMS and parse (&lt;code&gt;18-10-26&lt;/code&gt;, &lt;code&gt;09-Oct-26&lt;/code&gt;, &lt;code&gt;16Oct26&lt;/code&gt;, &lt;code&gt;21 Oct 2026&lt;/code&gt; ...),&lt;/li&gt;
&lt;li&gt;anything that fails is dropped and shown as a problem, never guessed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why not just let Gemma do everything? Because the one place a model must not be creative is a number you'll put in a letter to your bank.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The waiting is a workflow, not a reminder
&lt;/h3&gt;

&lt;p&gt;This is the part I'm proudest of, and the reason I picked Temporal. A failed payment is a &lt;strong&gt;slow&lt;/strong&gt; process by law: one day, five days, then thirty more. A cron job and a database row can do it, but every edge case (the laptop sleeps, the server restarts, the money comes back while you're writing the letter) becomes my bug.&lt;/p&gt;

&lt;p&gt;In Temporal it's just code that waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;deadline_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_wait_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversed_on&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Money came back on time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft_letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;LETTER_OPTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;rbi_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;complaint_sent_on&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_wait_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rbi_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reversed_on&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_wait_until&lt;/code&gt; is a durable timer that also wakes up early on a signal ("the money's back", "I sent the letter"). The workflow sleeps for weeks, survives restarts, and the code reads like the story.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Watching 45 days in 15 seconds
&lt;/h3&gt;

&lt;p&gt;A demo of something that waits a month is a problem. Temporal's &lt;strong&gt;time-skipping test server&lt;/strong&gt; is the answer: it fast-forwards the clock whenever the workflow is only waiting. The live preview runs the real &lt;code&gt;PaisaWapas&lt;/code&gt; workflow on it, so what you see is the real logic, not an animation.&lt;/p&gt;

&lt;p&gt;It took three tries to get right, and that's where most of my evening went:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The clock can't go back.&lt;/strong&gt; The test server starts at "now", so an SMS from 18 October can't be replayed from its own date. The preview now runs &lt;em&gt;as if the SMS arrived today&lt;/em&gt;, and says so on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking the event loop.&lt;/strong&gt; The letter activity called Gemma with a blocking HTTP request inside &lt;code&gt;async&lt;/code&gt; code, which froze the whole server. Fix: &lt;code&gt;await asyncio.to_thread(gemma, ...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time kept skipping during the model call.&lt;/strong&gt; While Gemma was writing (15-20 s), the test server skipped hours ahead, so the activity's 3-minute timeout fired in skipped time and it retried forever ("Activity not found on completion" in the logs). Fix: in the preview, the workflow records each letter request instantly, and Gemma writes those exact letters once the run is done. In real mode the real activity runs inside Temporal, with a timeout in days, because a letter is a draft a person sends later anyway.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5. The worst failure, and its guard
&lt;/h3&gt;

&lt;p&gt;The worst thing this app could do is put a wrong number in a letter to a bank. So after Gemma writes a letter, code checks that the amount, the compensation and the deadline it was given all appear in the text. If any is missing or changed, the Gemma letter is thrown away and a plain letter built straight from the facts is used instead. Boring, correct, sendable.&lt;/p&gt;

&lt;p&gt;A second, real one from tonight: a Gemma reply came back with extra text after the JSON and crashed my eval. Now the reader parses the first JSON object only, and if a reply can't be read at all, it records "the model's answer couldn't be read; nothing was assumed" instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemma vs the regex app I'd otherwise write
&lt;/h2&gt;

&lt;p&gt;The test set is 40 SMS written by hand in the real formats of SBI, HDFC, ICICI, Axis, Kotak, PNB, Canara, Bank of Baroda, Paytm and PhonePe (no real customer data): failed UPI to people and shops, ATMs, cards, IMPS, AePS, wallets, reversals, and normal payments, salary credits, OTPs and offers that must &lt;strong&gt;not&lt;/strong&gt; be chased. 25 of them are failed payments.&lt;/p&gt;

&lt;p&gt;The baseline is the honest alternative: keywords for the status ("failed", "pending", "not credited", "timed out"...), the first &lt;code&gt;Rs&lt;/code&gt;/&lt;code&gt;INR&lt;/code&gt; amount, the first 12-digit reference, the first date. &lt;strong&gt;It was written while looking at SMS 1-30&lt;/strong&gt;; SMS 31-40 were written afterwards (Hinglish, a balance printed before the amount, a reversal mentioned inside a success message, a forwarded PhonePe notification), so those ten are held out for it. Gemma was never tuned on any of them.&lt;/p&gt;

&lt;p&gt;"Letter-ready" means everything a letter needs is right: the deadline (the right T+1 / T+5 row), the amount and the date.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Letter-ready (deadline, amount and date right)&lt;/th&gt;
&lt;th&gt;Wrong chase / don't-chase call&lt;/th&gt;
&lt;th&gt;Amount right&lt;/th&gt;
&lt;th&gt;Held-out 31-40: letter-ready&lt;/th&gt;
&lt;th&gt;Time per SMS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regex baseline (written while looking at SMS 1-30)&lt;/td&gt;
&lt;td&gt;20/25&lt;/td&gt;
&lt;td&gt;3/40&lt;/td&gt;
&lt;td&gt;23/25&lt;/td&gt;
&lt;td&gt;4/8&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B A4B, thinking on&lt;/td&gt;
&lt;td&gt;24/25&lt;/td&gt;
&lt;td&gt;1/40&lt;/td&gt;
&lt;td&gt;25/25&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;23.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma 4 26B A4B, thinking minimal (default)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23/25&lt;/td&gt;
&lt;td&gt;1/40&lt;/td&gt;
&lt;td&gt;25/25&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;3.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What the numbers say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On the SMS the regex was written for, it looks great.&lt;/strong&gt; On the ten it hadn't seen, it fell apart: it took the &lt;em&gt;balance&lt;/em&gt; (₹3,412.50) as the amount, missed a Hinglish ATM failure ("ATM se paise nahi nikle par Rs 10,000 debit ho gaye") as "nothing to chase", and filed a failed wallet payment as a reversal. That's what every regex app does in real life: it works on the formats its author saw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma's misses are the interesting kind.&lt;/strong&gt; An SMS that says a new payment &lt;em&gt;succeeded&lt;/em&gt; but an &lt;em&gt;earlier&lt;/em&gt; one failed. A masked reference (&lt;code&gt;6284XXXX&lt;/code&gt;) it copied faithfully. An ATM ID it offered as a reference. None of them would have produced a wrong deadline in a letter; the guard catches values that aren't in the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking costs time, not much accuracy.&lt;/strong&gt; With Gemma's thinking set to &lt;code&gt;minimal&lt;/code&gt; a read takes about 3 s instead of about 20 s, so that's the default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Two places, timed
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Read one SMS&lt;/th&gt;
&lt;th&gt;Fast-forward 45 days (2 letters)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;My laptop (Windows, 8 GB)&lt;/td&gt;
&lt;td&gt;5.2 s&lt;/td&gt;
&lt;td&gt;16.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run, asia-south1&lt;/td&gt;
&lt;td&gt;3.6 s&lt;/td&gt;
&lt;td&gt;14.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gemma runs on Google AI Studio in both; the workflow, the rules and the checks run where the app runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Both halves of Paisa Wapas are open, and each one is open for a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma is an open-weight model.&lt;/strong&gt; Bank SMS are about as personal as data gets: account numbers, amounts, who you pay. The job here is small and fixed (read one SMS, fill six fields), which is exactly the kind of job an open model can do on hardware you control, with nobody else's terms deciding whether your messages are kept. For this week's build I called Gemma 4 through Google AI Studio, and I haven't run it on my own laptop yet; the reader only needs one HTTP call, so pointing it at a local Gemma is the next step, not a rewrite. I'd rather say that plainly than pretend.&lt;/p&gt;

&lt;p&gt;Where would a closed model do better? Probably on the hardest SMS, the "your new payment worked, your old one failed" kind. But it would also be the one part of the app I couldn't move, audit or run offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal is open source (MIT).&lt;/strong&gt; The whole point of Paisa Wapas is to wait for weeks without losing track. With Temporal, the waiting is plain code I can read, test (the 36-day case runs in seconds in &lt;code&gt;pytest&lt;/code&gt;), and self-host with &lt;code&gt;temporal server start-dev&lt;/code&gt; on the same laptop. And the time-skipping server, the thing that makes a 45-day story demo-able in 15 seconds, exists because the project is open and built to be tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules are open too.&lt;/strong&gt; RBI publishes the circular; I just turned it into ten rows of code with the annex row number next to each one, so anyone can check the app against the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;What it saves, for one late UPI transfer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remembering&lt;/strong&gt;: the T+1 date, the 30-day date. 0 minutes of your attention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counting&lt;/strong&gt;: days late × ₹100, on the day it's checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing&lt;/strong&gt;: two letters, each with the circular's number, the annex row, the amounts and the dates, already right. Writing one from scratch starts with finding the circular and the right annex row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not giving up&lt;/strong&gt;: the one that matters. The ₹100 a day only adds up for the people who keep going after day 5. The sloth always keeps going.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the whole product is a Temporal workflow that waits days by law (durable timers, signals for "money back" and "letter sent", queries for the status), plus Temporal's time-skipping test server to run the real workflow in the live demo and in the tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 reads every SMS (with every value checked against the text by code) and writes every letter (with every number checked again), evaluated against a regex baseline on 40 SMS, with held-out messages and both thinking levels measured.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How I worked: I built this with an AI coding assistant, dictating my side through Wispr Flow. The idea and the decisions are mine, and every claim here was checked against the code, the eval output and the RBI texts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>Three doctors, eight pills, one graph: what my agent found that nobody was looking for</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:19:07 +0000</pubDate>
      <link>https://dev.to/ankithm1006/three-doctors-eight-pills-one-graph-what-my-agent-found-that-nobody-was-looking-for-5h9e</link>
      <guid>https://dev.to/ankithm1006/three-doctors-eight-pills-one-graph-what-my-agent-found-that-nobody-was-looking-for-5h9e</guid>
      <description>&lt;p&gt;Meet Lakshmi. She is 72, and she doesn't exist. I built her for this project as a synthetic patient, but if you grew up in an Indian family you have probably met someone exactly like her.&lt;/p&gt;

&lt;p&gt;Lakshmi sees a general physician for her blood pressure, sugar and thyroid. A cardiologist added a blood thinner and a statin last month. Her knee started hurting, so an orthopaedic surgeon prescribed a painkiller and an antacid. Then she had a fever on a Tuesday night, and her son Ravi picked up a strip from the chemist downstairs.&lt;/p&gt;

&lt;p&gt;That is four prescribers, eight medicines, and every single one written by &lt;strong&gt;brand name&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Doctor&lt;/th&gt;
&lt;th&gt;What they wrote&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dr. Rao, general physician&lt;/td&gt;
&lt;td&gt;Telma 40, Glycomet 500, Thyronorm 50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dr. Menon, cardiologist&lt;/td&gt;
&lt;td&gt;Ecosprin 75, Atorva 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dr. Shetty, orthopaedic&lt;/td&gt;
&lt;td&gt;Aceclo Plus, Pan 40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chemist, for the fever&lt;/td&gt;
&lt;td&gt;Dolo 650&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each doctor saw their own page. Nobody saw the whole box. So here is the question Ravi can't answer on a Sunday morning, holding the pill organiser:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is anything in this box the same medicine twice? And does anything here fight with anything else?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;DawaiGraph&lt;/strong&gt; (&lt;em&gt;dawai&lt;/em&gt; is Hindi for medicine) for Graph Hacks to answer that question. This post is the story of how a graph answers it, one hop at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a graph problem, not a search problem
&lt;/h2&gt;

&lt;p&gt;Look at "Aceclo Plus" and "Dolo 650". The two names share nothing: no word, no letters that matter. Text search won't connect them, and neither will embeddings of the names.&lt;/p&gt;

&lt;p&gt;But Aceclo Plus is a combination pill: aceclofenac &lt;strong&gt;plus paracetamol&lt;/strong&gt;. And Dolo 650 is paracetamol. The connection doesn't live in the names. It lives one level down, in the salts.&lt;/p&gt;

&lt;p&gt;That is a pattern, and patterns are what a graph database is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b1:&lt;/span&gt;&lt;span class="n"&gt;Brand&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;c1:&lt;/span&gt;&lt;span class="ow"&gt;CONTAINS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;s:&lt;/span&gt;&lt;span class="n"&gt;Salt&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;c2:&lt;/span&gt;&lt;span class="ow"&gt;CONTAINS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b2:&lt;/span&gt;&lt;span class="n"&gt;Brand&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b1.id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$brand_ids&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b2.id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;$brand_ids&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b1.id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b2.id&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;s.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b1.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;brand_a&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c1.strength&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;strength_a&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;b2.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;brand_b&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c2.strength&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;strength_b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two brands, one shared salt, two hops. For Lakshmi, DawaiGraph returns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Aceclo Plus and Dolo 650 both contain paracetamol (325 mg + 650 mg).&lt;/strong&gt;&lt;br&gt;
Path: Aceclo Plus → CONTAINS → Acetaminophen → CONTAINS → Dolo 650&lt;br&gt;
Doctors to ask: Dr. Shetty, the chemist&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That path &lt;em&gt;is&lt;/em&gt; the explanation. Nobody has to trust a model's word for it; they can see the hops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph model
&lt;/h2&gt;

&lt;p&gt;Everything lives in one FalkorDB graph: what medicines &lt;em&gt;are&lt;/em&gt;, and what this family &lt;em&gt;takes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53arklq9j3qj8g4p5fmp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F53arklq9j3qj8g4p5fmp.png" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medicine knowledge:&lt;/strong&gt; &lt;code&gt;(:Brand)-[:CONTAINS {strength}]-&amp;gt;(:Salt)&lt;/code&gt;, &lt;code&gt;(:Salt)-[:INTERACTS {level}]-&amp;gt;(:Salt)&lt;/code&gt;, and &lt;code&gt;(:Salt)-[:IN_CLASS]-&amp;gt;(:DrugClass)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family memory:&lt;/strong&gt; &lt;code&gt;(:Person)-[:TAKES]-&amp;gt;(:Brand)&lt;/code&gt;, prescriptions with the doctor who wrote them, and &lt;code&gt;(:Finding)&lt;/code&gt; nodes for every problem raised, with &lt;code&gt;(:Carer)&lt;/code&gt; nodes for the family members who follow up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is all open: the A–Z Medicine Dataset of India from Kaggle (about 250,000 brand rows), DDInter for drug–drug interactions (160,235 pairs), and Wikidata for drug classes. I loaded the 70,000 best-known brands, which takes about 65 MB of FalkorDB Cloud's 100 MB free tier.&lt;/p&gt;

&lt;p&gt;One small thing made a big difference: a &lt;strong&gt;full-text index with phonetic matching&lt;/strong&gt; on brand names. When someone types, or a photo gets read as, "Telmaa 40", it still finds "Telma 40 Tablet".&lt;/p&gt;

&lt;h2&gt;
  
  
  Hop by hop
&lt;/h2&gt;

&lt;p&gt;Here is everything DawaiGraph found in Lakshmi's box, ordered by how far the graph had to walk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three hops: interactions live between salts, but people hold brands.&lt;/strong&gt;&lt;br&gt;
Telma 40 is telmisartan, and Ecosprin 75 is aspirin. DDInter lists a moderate interaction between those two salts. So the path is Brand → Salt → Salt → Brand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ecosprin 75 → CONTAINS → Acetylsalicylic acid → INTERACTS (Moderate) → Telmisartan → CONTAINS → Telma 40&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same walk found three more moderate interactions in her box: Atorva with Pan 40, Glycomet with Thyronorm, and Pan 40 with Thyronorm. Three of the four cross a doctor boundary: Dr. Menon's Atorva meets Dr. Shetty's Pan 40, Dr. Shetty's Pan 40 meets Dr. Rao's Thyronorm, and Dr. Menon's Ecosprin meets Dr. Rao's Telma. The doctor who wrote one half never saw the other half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four hops: some links only exist through a third medicine.&lt;/strong&gt;&lt;br&gt;
I added a tool to the agent that answers "why are these two connected?" with a shortest-path search over just this person's medicines. Ask it about Thyronorm and Ecosprin, two medicines with no direct link, and it comes back with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gz6likigst1vo5rrw4p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gz6likigst1vo5rrw4p.gif" alt=" " width="600" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thyronorm and Ecosprin are connected through &lt;strong&gt;PAN 40&lt;/strong&gt;, another medicine Lakshmi takes:&lt;br&gt;
Thyronorm → contains → Levothyroxine → Moderate interaction → Pantoprazole (in PAN 40) → Minor interaction → Acetylsalicylic acid → contained in → Ecosprin 75&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also adds a note that each link is a separate pairwise fact, not a combined effect. That honesty mattered to me more than the path itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hop that doesn't exist: India's data gap
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me.&lt;/p&gt;

&lt;p&gt;Open interaction datasets are built mostly around drugs sold in the US and Europe. Many salts Indians take every day simply aren't in them. When I matched the Indian brand list against DDInter, the most common salt with &lt;strong&gt;no interaction data at all&lt;/strong&gt; was aceclofenac: about 9,000 brands of it. Domperidone was right behind it.&lt;/p&gt;

&lt;p&gt;Lakshmi's knee painkiller, Aceclo Plus, contains aceclofenac. A plain lookup returns nothing. And in a medicine app, "nothing found" quietly reads as "safe".&lt;/p&gt;

&lt;p&gt;So I asked: what would a careful pharmacist do? They'd reason from the drug's family. Aceclofenac is an NSAID, in the same class as ibuprofen, naproxen and diclofenac, which &lt;em&gt;do&lt;/em&gt; have data. In the graph, that is a walk to the drug's cousins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IN_CLASS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;DrugClass&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IN_CLASS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;sib:&lt;/span&gt;&lt;span class="n"&gt;Salt&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sib.ddinter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sib&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;i:&lt;/span&gt;&lt;span class="n"&gt;INTERACTS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i.level&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Major'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Moderate'&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If at least 80% of at least 3 siblings interact with something in the regimen, DawaiGraph raises a &lt;strong&gt;class signal&lt;/strong&gt;. For Lakshmi:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No interaction data exists for aceclofenac (Aceclo Plus). &lt;strong&gt;10 of 10&lt;/strong&gt; drugs in its class have a Major or Moderate interaction with aspirin (Ecosprin 75). &lt;em&gt;Unverified: a signal to ask about, not a known interaction.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It raised a second one with metformin (Glycomet), and for Suresh, 68, another person in the demo whom Ravi also looks after, the same aceclofenac (in a dentist's Zerodol-SP) against his warfarin.&lt;/p&gt;

&lt;p&gt;I didn't pick 80% and 3 from thin air. I hid 272 drugs that &lt;em&gt;do&lt;/em&gt; have data, pretended they didn't, and checked how often the class signal predicted their real interactions. At 80% of at least 3 siblings, &lt;strong&gt;87.4% of the signals were real interactions&lt;/strong&gt; (recall 43%). Lower thresholds caught more but were wrong more often. For something a family reads about a parent's medicines, I chose fewer, more reliable signals, and the label says "unverified" every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  No hop: "no path" is not "safe"
&lt;/h2&gt;

&lt;p&gt;Suresh also takes Augmentin (from the dentist) and Amlong (from his cardiologist). Ask why they're connected, and the agent says there is no link in the data, not even through his other medicines, and then adds the sentence I care about most:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Having no data in the database is not the same as proving it is safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Making the agent unable to make things up
&lt;/h2&gt;

&lt;p&gt;A medicine agent that sounds confident and is wrong is worse than no agent. So I took a few decisions early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed tools, no text-to-Cypher.&lt;/strong&gt; The agent (Gemini, with function calling) can only call a small set of parameterised queries: resolve a brand, list the family, read a person's medicines, check them, explain a connection. It never writes its own Cypher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve every brand first, and ask when unsure.&lt;/strong&gt; When I gave it a photo of a synthetic prescription, it read the brands it could find in the graph and refused to guess one it couldn't read clearly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A name check after every answer.&lt;/strong&gt; Code collects every medicine name in the reply and checks that it came from a tool result or the user's own words. In testing, the agent wrote "paracetamol" where the data says "Acetaminophen". It got caught and rewrote the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How I got this" under every answer.&lt;/strong&gt; Every tool it called, the Cypher file behind it and what came back. Each decision can be traced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd1qau5uiaod3u1ogtud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftd1qau5uiaod3u1ogtud.png" alt=" " width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory that belongs to one family
&lt;/h2&gt;

&lt;p&gt;Findings are nodes, not chat history. A finding's key is a hash of what it's about, so if the same problem shows up next week, it updates the existing node instead of raising the alarm again. When Divya, Lakshmi's daughter, marks it "asked the doctor", Ravi sees that too.&lt;/p&gt;

&lt;p&gt;Each family is its own tenant in the graph, so one family never sees another's data. And before the next appointment, the app prints a &lt;strong&gt;doctor visit sheet&lt;/strong&gt;: one checklist per doctor, with what to raise first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;I wanted to know what the graph actually adds, so I measured it. I built 30 regimens of 5 to 9 real Indian brands each (half with a deliberately repeated salt), computed the ground truth straight from the source tables, and asked two systems for the duplicates and interactions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7a4eizd7lun91x5oatoj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7a4eizd7lun91x5oatoj.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;30 regimens, 119 known interactions&lt;/th&gt;
&lt;th&gt;DawaiGraph&lt;/th&gt;
&lt;th&gt;Gemini 3.5 Flash alone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactions found&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;119 / 119&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;48 / 119 (40%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stated interactions that DDInter lists&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stated interactions DDInter doesn't list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate salts found&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24 / 24&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21 / 24, plus 7 that aren't duplicates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be fair: DawaiGraph scores 100% because the graph is built from the same data, so this mainly proves the queries are faithful. The interesting column is the right one. The same model that writes DawaiGraph's explanations, asked without the graph, missed 60% of the listed interactions and added 52 that the data doesn't list. That is the whole argument for giving an agent a graph instead of asking it to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The brand data's prices are from 2022, and it lists at most two salts per brand.&lt;/li&gt;
&lt;li&gt;About 77% of brands matched a salt with interaction data. The rest are exactly the gap the class signal tries to cover, and it covers part of it, not all.&lt;/li&gt;
&lt;li&gt;All the people here are synthetic.&lt;/li&gt;
&lt;li&gt;It is not medical advice. DawaiGraph never says "safe" and never tells anyone to stop a medicine. It tells you which doctor to ask.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put the reasoning in the data, not the prompt.&lt;/strong&gt; Every finding above is a path in the graph, which is why it can be shown, checked and remembered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The empty result is a feature.&lt;/strong&gt; The most useful thing DawaiGraph does may be refusing to treat "no data" as "safe".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small free tiers teach you to be careful.&lt;/strong&gt; One unbounded path query on the whole graph froze my free FalkorDB instance. Restricting path search to one person's medicines made it fast and safe.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://dawaigraph-1037019152152.asia-south1.run.app" rel="noopener noreferrer"&gt;https://dawaigraph-1037019152152.asia-south1.run.app&lt;/a&gt; (each browser gets its own copy of the synthetic families)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code, graph model and every Cypher query:&lt;/strong&gt; &lt;a href="https://github.com/Ankith-m1006/dawaigraph" rel="noopener noreferrer"&gt;https://github.com/Ankith-m1006/dawaigraph&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/cQ_jSRnSp58" rel="noopener noreferrer"&gt;https://youtu.be/cQ_jSRnSp58&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;There's also a read-only &lt;strong&gt;MCP server&lt;/strong&gt;, so any AI assistant can check a medicine list against the same graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for Graph Hacks by WeMakeDevs × FalkorDB. I used Claude (Anthropic) as a coding assistant to build DawaiGraph and to help draft this post; the decisions, testing and final wording are mine.&lt;/p&gt;

&lt;h1&gt;
  
  
  GraphHacks @WeMakeDevs @falkordb
&lt;/h1&gt;

</description>
      <category>graphhacks</category>
      <category>falkordb</category>
      <category>ai</category>
      <category>wemakedevs</category>
    </item>
    <item>
      <title>BillSahi: I built a GST checker, and a shopkeeper told me GST wasn't his problem</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Sat, 17 Oct 2026 05:14:51 +0000</pubDate>
      <link>https://dev.to/ankithm1006/billsahi-i-built-a-gst-checker-and-a-shopkeeper-told-me-gst-wasnt-his-problem-26fj</link>
      <guid>https://dev.to/ankithm1006/billsahi-i-built-a-gst-checker-and-a-shopkeeper-told-me-gst-wasnt-his-problem-26fj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Four bills on a counter near Pavagada
&lt;/h3&gt;

&lt;p&gt;The supermarket is in a village outside Pavagada, in Tumkur district. Like most shops of its size it doesn't buy everything from one wholesaler. Soaps and Horlicks come from the Hindustan Unilever distributor, coconut oil and gram-flour soap from another, detergent from a third, and creams and lotions from whoever brings them that week. When I asked the owner for supplier bills, he handed me what he had kept: four of them.&lt;/p&gt;

&lt;p&gt;Two looked like any GST tax invoice. One was printed from a phone app with a running balance in big letters at the top: ₹2,13,672 due. One just said &lt;strong&gt;CASH BILL 2025&lt;/strong&gt;: 31 lines of creams and soaps, ₹15,252 in total, no supplier name, no GSTIN and no tax shown anywhere.&lt;/p&gt;

&lt;p&gt;I had come with a GST rate checker. I had spent the week building it: you photograph a supplier's bill, and it checks every line against the official rate. So I asked him what worries him about supplier bills.&lt;/p&gt;

&lt;p&gt;It wasn't the GST rate. He has hundreds of suppliers, he said, and most of them charge the same prices for the same goods. A few charge more, a few charge less, and that's what bothers him. The other problem, a big one in his words, is keeping track of credit: who he owes, and how much.&lt;/p&gt;

&lt;p&gt;Then he told me the part that made GST matter again. The shop is registered under the &lt;strong&gt;composition scheme&lt;/strong&gt;, so today it can't claim back any GST it pays on purchases. &lt;strong&gt;From January 2027 he's moving to regular GST&lt;/strong&gt;, and from then on every purchase bill has to be good enough to claim the GST back.&lt;/p&gt;

&lt;p&gt;So BillSahi changed. This post is about what it does now, and what it found on his four bills.&lt;/p&gt;

&lt;h3&gt;
  
  
  What BillSahi does now
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;BillSahi&lt;/strong&gt; checks a small Indian shop's supplier bills. Take a photo of the bill and it tells you four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the GST rate right on every line?&lt;/strong&gt; Every line is checked against the official rate, and any difference is shown in rupees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you claim this GST back?&lt;/strong&gt; It checks the supplier's GSTIN (format and check digit), your GSTIN on the bill, GST shown on every line, and the HSN and rate on every line, then gives the rupees of credit at risk if something is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did you pay less for this before?&lt;/strong&gt; Every line is saved with what one unit cost the shop. When a later bill charges more for the same item, from any supplier, you see it, with the rupees extra on that bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How much do you owe each supplier?&lt;/strong&gt; Every bill is added to that supplier's account; record a payment in one tap and see who you owe, next to what the supplier's own bill says you owe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also still does what I first built it for: type items the way a shop writes them ("Parle G 800g", "hawai chappal 199", "ಬಟ್ಟೆ ಸೋಪು 4pc") and get the HSN heading and GST rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who pays, and how much.&lt;/strong&gt; I asked the owner. He said he would pay, but not much. He buys stock only two or three times a month, so he would use it two or three times a month, and around &lt;strong&gt;₹49 a month&lt;/strong&gt; is what he said he'd pay. That number shaped the build: at ₹49 a month, every bill check has to cost almost nothing to run, which is why open models matter here (more on that below).&lt;/p&gt;

&lt;p&gt;The one rule everything is built around: &lt;strong&gt;the model never states a rate.&lt;/strong&gt; It picks an HSN heading, then one of the official notification entries for that heading, and code reads the rate from the entry. Everything that's arithmetic (rupees overcharged, credit at risk, the ₹2,500 line for clothing and footwear, what one unit cost, what you owe) is done by code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://billsahi-pb7k.onrender.com" rel="noopener noreferrer"&gt;https://billsahi-pb7k.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shop's four real bills, as a read-only demo book:&lt;/strong&gt; &lt;a href="https://billsahi-pb7k.onrender.com/?shop=kvr-demo#book" rel="noopener noreferrer"&gt;https://billsahi-pb7k.onrender.com/?shop=kvr-demo#book&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What it found on the four real bills
&lt;/h3&gt;

&lt;p&gt;62 lines across four bills from September and October 2026. The photos were forwarded on WhatsApp, so they are compressed. I didn't retake them; this is what a real shop would send.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bill&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Read right&lt;/th&gt;
&lt;th&gt;What BillSahi found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hair-oil and soap distributor&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10 of 10&lt;/td&gt;
&lt;td&gt;Aloe vera &lt;strong&gt;skin gel&lt;/strong&gt; billed as hair oil (HSN 3305 at 5%). Skin-care gel is 3304 at 18%: GST short by &lt;strong&gt;₹174.57&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detergent distributor&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3 of 3&lt;/td&gt;
&lt;td&gt;All rates right. Supplier's printed balance &lt;strong&gt;₹2,13,672&lt;/strong&gt; against this bill's ₹3,648.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cash bill&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;31 of 31&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No GST shown at all&lt;/strong&gt; on ₹15,252 of purchases. 30 of 31 items placed correctly; hair colour wasn't.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HUL distributor (dense, blurry photo)&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;partly&lt;/td&gt;
&lt;td&gt;Rows from line 15 shifted by one. BillSahi caught it and marked nothing (see the failure below).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The credit check is where his January problem shows up.&lt;/strong&gt; Neither GST invoice has the shop's GSTIN on it. Today that doesn't matter, because a composition shop claims nothing. From January, a bill without his GSTIN means the supplier's GST doesn't show up against his GSTIN, and he can't claim it. On these two bills alone that's &lt;strong&gt;₹834.76 + ₹556.48 = ₹1,391.24&lt;/strong&gt; of GST, and the cash bill would add ₹15,252 of purchases with no claimable GST at all. That's why BillSahi now ends every bill with "Can you claim this GST back?" and a list of ✓ and ✗.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The aloe vera catch&lt;/strong&gt; is the kind of thing nobody checks by hand. The bill prints HSN 33059011 (hair oil, 5% since GST 2.0). The product is a skin gel, which is 3304 at 18%. Here the supplier charged &lt;em&gt;less&lt;/em&gt; GST than due, so it's not money the shop lost today. But a wrong HSN on a purchase bill is exactly what an officer questions once the shop starts claiming credit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The detergent supplier's balance&lt;/strong&gt; is the credit problem in one number: the bill is for ₹3,648, and the top of it says ₹2,13,672 due. Two weeks after this bill, would the owner know which of those numbers is right? In BillSahi's supplier book, the supplier's printed balance sits next to what the shop's own bills add up to, so a gap is visible instead of buried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Each bill took between 3.6 and 7.7 minutes on the free hosted Gemma 4 31B, from photo to every line checked: 3 lines in 217 s, 10 lines in 259 s, 31 lines in 353 s, 18 dense lines in 461 s. Most of that is the vision read and one model call per line. It's slow for a demo and fine for a shop that buys stock three times a month; the page prints each line as soon as it's checked, so nobody stares at a spinner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ankith-m1006" rel="noopener noreferrer"&gt;
        Ankith-m1006
      &lt;/a&gt; / &lt;a href="https://github.com/Ankith-m1006/billsahi" rel="noopener noreferrer"&gt;
        billsahi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Right HSN code and official GST rate for small Indian shops, and a check of every supplier bill for the wrong rate. Open models: Qwen3.5-4B fine-tuned on Tinker, Gemma 4.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;BillSahi&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Is your supplier's bill &lt;em&gt;sahi&lt;/em&gt;? BillSahi checks a small Indian shop's supplier bills: the GST rate on every line, whether the bill is good enough to claim the GST back, whether a supplier is charging more than the shop paid before, and how much the shop owes each supplier.&lt;/p&gt;
&lt;p&gt;Live: &lt;a href="https://billsahi-pb7k.onrender.com" rel="nofollow noopener noreferrer"&gt;https://billsahi-pb7k.onrender.com&lt;/a&gt; · Demo book with four real bills from a supermarket near Pavagada, Tumkur: &lt;a href="https://billsahi-pb7k.onrender.com/?shop=kvr-demo#book" rel="nofollow noopener noreferrer"&gt;https://billsahi-pb7k.onrender.com/?shop=kvr-demo#book&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Type items the way a shop writes them (brands, Hinglish, Hindi or Kannada, abbreviations, pack sizes) or upload a photo of a supplier's GST bill. BillSahi finds the 4-digit HSN heading, matches the official GST notification entry, and reads the rate from that entry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The model never states a rate.&lt;/strong&gt; It picks a heading and an entry; code reads the rate from the official CBIC notifications. When the rate depends on something the item name doesn't say (pre-packaged or loose, sale value above…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ankith-m1006/billsahi" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The official rates, as data.&lt;/strong&gt; I parsed every goods entry of CBIC Notification 9/2025-CT(Rate) and the exemption Notification 10/2025-CT(Rate) (GST 2.0, effective 22 September 2025), and applied the later amendments 19/2025 and 01/2026: 1,570 rows covering 1,372 entries, each with its source notification, plus spot checks against the PDF text (&lt;code&gt;data/RATES_PROVENANCE.md&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The thing that surprised me is that an HSN heading often doesn't decide the rate on its own. Heading 3401 holds toilet soap in bars (5%) and other soap and washing preparations (18%). Heading 3304 holds talcum and face powder (5%) and skin-care creams (18%); that split is the aloe vera catch above. So the model's job isn't "guess the code", it's "pick the official entry that describes this item".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Search (Tiger Data).&lt;/strong&gt; The 12,604-code HSN directory and the 1,570 entries live in Postgres on Tiger Data with BM25 indexes (&lt;code&gt;pg_textsearch&lt;/code&gt;). Every item first gets a shortlist of headings from plain search. The shop's own book lives in the same database: bills, lines and payments, created on first use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reading the bill (Gemma 4).&lt;/strong&gt; Gemma 4 31B reads the photo and only transcribes: supplier and buyer GSTIN, invoice number and date, the bill total, any running balance, and for each line the item, printed HSN, MRP, quantity, taxable value, GST rate and line total. The request is streamed. Before I switched to streaming, the dense HUL bill failed three times in a row with "remote end closed connection": a long, silent request gets dropped as idle, while a streamed one keeps the connection busy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Heading, then entry.&lt;/strong&gt; For each line, the model picks a heading from the shortlist, then one entry from the official entries for that heading, shown without their rates. Code rejects a heading that isn't in the directory and an entry that isn't in the list, and applies the ₹2,500 threshold for clothes and footwear itself.&lt;/p&gt;

&lt;p&gt;Shop bills write things like "PCNO 500ML FT" (Parachute coconut oil) and "MORELIGHT LIQUID 99/-". From the name alone, neither can be placed. When the name fails, BillSahi uses the HSN printed on the bill as a hint and marks the line "read from the bill's HSN". On the real bills that rescued 7 lines: 5 coconut-oil lines and 2 detergent lines. It's used &lt;strong&gt;only&lt;/strong&gt; when the name fails, so a wrong HSN on a named item (like the aloe vera gel) can't hide a wrong rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Price memory and the supplier book.&lt;/strong&gt; Plain code (&lt;code&gt;app/ledger.py&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What one unit cost:&lt;/em&gt; the line total divided by the quantity. For a composition shop, the GST is part of the cost because it can't be claimed back.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Same item?&lt;/em&gt; The same pack size and MRP when both are printed, and most of the name's words shared. Suppliers spell brands differently: the cash bill says "LIFEBOY RED SOAP 125G", and another bill could say "LIFEBUOY". Near-identical words count as shared. A test against the saved real bills flagged a Lifebuoy line at ₹40 a piece against the ₹38 the shop paid before (₹48 extra on 24 pieces), and left alone a Garnier line whose MRP differed (a different pack) and a Santoor line at the same price.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Dues:&lt;/em&gt; the bills of a supplier minus the payments recorded, with the supplier's own printed balance shown next to it.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Credit check:&lt;/em&gt; the GSTIN check digit is a mod-36 checksum over the first 14 characters, so a GSTIN misread from a blurry photo shows up as "check digit doesn't match" instead of passing as valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. A fine-tuned open model (Tinker).&lt;/strong&gt; Before the shop visit, I fine-tuned Qwen3.5-4B with LoRA on Tinker to answer the same two questions (heading, then entry) as short JSON. The training data was written by open models and checked by another one. Gemma 4 and Qwen3.5-397B (on Tinker) wrote shop-style item names for every notification entry, and every item was classified blind by the &lt;em&gt;other&lt;/em&gt; model and dropped if the two disagreed on the chapter (about 76% kept). That caught real label errors, such as a Hep-B test kit filed under the vaccines' chapter. The result was 20,689 training conversations. Data generation and training together used about $10.69 of Tinker credit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Traces (Sentry).&lt;/strong&gt; Every item is an agent span with tool spans (search, official entries, sale value) and a chat span for each model call, so one slow or wrong line can be traced to the step that caused it. [TODO: screenshot of one bill's trace]&lt;/p&gt;

&lt;h3&gt;
  
  
  The failure that matters, and the guard
&lt;/h3&gt;

&lt;p&gt;The fourth bill is the Hindustan Unilever distributor's tax invoice: 18 lines, 14 columns, tiny print, photographed at an angle and compressed twice. The first version of BillSahi read it and reported that one Horlicks line was &lt;strong&gt;overcharged by ₹110.20&lt;/strong&gt;, charged 18% instead of 5%.&lt;/p&gt;

&lt;p&gt;That was wrong. Horlicks was billed at 5%. From line 15 down, the model had shifted every row by one: each item's name was paired with the next line's numbers, and the 18% belonged to the Vim bar below it. The last "line" it read was the bill's total row.&lt;/p&gt;

&lt;p&gt;For a shop, a false overcharge is the worst mistake this app can make. The owner calls the supplier, the supplier shows him the paper bill, and he never trusts the app again.&lt;/p&gt;

&lt;p&gt;So now BillSahi checks its own reading before it says anything about money. &lt;strong&gt;The lines must add up to the total printed on the bill.&lt;/strong&gt; On this bill they added up to ₹27,060 against a printed ₹13,745. When they don't match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no line is marked overcharged or undercharged; they go back to "check this line on the paper bill";&lt;/li&gt;
&lt;li&gt;none of the bill's prices are saved, so a misread price can't later produce a false "you paid less before";&lt;/li&gt;
&lt;li&gt;the receipt is stamped &lt;strong&gt;RETAKE PHOTO&lt;/strong&gt; instead of &lt;strong&gt;RATES OK&lt;/strong&gt;, with a note to send the original photo instead of a forwarded one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other three bills the lines matched the printed totals to the rupee: ₹17,530, ₹3,648 and ₹15,252. The check runs in code after every read (&lt;code&gt;ledger.reconcile&lt;/code&gt; in &lt;code&gt;app/ledger.py&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Results on the item test set
&lt;/h3&gt;

&lt;p&gt;Before the shop visit I wrote a test set of 57 shop items across kirana, personal care, hardware, stationery, medical, textile and footwear, labelled against the notification text and frozen before training.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Heading right&lt;/th&gt;
&lt;th&gt;Rate right&lt;/th&gt;
&lt;th&gt;Wrong rate stated confidently&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain search (BM25, no model)&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;2.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B, base&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;28% (asks 68% of the time)&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;5.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B fine-tuned on Tinker (v1, step 200)&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;td&gt;5.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B (hosted)&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;23.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fine-tuning moved the 4B model from 28% to 84% of rates right, at about a quarter of Gemma's time per item. But it still stated a wrong rate confidently on 16% of items, the same as plain search. Its failures were brand names (it filed several under 1511, palm oil) and the price thresholds. So &lt;strong&gt;the live app runs Gemma 4 31B&lt;/strong&gt;, which got every rate right on this set, and the fine-tuned model stays switchable with one setting (&lt;code&gt;HSN_MODEL_PATH&lt;/code&gt;). I'd rather ship the slower, correct model than the fast one that is confidently wrong one time in six.&lt;/p&gt;

&lt;p&gt;The training run died at step 209 of 647 when my laptop ran out of memory, and my Tinker credit ran out with it. The v1 numbers above are from the step-200 checkpoint. That's honest, and it's where the next ₹49 subscriptions would go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price.&lt;/strong&gt; At ₹49 a month and a few bills a month, a check can't cost more than a few rupees, even with 30 lines on a bill. Open models are what make that possible: Gemma 4 runs on free and cheap hosted endpoints today, and a fine-tuned 4B model runs on hardware a small company can own. A per-token closed API with a 30-line bill and two calls per line would eat that ₹49 quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data stays the shop's.&lt;/strong&gt; Supplier bills are a shop's purchase prices, margins and suppliers. With open weights the whole thing can run where the shopkeeper's data already lives, instead of every bill going to whoever owns the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The niche is the point.&lt;/strong&gt; GST 2.0 moved toothpaste, hair oil and toilet soap to 5% and left skin creams at 18%. A general model has no reason to know that the aloe vera gel on this bill is in the 18% heading, or that "PCNO 500ML FT" is coconut oil. With open weights I can train exactly that niche, check it against the official text, and hand the arithmetic to code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model would do better, honestly:&lt;/strong&gt; reading the blurry HUL bill. A larger closed vision model might have read it in one go. BillSahi's answer for now is to notice when it has misread a bill and say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;The owner of a supermarket near Pavagada, Tumkur, gave me his last four supplier bills and answered my questions. What changed because of him:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I stopped treating the GST rate as the main problem.&lt;/strong&gt; His suppliers charge the right rate: of the 31 lines on his three GST bills, I checked every one by hand, and the only wrong ones were the 2 aloe vera lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I added the credit check&lt;/strong&gt; because he's moving from composition to regular GST in January. Every one of his bills would fail it today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I added price memory&lt;/strong&gt; because "a few suppliers charge more" was the first thing he said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I added the supplier book&lt;/strong&gt; because tracking credit is, in his words, a big problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;₹49 a month&lt;/strong&gt; is what he said he would pay, for two or three uses a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;He hasn't paid yet; BillSahi isn't a business yet. But it is now the tool he described, not the one I walked in with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; Gemma 4 31B reads every bill photo (streamed vision), places every line in the live app, and got 100% of rates right on the 57-item test set. Gemma 4 also wrote half of the training data and blind-checked the other model's items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tinker:&lt;/strong&gt; Qwen3.5-4B LoRA fine-tuned on Tinker, from 28% to 84% of rates right (table above), plus Qwen3.5-397B on Tinker for training data. Switchable in the live app with &lt;code&gt;HSN_MODEL_PATH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render:&lt;/strong&gt; the web app and API run on Render: &lt;a href="https://billsahi-pb7k.onrender.com" rel="noopener noreferrer"&gt;https://billsahi-pb7k.onrender.com&lt;/a&gt;, with background jobs so the page prints each line as it's checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiger Data:&lt;/strong&gt; BM25 search with &lt;code&gt;pg_textsearch&lt;/code&gt; over the 12,604-code HSN directory and the 1,570 official entries, and the shop's book (bills, lines, payments) in the same Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry Agent Tracing:&lt;/strong&gt; an agent span per item with tool and chat spans for every step. [TODO: screenshot]&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The owner sends the next bill from the same HUL distributor, as an original photo, so price memory gets its first real repeat.&lt;/li&gt;
&lt;li&gt;WhatsApp: forward a bill to a number and get the check back, since that's how the bills arrive anyway.&lt;/li&gt;
&lt;li&gt;Finish the fine-tune (v2, with the brand-heavy retail data) so the cheap model can take over from Gemma.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BillSahi is a helper, not tax advice. Anything marked "check" or "ask" should be confirmed with an accountant.&lt;/p&gt;

&lt;p&gt;AI Assistance Disclosure: I have used Anthropic's Claude Code for planning and helping me to draft this post. &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My village has patchy signal and people who know every plant. I built a field guide that sends you to ask them.</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:06:14 +0000</pubDate>
      <link>https://dev.to/ankithm1006/my-village-has-patchy-signal-and-people-who-know-every-plant-i-built-a-field-guide-that-sends-you-4f2o</link>
      <guid>https://dev.to/ankithm1006/my-village-has-patchy-signal-and-people-who-know-every-plant-i-built-a-field-guide-that-sends-you-4f2o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wayside&lt;/strong&gt; is a pocket field guide for walks. You snap a plant, bird or insect, put the phone back in your pocket, and keep walking. A few seconds later the phone buzzes and &lt;strong&gt;reads the answer aloud&lt;/strong&gt;: what it is, two things to check with your own eyes, whether it is safe to touch, and what to look for next.&lt;/p&gt;

&lt;p&gt;I live in rural Karnataka. Two things about my walks shaped the whole app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The signal comes and goes.&lt;/strong&gt; So Wayside opens offline, saves photos taken with no signal, and identifies them on its own when the phone is back in range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The people around me know these plants by names no app has.&lt;/strong&gt; The names my mother uses for the plants in our garden are not always the ones in a book, and they change from village to village. So Wayside only shows a local (Kannada) name when Gemma is very sure. Otherwise it says &lt;strong&gt;"ask someone nearby what they call it"&lt;/strong&gt;, and saves their answer to your journal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second one is my favourite feature, because the best thing an AI can do on a walk is send you to talk to a person.&lt;/p&gt;

&lt;h3&gt;
  
  
  My walk
&lt;/h3&gt;

&lt;p&gt;On the morning of 9 October, at 7:53, I took Wayside around our garden after the monsoon. It was a short walk, about six minutes, and I spotted 9 things. Two of them came back with Kannada names: &lt;strong&gt;curry leaf, ಕರಿಬೇವು&lt;/strong&gt; and &lt;strong&gt;hibiscus, ದಾಸವಾಳ&lt;/strong&gt;. I asked my mom what she calls them, and she said the same names.&lt;/p&gt;

&lt;p&gt;What I liked most: it made me stop in the middle of the walk and look properly at things I walk past every day, and then go and learn what they are. It made me happy.&lt;/p&gt;

&lt;p&gt;At the end of a walk you get a &lt;strong&gt;walk card&lt;/strong&gt;: time, distance, what you found, and how much of the walk you spent &lt;strong&gt;with the screen off&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsz7p00avce9bvlpglzmh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsz7p00avce9bvlpglzmh.jpg" alt=" " width="800" height="1736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honest number: only 21% of my walk was with the screen off, because I kept looking at the results. That is exactly why the app vibrates and speaks the answer, so next time the phone can stay in my pocket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try it on your phone:&lt;/strong&gt; &lt;a href="https://wayside-1037019152152.asia-south1.run.app" rel="noopener noreferrer"&gt;https://wayside-1037019152152.asia-south1.run.app&lt;/a&gt; (no sign-up; add it to your home screen).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tap &lt;strong&gt;Start walk&lt;/strong&gt; and put the phone away.&lt;/li&gt;
&lt;li&gt;Snap anything green or alive.&lt;/li&gt;
&lt;li&gt;Wait for the buzz. If it says "ask someone nearby", ask, and type in what they call it.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;End walk&lt;/strong&gt; for your walk card.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ankith-m1006" rel="noopener noreferrer"&gt;
        Ankith-m1006
      &lt;/a&gt; / &lt;a href="https://github.com/Ankith-m1006/wayside" rel="noopener noreferrer"&gt;
        wayside
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A pocket field guide for walks: snap a plant, bird or insect and Gemma names it in English and your local language. Works with patchy signal.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Wayside&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A pocket field guide for walks.&lt;/strong&gt; Snap a plant, bird or insect you spot outside; Gemma names it in English and your local language, gives you two things to check with your own eyes, says whether it is safe to touch, and suggests what to look for next. Then you put the phone back in your pocket.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://wayside-1037019152152.asia-south1.run.app" rel="nofollow noopener noreferrer"&gt;https://wayside-1037019152152.asia-south1.run.app&lt;/a&gt; (works on any phone; add it to your home screen)&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 1: &lt;em&gt;Touch Grass&lt;/em&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Most nature apps keep you looking at the screen. Wayside is built the other way round:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snap and keep walking.&lt;/strong&gt; Identification takes a few seconds; your phone buzzes and &lt;strong&gt;reads the result aloud&lt;/strong&gt; when it is ready, so the phone stays in your pocket. Speech uses the device's own voices (Android Google TTS, Apple voices on iPhone and Mac, Windows voices); if the device has…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ankith-m1006/wayside" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed. No build step and no dependencies on the server: one Node file, one HTML file, a service worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 is the whole brain.&lt;/strong&gt; One multimodal call takes the photo (resized on the phone to 1024 px), the region name and the month, and returns strict JSON: common and scientific name, a confidence score, alternatives, two look-closer checks, a local fact, a safety level, the next thing to find, and the local name with its own confidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phone (PWA: IndexedDB queue, service worker)
   |  photo + "Karnataka, India" + "October"   (GPS never leaves the phone)
   v
Node server (/api/identify, /api/quest, /api/speak)
   v
Gemma 4 26B (gemma-4-26b-a4b-it) via Google AI Studio, falling back to Gemma 4 31B
or Gemma on your own machine through Ollama (gemma3:4b), fully offline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing it honestly.&lt;/strong&gt; I did not want a demo that only works on the photos I chose, so I wrote a field test (&lt;code&gt;scripts/fieldtest.mjs&lt;/code&gt;) that runs the exact production prompt on photos of 14 things you actually see on a Karnataka roadside, and scores the species and the Kannada name against an answer key.&lt;/p&gt;

&lt;p&gt;Results with Gemma 4 26B on the exact production prompt (14 photos, Wikimedia images of things on a Karnataka roadside):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Species identified correctly&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;14 / 14&lt;/strong&gt; (banana, bulbul, coconut, drongo, frangipani, hibiscus, ixora, kingfisher, lantana, mimosa, myna, neem, plain tiger butterfly, tulsi)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kannada names shown to the user&lt;/td&gt;
&lt;td&gt;5, &lt;strong&gt;all 5 correct&lt;/strong&gt; (ಬಾಳೆಹಣ್ಣು, ತೆಂಗಿನ ಮರ, ದಾಸವಾಳ, ಬೇವು ಮರ, ತುಳಸಿ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong Kannada names shown&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Not sure, ask someone nearby"&lt;/td&gt;
&lt;td&gt;8 of 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per photo&lt;/td&gt;
&lt;td&gt;21 to 60 s, about 36 s on average&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On my real walk the two Kannada names it showed were also right (my mom checked).&lt;/p&gt;

&lt;p&gt;What the test taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The confidence gate is doing real work.&lt;/strong&gt; For the kingfisher, Gemma produced ಬಿಳಿ ಗಂಟಲಿನ ಮೀನುಗಾರ, a word-for-word translation of "white-throated fisher", not a name anyone in a village uses (people say ಮಿಂಚುಳ್ಳಿ). It rated its own confidence at 0.7, so the app hid it and asked the walker to ask a neighbour. Eight out of fourteen times the honest answer was "ask someone", and that is the point of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first version of the prompt invented local names.&lt;/strong&gt; For an Ixora it gave "Rangan", which is the Marathi name, for a Kannada request. For the bulbul it wrote the English word in Kannada script. So the prompt now forbids other languages and transliterations, asks Gemma how sure it is about the local name, and the app hides anything below 0.85 and sends you to a neighbour instead. A wrong name stated confidently is worse than "ask someone".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I tried two more features and dropped them.&lt;/strong&gt; Bird calls: the Gemma 4 API does not take audio input yet. A written nature journal: Gemma thought for over 8,000 tokens and three minutes to write a bland paragraph. Neither got people outside, so they went.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Small things that keep the phone in the pocket:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The phone &lt;strong&gt;vibrates and speaks&lt;/strong&gt; the result (device voices, with a Google Cloud Text-to-Speech fallback for languages like Kannada that phones often lack).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best time to walk today&lt;/strong&gt; from Open-Meteo's open weather data: the driest, coolest hour before sunset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A daily quest:&lt;/strong&gt; three things common where you are in this season.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any region:&lt;/strong&gt; on first open it asks where you walk (Karnataka, California, Kenya...) so names and quests fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; only the photo, the region name and the month are sent. Location stays on the phone and is only used to draw your route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safety:&lt;/strong&gt; wild plants and mushrooms are never called safe to eat, whatever the model says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because Gemma is an open model, this does not have to depend on a big company's server or an internet connection. A village school, a farmers' group or a forest guide can run the same app on their own laptop with Ollama, with no bill and no signal. The names of plants belong to the people who use them, like my mom, and with open code anyone can add their own village's names. And because the model and the code are open, I could test exactly what it does, like the field test above, instead of trusting a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 (26B, with 31B fallback) does all the identification and quest writing in production; the same code runs on Gemma 3 4B locally with Ollama, fully offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I used an AI coding assistant while building this. The idea, the walk, the photos, my mom's names and the testing are mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading. Now put the phone down and go outside. 🌿&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I tested 11 AI models on Indian GST, UPI and lakh-crore. Three famous ones got Puducherry wrong.</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:34:02 +0000</pubDate>
      <link>https://dev.to/ankithm1006/i-tested-11-ai-models-on-indian-gst-upi-and-lakh-crore-three-famous-ones-got-puducherry-wrong-53e9</link>
      <guid>https://dev.to/ankithm1006/i-tested-11-ai-models-on-indian-gst-upi-and-lakh-crore-three-famous-ones-got-puducherry-wrong-53e9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;I build software for Indian small businesses (POS, billing, payments), and this month I've been contributing Kestra workflow blueprints for Indian finance: GSTIN validation, UPI end-of-day close, GSTR-2B reconciliation. Every one of them exists because one small mistake costs real money:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one mistyped character in a supplier's GSTIN and the input tax credit is at risk,&lt;/li&gt;
&lt;li&gt;IGST booked as CGST + SGST and the buyer can't claim it,&lt;/li&gt;
&lt;li&gt;"5 crore" written as 50 crore,&lt;/li&gt;
&lt;li&gt;a UPI payment that "went through" on the customer's phone but never reached the shop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I asked: &lt;strong&gt;can AI models handle everyday Indian business data?&lt;/strong&gt; I built a Kaggle benchmark with 6 tasks, 16 cases each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What the model has to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_gstin_check_digit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validate a GSTIN and compute its Luhn mod 36 check character (algorithm given)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_gstin_from_memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same, without being told the algorithm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_gst_tax_split&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Split GST into CGST + SGST, CGST + UTGST or IGST, with real traps: supplies to an SEZ unit in your own state, and Union Territories with and without a legislature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_invoice_number_rules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apply GST invoice-number rules (16 characters, only letters/digits/&lt;code&gt;-&lt;/code&gt;/&lt;code&gt;/&lt;/code&gt;, unique per financial year) with look-alike traps: en dash, division slash, trailing space, 17 characters, extra leading zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_lakh_crore_formats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Indian digit grouping (12,34,56,789), lakh/crore conversions, cheque amounts in words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;india_upi_reconciliation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Match 18–30 UPI bills against a settlement report and list the bills whose money never arrived, arrived short, or share a UPI reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How it's graded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every case is generated from a fixed seed with an &lt;strong&gt;exact answer key&lt;/strong&gt;. No LLM judge.&lt;/li&gt;
&lt;li&gt;Answers come back as structured output, so grading is exact.&lt;/li&gt;
&lt;li&gt;The GSTIN answer key is cross-checked against &lt;code&gt;python-stdnum&lt;/code&gt; (0 disagreements). The other answer keys use the same logic as my Kestra blueprints.&lt;/li&gt;
&lt;li&gt;Every model gets the same answer budget. A case that errors (provider down, credit limit) is &lt;strong&gt;not&lt;/strong&gt; counted as wrong. It's left blank.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I picked a mix to see what matters most: size, "thinking", or being open-weight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontier:&lt;/strong&gt; GPT-5.5, Claude Sonnet 5, Gemini 3.7 Flash, Gemini 3.5 Flash&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast / small:&lt;/strong&gt; GPT-5.4 nano, Claude Haiku 4.5, Gemini 3.1 Flash-Lite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight:&lt;/strong&gt; gpt-oss-20b, Gemma 4 31B, Qwen3 235B Instruct, DeepSeek R1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Grok 4.6 and gpt-oss-120b were unavailable through Kaggle's model proxy while I ran this, so they're not on the board.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fca4mzqey5bd6c9ysd97k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fca4mzqey5bd6c9ysd97k.jpg" alt=" " width="799" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhq55cc1o0ntqtr6z2xl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhq55cc1o0ntqtr6z2xl.jpg" alt=" " width="799" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Overall (average of task scores): &lt;strong&gt;GPT-5.5 1.00 · Gemini 3.7 Flash 1.00 · Gemini 3.5 Flash 0.99 · Claude Sonnet 5 0.98 · Gemma 4 0.97 · DeepSeek R1 0.96 · gpt-oss-20b 0.92 · Gemini 3.1 Flash-Lite 0.75 · Qwen3 235B 0.51 · Claude Haiku 4.5 0.45 · GPT-5.4 nano 0.34&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Puducherry is a State (for GST), and three famous models disagree
&lt;/h3&gt;

&lt;p&gt;This was my favourite result. Under the CGST Act, section 2(103), a Union Territory &lt;strong&gt;with its own legislature&lt;/strong&gt; (Puducherry, Delhi) counts as a &lt;em&gt;State&lt;/em&gt;. So a sale inside Puducherry is &lt;strong&gt;CGST + SGST&lt;/strong&gt;. UTGST is only for UTs without a legislature, like Chandigarh or Ladakh.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Sonnet 5, DeepSeek R1 and Gemma 4 all charged UTGST on a Puducherry sale.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Claude Haiku charged UTGST &lt;strong&gt;and&lt;/strong&gt; IGST on the same sale, so it taxed it twice.&lt;/li&gt;
&lt;li&gt;GPT-5.5, every Gemini model, Qwen and even tiny GPT-5.4 nano got it right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bigger isn't uniformly better. Models fail on &lt;em&gt;different&lt;/em&gt; local rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Checksums are a cliff, not a slope
&lt;/h3&gt;

&lt;p&gt;On the GSTIN check character, every model that reasons step by step scored &lt;strong&gt;100%&lt;/strong&gt;, and every fast model scored &lt;strong&gt;6–12%&lt;/strong&gt;. That's no better than guessing. Small open models that reason (gpt-oss-20b, Gemma 4) beat bigger fast ones. In an early test, one model needed about &lt;strong&gt;15,000 thinking tokens and 84 seconds for a single GSTIN&lt;/strong&gt;. Correct isn't always cheap.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The SEZ trap
&lt;/h3&gt;

&lt;p&gt;A supply to a Special Economic Zone unit in your &lt;em&gt;own&lt;/em&gt; state is still an inter-state supply, so it's IGST (IGST Act, section 7(5)(b)). Claude Haiku, gpt-oss-20b and GPT-5.4 nano split it into CGST + SGST anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. 10× errors with lakh and crore
&lt;/h3&gt;

&lt;p&gt;The small models made the most dangerous kind of mistake, being off by exactly one digit group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"5 crore" → &lt;code&gt;500000000&lt;/code&gt; (it's &lt;code&gt;50000000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;"Rs 98,64,000 in lakh" → &lt;code&gt;986.4&lt;/code&gt; (it's &lt;code&gt;98.64&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Indian grouping written the Western way: &lt;code&gt;8,263,425,718&lt;/code&gt; instead of &lt;code&gt;8,26,34,25,718&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Invisible characters
&lt;/h3&gt;

&lt;p&gt;On GST invoice numbers, the look-alikes caught the small models: a trailing space, a 17-character number, and a number with an extra leading zero (a different number, so it's allowed) that they called a duplicate. All the big models were perfect here.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. UPI: the misses are the dangerous part
&lt;/h3&gt;

&lt;p&gt;On days with 18–30 UPI bills, weaker models &lt;strong&gt;missed&lt;/strong&gt; bills whose money never arrived (Claude Haiku 44%, Qwen 25%). A false alarm costs a minute. A missed bill is silent lost money.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The best value is open
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepxotsdzrs229papkw7h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepxotsdzrs229papkw7h.jpg" alt=" " width="799" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Kaggle's score-vs-cost chart, the efficient frontier runs through the open-weight &lt;strong&gt;gpt-oss-20b&lt;/strong&gt; and &lt;strong&gt;Gemma 4 31B&lt;/strong&gt;. They get most of the way to the top score at a small fraction of the cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd measure next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GST rate schedules and HSN codes (which rate applies, not just how to split it)&lt;/li&gt;
&lt;li&gt;E-invoice JSON validation&lt;/li&gt;
&lt;li&gt;Invoices in Hindi and regional languages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/ankith111111111/indian-business-data-gst-upi-and-lakh-crore" rel="noopener noreferrer"&gt;Indian Business Data: GST, UPI and Lakh-Crore on Kaggle&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All 6 tasks and their notebooks are public, so you can run them on any model.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I used an AI assistant to draft this post. The idea, benchmarking,  the Indian accounting rules and the final review are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>GitHub said 31 issues were open. 0 were actually free. So I built Unclaimed for my best friend</title>
      <dc:creator>Ankith M</dc:creator>
      <pubDate>Sun, 04 Oct 2026 18:35:33 +0000</pubDate>
      <link>https://dev.to/ankithm1006/github-said-31-issues-were-open-0-were-actually-free-so-i-built-unclaimed-for-my-best-friend-39jh</link>
      <guid>https://dev.to/ankithm1006/github-said-31-issues-were-open-0-were-actually-free-so-i-built-unclaimed-for-my-best-friend-39jh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My best friend &lt;a href="https://github.com/SanjanaG-01" rel="noopener noreferrer"&gt;@SanjanaG-01&lt;/a&gt; and I are both taking part in Kestra Hacktober this October. Writing the code is not the hard part. Finding an issue nobody has taken yet is.&lt;/p&gt;

&lt;p&gt;On a busy repo, a fresh &lt;code&gt;good first issue&lt;/code&gt; gets its first "can I work on this?" comment within about 20 minutes. By the time you open it, it might already have a pull request, five people asking for it, or a maintainer saying it isn't ready yet. GitHub still shows it as &lt;strong&gt;open&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I measured it. GitHub's own search for open, unassigned good first issues in &lt;code&gt;kestra-io/kestra&lt;/code&gt; returned &lt;strong&gt;31 issues&lt;/strong&gt;. When I checked every one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;21&lt;/strong&gt; already had a pull request (one had three)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8&lt;/strong&gt; had people asking for them in the comments (up to 11 on a single issue)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; were marked "not ready for contributions"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; were actually free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I built &lt;strong&gt;Unclaimed&lt;/strong&gt;. Paste a repository, and it tells you which issues are really &lt;strong&gt;Free&lt;/strong&gt;, which are &lt;strong&gt;Claimed&lt;/strong&gt;, which &lt;strong&gt;Have a PR&lt;/strong&gt;, which are &lt;strong&gt;Assigned&lt;/strong&gt; and which are &lt;strong&gt;Not ready&lt;/strong&gt;, with proof for each one. Instead of opening 30 tabs and reading every comment, my friend sees the free ones first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27yyzssri3bmxb5uyn9u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27yyzssri3bmxb5uyn9u.jpg" alt=" " width="799" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Try it live: &lt;strong&gt;&lt;a href="https://unclaimed-production.up.railway.app" rel="noopener noreferrer"&gt;https://unclaimed-production.up.railway.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paste any public repo, or a link to a single issue. It works beyond Kestra too: on &lt;code&gt;zed-industries/zed&lt;/code&gt; it found that the repo calls its label &lt;code&gt;.contrib/good first issue&lt;/code&gt;, and showed 4 of the 15 newest issues as free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpaqpbxpb1l22zeky5ur1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpaqpbxpb1l22zeky5ur1.jpg" alt=" " width="799" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ankith-m1006" rel="noopener noreferrer"&gt;
        Ankith-m1006
      &lt;/a&gt; / &lt;a href="https://github.com/Ankith-m1006/unclaimed" rel="noopener noreferrer"&gt;
        unclaimed
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Find open-source issues nobody has taken yet. Gemma reads every issue thread for you.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Unclaimed&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Find open-source issues nobody has taken yet.&lt;/p&gt;
&lt;p&gt;During Hacktoberfest, a fresh &lt;code&gt;good first issue&lt;/code&gt; can get its first "can I work on this?" within 20 minutes. By the time you open it, it may be assigned, already have a pull request, carry a "not ready for contributions" banner, or have five people asking for it in the comments. Unclaimed checks all of that for you.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hard facts from the GitHub API.&lt;/strong&gt; For each open issue with the label you choose, it checks the assignees, linked pull requests (open or merged, from the issue timeline), and the "not ready for contributions" banner some projects add. Any of these settles the verdict without asking the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma reads the thread.&lt;/strong&gt; When nothing hard blocks the issue, the comment thread decides. Gemma 3 (4B), an open-weight model, runs on our own Ollama server and reads the issue and its…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ankith-m1006/unclaimed" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Unclaimed works in two steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Facts from GitHub first.&lt;/strong&gt; For each issue it checks the assignees, the linked pull requests, and the "not ready" banner some projects add. If any of these settles it, the model is not needed at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Gemma reads the rest.&lt;/strong&gt; When nothing hard blocks an issue, the comments decide. &lt;strong&gt;Gemma 3 4B&lt;/strong&gt;, running in &lt;strong&gt;Ollama&lt;/strong&gt; on our own small server, reads the issue and its latest comments and returns structured JSON: who asked to be assigned, who says they're working on it, what a maintainer decided, a one-line summary of the work, and a difficulty estimate.&lt;/p&gt;

&lt;p&gt;Every claim comes with its proof: the person's real comment, their avatar, the date, and a link. You don't have to trust the model blindly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3srmrdosqy3f9z0jd9sg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3srmrdosqy3f9z0jd9sg.jpg" alt=" " width="799" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stack is deliberately small: plain Node.js, one HTML page, Gemma 3 through Ollama, and two containers on Railway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong (and how I fixed it):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 1B model wasn't good enough.&lt;/strong&gt; &lt;code&gt;gemma3:1b&lt;/code&gt; was fast, but it said "nobody asked" on an issue where five people had. Switching to &lt;code&gt;gemma3:4b&lt;/code&gt; fixed accuracy at about 6 to 16 seconds per issue on CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests timing out.&lt;/strong&gt; The small model kept generating inside the JSON until it hit the timeout. Capping the answer length fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One rule made it worse.&lt;/strong&gt; Someone wrote "I think you missed assigning this to Omer", and it counted as a claim. When I told Gemma to only count people asking for themselves, it became too cautious and missed all five real claimants. A small code rule worked better: let the model be generous, then correct it with cheap checks you can verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden maintainers.&lt;/strong&gt; On freeCodeCamp, a maintainer's standard note was counted as six claims, because many maintainers keep their organisation membership private. Now anyone who labelled or assigned the issue counts as a maintainer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token that hid pull requests.&lt;/strong&gt; After I added a read-only GitHub token, issues with open PRs suddenly looked free. GitHub leaves pull request links out of the issue history for fine-grained tokens. The fix was to also search for PRs that mention the issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked it against 8 real issues I had read by hand, and it got all 8 right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Unclaimed reads other people's comments and usernames. With an open model on our own server, none of that goes to a closed AI company. There's no per-request bill either, so my friend can re-check as often as she likes during a race that is measured in minutes. And because it's open, she can run the whole thing on her own laptop with &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt; and &lt;code&gt;node server.js&lt;/code&gt;. An open model helping people make their first open-source contribution felt like the right fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Gemma&lt;/p&gt;




&lt;p&gt;Built with my best friend &lt;a href="https://github.com/SanjanaG-01" rel="noopener noreferrer"&gt;@SanjanaG-01&lt;/a&gt;. Good luck to everyone in Hacktober, and read the thread before you ask for an issue!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
