<?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: Rohit Purkait</title>
    <description>The latest articles on DEV Community by Rohit Purkait (@codeswithroh).</description>
    <link>https://dev.to/codeswithroh</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%2F740213%2Fbac29546-69e3-48c8-8300-c4fff645040e.JPG</url>
      <title>DEV Community: Rohit Purkait</title>
      <link>https://dev.to/codeswithroh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codeswithroh"/>
    <language>en</language>
    <item>
      <title>TJ Holowaychuk left Express in 2014. 6,483 of his lines still run. Gemma called his commits "3,527 silent years".</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Thu, 29 Oct 2026 22:44:20 +0000</pubDate>
      <link>https://dev.to/codeswithroh/tj-holowaychuk-left-express-in-2014-6483-of-his-lines-still-run-gemma-called-his-commits-3527-3c99</link>
      <guid>https://dev.to/codeswithroh/tj-holowaychuk-left-express-in-2014-6483-of-his-lines-still-run-gemma-called-his-commits-3527-3c99</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;In Hindi, &lt;em&gt;bhoot&lt;/em&gt; means ghost. It also means the past. &lt;em&gt;Bhoot kaal&lt;/em&gt; is the past tense.&lt;/p&gt;

&lt;p&gt;Every old repository has both. There are people who stopped committing years ago but whose code still runs every time someone types &lt;code&gt;npm start&lt;/code&gt;. And there are files that were born, argued over for a few hundred commits, and deleted, with the reason written in a commit message nobody will read again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bhoot&lt;/strong&gt; reads a repository's git history on your own laptop and finds both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Still haunting&lt;/strong&gt;: people who've been silent for a year or more, with how many of their lines &lt;code&gt;git blame&lt;/code&gt; still finds at HEAD, and the oldest line of theirs that's still there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The departed&lt;/strong&gt;: deleted files, each with a tombstone: when it was born and who wrote it, when it died and who removed it, and its &lt;strong&gt;cause of death&lt;/strong&gt;, which is the subject of the commit that deleted it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a local Gemma writes each of them a two-sentence remembrance, in the spirit of Día de los Muertos: warm, a little eerie, never mocking. And then code checks every name, number, date and file in those two sentences against git, because, as it turns out, Gemma is very good at tone and quietly bad at facts.&lt;/p&gt;

&lt;p&gt;The theme asked for something spooky, or something that honours the past. A graveyard for your codebase is both. The ghost in the machine is literal: TJ Holowaychuk hasn't committed to Express since February 2014, and &lt;strong&gt;6,483 of his lines still run&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%2F583louqo1pt3bnuvseco.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%2F583louqo1pt3bnuvseco.png" alt="The ghosts of expressjs/express" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Walk the graveyards: &lt;a href="https://bhoot-graveyard.vercel.app" rel="noopener noreferrer"&gt;bhoot-graveyard.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://bhoot-graveyard.vercel.app/app?repo=express" rel="noopener noreferrer"&gt;expressjs/express&lt;/a&gt;: 12 ghosts, 30 graves, from 6,183 commits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bhoot-graveyard.vercel.app/app?repo=request" rel="noopener noreferrer"&gt;request/request&lt;/a&gt;: the HTTP library half of npm leaned on, deprecated in 2020&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bhoot-graveyard.vercel.app/app?repo=underscore" rel="noopener noreferrer"&gt;jashkenas/underscore&lt;/a&gt;: born 2009&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Light a candle for a ghost (it remembers, in your browser). Hover any badge to see what the checker verified, or what it threw out.&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%2F4a82s18nr5kn65plm90n.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%2F4a82s18nr5kn65plm90n.gif" alt="Walking the Express graveyard" width="760" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To raise your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
git clone https://github.com/codeswithroh/bhoot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;bhoot
node src/cli.ts /path/to/your/repo &lt;span class="nt"&gt;--out&lt;/span&gt; graveyard
open graveyard/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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/codeswithroh" rel="noopener noreferrer"&gt;
        codeswithroh
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/bhoot" rel="noopener noreferrer"&gt;
        bhoot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      भूत: ghost, and the past. The people who left your repo but whose code still runs, and the files that died, remembered by a local Gemma and checked against git.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/bhoot/docs/cover.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%2Fcodeswithroh%2Fbhoot%2FHEAD%2Fdocs%2Fcover.png" alt="Bhoot: the people who left your repo but whose code still runs, and the files that died, remembered by a local Gemma and checked against git" width="100%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bhoot · भूत&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;In Hindi, &lt;em&gt;bhoot&lt;/em&gt; means ghost. It also means the past.&lt;/strong&gt;&lt;br&gt;
Bhoot reads your repository's git history, finds both, and writes them a remembrance that git can vouch for.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://bhoot-graveyard.vercel.app" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/32ea0108cdede3cb6db7ad36914526ca5ed9e78fec30dc909656006d26f5f3d5/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f6c6976652d62686f6f742d2d6772617665796172642e76657263656c2e6170702d6635613532343f7374796c653d666c61742d737175617265" alt="Live graveyards"&gt;&lt;/a&gt;
&lt;a href="https://ollama.com/library/gemma3" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fd21a94913461f30389e5b266a764038efdc7ee77b38827bdb7c724bea2d46d8/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f6d6f64656c2d47656d6d612532303325323034422d3261323633333f7374796c653d666c61742d737175617265" alt="Gemma 3 4B"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/bhoot#faq" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2cc2b5ce093e2aaab4b54f5485bd828ef5808189c4a1a7873007eb8bf3c46a62/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f72756e732d6c6f63616c6c792532432532307669612532304f6c6c616d612d3261323633333f7374796c653d666c61742d737175617265" alt="Runs locally"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/bhoot/test/checker.test.ts" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b820c4a5103c2b8b50f5328471caa27bfc734c44f60246f81cc09fa49c19a9d4/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f74657374732d6e6f64652532302d2d2d2d746573742d3261323633333f7374796c653d666c61742d737175617265" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/bhoot/package.json" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/75423eee5dcc56630476b355ec5040f29281e5badea8a21370d4aa097de6e88f/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f6e6f64652d25453225383925413532322e31382d3261323633333f7374796c653d666c61742d737175617265" alt="Node"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/bhoot/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0e2175b92a6fe0a2e5432f64c24903961fb3a617c9e5349e29163e3c54c63830/68747470733a2f2f7261737465722e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d3261323633333f7374796c653d666c61742d737175617265" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://bhoot-graveyard.vercel.app/app?repo=express" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Walk a graveyard&lt;/strong&gt;&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/bhoot#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/bhoot#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/bhoot#numbers" rel="noopener noreferrer"&gt;Numbers&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/bhoot#faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;&lt;/p&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/bhoot/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcodeswithroh%2Fbhoot%2FHEAD%2Fdocs%2Fdemo.gif" alt="Walking the Express graveyard: the ghosts, lighting a candle, the tombstones, and a family plot" width="760"&gt;&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;TJ Holowaychuk hasn't committed to Express since February 2014. &lt;strong&gt;6,483 of his lines still run.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every old repository has ghosts like that: people who moved on but whose code didn't. It also has graves: files that were born, argued over for a few hundred commits, and deleted, with the reason buried in a commit message. Bhoot finds both on your own laptop. A local Gemma writes each a short remembrance in the spirit of Día de los Muertos, and code checks every name, number, date and file in it against git before it's carved.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#features" rel="noopener noreferrer"&gt;Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#the-graveyards" rel="noopener noreferrer"&gt;The graveyards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#numbers" rel="noopener noreferrer"&gt;Numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/codeswithroh/bhoot#screenshots" rel="noopener noreferrer"&gt;Screenshots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;…&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/codeswithroh/bhoot" 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;
  
  
  Git gives the facts
&lt;/h3&gt;

&lt;p&gt;Everything Bhoot knows comes from four git commands: &lt;code&gt;log&lt;/code&gt; (who committed when, and what they said), &lt;code&gt;blame --line-porcelain&lt;/code&gt; at HEAD (whose lines are still here), &lt;code&gt;log --diff-filter=D -M&lt;/code&gt; (which files died; &lt;code&gt;-M&lt;/code&gt; so a renamed file doesn't count as a death), and &lt;code&gt;show &amp;lt;commit&amp;gt;^:&amp;lt;path&amp;gt;&lt;/code&gt; (what a file said the day before it was deleted).&lt;/p&gt;

&lt;p&gt;The first surprise was identity. In Express's history, TJ Holowaychuk is three people: &lt;code&gt;TJ Holowaychuk&lt;/code&gt;, &lt;code&gt;Tj Holowaychuk&lt;/code&gt;, and &lt;code&gt;visionmedia&lt;/code&gt;, the name on his earliest commits. Counted separately, none of them is the top ghost. Bhoot merges identities with a small union-find: a commit joins its author's name and email, so any spellings that ever shared an email become one person. TJ came out as one ghost: 3,527 commits, silent since 2014-02-19.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma writes the words
&lt;/h3&gt;

&lt;p&gt;Gemma 3 4B runs in Ollama on my 8 GB MacBook. For each stone it gets the facts as plain lines (name, first and last commit, counts, the oldest surviving line or the start of the deleted file) and one instruction: two sentences, Día de los Muertos in spirit, use only these facts.&lt;/p&gt;

&lt;p&gt;It's good at this. "Born July 13, 2010, this guide of 1,366 lines, crafted by Tj Holowaychuk, served as a helpful starting point. It faded April 16, 2012, a quiet end after 168 commits." Every fact there is right.&lt;/p&gt;

&lt;h3&gt;
  
  
  "3,527 silent years"
&lt;/h3&gt;

&lt;p&gt;Here is what Gemma wrote for TJ on the first run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TJ's spirit persists, &lt;strong&gt;3,527 silent years&lt;/strong&gt; woven into 6,483 lines of code, still running from 2009's initial release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every number in it is real. 3,527 is his commit count. It just isn't years.&lt;/p&gt;

&lt;p&gt;That turned out to be the most common way Gemma got things wrong, and not the way I expected. I expected invented numbers. What I got was real numbers attached to the wrong thing: 32 commits became "32 contributors", 9 commits became "nine brief lives", 145 commits became "145 silent years", and a man who was silent for three years was "quietly maintained for three years". A checker that asks "is this number in the facts?" passes every one of those.&lt;/p&gt;

&lt;p&gt;So the checker doesn't just look for numbers. It reads what each number counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A number has to count the right thing.&lt;/strong&gt; "6,483 lines" passes because 6,483 is a line count. "3,527 years" fails because 3,527 is a commit count. A number in front of a noun the facts never count ("316 paths", "nine tests") fails too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A year has to belong to the right event.&lt;/strong&gt; "Born in 2012" must be the birth year; "removed in 2021" must be the death year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A name has to have done what the sentence says.&lt;/strong&gt; "Written by Roman" must be someone who wrote it; "laid to rest by Sebastian" must be the person whose commit deleted it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else must be in the facts&lt;/strong&gt;: months, quotes, file names, function names, and any capitalised word that might be a name. "Sean Strom" can't be built out of the username &lt;code&gt;seanstrom&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a remembrance fails, Gemma gets one retry and is told exactly what was wrong ("it mentioned '3,527 silent years', which is not in the facts"). If it fails again, the stone carries plain dates instead. Every stone has a badge that says which happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  My checker was wrong too
&lt;/h3&gt;

&lt;p&gt;The first full Express run rejected Gemma's first attempt on 28 of 42 stones, and 15 ended up carrying plain dates. By my labels, only 8 of those 42 first attempts were actually false. Most of the rejections were my checker's mistakes, not Gemma's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It thought &lt;strong&gt;July was a person&lt;/strong&gt;. Capitalised mid-sentence, not in the facts as a word, so: an invented name. A file born on 2010-07-13 really was born in July.&lt;/li&gt;
&lt;li&gt;It read &lt;strong&gt;"eighty-four commits" as "four commits"&lt;/strong&gt;, decided four was wrong, and threw out a true sentence.&lt;/li&gt;
&lt;li&gt;On the held-out repo it rejected &lt;strong&gt;"David Chambers' code"&lt;/strong&gt; because it didn't strip a possessive apostrophe, so "Chambers'" wasn't Chambers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is now a test, next to the cases Gemma got wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing it
&lt;/h3&gt;

&lt;p&gt;I read every first attempt Gemma made and labelled it by hand: true, or saying something git contradicts. That includes a real number on the wrong thing. Then I scored the checker against my labels.&lt;/p&gt;

&lt;p&gt;I built and tuned the checker on two repos, Express and request. Then I froze it in a commit (&lt;code&gt;e2f3d9a&lt;/code&gt;) and ran a third repo it had never seen, underscore, once.&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;Gemma's first attempts that were false&lt;/th&gt;
&lt;th&gt;Caught by the checker&lt;/th&gt;
&lt;th&gt;True attempts wrongly rejected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;express + request (checker tuned on these)&lt;/td&gt;
&lt;td&gt;22 of 78 (28%)&lt;/td&gt;
&lt;td&gt;22 / 22&lt;/td&gt;
&lt;td&gt;0 / 56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;underscore, held out&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3 of 16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3 / 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 / 13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the held-out repo, every stone that ended up carved was true: 15 of them in Gemma's words, 1 in plain dates. The one wrongly rejected attempt was the "Chambers'" apostrophe bug; Gemma's retry passed, so nothing was lost.&lt;/p&gt;

&lt;p&gt;Sixteen stones is a small held-out set, and I'm the only labeller. The labels, every attempt Gemma made, and the scoring script are all in the repo (&lt;code&gt;eval/&lt;/code&gt;), so anyone can disagree with me line by line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Family plots
&lt;/h3&gt;

&lt;p&gt;request/request deleted sixteen proxy test files in one commit ("Merge all proxy tests into tests/test-proxy.js"). Sixteen near-identical tombstones made a boring graveyard, so files laid to rest by the same commit now share a family plot: one stone, with its siblings listed underneath.&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%2Fyfkeezmmnkvgix18xrud.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%2Fyfkeezmmnkvgix18xrud.png" alt="A tombstone and its cause of death" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Because a git history is personal.&lt;/strong&gt; It's who stayed late, who left, who rewrote whose code, and what they said when they deleted something. For an open-source repo that's public anyway. For your company's repo, or your team's, it isn't. Bhoot never sends it anywhere: git reads it, Gemma runs in Ollama on the same laptop, and the graveyard is one HTML file. A hosted model would mean uploading your team's entire commit history to remember the people in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I could make it reproducible.&lt;/strong&gt; With the weights on my machine I fix the seed, so the same repo gives the same first attempts every time. That's what made the evaluation honest. When I fixed the checker, I re-ran it on exactly the same 78 attempts Gemma had already written and measured the difference, instead of hoping a new sample came out similar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because it costs nothing to remember everyone.&lt;/strong&gt; Express has 387 people in its history. Writing a remembrance for each of them, with retries, is a lot of calls. Locally it's an evening of a laptop fan, with no budget to think about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the model is swappable.&lt;/strong&gt; &lt;code&gt;BHOOT_MODEL=gemma3:12b&lt;/code&gt; uses a bigger Gemma, and the checker stays the same either way. The checker matters more than the model: a better model will make fewer "3,527 silent years", and the checker catches the ones it still makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I ran it on my own team's repo.&lt;/strong&gt; Everyone was surprised first, and then it turned into a lot of laughing and teasing. Some of the team printed an image of the graveyard and put it up on their walls. Showing it to the whole team was a lot of fun.&lt;/p&gt;

&lt;p&gt;Día de los Muertos isn't about mourning. It's about remembering people by name, with the details right.&lt;/p&gt;

&lt;p&gt;That's what made the checker matter more to me than the prose. A stone that says "Born in 2012" when the file was born in 2011 isn't a remembrance. It's a mistake with a nice font. Getting the small facts right, the year, the line count, the name of the person who wrote it, is the part that honours someone.&lt;/p&gt;

&lt;p&gt;Some of what the graveyards remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Express's original docs: born in July 2010, written by TJ, deleted in April 2012 by a single commit that just says "removed old docs". It took 31 files with it.&lt;/li&gt;
&lt;li&gt;request/request's last commit, in February 2020, fixed a typo in its own deprecation notice.&lt;/li&gt;
&lt;li&gt;Jeremy Ashkenas's oldest line still in underscore is from 2009, the year the library was born: a test fixture in &lt;code&gt;test/arrays.js&lt;/code&gt;, &lt;code&gt;var list = [1, [2], [3, [[[4]]]]];&lt;/code&gt;.&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 Gemma.&lt;/strong&gt; Gemma 3 4B runs locally through Ollama and writes every remembrance. A checker holds it to git's facts, scored against hand labels, with a held-out repo run once on a frozen checker.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>'Baad mein' means later. Gemma reads the WhatsApp chat you keep with yourself, and tells you what you can forget</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Sat, 24 Oct 2026 02:45:49 +0000</pubDate>
      <link>https://dev.to/codeswithroh/baad-mein-means-later-gemma-reads-the-whatsapp-chat-you-keep-with-yourself-and-tells-you-what-3i45</link>
      <guid>https://dev.to/codeswithroh/baad-mein-means-later-gemma-reads-the-whatsapp-chat-you-keep-with-yourself-and-tells-you-what-3i45</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;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Almost everyone I know in India has a WhatsApp chat with themselves. WhatsApp even made it official: "Message yourself".&lt;/p&gt;

&lt;p&gt;It's where &lt;em&gt;later&lt;/em&gt; goes. A YouTube link you'll watch this weekend. "dahi, bread aur 2 maggi". "Raju ko 500 dena hai". "kal 9 baje bank jaana hai KYC ke liye". An address you'll need once. A recipe from Ma. The wifi password. An OTP from March that you never deleted.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Baad mein dekhenge.&lt;/em&gt; We'll see later. Later never comes, the chat gets longer, and every few weeks you scroll up through five months of your own notes, feel bad, and scroll back down.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Baad Mein&lt;/strong&gt; (बाद में, "later"). You export that chat, and Gemma reads it slowly, inside your browser tab, while you sleep. In the morning you get one card:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today and this week&lt;/strong&gt;: what's due, with dates worked out by code, never guessed by the model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money&lt;/strong&gt;: who owes you, who you owe, how much&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To buy&lt;/strong&gt;: every shopping note from the last three weeks, merged into one list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not sure&lt;/strong&gt;: the notes it refuses to guess about, with the reason&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete these from the chat&lt;/strong&gt;: OTPs, card numbers, Aadhaar, PAN and passwords, masked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let go&lt;/strong&gt;: old links, dead to-dos and stale shopping lists, with permission to forget them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the sloth part. Most of what's in a "later" pile doesn't need doing. It needs someone to tell you it's okay to stop carrying it.&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%2Fbeooravom982ch2k16hy.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%2Fbeooravom982ch2k16hy.png" alt="The morning card: 100 notes, 20 need you, 20 you can let go"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://baad-mein.vercel.app" rel="noopener noreferrer"&gt;baad-mein.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;See an example&lt;/strong&gt; for a finished card straight away. It's a made-up 100-note chat, and the answers are Gemma's real replies, recorded from a run on my laptop and replayed through the same code. Or press &lt;strong&gt;Read my chat&lt;/strong&gt; and give it your own export (Chrome or Edge on a laptop; Gemma 4 E2B downloads once, about 2 GB). If you don't want to use your own chat, there's a made-up export to download under "How to get your chat out of WhatsApp".&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%2Ft6lktu6r2zx1kgyq22ib.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%2Ft6lktu6r2zx1kgyq22ib.gif" alt="Baad Mein demo: the home screen, Gemma reading slowly, then the card"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tap any line on the card and it shows the note it came from, word for word, with the day you wrote it.&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/codeswithroh" rel="noopener noreferrer"&gt;
        codeswithroh
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/baad-mein" rel="noopener noreferrer"&gt;
        baad-mein
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Your WhatsApp chat with yourself, read slowly by Gemma 4 in your browser. One card in the morning: what's due, what to buy, who owes what, and what you can let go.
    &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;Baad Mein (बाद में, "later")&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/baad-mein/docs/cover.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%2Fcodeswithroh%2Fbaad-mein%2FHEAD%2Fdocs%2Fcover.png" alt="Baad Mein"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://baad-mein.vercel.app" rel="nofollow noopener noreferrer"&gt;https://baad-mein.vercel.app&lt;/a&gt;&lt;/strong&gt; · press &lt;strong&gt;See an example&lt;/strong&gt;, or read your own chat.&lt;/p&gt;
&lt;p&gt;Everyone has a WhatsApp chat with themselves. Links, "dahi bread aur maggi", "Raju ko 500 dena", an OTP from March. Things you'll deal with later.&lt;/p&gt;
&lt;p&gt;Baad Mein reads that chat slowly, with Gemma 4 running inside your browser tab, and gives you one card in the morning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today and this week&lt;/strong&gt;: what's due, with dates worked out by code, not guessed by the model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money&lt;/strong&gt;: who owes you, who you owe, with amounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To buy&lt;/strong&gt;: every shopping note from the last three weeks, merged into one list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not sure&lt;/strong&gt;: notes it won't guess about, with the reason&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete these from the chat&lt;/strong&gt;: OTPs, card numbers, Aadhaar, PAN and passwords, masked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let go&lt;/strong&gt;: old links, dead to-dos and stale lists you can stop carrying around&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nothing is…&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/codeswithroh/baad-mein" 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;
  
  
  Slow and steady, on purpose
&lt;/h3&gt;

&lt;p&gt;Gemma 4 E2B runs in the tab through MediaPipe's LLM Inference on WebGPU. It reads &lt;strong&gt;eight notes at a time&lt;/strong&gt;, and every answer is saved to IndexedDB the moment it arrives.&lt;/p&gt;

&lt;p&gt;That makes the slowness free. You can start it at night with the laptop on charge and walk away. If the tab closes, the laptop sleeps or the battery dies, open the same chat again and it picks up where it stopped. I tested exactly that: closed the tab at 55 of 100 notes, reopened, and it said "4 batches picked up from last time" and carried on to the same card.&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%2Fwbj87dgeode4c0z83jjw.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%2Fwbj87dgeode4c0z83jjw.png" alt="Gemma reading, eight notes at a time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On my 8 GB MacBook it's about 5 seconds per eight notes, so a 100-note chat takes under a minute, and a few thousand notes is a slow evening. Nobody has to watch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma copies. Code reads.
&lt;/h3&gt;

&lt;p&gt;A 2B model is good at "what kind of note is this?" It's bad at arithmetic, calendars and money. So the work is split in three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your chat export
  ├─ code     secrets (OTP, card, Aadhaar, PAN, passwords) → "delete these"
  │           bare links, empty notes → settled, no model needed
  ├─ Gemma    for each note: todo / buy / money / read / keep / done / noise,
  │           and COPY the words that matter ("what", "when", "who")
  └─ code     checks every copied word is really in the note
              works out every date, every amount, who owes whom
              closes a to-do when a later note says it's done
              marks anything that doesn't add up as "not sure"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma never sees your OTPs. Plain code finds them first, with checksums so a phone number or an order ID doesn't count: Luhn for card numbers, the Verhoeff check UIDAI uses for Aadhaar, the PAN pattern, and the words that announce a password or PIN. Those notes go straight to the "delete these" list, masked, and never reach the prompt.&lt;/p&gt;

&lt;p&gt;Gemma never writes a date. It copies the words ("kal subah 8 baje", "monday ko", "12 tarikh", "parso") and code works out what they mean from the day the note was written. That part came from an earlier project of mine, &lt;a href="https://github.com/codeswithroh/pakka" rel="noopener noreferrer"&gt;Pakka&lt;/a&gt;, where I learned that &lt;em&gt;kal&lt;/em&gt; means both tomorrow and yesterday in Hindi and only the verb tells you which.&lt;/p&gt;

&lt;p&gt;Gemma never decides who owes whom. It copies the name. Code reads the direction from the words: &lt;em&gt;dena, bhejna, wapas karna, dite hobe&lt;/em&gt; means you pay; &lt;em&gt;lena, se lena, nite hobe, debe, owes me&lt;/em&gt; means you get paid. If the words don't say, the note goes to "not sure".&lt;/p&gt;

&lt;p&gt;And Gemma's copies are checked. If the words it returns aren't actually in your note, the card shows your note as you wrote it and says why. On the held-out set, Gemma rewrote instead of copying &lt;strong&gt;0 times out of 49&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why code does the dates: 8 out of 19
&lt;/h3&gt;

&lt;p&gt;I tested the obvious alternative. Same 19 to-dos with due dates, the date each was written right there in the prompt, and Gemma asked to give the due date itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma: 8 of 19 right.&lt;/strong&gt; Eight of its answers weren't even valid dates ("2026-19-Oct-2026"). "renew PUC kal", written on 20 October, came back as the 19th.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code, from the words Gemma copied: 16 of 19 right&lt;/strong&gt;, 2 marked "not sure" instead of guessed, 1 wrong. The two "not sure" ones are honest: "bike service next sunday", written on a Tuesday, could mean the coming Sunday or the one after, so the card asks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing it
&lt;/h3&gt;

&lt;p&gt;I wrote two made-up "chats with yourself" in the mix I actually see: English, Hinglish and Banglish, links, lists, money, IDs, half-typed nonsense. Every note is labelled by hand.&lt;/p&gt;

&lt;p&gt;I tuned the prompt on a &lt;strong&gt;40-note dev set only&lt;/strong&gt;, then froze it in a commit before running the &lt;strong&gt;100-note held-out set&lt;/strong&gt; once. Same model and runtime as the app: Gemma 4 E2B in Chrome with WebGPU, on an 8 GB MacBook, driven by a script so the run is repeatable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Held-out, 100 notes&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kind of note right&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95 / 100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Due dates right&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;16 / 19&lt;/strong&gt; (2 "not sure", 1 wrong)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Money direction right&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9 / 9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets caught before Gemma&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 / 7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma rewrote instead of copying&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 49&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;To-dos closed by a later "done" note&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;49 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The raw runs, every reply included, are in &lt;code&gt;eval/results/&lt;/code&gt; in the repo.&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%2Flih9z1867mf5qjbazd7v.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%2Flih9z1867mf5qjbazd7v.png" alt="The money section: owed to you, you owe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Two bugs worth telling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gemma broke its own JSON, and a whole batch vanished.&lt;/strong&gt; On one dev run, five notes in a row came back as "Gemma didn't say what this is". The reply looked fine at a glance. It wasn't: Gemma had written &lt;code&gt;"who="""&lt;/code&gt; instead of &lt;code&gt;"who":""&lt;/code&gt;, and one bad key made the whole array unparseable. The classification inside was perfect. Now the reader repairs that slip before parsing, there's a test for it, and the dev set went from 36 to 40 out of 40.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netflix was tagged as X.&lt;/strong&gt; The link reader checked whether a URL contained &lt;code&gt;x.com&lt;/code&gt;. So does &lt;code&gt;netflix.com&lt;/code&gt;. A word boundary fixed it, and a test keeps it fixed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it can't do yet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It once read a bill as already paid.&lt;/strong&gt; "pay credit card bill 22nd" came back as &lt;em&gt;done&lt;/em&gt;, which would quietly hide a real bill. That's the mistake I like least, and the next thing I'd fix: a "done" note should need a past-tense verb in the note itself before code believes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"end of month"&lt;/strong&gt; isn't a date code understands yet, so "car PUC expires end of month, renew" goes to "not sure".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plans read as tasks.&lt;/strong&gt; "Pujo plans: Saturday Maddox Square, Sunday Bagbazar" is information, but Gemma filed it as a to-do.&lt;/li&gt;
&lt;li&gt;After the held-out run I fixed one bug it exposed: when Gemma copied the wrong words as the date ("office party ke liye"), code gave up instead of reading "30 oct" from the note. Code now reads the note's own day words in that case. The numbers above are from before the fix.&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%2Fzxitk1ow6sl5f8vx7ws1.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%2Fzxitk1ow6sl5f8vx7ws1.png" alt="Not sure: the notes it refuses to guess about"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Because this is the most private chat on your phone.&lt;/strong&gt; The chat you keep with yourself is where people paste the things they'd never send anyone else: Aadhaar numbers, bank account numbers, the netbanking password "just for now", OTPs. Sending that to a hosted model, to sort it, means handing a stranger the exact pile you'd most like deleted. Baad Mein reads it in your own browser tab. Once the model is downloaded, reading your chat makes no network calls at all. Nothing is uploaded, there's no account, and there's no server to leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because "slow" only works when it's free.&lt;/strong&gt; An overnight job over a few thousand notes is a lot of tokens. On a paid API, "let it run slowly while you sleep" is a bill. On an open model on your own laptop, it costs some electricity, and there's no reason to rush it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I could make it boring.&lt;/strong&gt; With the weights on my machine, I run Gemma greedy (top-k 1, temperature 0), so the same chat gives the same card every time. That made it possible to test properly: a frozen prompt, a held-out set, a recorded example that replays exactly. It also means the example in the app isn't a mock-up. It's Gemma's real output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the rules around the model are mine.&lt;/strong&gt; The secret detection, the date reader, the money words in three languages and the "let go" rules are plain code anyone can read and change. Where an open model ends and my code begins is visible line by line, so when it's wrong, I can say exactly which half was wrong.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;My first user.&lt;/strong&gt; I handed Baad Mein to someone this week who lives in WhatsApp. It's the app they open every day, so their chat with themselves is where the day's reminders end up. After using it, they told me it's genuinely useful: it reminds them of the daily things they'd parked there, and it has helped their productivity. That's the whole point. The reminders were already in WhatsApp; they just needed someone to read them back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers.&lt;/strong&gt; The made-up chat in the example covers five months: 100 notes. Reading through it yourself means scrolling, re-reading, deciding, and still forgetting half.&lt;/p&gt;

&lt;p&gt;Baad Mein read it in &lt;strong&gt;49 seconds&lt;/strong&gt;, unattended, and turned it into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 things that need you&lt;/strong&gt;: 5 due today or overdue, 4 this week, 9 about money, 2 it wasn't sure about&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 things to buy&lt;/strong&gt;, merged from six separate lists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 things you can let go&lt;/strong&gt;, with the reason for each&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 secrets to delete&lt;/strong&gt; from the chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You read one card instead of five months. Tap anything you don't trust and see the original note. Then press &lt;strong&gt;Copy as a WhatsApp message&lt;/strong&gt; and send the card back to yourself, to the same chat, where it belongs.&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%2Fhs6yhmp9wnk7140n2o9w.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%2Fhs6yhmp9wnk7140n2o9w.png" alt="Let go: permission granted"&gt;&lt;/a&gt;&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 E2B runs fully in the browser through MediaPipe on WebGPU. It's held to copying, never computing, and scored on a held-out set with the prompt frozen first.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>'Kal' means yesterday and tomorrow. I built home bakers a WhatsApp order sheet that never guesses which.</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Sun, 18 Oct 2026 01:05:12 +0000</pubDate>
      <link>https://dev.to/codeswithroh/kal-means-yesterday-and-tomorrow-i-built-home-bakers-a-whatsapp-order-sheet-that-never-guesses-21p3</link>
      <guid>https://dev.to/codeswithroh/kal-means-yesterday-and-tomorrow-i-built-home-bakers-a-whatsapp-order-sheet-that-never-guesses-21p3</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;p&gt;In Hindi, &lt;strong&gt;kal&lt;/strong&gt; means tomorrow. It also means yesterday.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Kal shaam 6 baje 1 kg truffle chahiye"&lt;/em&gt; is an order for tomorrow evening. &lt;em&gt;"Kal jo cake liya tha, sabko pasand aaya"&lt;/em&gt; is a thank-you for a cake that already got eaten. Same word. Only the verb tells you which.&lt;/p&gt;

&lt;p&gt;If you've ordered a birthday cake in India lately, there's a decent chance it came from someone's home kitchen, and that the whole order happened on WhatsApp: a flavour, a weight, "eggless please", a name to pipe on top, a GPay screenshot. Home bakers run real businesses this way. The order book is the chat list. The night before a busy day means scrolling through every chat, writing tomorrow's cakes into a notebook, and hoping nobody said "actually 1.5 kg kar do" three messages down.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Pakka&lt;/strong&gt; (Hindi for "confirmed, for sure"). You export the chats from WhatsApp and add them. Gemma 4 reads them &lt;strong&gt;on your own phone&lt;/strong&gt; and Pakka turns them into tomorrow's bake sheet. The one rule I held it to: &lt;strong&gt;when it isn't sure, it asks you. It doesn't guess.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&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%2Fok7ejl8gh8jtgz9mp7w9.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%2Fok7ejl8gh8jtgz9mp7w9.png" alt="Pakka home screen: add WhatsApp chats, try 18 sample chats, or see a recorded run" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pakka is a web app for home bakers who take orders on WhatsApp.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In WhatsApp: open a customer's chat → &lt;strong&gt;⋮&lt;/strong&gt; → &lt;strong&gt;More&lt;/strong&gt; → &lt;strong&gt;Export chat&lt;/strong&gt; → &lt;strong&gt;Without media&lt;/strong&gt;. Do that for each chat with an order.&lt;/li&gt;
&lt;li&gt;Add the files to Pakka (plain &lt;code&gt;.txt&lt;/code&gt; or the &lt;code&gt;.zip&lt;/code&gt; an iPhone makes).&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B reads each chat in your browser and Pakka builds a bake sheet grouped by day: time, customer, cake, weight, eggless or not, the message for the top, price, advance, and what's still due.&lt;/li&gt;
&lt;li&gt;Anything Pakka isn't sure about gets an amber note on the card saying exactly why. One tap shows the chat lines it came from.&lt;/li&gt;
&lt;li&gt;Each order has a &lt;strong&gt;Copy confirmation&lt;/strong&gt; button. It writes a short WhatsApp message back to the customer ("1 kg chocolate truffle (eggless), Sun 19 Oct, 6:00 pm, 'Happy Birthday Riya', balance ₹700. Sab sahi hai?"). The customer is the last check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing is uploaded. No account, no server, no API key. Customer names, phone chats and home addresses stay on the baker's phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for this?&lt;/strong&gt; A home baker with a dozen orders a week. The work Pakka replaces is the late-night notebook session and the occasional disaster: a cake made with egg for a family that doesn't eat egg, a cake ready a day late, a name spelled wrong in icing. One remade cake costs more than a year of a small app. I'd charge a one-time ₹299, or ₹49 a month with backup across devices. That works because there's no per-chat cost to me: the model runs on the baker's phone.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://pakka-gamma.vercel.app" rel="noopener noreferrer"&gt;https://pakka-gamma.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try 18 sample chats&lt;/strong&gt; runs Gemma for real in your browser (WebGPU: Chrome or Edge on a laptop or a recent Android phone, Safari 26). The first run downloads the model once, about 2 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No time for a 2 GB download?&lt;/strong&gt; Open &lt;a href="https://pakka-gamma.vercel.app/?replay" rel="noopener noreferrer"&gt;https://pakka-gamma.vercel.app/?replay&lt;/a&gt;. It replays the replies Gemma gave in my test run through Pakka's checker, live, with the chats re-dated to today.&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%2Fbhy7bzjlnv1oz1u6rw1t.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%2Fbhy7bzjlnv1oz1u6rw1t.png" alt="The bake sheet: totals up top, orders grouped by day, and a summary of what to bake and collect" width="799" height="487"&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%2Fj5aspqheir63v39ev0b6.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%2Fj5aspqheir63v39ev0b6.png" alt="A card Pakka isn't sure about: a weekday named on that same weekday" width="799" height="253"&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%2Fmp10305euer3ijy30pl8.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%2Fmp10305euer3ijy30pl8.png" alt="Chats Gemma saw that Pakka left off the sheet: a price question and a cancelled order" width="800" height="172"&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/codeswithroh" rel="noopener noreferrer"&gt;
        codeswithroh
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/pakka" rel="noopener noreferrer"&gt;
        pakka
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A home baker's WhatsApp orders, turned into tomorrow's bake sheet. Gemma 4 E2B reads the chats on the phone; code checks every detail.
    &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;Pakka&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A home baker's WhatsApp orders, turned into tomorrow's bake sheet.&lt;/strong&gt;
Gemma 4 reads the chats on the baker's own phone. Code checks every detail against the chat.&lt;/p&gt;
&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://pakka-gamma.vercel.app" rel="nofollow noopener noreferrer"&gt;https://pakka-gamma.vercel.app&lt;/a&gt;&lt;/strong&gt; · recorded run, no download: &lt;strong&gt;&lt;a href="https://pakka-gamma.vercel.app/?replay" rel="nofollow noopener noreferrer"&gt;https://pakka-gamma.vercel.app/?replay&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Hindi, &lt;em&gt;kal&lt;/em&gt; means tomorrow and also yesterday. &lt;em&gt;"Kal shaam 6 baje 1 kg truffle chahiye"&lt;/em&gt; is an order
&lt;em&gt;"Kal jo cake liya tha, sabko pasand aaya"&lt;/em&gt; is a thank-you. Pakka's rule: when it isn't sure, it asks the baker. It doesn't guess.&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;Export chats from WhatsApp (&lt;strong&gt;⋮ → More → Export chat → Without media&lt;/strong&gt;) and add the &lt;code&gt;.txt&lt;/code&gt; / &lt;code&gt;.zip&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; runs in the browser (MediaPipe LLM Inference on WebGPU, ~2 GB, downloaded once and cached)
It finds each order and &lt;strong&gt;copies the exact words&lt;/strong&gt; for each detail. It never converts or calculates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code does the reading&lt;/strong&gt;, and every step is unit-tested
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;findQuote()&lt;/code&gt;…&lt;/li&gt;
&lt;/ul&gt;
&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/codeswithroh/pakka" 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;The stack is small on purpose: TypeScript and Vite, no framework, no backend, deployed as static files on Vercel.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;litert-community/gemma-4-E2B-it-litert-lm&lt;/code&gt;, web build)&lt;/td&gt;
&lt;td&gt;Reads each chat and copies the words for each detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;MediaPipe LLM Inference&lt;/strong&gt; on WebGPU&lt;/td&gt;
&lt;td&gt;Runs Gemma in the browser. Weights stream from Hugging Face and are kept in the Cache API, so later visits don't download them again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;whatsapp.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parses Android and iPhone export formats, multi-line messages, &lt;code&gt;.zip&lt;/code&gt; exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;when.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns "kal shaam 6 baje" into a date and a time, and says when it can't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fields.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns "aadha kilo", "dedh kg", "₹1,200", "500 bhej diya", "bina ande ka" into values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extract.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The prompt, plus the checker that holds Gemma's answer up against the chat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Gemma copies. Code reads.
&lt;/h3&gt;

&lt;p&gt;The obvious design is to ask Gemma for the finished answer: a date like &lt;code&gt;2026-10-18&lt;/code&gt;, a weight like &lt;code&gt;1.5&lt;/code&gt;, &lt;code&gt;eggless: true&lt;/code&gt;. I tested that design as the baseline. It reads beautifully. Eggless, advance paid, the name for the cake: all right. Then you check the dates. Rahul thanked the baker for yesterday's cake ("kal jo red velvet liya tha…") and in the next message ordered a pineapple cake for Tuesday. Gemma put the pineapple cake on &lt;strong&gt;tomorrow&lt;/strong&gt;. Ishita asked for "kal", heard back "kal full hai, parso chalega?", and agreed to parso. Gemma put it on &lt;strong&gt;tomorrow&lt;/strong&gt; too.&lt;/p&gt;

&lt;p&gt;So I split the job. Gemma does the part small models are good at: &lt;strong&gt;find the order and copy the exact words for each detail&lt;/strong&gt;. The prompt says it plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each order, copy the customer's or baker's EXACT words for each detail,
character for character, from one message. Do not translate, explain,
convert or calculate anything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What comes back looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chocolate truffle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 kg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"eggless"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eggless please"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"day"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kal shaam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6 baje"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cake_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upar likh dena &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Happy Birthday Riya&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1200 hoga"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"advance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500 advance bhej diya"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then code takes over, and every step is something you can test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the quote really in the chat?&lt;/strong&gt; &lt;code&gt;findQuote()&lt;/code&gt; searches the messages (newest first, ignoring case and punctuation, allowing for a small model that trims a word). If the words aren't there, the detail is dropped and the card says &lt;em&gt;"Gemma wrote '…', but those words aren't in the chat."&lt;/em&gt; Gemma can't add a detail that isn't in the chat. It can only point at one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What do the words mean?&lt;/strong&gt; "aadha kilo" is 0.5 kg. "1.2k" is ₹1,200. "egg chalega" means with egg. Each parser returns &lt;code&gt;null&lt;/code&gt; instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did they change it later?&lt;/strong&gt; If a later message mentions a different size, the card asks: &lt;em&gt;"A later message says 1.5 kg. Did they change it?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Kal is two days
&lt;/h3&gt;

&lt;p&gt;Gemma never decides what "kal" means. It copies "kal shaam" and code works out the day, relative to &lt;strong&gt;when that message was sent&lt;/strong&gt;, not when you open the app.&lt;/p&gt;

&lt;p&gt;For "kal" and "parso" (which also points both ways: the day before yesterday, or the day after tomorrow), &lt;code&gt;when.ts&lt;/code&gt; looks at the clause the word sits in. Past-tense markers (&lt;em&gt;tha, liya, diya, bheja, aaya&lt;/em&gt;) push one way, request markers (&lt;em&gt;chahiye, dena, ready, pickup, tak&lt;/em&gt;) push the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twoWay&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isHindi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clauseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;ke&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+liye&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "ke liye" = "for"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;past&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PAST&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FUTURE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;past&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" here sounds like the past…`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;past&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" mixes past and future…`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works per clause because real messages mix tenses: &lt;em&gt;"kal 4 baje de dena, 500 bhej diya"&lt;/em&gt; has a past verb in it (&lt;em&gt;bhej diya&lt;/em&gt;, "I sent"), but in a different clause from &lt;em&gt;kal&lt;/em&gt;. Splitting on commas and &lt;em&gt;aur / par / lekin&lt;/em&gt; keeps that order on the sheet for tomorrow.&lt;/p&gt;

&lt;p&gt;The same file handles the other ways a day gets named: "Sunday" sent on a Sunday (today, or next week?), Indian-English "next Sunday" (usually the coming one, not always), "25th", "25 tarikh", "3rd" when it's already the 17th (next month), and Devanagari (&lt;em&gt;कल शाम 7 बजे&lt;/em&gt;). Times without am/pm ("6 baje") get read from the words around them (&lt;em&gt;shaam&lt;/em&gt;, &lt;em&gt;subah&lt;/em&gt;); when there are none, Pakka guesses afternoon and tells you it guessed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing it
&lt;/h3&gt;

&lt;p&gt;I wrote &lt;strong&gt;18 WhatsApp chats&lt;/strong&gt; by hand to test it. They're modelled on how orders to home bakers read, but every chat and name is made up. Most are ordinary orders. Some are traps: "kal" about yesterday, "parso" about a complaint, a size changed the next morning, an order cancelled, a customer only asking prices, Devanagari script, two orders in one chat, "next Sunday", and a weekday named on that same weekday.&lt;/p&gt;

&lt;p&gt;I ran every chat through Gemma 4 E2B twice, in the Chromium browser pane on a Mac mini (M4, 16 GB), with the same runtime the app uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma alone:&lt;/strong&gt; told today's date and asked for the finished values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pakka:&lt;/strong&gt; Gemma copies the words, and the checker and parsers above do the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main set: 18 chats, 16 orders.&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;&lt;/th&gt;
&lt;th&gt;Gemma alone&lt;/th&gt;
&lt;th&gt;Pakka&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery day right&lt;/td&gt;
&lt;td&gt;4 / 16&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16 / 16&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery day &lt;strong&gt;confidently wrong&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;8&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;No day at all&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;13 / 16&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;10 / 15&lt;/td&gt;
&lt;td&gt;14 / 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;13 / 16&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eggless&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;td&gt;16 / 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advance paid&lt;/td&gt;
&lt;td&gt;14 / 14&lt;/td&gt;
&lt;td&gt;14 / 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text for the cake&lt;/td&gt;
&lt;td&gt;15 / 16&lt;/td&gt;
&lt;td&gt;15 / 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders that weren't orders&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Right" for the two ambiguous chats ("Saturday" sent on a Saturday, "next Sunday") means the card asks the baker. Gemma alone has no way to say "not sure" in that format, so those two count against it by design. Take them out and Gemma alone still gets 10 of the other 14 dates wrong or leaves them blank. Pakka gets 0.&lt;/p&gt;

&lt;p&gt;Now the part I have to be upfront about. Pakka's &lt;strong&gt;first&lt;/strong&gt; run on this set wasn't 16/16. It was 15 right, 1 asked, 0 wrong, with 3 orders that weren't orders: a customer who only asked the price, an order cancelled the next morning, and a size change that Gemma returned as two separate orders. I fixed those with three chat-level rules in code (a price question with no day and no money is an enquiry; a later "cancel kar do" takes the order off; a repeated item is one order with the latest details), plus one bug: "ke liye" ("for") was being read as the past-tense &lt;em&gt;liye&lt;/em&gt;. Then I re-scored the same recorded replies. That's tuning on the test set, so I wrote &lt;strong&gt;8 new chats&lt;/strong&gt; after freezing the code and ran them once:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Held-out set: 8 new chats, 7 orders, code frozen, run once.&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;&lt;/th&gt;
&lt;th&gt;Gemma alone&lt;/th&gt;
&lt;th&gt;Pakka&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery day right&lt;/td&gt;
&lt;td&gt;3 / 7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6 / 7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery day &lt;strong&gt;confidently wrong&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;4&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;Asked the baker&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;6 / 7&lt;/td&gt;
&lt;td&gt;7 / 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price · advance&lt;/td&gt;
&lt;td&gt;7 / 7 · 7 / 7&lt;/td&gt;
&lt;td&gt;7 / 7 · 7 / 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders that weren't orders&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one it asked about was a real bug: "the day after tomorrow" contains the word "tomorrow", so &lt;code&gt;when.ts&lt;/code&gt; saw two different days and asked which one. It failed the safe way. That's fixed now (with a test), and the held-out set re-scores 7/7. The numbers in the table are from before the fix.&lt;/p&gt;

&lt;p&gt;The row I care most about is &lt;strong&gt;confidently wrong dates&lt;/strong&gt;: the cake is on the sheet for the wrong day and nothing warns you. Across both sets, Gemma alone got &lt;strong&gt;12&lt;/strong&gt; delivery days confidently wrong, and &lt;strong&gt;5&lt;/strong&gt; of those 12 were "tomorrow": for a Tuesday, for a Monday, for &lt;em&gt;parso&lt;/em&gt; twice (once written परसों), and for a &lt;em&gt;parso&lt;/em&gt; agreed right after a &lt;em&gt;kal&lt;/em&gt; was turned down. When it isn't sure, a small model reaches for tomorrow. Pakka got &lt;strong&gt;0&lt;/strong&gt; wrong. Same model, same chats. The only difference is who does the date arithmetic.&lt;/p&gt;

&lt;p&gt;Here's the Rahul chat on Pakka's sheet. The highlighted lines are where each detail came from. The "kal" message about yesterday's cake isn't highlighted, because nothing on the card came from it:&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%2Fzto37ciuryxai5ds9nsy.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%2Fzto37ciuryxai5ds9nsy.png" alt="Rahul's order on the right day, with the chat lines each detail came from (the demo re-dates the chats to today, so the weekday differs from the eval run)" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The eval page ships with the app (&lt;code&gt;/eval.html&lt;/code&gt;), and every raw Gemma reply is committed in &lt;code&gt;eval/results/&lt;/code&gt;. &lt;code&gt;node eval/rescore.ts &amp;lt;file&amp;gt;&lt;/code&gt; re-scores them with the current checker, no model needed.&lt;/p&gt;

&lt;p&gt;Speed: &lt;strong&gt;2.4 seconds per chat&lt;/strong&gt; (median) on the M4 once the model is loaded. A dozen chats takes about half a minute. Fine for the night before, not instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it can't do yet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The test set is mine.&lt;/strong&gt; I wrote those chats, so they reflect what I thought to test. Real chats will be messier: voice notes, photos of reference cakes, orders split across days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice notes are skipped.&lt;/strong&gt; Lots of orders arrive as voice notes. Gemma 4 E2B can take audio, so that's the next step, but I haven't built it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Needs WebGPU and 2 GB.&lt;/strong&gt; Older Android phones can't run it. The recorded replay shows what Pakka does, but it can't read &lt;em&gt;your&lt;/em&gt; chats on those phones. And in the browser I tested in, the cached model didn't survive a reload, so it downloaded again. Browsers that cap storage per site will do the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exporting is a chore.&lt;/strong&gt; One chat at a time from WhatsApp. On Android, a share target that receives the export directly would remove a step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't know your menu or prices.&lt;/strong&gt; It reads what was said. It doesn't check that a 1.5 kg truffle is ₹1,350.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Because the chats are private.&lt;/strong&gt; A baker's WhatsApp has customers' names, phone numbers, home addresses, their kids' names and birthdays. With an open-weight model running in the browser, none of it goes anywhere. I didn't have to write a privacy policy to make that true. It's just how the app works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the margins are small.&lt;/strong&gt; A home baker makes a few hundred rupees on a cake. A hosted model would charge for every chat, every night. Gemma on the phone costs nothing per chat, so I can sell this for a one-time price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the kitchen Wi-Fi is bad.&lt;/strong&gt; Once Gemma is on the phone, reading chats needs no connection at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I could open it up and fix it.&lt;/strong&gt; When Gemma wrote finished values, I couldn't see why it got a date wrong. With an open model, I could change what I asked of it, run every test chat through it twice on my own machine for free, and move every judgement I didn't trust out of the model and into code a person can read and test.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 E2B runs fully in the browser (MediaPipe LLM Inference on WebGPU), reading Hinglish and Devanagari WhatsApp chats on the baker's own device. The design is built around what a 2B-effective model is good at (finding and copying) and keeps it away from what it isn't (date arithmetic in a language where one word means two days), with an eval to show the difference.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you know a home baker who lives in WhatsApp, send them the link, and tell me what Pakka got wrong.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built an AI that only works when my phone is in my pocket</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Wed, 07 Oct 2026 05:56:15 +0000</pubDate>
      <link>https://dev.to/codeswithroh/i-built-an-ai-that-only-works-when-my-phone-is-in-my-pocket-3ipn</link>
      <guid>https://dev.to/codeswithroh/i-built-an-ai-that-only-works-when-my-phone-is-in-my-pocket-3ipn</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;Every "go outside" app I have tried wants me to look at it once I'm outside: a map, a streak, a badge, a feed. I wanted the opposite: an app that is &lt;strong&gt;finished with me before I reach the front door&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drift&lt;/strong&gt; is a walk with no destination. You choose how long and a mood (&lt;em&gt;quiet&lt;/em&gt;, &lt;em&gt;colors&lt;/em&gt;, &lt;em&gt;old things&lt;/em&gt;, &lt;em&gt;sounds&lt;/em&gt;). &lt;strong&gt;Gemma 4&lt;/strong&gt;, running in your browser, writes a short set of playful rules. An open-weight voice reads them to you, one at a time, while the phone sits in your pocket:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Trace the outline of the oldest brick you can find with your hand."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Follow the faint scent that seems to remember rain."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Stand still until the distant, muted sound settles into your bones."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Those are real rules Gemma wrote during testing.)&lt;/p&gt;

&lt;p&gt;The idea is borrowed from the &lt;strong&gt;dérive&lt;/strong&gt; (French for "drift"), a practice from the 1950s Situationists in Paris. They walked cities by following rules instead of routes, to notice what a destination hides. Seventy years later we have a better excuse never to look up, so I gave the rules to an AI and took the screen away.&lt;/p&gt;

&lt;p&gt;When you get home, you &lt;strong&gt;talk&lt;/strong&gt; for a minute about what you noticed. Whisper transcribes it on the device, and Gemma writes it up as an entry in a field notebook. Each entry leaves one line of memory, so the next drift leans toward what you enjoyed.&lt;/p&gt;

&lt;p&gt;There is one number Drift actually cares about: &lt;strong&gt;how many seconds your screen was on during the walk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;Pip&lt;/strong&gt; 🦔, who keeps you company before and after. Pip watches your finger, falls asleep while you walk, and gets star-eyed if you barely looked at your phone.&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%2F0h37seoohquxxy83t9h8.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%2F0h37seoohquxxy83t9h8.png" alt="Drift: choose a mood, walk with your phone locked, see your screen time" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  I took it outside
&lt;/h3&gt;

&lt;p&gt;I opened Drift on my iPhone in Safari, and Gemma 4 wrote me a drift called &lt;strong&gt;The Quiet Current&lt;/strong&gt;, right there on the phone. Then I went out and walked for about ten minutes.&lt;/p&gt;

&lt;p&gt;I noticed things I hadn't noticed before: how the leaves were moving, and how the wind felt when it touched me. It was hot out, but there was a cool edge underneath the heat. It's October, and even in the sun I could feel a little of the cold that's coming. It was a very engaging feeling, and honestly I can't quite put it into words.&lt;/p&gt;

&lt;p&gt;Then I came home and looked at what Drift had recorded, and it caught me out:&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%2Fbabyubiz17n4ncdymobq.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%2Fbabyubiz17n4ncdymobq.png" alt="My first real drift in the field notebook" width="622" height="292"&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;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;td&gt;&lt;em&gt;The Quiet Current&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;iPhone, Safari, Gemma 4 running on the phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actually walked&lt;/td&gt;
&lt;td&gt;about 10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Walk the app counted&lt;/td&gt;
&lt;td&gt;2 minutes (I ended the drift early and kept walking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen on&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 min 5 sec, the whole time it was counting&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notebook entry&lt;/td&gt;
&lt;td&gt;&lt;em&gt;"Thank you."&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honest lessons from one short walk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I didn't lock my phone.&lt;/strong&gt; I kept Drift open in my hand, so the screen-time counter did exactly its job and recorded that the screen was on the whole time. The best part of the walk happened after I stopped looking at the app. That's the whole thesis of Drift, demonstrated on its own author.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My notebook entry just said "Thank you."&lt;/strong&gt; That's a well-known Whisper failure: when it hears silence or almost nothing, it outputs "Thank you." (it was trained on a lot of subtitles). Drift saved that as my notebook entry. It now checks whether a recording is too short or too quiet, drops Whisper's silence phrases, and asks you to try again. There's a test for it with real Whisper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time the phone goes in my pocket.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a drift actually looks like
&lt;/h3&gt;

&lt;p&gt;Here is a real one, written by Gemma 4 E2B running on WebGPU in my browser. I picked 30 minutes and the mood &lt;em&gt;old things&lt;/em&gt;. Gemma wrote this in &lt;strong&gt;11.5 seconds&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Echoes in the Cobblestones&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the object with the deepest shadow beneath a large leaf.&lt;/li&gt;
&lt;li&gt;Listen intently to the rhythm of the oldest wood.&lt;/li&gt;
&lt;li&gt;Touch the texture of the wall that feels the longest.&lt;/li&gt;
&lt;li&gt;Follow the faint scent that seems to remember rain.&lt;/li&gt;
&lt;li&gt;Stand still until the distant, muted sound settles into your bones.&lt;/li&gt;
&lt;li&gt;Allow your gaze to simply rest on the most enduring shape nearby.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;None of these need a map, and none of them could be followed while looking at a phone. That's the point. You can't do &lt;em&gt;"touch the texture of the wall that feels the longest"&lt;/em&gt; from a screen.&lt;/p&gt;

&lt;p&gt;Every time you press &lt;strong&gt;Prepare&lt;/strong&gt; the rules are new. Other drifts Gemma wrote while I was building: &lt;em&gt;The Echo Chamber Wander&lt;/em&gt;, &lt;em&gt;Chromatic Descent&lt;/em&gt;, &lt;em&gt;Dust &amp;amp; Echoes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Then Kokoro reads them in a calm voice, with a soft chime before each rule and minutes of silence after it. The rendered walk for &lt;em&gt;The Echo Chamber Wander&lt;/em&gt; came out at &lt;strong&gt;31 minutes of audio, from a single file&lt;/strong&gt;. On the deployed site I watched the counter tick from "Starting" to "Rule 1 of 6" while the audio played, with the page doing nothing else.&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;Time on my laptop (Apple GPU, WebGPU)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 writes a drift&lt;/td&gt;
&lt;td&gt;11.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 writes a notebook entry&lt;/td&gt;
&lt;td&gt;2.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-time model download (2 GB)&lt;/td&gt;
&lt;td&gt;~11–14 min on my connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network needed during the walk&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;🌿 &lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://drift-three-nu.vercel.app" rel="noopener noreferrer"&gt;drift-three-nu.vercel.app&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/codeswithroh/drift" rel="noopener noreferrer"&gt;github.com/codeswithroh/drift&lt;/a&gt;. I've run it on an iPhone (Safari) and a MacBook (Chrome). It needs WebGPU: Chrome or Edge, or Safari 26+. The first visit downloads about 2.5 GB of models, so use Wi-Fi.&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%2F44bqtp4ea6n3mwk5k6ok.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%2F44bqtp4ea6n3mwk5k6ok.gif" alt="The Drift flow in under a minute" width="360" height="779"&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;Choose&lt;/th&gt;
&lt;th&gt;Prepare&lt;/th&gt;
&lt;th&gt;Walk&lt;/th&gt;
&lt;th&gt;Screen time&lt;/th&gt;
&lt;th&gt;Notebook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2F046g5c8m2gxt64vf0v17.png" alt="Home" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2Fh72qi21k89ogbocfim69.png" alt="Ready" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2Fa4y8xk0eiqnu2if3inv0.png" alt="Walking" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2Feppwntxugtf7krj1ucxe.png" alt="Screen time" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2Fvxgowwec7woy5nq6tgln.png" alt="Notebook entry" width="390" height="844"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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/codeswithroh" rel="noopener noreferrer"&gt;
        codeswithroh
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/drift" rel="noopener noreferrer"&gt;
        drift
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A walk with no destination. On-device Gemma writes the rules, Kokoro reads them, your phone stays in your pocket.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/drift/docs/hero.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%2Fcodeswithroh%2Fdrift%2FHEAD%2Fdocs%2Fhero.png" alt="Drift: a walk with no destination. Gemma writes the rules, a voice reads them, your phone stays in your pocket." width="100%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Drift&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A walk with no destination.&lt;/strong&gt; An open-weight AI writes the rules, an open-weight voice reads them, and your phone stays in your pocket.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://drift-three-nu.vercel.app" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fe0810bccbaf4a91b87feba9fb0efc8598fb9569597986c92003949b1beb74a2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64656d6f2d64726966742d2d74687265652d2d6e752e76657263656c2e6170702d3364366233353f7374796c653d666c61742d737175617265" alt="Live demo"&gt;&lt;/a&gt;
&lt;a href="https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2a5f733407fdb96a21e2da726ec71c473041e6241251909647d99c282c008540/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c4c4d2d47656d6d61253230342532304532422d3166326131663f7374796c653d666c61742d737175617265" alt="Gemma 4"&gt;&lt;/a&gt;
&lt;a href="https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/288e6e4fb2a828b257290218aeeb6e61a8b03820a6d8e733de9a33305f7bda1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f766f6963652d4b6f6b6f726f2d2d38324d2d3166326131663f7374796c653d666c61742d737175617265" alt="Kokoro"&gt;&lt;/a&gt;
&lt;a href="https://huggingface.co/onnx-community/whisper-base" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/62aa90432d559eec3707b378c406e25bfe00ca6595d05c2758a1141d949a0a54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c697374656e65722d576869737065722d3166326131663f7374796c653d666c61742d737175617265" alt="Whisper"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/drift#tests" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/be6ddf3f839008beac4b6cd9589470f9dd3f607ab966b513fef8d2a384628aa3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6532652d506c61797772696768742532432532307265616c2532306d6f64656c732d3564366235383f7374796c653d666c61742d737175617265" alt="E2E tests"&gt;&lt;/a&gt;
&lt;a href="https://github.com/codeswithroh/drift/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fb095352565fb7c72d7c746263da1a3f8772d36e5cbb72a4182bc0d4791fc1c9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d3564366235383f7374796c653d666c61742d737175617265" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://drift-three-nu.vercel.app" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/drift#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/drift#run-it-locally" rel="noopener noreferrer"&gt;Run it&lt;/a&gt; · &lt;a href="https://github.com/codeswithroh/drift#tests" rel="noopener noreferrer"&gt;Tests&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Every "go outside" app I have used wants me to look at it outside: a map, a feed, a badge. Drift takes the opposite approach. You spend about 30 seconds on the screen before you leave, and then the phone goes in your pocket.&lt;/p&gt;
&lt;p&gt;It is based on the &lt;strong&gt;dérive&lt;/strong&gt; (French for "drift"), a 1950s Situationist practice: walk a city by following playful rules instead of a destination, and notice what the route would have hidden. Drift asks Gemma to invent those rules for you, has an open-weight voice read them, and counts &lt;strong&gt;how many seconds your screen was on&lt;/strong&gt; while you walked.&lt;/p&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/drift/docs/drift.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fcodeswithroh%2Fdrift%2FHEAD%2Fdocs%2Fdrift.gif" alt="The Drift flow: choose a mood, prepare, walk with the phone locked, see your screen time, speak a debrief, read the notebook entry" width="300"&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;🌿 &lt;strong&gt;Rules, not routes.&lt;/strong&gt; Gemma writes a fresh drift for…&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/codeswithroh/drift" 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;Drift is a static web app: Vite and TypeScript, no framework, and &lt;strong&gt;no backend at all&lt;/strong&gt;. Every model runs on your device.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Open model&lt;/th&gt;
&lt;th&gt;How it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write the rules + the notebook&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (web build, 2 GB)&lt;/td&gt;
&lt;td&gt;MediaPipe LLM Inference on &lt;strong&gt;WebGPU&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read the rules aloud&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Kokoro-82M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kokoro-js&lt;/code&gt;, WebGPU or WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Listen to your debrief&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Whisper base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transformers.js&lt;/code&gt;, WebGPU or WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The trick: the whole walk is one audio file
&lt;/h3&gt;

&lt;p&gt;My first idea was to speak each rule on a timer. That doesn't work: a locked phone suspends a web page's timers and background GPS, so the app goes to sleep with you.&lt;/p&gt;

&lt;p&gt;Audio, though, keeps playing. So Drift does all the work &lt;strong&gt;before you leave&lt;/strong&gt;. Gemma writes, say, 7 rules. Kokoro speaks each one. Then I stitch them into a single WAV file: a soft two-note chime, the spoken rule, then minutes of silence while you walk, all repeated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;af_heart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toMono&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampling_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;walkSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt; &lt;span class="c1"&gt;// walk&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You press play, lock the phone, and the next 30 minutes need no wake-ups, no GPS and no signal. The screen-time counter is just the Page Visibility API: every second the page is visible during the walk counts against you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that broke (and what I learned)
&lt;/h3&gt;

&lt;p&gt;I wrote end-to-end tests with Playwright that drive the real app with &lt;strong&gt;real models&lt;/strong&gt;: real Gemma, real Kokoro, and a fake microphone playing recorded speech so Whisper has something to transcribe. They found bugs I would have shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 returned an empty string.&lt;/strong&gt; MediaPipe passes your prompt to the model as-is, and Gemma 4 only answers inside its chat template (&lt;code&gt;&amp;lt;|turn&amp;gt;user … &amp;lt;turn|&amp;gt;&amp;lt;|turn&amp;gt;model&lt;/code&gt;). Without it, every drift silently fell back to my built-in rules. One line fixed it, and it was invisible until a test asserted that the title &lt;em&gt;wasn't&lt;/em&gt; the fallback title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 2 GB model didn't fit.&lt;/strong&gt; Some browsers cap a site's storage below 2 GB. My &lt;code&gt;cache.put()&lt;/code&gt; failed quietly, so the app re-downloaded Gemma on every visit. Now it checks &lt;code&gt;navigator.storage.estimate()&lt;/code&gt; first and tells you honestly when the model can't be kept offline. The walk itself still never needs a network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models can't add up.&lt;/strong&gt; Gemma's durations rarely total the 30 minutes you asked for, so Drift rescales the silences to fit exactly. Gemma also loved copying my example rules word for word, so the prompt now asks it to invent its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The notebook made things up.&lt;/strong&gt; My tests play a recorded debrief through a fake microphone: &lt;em&gt;"I followed a yellow bicycle past a bakery. Then I stood under a huge old tree and listened to the wind in the leaves."&lt;/em&gt; Whisper transcribed it correctly. But with the smaller laptop fallback model (Gemma 3 4B via Ollama), the notebook entry came back mentioning &lt;em&gt;"the coarse, warm texture of the brick wall against my palm."&lt;/em&gt; I never said anything about a wall. Gemma 4 had the opposite problem at first: it copied the transcript word for word. The prompt now asks for a rewrite in a naturalist's voice and forbids inventing sights. A notebook that lies about your own walk is worse than no notebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Thank you."&lt;/strong&gt; My first real walk produced a notebook entry that was just Whisper's silence hallucination (see above). Silent recordings are now rejected before they reach the notebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Rule 5 of 4."&lt;/strong&gt; The progress counter included the intro and outro clips. A classic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pip
&lt;/h3&gt;

&lt;p&gt;Pip is drawn as two 3×3 sprite sheets (nine head directions, nine expressions), adapted from the MIT-licensed &lt;a href="https://github.com/nilbuild/page-mascot" rel="noopener noreferrer"&gt;page-mascot&lt;/a&gt; by Kamran Ahmed. I ported the React component to plain TypeScript and gave Pip a mood for every screen. It's a small thing, but it makes a 2 GB download feel like waiting with a friend.&lt;/p&gt;

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

&lt;p&gt;Drift could not exist as a closed-API app, for three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It has to work with no signal.&lt;/strong&gt; The walk happens where the network doesn't reach: parks, trails, the back streets the rules send you down. With an open-weight model the rules are written on your phone before you leave, and the walk needs nothing at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your voice stays yours.&lt;/strong&gt; The debrief is a recording of you, outside, saying what you noticed. Sending that to someone else's server to be transcribed and summarized would make me uneasy, so with Whisper and Gemma in the browser it never leaves the phone. There's no account, no API key, and no database. There is nothing to leak because there is nothing to collect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It costs nothing to run.&lt;/strong&gt; Drift is a static site. Every walk is computed by the walker's own device, so 10 users or 10,000 cost me the same: zero. A closed-model version would need a backend, a bill, and eventually a subscription. That's a strange thing to charge for in an app whose whole point is using your phone &lt;em&gt;less&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Open models also let me make design choices a hosted API never would: swap Gemma for a smaller model on older phones, pick the voice, change exactly how the notebook is written, and read the source of every layer when something breaks (see "Gemma 4 returned an empty string").&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 E2B runs entirely in the browser on WebGPU via MediaPipe. It writes every drift, writes every notebook entry, and keeps the memory that shapes the next walk.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Thanks to the Gemma, LiteRT, Kokoro, Whisper and transformers.js teams for making all of this run in a browser tab, and to Guy Debord for the idea, 70 years early.&lt;/em&gt; 🌿&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My mom reads Bengali, not English. So I built her a reader that catches scams, on open-weight Gemma.</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:41:19 +0000</pubDate>
      <link>https://dev.to/codeswithroh/my-mom-reads-bengali-not-english-so-i-built-her-a-reader-that-catches-scams-on-open-weight-gemma-47ef</link>
      <guid>https://dev.to/codeswithroh/my-mom-reads-bengali-not-english-so-i-built-her-a-reader-that-catches-scams-on-open-weight-gemma-47ef</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;Last month Ma forwarded me a WhatsApp message saying her SBI account would be blocked unless she updated her KYC. She hadn't clicked the link. She had called the number.&lt;/p&gt;

&lt;p&gt;My mom reads Bengali. So do my grandparents. Most of the paper that runs their lives doesn't: electricity bills, bank letters, the strip on a blood-pressure tablet, and the steady stream of "Dear Customer, your account will be BLOCKED today" messages that every Indian parent's phone collects.&lt;/p&gt;

&lt;p&gt;So every one of those ends up as a photo sent to me, with "এটা কী?" (what is this?). If I'm in a meeting, it waits. If the message is a scam, waiting is the dangerous part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://chithi-teal.vercel.app" rel="noopener noreferrer"&gt;চিঠি (Chithi)&lt;/a&gt;&lt;/strong&gt; means "letter" in Bengali. It does one thing. You point the phone at any piece of paper or any screenshot, and it says out loud, in your language:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What it is.&lt;/strong&gt; "This is your CESC electricity bill."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What you need to do.&lt;/strong&gt; "Pay ₹1,182.00 by 12-10-2026."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether it's a trap.&lt;/strong&gt; A red &lt;strong&gt;প্রতারণা!&lt;/strong&gt; (scam!) stamp, the words "Do nothing. Don't tap the link, don't call the number in the message, don't share an OTP," and one big button that calls me.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It started as a Bengali app for my family. Then I realised the same problem exists in every Indian language, so it now speaks all 22 languages in India's Eighth Schedule, plus English.&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%2F81l8iz4umefhnt6pbmw9.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%2F81l8iz4umefhnt6pbmw9.png" alt="One fake SBI KYC message read in six languages"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it: &lt;a href="https://chithi-teal.vercel.app" rel="noopener noreferrer"&gt;chithi-teal.vercel.app&lt;/a&gt;&lt;/strong&gt;. Pick a language, then tap one of the sample photos (a scam SMS, a bill, a medicine strip), or upload your own.&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%2Fr4srdlvn4fj081qqqlik.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%2Fr4srdlvn4fj081qqqlik.gif" alt="Chithi demo: choose a language, read a scam SMS, a bill, then the same scam in Hindi and Tamil"&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;Pick a language&lt;/th&gt;
&lt;th&gt;Bengali home&lt;/th&gt;
&lt;th&gt;A scam&lt;/th&gt;
&lt;th&gt;A bill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fcz3xtaqoz3ai36ygnae8.png" alt="Language picker"&gt;&lt;/td&gt;
&lt;td&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%2F7egohlv7z9ti7wkzlitr.png" alt="Home screen in Bengali"&gt;&lt;/td&gt;
&lt;td&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%2F6r3u8rkscoasw17pi6ii.png" alt="Scam result in Bengali"&gt;&lt;/td&gt;
&lt;td&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%2Fg4bs1et7rcfchmha7vaj.png" alt="Bill result in Bengali"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The public demo runs Gemma in the cloud so you can try it without installing anything, and the page says so. At home it runs on our own laptop. More on why that difference matters below.&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/codeswithroh" rel="noopener noreferrer"&gt;
        codeswithroh
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/chithi" rel="noopener noreferrer"&gt;
        chithi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Point your phone at any bill, letter or WhatsApp forward. Hear it in your language. Know if it's a scam. Open-weight Gemma, 22 Indian languages.
    &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;চিঠি (Chithi)&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/chithi/public/press/cover.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%2Fcodeswithroh%2Fchithi%2FHEAD%2Fpublic%2Fpress%2Fcover.png" alt="Chithi"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo: &lt;a href="https://chithi-teal.vercel.app" rel="nofollow noopener noreferrer"&gt;https://chithi-teal.vercel.app&lt;/a&gt;&lt;/strong&gt;. Pick a language, then tap a sample photo
Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01" rel="nofollow"&gt;DEV Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;. MIT licensed.&lt;/p&gt;
&lt;p&gt;A pocket reader for my mom and grandparents, who read Bengali but not English.&lt;/p&gt;
&lt;p&gt;Point the phone at any bill, bank letter, medicine strip, or WhatsApp forward. Chithi reads it with
&lt;strong&gt;Gemma&lt;/strong&gt; (an open-weight model running on the family laptop), then says out loud, in your language:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;what it is,&lt;/li&gt;
&lt;li&gt;what you need to do (amount, due date),&lt;/li&gt;
&lt;li&gt;whether it's a scam, with one big button to call me.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Photos never leave the house. There's no cloud API, no account, and no monthly bill.&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;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;phone (PWA, camera)  ──Wi-Fi──▶  laptop: Bun server  ──▶  Ollama + Gemma (vision)
        ▲                                │
        └── speech in your language      └── scam rule layer (can only raise risk, never lower it)
            (phone's offline voice)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server.ts&lt;/code&gt; serves…&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/codeswithroh/chithi" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;It's small on purpose: one Bun server, a phone web app with no framework, and under 200 lines of shared logic for the prompt, cleanup and scam rules.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ma's phone (web app, camera)  ── home Wi-Fi ──▶  family laptop: Bun server ──▶ Ollama + Gemma 3 4B (vision)
       ▲                                               │
       └── spoken answer (phone's own offline voice)    └── scam rules (can raise the risk, never lower it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The model.&lt;/strong&gt; Gemma 3 4B, an open-weight vision model, running in &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; on my 8 GB M3 MacBook. The phone sends a photo over home Wi-Fi. Gemma reads it and answers in a fixed JSON shape: original text, document type, a two-line explanation, what to do, amount, deadline, and a risk level of &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;caution&lt;/code&gt; or &lt;code&gt;scam&lt;/code&gt;. Ollama's JSON-schema mode means the reply always parses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The voice.&lt;/strong&gt; The phone's own text-to-speech in &lt;code&gt;bn-IN&lt;/code&gt; (or &lt;code&gt;hi-IN&lt;/code&gt;, &lt;code&gt;ta-IN&lt;/code&gt;, …). It works offline. Languages without a phone voice fall back to one that reads the same script, so Maithili goes through the Hindi voice and Konkani through Marathi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The phone app.&lt;/strong&gt; A tiny web app you add to the home screen. Giant buttons, big type, and nothing to learn. It has one button that opens the camera and one that picks a screenshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  What testing taught me
&lt;/h3&gt;

&lt;p&gt;Most of the work was not "call the model." It was finding the ways a 4B model fails, then making sure those failures can't reach my mom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The model caught the scam, then gave the scammer's number.&lt;/strong&gt; My first test was a fake SBI "KYC update" SMS. Gemma correctly said &lt;em&gt;scam&lt;/em&gt;, then advised: &lt;em&gt;"call the helpline number in the message to find out more."&lt;/em&gt; That helpline is the scammer. Since then, when the verdict is scam, the advice is fixed text written by a person in each language. The model never writes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A small model shouldn't be the only guard.&lt;/strong&gt; A deterministic rule layer in &lt;code&gt;scam.ts&lt;/code&gt; checks the transcribed text for OTP and PIN requests, KYC, "account blocked", lottery and prize wording, short links, &lt;code&gt;.apk&lt;/code&gt; files, and remote-access apps like AnyDesk. It can &lt;strong&gt;raise&lt;/strong&gt; the risk level and never lower it. A printed MRP on a medicine strip is recorded, not escalated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Numbers are too important to paraphrase.&lt;/strong&gt; Gemma once read ₹1,182 aloud as "one thousand one hundred eighty-eight". Now the explanation is forbidden from containing numbers at all. The amount and due date come from separate fields copied exactly as printed, and the app shows them in their own boxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Small models drift between scripts.&lt;/strong&gt; Bengali answers picked up Korean characters. Malayalam answers picked up Romanian and Russian words. Now any letter that isn't in the reader's own script (or plain English, for brand names like SBI) is stripped, and a badly drifted answer gets one retry. Without the stripping, the phone's Bengali voice would stumble over Hangul mid-sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. An 8 GB laptop has rules.&lt;/strong&gt; The first photo took almost four minutes while the model loaded. Then one of my fixes made things worse: the warm-up request and the real one used different context sizes, so Ollama loaded the model twice and timed out. Now the server warms the model at startup with the same settings and keeps it loaded. A photo takes 25 to 60 seconds at home, which is slow for an app and fine for a letter.&lt;/p&gt;

&lt;h3&gt;
  
  
  All 22 languages
&lt;/h3&gt;

&lt;p&gt;On first launch, Chithi shows every language in its own script, with the phone's language suggested first. After that, everything follows the choice: the buttons, Gemma's explanation, the scam warning, the voice, the font, and right-to-left layout for Urdu, Kashmiri and Sindhi.&lt;/p&gt;

&lt;p&gt;I was honest with myself about quality. On the 4B laptop model, Hindi, Bengali, Marathi, Telugu, Tamil, Gujarati, Urdu and Kannada are good. Odia and Malayalam were weak, and Gemma 3 4B can't write Santali's Ol Chiki script at all. Those languages carry a "beta" label and an on-screen warning to double-check with family.&lt;/p&gt;

&lt;h3&gt;
  
  
  The public demo
&lt;/h3&gt;

&lt;p&gt;Judges can't come to my house, so &lt;a href="https://chithi-teal.vercel.app" rel="noopener noreferrer"&gt;the demo&lt;/a&gt; runs the same code on Vercel. The only difference is where Gemma lives: &lt;strong&gt;Gemma 4 26B&lt;/strong&gt; (a mixture-of-experts model with 4B active parameters) on Google's free Gemini API tier. Two things I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 thinks before it answers, and it ran out of room.&lt;/strong&gt; The first deploy failed because the model spent 1,497 of its 1,500 output tokens thinking and never wrote the JSON. Reading a bill aloud doesn't need deliberation, so I set &lt;code&gt;thinkingLevel&lt;/code&gt; to &lt;code&gt;minimal&lt;/code&gt;. Replies went from 40 to 55 seconds down to about &lt;strong&gt;5 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bigger model closes the language gap.&lt;/strong&gt; In 63 live runs across 16 languages, every scam was caught and every amount and due date was exact. Malayalam and Odia came out clean, and Santali came back in proper Ol Chiki. Same open model family, bigger size, no beta label needed.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Because the photos are my mom's bank letters and prescriptions.&lt;/strong&gt; At home, the photo goes from her phone to a laptop in the next room and stops there. There's no account, no API key, and no company's servers. A closed API would mean sending a parent's KYC details to a third party to &lt;em&gt;protect&lt;/em&gt; them from people who want their KYC details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because a gift shouldn't need a subscription.&lt;/strong&gt; This app costs nothing to run. If a pricing page changes, or a company decides Bengali isn't a priority market, my mom's reader keeps working. Open weights mean the model on that laptop is ours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I could choose the model to fit the house.&lt;/strong&gt; An 8 GB laptop runs Gemma 3 4B. A family with a 16 GB machine can run Gemma 4 instead by changing one environment variable. When the 4B model was weak in Malayalam, the answer wasn't "wait for a vendor." It was "try a bigger open model." I did, and it fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the safety layer has to be mine.&lt;/strong&gt; The scam rules, the fixed advice, and the script cleanup are code I can read and change. With an open model, I can see exactly where its answer ends and my rules begin.&lt;/p&gt;

&lt;p&gt;Where did running it at home lose? Speed. The hosted Gemma answers in 5 seconds, the laptop in 30. For a letter you read once, I'll take 30 seconds and privacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing It Over
&lt;/h2&gt;

&lt;p&gt;I gave it to Ma this weekend: the laptop stays on at home, the app sits on her phone's home screen, and my number is already behind the red call button.&lt;/p&gt;

&lt;p&gt;She used it, and she told me it's one of the best things I've built for her, and that it will help her a lot.&lt;/p&gt;

&lt;p&gt;She was proud of what I'm doing. Honestly, that landed harder than any test passing.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built Chithi in one weekend with an AI coding agent (Claude Code) as a pair. I made the product calls (who it's for, the scam-first flow, the "never let the model write scam advice" rule), and the agent wrote and tested most of the code with me. The testing loop above, with its real failures, came out of that session.&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 3 4B runs locally on the family laptop through Ollama, and Gemma 4 26B powers the public demo through the Gemini API. The app picks the best available Gemma automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for my mom and my grandparents. If your parents get "Dear Customer" messages too, the code is MIT-licensed. Point it at your family's laptop.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>What If the Blockchain Could Judge Your Bluff Without Seeing Your Dice?</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:07:15 +0000</pubDate>
      <link>https://dev.to/codeswithroh/what-if-the-blockchain-could-judge-your-bluff-without-seeing-your-dice-28lf</link>
      <guid>https://dev.to/codeswithroh/what-if-the-blockchain-could-judge-your-bluff-without-seeing-your-dice-28lf</guid>
      <description>&lt;p&gt;Liar’s Dice sounds like a perfect game to put onchain.&lt;/p&gt;

&lt;p&gt;The rules are simple, every move can be verified, and you don’t need a centralized game server deciding who won.&lt;/p&gt;

&lt;p&gt;There is just one problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockchains are public. Liar’s Dice only works if your dice are private.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I simply stored every roll inside a normal smart contract, anyone could inspect the state and know exactly what everyone was holding.&lt;/p&gt;

&lt;p&gt;At that point, there is no bluffing.&lt;/p&gt;

&lt;p&gt;You would basically be playing poker with everyone's cards face up.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;FHE Liar’s Dice&lt;/strong&gt;, a decentralized version of the game where your dice remain encrypted while the game is being played.&lt;/p&gt;

&lt;p&gt;Not hidden behind a backend.&lt;/p&gt;

&lt;p&gt;Not stored privately in some database.&lt;/p&gt;

&lt;p&gt;Encrypted onchain.&lt;/p&gt;

&lt;p&gt;And the interesting part is that the smart contract can still use those encrypted dice to determine whether you are lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with putting hidden-information games onchain
&lt;/h2&gt;

&lt;p&gt;Most blockchain games actually benefit from transparency.&lt;/p&gt;

&lt;p&gt;If you're building something like chess, every player is supposed to know the complete state of the board.&lt;/p&gt;

&lt;p&gt;Liar’s Dice is different.&lt;/p&gt;

&lt;p&gt;Each player starts with five dice that only they should be able to see.&lt;/p&gt;

&lt;p&gt;Players then make public claims about the combined dice across the entire table.&lt;/p&gt;

&lt;p&gt;You might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are six 4s on the table.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next player has two choices.&lt;/p&gt;

&lt;p&gt;Raise the bid.&lt;/p&gt;

&lt;p&gt;Or call your bluff.&lt;/p&gt;

&lt;p&gt;The entire game comes from the fact that nobody knows exactly what everyone else is holding.&lt;/p&gt;

&lt;p&gt;But a traditional smart contract has the opposite property.&lt;/p&gt;

&lt;p&gt;Its state is transparent.&lt;/p&gt;

&lt;p&gt;Even if the frontend refuses to display your dice, someone can simply inspect the contract, query the state, watch events, or build their own interface.&lt;/p&gt;

&lt;p&gt;Hiding something in the UI isn't privacy.&lt;/p&gt;

&lt;p&gt;I needed the actual game state itself to remain secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  FHE turned out to be a very good fit for the game
&lt;/h2&gt;

&lt;p&gt;I built the game using &lt;strong&gt;Fhenix CoFHE&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fully Homomorphic Encryption is interesting because it allows computation to happen directly over encrypted values.&lt;/p&gt;

&lt;p&gt;Normally encryption gives you privacy while data is stored, but you need to decrypt the data before doing anything useful with it.&lt;/p&gt;

&lt;p&gt;With FHE, you can keep the value encrypted and still perform computations over it.&lt;/p&gt;

&lt;p&gt;That completely changes what an onchain game can do.&lt;/p&gt;

&lt;p&gt;In FHE Liar’s Dice, the contract generates each player's dice as encrypted values.&lt;/p&gt;

&lt;p&gt;Conceptually, instead of the contract storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2, 6, 4, 1, 3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it stores encrypted representations of those values.&lt;/p&gt;

&lt;p&gt;No plaintext die value is written into contract storage or emitted through an event while the round is active.&lt;/p&gt;

&lt;p&gt;Only the player who owns that hand can unseal it and see what they rolled.&lt;/p&gt;

&lt;p&gt;So your opponents can't read your dice.&lt;/p&gt;

&lt;p&gt;An indexer can't read your dice.&lt;/p&gt;

&lt;p&gt;And even the application operator doesn't get the plaintext values.&lt;/p&gt;

&lt;p&gt;That gives the game something I couldn't get from a normal smart contract:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;private state with public execution.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird part: checking a bluff without revealing the dice
&lt;/h2&gt;

&lt;p&gt;This was the part I found most interesting while building the game.&lt;/p&gt;

&lt;p&gt;Imagine I bid:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Six 4s.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Someone calls me a liar.&lt;/p&gt;

&lt;p&gt;Now the contract needs to inspect every active player's dice and determine how many 4s actually exist.&lt;/p&gt;

&lt;p&gt;In the version of Liar’s Dice I implemented, ones are wild, so they also count toward most bids.&lt;/p&gt;

&lt;p&gt;The obvious implementation would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;decrypt all dice
count the matches
compare count &amp;gt;= bid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that defeats the entire point.&lt;/p&gt;

&lt;p&gt;Instead, the contract performs the comparison over the encrypted values themselves.&lt;/p&gt;

&lt;p&gt;It checks every encrypted die, calculates whether it matches the bid, includes wild ones where appropriate, and builds the result without revealing the underlying hands.&lt;/p&gt;

&lt;p&gt;The final thing that needs to become public is essentially one bit of information:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was the bid valid?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes or no.&lt;/p&gt;

&lt;p&gt;If the true encrypted count meets the bid, the challenger loses.&lt;/p&gt;

&lt;p&gt;If it doesn't, the bidder was lying and gets eliminated.&lt;/p&gt;

&lt;p&gt;The actual count doesn't need to be revealed to reach that verdict.&lt;/p&gt;

&lt;p&gt;That distinction is small, but it is the entire architecture of the game.&lt;/p&gt;

&lt;p&gt;The chain gets enough information to enforce the rules without learning information it doesn't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays private and what stays public
&lt;/h2&gt;

&lt;p&gt;I didn't want to make everything private.&lt;/p&gt;

&lt;p&gt;That would remove one of the reasons to build the game onchain in the first place.&lt;/p&gt;

&lt;p&gt;So I separated the game state into two categories.&lt;/p&gt;

&lt;p&gt;Things like these are public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who joined the table&lt;/li&gt;
&lt;li&gt;turn order&lt;/li&gt;
&lt;li&gt;every bid&lt;/li&gt;
&lt;li&gt;who challenged&lt;/li&gt;
&lt;li&gt;whether the bid was valid&lt;/li&gt;
&lt;li&gt;who was eliminated&lt;/li&gt;
&lt;li&gt;the final result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The active dice are private.&lt;/p&gt;

&lt;p&gt;That creates a nice split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The strategy stays private. The execution stays verifiable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a challenge resolves, the hands from that round can be revealed so everyone at the table can see what actually happened.&lt;/p&gt;

&lt;p&gt;Then the surviving players receive fresh encrypted hands for the next round.&lt;/p&gt;

&lt;p&gt;The last player standing wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't just use a server
&lt;/h2&gt;

&lt;p&gt;The easy version of this project would have been straightforward.&lt;/p&gt;

&lt;p&gt;Generate the dice on a backend.&lt;/p&gt;

&lt;p&gt;Send each player their own hand.&lt;/p&gt;

&lt;p&gt;Have the server determine whether every challenge succeeds.&lt;/p&gt;

&lt;p&gt;From a game-development perspective, that works perfectly fine.&lt;/p&gt;

&lt;p&gt;But then the server becomes the dealer, referee, and source of truth.&lt;/p&gt;

&lt;p&gt;It knows everybody's dice.&lt;/p&gt;

&lt;p&gt;It could theoretically alter rolls.&lt;/p&gt;

&lt;p&gt;It could leak information.&lt;/p&gt;

&lt;p&gt;And players ultimately have to trust whoever operates it.&lt;/p&gt;

&lt;p&gt;That isn't particularly interesting to me.&lt;/p&gt;

&lt;p&gt;The point of the experiment was to see whether we could remove that trusted game master without destroying the hidden-information mechanics that make Liar’s Dice fun.&lt;/p&gt;

&lt;p&gt;FHE makes that possible.&lt;/p&gt;

&lt;p&gt;The smart contract becomes the referee without becoming the omniscient dealer.&lt;/p&gt;

&lt;h2&gt;
  
  
  I also built a completely offline practice mode
&lt;/h2&gt;

&lt;p&gt;One problem with building blockchain games is that people often need to connect a wallet before they even understand what the product does.&lt;/p&gt;

&lt;p&gt;I didn't want that.&lt;/p&gt;

&lt;p&gt;So FHE Liar’s Dice also has a practice mode.&lt;/p&gt;

&lt;p&gt;It uses the same basic bluffing loop but runs locally against bots.&lt;/p&gt;

&lt;p&gt;No wallet.&lt;/p&gt;

&lt;p&gt;No blockchain transaction.&lt;/p&gt;

&lt;p&gt;No encryption.&lt;/p&gt;

&lt;p&gt;You can understand the game first, then enter the actual onchain lobby when you want to play the privacy-preserving version against other people.&lt;/p&gt;

&lt;p&gt;The distinction is intentional.&lt;/p&gt;

&lt;p&gt;There is no reason to pretend the offline version is decentralized.&lt;/p&gt;

&lt;p&gt;It exists purely as an onboarding layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The game is still intentionally simple
&lt;/h2&gt;

&lt;p&gt;The current version is a prototype.&lt;/p&gt;

&lt;p&gt;There are no wagers.&lt;/p&gt;

&lt;p&gt;No ranked mode.&lt;/p&gt;

&lt;p&gt;No tournament system.&lt;/p&gt;

&lt;p&gt;No timers.&lt;/p&gt;

&lt;p&gt;I wanted to solve the harder primitive first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you run an actual hidden-information bluffing game through a smart contract without exposing the hidden information?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once that loop works, everything else becomes product design.&lt;/p&gt;

&lt;p&gt;One known tradeoff in the current implementation is dice generation.&lt;/p&gt;

&lt;p&gt;The dice are derived using an encrypted random &lt;code&gt;uint8&lt;/code&gt;, reduced modulo six and shifted into the 1–6 range.&lt;/p&gt;

&lt;p&gt;Because 256 isn't perfectly divisible by six, there is a very small distribution bias: faces 1 through 4 appear marginally more often than 5 and 6.&lt;/p&gt;

&lt;p&gt;For a casual prototype I accepted that tradeoff and documented it rather than hiding it.&lt;/p&gt;

&lt;p&gt;The contracts are also unaudited, so this is absolutely not something I would attach real-money wagering to in its current form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building this changed for me
&lt;/h2&gt;

&lt;p&gt;Before this project, it was easy for me to think about blockchain privacy mostly in terms of private transfers or private financial positions.&lt;/p&gt;

&lt;p&gt;Building a game made the idea much more tangible.&lt;/p&gt;

&lt;p&gt;There is a huge class of applications where you don't necessarily need to hide the entire system.&lt;/p&gt;

&lt;p&gt;You need to hide &lt;strong&gt;specific pieces of state while still allowing programs to compute over them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Games are one example.&lt;/p&gt;

&lt;p&gt;Auctions are another.&lt;/p&gt;

&lt;p&gt;Voting, identity, trading strategies and many financial applications have variations of the same problem.&lt;/p&gt;

&lt;p&gt;Public blockchains are extremely good at proving that computation happened according to known rules.&lt;/p&gt;

&lt;p&gt;FHE adds another interesting possibility:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;proving that rules were followed without forcing every input involved in those rules to become public.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Liar’s Dice happens to make that idea very easy to see.&lt;/p&gt;

&lt;p&gt;You have five dice.&lt;/p&gt;

&lt;p&gt;Nobody else should know them.&lt;/p&gt;

&lt;p&gt;The contract still needs to know whether your bluff worked.&lt;/p&gt;

&lt;p&gt;And somehow both things need to be true at the same time.&lt;/p&gt;

&lt;p&gt;That is what I wanted to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try bluffing against the blockchain
&lt;/h2&gt;

&lt;p&gt;The project is live here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fhe-liars-dice.vercel.app/" rel="noopener noreferrer"&gt;https://fhe-liars-dice.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can jump into practice mode without a wallet or create an onchain table and play the FHE version with other players.&lt;/p&gt;

&lt;p&gt;The simplest description of the project is still probably the one I started with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody sees your dice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everyone sees your lies.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>fhe</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I built TraceGate because my AI agent demo passed, but the traces told a different story</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:46:53 +0000</pubDate>
      <link>https://dev.to/codeswithroh/i-built-tracegate-because-my-ai-agent-demo-passed-but-the-traces-told-a-different-story-36c2</link>
      <guid>https://dev.to/codeswithroh/i-built-tracegate-because-my-ai-agent-demo-passed-but-the-traces-told-a-different-story-36c2</guid>
      <description>&lt;p&gt;Most AI agent demos stop at the final answer.&lt;/p&gt;

&lt;p&gt;Mine did too at first. I had a support agent that could answer a refund question, call a policy tool, and avoid a prompt-injection path. From the outside, it looked fine.&lt;/p&gt;

&lt;p&gt;Then I asked a less comfortable question: if this agent broke in production, would I actually know what happened?&lt;/p&gt;

&lt;p&gt;That question became TraceGate.&lt;/p&gt;

&lt;p&gt;TraceGate is a release gate for AI agents built with OpenTelemetry and SigNoz. It runs agent scenarios, sends telemetry to SigNoz, then checks whether the run produced enough evidence to safely ship.&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%2Fy5fpsn7gwie9fl0d0qqt.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%2Fy5fpsn7gwie9fl0d0qqt.png" alt="tracegate flow" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;AI agents can pass a demo while still being hard to debug.&lt;/p&gt;

&lt;p&gt;A tool can retry three times and still return a successful answer. An LLM call can happen without cost metadata. A prompt-injection test can pass, but leave no trace of which safety path was used. Those are not product failures in the usual sense. They are observability failures.&lt;/p&gt;

&lt;p&gt;That is what I wanted TraceGate to catch.&lt;/p&gt;

&lt;p&gt;The idea is simple: before an agent ships, it should satisfy an observability contract. If the contract fails, the release is blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TraceGate checks
&lt;/h2&gt;

&lt;p&gt;TraceGate uses a YAML contract to describe what a safe release run should include.&lt;/p&gt;

&lt;p&gt;Here is part of the contract I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TraceGate AI Agent Release Contract&lt;/span&gt;
&lt;span class="na"&gt;serviceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tracegate-demo-agent&lt;/span&gt;

&lt;span class="na"&gt;budgets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;maxRunCostUsd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.005&lt;/span&gt;
  &lt;span class="na"&gt;maxP95LatencyMs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2000&lt;/span&gt;
  &lt;span class="na"&gt;maxToolRetries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;span-agent-run&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required-span&lt;/span&gt;
    &lt;span class="na"&gt;spanName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent.run&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;span-llm-call&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required-span&lt;/span&gt;
    &lt;span class="na"&gt;spanName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm.call&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attr-llm-model&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required-attribute&lt;/span&gt;
    &lt;span class="na"&gt;spanName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm.call&lt;/span&gt;
    &lt;span class="na"&gt;attribute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;retry-budget-trace-lookup&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max-tool-retries&lt;/span&gt;
    &lt;span class="na"&gt;toolName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trace.lookup&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default demo intentionally fails one check. The &lt;code&gt;trace.lookup&lt;/code&gt; tool retries three times, but the contract only allows one retry.&lt;/p&gt;

&lt;p&gt;That makes the demo useful because TraceGate is not pretending everything is healthy. It blocks the release and explains why.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TraceGate FAIL
Checks: 7/8 passed
Critical failures: 1
FAIL retry-budget-trace-lookup - Worst retry count for 'trace.lookup' was 3; limit 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F1juipzv0tq3ylkvijjvj.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%2F1juipzv0tq3ylkvijjvj.png" alt="tracegate overview" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The stack is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vite
React
TypeScript
Node.js
OpenTelemetry
SigNoz
OpenAI
YAML contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario -&amp;gt; Agent runner -&amp;gt; OpenTelemetry -&amp;gt; SigNoz -&amp;gt; TraceGate contract evaluator -&amp;gt; Pass or block
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Node runner executes the scenario. OpenTelemetry records spans, metrics, and logs. SigNoz receives the telemetry through OTLP. TraceGate reads the run result and evaluates it against the contract.&lt;/p&gt;

&lt;p&gt;For the OpenTelemetry setup, I used the OTLP HTTP exporters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:4318&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodeSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;traceExporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPTraceExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/traces`&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;logRecordProcessor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BatchLogRecordProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPLogExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/logs`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;metricReader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PeriodicExportingMetricReader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPMetricExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/metrics`&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;exportIntervalMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my local setup, SigNoz received spans for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent.run
llm.call
tool.ticket.lookup
tool.policy.search
tool.trace.lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fies69f0xjz598gr7o4fr.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%2Fies69f0xjz598gr7o4fr.png" alt="signoz overview" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A build issue I hit
&lt;/h2&gt;

&lt;p&gt;The part that took the most debugging was the local SigNoz setup.&lt;/p&gt;

&lt;p&gt;I used Foundry to run SigNoz locally. The first time I started everything, the UI was up, but the telemetry path was not useful. The collector looked alive, but the pipeline was effectively not sending the data I expected.&lt;/p&gt;

&lt;p&gt;The issue was in the generated local configuration around the OpAMP and MCP service wiring. I patched the generated setup so the ingester pointed to the actual SigNoz service and the MCP server pointed to the SigNoz UI service. After that, the OTLP export worked and I could verify spans in ClickHouse.&lt;/p&gt;

&lt;p&gt;That was the most “real” part of the project for me. The code for the gate was straightforward compared to proving that telemetry actually reached the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workbench
&lt;/h2&gt;

&lt;p&gt;TraceGate has a landing page and a workbench.&lt;/p&gt;

&lt;p&gt;The landing page explains the idea: ship agents only when the traces agree.&lt;/p&gt;

&lt;p&gt;The workbench shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;latest release verdict
checks passed
critical failures
cost
latency
gate matrix
failed evidence
SigNoz investigation query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate matrix is the part I care about most. It turns vague release confidence into specific checks.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Root agent run span: pass
LLM calls traced: pass
Model attribute present: pass
Cost attribute present: pass
Cost budget: pass
Trace lookup retry budget: fail
Refund support scenario: pass
Prompt injection scenario: pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives a reviewer a much better starting point than “the demo worked on my machine.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Judge test mode
&lt;/h2&gt;

&lt;p&gt;I also added a judge-operated test mode because I did not want the hosted app to feel like a static mockup.&lt;/p&gt;

&lt;p&gt;A judge can enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service name
scenario
tool name
retry budget
observed retries
cost
latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then TraceGate generates a new verdict.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service: checkout-agent
tool: inventory.lookup
allowed retries: 1
observed retries: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That blocks the release.&lt;/p&gt;

&lt;p&gt;If the observed retries are changed to &lt;code&gt;0&lt;/code&gt;, the release becomes ready.&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%2F8y6wq7lrub3jlviha76d.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%2F8y6wq7lrub3jlviha76d.png" alt="signoz workflow" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This makes the product easier to test without asking someone to set up my whole local environment first.&lt;/p&gt;

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

&lt;p&gt;The main thing I learned is that observability is more useful when it is tied to a decision.&lt;/p&gt;

&lt;p&gt;Before this project, I mostly treated observability as something I would open after a bug. With AI agents, that feels too late. If an agent takes a risky path, skips metadata, hides cost, or retries a flaky tool several times, I want to know during release validation.&lt;/p&gt;

&lt;p&gt;The second thing I learned is that “the agent worked” is too broad. I now split it into two questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did the agent produce the expected outcome?
Did the agent produce enough evidence to debug that outcome?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TraceGate focuses on the second question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;The next version should have a visual contract editor. Writing YAML is fine for a hackathon, but a team should be able to create a release gate by choosing required spans, attributes, budgets, and scenarios in the UI.&lt;/p&gt;

&lt;p&gt;I would also add GitHub Actions support. The natural place for TraceGate is in CI, where it can block an agent release before merge.&lt;/p&gt;

&lt;p&gt;The final improvement would be deeper SigNoz artifacts. Each contract should generate a matching dashboard, alert, and investigation prompt so the release gate and the debugging workflow stay connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;TraceGate started from one uncomfortable question: if my AI agent fails later, will I have the evidence to understand it?&lt;/p&gt;

&lt;p&gt;SigNoz already gives teams a strong place to inspect telemetry. TraceGate adds a release workflow on top of that telemetry.&lt;/p&gt;

&lt;p&gt;The project is small, but the idea feels useful: agents should not ship only because the final answer looked good. They should ship when their behavior is traceable enough to trust.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Every AI-built site looks the same, so I built a skill that locks taste before any code is written</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Sat, 18 Jul 2026 00:40:32 +0000</pubDate>
      <link>https://dev.to/codeswithroh/every-ai-built-site-looks-the-same-so-i-built-a-skill-that-locks-taste-before-any-code-is-written-4f6d</link>
      <guid>https://dev.to/codeswithroh/every-ai-built-site-looks-the-same-so-i-built-a-skill-that-locks-taste-before-any-code-is-written-4f6d</guid>
      <description>&lt;p&gt;I build a lot of side projects with AI coding tools. Mostly Claude Code. A few months ago I noticed something that kept bugging me. Every site I shipped looked the same. Same indigo gradient. Same default font. Same rounded cards with the same soft shadow.&lt;/p&gt;

&lt;p&gt;Then I started noticing it on other people's projects too. Demo days, launch posts, screenshots on social media. The indigo gradient is everywhere. It is basically a uniform at this point.&lt;/p&gt;

&lt;p&gt;This is not really the AI's fault. When you do not give a model a design direction, it picks the average of the internet. And the average of the internet is a Tailwind starter template with an indigo gradient and Inter. The model is doing exactly what it was trained to do. The problem is that nobody told it what you actually want.&lt;/p&gt;

&lt;p&gt;So I built tastemaker. It is a skill for Claude Code, and it also works with Cursor, Windsurf, and Codex. The idea is simple: lock the design system before the AI writes a single line of UI.&lt;/p&gt;

&lt;p&gt;Full disclosure before we go further: I built this. I am a solo builder. It is free and open source under the MIT license. I am posting it here because I want honest feedback, not because I have anything to sell you. There is nothing to buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taste first, code second
&lt;/h2&gt;

&lt;p&gt;A skill is just a folder of instructions the AI reads before it starts working. tastemaker forces a design decision step at the beginning, with real constraints, instead of letting the model improvise the look as it goes.&lt;/p&gt;

&lt;p&gt;When you start a UI project with it installed, this is what gets locked before any code exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A palette. 5 presets, each matched to a mood. Not random hex codes. Combinations that were picked to work together.&lt;/li&gt;
&lt;li&gt;Fonts. 24 curated Google Font pairings. A display font and a body font that actually belong on the same page.&lt;/li&gt;
&lt;li&gt;Real assets. Illustrator-grade illustrations, recolored to your palette. No gray placeholder boxes. No generic stock photo energy.&lt;/li&gt;
&lt;li&gt;A logo. A constructed geometric mark, plus a full favicon set, so the tab icon is not the framework default.&lt;/li&gt;
&lt;li&gt;Motion. GSAP and ScrollTrigger wired in by default, so scroll animation is a starting point, not a TODO you never get to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not that my taste is better than yours. The point is that any taste, written down and locked, beats no taste at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I failed
&lt;/h2&gt;

&lt;p&gt;This is my favorite part of the story, mostly because it is embarrassing.&lt;/p&gt;

&lt;p&gt;I picked the 5 palette presets by eye. They looked good to me. I was confident. Then, because I wanted to be thorough, I wrote a small Python script to check every text-on-background combination against WCAG contrast ratios.&lt;/p&gt;

&lt;p&gt;The first run failed 2 of my 5 presets.&lt;/p&gt;

&lt;p&gt;My eyes said the palettes were fine. The math said real people would struggle to read the text. The math was right. I went back, adjusted the failing colors, and kept running the check until everything passed. All 5 presets that ship today pass that contrast check.&lt;/p&gt;

&lt;p&gt;That little story is the whole argument for the project. Taste feels subjective until you write it down and test it. Once it is written down, you can check it, fix it, and reuse it. My eyeballs were wrong 40 percent of the time. A short Python script was right every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;One command, then restart Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/codeswithroh/tastemaker ~/.claude/skills/tastemaker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the whole setup. No API keys. No config file. No account. Restart, start a UI project, and the design system gets locked before the AI starts guessing.&lt;/p&gt;

&lt;p&gt;More details on the site: &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://tastemaker-skill.online/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;tastemaker-skill.online&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
Source code: &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
&lt;br&gt;
  &lt;div class="readme-overview"&gt;
&lt;br&gt;
    &lt;h2&gt;
&lt;br&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;&lt;br&gt;
      &lt;a href="https://github.com/codeswithroh" rel="noopener noreferrer"&gt;&lt;br&gt;
        codeswithroh&lt;br&gt;
      &lt;/a&gt; / &lt;a href="https://github.com/codeswithroh/tastemaker" rel="noopener noreferrer"&gt;&lt;br&gt;
        tastemaker&lt;br&gt;
      &lt;/a&gt;&lt;br&gt;
    &lt;/h2&gt;
&lt;br&gt;
    &lt;h3&gt;
&lt;br&gt;
      A Claude Code skill that grounds AI-generated UI in real reference images and a persistent per-developer taste profile, instead of generic AI-slop defaults.&lt;br&gt;
    &lt;/h3&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="ltag-github-body"&gt;
&lt;br&gt;
    

&lt;div id="readme" class="md"&gt;&lt;div&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/codeswithroh/tastemaker/.github/assets/banner.svg"&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%2Fcodeswithroh%2Ftastemaker%2FHEAD%2F.github%2Fassets%2Fbanner.svg" alt="tastemaker" width="100%"&gt;&lt;/a&gt;
  &lt;p&gt;
    &lt;a href="https://github.com/codeswithroh/tastemaker/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/55d226870f5f6073e7e89152f835e2f9408d7a17571e2da5572c89b153e803d6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d3334443339393f7374796c653d666c61742d737175617265" alt="MIT License"&gt;&lt;/a&gt;
    &lt;a href="https://github.com/codeswithroh/tastemaker/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/429513393d9a082cb7422cf517c1238b0c5dab86cea135bde9041fc784c60427/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f636f64657377697468726f682f74617374656d616b65723f7374796c653d666c61742d73717561726526636f6c6f723d333444333939266c6162656c3d7374617273" alt="Stars"&gt;&lt;/a&gt;
    &lt;a href="https://github.com/codeswithroh/tastemaker/CONTRIBUTING.md" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c303fb350e3b9ca60df4cb74e20e8380edb106e2cd82361cc09cfce892b6fe0c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d77656c636f6d652d3034373835373f7374796c653d666c61742d737175617265" alt="PRs welcome"&gt;&lt;/a&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/6a6efd47bf46ba77456682d3d6cb4f59ed9f8a0af08f65a1123671020c2ebf3e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436c61756465253230436f64652d736b696c6c2d3334443339393f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/6a6efd47bf46ba77456682d3d6cb4f59ed9f8a0af08f65a1123671020c2ebf3e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436c61756465253230436f64652d736b696c6c2d3334443339393f7374796c653d666c61742d737175617265" alt="Claude Code skill"&gt;&lt;/a&gt;
    &lt;a href="https://tastemaker-ai-skill.netlify.app" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e7f43dd9aba9f675224bd614673aee8da984b693d678b1e73c5031dd06e067dc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6976652d64656d6f2d3034373835373f7374796c653d666c61742d737175617265" alt="Live demo"&gt;&lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;&lt;b&gt;A skill that gives AI real design taste, so the UI it builds does not look AI-generated.&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;
    &lt;a href="https://github.com/codeswithroh/tastemaker#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
    &lt;a href="https://github.com/codeswithroh/tastemaker#why-ai-ui-all-looks-the-same" rel="noopener noreferrer"&gt;Why&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
    &lt;a href="https://github.com/codeswithroh/tastemaker#what-you-get" rel="noopener noreferrer"&gt;Features&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
    &lt;a href="https://github.com/codeswithroh/tastemaker#the-five-presets" rel="noopener noreferrer"&gt;Presets&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
    &lt;a href="https://github.com/codeswithroh/tastemaker#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; &amp;nbsp;·&amp;nbsp;
    &lt;a href="https://github.com/codeswithroh/tastemaker#contributing" rel="noopener noreferrer"&gt;Contributing&lt;/a&gt;
  &lt;/p&gt;
  &lt;p&gt;&lt;a href="https://tastemaker-ai-skill.netlify.app" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;See it live and try the demo →&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;



&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What this is&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Tastemaker is a skill for coding agents (Claude Code, Cursor, Windsurf). You install it once and forget it. Whenever you ask your agent to build or style a UI, tastemaker steps in and gives it a real design system to work from, instead of the generic defaults every model reaches for.&lt;/p&gt;

&lt;p&gt;It is plain Markdown and small Python scripts. Everything runs on your machine. There is no hosted backend, no account, and no API key.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why AI UI all looks the same&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Ask any model to build a UI and you tend to get the same thing: an indigo to purple gradient, a soft shadow card, a generic…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/codeswithroh/tastemaker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;A few things, in order of how much they surprised me.&lt;/p&gt;

&lt;p&gt;First, constraints make AI output better, not worse. I expected locking the palette and fonts to feel limiting. It did the opposite. The model stopped spending effort on design indecision and spent it on the actual product. The output got more interesting, not less.&lt;/p&gt;

&lt;p&gt;Second, the default look is a choice. Every time you skip the design conversation, you are choosing the average of the training data. That is still a design decision. It is just a boring one you made by accident.&lt;/p&gt;

&lt;p&gt;Third, scripts beat eyeballs. I trust my taste. My taste failed 2 out of 5 times on a basic readability check. Now I write the check first and pick the colors second, and I think that order matters more than people realize.&lt;/p&gt;

&lt;p&gt;Fourth, design debt starts at line one. Most of us treat design as a cleanup pass at the end of a project. With AI tools, the end never arrives, because there is always one more feature to generate. Deciding first is cheaper than fixing later.&lt;/p&gt;

&lt;p&gt;If you try tastemaker, tell me what broke. Feedback is genuinely welcome, in the comments here or as an issue on GitHub. And if it saves you a few hours, a star on the repo helps other people find it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>css</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Understanding Flow’s Lending Protocol Architecture</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Fri, 12 Dec 2025 08:14:06 +0000</pubDate>
      <link>https://dev.to/codeswithroh/understanding-flows-lending-protocol-architecture-45gp</link>
      <guid>https://dev.to/codeswithroh/understanding-flows-lending-protocol-architecture-45gp</guid>
      <description>&lt;h3&gt;
  
  
  A Beginner Friendly Guide for Web3 Developers
&lt;/h3&gt;

&lt;p&gt;Lending protocols are one of the most important building blocks in decentralized finance. They let users borrow, lend, and build leveraged positions without intermediaries. Protocols like Aave and Compound defined the standard for collateralized on chain lending. Flow introduces a new approach through its Flow Credit Market (FCM) and the product built on top of it, Flow Yield Vaults (FYV).&lt;/p&gt;

&lt;p&gt;This guide explains the context behind Flow’s design, how traditional lending protocols work, and what Flow is doing differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What Is a Lending Protocol?
&lt;/h2&gt;

&lt;p&gt;A lending protocol is a smart contract system where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users deposit assets as collateral&lt;/li&gt;
&lt;li&gt;They borrow other assets against that collateral&lt;/li&gt;
&lt;li&gt;A health factor keeps the position safe&lt;/li&gt;
&lt;li&gt;Collateral is liquidated if health becomes too low&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Liquidation ensures the protocol does not end up with bad debt. But liquidation risk is one of the biggest user pain points in existing systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. How Existing Protocols Handle Liquidations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example: Aave
&lt;/h3&gt;

&lt;p&gt;A typical liquidation flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your collateral value falls&lt;/li&gt;
&lt;li&gt;A Keeper detects your unhealthy position&lt;/li&gt;
&lt;li&gt;The Keeper repays some of your debt using a flashloan&lt;/li&gt;
&lt;li&gt;The Keeper receives a large chunk of your collateral at a discount&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Problems with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liquidations are large&lt;/li&gt;
&lt;li&gt;Keepers profit from user loss&lt;/li&gt;
&lt;li&gt;Liquidations can happen during brief volatility&lt;/li&gt;
&lt;li&gt;Positions can get wiped out instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This model works, but it is harsh and inefficient for users.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Flow’s Architecture: Two Layers Working Together
&lt;/h2&gt;

&lt;p&gt;Flow has two related but distinct systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Flow Credit Market (FCM)
&lt;/h3&gt;

&lt;p&gt;This is the base lending layer. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Borrowing and collateral management&lt;/li&gt;
&lt;li&gt;Health factor logic&lt;/li&gt;
&lt;li&gt;Liquidation rules&lt;/li&gt;
&lt;li&gt;An automation framework for top ups and draw downs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FCM is similar to Aave in role, but with improvements in liquidation mechanics.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Flow Yield Vaults (FYV)
&lt;/h3&gt;

&lt;p&gt;This is a strategy built on top of FCM. FYV uses the automation hooks inside FCM to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuously monitor position health&lt;/li&gt;
&lt;li&gt;Sell a small portion of yield tokens when collateral drops&lt;/li&gt;
&lt;li&gt;Buy more yield tokens when collateral rises&lt;/li&gt;
&lt;li&gt;Maintain a stable health band so liquidation never occurs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FYV behaves like an automated rebalancing engine that runs continuously.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. How Flow Handles Liquidations Differently
&lt;/h2&gt;

&lt;p&gt;Flow’s liquidation system improves upon legacy designs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal Liquidation
&lt;/h3&gt;

&lt;p&gt;Flow sells only the smallest amount of collateral needed to restore health.&lt;/p&gt;

&lt;h3&gt;
  
  
  DEX First Execution
&lt;/h3&gt;

&lt;p&gt;Liquidations are routed through a decentralized exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable pricing&lt;/li&gt;
&lt;li&gt;No dependency on external bots&lt;/li&gt;
&lt;li&gt;Best possible market rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a Keeper wants to liquidate, they must beat the DEX price.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple Small Adjustments
&lt;/h3&gt;

&lt;p&gt;If the market keeps dropping, FCM performs several small corrections instead of one destructive liquidation.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Why FYV’s Automation Matters
&lt;/h2&gt;

&lt;p&gt;Flow Yield Vaults automate everything a user would manually do.&lt;/p&gt;

&lt;p&gt;Example behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If FLOW price drops ten percent, FYV sells ten percent of yield tokens&lt;/li&gt;
&lt;li&gt;If FLOW price rises fifteen percent, FYV buys fifteen percent more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximally invested when safe&lt;/li&gt;
&lt;li&gt;Hedged when risky&lt;/li&gt;
&lt;li&gt;Protected from liquidation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simulations show FYV achieves higher returns than similar strategies on Aave because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It avoids liquidation loss&lt;/li&gt;
&lt;li&gt;It maintains safe leverage&lt;/li&gt;
&lt;li&gt;Automation captures more upside opportunities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Key Takeaways for Developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lending protocols can be redesigned to be more user friendly
&lt;/h3&gt;

&lt;p&gt;Flow reduces liquidation pain and aligns incentives between the protocol and users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation is becoming essential
&lt;/h3&gt;

&lt;p&gt;Instead of relying on keepers or manual adjustments, Flow builds automated risk management at the protocol layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deterministic liquidation improves user trust
&lt;/h3&gt;

&lt;p&gt;DEX based execution ensures transparent and predictable behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traditional Lending Protocols&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large liquidations&lt;/li&gt;
&lt;li&gt;Keeper profit is user loss&lt;/li&gt;
&lt;li&gt;Sudden position wipeouts&lt;/li&gt;
&lt;li&gt;No built in protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flow Credit Market + Flow Yield Vaults&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated health balancing&lt;/li&gt;
&lt;li&gt;Minimal liquidation&lt;/li&gt;
&lt;li&gt;DEX driven execution&lt;/li&gt;
&lt;li&gt;Continuous rebalance strategy&lt;/li&gt;
&lt;li&gt;Higher return with reduced risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow builds a safer and more efficient foundation for leveraged yield strategies, especially for developers building products on top of lending primitives.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>tutorial</category>
      <category>architecture</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Adding Telegram Notifications to Your Application: A Step-by-Step Guide</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Tue, 07 Jan 2025 13:30:49 +0000</pubDate>
      <link>https://dev.to/codeswithroh/adding-telegram-notifications-to-your-application-a-step-by-step-guide-26n0</link>
      <guid>https://dev.to/codeswithroh/adding-telegram-notifications-to-your-application-a-step-by-step-guide-26n0</guid>
      <description>&lt;p&gt;Telegram is a powerful and widely-used messaging platform that offers seamless integration with applications through its &lt;code&gt;Bot API&lt;/code&gt;. Whether you’re building a monitoring service, a trading bot, or a notification system, Telegram notifications can enhance user engagement by providing instant updates.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll walk you through the process of setting up a Telegram bot and enabling it to send notifications to your desired chat.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Telegram for Notifications?
&lt;/h3&gt;

&lt;p&gt;• &lt;strong&gt;Ease of Use&lt;/strong&gt;: Telegram bots are easy to set up and integrate with your application.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Instant Delivery&lt;/strong&gt;: Notifications are delivered in real-time.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Global Reach&lt;/strong&gt;: Telegram has millions of users worldwide.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Customizable Experience&lt;/strong&gt;: You can tailor the bot to send messages in any format, including text, images, and even buttons.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Create a Telegram Bot
&lt;/h4&gt;

&lt;p&gt;The first step is to create a bot in Telegram using the BotFather:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open Telegram: Log into your Telegram account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for BotFather: In the search bar, type “BotFather” and select the verified bot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start a Chat: Send the /start command to begin interacting with BotFather.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a New Bot: Use the /newbot command. You’ll be prompted to:&lt;br&gt;
• Choose a name: This is the display name for your bot.&lt;br&gt;
• Create a username: Must end with “bot” (e.g., MyNotificationBot).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Receive Your Bot Token: Once created, BotFather will provide a token that you’ll use to authenticate API calls. Save this token securely, as it’s your bot’s access key.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fwde3wxck0q8248jt1bmw.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.amazonaws.com%2Fuploads%2Farticles%2Fwde3wxck0q8248jt1bmw.png" alt="Botfather 2" width="799" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Get Your Chat ID&lt;/p&gt;

&lt;p&gt;Telegram needs to know which chat to send notifications to. To retrieve your chat ID:&lt;br&gt;
    1.  Start a Chat with Your Bot: Search for your bot’s username in Telegram and send a message (e.g., “Hello”).&lt;br&gt;
    2.  Use the Bot API: Use the following API endpoint to fetch updates and find your chat ID:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/getUpdates&lt;/p&gt;

&lt;p&gt;Replace  with the token provided by BotFather.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3.  Inspect the Response: The response will include a JSON object. Look for the chat field, which contains the id. This is your chat ID.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Step 3: Understanding the Telegram Bot API&lt;/p&gt;

&lt;p&gt;Telegram provides a simple HTTP-based API to interact with your bot. The endpoint to send a message is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/sendMessage&lt;/p&gt;

&lt;p&gt;Required Parameters&lt;br&gt;
    • chat_id: The ID of the chat where the message should be sent.&lt;br&gt;
    • text: The message content.&lt;/p&gt;

&lt;p&gt;Optional Parameters&lt;br&gt;
    • parse_mode: Format text using Markdown or HTML.&lt;br&gt;
    • disable_notification: Send messages silently (without sound).&lt;br&gt;
    • reply_markup: Add interactive buttons or keyboards.&lt;/p&gt;

&lt;p&gt;Step 4: Test Your Telegram Bot&lt;/p&gt;

&lt;p&gt;Once you have the bot token and chat ID, it’s time to test. You can use a tool like curl or Postman to send a sample message:&lt;/p&gt;

&lt;p&gt;Using curl&lt;/p&gt;

&lt;p&gt;curl -X POST "&lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/sendMessage" \&lt;br&gt;
     -H "Content-Type: application/json" \&lt;br&gt;
     -d '{"chat_id": "", "text": "Hello from my bot!"}'&lt;/p&gt;

&lt;p&gt;Using Postman&lt;br&gt;
    1.  Set the method to POST.&lt;br&gt;
    2.  Use the URL: &lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/sendMessage.&lt;br&gt;
    3.  In the body, use the JSON format:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
    "chat_id": "",&lt;br&gt;
    "text": "Hello from my bot!"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Step 5: Automating Notifications&lt;/p&gt;

&lt;p&gt;To integrate Telegram notifications into your application, you’ll need to:&lt;br&gt;
    1.  Capture the event or payload in your app that triggers a notification.&lt;br&gt;
    2.  Format the message content dynamically (e.g., using placeholders for data like symbols, signals, or confidence levels).&lt;br&gt;
    3.  Use the Telegram Bot API to send the message programmatically.&lt;/p&gt;

&lt;p&gt;Best Practices&lt;br&gt;
    • Secure Your Bot Token: Treat it like a password. Avoid hardcoding it in your application.&lt;br&gt;
    • Rate Limiting: Telegram has limits on API requests. Ensure your app respects these limits to avoid being blocked.&lt;br&gt;
    • Error Handling: Check the API response for errors and retry failed requests if necessary.&lt;br&gt;
    • Use Webhooks: For advanced integrations, consider setting up a webhook to receive real-time updates from Telegram.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;Adding Telegram notifications to your application is a straightforward way to enhance communication with your users. By leveraging the Telegram Bot API, you can deliver real-time updates, alerts, or even automate workflows with minimal effort.&lt;/p&gt;

&lt;p&gt;This blog covered the setup process up to testing your bot. In the next part, we’ll dive into programmatically sending notifications using a backend service. Stay tuned!&lt;/p&gt;

&lt;p&gt;If you have any questions or need assistance, feel free to drop them in the comments below. Happy coding! 🚀&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>programming</category>
      <category>coding</category>
    </item>
    <item>
      <title>Compiling 2025: My Roadmap for the Year Ahead</title>
      <dc:creator>Rohit Purkait</dc:creator>
      <pubDate>Thu, 02 Jan 2025 21:56:10 +0000</pubDate>
      <link>https://dev.to/codeswithroh/compiling-2025-my-roadmap-for-the-year-ahead-4bp8</link>
      <guid>https://dev.to/codeswithroh/compiling-2025-my-roadmap-for-the-year-ahead-4bp8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/newyear"&gt;2025 New Year Writing challenge&lt;/a&gt;: Compiling 2025.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As we step into 2025, I’m excited to share my personal roadmap for the year—a mix of new skills, ambitious projects, and career aspirations. It’s all about diving deeper into Web3, building tools to empower developers, and evolving my professional journey.&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.amazonaws.com%2Fuploads%2Farticles%2F2dx7tvcrtdwifw9t93zq.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.amazonaws.com%2Fuploads%2Farticles%2F2dx7tvcrtdwifw9t93zq.png" alt="Web3 Journey" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  New Skill: Embracing Web3
&lt;/h3&gt;

&lt;p&gt;The buzz around Web3 continues to grow, and I believe 2025 will be its breakout year. To prepare, I’m delving headfirst into this transformative technology. My learning path includes mastering &lt;code&gt;Solidity&lt;/code&gt;, exploring &lt;code&gt;Move by Aptos&lt;/code&gt;, and diving into other Web3 tools and frameworks.&lt;/p&gt;

&lt;p&gt;Web3 represents the future of decentralized applications, and I’m thrilled to align my skillset with this rapidly evolving space. The potential is limitless, and I can’t wait to see where this journey leads.&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.amazonaws.com%2Fuploads%2Farticles%2Far3wifmc60pe8d26qw3i.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.amazonaws.com%2Fuploads%2Farticles%2Far3wifmc60pe8d26qw3i.png" alt="Embracing Web3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Side Projects: Building for the Community
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Sepolia Faucet
&lt;/h4&gt;

&lt;p&gt;One of my key projects is creating a Sepolia faucet where developers can purchase Sepolia tokens using INR. Currently, most faucets require 0.001 ETH on the mainnet—a barrier for newcomers just starting out.&lt;/p&gt;

&lt;p&gt;By offering a more accessible solution, my goal is to fuel creativity and innovation. Developers will be able to acquire Sepolia ETH with a minimal amount of INR, allowing them to focus on building and experimenting with their dApps. This project isn’t just about simplifying access—it’s about empowering the next wave of Web3 creators.&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.amazonaws.com%2Fuploads%2Farticles%2F7lpgdzbv02x3zs6cs25j.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.amazonaws.com%2Fuploads%2Farticles%2F7lpgdzbv02x3zs6cs25j.png" alt="Sneak Peek" width="800" height="562"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Just a base sneak peek. Will be launching on 7th Jan, 2025.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Decentralized Identification (DeID)
&lt;/h4&gt;

&lt;p&gt;Another project close to my heart is building dApps around Decentralized Identification (DeID). Our current identification systems are outdated and ill-equipped for the modern era, where data breaches are increasingly common.&lt;/p&gt;

&lt;p&gt;Through DeID, I envision a system that allows individuals to securely verify their identity wherever they go, putting control back into the hands of the user. This project is a step toward a future where personal data is safe, accessible, and decentralized—a future I’m eager to help shape.&lt;/p&gt;




&lt;h3&gt;
  
  
  Career Aspirations: Shifting Focus to Smart Contracts
&lt;/h3&gt;

&lt;p&gt;As a Full Stack Developer, I’ve had the opportunity to work across the technology spectrum, but this year, I’m pivoting toward a specialized path: Smart Contract Development.&lt;/p&gt;

&lt;p&gt;My goal is to deepen my understanding of this crucial Web3 component and eventually transition into a dedicated role as a smart contract developer. This shift aligns perfectly with my passion for blockchain technology and the ever-expanding possibilities of decentralized solutions.&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.amazonaws.com%2Fuploads%2Farticles%2F4uthc4194z0gc8ek078t.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.amazonaws.com%2Fuploads%2Farticles%2F4uthc4194z0gc8ek078t.png" alt="web3 dream" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Looking Ahead
&lt;/h3&gt;

&lt;p&gt;2025 is shaping up to be a pivotal year for me, full of learning, building, and growing. Whether it’s mastering new skills, developing impactful projects, or pursuing career aspirations, I’m excited for what lies ahead.&lt;/p&gt;

&lt;p&gt;To all my fellow developers, dreamers, and innovators—let’s make 2025 a year to remember!&lt;/p&gt;

&lt;p&gt;What’s on your 2025 roadmap? Share your journey and join the conversation!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>newyearchallenge</category>
      <category>career</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
