<?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: Tanay</title>
    <description>The latest articles on DEV Community by Tanay (@tanay_singh_1005).</description>
    <link>https://dev.to/tanay_singh_1005</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%2F4158034%2F440d8d04-4878-4135-8bb5-798a615ff6a6.jpg</url>
      <title>DEV Community: Tanay</title>
      <link>https://dev.to/tanay_singh_1005</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanay_singh_1005"/>
    <language>en</language>
    <item>
      <title>A lazy day is just packet loss. So my step goal backs off like TCP.</title>
      <dc:creator>Tanay</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:08:22 +0000</pubDate>
      <link>https://dev.to/tanay_singh_1005/a-lazy-day-is-just-packet-loss-so-my-step-goal-backs-off-like-tcp-4ogc</link>
      <guid>https://dev.to/tanay_singh_1005/a-lazy-day-is-just-packet-loss-so-my-step-goal-backs-off-like-tcp-4ogc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before writing this post I exported my own last 90 days of steps. I reached 10,000 on &lt;strong&gt;none&lt;/strong&gt; of them. Not&lt;br&gt;
one day, in a stretch where I logged 43 workouts in Hevy. Every fitness app I have used still opens with the&lt;br&gt;
same number, and a goal you never reach is not a goal. It is a reason to stop opening the app.&lt;/p&gt;

&lt;p&gt;I work on message queues for a living, so I spend my days around a very old idea: when you do not know how&lt;br&gt;
much a network can take, &lt;strong&gt;start slow, speed up while things go well, and back off when something is lost.&lt;/strong&gt;&lt;br&gt;
That is TCP congestion control. Then I remembered that a monster from a game I love has an ability with the&lt;br&gt;
same name, &lt;strong&gt;Slow Start&lt;/strong&gt;: it halves its stats for its first five turns, then never stops.&lt;/p&gt;

&lt;p&gt;So I built a fitness game for lazy people where your daily goal works like TCP, an open model keeps it&lt;br&gt;
honest, and a sleepy stone giant wakes up as you walk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow Start&lt;/strong&gt; sets your step goal like TCP: start small (1,500, or your own level if you import your
history), double after a win, climb steadily near your level, halve after a miss, &lt;strong&gt;never to zero&lt;/strong&gt;.
TabPFN v2, an open tabular model, caps the goal at a level you reach about 7 days in 10. You never set a
goal yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The game:&lt;/strong&gt; every day a wild creature blocks the path. Its health is your goal, every step you log is a
hit, and caught creatures follow your giant, &lt;strong&gt;Slumbrock&lt;/strong&gt;, home. All pixel art is original and drawn in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honest result:&lt;/strong&gt; on held-out days from 33 real people, a fixed 10,000 steps was met on &lt;strong&gt;34.6%&lt;/strong&gt; of
days. My first controller hit &lt;strong&gt;86.6%&lt;/strong&gt;, but only by asking for half of what people usually walk. Tuned on
a validation window, Slow Start meets &lt;strong&gt;71.1%&lt;/strong&gt; of days at &lt;strong&gt;74%&lt;/strong&gt; of a usual day, which &lt;strong&gt;ties&lt;/strong&gt; a plain
"your own 30th percentile" rule (69.7%). On my own 90 days: &lt;strong&gt;71.1%&lt;/strong&gt; against &lt;strong&gt;0%&lt;/strong&gt; for 10,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open, end to end:&lt;/strong&gt; TabPFN v2 (Apache 2.0) and Gemma 3 4B run on a server I control. Nothing goes to a
closed AI service. Web, Android APK and source are all public.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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%2Fdcflfpfazdaji9gt3lxu.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%2Fdcflfpfazdaji9gt3lxu.gif" alt="The arena: a Leafwing blocks the path, Slumbrock stomps for 1,500 steps, and the creature is caught" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://slowstart.onrender.com" rel="noopener noreferrer"&gt;Slow Start&lt;/a&gt;&lt;/strong&gt; is a web game and an Android app with three ideas inside:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A goal you can win, that moves with you.&lt;/strong&gt; The step controller is TCP's congestion window. A second
limit, TabPFN's prediction of tomorrow, plays the receiver window. Your goal is the lower of the two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reason to come back.&lt;/strong&gt; Slumbrock, a stone giant asleep for three hundred years, wakes through four
stages as you earn XP. His Speed and Power both start at half, like his namesake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No decisions.&lt;/strong&gt; The goal arrives every morning. If you lift, the next weights and reps arrive too.
Walking alone is a full way to play, and a sick day is excused, not punished.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;th&gt;Slow Start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Congestion window, grown by the sender&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Window:&lt;/strong&gt; the controller's goal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receiver window, advertised by the receiver&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cap:&lt;/strong&gt; TabPFN's 30th percentile of tomorrow's steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective window = min(cwnd, rwnd)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Goal&lt;/strong&gt; = min(window, cap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow start: double per round trip&lt;/td&gt;
&lt;td&gt;Double the goal after a won day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Congestion avoidance: add one per round trip&lt;/td&gt;
&lt;td&gt;Add 1,500 steps per won day once near your level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loss: halve, set ssthresh&lt;/td&gt;
&lt;td&gt;Missed day: halve the window, remember the level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached metrics for a host seen before&lt;/td&gt;
&lt;td&gt;Imported history: start from your own level, not 1,500&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%2Fhyk4dm43b4zpt4gppydj.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%2Fhyk4dm43b4zpt4gppydj.png" alt="The landing page as you scroll: Slumbrock asleep at night, drowsy at dawn, awake at noon, roaring at dusk" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Play it:&lt;/strong&gt; &lt;a href="https://slowstart.onrender.com" rel="noopener noreferrer"&gt;https://slowstart.onrender.com&lt;/a&gt;. No account. Scroll the first screen slowly and watch him wake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the demo:&lt;/strong&gt; &lt;a href="https://slowstart.onrender.com/play.html?demo=1" rel="noopener noreferrer"&gt;one real person's 40 days&lt;/a&gt; from the
public dataset, one tap per day. You can see the goal double, get capped, halve after a miss and climb back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The field guide:&lt;/strong&gt; &lt;a href="https://slowstart.onrender.com/guide" rel="noopener noreferrer"&gt;every creature&lt;/a&gt;, where it lives, how to catch it,
and trivia nobody asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bring your own steps:&lt;/strong&gt; &lt;a href="https://slowstart.onrender.com/export" rel="noopener noreferrer"&gt;the export guide&lt;/a&gt; walks through Google
Fit, a Fitbit or Pixel Watch, Hevy or any CSV, and the import sits right on the game's Today screen. If your
file comes out wrong, the app links to a GitHub issue form pre-filled with what it saw, and the case
becomes a rule in the cleaner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; the &lt;a href="https://github.com/Tanay3484/slow-start/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; is the same build wrapped
with Capacitor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things to try: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tap Slumbrock and the creature. &lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Why this number?&lt;/strong&gt; under the goal.&lt;/li&gt;
&lt;li&gt; On the Talk tab, tell Slumbrock how your day went.&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%2Fue4cdu6vnqc2plcu9j1a.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%2Fue4cdu6vnqc2plcu9j1a.png" alt="The game on a laptop: the arena pinned on the left, the goal and steps on the right" width="800" height="500"&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%2F4p8y9uankotc5s53bwq1.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%2F4p8y9uankotc5s53bwq1.png" alt="The game on a phone" width="780" height="1688"&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%2Flclntxd8il09izc78i6g.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%2Flclntxd8il09izc78i6g.png" alt="The field guide: every creature, where it lives, how to catch it, and trivia" width="800" height="500"&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/Tanay3484" rel="noopener noreferrer"&gt;
        Tanay3484
      &lt;/a&gt; / &lt;a href="https://github.com/Tanay3484/slow-start" rel="noopener noreferrer"&gt;
        slow-start
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Slow Start: TCP-style goals for lazy people, with a pixel-art giant to look after (DEV Hacktoberfest 2026 Week 3)
    &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;Slow Start&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A fitness game for lazy people. Your daily step goal and your next workout are set the way TCP sets how
fast a connection may send: &lt;strong&gt;start small, double after each success, grow slowly once you're near your
level, and halve after a miss, never to zero.&lt;/strong&gt; A second limit, TabPFN's prediction of what you'll
probably manage tomorrow, stops it asking for too much on a day the history says will be a low one.&lt;/p&gt;
&lt;p&gt;You look after &lt;strong&gt;Slumbrock&lt;/strong&gt;, a pixel-art stone giant that wakes up as you keep going. The idea comes from
a monster whose ability, also called Slow Start, halves its stats for its first five turns. The creature
art and names here are original; nothing is taken from Pokémon or Nintendo.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest 2026 Open-Source AI Challenge, Week 3: &lt;em&gt;Slow and Steady&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://slowstart.onrender.com" rel="nofollow noopener noreferrer"&gt;https://slowstart.onrender.com&lt;/a&gt; (the game is at &lt;code&gt;/play&lt;/code&gt;…&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/Tanay3484/slow-start" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;backend/&lt;/code&gt; is Python (FastAPI, TabPFN v2, Gemma through Ollama). &lt;code&gt;app/&lt;/code&gt; is TypeScript with no framework: a&lt;br&gt;
small game engine on a 240 × 135 canvas, wrapped as an Android app with Capacitor. Every number in this post&lt;br&gt;
comes from a script in &lt;code&gt;scripts/&lt;/code&gt; and a file in &lt;code&gt;results/&lt;/code&gt;. MIT licensed.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Code decides, the models advise
&lt;/h3&gt;

&lt;p&gt;The rule I kept from my earlier weeks: &lt;strong&gt;anything that sets a number is plain, tested code.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The controller&lt;/strong&gt; is a pure function of your history. It replays every day, so the same history always
gives the same goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN v2&lt;/strong&gt; gets a few features (the last three days, the week's mean and minimum, the fortnight's median,
the weekday) and predicts a distribution for tomorrow. The cap is its 30th percentile. It is fitted on your
own earlier days plus a small public prior, so a new user is not starting from nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; reads a one-line check-in ("leg day was brutal") and answers in Slumbrock's voice. Code, not
the model, decides whether the day is excused (a word list with a negation check) and picks the recovery
tip from a curated list, so nothing medical is ever made up.&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%2Fxxckrcyy1d57dx28vofe.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%2Fxxckrcyy1d57dx28vofe.png" alt="A check-in: " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Render's two CPUs Gemma 4B takes about fifteen seconds to answer. Slumbrock is a sleepy rock, so&lt;br&gt;
the button says he is thinking, and if a reply comes back malformed a template line takes over.&lt;/p&gt;

&lt;h3&gt;
  
  
  The game engine, all in code
&lt;/h3&gt;

&lt;p&gt;There are no image files for the creatures. &lt;code&gt;scripts/draw_sprites.py&lt;/code&gt; draws them from shaded shapes: spheres&lt;br&gt;
and bevelled blocks lit from the top left, a four-tone ramp with a little dithering, outlines in the darkest&lt;br&gt;
tone of each colour. Each creature has idle frames (wings flap, Kettlebub lifts its little bar), a blink and&lt;br&gt;
a hit face. The arena is about 900 lines of TypeScript: an entrance for each new creature, an attack with a&lt;br&gt;
damage number for every step you log, a capture after which the creature follows you, an evolution burst,&lt;br&gt;
and weather and time of day per place. Text is drawn on a second, full-resolution canvas layered on top,&lt;br&gt;
because pixel-font text scaled up from 240 × 135 was an unreadable blur.&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%2Fg1f2vxj2yr3f3pu7tkhe.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%2Fg1f2vxj2yr3f3pu7tkhe.gif" alt="A day at the waterfront: a hit, the capture, and Slumbrock waking up a stage" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The evidence
&lt;/h3&gt;

&lt;p&gt;The data is the public &lt;strong&gt;Fitbit Fitness Tracker Data&lt;/strong&gt; on Kaggle (CC0): daily steps from 35 people in&lt;br&gt;
spring 2016. Of 1,373 person-days, 133 have exactly zero steps. I treat those as "watch not worn", not&lt;br&gt;
scored and not fed to any policy.&lt;/p&gt;

&lt;p&gt;Every policy sets each day's goal seeing only the days before it, then the day is revealed. TabPFN is fitted&lt;br&gt;
per calendar day on everyone's earlier days only. A test proves no policy is ever shown the day it is&lt;br&gt;
setting a goal for. Two windows: a &lt;strong&gt;validation&lt;/strong&gt; window (the 14 days before 2016-04-28) for choices, and a&lt;br&gt;
&lt;strong&gt;final&lt;/strong&gt; window (the last 14 days) run once at the end.&lt;/p&gt;

&lt;p&gt;Two numbers per policy, because either alone can be gamed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal met:&lt;/strong&gt; the share of days the person reached the goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asks for:&lt;/strong&gt; the goal as a share of the person's usual day. A policy that wins by setting tiny goals shows
up here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final window, 32 people, 350 person-days:&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;Goal&lt;/th&gt;
&lt;th&gt;Goal met&lt;/th&gt;
&lt;th&gt;Asks for&lt;/th&gt;
&lt;th&gt;Met vs plain 30th percentile (95% CI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed 10,000&lt;/td&gt;
&lt;td&gt;34.6%&lt;/td&gt;
&lt;td&gt;148%&lt;/td&gt;
&lt;td&gt;−35.1 pts (−48.3 to −21.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your 28-day median&lt;/td&gt;
&lt;td&gt;46.3%&lt;/td&gt;
&lt;td&gt;101%&lt;/td&gt;
&lt;td&gt;−23.4 pts (−28.7 to −18.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your 28-day 30th percentile&lt;/td&gt;
&lt;td&gt;69.7%&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;(baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN 30th percentile alone&lt;/td&gt;
&lt;td&gt;66.6%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;−3.1 pts (−9.1 to +2.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Slow Start&lt;/strong&gt; (controller + TabPFN cap)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.4 pts (−4.8 to +7.4)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The intervals resample &lt;strong&gt;people&lt;/strong&gt;, not days, because one person's days are not independent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What went wrong, and what I did about it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My first controller won by cheating.&lt;/strong&gt; With a climb of 250 steps per win, it met &lt;strong&gt;86.6%&lt;/strong&gt; of goals, which
looked great until the guardrail column said it was asking for &lt;strong&gt;50%&lt;/strong&gt; of a usual day. The TCP sawtooth was
the reason: after every halving, climbing back 250 steps a day takes weeks, so the goal sat at half your
level most of the time. I swept the climb (250 to 1,500) on the &lt;strong&gt;validation&lt;/strong&gt; window under a rule I wrote
down before running it: the largest step that still meets at least 70% of days. That was 1,500, the top of
the range I tried, so a larger one might pass too. Then I ran it once on the final window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN did not beat a percentile.&lt;/strong&gt; On the validation window TabPFN's cap was ahead of the plain 30th
percentile by 3.9 points; on the final window it was behind by 3.1. The sign flips, so on 33 people the two
are a tie. I also tried a second feature set (everything relative to the person's own median) and picked
between them on validation by a rule written before I saw the second result. It made no difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma answered "rest".&lt;/strong&gt; One word. The prompt asked for one short sentence, and the small model took that
literally. Now Gemma writes the reaction with a few examples of Slumbrock's voice, code adds the tip, and
one reply in three gets a rock joke. Gemma 4B once ended a reply with a curly quote and eighteen &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt;
tags; replies are now cut at a stray quote, brace or tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Walked to work and back" got a tip for a sore back.&lt;/strong&gt; Words like "back" and "head" now only count as body
parts in context ("my back", "back is stiff").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own statistics function would have crashed on real data.&lt;/strong&gt; The bootstrap built a list of lists where
it needed a flat list of days. The tests passed because every test person had the same number of days. A
new test with uneven people caught it, and the fix was one word (&lt;code&gt;extend&lt;/code&gt;, not &lt;code&gt;append&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not tested:&lt;/strong&gt; whether these goals change what people do. A backtest replays what people did &lt;em&gt;without&lt;/em&gt; the
app. The public dataset is short (about two months) and from 2016; my own is one person.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Then I ran it on myself
&lt;/h3&gt;

&lt;p&gt;The public set is 33 strangers from 2016. So I exported my own history: Google Fit steps since 2024 (1,028&lt;br&gt;
days) and my Hevy log (633 workouts since 2022). My own data broke the app four times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The importer refused my steps file.&lt;/strong&gt; Google Fit calls the column &lt;code&gt;Step count&lt;/code&gt;, not &lt;code&gt;steps&lt;/code&gt;. The Takeout
folder also has a raw stream per device, and adding them up would have counted every step twice, because
my phone and my watch overlap. The importer now reads the right file and refuses the wrong ones with a
message naming the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three years of history broke the goal.&lt;/strong&gt; The app sent every stored day to the server, which refuses more
than 800. It now sends the last 365.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most of my steps are a phone in a pocket.&lt;/strong&gt; The watch reached Google Fit on only 42 of those days, so a
day with the phone left on a desk counts as a few hundred steps. Under 500 now counts as "not carried":
rest, not a miss. I wrote that line down before running the backtest, and every upload gets the same
cleaning now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then the import changed nothing.&lt;/strong&gt; I loaded all 1,028 days into the live game and it still gave me
1,500. Imported days carry no goal, so the controller had nothing to replay and started from scratch, as
if I had never walked. Now, like TCP reusing what it learned about a host it has seen before, the
controller starts from your own 30th percentile of the last 28 imported days. On my history that is 4,100,
just under TabPFN's cap of 4,150.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My last 90 days, held out (TabPFN v2 fitted 118 times, median 5.0 s each):&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;Goal&lt;/th&gt;
&lt;th&gt;Goal met&lt;/th&gt;
&lt;th&gt;Asks for&lt;/th&gt;
&lt;th&gt;Met vs plain 30th percentile (95% CI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed 10,000&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;301%&lt;/td&gt;
&lt;td&gt;−68.9 pts (−75.6 to −61.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My 28-day median&lt;/td&gt;
&lt;td&gt;52.2%&lt;/td&gt;
&lt;td&gt;116%&lt;/td&gt;
&lt;td&gt;−16.7 pts (−24.4 to −9.9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My 28-day 30th percentile&lt;/td&gt;
&lt;td&gt;68.9%&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;(baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN 30th percentile alone&lt;/td&gt;
&lt;td&gt;68.9%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;+0.0 pts (−6.7 to +5.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slow Start&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+2.2 pts (−5.5 to +8.8)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With one person the interval resamples weeks, not people. &lt;strong&gt;A fixed 10,000 was met on none of my 90 days.&lt;/strong&gt;&lt;br&gt;
Slow Start and the plain percentile tie again, as they did on 33 strangers. On my data TabPFN alone met&lt;br&gt;
exactly as often as the percentile while asking for a little more (89% of a usual day against 82%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workouts broke my exercise controller.&lt;/strong&gt; It was built for a beginner's programme: three sets of eight&lt;br&gt;
at one weight, plus 2.5 kg after a good session. I do not train like that. I work up to one heavy top set&lt;br&gt;
(a deadlift day is 100 × 4, 140 × 3, 140 × 2, 100 × 6), so "repeat last session" was met &lt;strong&gt;0%&lt;/strong&gt; of the time,&lt;br&gt;
and the controller "won" by cutting the weight to &lt;strong&gt;12%&lt;/strong&gt; of what I lift. The guardrail caught it, as it did&lt;br&gt;
for steps. I changed the spec to score the top set instead, and reran:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scored on the top set (exploratory)&lt;/th&gt;
&lt;th&gt;Target met&lt;/th&gt;
&lt;th&gt;Asks for vs last time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repeat last top set&lt;/td&gt;
&lt;td&gt;31.4%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a step every session (2.5 kg, or one rep)&lt;/td&gt;
&lt;td&gt;8.6%&lt;/td&gt;
&lt;td&gt;106%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controller&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That design came &lt;em&gt;after&lt;/em&gt; seeing my log, on the same log, so it is exploratory, not a held-out result. It is&lt;br&gt;
35 sessions across six lifts.&lt;/p&gt;

&lt;p&gt;One more thing only real use found: anyone who had watched the demo once was stuck in it. The Play button&lt;br&gt;
kept reopening the demo, and the way out was hidden in a settings tab. I only noticed because I tried to&lt;br&gt;
start for real with my own data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built with an agent, with four functions by hand
&lt;/h3&gt;

&lt;p&gt;I built this with Claude Code under a spec-first workflow: brief, requirements, design and tasks, each&lt;br&gt;
reviewed before code. Four core functions were set aside to write by hand:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Written by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H1&lt;/td&gt;
&lt;td&gt;The step controller&lt;/td&gt;
&lt;td&gt;me, from a skeleton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H3&lt;/td&gt;
&lt;td&gt;The evaluation statistics (goal level, people-resampled intervals)&lt;/td&gt;
&lt;td&gt;me, from a skeleton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2&lt;/td&gt;
&lt;td&gt;The exercise controller&lt;/td&gt;
&lt;td&gt;Claude, when I ran out of time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H4&lt;/td&gt;
&lt;td&gt;The XP and stage rule&lt;/td&gt;
&lt;td&gt;Claude, when I ran out of time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health data stays with you.&lt;/strong&gt; Your steps, workouts and check-ins go only to a server I run, which
processes each request in memory and keeps nothing. Everything is saved on your device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The open version was the only usable one.&lt;/strong&gt; TabPFN 2.5 and later are licensed for non-commercial use only.
TabPFN v2 is Apache 2.0, so the app pins v2 and bakes the weights into the image. A predictor I can
download, run on a plain CPU and check is one I can actually ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No price per call.&lt;/strong&gt; A goal costs about five seconds of CPU (measured in the container with no network,
1 CPU and 2 GB). There is no per-request bill to make the "lazy" goal expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable and checkable.&lt;/strong&gt; The predictor and the language model sit behind small interfaces, and the
data, the weights and the evaluation script are all public, so anyone can rerun the tables above.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;You make &lt;strong&gt;zero decisions&lt;/strong&gt;. The app sets one step goal a day, and one target per exercise per session if&lt;br&gt;
you lift.&lt;/p&gt;

&lt;p&gt;In my last 90 days on Hevy I logged 43 workouts: 327 times I picked an exercise and a weight, and 685&lt;br&gt;
working sets. Slow Start plans only the main lifts, so it takes about one decision in ten off me (35 of&lt;br&gt;
them); the machines and cables are still mine. For steps it is every decision: one number a day, won 7&lt;br&gt;
days in 10 on my data, where a fixed 10,000 was won on none. Lower goals are easier to win, which is exactly&lt;br&gt;
why every table also shows how much each goal asks for.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Best Use of TabPFN
&lt;/h3&gt;

&lt;p&gt;TabPFN v2 is the &lt;strong&gt;receiver window&lt;/strong&gt;: the half of the TCP analogy that knows what you can take tomorrow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it does on every goal.&lt;/strong&gt; The game asks for today's goal, and TabPFN gets a handful of features
(the last three days, the week's mean and minimum, the fortnight's median, the weekday). It is fitted in
context on your own earlier days plus 400 rows from a public prior, so someone with a week of history is
not starting from nothing. It returns quantiles, not a point. The cap is the &lt;strong&gt;30th percentile&lt;/strong&gt;, a level
you reach about 7 days in 10, and the goal is &lt;code&gt;min(controller window, cap)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why TabPFN and not a model I train.&lt;/strong&gt; There is nothing to train per person. One person has a few weeks of
rows, far too few to fit a model, but TabPFN does in-context learning on exactly that kind of small table
and gives calibrated quantiles out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why v2.&lt;/strong&gt; It is Apache 2.0. TabPFN 2.5 and later are non-commercial, so the app pins v2 and bakes the
weights into the Docker image at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it runs.&lt;/strong&gt; It runs on CPU with four estimators. I measured about 5 seconds per goal, 598 MB at peak
and 2.9 s to load, in a container with no network, 1 CPU and 2 GB. On Render a goal takes 5 to 6.5 s.
If it fails or times out, the cap falls back to 80% of your recent median, and the app says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How I tested it.&lt;/strong&gt; Walk-forward on 33 people, with TabPFN refitted per calendar day on earlier days
only. Two feature sets were picked on a validation window by a rule written before the second result
came in. Against a plain "your own 30th percentile" it is &lt;strong&gt;a tie&lt;/strong&gt;, and I report it as one. On my own
last 90 days, with the product's own code, it ran 118 fits at a median of 5.0 s each. It met exactly as
often as the percentile (68.9%) while asking for a little more of a usual day (89% against 82%).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Gemma 3 4B is &lt;strong&gt;Slumbrock's voice&lt;/strong&gt;. You tell him how the day went ("upper body is really sore from weighted&lt;br&gt;
pull ups yesterday"), and he answers in character.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted.&lt;/strong&gt; It runs on Ollama, on Render, with the model baked into the image so a restart never
re-downloads it. No check-in ever leaves a server I run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What Gemma decides, and what it does not.&lt;/strong&gt; Under a JSON schema Gemma returns three things: an energy
level, the body parts mentioned and one reply line. Code checks each of them:

&lt;ul&gt;
&lt;li&gt;A body part is kept only if it is a known one &lt;strong&gt;and&lt;/strong&gt; the person actually wrote it, so Gemma cannot
invent a sore knee.&lt;/li&gt;
&lt;li&gt;The reply must be 4 to 40 words with &lt;strong&gt;no digits&lt;/strong&gt;, so it can never state a number, a dose or a goal.&lt;/li&gt;
&lt;li&gt;Whether the day is excused comes from a word list with a negation check ("not sick" is not sick), not
from the model.&lt;/li&gt;
&lt;li&gt;The recovery tip comes from a curated list chosen by code, so nothing medical is made up.&lt;/li&gt;
&lt;li&gt;The rock jokes ("You are, to be clear, talking to a rock.") are added by code, about one reply in three,
never on a sick day.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taming a small model.&lt;/strong&gt; Gemma's first answer was one word: "rest". A few examples of Slumbrock's voice
in the prompt fixed that. Gemma 4B once ended a reply with a curly quote and eighteen &lt;code&gt;&amp;lt;/div&amp;gt;&lt;/code&gt; tags, so
output is capped (&lt;code&gt;num_predict&lt;/code&gt; 240, &lt;code&gt;maxLength&lt;/code&gt; 220) and cut at the first stray quote, brace or tag.
Anything that fails the checks falls back to a template line, so a check-in never breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of thinking.&lt;/strong&gt; About 15 seconds a reply on Render's 2 CPUs. I tried 1B on a smaller plan and its
replies were noticeably flatter, so the 4B earned its plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Best Use of Render
&lt;/h3&gt;

&lt;p&gt;One Blueprint (&lt;code&gt;render.yaml&lt;/code&gt;) brings up the whole product: two services, a private link between them and&lt;br&gt;
a health check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;slowstart&lt;/code&gt;, a web service (Docker, 1 CPU / 2 GB).&lt;/strong&gt; One container serves the FastAPI API, the built
game and site, and TabPFN v2. A multi-stage Dockerfile builds the TypeScript app with Node, then bakes the
TabPFN weights into the Python image, so the service starts with no downloads. 1c-2g is the smallest
plan that fits: TabPFN with torch peaked at 598 MB, too much for a 512 MB plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;slowstart-ollama&lt;/code&gt;, a private service (Docker, 2 CPUs / 4 GB) with Gemma 3 4B baked in.&lt;/strong&gt; It has no
public URL. The web service reaches it over Render's private network, via &lt;code&gt;fromService … hostport&lt;/code&gt;
in the Blueprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lessons the deploy taught me, all written into the Blueprint.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The private service needs &lt;code&gt;PORT=11434&lt;/code&gt;, or the hostport defaults to 10000 and every model call is
refused.&lt;/li&gt;
&lt;li&gt;Inside the container llama.cpp sees the whole host, not the plan, so &lt;code&gt;LLAMA_ARG_THREADS=2&lt;/code&gt;,
&lt;code&gt;LLAMA_ARG_CACHE_RAM=0&lt;/code&gt; and &lt;code&gt;LLAMA_ARG_NO_REPACK=1&lt;/code&gt; keep it inside 4 GB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OLLAMA_KEEP_ALIVE=-1&lt;/code&gt; keeps the model loaded between check-ins.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping by pull request.&lt;/strong&gt; Both services deploy from &lt;code&gt;main&lt;/code&gt; with auto-deploy on, behind
&lt;code&gt;/api/health&lt;/code&gt;. Every fix in this post was a merged PR that was live a few minutes later, including the
ones my own data forced on deploy night.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless on purpose.&lt;/strong&gt; The server keeps nothing: every request carries the history it needs, and your
data lives on your device. A restart or a redeploy loses no one's progress.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Holdfast: a webhook safety net where Gemma explains what broke and code decides what happens</title>
      <dc:creator>Tanay</dc:creator>
      <pubDate>Sun, 18 Oct 2026 13:06:25 +0000</pubDate>
      <link>https://dev.to/tanay_singh_1005/holdfast-a-webhook-safety-net-where-gemma-explains-what-broke-and-code-decides-what-happens-4f1c</link>
      <guid>https://dev.to/tanay_singh_1005/holdfast-a-webhook-safety-net-where-gemma-explains-what-broke-and-code-decides-what-happens-4f1c</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;My day job is message queues in containers. The first rule there is boring and absolute: &lt;strong&gt;a message isn't acknowledged until it's safely stored.&lt;/strong&gt; Lose power a millisecond later and it's still there.&lt;/p&gt;

&lt;p&gt;Webhooks don't work like that. Stripe or GitHub sends your app a message, and if the app is down, slow, or chokes on a field that was renamed overnight, the message is usually just gone. The person who finds out is the shop owner, two days later, asking why three payments never showed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Holdfast&lt;/strong&gt; is that queue rule, turned into a product for the people who get that phone call: freelance developers and small agencies who look after integrations for a handful of clients. It sits between the provider and the app and:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Saves every webhook to disk before saying "got it".&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks the signature&lt;/strong&gt;, and keeps forged or replayed messages aside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivers patiently&lt;/strong&gt;: retries when the app is down, holds when a retry won't help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explains what broke&lt;/strong&gt; in two voices: a plain sentence the client can read, and a precise fix for the developer. Gemma writes both; code checks every word.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sends held messages again, never twice.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Who pays:&lt;/strong&gt; the freelancer who maintains "the Stripe thing" for five small clients. One lost payment webhook is a lost order and an awkward conversation. Holdfast turns that into a note they can forward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kabir's ₹1050 payment for Ginger chai, 500 g couldn't reach Chai &amp;amp; Co. because the sender changed how it sends customer information. Holdfast is keeping the message safe, and it can be sent again once the app is updated to expect the new format.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a real answer from the deployed site, written by Gemma 3 4B running on Render.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://holdfast-o9ba.onrender.com" rel="noopener noreferrer"&gt;https://holdfast-o9ba.onrender.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It has two modes, switchable from the header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo&lt;/strong&gt; (&lt;code&gt;/demo&lt;/code&gt;, no sign-up): Chai &amp;amp; Co., a pretend tea shop with made-up customers. Seven buttons break it on purpose: the shop goes down, a field is renamed, a payment arrives twice, a signature is forged, an old payment is replayed. The relay, retries, checks and explanations are the real engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live&lt;/strong&gt; (&lt;code&gt;/live&lt;/code&gt;): sign in with GitHub, pick one of your repositories, and Holdfast adds itself as the webhook. GitHub's test ping arrives within seconds. If you don't have an app, deliver to Holdfast's built-in &lt;strong&gt;test app&lt;/strong&gt; and switch it to "Server down" or "Turns messages away" to watch a real webhook get held, explained and resent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Landing Page:&lt;br&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%2Fds2gm7b56he10yyynxhs.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%2Fds2gm7b56he10yyynxhs.png" alt="Landing Page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo:&lt;br&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%2Feapv7oyxqccagnf0fkcy.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%2Feapv7oyxqccagnf0fkcy.gif" alt="Demo GIF" width="799" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Event Gemma:&lt;br&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%2Fitz2tizjdk3om8edpugt.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%2Fitz2tizjdk3om8edpugt.png" alt="Event Gemma" width="800" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One honest note: Gemma runs on two CPU cores, so an explanation takes up to a minute and a half (48 s and 83 s in my last two live runs). The page shows the built-in rules' answer instantly and swaps in Gemma's when it's ready.&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/Tanay3484" rel="noopener noreferrer"&gt;
        Tanay3484
      &lt;/a&gt; / &lt;a href="https://github.com/Tanay3484/holdfast" rel="noopener noreferrer"&gt;
        holdfast
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A safety net for webhooks: stores every event, holds what your app fails on, explains why with an open-weight model, and resends safely.
    &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;Holdfast&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A safety net for webhooks. Holdfast sits between a provider (Stripe, GitHub) and your app. It stores every
event before acknowledging it, holds the ones your app fails on, explains why in plain words with an
open-weight model, and resends them safely once the problem is fixed.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12" rel="nofollow"&gt;Hacktoberfest 2026 Open-Source AI Challenge, Week 2: Side Hustle&lt;/a&gt;.&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;provider ──▶ /in/{route} ──▶ check signature ──▶ save to disk ──▶ "got it"
                                                      │
                     deliver to your app ◀────────────┘
                       ├─ accepted      → done
                       ├─ worth a retry → wait a little longer each time
                       └─ won't work    → hold it safely
                                             │
                     code collects the facts ┘  (signature, every try, what changed since the last success)
                     Gemma names the cause from a fixed list and explains it; code checks every word
                     resend: never twice, never anything that failed its signature check
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code decides, the model explains.&lt;/strong&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/Tanay3484/holdfast" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Go, about 5,900 lines plus 1,550 lines of tests (59 tests). Every feature has a spec in &lt;a href="https://github.com/Tanay3484/holdfast/tree/main/specs" rel="noopener noreferrer"&gt;&lt;code&gt;specs/&lt;/code&gt;&lt;/a&gt; with numbered requirements.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Open-weight model:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3 4B&lt;/a&gt;, served by &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; in a private service next to the app. &lt;strong&gt;Stack:&lt;/strong&gt; Go standard library, SQLite (pure-Go driver), server-rendered HTML, a little vanilla JS. &lt;strong&gt;Hosting:&lt;/strong&gt; a single Render Blueprint (more below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Code decides, the model explains
&lt;/h3&gt;

&lt;p&gt;The rule I held to: &lt;strong&gt;the model never decides what happens to a message.&lt;/strong&gt; Retries, holds, duplicate detection and resends are plain, tested Go. Gemma gets the facts code collected (signature result, every delivery attempt, the app's error, what changed in the payload) and does two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pick a cause from a &lt;strong&gt;fixed list of eight&lt;/strong&gt;, under a JSON schema, and&lt;/li&gt;
&lt;li&gt;write the explanation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then code checks what it wrote. The client summary is rejected and replaced with the built-in rules' wording if it contains jargon ("HTTP", "payload", "endpoint", an error number) or &lt;strong&gt;promises something nobody did&lt;/strong&gt;. Titles always come from code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that taught me the most
&lt;/h3&gt;

&lt;p&gt;First version, renamed-field scenario. Gemma got the cause right, but the client summary said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A payment notification failed to reach the Chai &amp;amp; Co. system. The notification is being kept safe &lt;strong&gt;and reviewed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody was reviewing anything. Worse, the developer note said the app "now requires &lt;code&gt;customer_id&lt;/code&gt;", which is &lt;strong&gt;backwards&lt;/strong&gt;: Stripe changed, the app didn't.&lt;/p&gt;

&lt;p&gt;The fix wasn't a better prompt alone. It was moving more work into code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code finds the rename.&lt;/strong&gt; A shape-diff walks both JSON bodies and spots that &lt;code&gt;data.object.customer_id&lt;/code&gt; disappeared while &lt;code&gt;data.object.customer.id&lt;/code&gt; appeared. That's one rename, with a direction. Gemma is told "the sender now sends X where it used to send Y; the app did not change" instead of a raw list of paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The summary has a required shape:&lt;/strong&gt; what happened to &lt;em&gt;this&lt;/em&gt; payment (who, how much), why in everyday words, what happens next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;Promise&lt;/code&gt; check&lt;/strong&gt; rejects "being reviewed", "will resend", "our team is…".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The developer fix must name the field that moved&lt;/strong&gt;, or the rules' fix is used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the fix, on the same scenario:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For your developer:&lt;/strong&gt; The sender now uses &lt;code&gt;data.object.customer.id&lt;/code&gt; instead of &lt;code&gt;data.object.customer_id&lt;/code&gt;. Change the app to expect &lt;code&gt;data.object.customer.id&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Model size, measured
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Result on the renamed-field case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B&lt;/td&gt;
&lt;td&gt;laptop&lt;/td&gt;
&lt;td&gt;Wrote "the app will resend the held events" (a promise) and told the &lt;em&gt;sender&lt;/em&gt; to change. The checks now catch both and fall back to the rules' answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B&lt;/td&gt;
&lt;td&gt;laptop GPU&lt;/td&gt;
&lt;td&gt;Named the customer and amount, explained the change plainly, and pointed at the exact field. About 16 s.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B&lt;/td&gt;
&lt;td&gt;Render, 2 CPUs&lt;/td&gt;
&lt;td&gt;Same quality. &lt;strong&gt;92 s&lt;/strong&gt; for one explanation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B&lt;/td&gt;
&lt;td&gt;Render, after the fixes below&lt;/td&gt;
&lt;td&gt;Two live runs, both explained by Gemma: &lt;strong&gt;48 s and 83 s&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a laptop CPU with two threads, one explanation was 740 prompt tokens read in 15 s and 115 tokens written in 14 s. That's why the page shows a rules answer first.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it: an unlock game
&lt;/h3&gt;

&lt;p&gt;I built this with Claude Code, spec first. The four core pieces started as &lt;strong&gt;stubs I had to write by hand&lt;/strong&gt;, each a "level" with known-answer checks. &lt;code&gt;go run ./cmd/unlock&lt;/code&gt; prints a progress board, and while a level was locked, every part of the website it powered showed a "WIP: write this file to reveal how this will look" tile. Writing a file literally revealed part of the site.&lt;/p&gt;

&lt;p&gt;Being honest about who wrote what (it's recorded in &lt;a href="https://github.com/Tanay3484/holdfast/blob/main/specs/001-holdfast-mvp/tasks.md" rel="noopener noreferrer"&gt;&lt;code&gt;tasks.md&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 1, signatures:&lt;/strong&gt; I wrote GitHub's HMAC check. Stripe's was started by me and finished by Claude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2, retry policy:&lt;/strong&gt; written by me (backoff, &lt;code&gt;Retry-After&lt;/code&gt;, the hold rules), polished by Claude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Levels 3 and 4, shape diff and resend plan:&lt;/strong&gt; written by Claude at my request. Level 3 was a worked example to teach me recursion, which I'd stalled on, and level 4 was the last night before the deadline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Two safety details I'm glad I didn't skip
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-side request forgery.&lt;/strong&gt; Live mode lets you type the URL Holdfast forwards to. Without care, anyone could point it at &lt;code&gt;holdfast-ollama:11434&lt;/code&gt; or a cloud metadata address and read the answers on the event page. Destinations are checked when saved &lt;em&gt;and&lt;/em&gt; every connection is checked again at dial time, so a hostname that later resolves to a private IP is still refused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates.&lt;/strong&gt; Providers deliver at least once. The resend plan sends each event once, in arrival order, never one that was already delivered, and never one whose signature failed.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data is the business's customers.&lt;/strong&gt; Payment webhooks carry names, amounts and addresses. With an open-weight model on the same private network, none of it goes to an AI company, and the freelancer can tell their client that truthfully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-message bill.&lt;/strong&gt; A small shop's integration might send hundreds of events a day. One small model on one server explains every failure for a flat cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could look inside the failure.&lt;/strong&gt; When Gemma promised a review that wasn't happening, I could see exactly what it was given, change what code tells it, and constrain its output with a JSON schema. The fix was engineering, not hoping a vendor's model changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable.&lt;/strong&gt; The model is one setting (&lt;code&gt;HF_MODEL&lt;/code&gt;). I measured 1B against 4B and chose on evidence.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Built pair-programming with &lt;strong&gt;Claude Code&lt;/strong&gt;: spec first (brief, requirements, design, tasks, each approved by me), then implementation task by task, with subagents for coaching me on my levels, writing specs, and drafting this post. The coach was told never to write my code, only give hints and review.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Gemini CLI"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building Holdfast: a webhook safety net with Gemma&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Status is approved. Set up the rest today: the frontend website, and leave certain things as "WIP: work on this file to reveal how the website will look", something like a game for me, where every file I code, I unlock a bit of the website. Obviously the coding bits are still to be done by me.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Plan: the four core pieces (signature checks, retry policy, JSON shape diff, resend plan) become documented stubs that return wip.ErrLocked. Each is a 'level' with known-answer checks in internal/unlock. The relay falls back to simpler behaviour while a level is locked, and every part of the site a locked level powers renders a WIP tile naming the file to write. &lt;code&gt;go run ./cmd/unlock&lt;/code&gt; prints the board.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;Write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;internal/verify/verify.go (stub + contract + GitHub's documented known answer)&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stub&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;GitHub&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;documented&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;go run ./cmd/unlock&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ go run ./cmd/unlock&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;HOLDFAST · UNLOCK BOARD  0 of 4 levels
[··········]  Level 1  Signature check  LOCKED
  next → open internal/verify/verify.go (H1)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Then the engine: SQLite store that commits before the relay answers 200, a delivery worker with a one-try fallback, rule-based diagnosis plus Gemma via Ollama under a JSON schema, a client note, and the website with a live demo shop (Chai &amp;amp; Co.) that visitors can break on purpose.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;fire every demo scenario at the local server, read the dashboard&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ fire every demo scenario at the local server, read the dashboard&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;3 payments held safely … What happened: the app's server was down.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;So shouldn't we first have some kind of struct present that will be needed when unmarshalling?&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;No struct: Of has to accept any JSON. Unmarshal into &lt;code&gt;any&lt;/code&gt;, then a type switch over map[string]any, []any, string, float64, bool and nil tells you what each node is. The rest is a small recursive walk that records the kind at each path.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Just write it for me so I can understand what you're trying to tell me. I will do the next one on my own.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;internal/shape/shape.go: walk(s, path, v) records the kind, then recurses int...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;internal/shape/shape.go: walk(s, path, v) records the kind, then recurses into object keys (path.key) and array items (path[]); record() marks Mixed when array items disagree; Diff compares two shapes and sorts by path&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Level 3 UNLOCKED (7/7 checks)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;It is the folder-listing idea: write down what this thing is, and if it contains other things, call walk again for each with a longer path. tasks.md records that I wrote this level at your request.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Ok just check and lmk if Function 1 is ok?&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;go run ./cmd/unlock -v&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ go run ./cmd/unlock -v&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Level 1  Signature check  4/10 checks
✓ GitHub: accepts GitHub's own documented example
✓ GitHub: rejects a body changed after signing
✓ GitHub: reports a missing header
✓ GitHub: rejects a header without sha256=&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;GitHub is correct, and decoding the hex and comparing raw bytes with hmac.Equal is better than comparing hex strings. The remaining six checks are Stripe's: same seal, but over "t.body", any v1 may match, then a freshness check.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Gemma only says: "A payment notification failed to reach the Chai &amp;amp; Co. system. The notification is being kept safe and reviewed." Is this temporary? Because come on it should say more than that.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;read the held event's page&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ read the held event's page&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;For your developer: The app is now requiring a 'customer_id' field, which was previously absent…  (backwards: the sender changed, not the app)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Not temporary: the prompt only asked for 'two short sentences, no jargon', nothing checked for promises ('reviewed'), and Gemma was handed raw paths and guessed the direction. Fix by moving work into code: detect renames (customer_id → customer.id) and tell the model the sender changed; require what happened / why / what next; a Promise check; and the developer fix must name the moved field or the rules' fix is used.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;renamed-field demo again on gemma3:4b&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ renamed-field demo again on gemma3:4b&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Nisha's ₹1050 payment … couldn't reach Chai &amp;amp; Co. because the sender changed how they send customer information. Holdfast is keeping the message safe and it can be sent again once the system is updated.
For your developer: The sender now uses data.object.customer.id instead of data.object.customer_id.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;same demo on gemma3:1b&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ same demo on gemma3:1b&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;"The app will resend the held events." / "Ensure the sender updates the customer ID field."  → guards now replace both&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I want to go 4b since the good answers is what we want. How will we host it on Render though? I want to mimic my Week 1 setup.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Same shape as Week 1: a public web service plus a private Ollama service with the model baked into its image, now on 2c-4g because gemma3:4b needs about 3.5 GB loaded. Measured on a CPU with 2 threads: 740 prompt tokens in 15 s, 115 output tokens in 14 s, so the page shows the rules' answer at once and swaps in Gemma's when it arrives.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;relay: save the rules' diagnosis first (pending), replace with the model's; r...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;relay: save the rules' diagnosis first (pending), replace with the model's; resume pending ones after a restart&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;tests pass&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Can you do Level 4 for me? And then overhaul the website since it's no longer a game. Can we have two modes, Demo and Live? Live will be for users to connect GitHub. I don't have a Stripe account, so we will be honest about it in our submission.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;internal/replay/replay.go: DedupeKey (GitHub delivery id, Stripe event id, el...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;internal/replay/replay.go: DedupeKey (GitHub delivery id, Stripe event id, else sha256 of body); Plan sorts a copy by arrival, sends each key once, never a forged or already-delivered event&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Every level unlocked. Holdfast is ready to ship.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Live mode: GitHub OAuth (admin:repo_hook only), Holdfast adds the webhook to the chosen repo, delivery to the user's app or a built-in test app that can be made to fail. Routes become dynamic; user-typed destinations go through a guard that refuses private addresses at save time and again at dial time, so nobody can point Holdfast at its own Ollama service. Tokens and webhook secrets are sealed with AES-GCM. Stripe connects by hand, and the site says so.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;go test ./internal/netguard/&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ go test ./internal/netguard/&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;ok  (refuses 127.0.0.1, 10.x, 169.254.169.254, holdfast-ollama:11434; dial-time check refuses loopback)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The GitHub login looks a bit weird. Also there's too much text on the demo and live page. We want it attractive to customers so they will pay.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The page in the screenshot was rendering with almost no CSS: the stylesheet was linked as app.css?v=dev, so the browser kept an old cached copy. Assets are now linked with a hash of their contents. Then the copy: Live becomes a headline, one line, a dark 'Continue with GitHub' button, three chips and three step cards; Demo becomes 'Break the shop. Lose nothing.' with short button labels.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;For the how it works section, can we add an animation rather than textboxes? Makes it easier for people to understand.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;A 15-second SVG/CSS loop: a payment travels Stripe → Holdfast → the shop, is saved, checked, finds the shop down, is held, explained, then sent again while a duplicate is crossed out; five captions light up in step. Headless screenshots showed it frozen until I added --disable-threaded-animation: transform and opacity animations run on the compositor, which virtual time doesn't drive.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Deployed. GitHub seems to be working fine.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;render logs (holdfast)&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ render logs (holdfast)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;WARN model unavailable; used rules err="ollama: Post \"http://holdfast-ollama:10000/api/chat\": dial tcp …:10000: connect: connection refused"&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;render logs (holdfast-ollama)&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ render logs (holdfast-ollama)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;ollama ready with gemma3:4b
Detected service running on port 11434&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Ollama is healthy on 11434, but the Blueprint's fromService hostport used Render's default port 10000. The rules answered every held event, so the site stayed usable. Fix: PORT=11434 on the private service.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;after the redeploy: send a renamed-field payment to the live site&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ after the redeploy: send a renamed-field payment to the live site&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;after 92s: Kabir's ₹1050 payment for Ginger chai, 500 g couldn't reach Chai &amp;amp; Co. because the sender changed how it sends customer information…
Explained by Gemma, checked by code.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        20 of 20 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;My first user is a real app with a real repository: &lt;strong&gt;&lt;a href="https://tasklet.onrender.com" rel="noopener noreferrer"&gt;Tasklet&lt;/a&gt;&lt;/strong&gt;, a small todo app I built (&lt;a href="https://github.com/Tanay3484/tasklet" rel="noopener noreferrer"&gt;source&lt;/a&gt;) that turns GitHub issues into todos. It's strict on purpose, like a real handler, and it has a maintenance switch that makes it answer 503. I signed in to Holdfast's Live mode with GitHub, connected &lt;code&gt;Tanay3484/tasklet&lt;/code&gt;, and pointed it at Tasklet's webhook address. My coding agent ran the rest from the command line while I was out, and I checked the results when I got back.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connect the repo&lt;/td&gt;
&lt;td&gt;Holdfast added the webhook. GitHub's test ping was acknowledged in &lt;strong&gt;0.27 s&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open issue #1&lt;/td&gt;
&lt;td&gt;GitHub → Holdfast acknowledged in 0.82 s. &lt;strong&gt;The todo appeared in Tasklet within 4 s.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tasklet in maintenance, open issue #2&lt;/td&gt;
&lt;td&gt;Holdfast tried &lt;strong&gt;5 times in 34 s&lt;/strong&gt; (waiting 2, 4, 8, 16 s), got 503 each time, and &lt;strong&gt;held it&lt;/strong&gt;. Gemma explained it in 40 s.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close issue #1&lt;/td&gt;
&lt;td&gt;The todo was ticked off within 4 s.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance off, then "Send all held again"&lt;/td&gt;
&lt;td&gt;I pressed it in Live mode. &lt;strong&gt;Issue #2's todo appeared in Tasklet&lt;/strong&gt;, once. Nothing was lost while the app was down.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One detail I'll own: Tasklet runs on Render's free plan, which doesn't keep a disk, so its todos reset whenever it falls asleep. That's why issue #1's todo had disappeared by the time I resent #2. Holdfast's copy of every message was still there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did anyone pay?&lt;/strong&gt; No. My first user is me and my own app, so I won't claim a sale. What the run convinced me of is the thing I'd charge for: I switched an app off, opened an issue, and nothing was lost, and I got a sentence explaining why it was held that I could forward to a client. &lt;strong&gt;What I'd build next:&lt;/strong&gt; an email or Slack alert the moment something is held, one-click Stripe sign-in, and a share link for the client note.&lt;/p&gt;

&lt;p&gt;Something real from deploy day: on the first deploy, &lt;strong&gt;every Gemma call failed&lt;/strong&gt; with &lt;code&gt;connection refused&lt;/code&gt; on &lt;code&gt;holdfast-ollama:10000&lt;/code&gt;. Ollama was healthy on port 11434, but Render's service-to-service address used its default port. The rules took over automatically, so the site stayed fully usable, which is exactly the fallback I designed for. One line in &lt;code&gt;render.yaml&lt;/code&gt; (&lt;code&gt;PORT=11434&lt;/code&gt;) fixed it.&lt;/p&gt;

&lt;p&gt;Then two more, the same afternoon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Out of memory.&lt;/strong&gt; Render killed the Gemma service. Inside the container, llama.cpp sees the whole host (60+ GB, 16 CPUs), not my 4 GB plan, so it kept a prompt cache that could grow to 8 GB and a second, repacked copy of the weights. Three &lt;code&gt;LLAMA_ARG_*&lt;/code&gt; settings (no prompt cache, no repack, 2 threads) brought it back under 4 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A runaway answer.&lt;/strong&gt; Some diagnoses ran on for hundreds of tokens and never closed their JSON. I reproduced it on my laptop's CPU with Render's exact settings: in 3 of 5 runs Gemma tried to end a string with a &lt;strong&gt;curly quote&lt;/strong&gt; (&lt;code&gt;”}&lt;/code&gt;), which JSON treats as ordinary text, so it never stopped and started repeating itself. A second cause: Go wrote the JSON format's fields alphabetically (cause, fix, summary), so Gemma poured the whole story into the developer's fix and left the owner one line. The fix: length limits on each field, code that cuts a field at the stray &lt;code&gt;”}&lt;/code&gt;, a check that the summary ends like a sentence, and the fields listed as cause, summary, fix. &lt;strong&gt;Before: 2 of 5 runs usable. After: 4 of 4, in 16–40 s.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each time, the visitor still got a correct, plain-language answer from the rules. Gemma makes the explanation better; it was never the thing keeping a payment safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What isn't built:&lt;/strong&gt; one-click Stripe sign-in. Stripe Connect needs a Stripe platform account, which I don't have, so Stripe connects by hand. You paste one address into Stripe's webhook settings and its signing secret into Holdfast. The site says so plainly.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render.&lt;/strong&gt; Render is the AI runtime: one Blueprint runs the public Go service (with a persistent disk for the event store) and a private Ollama service with Gemma baked into the image, talking over Render's private network. The model is never exposed to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 4B writes every explanation and client note. Its output is constrained to a schema and checked by code, and I measured it against 1B to choose the size.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>go</category>
      <category>ai</category>
    </item>
    <item>
      <title>Golden Hour: an open model that finds your 40 minutes outside, and where to spend them</title>
      <dc:creator>Tanay</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:08:21 +0000</pubDate>
      <link>https://dev.to/tanay_singh_1005/golden-hour-an-open-model-that-finds-your-40-minutes-outside-and-where-to-spend-them-5fe8</link>
      <guid>https://dev.to/tanay_singh_1005/golden-hour-an-open-model-that-finds-your-40-minutes-outside-and-where-to-spend-them-5fe8</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;I don't stay indoors because I don't want to go out. I stay in because two small decisions are easy to put off: &lt;em&gt;when&lt;/em&gt;, and &lt;em&gt;to do what&lt;/em&gt;. Weather apps tell me what it's like outside. Nothing tells me "go at 18:00, and here's where."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Golden Hour&lt;/strong&gt; does exactly that, and nothing else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tell it when you're free today.&lt;/strong&gt; Or don't, and it assumes "from now until sunset".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It picks your best 20–40 minutes.&lt;/strong&gt; It reads the hourly forecast and sunset for where you are, scores every possible slot, and explains its choice: "dry", "golden hour", "chilly (13°C)".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick up to three hobbies,&lt;/strong&gt; like photography, history or coffee outside, and it finds the &lt;strong&gt;top 5 real places within walking distance&lt;/strong&gt; from OpenStreetMap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma, an open-weight model running on my own server, writes one line for each place:&lt;/strong&gt; what to do there, right now, in this light and this weather.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You get a countdown and a calendar invite&lt;/strong&gt; with a 10-minute reminder. Then you close it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's deliberately not an app you spend time in. Success is closing the tab.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://golden-hour-ahl6.onrender.com" rel="noopener noreferrer"&gt;https://golden-hour-ahl6.onrender.com&lt;/a&gt; &lt;br&gt;&lt;br&gt;
(best on a phone: allow location, or search for your city)&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%2F6o5oplw70u50jfxbqteh.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%2F6o5oplw70u50jfxbqteh.png" alt="Golden Hour on desktop: the planner on the left, and the chosen window with its score, weather and daylight timeline on the right" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a real plan it made for a morning in central Pune. I picked photography and history, and left my free time empty:&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%2F1vvz9wl58thfxg0la6mo.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%2F1vvz9wl58thfxg0la6mo.png" alt="The chosen window: 06:30–07:10, score 94, with weather tiles and a sunrise-to-sunset timeline" width="800" height="1533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;06:30–07:10, 94/100.&lt;/strong&gt; Dry and calm, and it only loses points for being 26 °C at dawn. Then the five places within a 13-minute walk:&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%2F0zxloem8u2vwwtcng2aq.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%2F0zxloem8u2vwwtcng2aq.png" alt="Top 5 near you: Shaniwarwada, the Bajirao I statue, two parks and a memorial, each with a line from Gemma" width="800" height="2045"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shaniwarwada, the 18th-century fort, is three minutes away, and Gemma's line for it was "Start with a quick photo of the surrounding architecture to capture the morning ambiance." Two of the lines, for the second park and the memorial, are my hand-written templates: the model's attempts didn't pass the checks I describe below, so the app quietly used safe lines instead.&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/Tanay3484" rel="noopener noreferrer"&gt;
        Tanay3484
      &lt;/a&gt; / &lt;a href="https://github.com/Tanay3484/golden-hour" rel="noopener noreferrer"&gt;
        golden-hour
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Your best 20-40 minutes outside today, planned by an open-weight model. Hacktoberfest 2026 'Touch Grass' challenge.
    &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;🌅 Golden Hour&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/Tanay3484/golden-hour/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/Tanay3484/golden-hour/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://render.com/deploy?repo=https://github.com/Tanay3484/golden-hour" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c3053e93bc9f0a2cd84050a5ff9f07cc5e639621a72e50dce48781f4a38f10e2/68747470733a2f2f72656e6465722e636f6d2f696d616765732f6465706c6f792d746f2d72656e6465722d627574746f6e2e737667" alt="Deploy to Render"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Your best 20–40 minutes outside today, and one small thing to do with them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Tell Golden Hour when you're free. It checks the hourly forecast and sunset for where you are, picks the
nicest window to be outside, and finds the &lt;strong&gt;top 5 real places within walking distance&lt;/strong&gt; that suit your
hobbies, from &lt;a href="https://www.openstreetmap.org" rel="nofollow noopener noreferrer"&gt;OpenStreetMap&lt;/a&gt;. An open-weight model
(&lt;a href="https://ai.google.dev/gemma" rel="nofollow noopener noreferrer"&gt;Gemma&lt;/a&gt;, served by &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;) writes one line for each on
what to do there right now. You get a countdown and a calendar invite with a 10-minute reminder.&lt;/p&gt;
&lt;p&gt;Built for the DEV &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05" rel="nofollow"&gt;Hacktoberfest 2026 Open-Source AI Challenge, Week 1: "Touch Grass"&lt;/a&gt;.&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;Browser ──▶ FastAPI ──▶ Open-Meteo (forecast + sunset, open data, no key)
                │
                ├─ scoring.py   deterministic: every 20–40 min slot in your free time, scored 0–100
                │               (rain, temperature, wind, UV, +15 for golden hour)
                │
                ├─ places.py ──▶ OpenStreetMap (Overpass):&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Tanay3484/golden-hour" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; FastAPI · vanilla JS (about 30 KB, no build step) · &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; serving Gemma 3 (&lt;code&gt;gemma3:1b&lt;/code&gt; in production, &lt;code&gt;gemma3:4b&lt;/code&gt; on my laptop) · &lt;a href="https://open-meteo.com" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; for weather and &lt;a href="https://www.openstreetmap.org" rel="noopener noreferrer"&gt;OpenStreetMap&lt;/a&gt; for places (both open data, no API keys) · two services on Render.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;free time + forecast  ──▶  plain Python picks the window      (tested, explainable)
hobbies + location    ──▶  OpenStreetMap → Python ranks top 5  (no model involved)
window + 5 places     ──▶  Gemma writes one line per place     (JSON schema, validated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building it taught me four things I didn't expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Let math pick the time. Let the model write.
&lt;/h3&gt;

&lt;p&gt;My first instinct was to hand Gemma the whole forecast and ask "when should I go out?". I didn't, for three reasons: a 1B model on a shared CPU is slow, it's bad at arithmetic over 48 rows of numbers, and I wanted to be able to explain &lt;em&gt;why&lt;/em&gt; it chose 18:00.&lt;/p&gt;

&lt;p&gt;So the window is picked by under 200 lines of plain, tested Python. Every quarter-hour slot in your free time gets the &lt;em&gt;worst&lt;/em&gt; weather of the hours it touches, then a score: start at 100, lose 0.8 points per % chance of rain, 3 per °C outside 15–24, 2 per km/h of wind above 20, 5 per UV point above 6, and &lt;strong&gt;gain 15 if it overlaps the hour before sunset&lt;/strong&gt;. The reasons on the card come from the same list of factors as the score, so the explanation can't disagree with the number.&lt;/p&gt;

&lt;p&gt;Writing tests caught a bug no demo would have: on a perfect day every slot scores 100 &lt;em&gt;after&lt;/em&gt; capping, so the golden-hour bonus vanished and the earliest slot won. You'd only notice on the one perfect evening you actually wanted. A tie-break that prefers golden hour fixed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Small models need guardrails, not just better prompts
&lt;/h3&gt;

&lt;p&gt;Gemma only does the creative part, and its answers have to match a JSON schema (Ollama's structured outputs make this reliable even at 1B). Testing it locally on real data was humbling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What Gemma did&lt;/th&gt;
&lt;th&gt;What fixed it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;With no location, it sent me to a nonexistent "Elm Street"&lt;/td&gt;
&lt;td&gt;It isn't allowed to name places it wasn't given; it describes &lt;em&gt;kinds&lt;/em&gt; of places instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At a 70% chance of rain, it suggested an open field&lt;/td&gt;
&lt;td&gt;"Match the weather" was too vague. Now the first step &lt;em&gt;must&lt;/em&gt; say how to stay dry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asked to "pick the best 5" of 13 real places, it returned the first five in list order. 4B called a Berlin memorial the "Frauenkirche"&lt;/td&gt;
&lt;td&gt;Code ranks the places (hobby, walking time, weather, variety). Gemma only describes them, using facts from OpenStreetMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;It wrote a park's line about a statue elsewhere on the list&lt;/td&gt;
&lt;td&gt;Gemma must echo each place's name, and the server rejects any line that doesn't match, names another place, or borrows another place's facts. Rejected lines become a hand-written template&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest result: &lt;code&gt;gemma3:1b&lt;/code&gt; now puts every line on the right place, but still adds the odd plausible detail ("a pastry" at a café). &lt;code&gt;gemma3:4b&lt;/code&gt; doesn't, but it's about five times slower. On a 1-CPU server, I chose fast, and I wrote the gap down in the spec instead of hiding it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Open data is free, but not effortless
&lt;/h3&gt;

&lt;p&gt;The public OpenStreetMap query servers (Overpass) are a shared, volunteer-run resource. In one evening of testing, two of three public instances failed for every city, and the main one kept answering "server too busy" for Pune and Mumbai while Berlin worked fine.&lt;/p&gt;

&lt;p&gt;The first fix was one directive. Overpass decides whether to admit a query based on how much memory it &lt;em&gt;might&lt;/em&gt; need, and the default is 512 MB. Declaring 64 MB (&lt;code&gt;[maxsize:67108864]&lt;/code&gt;) got Pune through in 3 seconds.&lt;/p&gt;

&lt;p&gt;Then I deployed, and the live site found &lt;strong&gt;zero&lt;/strong&gt; places, twelve times in a row. The same query from my home connection returned 44 places in 3 seconds. Overpass limits how many queries each IP address can run, and my Render server shares its outbound IP with a lot of other people's apps. So now, if the server can't reach OpenStreetMap, your &lt;em&gt;browser&lt;/em&gt; asks it directly, with the exact same query, and hands the raw results back to the server to rank. Every visitor brings their own rate limit. Anything a browser sends back is treated as untrusted and never cached, so nobody can plant fake places for the next person. If both routes fail, you get a single Gemma-written activity instead, and the page never breaks.&lt;/p&gt;

&lt;p&gt;For privacy, OpenStreetMap only ever sees the centre of your ~1 km area. Exact walking times are computed on my server, and nothing is stored.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. One setting doubled my speed on Render
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;render.yaml&lt;/code&gt; is a two-service Blueprint: a public FastAPI web service, and a &lt;strong&gt;private&lt;/strong&gt; Ollama service with Gemma baked into its Docker image. The web app reaches it over Render's private network, so the model has no public endpoint at all.&lt;/p&gt;

&lt;p&gt;The first live run was painfully slow: 38–80 seconds per suggestion, about twice what I'd measured in Docker with the same 1-CPU limit. The cause was one setting. Ollama sizes its thread pool from the &lt;em&gt;host machine's&lt;/em&gt; CPU count, so on a big shared server it ran dozens of threads fighting over the single CPU my plan provides. Setting &lt;code&gt;num_thread&lt;/code&gt; to 1 doubled the speed: suggestions now usually take 15–30 seconds instead of up to 80. If you run a small model on a small cloud instance, check this first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built spec-first, with an AI pair
&lt;/h3&gt;

&lt;p&gt;I built Golden Hour spec-first: requirements with testable acceptance criteria, then a design, then a task list, each approved before any code was written. It's all in &lt;a href="https://github.com/Tanay3484/golden-hour/tree/main/specs" rel="noopener noreferrer"&gt;&lt;code&gt;specs/&lt;/code&gt;&lt;/a&gt;. Every task had an owner: me, or my AI pair, &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wrote the first scoring rules myself. Then my GRE landed in the same week, and I handed most of the build to Claude, working from the approved spec while I reviewed and merged. Every change of plan, from the golden-hour tie-break to the browser fallback for OpenStreetMap, went into a spec changelog &lt;em&gt;before&lt;/em&gt; it went into the code. So the repo shows why things changed, not just what. It's covered by 154 Python tests, a Node test for the countdown and calendar export, and CI on every pull request.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Because "when are you free" and "where are you" are personal.&lt;/strong&gt; A closed-API version of this app would send your location and schedule to a third party every time you opened it. Here, the model runs on a server I control and never receives coordinates at all. Weather and places come from open data, with no keys and no accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because it should be cheap enough to just exist.&lt;/strong&gt; An app that nudges you outside once a day shouldn't come with a per-token bill or a usage cap. The whole production stack is two small CPU services on Render, about $32 a month, with no metered API anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because I got to choose the trade-off.&lt;/strong&gt; Same code, same prompt: 4B on my laptop for richer lines, 1B in production to fit a small box. Switching is one environment variable. With a closed model, someone else decides the size, the price and whether it's still available next month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because you can see why it said 18:00.&lt;/strong&gt; The scoring rules, the prompts, the guardrails and the model weights are all open. If it ever sends you out into the rain, you can find the exact rule that let it happen, and send a pull request.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An installable phone version with a local "your golden hour starts in 10 minutes" notification, so it reaches you even when the tab is closed&lt;/li&gt;
&lt;li&gt;Running Gemma on the phone itself, so it works offline and fully privately&lt;/li&gt;
&lt;li&gt;The field test I owe this project, right after the GRE&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Render:&lt;/strong&gt; Render is the project's AI runtime. Gemma runs on a private Render service via Ollama, behind a public web service, deployed from a one-click Render Blueprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; Gemma 3 writes every activity suggestion and every place line: &lt;code&gt;gemma3:1b&lt;/code&gt; in production, &lt;code&gt;gemma3:4b&lt;/code&gt; locally, constrained to JSON schemas.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Order Taker: turning "kal subah 2 box brownies" into real orders with open AI</title>
      <dc:creator>Tanay</dc:creator>
      <pubDate>Sun, 04 Oct 2026 12:55:48 +0000</pubDate>
      <link>https://dev.to/tanay_singh_1005/order-taker-turning-kal-subah-2-box-brownies-into-real-orders-with-open-ai-5h5h</link>
      <guid>https://dev.to/tanay_singh_1005/order-taker-turning-kal-subah-2-box-brownies-into-real-orders-with-open-ai-5h5h</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;Picture the home baker down your street. They take every order on WhatsApp:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Priya:&lt;/strong&gt; Hi aunty! Can I get 1 kg chocolate truffle cake for Sunday evening? Eggless please 🙏&lt;br&gt;
&lt;strong&gt;Priya:&lt;/strong&gt; Write "Happy Birthday Arjun" on it&lt;br&gt;
&lt;strong&gt;Rahul Bhaiya:&lt;/strong&gt; 2 box brownies kal subah, address 14B Lakeview Apts&lt;br&gt;
&lt;strong&gt;Rahul Bhaiya:&lt;/strong&gt; sorry make it 3 boxes&lt;br&gt;
&lt;strong&gt;Meena:&lt;/strong&gt; Good night aunty 😊&lt;br&gt;
&lt;strong&gt;Sneha:&lt;/strong&gt; 12 cupcakes vanilla + 6 red velvet, Saturday 4pm pickup. My number 98450 12345&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's six messages in a mix of English and Hindi. There's an edit ("make it 3 boxes"), relative dates ("kal", "Sunday"), a pickup, and someone just saying good night. Multiply that by a busy festival week and orders get missed, quantities get mixed up, and customers keep messaging "aunty, is my cake ready?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order Taker&lt;/strong&gt; is built for that person: someone who is great at baking, not at spreadsheets, and who runs the whole business from one WhatsApp chat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The shop owner&lt;/strong&gt; pastes (or uploads) the WhatsApp chat. A small team of AI helpers reads it, and a friendly &lt;em&gt;"What's happening"&lt;/em&gt; box explains each step in plain English as it goes. Each order shows up as a card to &lt;strong&gt;Accept, Edit or Discard&lt;/strong&gt;, with anything odd flagged: &lt;em&gt;"Delivery or pickup? No address was given."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Accepted orders move along with one big thumb-sized button: &lt;strong&gt;Received → Confirmed → Preparing → Ready → Delivered.&lt;/strong&gt; There's a per-day &lt;strong&gt;prep list&lt;/strong&gt; ("3 boxes brownies, 2 kg chocolate cake") and a CSV export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt; log in from their phone with their number and a PIN that the shop sends them on WhatsApp. They see a progress bar for their order, and they can &lt;strong&gt;change or cancel it in the app&lt;/strong&gt;. Before the shop confirms an order, changes apply straight away. After that, they go to the shop owner as a request to approve, shown old → new.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing runs &lt;strong&gt;on the shop owner's laptop&lt;/strong&gt;. Customers' phones reach it over the same Wi‑Fi.&lt;/p&gt;

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


&lt;div class="ltag__huggingface"&gt;
  &lt;iframe src="https://midking1234-order-taker.hf.space" title="Hugging Face Space" width="100%" height="600"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;


&lt;p&gt;The app is deliberately &lt;strong&gt;not&lt;/strong&gt; hosted anywhere: customer names, phone numbers and home addresses never leave the laptop. To try it yourself, follow the 3-step setup in the README with &lt;code&gt;samples/sample_chat.txt&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update: from one laptop to a fully deployed app&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping it on the laptop was a choice for this first version, not a limit of the design. The same code can become a normal hosted web app that customers open from anywhere, not just on the shop's Wi‑Fi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The web app&lt;/strong&gt; is a standard FastAPI service. Every machine-specific setting is already an env var (&lt;code&gt;ORDER_DB&lt;/code&gt;, &lt;code&gt;ORDER_PORT&lt;/code&gt;, &lt;code&gt;ORDER_PUBLIC_URL&lt;/code&gt;, &lt;code&gt;OLLAMA_URL&lt;/code&gt;), so it can run on Render, Fly.io or a small VPS behind HTTPS with secure-only cookies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data&lt;/strong&gt; moves from one SQLite file to a managed Postgres database. The numbered migrations are plain SQL, so they carry over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI stays open.&lt;/strong&gt; &lt;code&gt;OLLAMA_URL&lt;/code&gt; can point to an open-weight model on a GPU server you control, or even back to the shop's own laptop through a private tunnel. Then the model still runs on the owner's machine, while customers get a public link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next steps after that:&lt;/strong&gt; WhatsApp Business webhooks so orders arrive automatically instead of being pasted, an SMS or WhatsApp one-time code instead of a shop-issued PIN, and one install serving many home businesses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is honest: once it's hosted, customer data lives on a server instead of only on the laptop. That's why the project's specs treat it as a deliberate change to its "local-first" rule. The owner should choose it on purpose, not have it happen by accident.&lt;/p&gt;
&lt;/blockquote&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/Tanay3484" rel="noopener noreferrer"&gt;
        Tanay3484
      &lt;/a&gt; / &lt;a href="https://github.com/Tanay3484/order-taker" rel="noopener noreferrer"&gt;
        order-taker
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Turn messy WhatsApp orders into a clean order board for home bakers, with local multi-agent AI (Ollama) so customer data never leaves the laptop.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tbody&gt;
  &lt;tr&gt;
    &lt;th&gt;title&lt;/th&gt;
    &lt;td&gt;Order Taker&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;emoji&lt;/th&gt;
    &lt;td&gt;🧁&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;colorFrom&lt;/th&gt;
    &lt;td&gt;pink&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;colorTo&lt;/th&gt;
    &lt;td&gt;yellow&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;sdk&lt;/th&gt;
    &lt;td&gt;docker&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;app_port&lt;/th&gt;
    &lt;td&gt;7860&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;pinned&lt;/th&gt;
    &lt;td&gt;false&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;license&lt;/th&gt;
    &lt;td&gt;mit&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;short_description&lt;/th&gt;
    &lt;td&gt;Local AI agents turn WhatsApp orders into a bakery board&lt;/td&gt;
  &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Order Taker 🧁&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Turns messy WhatsApp order messages into clean orders for a small home business. The shop owner pastes the chat, a team of small AI helpers reads it, and customers can log in from their phones to see where their order is.&lt;/p&gt;
&lt;p&gt;Everything runs on the shop owner's laptop with an open-weight model via &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;, so customer names, numbers and addresses never leave the machine.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shop owner&lt;/strong&gt; (admin): paste or upload the WhatsApp chat. Watch a plain-English "What's happening" feed while it's read. Check, edit and accept each order, then move it along: Received → Confirmed → Preparing → Ready → Delivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt;: log in with their phone number and a PIN the shop sends…&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/Tanay3484/order-taker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Open-weight model, running locally:&lt;/strong&gt; &lt;code&gt;qwen2.5:7b&lt;/code&gt; through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, on an ordinary laptop with a 4 GB GTX 1650. Every model call uses Ollama's structured output with a JSON schema, and the result is validated with Pydantic before anything is stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent intake, without a framework.&lt;/strong&gt; The flow is a fixed pipeline, so plain &lt;code&gt;asyncio&lt;/code&gt; was enough (about 150 lines), and it let me attach a progress message to every step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pasted chat ─▶ Master (plain code): split by person, skip messages already read,
                spot pure "good night / thank you" without any AI
                  │  one track per person, all at the same time
                  ├─▶ [Sorter, small model*] ─▶ [Extractor, 7B] ─▶ [Checker, plain code] ─▶ draft card
                  └─▶ …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Master&lt;/strong&gt; splits the chat by sender and skips messages it has already handled, so you can paste the &lt;em&gt;whole&lt;/em&gt; chat every evening and only new messages get read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extractor&lt;/strong&gt; (one per person) sees only that person's messages and their &lt;em&gt;open orders&lt;/em&gt;. That's how "sorry make it 3 boxes" becomes a change to the existing order instead of a duplicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checker&lt;/strong&gt; is just rules: missing date, missing address/pickup, missing phone, duplicates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI proposes, a human decides.&lt;/strong&gt; Nothing becomes a real order until the shop owner taps Accept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The most interesting lesson: let code do what code is good at.&lt;/strong&gt; On the first real run, the 7B model read Priya's "Sunday" as a &lt;strong&gt;Thursday&lt;/strong&gt; and Sneha's "Saturday" as a &lt;strong&gt;Wednesday&lt;/strong&gt;. Small models are bad at calendar arithmetic. So now the model only &lt;em&gt;copies&lt;/em&gt; what the customer wrote ("Sunday", "kal", "5th Oct"), and a small lookup table in Python turns it into a date, counted from when the message was sent. Same with phone numbers: a regex finds "My number 98450 12345" every time; the model didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest numbers&lt;/strong&gt; on my laptop (the model only partly fits on the GPU, so Ollama runs one request at a time):&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;Old (one big call)&lt;/th&gt;
&lt;th&gt;Multi-agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First order ready to check&lt;/td&gt;
&lt;td&gt;~41 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~20 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whole chat done&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~41 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~55 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates, phone, chit-chat correct on the sample&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So on modest hardware the win is &lt;em&gt;time to first order&lt;/em&gt; and &lt;em&gt;correctness&lt;/em&gt;, not total time. On a machine with more VRAM and &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; set, the per-person tracks really do run side by side. The repo includes &lt;code&gt;scripts/bench_intake.py&lt;/code&gt; so anyone can measure their own machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No "Processing…" spinner.&lt;/strong&gt; A spinner that sits there for a minute makes non-technical users think the app is broken. Instead, the progress box narrates every step: &lt;em&gt;"Meena is just chatting. Nothing to order."&lt;/em&gt;, &lt;em&gt;"Still working on Priya's messages, nearly there…"&lt;/em&gt;, &lt;em&gt;"All done in 53 seconds: 3 things to check, 1 just chatting."&lt;/em&gt; These messages come from fixed templates, never from raw model output. A test fails the build if any of them contains words like "JSON", "API", "token" or "Ollama".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec Driven Development.&lt;/strong&gt; Before writing code, I wrote a short constitution (local-first, plain English, AI proposes / human decides, mobile-first) and five feature specs with numbered acceptance criteria. Every one of the 154 tests names the criterion it covers (&lt;code&gt;INT-21&lt;/code&gt;, &lt;code&gt;TRK-23&lt;/code&gt;…). When the real model proved a spec wrong (the dates!), I updated the spec first and then the code. It's all in &lt;a href="https://github.com/Tanay3484/order-taker/tree/main/specs" rel="noopener noreferrer"&gt;&lt;code&gt;specs/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stack: FastAPI, SQLite, Jinja2, a little vanilla JS, Server-Sent Events for the live feed. No CDNs, no external calls.&lt;/p&gt;

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

&lt;p&gt;For this user, a closed API wasn't really an option:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; these chats are full of customers' phone numbers and home addresses. With an open-weight model running locally, &lt;strong&gt;none of it leaves the laptop&lt;/strong&gt;. There's no third-party processor and nothing to explain to customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; a home bakery might get 40 messages a day. Any per-token bill is real money for a tiny business. Local inference costs nothing per message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works when the internet doesn't:&lt;/strong&gt; the app and the model run on the laptop, and phones only need the home Wi‑Fi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control:&lt;/strong&gt; I could look at exactly where the model failed (weekday maths, optional JSON fields) and design around it. When I needed every field filled in, I tightened the JSON schema sent to Ollama. Swapping to another model is one env var (&lt;code&gt;ORDER_MODEL=llama3.2&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because the app is MIT-licensed, any other home business can run it, and any developer can add their language's words for "tomorrow".&lt;/p&gt;

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

&lt;p&gt;I built this pair-programming with &lt;strong&gt;Claude Code&lt;/strong&gt; as my coding agent, using a spec-first workflow: we agreed the constitution and specs together, then it implemented feature by feature against them and ran the real model to check its own work.&lt;/p&gt;

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