<?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: Ibrahim Awab</title>
    <description>The latest articles on DEV Community by Ibrahim Awab (@miawab).</description>
    <link>https://dev.to/miawab</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%2F3792175%2F331d3152-7584-4eff-b422-7090b1d4d67a.jpeg</url>
      <title>DEV Community: Ibrahim Awab</title>
      <link>https://dev.to/miawab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/miawab"/>
    <language>en</language>
    <item>
      <title>I built Wait For It: tell an open Gemma what you're waiting for, it checks rarely, and it got 35 of 36 real pages right with 0 false alarms</title>
      <dc:creator>Ibrahim Awab</dc:creator>
      <pubDate>Wed, 21 Oct 2026 22:47:05 +0000</pubDate>
      <link>https://dev.to/miawab/i-built-wait-for-it-tell-an-open-gemma-what-youre-waiting-for-it-checks-rarely-and-it-got-35-of-jgg</link>
      <guid>https://dev.to/miawab/i-built-wait-for-it-tell-an-open-gemma-what-youre-waiting-for-it-checks-rarely-and-it-got-35-of-jgg</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;This idea started with me waiting. On Monday I opened my coding agent and typed, word for word: "yo yk how we were waiting for hacktober dev.to week 3 topic. why were we waityinh?" I had been refreshing the challenge page for the new theme, and before that the Week 2 page for results, and before that the petrol price board. I spend a silly amount of my week checking pages for one sentence that isn't there yet.&lt;/p&gt;

&lt;p&gt;Wait For It is a web page where you type what you're waiting for in plain words, add a link if you have one, and then stop checking. It looks for you, rarely and on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You write "the Week 2 winners are announced" with the challenge link, or "پاکستان میں پٹرول کی قیمت بدل جائے" (tell me when the petrol price changes in Pakistan) with no link at all.&lt;/li&gt;
&lt;li&gt;Gemma 4, an open-weight model, reads the page (or the past week's Google results) and answers two questions: has it happened, and when should I look next?&lt;/li&gt;
&lt;li&gt;When it has happened, or the date moved, you get a browser notification and a short voice note, in English or Urdu.&lt;/li&gt;
&lt;li&gt;The rest of the time it stays quiet. A watch for something a month away gets looked at once a week, not every hour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who gets to be lazier: anyone waiting on a page that will change once. Students waiting for results, people waiting for a price to drop, developers waiting for a release, and me, waiting for this challenge's own results. The effort it saves is the refreshing and the second-guessing, which for me is many small checks a day.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8cWMEo02lO4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The live page is at &lt;a href="https://waitforit.onrender.com" rel="noopener noreferrer"&gt;https://waitforit.onrender.com&lt;/a&gt; (free Render instance, so the first load can take up to a minute while it wakes up). Notifications need you to click "Turn on notifications" once.&lt;/p&gt;

&lt;p&gt;What a first check looks like. I typed "Linux 7.4 is released" and the kernel.org link, and Gemma's first look came back in about 5 seconds:&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%2Fssu3cbwm8fmy7n742p97.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%2Fssu3cbwm8fmy7n742p97.png" alt="First check of a new watch: Linux 7.3 is the latest, 7.4 not yet, next look in 24 hours" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real watches I left running overnight, including two that answer in Urdu. Each card shows what Gemma found, when it will look again, and the expected date it pulled from the page:&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%2F4dpnaz3fajui8ajcrd46.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%2F4dpnaz3fajui8ajcrd46.png" alt="Watches running on the live site: Python 3.16 in Urdu, GTA 6 expected November 19 with the next look in 7 days" width="800" height="450"&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/miawab" rel="noopener noreferrer"&gt;
        miawab
      &lt;/a&gt; / &lt;a href="https://github.com/miawab/waitforit" rel="noopener noreferrer"&gt;
        waitforit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Tell it what you're waiting for. Stop checking. Gemma decides when it has happened.
    &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;Wait For It&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Tell it what you're waiting for. Stop checking.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Tell me when the results are out" + a link
"Tell me when petrol prices change in Pakistan" (no link needed)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It looks rarely, on purpose. Each time it wakes up, &lt;strong&gt;Gemma&lt;/strong&gt; (an open-weight model) reads the page or the latest search results and decides one thing: &lt;em&gt;has it happened, or changed, since I last told you?&lt;/em&gt; Then it picks when to look next: once a week if the date is far away, every few hours when it's close. You only hear from it when there's real news.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A watch&lt;/strong&gt; = what you're waiting for, plus a link (or nothing, and it searches instead).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A free pinger&lt;/strong&gt; (like cron-job.org) calls &lt;code&gt;/api/tick&lt;/code&gt; every 30 minutes; the server runs only the watches that are due.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each check&lt;/strong&gt;: fetch the page (skip Gemma entirely if the page hasn't…&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/miawab/waitforit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Plain Node (no framework), about 900 lines. MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&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%2Fhifiootxoz53kht9y9vn.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%2Fhifiootxoz53kht9y9vn.png" alt="Pipeline diagram: a pinger every 30 minutes, only due watches, skip Gemma if the page is unchanged, Gemma reads it, code checks the quote, then the notification and Urdu voice note" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture is deliberately boring, because the interesting part is the decision in the middle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A free pinger (cron-job.org) calls &lt;code&gt;/api/tick&lt;/code&gt; every 30 minutes with a secret key. That wakes the free Render server, which runs only the watches whose next check is due, and goes back to sleep. Nothing has to stay on, including my PC.&lt;/li&gt;
&lt;li&gt;For a watch with a link, the server fetches the page, strips menus, footers and hidden HTML, and hashes the text. If the hash matches last time, nothing could have changed, so Gemma isn't asked at all.&lt;/li&gt;
&lt;li&gt;For a watch without a link, it asks SerpApi for the past week's Google results instead. Gemma turns the person's sentence into a short query once, when the watch is created, because searching the literal Urdu sentence found nothing.&lt;/li&gt;
&lt;li&gt;Gemma 4 (26B A4B, the open-weight mixture-of-experts model, hosted on Google AI Studio) gets the request, today's date, what it told the person last time, and the page. It replies with JSON: a status (happened, changed, delayed or nothing), a short summary in the person's language, the earliest and latest likely dates, how many hours until it should look again, and a quote.&lt;/li&gt;
&lt;li&gt;MongoDB Atlas holds the watches, their last 50 checks, the browser push subscriptions, the voice notes and the daily counters.&lt;/li&gt;
&lt;li&gt;ElevenLabs turns the summary into a voice note (Flash v2.5 handles Urdu) only when something happened or was delayed, never for "nothing".&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The model never gets the last word
&lt;/h3&gt;

&lt;p&gt;The first version trusted Gemma's status. That is exactly what keyword alerts get wrong in the other direction: they trust the page. So now Gemma has to show its work. For any news it must copy a few words from the page, and code checks that those words are really there before anyone is told:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A quote that skips words ("India were champions... beat New Zealand") counts if every piece is there.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;quoted&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="nx"&gt;text&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;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFKC&lt;/span&gt;&lt;span class="dl"&gt;'&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;\p&lt;/span&gt;&lt;span class="sr"&gt;{M}/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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;[^\p&lt;/span&gt;&lt;span class="sr"&gt;{L}&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{N}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/gu&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="nf"&gt;trim&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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;pieces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&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;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.{3}&lt;/span&gt;&lt;span class="sr"&gt;|…/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Long enough to mean something: two words, or a number like a version ("158.0").&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enough&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;/&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;q&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enough&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pieces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;t&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="nf"&gt;includes&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;q&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="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&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;/ /g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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;/ /g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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;If the quote isn't on the page, the status is downgraded to "nothing", the check is saved with the rejected quote for me to look at, and no notification or voice note goes out. The same idea runs through the rest of the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemma picks when to look next, but code clamps it to between 30 minutes and 7 days, and forces a look at least every 6 hours once the earliest likely date is within 3 days. In the eval Gemma once asked to sleep for 720 hours (the 2027 Cricket World Cup); the code cut that to a week.&lt;/li&gt;
&lt;li&gt;Gemma can say a watch is done, but for anything that happens again ("tell me when the price changes"), code ignores that and keeps watching. I added this after the petrol watch found a two-day-old price cut on its first look and switched itself off.&lt;/li&gt;
&lt;li&gt;File links Gemma wants to save have to be copied from the page's real link list; anything it invents is dropped.&lt;/li&gt;
&lt;li&gt;The page fetcher refuses private and local addresses, so nobody can point it at the server's own network.&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%2Fgh6i6pb2uihh7d8s2dlw.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%2Fgh6i6pb2uihh7d8s2dlw.png" alt="The quote check: Gemma's claim and quote on the left, the matching words highlighted on the real page on the right" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What one check costs
&lt;/h3&gt;

&lt;p&gt;Every key has a hard daily cap stored in the database (so a restart doesn't reset it), because I am a student on free tiers and a loop or a crawler shouldn't be able to spend anything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per check&lt;/th&gt;
&lt;th&gt;Typical&lt;/th&gt;
&lt;th&gt;Daily cap in code&lt;/th&gt;
&lt;th&gt;Free tier it protects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma call, page watch&lt;/td&gt;
&lt;td&gt;10,383 tokens in, 122 out, about 4 s&lt;/td&gt;
&lt;td&gt;400 calls&lt;/td&gt;
&lt;td&gt;AI Studio free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma call, search watch&lt;/td&gt;
&lt;td&gt;about 930 tokens in, 80 out&lt;/td&gt;
&lt;td&gt;(same 400)&lt;/td&gt;
&lt;td&gt;AI Studio free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma call when the page is unchanged&lt;/td&gt;
&lt;td&gt;0 (skipped by the hash)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi search, no-link watch only&lt;/td&gt;
&lt;td&gt;1 search&lt;/td&gt;
&lt;td&gt;8 searches&lt;/td&gt;
&lt;td&gt;250 searches a month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs voice note, news only&lt;/td&gt;
&lt;td&gt;about 60 characters, 3 s of audio&lt;/td&gt;
&lt;td&gt;15 notes&lt;/td&gt;
&lt;td&gt;10,000 credits a month; the key itself is limited to 5,000 and to text-to-speech only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of that: at most 5 watches per person, 60 in total, 10 new watches per IP per day, and every watch ends after 45 days. I'll delete the keys when judging is over.&lt;/p&gt;

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

&lt;p&gt;A watcher is only as good as its "has it happened?" call, so I built a test set before writing this post. I picked 36 real pages on October 21, wrote a question for each, and labelled every answer by reading the page itself, separately from Gemma:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 release pages (kernel.org, python.org, nodejs.org, go.dev, the Rust blog, Deno's GitHub releases, Firefox release notes), mostly in pairs on the same page: "Linux 7.3 is released" (yes) and "Linux 7.4 is released" (not yet).&lt;/li&gt;
&lt;li&gt;21 Wikipedia pages for events: the 2026 World Cup final (happened), the 2030 World Cup (not yet), GTA 6 (scheduled for November 19), the Nobel Peace Prize winner (announced) and its ceremony (December 10, not yet), and so on.&lt;/li&gt;
&lt;li&gt;19 have happened and 17 haven't. The "not yet" pages are the hard ones, because they are full of words like released, announced and champion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page is saved once, so every run reads the same text, and &lt;code&gt;node eval/run.mjs&lt;/code&gt; runs the real check (the same &lt;code&gt;checkWatch&lt;/code&gt; the server uses, including the quote check) next to a keyword rule that fires when a done-sounding word is on the page. That rule is what a "tell me when this page changes" alert effectively does.&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%2F28mqbqvq7w1elne3pmi2.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%2F28mqbqvq7w1elne3pmi2.png" alt="Bar chart: keyword alert 18 of 36 correct with 15 false alarms; Gemma with the quote check 35 of 36 correct with 0 false alarms" width="800" height="459"&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;36 pages&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;False alarms&lt;/th&gt;
&lt;th&gt;Missed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyword rule&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 + quote check&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Split by kind of page, the keyword rule is bad everywhere, and Gemma's only miss is on Wikipedia:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page type&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Gemma correct&lt;/th&gt;
&lt;th&gt;Gemma false alarms&lt;/th&gt;
&lt;th&gt;Keyword correct&lt;/th&gt;
&lt;th&gt;Keyword false alarms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Release pages&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wikipedia events&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second thing I wanted to know is whether Gemma is actually lazy in a useful way. This is what it chose for "when should I look next?" on the same pages, before the code clamps it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the page said&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Gemma's next look&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;It has happened&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;6 to 12 hours (and the watch ends)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not yet, no date known&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not yet, a date weeks or months away&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;48 hours to 7 days (one asked for 30 days, cut to 7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not yet, missed&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is the behaviour I hoped for: it looks rarely when the date is far and the page gives one, and daily when it has no idea.&lt;/p&gt;

&lt;h3&gt;
  
  
  What went wrong
&lt;/h3&gt;

&lt;p&gt;All of these are real, and most of them changed the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The one miss is Artemis II. The page says it launched on April 1, 2026 and landed April 11. Gemma read "launch date April 1, 2026" as a plan and said it hadn't happened, even though today's date (October 21) was in the prompt. Dates in the past written like schedules are still a weak spot.&lt;/li&gt;
&lt;li&gt;My first full run scored 19 of 36, and 16 of those failures were mine: the free tier limits tokens per minute, and long pages (8,000 to 14,000 tokens each) hit it after two calls. The app now waits as long as Google's error says and carries on, which means a run takes about a minute per page.&lt;/li&gt;
&lt;li&gt;One label was wrong, and it was my fault. I marked "Firefox 158 is out" as not yet because my search pattern missed it; Gemma said it was out, and 158.0 and 158.0.1 are both on the page. I fixed the label and added Firefox 159 as the "not yet" case.&lt;/li&gt;
&lt;li&gt;The quote check was too strict twice: it refused "158.0" (too short) and a quote with "..." joining two sentences from the T20 World Cup page. Both were true. Version numbers and quotes with gaps now count when every piece is on the page.&lt;/li&gt;
&lt;li&gt;Before that, Gemma sometimes returned an empty answer. It thinks before it answers, and with my original 900-token limit the thinking used up the whole budget. I raised it to 2,048 and retry empty or cut-off replies.&lt;/li&gt;
&lt;li&gt;Wikipedia pages were wasting the 14,000-character budget on menus, language lists and HTML snippets hidden inside attributes. The text extractor now skips nav, footers, comments and quoted attributes, and the Artemis II page went from mostly menu to the mission facts in the first 1,000 characters.&lt;/li&gt;
&lt;li&gt;The local version was quietly broken until I tested it for this post. Ollama gives a model a 4,096-token context by default, so on a 10,000-token page local Gemma only ever saw the first 2,051 tokens and mostly answered with nothing. The Ollama call now asks for 12,288 tokens; 20,480 was too big for my 6 GB GPU and pushed half the model onto the CPU.&lt;/li&gt;
&lt;li&gt;The petrol watch switched itself off after its first look, because a recent price cut looked like "it happened". Things that repeat now never end.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Pages that only render with JavaScript, or sit behind a login, come back nearly empty. University portals are the obvious example. A headless browser would fix it, at a cost I haven't paid yet.&lt;/li&gt;
&lt;li&gt;It reads the first 14,000 characters of a page. If the news is at the bottom of a very long page, it won't see it.&lt;/li&gt;
&lt;li&gt;No-link watches depend on Google having the news in the past week, and on 8 searches a day for the whole site.&lt;/li&gt;
&lt;li&gt;The free server sleeps, so a check can run up to 30 minutes late. For exam results that's fine; for concert tickets it isn't.&lt;/li&gt;
&lt;li&gt;Notifications need the browser to have been allowed once, and iPhones only show web push for sites added to the home screen.&lt;/li&gt;
&lt;li&gt;The test set is 36 pages picked by one person on one day. It shows the idea works and where it breaks; it isn't a benchmark.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this project it matters for three concrete reasons, not as a slogan.&lt;/p&gt;

&lt;p&gt;The first is that the pages people wait on are often private. A results page you are logged into, a supplier's price list, a hospital's appointment page: I don't want any of those going to a company's API just to ask "has it changed?". Because Gemma's weights are open, the same code switches to a model running on your own machine with one setting. Set &lt;code&gt;OLLAMA_URL&lt;/code&gt; and Wait For It uses &lt;code&gt;gemma4:e4b-it-qat&lt;/code&gt; through Ollama instead of AI Studio, and the page never leaves your PC. I checked this on my own 6 GB GTX 1660 Super with six of the eval pages: local Gemma got 5 right and failed one (the long Firefox release-notes page, where it ran out of time before producing an answer). It is slow, 42 to 124 seconds a page against about 4 seconds hosted, but a watcher that looks once a day can afford to be slow.&lt;/p&gt;

&lt;p&gt;The second is cost and control over time. A watcher runs for weeks. With a closed API, the price, the rate limits or the model itself can change halfway through a watch, and I would find out when it broke. With open weights I can pin the exact model the eval was run on, and if the hosted free tier goes away, the fallback is a download, not a rewrite.&lt;/p&gt;

&lt;p&gt;The third is that I could build on other open work instead of starting from zero. The Gemma calling code is adapted from my earlier open-source project, scamcheck (credited in the README); edge-tts and Remotion, both open source, made the video; and Ollama makes "run it yourself" a single command. Open models are also what let me look inside the failure: when Gemma returned empty answers, the API showed me the thinking tokens, and that is how I found the budget problem.&lt;/p&gt;

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

&lt;p&gt;I am using it for the things I'm actually waiting for. These are the five watches running on the live site right now, all started on October 22 at about 2:45 AM my time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Watch&lt;/th&gt;
&lt;th&gt;First answer&lt;/th&gt;
&lt;th&gt;Next look Gemma chose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The Week 2 winners are announced (the challenge page)&lt;/td&gt;
&lt;td&gt;Expected during the week of October 19&lt;/td&gt;
&lt;td&gt;6 hours (inside the window)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Petrol price in Pakistan changes (Urdu, no link)&lt;/td&gt;
&lt;td&gt;No new change since the last cut&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux 7.4 is released (kernel.org)&lt;/td&gt;
&lt;td&gt;7.3 is the latest, 7.4 not yet&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GTA 6 comes out (Wikipedia)&lt;/td&gt;
&lt;td&gt;Still November 19&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 3.16 is released (python.org, Urdu)&lt;/td&gt;
&lt;td&gt;Not yet, expected around October 2027&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How much refreshing that replaces depends on how anxious you are, so here is the comparison against someone who checks three times a day, using the schedule rules above and assuming the dates on the pages hold:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Watch&lt;/th&gt;
&lt;th&gt;Until&lt;/th&gt;
&lt;th&gt;Person checking 3 times a day&lt;/th&gt;
&lt;th&gt;Wait For It's looks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GTA 6&lt;/td&gt;
&lt;td&gt;November 19 (28 days)&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;about 9: weekly, then every 6 hours in the last 3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux 7.4&lt;/td&gt;
&lt;td&gt;early December (about 45 days, no date on the page)&lt;/td&gt;
&lt;td&gt;about 135&lt;/td&gt;
&lt;td&gt;about 45: daily, since it has no date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 3.16&lt;/td&gt;
&lt;td&gt;October 2027&lt;/td&gt;
&lt;td&gt;over 1,000&lt;/td&gt;
&lt;td&gt;about 60: weekly for a year, then every 6 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Week 2 winners&lt;/td&gt;
&lt;td&gt;this week&lt;/td&gt;
&lt;td&gt;3 a day&lt;/td&gt;
&lt;td&gt;4 a day, and I don't have to do any of them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And for every one of those looks where the page hasn't changed, Gemma isn't even asked. That is the whole point: the only looks these pages need from now on are Gemma's, and I can close the tabs.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma: Gemma 4 (26B A4B on AI Studio, or E4B locally through Ollama) is the judge of every check: has it happened, what changed, when to look next, and the quote that proves it, in English or Urdu. Measured on 36 hand-labelled pages: 35 right, 0 false alarms.&lt;/li&gt;
&lt;li&gt;Best Use of SerpApi: watches without a link ("tell me when the petrol price changes") search the past week of Google results through SerpApi, with a query Gemma writes once from the person's own sentence, capped at 8 searches a day.&lt;/li&gt;
&lt;li&gt;Best Use of ElevenLabs: when something happens or is delayed, the summary becomes a short voice note with Flash v2.5, including in Urdu, through a key restricted to text-to-speech with its own credit limit.&lt;/li&gt;
&lt;li&gt;Best Use of MongoDB: Atlas stores every watch with its last 50 checks, push subscriptions, voice notes and the daily spending counters that survive restarts, behind an IP list that only allows the Render servers.&lt;/li&gt;
&lt;li&gt;Best Use of Render: a free Render web service is the whole backend; a 30-minute ping wakes it to run only the watches that are due, so nothing stays on between checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code, the eval set and the results file are in the repo. I built this with Claude Code as my coding agent, and it helped draft this post; every number in it comes from the eval output in the repo or from the live site.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>pricecheck: my friend didn't know what to offer for a used monitor, so now he gets three numbers</title>
      <dc:creator>Ibrahim Awab</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:04:39 +0000</pubDate>
      <link>https://dev.to/miawab/pricecheck-is-this-olx-price-fair-local-gemma-reads-the-listing-tabpfn-prices-it-code-tells-you-2ipe</link>
      <guid>https://dev.to/miawab/pricecheck-is-this-olx-price-fair-local-gemma-reads-the-listing-tabpfn-prices-it-code-tells-you-2ipe</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A friend of mine in Islamabad needs a monitor. He told me what he wanted like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want a 27 inch monitor… has to be obviously a reputable brand, I don't know much about monitors tho… like 27 inch IPS, budget maybe around 35k."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then he said the part that actually turned into a project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Don't know how to bargain too. If I had some frame of reference, like someone put a monitor at 55k and I can bring it down to 35k, that would be easy for me. So that's an issue, that I don't know how much I can haggle, plus I don't know much about monitors."&lt;/p&gt;
&lt;/blockquote&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%2Fl8l6n7ilkn5zi39wsby1.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%2Fl8l6n7ilkn5zi39wsby1.png" alt="My friend's problem: he doesn't know how much he can haggle" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Pakistan, used electronics mostly change hands on OLX. Every listing is an asking price, and every buyer is expected to bargain. If you don't know what similar monitors usually go for, you either overpay or you insult the seller with a lowball and lose the deal. My friend's problem wasn't "which monitor is best". It was "what number do I say out loud".&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;pricecheck&lt;/strong&gt;. You paste an OLX link (or browse a category with filters), and it tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what similar listings usually ask, as a range, not one number&lt;/li&gt;
&lt;li&gt;a verdict: good deal, fair, overpriced, suspiciously cheap, or hard to judge&lt;/li&gt;
&lt;li&gt;three numbers to use when you bargain: &lt;strong&gt;open your offer at&lt;/strong&gt;, &lt;strong&gt;a fair deal is&lt;/strong&gt;, &lt;strong&gt;most you should pay&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the similar listings it compared against, what it read from the listing, what to check in person, and what Pakistani shops charge for the same model new&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs on my own PC with open models. Gemma 4 E4B reads the listings locally through Ollama on a GTX 1660 Super. TabPFN predicts the price range. Plain Python decides the advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it:&lt;/strong&gt; anyone buying used electronics on OLX who doesn't bargain for a living, like my friend. It's also useful to people who resell: the same range tells a seller whether they're asking more than similar listings.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live site (browse 500+ priced monitors, no setup):&lt;/strong&gt; &lt;a href="https://pricecheck-gamma.vercel.app" rel="noopener noreferrer"&gt;https://pricecheck-gamma.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/-1gWavzvo9s" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;What the recording shows, step by step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. My friend's filters.&lt;/strong&gt; 27", IPS, Islamabad, budget 35,000, plus "include ones I could bargain down to it". One listing matches: an ASUS PB278Q (27", 1440p, IPS) asking &lt;strong&gt;Rs 40,000&lt;/strong&gt;, which pricecheck marks &lt;strong&gt;overpriced, 24% above usual&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%2Fazb11vsrx64xe66bf2vm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fazb11vsrx64xe66bf2vm.jpg" alt="Browsing with my friend's filters: 27 inch, IPS, Islamabad, 35k budget" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The bargaining panel.&lt;/strong&gt; Half of similar listings ask between Rs 26,583 and Rs 39,679, so it says: open at &lt;strong&gt;Rs 24,500&lt;/strong&gt;, a fair deal is &lt;strong&gt;Rs 26,500 to 32,000&lt;/strong&gt;, and don't go above &lt;strong&gt;Rs 39,500&lt;/strong&gt;. Below that: what Gemma read from the listing, what to check in person, and the similar listings it compared against.&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%2Fgkp1b7ioxgdpsgcex16h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkp1b7ioxgdpsgcex16h.jpg" alt="The bargaining panel for the ASUS PB278Q" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A listing that is not in my data.&lt;/strong&gt; A Dell SE2717HX (27", IPS, 75 Hz) went up on OLX today for &lt;strong&gt;Rs 20,500&lt;/strong&gt;. I paste the link on the local page and it checks it on the spot: fetches the listing, Gemma reads it, TabPFN prices it against 510 others, code writes the advice.&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%2Fyj09xg14dcxyrpmap74n.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%2Fyj09xg14dcxyrpmap74n.png" alt="Pasting a brand-new OLX link and checking it live" width="800" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verdict: &lt;strong&gt;good deal, 29% below usual&lt;/strong&gt;. Similar listings ask Rs 24,078 to 34,490. Open at &lt;strong&gt;Rs 19,000&lt;/strong&gt;, and don't pay more than the asking price.&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%2F9hygpe4xcbdnugx381ws.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hygpe4xcbdnugx381ws.jpg" alt="Live result for the Dell SE2717HX" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The Sentry trace&lt;/strong&gt; of a live check of that same listing (in How I Built It below).&lt;/p&gt;

&lt;p&gt;That Dell is exactly what my friend described, inside his budget, with room to spare. It's in Lahore, not Islamabad, which is the kind of thing he still has to decide himself.&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/miawab" rel="noopener noreferrer"&gt;
        miawab
      &lt;/a&gt; / &lt;a href="https://github.com/miawab/pricecheck" rel="noopener noreferrer"&gt;
        pricecheck
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Is this OLX price fair? Gemma reads listings, TabPFN prices them, all on your own PC.
    &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;pricecheck&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Is this OLX price fair?&lt;/strong&gt; Paste an OLX link or browse a category, and see what similar listings usually ask, whether this one is a good deal, and what to offer when you bargain.&lt;/p&gt;
&lt;p&gt;Everything runs on your own computer with open models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma&lt;/strong&gt; (Google's open-weight model, run locally with Ollama) reads each messy listing ("27 inch ips lush condition 10/10 box available") into clean columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN&lt;/strong&gt; (Prior Labs' tabular model, also local) learns from all the other listings what a similar item usually costs, and gives a price &lt;em&gt;range&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain code&lt;/strong&gt; turns that range into advice: open at, fair deal, most you should pay. The models supply facts; code decides.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nothing runs in the background. Data only changes when you run the update yourself.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What you need&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;A Windows, Mac or Linux PC. An NVIDIA GPU with 6 GB+ makes Gemma fast (a few seconds per listing)…&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/miawab/pricecheck" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The live site is static, so anyone can browse the priced listings. To check brand-new links like in the demo, run it locally: the README walks through installing Ollama, pulling Gemma, getting a free TabPFN key, and &lt;code&gt;python -m pricecheck.doctor&lt;/code&gt; tells you what's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&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%2Foeui3h5e9aonaulca5dz.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%2Foeui3h5e9aonaulca5dz.png" alt="How a check works: OLX listing, Gemma reads it, TabPFN prices it, plain code decides the advice" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Collect.&lt;/strong&gt; pricecheck reads OLX's public monitor category: one page every 2 seconds, nothing behind a login. Phone numbers are stripped from titles and descriptions before anything is saved (933 of them had one), and seller names are never stored. On Oct 14 that gave 1,180 listings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Gemma reads each listing, once.&lt;/strong&gt; OLX listings look like "27 inch ips lush condition 10/10 box available urgent sale". Gemma 4 E4B turns that into columns: brand, model, size, panel, resolution, refresh rate, condition, whether it has a fault, whether the box is included. It runs locally through Ollama, so there's no API bill per listing, and each listing is only read once. 643 listings have been read so far.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. TabPFN prices each listing from the others.&lt;/strong&gt; TabPFN is Prior Labs' foundation model for tables: you give it rows with known prices and it predicts prices for new rows, with no training loop on my side. I ask it for the 25th, 50th and 75th percentile, so every listing gets a "usual range", not a single guess. When pricing the whole dataset I use 5 folds, so a listing's own price is never in the rows used to price it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Plain code turns the range into advice.&lt;/strong&gt; This is the bit I was most careful about. The models supply facts; code decides what to tell a person who is about to spend their money:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;advice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q75&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Turn a price range into what to do. Code decides; the model only supplied the range.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;q50&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;q75&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Similar listings vary too much in price to judge this one. ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too_cheap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Far below similar listings. Check carefully in person: ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good_deal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cheaper than most similar listings. ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;q75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In the usual range. There&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s some room to bargain.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;say&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overpriced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Above the usual range. Bargain hard or ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;target_low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round500&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fair_from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round500&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_low&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fair_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round500&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_high&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_pay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round500&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q75&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;  &lt;span class="c1"&gt;# never more than they ask, never above the usual range
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules matter most. It will never tell you to pay more than the seller asked. And if similar listings are spread too wide to judge, it says "hard to judge" instead of inventing confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. SerpApi checks what shops charge.&lt;/strong&gt; For listings with a clear model name, pricecheck searches Google Shopping through SerpApi for Pakistani shops selling the same model new (OLX and Facebook results excluded). It only shows one number when the shops roughly agree; otherwise it shows the range and links. Found for 45 listings so far, 19 where shops agree, using 35 searches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Sentry traces every check as an agent run.&lt;/strong&gt; Each live check is an &lt;code&gt;invoke_agent&lt;/code&gt; span, with a &lt;code&gt;chat gemma&lt;/code&gt; span for the read and tool spans for &lt;code&gt;fetch_olx_listing&lt;/code&gt;, &lt;code&gt;tabpfn_price_range&lt;/code&gt;, &lt;code&gt;bargain_advice&lt;/code&gt; and &lt;code&gt;serpapi_new_price&lt;/code&gt;. Here is the trace of a live check of the Dell from the demo:&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%2F2tnclo5kmpncqdxqidq4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tnclo5kmpncqdxqidq4.jpg" alt="Sentry trace of one live check" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole check took 19.04 s: OLX fetch 1.06 s, Gemma 5.18 s, TabPFN 10.88 s, the advice itself 6.56 ms, SerpApi 1.84 s. That breakdown is how I know where time actually goes. Without it I would have guessed Gemma was the slow part. It isn't any more; TabPFN is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Any OLX category.&lt;/strong&gt; Monitors is one "recipe", a small TOML file listing where to collect from and which fields matter. &lt;code&gt;python -m pricecheck.new_recipe phones "&amp;lt;OLX link&amp;gt;"&lt;/code&gt; lets Gemma draft a new recipe from real listings, which you then read and fix by hand. Nothing runs on a schedule; prices only update when you run the update yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I wanted to know if TabPFN was actually earning its place, so I compared it with simpler ways to price the same listings. &lt;code&gt;python -m pricecheck.evaluate monitors&lt;/code&gt; prices all 510 single-monitor listings, each one without seeing its own price:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Typical error&lt;/th&gt;
&lt;th&gt;Within 15% of asking price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same-specs median (what a person would do by hand)&lt;/td&gt;
&lt;td&gt;28.6%&lt;/td&gt;
&lt;td&gt;26.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;22.4%&lt;/td&gt;
&lt;td&gt;35.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;39.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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%2F9op4d3uwm58t31g1tzeb.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%2F9op4d3uwm58t31g1tzeb.png" alt="Typical pricing error: same-specs median 28.6%, gradient boosting 22.4%, TabPFN 19.2%" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TabPFN's "usual range" contained the real asking price &lt;strong&gt;51%&lt;/strong&gt; of the time. A 25th-75th percentile range should hold about half of prices, so 51% means the ranges are honest: not too narrow to look confident, not too wide to be useless. On an earlier test with only 27" monitors, TabPFN's typical error was 22% against 35% for the simple rule.&lt;/p&gt;

&lt;p&gt;19% typical error sounds large until you remember what it's predicting: asking prices, which are already noisy. Sellers price the same model very differently, and some never expect to get what they ask. The goal isn't to guess the exact number. It's to give someone a frame of reference, which is the phrase my friend used.&lt;/p&gt;

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

&lt;p&gt;For a price checker, the economics decide whether it can exist at all. A check has to cost close to nothing, because nobody pays much to be told what to offer for a 20,000-rupee monitor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No bill per listing.&lt;/strong&gt; Gemma read 643 listings on my own 6 GB GPU, and each update only reads the new ones. TabPFN runs on my CPU. The only outside calls are SerpApi for shop prices (35 searches so far, inside the free tier) and Sentry for traces (free tier). Re-reading the whole category costs electricity, not tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could change how the model behaves.&lt;/strong&gt; Gemma 4's thinking mode was making each listing take about 20 seconds. Because I run it myself, I turned it off and forced its answer into a fixed JSON schema, which brought it down to 5-7 seconds and gave me the "has a fault" field that fixed the broken-monitor mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swapping models is one setting.&lt;/strong&gt; On a weaker PC, &lt;code&gt;PRICECHECK_MODEL=gemma4:e2b-it-qat&lt;/code&gt; switches to the smaller Gemma. Nothing else changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No listing goes to a model provider.&lt;/strong&gt; The only things that leave my PC are the shop-price searches and the Sentry traces. The listings are public, but I still strip phone numbers before saving anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't benchmark a closed API against this, so I can't claim it would be less accurate. The win is that running it costs me nothing per check and I control every step, which is what makes it something a person could actually sell.&lt;/p&gt;

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

&lt;p&gt;My friend tried it before I wrote any of this. His reaction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"W, this is nice. Does this take into account how old a model is? Good though, very helpful."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;His question is a good one, and the honest answer is: partly. pricecheck uses how long a listing has been up, its condition, and its specs (refresh rate, resolution, panel type), and those track a model's generation fairly well. But it does not know a model's release year. A Dell P2719H from 2018 and a P2725H from 2024 are both "27-inch 1080p IPS Dell" to it, and they shouldn't be priced the same. That's the first thing I'd add next.&lt;/p&gt;

&lt;p&gt;I also asked him straight out whether he would pay for it. His answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"lol if i had the money yeah. but yeah def a lot of ppl would"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair point: a student hunting for a 35k monitor has little to spare. My guess is that the paying users are people with more at stake per purchase, like resellers checking what to ask for their stock, but I haven't tested that yet.&lt;/p&gt;

&lt;p&gt;He hasn't contacted a seller or bought anything yet, so I can't tell you how the bargaining went. If he buys one before the deadline, I'll add what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd charge
&lt;/h2&gt;

&lt;p&gt;My friend's answer shaped this. Buyers like him are students with little to spare, so checking a listing stays free. One check costs me a few seconds of my own GPU and CPU, so a few free checks a day per person costs close to nothing.&lt;/p&gt;

&lt;p&gt;The paid plan is for resellers, people who buy and sell used monitors and phones on OLX every week. For them the numbers are bigger. In the demo, the ASUS listing asks Rs 40,000 and pricecheck says a fair deal is Rs 26,500 to 32,000. A reseller who gets that one wrong loses more than a month's subscription. What I'd sell them, for &lt;strong&gt;Rs 1,000 a month (about $3.50)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unlimited checks&lt;/li&gt;
&lt;li&gt;their own listings compared with the usual range, so they know when they're asking too much to sell, or too little&lt;/li&gt;
&lt;li&gt;a message when a new listing in their category goes up well below the usual range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two already work. The third is the update I already run, plus a message. I haven't asked a reseller to pay yet, so this is a plan, not a result. The next step is to show it to a few resellers in Islamabad and ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma marked working monitors as broken.&lt;/strong&gt; My first schema had one field mixing "what kind of listing is this" with "is something wrong with it". Splitting it into a listing type and a separate "has a fault" field fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was slow at first: about 20 s per listing.&lt;/strong&gt; Turning off Gemma 4's thinking mode and using short JSON keys brought it to 5-7 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some answers stopped mid-JSON.&lt;/strong&gt; Those are now retried, and Sentry flags them so I can see how often it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first prices were nonsense for rare items.&lt;/strong&gt; A 27" 4K monitor "usually" cost 79k to 398k, because a few Apple displays stretched the range. I fixed it three ways: size is required, wildly-off prices are kept out of training, and the "hard to judge" verdict exists for exactly this case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sellers mislabel brands.&lt;/strong&gt; One Koorui monitor was listed under the brand "1st Player". Gemma now reads the brand from the title and description instead of trusting OLX's brand field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cold model is very slow.&lt;/strong&gt; Sentry's first trace showed one check taking 1.84 minutes because Gemma wasn't loaded on the GPU yet. Warm, the Gemma step is about 5 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shop prices from Google are noisy.&lt;/strong&gt; One Lenovo model was 34k at one shop and 97k at another. That's why the page only shows a single shop price when shops agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My GPU ran hot.&lt;/strong&gt; Under load it sat at 88-91 °C with the fan reading 0%. I stopped Gemma with 562 listings still unread rather than push it. Those listings are collected but not priced on the site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OLX only shows asking prices, never sold prices.&lt;/strong&gt; pricecheck tells you what sellers ask, not what buyers paid. Listings that disappear are kept as the closest thing to a sale price, but that's an estimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't know a model's age&lt;/strong&gt; (my friend's question above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't see the monitor.&lt;/strong&gt; Dead pixels, backlight bleed and broken ports only show up in person, so every listing gets a short "check this in person" list instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's monitors only, for now.&lt;/strong&gt; The recipe system works for other categories, but I've only tested monitors properly.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN:&lt;/strong&gt; TabPFN is the decision core of the agent. It produces the 25th/50th/75th percentile price range for every listing (5-fold so no listing sees its own price), beats a same-specs median and gradient boosting on 510 real listings (19.2% vs 28.6% and 22.4% typical error), and its range holds the real price 51% of the time. Code turns that range into the bargaining advice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; Gemma 4 E4B runs locally through Ollama on a 6 GB GPU. It reads every messy OLX listing into structured specs, reads brand-new links live during a check, and drafts recipes for new OLX categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry (Agent Tracing):&lt;/strong&gt; every live check is traced as an &lt;code&gt;invoke_agent&lt;/code&gt; span with a &lt;code&gt;chat gemma&lt;/code&gt; span and four tool spans; the trace screenshot above is from a live check of the Dell listing in the demo. Tracing is how I found the 1.84-minute cold start and that TabPFN, not Gemma, is now the slow step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi:&lt;/strong&gt; Google Shopping results through SerpApi give the new-from-shop price for the same model, so a buyer can see when a used listing costs nearly as much as a new one.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Love Fragrances, So I Built a 6-Game Arcade + Concierge About My Obsession</title>
      <dc:creator>Ibrahim Awab</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:12:15 +0000</pubDate>
      <link>https://dev.to/miawab/i-love-fragrances-so-i-built-a-6-game-arcade-concierge-about-my-obsession-a6g</link>
      <guid>https://dev.to/miawab/i-love-fragrances-so-i-built-a-6-game-arcade-concierge-about-my-obsession-a6g</guid>
      <description>&lt;p&gt;Hi, my name's Ibrahim, I'm a university student, and I have a problem: I love fragrances way more than my bank account loves me for it.&lt;/p&gt;

&lt;p&gt;It started small, the way these things always do. A cheap Middle Eastern attar someone gave me as a gift, the kind that costs less than a coffee but somehow smells like it belongs in a much fancier bottle. Then another. Then I started actually reading about notes, pyramids, accords, sillage, the whole rabbit hole. Fast forward through a lot of saved-up allowance and skipped nights out, and I've now got about 20 bottles on my shelf. Mostly affordable Middle Eastern gems (some of them genuinely punch way above their price), with a small handful of designer pieces I saved up for and treat like trophies.&lt;/p&gt;

&lt;p&gt;If you're a fellow fragrance enthusiast, you already know the feeling: you don't just "wear" a scent, you &lt;em&gt;collect&lt;/em&gt; them, you &lt;em&gt;study&lt;/em&gt; them, you have opinions about whether a note is top, heart, or base and you will absolutely fight someone about it. That obsession is basically the entire reason this project exists.&lt;/p&gt;

&lt;p&gt;So when I saw the DEV Weekend Challenge's "Passion" prompt, there was only one thing I could possibly build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built: recommendmeafragrance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;recommendmeafragrance&lt;/strong&gt; is a browser arcade for fragrance nerds: six small daily games built around real perfume data (notes, brands, years, price tiers), plus an AI Concierge you can actually talk to about what you're in the mood for. Every game feeds into a personal "shelf" that tracks which fragrances you've discovered, plus streaks so you have a reason to come back tomorrow.&lt;/p&gt;

&lt;p&gt;Here's the tour.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Scentle: Wordle, but for your nose
&lt;/h3&gt;

&lt;p&gt;A new fragrance is picked every day (the same one for everyone, worldwide, no matter your timezone). You get 6 guesses, and after each one you get Wordle style feedback: was the brand exact or just the same house family, did the real answer come out earlier or later than your guess, is it pricier or cheaper, same gender, same concentration, how many notes do you actually share. You can even copy a little emoji share-grid at the end, because obviously.&lt;/p&gt;

&lt;p&gt;There's now an &lt;strong&gt;Easy/Hard toggle&lt;/strong&gt;: Easy leans on a famous, recognizable bottle for the day; Hard pulls from the deep catalog for people who want a real challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Note Detective: reverse engineer a scent from its notes only
&lt;/h3&gt;

&lt;p&gt;This one's my favorite. You're given nothing but notes, revealed one at a time in rotating order (one top, one heart, one base, cycling through the pyramid), and you have to guess the perfume before your score hits zero. Every note you reveal costs points, but the cost is calculated as a percentage of that specific perfume's total note count, so a fragrance with 6 notes and one with 16 notes both cost the same fair share to fully reveal. As you burn through reveals, letters of the name itself slowly uncover hangman-style, so there's a nice tension between "guess now" and "wait, I need one more hint."&lt;/p&gt;

&lt;p&gt;Also has an Easy/Hard mode, same famous-vs-deep-catalog split as Scentle.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧴 Build-a-Bottle — invent your own scent, find out what it actually is
&lt;/h3&gt;

&lt;p&gt;Pick as many top, heart, and base notes as you want from a searchable list of hundreds of real notes, hit "invent," and the app runs a weighted similarity match (base notes count extra, since they carry a scent's identity the most) against the whole catalog to tell you what you basically just invented, plus two runner-up matches. It also treats closely related notes as the same family (so picking "vanilla" still matches perfumes listed under "Madagascar vanilla," etc.), because with hundreds of real notes in the system, exact-string-only matching would've been brutal.&lt;/p&gt;

&lt;h3&gt;
  
  
  📈 Higher or Lower
&lt;/h3&gt;

&lt;p&gt;Two perfumes, one question: which came out first, or which one's pricier? Keep guessing right to build a streak, one miss ends the run. Easy mode keeps the gap obvious and leans on famous bottles; Hard mode pulls from the deep catalog and can come down to a single year or price tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎰 Scent Roulette
&lt;/h3&gt;

&lt;p&gt;Set some dials (season, occasion, how much chaos you want), pull the lever, get a fragrance recommendation that actually respects your filters instead of just being random.&lt;/p&gt;

&lt;h3&gt;
  
  
  💘 Blind Date
&lt;/h3&gt;

&lt;p&gt;Swipe through fragrances blind, no brand or hype attached, just notes and vibe, and decide "buy" or "skip." At the end it tells you your "nose score," how many of your buys were actually the famous, universally-loved ones. Humbling, honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the Concierge
&lt;/h2&gt;

&lt;p&gt;Alongside the games there's a chat-style AI Concierge, built to feel like the app's little mascot come to life: warm, encouraging, genuinely curious about what you like, not just a recommendation-spitting bot. Tell it "something warm and cozy for winter nights" and it'll actually ask a follow-up question before throwing fragrances at you, and when it does recommend something, it comes with a real, buyable card attached, not just a name typed in a chat bubble.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bringing Google AI into it 🔑
&lt;/h3&gt;

&lt;p&gt;This is where I'm submitting for the &lt;strong&gt;Best Use of Google AI&lt;/strong&gt; category. The Concierge runs on a shared daily budget by default so everyone gets to try it for free, but I added a &lt;strong&gt;"bring your own Gemini API key"&lt;/strong&gt; option: paste in a free key from Google AI Studio, and the Concierge switches over to running entirely on &lt;strong&gt;Gemini&lt;/strong&gt; for you, with zero daily limit. The key never touches our servers, it lives only in your browser's local storage and gets sent along with your own requests, so you're fully in control of it. It felt like the right way to let people who want unlimited access get it, without me needing to foot an unlimited AI bill as a student running this on personal savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real bottles, real links
&lt;/h2&gt;

&lt;p&gt;None of this matters if it's just a trivia game, so every single fragrance in the catalog (over a thousand of them) is linked to a live, working offer through FragranceShop.com. Win a game, discover something in the Concierge, or just click "buy" on a card, and it adds to your personal shelf and takes you straight to a real product you can actually purchase. It's a small, honest way to make an app about a hobby self-sustaining without shoving ads in your face.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://www.recommendmeafragrance.com/" rel="noopener noreferrer"&gt;recommendmeafragrance.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No pressure to sign up for anything just to poke around, though creating a free account (Google sign-in, ten seconds) saves your streaks and shelf across devices and unlocks the Concierge chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;I've genuinely lost track of how many hours went into this between actual classwork, but every time I opened a new bottle from my (very modest, very Middle-Eastern-attar-heavy) collection for "research," it felt worth it. If you're even a little bit of a fragrance nerd, or you just like small, weird, well-made web games, I'd love for you to come play and tell me what your nose score is.&lt;/p&gt;

&lt;h1&gt;
  
  
  weekendchallenge #googleai #webdev #showdev
&lt;/h1&gt;

</description>
      <category>weekendchallenge</category>
      <category>googleai</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Playable Life Sim That Reveals Your Real Climate Impact at the End</title>
      <dc:creator>Ibrahim Awab</dc:creator>
      <pubDate>Sun, 19 Apr 2026 18:27:53 +0000</pubDate>
      <link>https://dev.to/miawab/i-built-a-playable-life-sim-that-reveals-your-real-climate-impact-at-the-end-3h43</link>
      <guid>https://dev.to/miawab/i-built-a-playable-life-sim-that-reveals-your-real-climate-impact-at-the-end-3h43</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Earth Day Edition — Earth Impact&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;Most climate apps show you a number.&lt;/p&gt;

&lt;p&gt;Earth Impact makes you live one.&lt;/p&gt;

&lt;p&gt;It’s a BitLife-style life simulation where you start at age 18 and move through an ordinary life: choosing a career, buying a car, booking flights, investing money, starting a family, upgrading your home. At first, it feels like a normal decision game. Nothing about it feels like an environmental tool.&lt;/p&gt;

&lt;p&gt;That’s intentional.&lt;/p&gt;

&lt;p&gt;The environmental cost is hidden during play, then revealed at the end.&lt;/p&gt;

&lt;p&gt;When the life finishes, the game translates your choices into their real-world consequences: not just tons of CO2, but heat, displacement, harvest loss, water scarcity, habitat collapse, and indirect deaths tied to the cumulative weight of a lifetime. The ending is designed to feel less like a scoreboard and more like a receipt.&lt;/p&gt;

&lt;p&gt;The goal wasn’t to lecture. It was to make the player pause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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://earth-impact-three.vercel.app/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;earth-impact-three.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&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/miawab" rel="noopener noreferrer"&gt;
        miawab
      &lt;/a&gt; / &lt;a href="https://github.com/miawab/Earth-Impact" rel="noopener noreferrer"&gt;
        Earth-Impact
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;I structured the project so the core simulation stays fast and deterministic, while AI is used only where it adds value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local-first gameplay
&lt;/h3&gt;

&lt;p&gt;Most of the game runs locally. A reducer drives the state machine, so the player’s life progression is predictable and easy to reason about. The game keeps track of age, health, money, reputation, risk, active memories, current scenario, and environmental totals through a central &lt;code&gt;GameState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A local rarity engine decides which years should trigger meaningful events. Each year can fall into one of several tiers — common, uncommon, rare, epic, legendary, or skip. Skip years still matter: they apply passive environmental and economic drift locally, without making an API call.&lt;/p&gt;

&lt;p&gt;That means the game can simulate a full life while only reaching out to Backboard when it has something meaningful to ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smarter scenario generation
&lt;/h3&gt;

&lt;p&gt;Rather than sending an open-ended prompt every time, the game decides the scenario category before the request goes out. That keeps the AI brief narrow and concrete.&lt;/p&gt;

&lt;p&gt;The result is a more grounded system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer repetitive prompts,&lt;/li&gt;
&lt;li&gt;more relevant life events,&lt;/li&gt;
&lt;li&gt;better pacing across the player’s lifespan,&lt;/li&gt;
&lt;li&gt;and less unnecessary API usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added memory and anti-repeat logic so the game doesn’t keep asking the same kinds of questions. If the player already bought a car, married, had children, or made a major purchase, future scenarios can respond to that history instead of ignoring it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefetching and responsiveness
&lt;/h3&gt;

&lt;p&gt;To keep the game feeling immediate, the next scenario is prefetched in the background as soon as the player makes a choice.&lt;/p&gt;

&lt;p&gt;That means the game doesn’t sit and wait after every action. By the time the result is read, the next branch is often already ready.&lt;/p&gt;

&lt;p&gt;This makes a bigger difference than it sounds like. It keeps the experience from feeling like “request → loading → request → loading” and makes it feel more like a continuous life unfolding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final summary system
&lt;/h3&gt;

&lt;p&gt;The ending is where the game becomes more than a life sim.&lt;/p&gt;

&lt;p&gt;I built a reference layer of climate and ecological context so the final summary could translate numbers into human-scale consequences. Instead of saying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“you emitted X tons of CO2,”&lt;/li&gt;
&lt;li&gt;“you used Y litres of water,”&lt;/li&gt;
&lt;li&gt;“you cleared Z hectares of forest,”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the final panel turns those into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;heat in a specific region,&lt;/li&gt;
&lt;li&gt;thirst for a specific community,&lt;/li&gt;
&lt;li&gt;forest loss tied to named ecosystems,&lt;/li&gt;
&lt;li&gt;and indirect deaths grounded in statistical reality.&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.amazonaws.com%2Fuploads%2Farticles%2Fllmt73401g6x2ga7s404.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%2Fllmt73401g6x2ga7s404.png" alt=" " width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That made the ending much more emotionally readable. The player doesn’t just see impact; they feel scale, place, and consequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backboard integration
&lt;/h3&gt;

&lt;p&gt;Backboard is at the heart of Earth Impact’s generative experience. It’s not just used for origin creation, scenario generation, chapter summaries, and the final reflection—it’s also the backbone for persistent memory throughout gameplay.&lt;/p&gt;

&lt;p&gt;Unlike typical generative AI setups that invent facts from scratch, Backboard is always provided with structured state, impact data, and tightly scoped prompts. This keeps the narrative grounded and consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Backboard?&lt;/strong&gt;&lt;br&gt;
Backboard’s real strength is in memory storage and continuity. By leveraging its robust memory capabilities, Earth Impact maintains narrative coherence across sessions and player choices. The AI can reference past events and decisions, ensuring that the story evolves in a way that feels meaningful and connected.&lt;/p&gt;

&lt;p&gt;Game logic decides the facts&lt;br&gt;
Backboard remembers and tells the story&lt;br&gt;
This approach lets Backboard do what it does best: shape the tone and phrasing of the narrative, while the core game logic determines what actually happens.&lt;/p&gt;

&lt;p&gt;In short: Earth Impact decides what happens. Backboard helps remember—and tell—the story.&lt;/p&gt;

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

&lt;p&gt;This project ended up being less about “building a climate game” and more about translating abstract data into something emotionally legible.&lt;/p&gt;

&lt;p&gt;A few things stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;numbers alone are easy to ignore,&lt;/li&gt;
&lt;li&gt;place matters more than scale,&lt;/li&gt;
&lt;li&gt;specific human consequences land harder than generic comparisons,&lt;/li&gt;
&lt;li&gt;and pacing matters just as much as writing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest challenge was finding the balance between being truthful and being readable. I didn’t want the ending to feel like a lecture or a guilt trip. I wanted it to feel like a quiet final reveal.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Backboard&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;Earth Impact is about the gap between how a decision feels in the moment and what it costs over a lifetime.&lt;/p&gt;

&lt;p&gt;At first, it’s just a game about choices.&lt;/p&gt;

&lt;p&gt;At the end, it’s a game about consequences.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>backboard</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
