<?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: JinYi123456</title>
    <description>The latest articles on DEV Community by JinYi123456 (@jinyi123456).</description>
    <link>https://dev.to/jinyi123456</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%2F4174845%2Fce43baaf-a484-4627-995b-fbd149abe4b2.png</url>
      <title>DEV Community: JinYi123456</title>
      <link>https://dev.to/jinyi123456</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jinyi123456"/>
    <language>en</language>
    <item>
      <title>I measured open vs closed models on my own product, and open weights won one and lost one</title>
      <dc:creator>JinYi123456</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:23:42 +0000</pubDate>
      <link>https://dev.to/jinyi123456/i-measured-open-vs-closed-models-on-my-own-product-and-open-weights-won-one-and-lost-one-2n7a</link>
      <guid>https://dev.to/jinyi123456/i-measured-open-vs-closed-models-on-my-own-product-and-open-weights-won-one-and-lost-one-2n7a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this post and most of the code were produced with an AI coding agent working under my direction — I chose the problem, the measurements and what to keep or cut, and I checked every number by running the deployed app. The agent is not named here because the repo is deliberately a product, not a transcript. What the agent got wrong and how it was caught is in &lt;em&gt;Things that only happen on a real deployment&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;A cleaner stands in a stranger's kitchen at six in the evening with the client waiting behind her. She has looked at the job for ninety seconds and now she has to name a price.&lt;/p&gt;

&lt;p&gt;Both of her options lose money. Say &lt;em&gt;"I'll email you tonight"&lt;/em&gt; and by tonight the client has two other quotes and picks the fastest. Quote from gut memory on the spot and the number is either padded — and lost — or short, and the job gets worked at a loss. She knows her own numbers. She just cannot do the arithmetic while somebody watches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chalk&lt;/strong&gt; is the third option. She photographs the kitchen, holds a button and talks through what she would do, and by the time she is back at the van there is a priced, itemized, client-ready quote — priced in context over &lt;em&gt;her own last 42 jobs&lt;/em&gt;, not against an industry average she does not match.&lt;/p&gt;

&lt;p&gt;This is not a mock-up. It is the deployed build, run end to end just now on two photographs of a kitchen and an oven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scrub kitchen floor tiles                    1.5h   $74   [photo 1]
Clean greasy hob and splashback               2h    $112   [photo 1]
Remove and clean oven interior racks          1h     $50   [photo 2]
Degrease and sanitize oven interior          2.5h    $156   [photo 2]
Wipe down dining table and chairs            0.5h     $22   [photo 1]
Materials and consumables                            $26
                                                   ─────
Fair price                                   $318   range $254 – $394
You usually charge about $421 for move-out work this size —
this quote is $103 under your own average.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happened there that a generic assistant does not do. The vision model wrote the caption on each photograph — &lt;code&gt;kitchen: greasy hob, tiled splashback&lt;/code&gt;, &lt;code&gt;oven: caked-on grime, interior racks&lt;/code&gt; — and attached each line item to the photograph that justifies it. And the price came with an argument: &lt;em&gt;you are undercharging yourself&lt;/em&gt;, based on what this pro has actually billed for this kind of job.&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%2Fgqso1ievevq41bxxjozs.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%2Fgqso1ievevq41bxxjozs.png" alt="The quote as the pro sees it: line items, the fair range, and a live market check" 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%2F5o2xozr6c4bsc7a5vxeq.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%2F5o2xozr6c4bsc7a5vxeq.png" alt="Her side of the desk: win odds, her usual price, the undercharge warning, and the price ladder" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client never sees the win probability or the margin. They see the work, the price, the assumptions, and the photographs that explain it — on their own phone, at a link the pro texted them — with &lt;strong&gt;Accept&lt;/strong&gt; and &lt;strong&gt;Not right now&lt;/strong&gt; underneath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1tjkenu4rg5wdvfik0ba.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%2F1tjkenu4rg5wdvfik0ba.png" alt="What the client gets: the same record, the photographs that justify each line, and no margin" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last part is what closes the loop. When the client taps Accept, the quote comes back as a won job: the row joins the pro's history, the win rate moves, and TabPFN refits over it. So the odds on the &lt;em&gt;next&lt;/em&gt; quote are built from real answers rather than from the pro's memory of them.&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%2Fsv82lf42chyucqwoop5g.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%2Fsv82lf42chyucqwoop5g.png" alt="After the tap: the job is booked in her ledger and the win rate has moved" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;What answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Walk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;photos plus the pro's own words, typed or dictated&lt;/td&gt;
&lt;td&gt;open-weight vision reads surfaces, sizes and price-changing conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;itemized scope → a fair price, an honest range, the odds this client says yes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;TabPFN-3.5&lt;/strong&gt; in-context over the pro's own history — regression for price, classification for odds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Send&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;client message in the pro's voice, with assumptions and a validity window&lt;/td&gt;
&lt;td&gt;an open-weight text model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Answer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;one link the pro texts; the client accepts on their own phone&lt;/td&gt;
&lt;td&gt;the same stored record, through a client-safe face&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://hacktoberfestp5vqqk.tech" rel="noopener noreferrer"&gt;https://hacktoberfestp5vqqk.tech&lt;/a&gt; — the real app, not a recording. (Same deployment as &lt;a href="https://chalk-nine-flax.vercel.app" rel="noopener noreferrer"&gt;https://chalk-nine-flax.vercel.app&lt;/a&gt;, if you would rather click that.)&lt;/p&gt;

&lt;p&gt;There is a seeded pro with 42 past jobs so the pricing model has something to reason over. Walk the first tab, add a photo or two, and press quote. Every layer reports which engine answered, so the degradation is visible too — no key for a layer, and the card says so instead of hiding it. &lt;strong&gt;&lt;a href="https://hacktoberfestp5vqqk.tech/try" rel="noopener noreferrer"&gt;https://hacktoberfestp5vqqk.tech/try&lt;/a&gt;&lt;/strong&gt; hands a real quote to a stranger and asks one question — &lt;em&gt;would you pay about $15 a month for this?&lt;/em&gt; — because that is the number I would rather measure than assume.&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%2Fuw6gbqbiv5wejykoip9x.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%2Fuw6gbqbiv5wejykoip9x.png" alt="The Walk tab: two photographs, the client's name, and the pro's own words before anything is priced" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it offline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/JinYi123456/chalk.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;chalk
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build:all &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Film — 1:32, one unbroken take of the deployed app, with a real stopwatch on screen:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Eok52SebcJk" rel="noopener noreferrer"&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%2Fi2ja6x5dea8k6etj885m.png" alt="Chalk — same job, two ways to quote: one unbroken take of the deployed app" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No API key is required to start it. Every layer reports the one it is missing.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/JinYi123456/chalk" rel="noopener noreferrer"&gt;https://github.com/JinYi123456/chalk&lt;/a&gt; — MIT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web                 Vite + React PWA, plain CSS, no UI framework
services/agent           Hono + Mastra on Node
  ├─ lib/vision.ts       photo → site read            (open-weight vision)
  ├─ lib/scope.ts        site + words → itemized scope (open-weight text)
  ├─ lib/pricing.ts      TabPFN cloud → local service → transparent heuristic
  ├─ lib/benchmark.ts    the head-to-head below, with its answer key
  ├─ lib/quote-agent.ts  Mastra agent with market_rate and similar_jobs tools
  ├─ lib/memory.ts       MongoDB Atlas job memory
  ├─ lib/tiger.ts        won/lost margin time-series
  ├─ lib/temporal.ts     durable follow-ups
  └─ lib/tts.ts          ElevenLabs read-back
services/tabpfn          FastAPI, the same tabular model on local weights
services/tinker          fine-tune for the pro's quote voice
eval/                    the six walkthrough photographs and the stored run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The table I could not find
&lt;/h2&gt;

&lt;p&gt;Before I wrote a line of this post I looked around at what other people were building this week, because I wanted to know what I was actually competing with. A lot of it was excellent. And in the write-ups, one sentence kept appearing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;…where your open-based approach worked better than a closed one.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the prompt, asking directly. And the shape of almost every answer I found was: open weights are nicer, cheaper, more private — asserted, with no measurement next to it.&lt;/p&gt;

&lt;p&gt;So I measured it. Same walkthrough, same production prompt, sent to closed models, to hosted open-weight models, and to a model running on this laptop with no network at all. Two tasks, scored apart, so a weak pair of eyes can never be blamed on a weak brain.&lt;/p&gt;

&lt;p&gt;The honest question is not &lt;em&gt;is the model smart&lt;/em&gt;. It is &lt;em&gt;did the answer survive the parser&lt;/em&gt;, because a quote is a data structure before it is a sentence. So each row is scored twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;usable&lt;/strong&gt; — recall over what the product could actually parse and price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;in-prose&lt;/strong&gt; — the same score read off the raw reply text before parsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between those two columns is work the model knew and still could not deliver in a form you can charge for.&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%2Fx0r7079bea82bwux0dn7.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%2Fx0r7079bea82bwux0dn7.png" alt="The scoreboard, as the deployed app renders it on the You tab" width="708" height="1450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Eyes — reading the walkthrough photographs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model that answered&lt;/th&gt;
&lt;th&gt;weights&lt;/th&gt;
&lt;th&gt;usable&lt;/th&gt;
&lt;th&gt;in-prose&lt;/th&gt;
&lt;th&gt;invented facts&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;$/call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.8-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;4/4 · &lt;strong&gt;91%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10.5s&lt;/td&gt;
&lt;td&gt;2299&lt;/td&gt;
&lt;td&gt;$0.0021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google/gemma-3-27b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;4/4 · &lt;strong&gt;82%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2.0s&lt;/td&gt;
&lt;td&gt;840&lt;/td&gt;
&lt;td&gt;$0.0001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;4/4 · &lt;strong&gt;81%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;td&gt;2258&lt;/td&gt;
&lt;td&gt;$0.0007&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-VL-30B-A3B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;4/4 · &lt;strong&gt;79%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;1754&lt;/td&gt;
&lt;td&gt;$0.0003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gemma3:4b&lt;/code&gt; (this laptop)&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;4/4 · &lt;strong&gt;74%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;140.3s&lt;/td&gt;
&lt;td&gt;865&lt;/td&gt;
&lt;td&gt;$0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Open weights won this one.&lt;/strong&gt; &lt;code&gt;gemma-3-27b-it&lt;/code&gt; through Nebius scored 82% against the cheapest closed model's 81% — and cost &lt;strong&gt;seven times less&lt;/strong&gt;. The only closed model that beat it by a real margin spent &lt;strong&gt;21× the money&lt;/strong&gt; and took &lt;strong&gt;five times as long&lt;/strong&gt; to do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody invented anything.&lt;/strong&gt; Five arms, four photographs, zero hallucinated facts. That was a design decision, not luck: the prompt says &lt;em&gt;report only what you can actually see, never invent a size you cannot gauge&lt;/em&gt;, and the answer key actively punishes fabrication — each case lists things that are &lt;em&gt;not&lt;/em&gt; in the picture, and naming a carpet in a kitchen photograph is scored as an invention. Every arm cleared it. For a product where the client reads the evidence back to you, that is the number that matters most, and it is the one nobody reports.&lt;/p&gt;

&lt;p&gt;The laptop arm is the interesting failure. 74% usable for no money at all — but two and a half minutes per read, and it got the caption count right in only three of four cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Brain — itemizing the scope
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model that answered&lt;/th&gt;
&lt;th&gt;weights&lt;/th&gt;
&lt;th&gt;usable&lt;/th&gt;
&lt;th&gt;in-prose&lt;/th&gt;
&lt;th&gt;hours in band&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;$/call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;6/6 · &lt;strong&gt;82%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;1.3s&lt;/td&gt;
&lt;td&gt;$0.0005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;6/6 · &lt;strong&gt;82%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;46.5s&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Qwen3-30B-A3B&lt;/code&gt; (Nebius)&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;5/6 · &lt;strong&gt;73%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;8.9s&lt;/td&gt;
&lt;td&gt;$0.0001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Qwen3-30B-A3B&lt;/code&gt; (Featherless)&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;5/6 · &lt;strong&gt;71%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;$0.0002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.8-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;closed&lt;/td&gt;
&lt;td&gt;5/6 · &lt;strong&gt;63%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;8.9s&lt;/td&gt;
&lt;td&gt;$0.0013&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gemma4:e2b&lt;/code&gt; (this laptop)&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;1/6 · &lt;strong&gt;11%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;123.9s&lt;/td&gt;
&lt;td&gt;$0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heuristicScope()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;6/6 · &lt;strong&gt;13%&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;0.0s&lt;/td&gt;
&lt;td&gt;$0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three findings I did not expect and do not like much:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Open tied closed at 82% — and the tie hides the real difference.&lt;/strong&gt; &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; and &lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt; scored identically on work found. But the open one took &lt;strong&gt;46.5 seconds against 1.3&lt;/strong&gt;, and it put the hours estimate inside a sane band on only a third of jobs against two thirds. Quality parity, decided entirely on latency and calibration. For a pro standing in a hallway, 46 seconds is not a slower answer, it is no answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The most expensive closed model came second-to-last.&lt;/strong&gt; &lt;code&gt;gemini-3.8-flash&lt;/code&gt; scored 63% usable — worse than a $0.0001 open model. Spending more did not buy better. It bought a bigger, slower generalist that followed the scope schema less reliably than its cheaper sibling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The parser is where the work goes to die, and it is not an open-weights problem.&lt;/strong&gt; Qwen through Nebius found 92% of the billable work in its prose and delivered 73% of it in a parseable shape. The expensive closed model lost 15 points the same way. This gap is a property of asking a language model for structured output, not of whether you can read its weights.&lt;/p&gt;

&lt;p&gt;The laptop result is the one that stings: &lt;strong&gt;25% of the work found in prose, 11% usable, one job in six parseable at all.&lt;/strong&gt; It named the tasks. It would not put them in the boxes. So the honest version of the on-device dream, measured on my own hardware, is that a 4B model is good enough to be the &lt;strong&gt;camera&lt;/strong&gt; and not good enough to be the &lt;strong&gt;accountant&lt;/strong&gt;. That is a real constraint on "runs on your laptop", and I would rather publish it than quietly ship the cloud path.&lt;/p&gt;

&lt;p&gt;And note the bottom row: the deterministic floor scored &lt;em&gt;13%&lt;/em&gt; on work found but &lt;em&gt;67%&lt;/em&gt; on hours-in-band. A coarse guess lands inside a wide band more often than a smart model finds the right work. If you only ever measure one number, you will build the wrong thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to recompute all of this:&lt;/strong&gt; &lt;code&gt;node services/agent/scripts/benchmark.mjs&lt;/code&gt;. The answer key, the prompts and the scoring all live in &lt;code&gt;services/agent/src/lib/benchmark.ts&lt;/code&gt;, and the prompts are imported from the same modules the product uses, so the benchmark cannot quietly drift from the thing it claims to measure. Scoring is keyword-set recall against a hand-written key — deliberately &lt;strong&gt;no model judging a model&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;The open-source parts are load-bearing, not decorative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TabPFN-3.5&lt;/strong&gt; (Prior Labs) is the pricing model. It does in-context learning over tables: hand it rows — job type, hours, task count, rooms, average and maximum complexity, materials, access difficulty, risk count, whether the job was won — and it predicts the next row's price without training anything on this pro's data. It answers with quantiles rather than a number, which is why a Chalk quote ships with an honest range instead of false precision. It runs three ways and says which one answered: hosted REST, a local FastAPI service on real weights, or a transparent distance-weighted heuristic over the same rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mastra&lt;/strong&gt; is the agent harness: the quote writer gets two tools, &lt;code&gt;market_rate&lt;/code&gt; and &lt;code&gt;similar_jobs&lt;/code&gt;, and can call them mid-sentence. &lt;strong&gt;MongoDB Atlas&lt;/strong&gt; is job memory, with &lt;code&gt;$vectorSearch&lt;/code&gt; when configured. &lt;strong&gt;Tiger Data&lt;/strong&gt; holds the won/lost margin time-series. &lt;strong&gt;Temporal&lt;/strong&gt; wraps the follow-up in a durable workflow so a nudge survives a restart and an accept arrives as a signal. &lt;strong&gt;Sentry&lt;/strong&gt; carries one trace per compiled quote with per-stage spans. &lt;strong&gt;ElevenLabs&lt;/strong&gt; reads the client message back so the pro can hear it before sending. &lt;strong&gt;SerpApi&lt;/strong&gt; puts a live local rate on the card instead of a memorised one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tinker is the one that makes "open" mean something more than "cheap".&lt;/strong&gt; &lt;code&gt;services/tinker/train_voice.py&lt;/code&gt; distils the pro's own accepted client messages into context → message pairs and fine-tunes a LoRA on Qwen3-8B — four pairs, rank 16, three steps — then saves a sampler the agent calls like any other endpoint. When the sampler answers, the quote card says &lt;code&gt;voice: tinker lora&lt;/code&gt;; when it does not, the agent's own draft stands and says whose words you are reading. It is a voice sketch, not a rewritten model, and at four pairs that is exactly what it deserves to be. What it proves is the shape: the thing that makes this pro's quotes sound like &lt;em&gt;them&lt;/em&gt; is a file they own and can take with them, not a vendor's prompt.&lt;/p&gt;

&lt;p&gt;Getting the answer out took one more lesson in the same category as the parser: the sampler wrote its reply into &lt;code&gt;reasoning_content&lt;/code&gt; and left &lt;code&gt;content&lt;/code&gt; empty, and the parser — correctly — refused it. &lt;code&gt;chat_template_kwargs: {"enable_thinking": false}&lt;/code&gt; did not hold. Qwen3's own &lt;code&gt;/no_think&lt;/code&gt; token did: the answer came back in &lt;code&gt;content&lt;/code&gt; for two reasoning tokens, where widening the budget had burned two thousand and still returned nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen3-VL-30B-A3B&lt;/strong&gt; and &lt;strong&gt;Gemma&lt;/strong&gt; do the reading and the writing, through one OpenAI-compatible layer that walks a chain of endpoints until something answers and caches the winner — Ollama, llama.cpp, LM Studio, or any hosted open-weight catalog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that only happen on a real deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The deployed app was broken for reasons the local app could not show.&lt;/strong&gt; &lt;code&gt;api/index.mjs&lt;/code&gt; exported &lt;code&gt;handle(app)&lt;/code&gt; as its default export. Vercel reads a default export as the legacy &lt;code&gt;(req, res) =&amp;gt; void&lt;/code&gt; Node signature and &lt;em&gt;discards the return value&lt;/em&gt;, so every &lt;code&gt;/api&lt;/code&gt; request hung until the 300-second function timeout. Locally the agent serves the built PWA itself and never touches that file. The fix was to export it as the web handler it actually is — named &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A latency guard meant for a human in a hallway silently disabled the product's headline feature.&lt;/strong&gt; Prior Labs' predict call swings from ~4s to over a minute, so the hosted call gets a bounded 12-second wait and the local layer answers past that. Reasonable. Except a &lt;em&gt;cold serverless instance has not fitted anything yet&lt;/em&gt; — that first call has to upload the history and fit before it can predict at all. So every first quote on the deployed app priced by heuristic and the pricing model never appeared. Now only the first call per instance gets the longer budget: worst case one slow quote per cold start, instead of a demo that never shows the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thinking models eat their own budget.&lt;/strong&gt; Gemma 4 narrates inside a thinking block that is billed from the same &lt;code&gt;max_tokens&lt;/code&gt;. Cap it tightly and you get an empty answer, not a short one. It gets headroom now, and the block is stripped before parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A model's honest uncertainty can still be useless.&lt;/strong&gt; The first live demo quoted $396 and printed a fair range of &lt;strong&gt;$53–$744&lt;/strong&gt;. Both ends were real quantiles from a real model, over a ledger where a $70 gutter clean sits beside an $800 repaint. No client reads that as a range; they read it as a guess. The fix keeps the model's interval but clips it to the pro's own ledger — 10th to 90th percentile for that job type, never below half the recommended price. &lt;code&gt;saneRange&lt;/code&gt; is unit-tested against that exact $53–$744 case so it cannot come back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I asked the classifier for the win-odds ladder, and then refused to show its answer.&lt;/strong&gt; Every other build draws the curve through one estimate — five numbers with a slope I chose. TabPFN takes a batch, so I fitted the classifier with the price charged as a feature and asked about all five rungs at once. It answered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$219 66%  ·  $245 67%  ·  $258 67%  ·  $284 65%  ·  $315 67%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real answer from a real fit, and useless: it tells a pro that price does not change their odds. Over 42 jobs with a 67% base rate the win/lose signal is mostly noise, so the classifier cannot see a price effect yet — and a card implying &lt;em&gt;"charge $315, same chance"&lt;/em&gt; is worse than no card. &lt;code&gt;usableBand()&lt;/code&gt; now requires the ladder to slope before it replaces the drawn shape, and the UI says which one you are looking at and why. When the ledger grows, the same code starts showing the model's own numbers with no change to the interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The writer invented its own expiry.&lt;/strong&gt; The quote said &lt;code&gt;validDays: 14&lt;/code&gt; and the client message said &lt;em&gt;"valid for 7 days."&lt;/em&gt; Two numbers, one quote — the kind of detail a pro never notices and a client holds you to. The validity window now flows from one place into the prompt, the message, the stored quote and the follow-up workflow, and a regex pass rewrites any invented window back to the real one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then the same class of bug bit the parser, on the demo path, at scale.&lt;/strong&gt; I pressed quote on the deployed link while writing this and got back a three-line estimate for a walkthrough the model had itemized into six proper tasks. The card said &lt;em&gt;deterministic estimate (no model reachable)&lt;/em&gt;, which was a lie of omission — the model was reachable, had answered in 5 seconds, and had answered perfectly. The prompt tells the estimator to &lt;em&gt;omit&lt;/em&gt; &lt;code&gt;photo&lt;/code&gt; for a task no photograph shows. The model, being polite, wrote &lt;code&gt;"photo": null&lt;/code&gt;. The schema wanted a number or nothing, so one null on one task failed the parse, the repair pass failed the same way, and the whole scope was thrown away.&lt;/p&gt;

&lt;p&gt;That is finding 3 from my own table, happening to me, in production, on the flow every judge clicks first. The fix is &lt;code&gt;nullish()&lt;/code&gt; instead of &lt;code&gt;optional()&lt;/code&gt; — one word — plus a regression test that feeds the parser the verbatim reply and insists the six tasks survive. The client page also had a cousin of it: the header under the headline printed the &lt;em&gt;expiry&lt;/em&gt; date as the date the quote was written, so every client was told their quote was prepared the day it stops standing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The privacy promise was a rendering convention.&lt;/strong&gt; Chalk's whole pitch is two faces of one record: the pro sees the odds and the margin, the client sees the work and the price. I checked what a stranger holding the client link could actually read, and the answer was &lt;em&gt;everything&lt;/em&gt; — &lt;code&gt;GET /api/jobs/&amp;lt;id&amp;gt;&lt;/code&gt; served &lt;code&gt;winProbability&lt;/code&gt;, &lt;code&gt;usualPrice&lt;/code&gt;, the market check and the hours behind every line, because the client page simply chose not to render them. A claim that only holds while nobody looks at the network tab is not a claim. The client screen now calls &lt;code&gt;GET /api/jobs/&amp;lt;id&amp;gt;/client&lt;/code&gt;, which answers from an allow-list, and a test asserts the private fields are absent from the serialized response rather than merely invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshots lie, so the app prints its own split.&lt;/strong&gt; Every quote carries the milliseconds each stage actually took, and the attribution block names the model that answered. When I could not verify the UI visually in my harness, I checked it through the accessibility tree and computed styles — which proves the handlers and the persistence but not the appearance, and I said so rather than implying I had looked.&lt;/p&gt;

&lt;p&gt;51 hermetic specs run offline with &lt;code&gt;npm test&lt;/code&gt;, plus the TabPFN service tests. They force offline mode, so a contributor needs no key and a CI box needs no secrets.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Because the business only works open.&lt;/strong&gt; The measured cost of the eyes on a job, at list prices: &lt;code&gt;gemma-3-27b-it&lt;/code&gt; at &lt;strong&gt;$0.0001&lt;/strong&gt;, the closed model that beats it by nine points at &lt;strong&gt;$0.0021&lt;/strong&gt;. A pro who photographs four rooms on every visit, twelve jobs a day, in a busy season, is looking at a per-image bill that changes what they charge. At $0.0001 the inference is not a line item, and the whole category of tool becomes affordable to the person who needs it. That is not an aesthetic preference — it is the difference between a product a solo trade business can run and a demo that has a pricing page in its future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because the margin history is the family silver.&lt;/strong&gt; What this pro charges, what they win at, and where they lose money is the most valuable thing their business owns. The whole point of pricing in-context over &lt;em&gt;their own&lt;/em&gt; rows is that the data is the product. It should be able to live where they do: &lt;code&gt;CHALK_PROVIDER=ollama&lt;/code&gt; moves the reading, the scoping and the writing onto the laptop, with no server that could leak it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because swappable is a feature, not a vibe.&lt;/strong&gt; Every model call goes through one layer. Any OpenAI-compatible endpoint works. Nothing in the stack locks the business to a vendor's price list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And because open has a licence edge a closed API cannot give you — and a licence trap I nearly walked into.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The edge: the standard quality scorers for this kind of evaluation are CC BY-NC-SA, which means a &lt;em&gt;paid&lt;/em&gt; tool legally cannot use them. Apache-2.0 weights are the only path where the evaluation and the product are allowed to be the same product.&lt;/p&gt;

&lt;p&gt;The trap, which I found while writing this and which changes how Chalk is put together: &lt;strong&gt;the TabPFN open weights are licensed non-commercial&lt;/strong&gt;, and the licence text restricts using the outputs in production systems without a separate commercial licence. A "side hustle" whose pricing model runs on those weights would be a product I am not allowed to sell. So Chalk treats &lt;strong&gt;Prior Labs' hosted API as the commercial path&lt;/strong&gt; — which is what the deployed demo uses — and the local weights service as the offline fallback, clearly labelled as such. The README says this out loud rather than leaving a reader to discover it after they have built a business on it.&lt;/p&gt;

&lt;p&gt;Open here also means &lt;em&gt;auditable&lt;/em&gt;. The answer key, the prompts and the scoring for every number above are in the repo. Nobody has to take my word for the table.&lt;/p&gt;

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

&lt;p&gt;The build history is in the repository, and the interesting part is not that an agent wrote most of the code but what the measurements caught that the code review did not: a deployed function that hung for 300 seconds because of an export signature, a latency guard that quietly turned off the headline feature on a cold start, and a classifier that answered a question correctly in a way that was useless to show anyone. All three were found by running the thing, not by reading it.&lt;/p&gt;

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

&lt;p&gt;Named, with the exact job each one does here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it actually does in Chalk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN (Prior Labs)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The price and the win odds — both heads fitted in-context over the pro's own job history, quantiles for the range, classification for the odds, inside a wider prediction agent with tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mastra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The quote-writing agent itself, with &lt;code&gt;market_rate&lt;/code&gt; and &lt;code&gt;similar_jobs&lt;/code&gt; tools it calls mid-quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MongoDB Atlas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job memory: every quote stored with an embedding and recalled by similarity into the next quote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tinker (Thinking Machines)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The quote voice: a LoRA fine-tuned on this pro's own accepted client messages (&lt;code&gt;services/tinker/train_voice.py&lt;/code&gt;, four pairs, Qwen3-8B, rank 16). When it answers, the quote card says &lt;code&gt;voice: tinker lora&lt;/code&gt;; when it does not, the agent's own draft stands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiger Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The won/lost and margin time-series behind the profile screen — one row per quote event, queried live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Temporal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Durable follow-ups — nudge, expire, and an accept signal that survives a restart. Wired end to end in the repo; the deployed demo runs with it switched off and the health endpoint says so, which is why follow-ups there are in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sentry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One trace per compiled quote with per-stage spans; the latency numbers in this post came out of those spans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SerpApi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The live local market rate on every quote — a real number, not a memorised one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hands-free read-back of the client message, and the narration on the demo video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The open-weight models measured head-to-head below — &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; for scope, &lt;code&gt;google/gemma-3-27b-it&lt;/code&gt; and local &lt;code&gt;gemma3:4b&lt;/code&gt; for the eyes — and the on-device path (&lt;code&gt;gemma4:e2b&lt;/code&gt; through Ollama) the provider chain falls back to when there is no network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;a .tech domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The address the demo runs on: &lt;a href="https://hacktoberfestp5vqqk.tech" rel="noopener noreferrer"&gt;https://hacktoberfestp5vqqk.tech&lt;/a&gt;, registered for this build and pointed at the deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these is wired to &lt;em&gt;degrade&lt;/em&gt;, not to break. Pull any of them out and the quote still compiles, and the card says which layer answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single pro.&lt;/strong&gt; No auth, no multi-tenancy: one ledger, one device. A real product needs accounts before it needs anything else here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The win-odds ladder is drawn, not learned, at this ledger size&lt;/strong&gt; — and the card says so, because the classifier's own flat answer was worse than nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 4B on-device model cannot be the estimator.&lt;/strong&gt; Measured: 11% usable. The laptop is the camera, not the accountant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The market benchmark can be wildly off.&lt;/strong&gt; On the run above SerpApi returned $863–$1,438 for a job Chalk priced at $318, because the source it found quotes whole-house move-out cleans. It is labelled as a live external number and it is not allowed to move the price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quote is not a contract.&lt;/strong&gt; The scope is read from photographs and words; the pro still walks the job. The assumptions list exists to make that explicit to the client before they agree to anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client photographs are personal data.&lt;/strong&gt; A quote stores pictures of somebody's home, so the deletion path has to be real rather than promised: &lt;code&gt;DELETE /api/jobs/&amp;lt;id&amp;gt;&lt;/code&gt; removes the record, its photographs and the client's link together and cancels that job's follow-up timer, and the quote view carries the button that calls it. The client link is served from a field allow-list, so the odds and the margin never cross the wire to a stranger; anyone who can reach the URL can still use it on this build, because there is no auth — the same limit as everything else here, and the reason accounts come first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency is the hosted model's, not mine.&lt;/strong&gt; Prior Labs' predict swings from ~4s to over a minute, which is why the first call per cold instance waits longer and every call after it is capped.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Chalk is named for the thing a pro always has in the van: a way to mark the job down before it stops being fresh — and then do a good job of it.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Helio: an open-weight AI that makes you touch grass — and refuses to believe you did</title>
      <dc:creator>JinYi123456</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:36:20 +0000</pubDate>
      <link>https://dev.to/jinyi123456/helio-an-open-weight-ai-that-makes-you-touch-grass-and-refuses-to-believe-you-did-2hm5</link>
      <guid>https://dev.to/jinyi123456/helio-an-open-weight-ai-that-makes-you-touch-grass-and-refuses-to-believe-you-did-2hm5</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;blockquote&gt;
&lt;p&gt;A photo of your keyboard does not count. I checked.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;Helio&lt;/strong&gt; is an offline-first PWA that gets you off the screen and into the world — built on open-weight models, running on your own machine. It does two jobs no other outdoors app does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It tells you &lt;em&gt;when&lt;/em&gt; to go.&lt;/strong&gt; A tiny tabular foundation model — &lt;strong&gt;TabPFN&lt;/strong&gt; — reads ten years of your city's weather history plus the days you personally walked, and scores the next seven days for grass-worthiness. On CPU. In about a second. No training loop — it learns in-context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It verifies that you actually went.&lt;/strong&gt; When you come back and photograph what you found, a local &lt;strong&gt;Gemma&lt;/strong&gt; vision model checks the photo against the mission's proof criteria — &lt;em&gt;on your device&lt;/em&gt;. No upload. If the photo is your desk, the verdict is "not grass".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reviewing the loop visually — the dial that reads a decade of weather, the door that hands you one whisper, the pocket screen that sends you away, the stamp that only a real photo earns:&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%2Fy8w9q89pbrxjnovcx2vd.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%2Fy8w9q89pbrxjnovcx2vd.png" alt="helio's dial — today's grass window and the seed card" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Between those two poles sits the experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the dial&lt;/strong&gt; — a sun-compass that lights up the best outdoor hours of today, over a one-line weather strip (today's high/low, rain, cloud, the week ahead); one tap on &lt;em&gt;use my location&lt;/em&gt; finds your actual sky — Helio reverse-geocodes you, keeps the nearest baked city's decade of history if you're within 50 km, and otherwise builds the climate rules from two years of open weather archive for your own coordinates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the door&lt;/strong&gt; — a Mastra + Gemma &lt;em&gt;mission compiler&lt;/em&gt; writes you a tiny whispered assignment ("Find the oldest standing thing within walking distance…"), and a &lt;strong&gt;Temporal workflow&lt;/strong&gt; holds that mission on a durable timer until its grass window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the pocket screen&lt;/strong&gt; — literally tells you the screen is done now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the proof&lt;/strong&gt; — the on-device verdict, stamped with confidence and what the model saw&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the journal &amp;amp; meadow&lt;/strong&gt; — every verified walk gets a 5-7-5 haiku and grows a meadow with a 12-species field dex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design rule from commit one: &lt;strong&gt;every dependency is a leaf node&lt;/strong&gt;. Sever any edge of the graph and the product still works, labelled honestly — the dial shows climatology when the model is away, hand-written seeds walk in when the LLM is down, the browser reads the whisper when the voice API is missing. It survives airplane mode, backcountry, and a dead wifi router — which is exactly what an outdoors app should do. &lt;em&gt;(Fun failure mode: it also survives the wrong photo. Try your keyboard.)&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://helio-agent.onrender.com" rel="noopener noreferrer"&gt;https://helio-agent.onrender.com&lt;/a&gt; (free Render instance — first load may take ~1 min to wake)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/JinYi123456/helio" rel="noopener noreferrer"&gt;https://github.com/JinYi123456/helio&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;79-second walkthrough&lt;/strong&gt; — the whole loop recorded live: onboarding, helio locating you and re-reading the sky over your own coordinates, the mission compiled by Gemma, the photo verified on-device, the journal that remembers where you were standing, and the meadow grown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/ar8bz7y_dWM" rel="noopener noreferrer"&gt;https://youtu.be/ar8bz7y_dWM&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;90-second local run:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
ollama pull gemma4:e2b
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a laptop with no camera, the proof screen falls back to file upload — the app treats that as a first-class path, not an error.&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%2Fp37bjqncbyy6ry7o8hrg.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%2Fp37bjqncbyy6ry7o8hrg.png" alt="the door — one whisper, one proof criterion" 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%2Fvh82uppglounagx83u4z.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%2Fvh82uppglounagx83u4z.png" alt="the pocket screen — the product that asks to be put away" 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%2Fzpavtc0jgah2sy7vydpp.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%2Fzpavtc0jgah2sy7vydpp.png" alt="the stamp — a real sprout photo, judged on-device at 95%" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The meadow only grows on verified walks — twelve species to meet, one streak to keep alive without lying:&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%2Fl1ajd13jj9qntai1x879.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%2Fl1ajd13jj9qntai1x879.png" alt="the meadow and the species dex" width="780" height="1750"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Three services, one loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web            Vite + React PWA — dial · door · pocket · proof · journal · meadow
services/agent      Hono + Mastra — mission compiler agent, vision verifier, haiku, TTS
services/tabpfn     FastAPI — TabPFN grass-window model + precompute baker
services/tinker     Python — dataset builder + LoRA fine-tune of Helio's whisper voice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grass window is the heart. The whole preference chain is ~20 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# services/tabpfn/app/service.py
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;precompute_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# tiny host? serve the bake
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_precomputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# TabPFN, in-context, on CPU
&lt;/span&gt;    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;served&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;_mark_model_failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# 600s cooldown — never retry-storm
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_precomputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# bake wins next
&lt;/span&gt;        &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;served&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;precomputed-fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heuristic_scores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# still honest
&lt;/span&gt;        &lt;span class="n"&gt;served&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heuristic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mission compiler is one Mastra agent with three tools (grass window, local ground notes, field memory) and a zod'd draft schema. Every mission — model-written or fallback — carries &lt;strong&gt;photo-checkable proof criteria&lt;/strong&gt;, because a mission you can't verify isn't a mission, it's a vibe.&lt;/p&gt;

&lt;p&gt;The vision verdict comes back as strict JSON and is clamped on the way in — a model that hallucinates "confidence: 500" can't stamp anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/agent/src/lib/vision.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verdictFromRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// … clamp confidence to [0,1], default `seen`, mark `loose` when the&lt;/span&gt;
  &lt;span class="c1"&gt;// pass/fail flag is missing rather than trusting the model's prose.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests are the contract: &lt;strong&gt;44 vitest specs&lt;/strong&gt; across tools, verdicts, windows, seeds, location matching and the durability layer, plus &lt;strong&gt;21 pytest specs&lt;/strong&gt; for the forecast chain and the bring-your-own-sky coordinates (the model specs run against the real license-accepted model — see below). CI runs both on every push, and a daily GitHub Action re-bakes the grass windows on the real model and commits the JSON — the free-tier host just serves the bake.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Grounded in the real runtime, not in guesses.&lt;/strong&gt; Every claim in this section was verified by running the thing: model versions, ports, timings, and every failure below is a real failure from the build log (kept in &lt;code&gt;docs/building-log.md&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And when the walk ends, the local model writes it a 5-7-5 haiku and files it away — photo, verdict, weather, stamp, tags:&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%2Fpgq1mph84nkbxrg6oda8.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%2Fpgq1mph84nkbxrg6oda8.png" alt="the field journal — one verified page" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN's weights are license-gated.&lt;/strong&gt; The first server boot tried to open a browser for a one-time license acceptance at 2 AM — and hung waiting on stdin. A server must never block on an interactive login. Fix that became a feature: &lt;code&gt;TABPFN_NO_BROWSER=1&lt;/code&gt;, a 600-second failure cooldown, and the fallback chain above. The polite fix for tiny hosts: &lt;code&gt;app/precompute.py&lt;/code&gt; bakes windows by hand or in CI; a daily Action refreshes them so the hosted demo always serves fresh, model-scored JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking models don't stop thinking.&lt;/strong&gt; &lt;code&gt;gemma4:e2b&lt;/code&gt; is a thinking model — without suppression it burns its whole budget reasoning and never answers. On Ollama's native API &lt;code&gt;think:false&lt;/code&gt; fixes it; over the OpenAI-compatible endpoint you inject &lt;code&gt;reasoning_effort:"none"&lt;/code&gt; via &lt;code&gt;transformRequestBody&lt;/code&gt;. Verified: three clean tokens back instead of a 4,000-token monologue. It's in the code as a provider-scoped transform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your walks post-date the weather archive.&lt;/strong&gt; Personal "I walked today" labels were silently matching nothing — the archive ends before today. Fix: synthesize feature rows from the walk-time weather (which the app records anyway) and fill the rest from same-day climatology. Now the model actually learns &lt;em&gt;your&lt;/em&gt; good days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React StrictMode raced the mission compiler.&lt;/strong&gt; Dev double-mount meant two ~70-second Gemma missions started in parallel; one finished and rendered, the other overwrote state — same walk, two different mission titles. An in-flight ref guard fixed it. Production was never affected, but a confused user would have been — and a confused &lt;em&gt;me&lt;/em&gt; at 3 AM definitely was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free instances are 512MB; torch is not.&lt;/strong&gt; So the deployed grass-window service deliberately does not install tabpfn — it serves CI-baked windows, recomputes live forecasts when it can, and keeps heuristic scoring as the floor. Zero paid anything, honest &lt;code&gt;served&lt;/code&gt; field on every response so you always know which layer answered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry's "one-line" Mastra integration silently did nothing.&lt;/strong&gt; Node reads an ESM module graph &lt;em&gt;before&lt;/em&gt; any module body runs, so &lt;code&gt;init()&lt;/code&gt; called from app code lands after &lt;code&gt;@mastra/core&lt;/code&gt; has already loaded — and already-loaded modules are never instrumented. The fix that made it real: a four-line, DSN-gated &lt;code&gt;node --import&lt;/code&gt; preload, plus a constructed &lt;code&gt;Mastra&lt;/code&gt; instance (a lone &lt;code&gt;Agent&lt;/code&gt; emits nothing to an exporter). Verified against exactly what Render runs: every compile is now one &lt;code&gt;gen_ai&lt;/code&gt; trace with token usage — and with no DSN, Sentry never enters the process at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mission is a promise with a deadline — that's a workflow, not a cron.&lt;/strong&gt; "Walk when the window opens, prove it before it closes" is exactly the shape Temporal exists for, so every compiled mission starts a &lt;code&gt;missionLifecycle&lt;/code&gt; workflow: sleep until the TabPFN-picked golden hour, a 6-hour expiry, and one &lt;code&gt;markDone&lt;/code&gt; signal the verifier fires when your photo passes. The workflow races &lt;code&gt;sleep()&lt;/code&gt; against &lt;code&gt;condition(done)&lt;/code&gt; and honestly returns &lt;code&gt;"verified"&lt;/code&gt; or &lt;code&gt;"missed"&lt;/code&gt;. Verified for real: a mission that signalled during its sleep completed as &lt;code&gt;Result: "verified"&lt;/code&gt; (2m33s wall time, timer cancelled), and a probe workflow woke at its golden hour, POSTed a &lt;code&gt;ripe&lt;/code&gt; event back into the agent, then closed as &lt;code&gt;"missed"&lt;/code&gt; — all visible in the Temporal UI. With no &lt;code&gt;TEMPORAL_ADDRESS&lt;/code&gt;, the mission still compiles and the API says &lt;code&gt;lifecycle: { scheduled: false }&lt;/code&gt; — leaf node, as promised.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What It Can't Do (Yet)
&lt;/h2&gt;

&lt;p&gt;Honest limits, because the meadow only grows on verified walks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It can't prove &lt;em&gt;you&lt;/em&gt; took the photo.&lt;/strong&gt; The vision check verifies content, not authorship — no location attestation, no sensor checks. A determined cheater with a stock photo of moss could still fool it. Helio is a nudge with a memory, not a cop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hosted demo answers for any coordinates, but only three skies are pre-baked.&lt;/strong&gt;
&lt;em&gt;Use my location&lt;/em&gt; reverse-geocodes where you are: within 50 km of a baked city you get that city's sky with an honest distance note; anywhere else Helio rounds your coordinates to three decimals and keys the whole pipeline off them — a two-year
climate archive fetched on demand, percentile-derived label rules, and a live
forecast whenever the free forecast endpoint will answer a datacenter IP (archived
climatology for your exact coordinates when it won't). With Torch on your machine
that sky scores on the real model; on the free-tier host it falls back to the
heuristic chain, and the &lt;code&gt;served&lt;/code&gt; field always says which layer answered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deployed window service serves precomputed scores.&lt;/strong&gt; Torch doesn't fit a 512MB
free instance, so the real model runs in the daily GitHub Action bake and the service
recomputes live forecasts on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-device verdicts take ~45 seconds on CPU.&lt;/strong&gt; That's the honest price of the photo
never leaving your machine. The hosted demo uses cloud Gemma instead; locally, put the
kettle on while it judges your grass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No accounts, no cross-device sync&lt;/strong&gt; unless you point it at MongoDB Atlas; photos and
logs live in your browser's IndexedDB, exportable and burnable by you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hosted demo doesn't run a Temporal cluster.&lt;/strong&gt; Durable lifecycle timers need a
Temporal server next to the worker (&lt;code&gt;temporal server start-dev&lt;/code&gt; + &lt;code&gt;npm run temporal:worker&lt;/code&gt;); on the hosted build &lt;code&gt;/api/health&lt;/code&gt; reports &lt;code&gt;temporal: false&lt;/code&gt; and missions are simply not scheduled — everything else works.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Every app that "helps you go outside" should itself be able to survive outside. Closed, server-dependent AI fails exactly when you need it most — on a mountain, on a plane, at the edge of a network. Helio inverts the default: the model runs on your machine, your photos never leave the device, and the whole thing costs nothing to run because there's nothing to meter.&lt;/p&gt;

&lt;p&gt;There's also a philosophical bit I care about: an app that gamifies touching grass shouldn't demand that you stare at it. Helio's pocket screen literally sends you away. Its memory of your walks lives in your browser's IndexedDB, exportable and burnable by you. The weights are open — ollama today, llama.cpp tomorrow, the model belongs to everyone. When the product's pitch is "own your attention", the stack should practice what it preaches. That's what open weights make possible: a tool that can be &lt;em&gt;entirely yours&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;I built Helio in an agentic coding session, and the most useful pattern was treating the agent as an accountable colleague rather than an autocomplete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research before code.&lt;/strong&gt; The session started with the partner stack: what TabPFN actually is (in-context tabular foundation model — perfect for weather+habit scoring), which ports were free, which model names were real. Several "obvious" choices died in research rather than in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything ran for real.&lt;/strong&gt; No mocked model calls. The full loop was walked in a browser against real services — the screenshots in this post are that session, including a real sprout photo stamped &lt;strong&gt;95% sure, checked in 42s on CPU&lt;/strong&gt; — and a keyboard photo was &lt;strong&gt;correctly rejected&lt;/strong&gt; on an earlier pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors became features.&lt;/strong&gt; The three nastiest bugs (license-gated weights, thinking token burn, StrictMode race) each ended up &lt;em&gt;shaping the architecture&lt;/em&gt; — the cooldown, the provider-scoped transform, the in-flight guard. That's the log in &lt;code&gt;docs/building-log.md&lt;/code&gt;, kept unedited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests as the contract.&lt;/strong&gt; Before touching the vision normalizer or the fallback chain, specs existed for the ugly cases: null garbage, confidence clamping, missing flags, single-hour windows. The agent's output could then be trusted precisely where trust was earned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest version: the agent was brilliant at breadth (research, scaffolding, tests) and needed supervision at depth (the 2 AM browser-hang was &lt;em&gt;my&lt;/em&gt; reviewer instinct catching a hang the code was happy to wait out). The best results came from the loop — build, verify on the real runtime, and let the failures teach.&lt;/p&gt;

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

&lt;p&gt;All 16 partner categories, with receipts where Helio truly uses the tech (every one of them degrades gracefully when absent — try removing any):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;In Helio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;render.yaml&lt;/code&gt; blueprint: agent (Node, serves the PWA) + grass-window service (Python, precompute mode). Free tier, two services, one blueprint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The grass window itself — in-context over 10y of weather (or a 2-year archive fetched on demand for coordinates you bring), personal labels from real walks, CPU inference, license-aware fallback chain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Two jobs: the mission compiler (via Mastra) and the photo proof verifier — both on-device via Ollama.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mastra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The mission compiler agent: three tools, multi-step, structured draft + repair pass.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whisper TTS for missions, disk-cached; falls back to browser speech.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SerpApi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"What's in season right now" ground notes folded into missions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MongoDB Atlas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Field memory over walk notes — &lt;code&gt;$vectorSearch&lt;/code&gt; when configured, RAM otherwise.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sentry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real agent tracing: a DSN-gated &lt;code&gt;--import&lt;/code&gt; preload wires runtime instrumentation, so every mission compile lands in Sentry as one trace — &lt;code&gt;http.server → mission.compile → gen_ai.invoke_agent → gen_ai.chat → http.client&lt;/code&gt; — with model name, input messages and token usage as &lt;code&gt;gen_ai.*&lt;/code&gt; attributes. API errors are captured too.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tinker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Helio's whisper voice as a trainable artifact: &lt;code&gt;services/tinker/&lt;/code&gt; distills the hand-written field corpus into context→whisper pairs and fine-tunes a small open-weight model over Tinker's SDK; the agent then samples whispers through Tinker's OpenAI-compatible endpoint. Wired end-to-end and env-gated — the training run is waiting on the account's billing/credit activation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Temporal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The durable mission lifecycle: every mission is a &lt;code&gt;missionLifecycle&lt;/code&gt; workflow — timer to the TabPFN-picked golden hour, 6-hour expiry, &lt;code&gt;markDone&lt;/code&gt; signal from the verifier. Verified locally end-to-end: signal → &lt;code&gt;Result: "verified"&lt;/code&gt;; timer → &lt;code&gt;ripe&lt;/code&gt; + &lt;code&gt;missed&lt;/code&gt; events posted back to the agent. No &lt;code&gt;TEMPORAL_ADDRESS&lt;/code&gt;? The mission compiles anyway, honestly unscheduled.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiger Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The time-series shadow of the meadow: walks land in a &lt;code&gt;footprints&lt;/code&gt; hypertable (TimescaleDB, via &lt;code&gt;pg&lt;/code&gt;) and &lt;code&gt;GET /api/footprints&lt;/code&gt; returns the server-side 7-day series. Wired end-to-end and env-gated: no &lt;code&gt;TIGER_URL&lt;/code&gt; → the endpoint answers &lt;code&gt;{enabled:false}&lt;/code&gt; and the app is untouched; with a URL it's one key away.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DigitalOcean&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not used this week — Render carries the hosted demo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not used — Helio's memory layer must run offline-first; a hosted control plane is against the grain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entire&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not used.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not used.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Arduino&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not used — no hardware in this build… yet. A sun-dial lamp that glows when your window opens is the obvious next step.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Helio: proof of grass. MIT-licensed, photos never leave your device, and the meadow only&lt;br&gt;
grows when you actually go. See you outside.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
