<?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: StarKnight</title>
    <description>The latest articles on DEV Community by StarKnight (@starknight).</description>
    <link>https://dev.to/starknight</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%2F908158%2Fe2dfd532-53d7-4ee5-8d00-7b53d2be96b3.jpg</url>
      <title>DEV Community: StarKnight</title>
      <link>https://dev.to/starknight</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/starknight"/>
    <language>en</language>
    <item>
      <title>Waymark: a trail guide you listen to, written by Gemma 4 in your browser</title>
      <dc:creator>StarKnight</dc:creator>
      <pubDate>Mon, 05 Oct 2026 22:35:41 +0000</pubDate>
      <link>https://dev.to/starknight/waymark-a-trail-guide-you-listen-to-written-by-gemma-4-in-your-browser-3pgf</link>
      <guid>https://dev.to/starknight/waymark-a-trail-guide-you-listen-to-written-by-gemma-4-in-your-browser-3pgf</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;Most trail apps ask you to look at them. At a junction you take the phone out, wake the screen and find the blue dot, and for that moment you are looking at a map instead of the trail. This week's theme asks for builds where the screen is the shortest part of the experience, so I built a trail guide that you listen to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waymark&lt;/strong&gt; turns a real trail into a short spoken guide. You pick a trail, or open a GPX file, and Waymark works out the facts from open data: where the junctions are and which way to go at each one, where the climbs start and how steep they are, where the water, bridges, steps and viewpoints are, and how long the walk should take. Gemma 4 E2B, an open-weight model running inside the browser, writes one or two plain sentences for each of those waymarks and a short briefing for the whole walk.&lt;/p&gt;

&lt;p&gt;Before you leave, a 3D model of the real terrain previews the walk in about a minute, with every cue shown where it will be spoken. Then you send the guide to your phone and put the phone in your pocket. Pocket mode keeps the screen dark, follows GPS and speaks each cue when you reach that waymark, for example: "Continue straight onto Mist Trail. John Muir Trail on the right is not your route." On a two-hour walk, you look at the screen for about a minute.&lt;/p&gt;

&lt;p&gt;It is for walkers and hikers on trails they do not know well, and for anyone who would rather keep their eyes on the path. A group can share one link.&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%2F8uptz0d63gg01re09aam.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%2F8uptz0d63gg01re09aam.gif" alt="A flyover of the Mist Trail in Yosemite, with each cue shown where it will be spoken"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://starknightt.github.io/waymark/" rel="noopener noreferrer"&gt;https://starknightt.github.io/waymark/&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick a demo trail.&lt;/strong&gt; Mist Trail (Yosemite, USA), Lake Agnes Trail (Banff, Canada), Galu Devi to Triund (Himachal Pradesh, India) and the Pen y Fan and Corn Du circular walk (Wales, UK). Their guides were written by Gemma 4 E2B in Chrome and are saved with the site, so nothing large is downloaded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview the walk.&lt;/strong&gt; The camera flies along the route and slows at each waymark. Tick "Read the cues aloud" to hear the guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the facts behind each cue.&lt;/strong&gt; Each cue then lists the facts it was checked against, and what the checker changed, if anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk it with your phone.&lt;/strong&gt; Scan the QR code, then press "Try a simulated walk". It replays the route at eight times walking speed and speaks each cue as the simulated walker reaches it. "Start walk" uses real GPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a guide yourself.&lt;/strong&gt; On a desktop with Chrome or Edge, press "Write it again on this device" on any demo trail, or "Make a guide for a trail" to search for a mapped route, paste an OpenStreetMap route link or open a GPX file. The first time, the browser downloads the model (2.0 GB) and keeps it for next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The phone view during a simulated walk. Each cue is also shown as text when it is spoken:&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%2Fbu8ukj47ognjh31kubjy.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%2Fbu8ukj47ognjh31kubjy.gif" alt="Pocket mode on a phone, speaking cues during a simulated walk on the Lake Agnes Trail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemma writing a guide for the Triund trail in the browser, with nothing mocked (sped up 2 times):&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%2Fhr8uv0mrvfzwkqgciglp.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%2Fhr8uv0mrvfzwkqgciglp.gif" alt="Gemma 4 E2B writing the Triund guide inside the browser"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each cue next to the facts it came from:&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%2Fji9al3ll1aqz2443erf7.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%2Fji9al3ll1aqz2443erf7.png" alt="A cue with the facts it was checked against"&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/StarKnightt" rel="noopener noreferrer"&gt;
        StarKnightt
      &lt;/a&gt; / &lt;a href="https://github.com/StarKnightt/waymark" rel="noopener noreferrer"&gt;
        waymark
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A trail guide you listen to. Gemma 4 runs in your browser and writes spoken cues for real trails from open map data.
    &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;Waymark&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A trail guide you listen to.&lt;/strong&gt; Pick a real trail. Waymark builds the facts from OpenStreetMap and open elevation data, and Gemma 4 E2B, running in your browser on WebGPU, writes a short spoken cue for every junction, climb and landmark. A 3D model of the real terrain previews the walk in about a minute. Then the phone goes in your pocket: it keeps the screen dark and speaks each cue when you reach that waymark.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://starknightt.github.io/waymark/" rel="nofollow noopener noreferrer"&gt;https://starknightt.github.io/waymark/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/waymark/docs/flyover.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Fwaymark%2FHEAD%2Fdocs%2Fflyover.gif" alt="A flyover of the Mist Trail with its spoken cues"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 1 (Touch Grass). The repository was started on October 5, 2026, inside the challenge window.&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;Four demo trails&lt;/strong&gt; that open instantly: Mist Trail (Yosemite, USA), Lake Agnes Trail (Banff, Canada), Galu Devi to Triund (Himachal Pradesh, India) and the Pen y Fan and Corn Du circular walk (Wales, UK). Their guides were written by Gemma 4 E2B in Chrome…&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/StarKnightt/waymark" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;TypeScript, Vite, Three.js and LiteRT-LM, under the MIT license. All of the code was written during the challenge window, starting on October 5.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Facts first, then the model
&lt;/h3&gt;

&lt;p&gt;A spoken guide is worse than useless if it says left when the path goes right, and a 2B model cannot do geometry. So the work is split. Code builds the facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Junctions&lt;/strong&gt; come from the OpenStreetMap path network. Wherever other paths meet the route, Waymark measures the bearing of every branch over about 22 m and decides whether the route turns left or right, keeps left or right at a fork, or continues straight, and which of the other ways are not the route. When two ways leave on the same side, it says which one to take.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Climbs and descents&lt;/strong&gt; come from open elevation tiles: sections steeper than 7% that gain at least 40 m, with their length, gain, average grade and top elevation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landmarks near the path:&lt;/strong&gt; drinking water, springs, waterfalls, viewpoints, summits, huts, bridges, flights of steps, lakes that come into view, and where the path enters or leaves woodland.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walking time&lt;/strong&gt; uses Tobler's hiking function on the elevation profile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are merged into waymarks, between 5 and 22 on the twelve test trails, each a short list of facts. The start, the finish and every junction where the route turns or moves onto a different path are marked as required. This is what Gemma sees for one waymark on the Mist Trail, wrapped here to fit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w8 (REQUIRED): junction: continue straight onto "Mist Trail";
  "John Muir Trail" on the right is not your route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gemma 4 E2B in the browser
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; the JavaScript API of LiteRT-LM from Google AI Edge, on WebGPU. The model file is &lt;code&gt;gemma-4-E2B-it-web.litertlm&lt;/code&gt; (2.0 GB). The browser downloads it once into its private file storage (OPFS), and after that it loads in a few seconds (2.3 s on my PC).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One constrained tool call:&lt;/strong&gt; &lt;code&gt;write_guide({ cues: [{ waymark, text }], briefing })&lt;/code&gt;. The &lt;code&gt;waymark&lt;/code&gt; field is an enum of the real waymark IDs, so the model cannot invent a waymark, and constrained decoding guarantees the call can be parsed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules in the prompt:&lt;/strong&gt; use only the facts listed for that waymark, give directions exactly as written, one or two short sentences, and a cue for every required waymark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of a guide:&lt;/strong&gt; about 1,400 prompt tokens and 700 output tokens, 18 seconds on my PC's RTX 4060.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A checker that knows what each number means
&lt;/h3&gt;

&lt;p&gt;Every cue is checked against the facts of its own waymark before it is saved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numbers&lt;/strong&gt; must exist in the facts with the same meaning. The words around a number decide its role: "gains 70 m" is a gain, "topping out at 1,980 m" is an elevation and "the next 450 m" is a length. Numbers written as words, such as "an hour and a half", are checked as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names&lt;/strong&gt; must exist on the route, including names in other scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directions&lt;/strong&gt; must match the junction. A wrong side is swapped, an invented turn is removed, and a turn that went missing is restored from the facts. When a waymark holds two junctions close together, both turns are allowed, in order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features:&lt;/strong&gt; a summit, waterfall or bridge can only be mentioned where the facts mention one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sentence that fails is removed. If nothing safe is left, the cue is rebuilt from the facts. The checker has 35 unit cases that run in Chrome, and most of them come from real mistakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What went wrong along the way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One call instead of many.&lt;/strong&gt; My first design asked Gemma to call &lt;code&gt;add_cue&lt;/code&gt; once per waymark and &lt;code&gt;set_briefing&lt;/code&gt; once. It called &lt;code&gt;set_briefing&lt;/code&gt; and stopped, in every test. A single &lt;code&gt;write_guide&lt;/code&gt; call with an array fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every number was right and the sentence was wrong.&lt;/strong&gt; "Gain 730 meters in 1.5 km." Both numbers were in the facts: 730 m was the length of the climb and 1.5 km was its distance from the start. A checker that only looks for numbers passed it. That is why numbers now carry a role, and why the prompt no longer includes each waymark's distance from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My first checker was wrong more often than the model.&lt;/strong&gt; In its first full run over 12 trails it changed 12 cues. Four of those changes were right. Five turned a correct cue into a wrong one: on the Dipsea Trail, two junctions 40 m apart share a waymark ("turn left onto Hazel Avenue, then right onto Hale Lane"), and the checker "corrected" the second turn to match the first. Three more removed correct sentences, such as "Find the Sharma store on your right", rejected because "Find the Sharma" looked like an unknown place. Each of these is now a unit case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrases copied between waymarks.&lt;/strong&gt; Gemma sometimes repeated "the path straight ahead is not your route" at a junction with no such path. The checker now requires every side path it mentions to be listed in that junction's facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map data needs care.&lt;/strong&gt; I had mapped OpenStreetMap's wetland areas to water, so the bogs around Pen y Fan were drawn as lakes, and the Merced River's water area produced "a lake comes into view on your right" on the Mist Trail. Forest areas also run over 3,000 m ridges, so the 3D view applies a treeline estimated from latitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A colour space bug.&lt;/strong&gt; &lt;code&gt;THREE.Color&lt;/code&gt; converts colours to linear light, so my tree placement compared sRGB pixels with linear values and matched nothing. Every mountain was bare until I read the hex value directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map servers.&lt;/strong&gt; From my network only one of the three public Overpass servers answered, and slowly. The demo trails are built ahead of time from the OpenStreetMap API, the app tries the three servers in turn, and if none answers it still builds a guide from elevation alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;Twelve trails in seven countries, Chrome on an RTX 4060 desktop GPU, greedy decoding. The second column asks for the same guide as JSON in plain text, without constrained decoding.&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;One constrained tool call (shipped)&lt;/th&gt;
&lt;th&gt;JSON in plain text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cues written by Gemma&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passed every check unchanged&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;td&gt;170&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed by the checker&lt;/td&gt;
&lt;td&gt;9 (8 of them on Mount Takao)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required waymarks covered&lt;/td&gt;
&lt;td&gt;120 of 120&lt;/td&gt;
&lt;td&gt;120 of 120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output that could not be used&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per guide&lt;/td&gt;
&lt;td&gt;18.2 s&lt;/td&gt;
&lt;td&gt;11.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decoding speed&lt;/td&gt;
&lt;td&gt;44 tokens/s&lt;/td&gt;
&lt;td&gt;80 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight of the nine changes were on Mount Takao, where Gemma translated or romanised Japanese path names, for example "Takao Mountain Line" for 高尾山線. The checker replaced them with the names on the map and kept the turns.&lt;/p&gt;

&lt;p&gt;The result I did not expect: Gemma 4 E2B followed the JSON format in plain text on every trail, so constrained decoding mainly bought certainty, at a cost of about 45% of the decoding speed. I kept it, because a guide for a trail nobody has tested must always parse. Most of the accuracy came from the facts and the checker rather than from the decoding method.&lt;/p&gt;

&lt;p&gt;Two more checks. On the live site, in a fresh browser, the first guide took about 10 minutes, nearly all of it the 2.0 GB download on my connection; writing the guide itself took about 20 seconds. All 62 cues of the four demo guides were also checked line by line against their source facts during development. After the checker, none of them gives a wrong turn, distance or climb, though a few read awkwardly, such as "There is a bridge, Mist Trail.", where the bridge is mapped as part of the Mist Trail. One mistake the checker cannot see is in a briefing: the Pen y Fan briefing says the walk reaches the summit of Pen y Fan, while the map data puts the summit 250 m from the route.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it cannot do yet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;It is not a navigation or safety device. Carry a map and check conditions.&lt;/li&gt;
&lt;li&gt;Writing a guide needs WebGPU and about 2 GB of storage, so in practice a desktop or laptop. Any phone can play the guide.&lt;/li&gt;
&lt;li&gt;The cues are only as good as the map. An unmapped junction is missed, and English voices read names in other scripts, such as 高尾山線 on Mount Takao, poorly.&lt;/li&gt;
&lt;li&gt;The elevation data has a resolution of roughly 10 to 30 m, so sharp summits read lower than their true height.&lt;/li&gt;
&lt;li&gt;On a phone the page has to stay open with the screen awake for GPS and speech to keep working. Waymark keeps the screen dark to save power.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Where you walk, and when, is personal.&lt;/strong&gt; In January 2018, Strava's public heatmap of its users' activity revealed the locations and patrol routes of military bases. A trail plan says where you will be and when you will be away from home. With an open model running in the browser, the route, the guide and your position never go to an AI provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to work without signal.&lt;/strong&gt; Trails are often where coverage fails. The guide is written before you leave and fits in a link of about 1.5 KB, so on the trail the phone needs neither a network nor a model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could control how the model writes, not only what I ask it.&lt;/strong&gt; Constrained decoding with my own schema, where the only valid waymarks are the real ones, and a 12-trail evaluation that I could re-run as often as I liked, at no cost. Most of the mistakes described above were found by those repeated runs. With a paid API I would have run the evaluation less often and found fewer of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is open all the way down.&lt;/strong&gt; OpenStreetMap data, open elevation tiles, an Apache-2.0 model, an Apache-2.0 runtime and MIT code. Anyone can host Waymark as static files, swap in another model, or adjust the facts for the trails where they live.&lt;/p&gt;

&lt;p&gt;The trade-off is a 2.0 GB first download and a desktop GPU to write guides. A larger hosted model would write more varied sentences, but for spoken directions being right matters more than style, and that comes from the facts and the checker, which work the same way with an open model on my own machine.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 E2B is the only model in Waymark, and it runs entirely in the browser through LiteRT-LM on WebGPU. It writes every spoken cue and every briefing through a constrained tool call whose schema only allows real waymarks. Every demo guide on the site was written by it, and "Write it again on this device" runs it live.&lt;/p&gt;

&lt;p&gt;Thanks for reading. If you try Waymark on a trail near you, I would like to hear how the cues held up.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>webgpu</category>
    </item>
    <item>
      <title>I built my grandmother a tiny island that keeps track of her day, with Gemma running inside the browser</title>
      <dc:creator>StarKnight</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:15:53 +0000</pubDate>
      <link>https://dev.to/starknight/i-built-my-grandmother-a-tiny-island-that-keeps-track-of-her-day-with-gemma-running-inside-the-gha</link>
      <guid>https://dev.to/starknight/i-built-my-grandmother-a-tiny-island-that-keeps-track-of-her-day-with-gemma-running-inside-the-gha</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;I built this for my grandmother.&lt;/p&gt;

&lt;p&gt;Because of her age, she can't remember a lot of things these days, and keeping track of her daily routine, such as when to take which medicine, has become hard for her. I wanted to give her something calm and simple that shows her day and what is left to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Little World&lt;/strong&gt; is a daily routine tracker shown as a small 3D island. You describe the routine in plain English. Gemma, an open model running inside the browser, turns the description into a schedule. Each task appears on the rim of the island at its time of day, and the sun and moon move across the sky with the real clock, so the current time and the next task are visible at a glance.&lt;/p&gt;

&lt;p&gt;This is the example routine I used during development. It is made up, not hers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thyroid tablet, 50 mcg, when I wake up, on an empty stomach.&lt;br&gt;
Blood pressure tablet, Amlodipine 5 mg, after breakfast.&lt;br&gt;
Metformin 500 mg before lunch and before dinner.&lt;br&gt;
Eye drops in both eyes at 4 PM.&lt;br&gt;
Calcium tablet after dinner, not with the thyroid tablet.&lt;br&gt;
A 20 minute walk at 6 PM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a task is due, its marker lights up and a reminder appears. Pressing &lt;strong&gt;Done&lt;/strong&gt; marks it complete. The &lt;strong&gt;Ask&lt;/strong&gt; tab answers questions such as "What's next?" or "Did I take my eye drops?" using only the routine that was entered.&lt;/p&gt;

&lt;p&gt;The routine never leaves the device. There is no account, no server and no analytics, and after the first load the app works offline. For something as personal as a medicine list, that was a firm requirement.&lt;/p&gt;

&lt;p&gt;Little World is not a medical tool, and the app says so. It only repeats what it was told. When I asked "Can I take calcium with my thyroid tablet?", it answered that it could not see that in its notes and that I should check with a doctor or pharmacist, which is the behavior I wanted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Showing it to my grandmother
&lt;/h3&gt;

&lt;p&gt;I showed Little World to my grandmother. Her reaction was "oh wow, this is so good". Then she said "you made something for me", and she was genuinely pleased.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designed for an older user
&lt;/h3&gt;

&lt;p&gt;After that, I focused on making it easier to use for someone her age:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"What do I do now?"&lt;/strong&gt; A single button opens a full-screen view with one task in very large type, one large "I've done it" button with an undo option, and the next few tasks below. It can be set as the default screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spoken reminders.&lt;/strong&gt; Reminders can be read aloud with the browser's built-in speech synthesis, so no external voice service is involved. If a task is still not done, the reminder repeats after 20 and 40 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup by a family member.&lt;/strong&gt; Gemma needs a computer with a capable GPU, but the person using the routine does not. A family member can enter the routine on a computer, select "Send to another device", and share the QR code or link. The routine is encoded in the part of the link after the &lt;code&gt;#&lt;/code&gt;, which browsers do not send to a server. Opening the link on a phone or tablet shows the routine and asks for confirmation before using it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability.&lt;/strong&gt; Larger text throughout, buttons at least 48 pixels tall, labels that stay readable against the daytime sky, and task states written as words ("Now", "Waiting", "Done") rather than shown only by color. A reduced-motion setting turns on automatically when the device requests it.&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%2Fxd8wdk6uvi3m9s2qvurz.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%2Fxd8wdk6uvi3m9s2qvurz.png" alt="The " 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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1nz0d2te44tr9lgeushx.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%2F1nz0d2te44tr9lgeushx.gif" alt="Sending the routine to another device, opening it there and marking a task done" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://starknightt.github.io/little-world/" rel="noopener noreferrer"&gt;https://starknightt.github.io/little-world/&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Try the demo island"&lt;/strong&gt; works on any device, including phones. It loads a routine that Gemma already parsed, so nothing is downloaded. From there, "What do I do now?" opens the large view and "Send to another device" shows the QR code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Describe my own routine"&lt;/strong&gt; requires desktop Chrome or another browser with WebGPU. Gemma 2 2B is downloaded once (about 1.4 GB) and stored by the browser. After that it loads in a few seconds and works offline.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08en7vz45nks1oi3s2q2.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%2F08en7vz45nks1oi3s2q2.gif" alt="A full day passing on the island" width="600" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clip below shows the real model with nothing mocked: I enter a routine, Gemma parses it, I review the preview and save it, then ask two questions. The GIF runs at 2.5x speed; the &lt;a href="https://github.com/StarKnightt/little-world/blob/main/docs/demo.mp4" rel="noopener noreferrer"&gt;27-second video&lt;/a&gt; runs at 1.25x.&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%2Fp2fkm4wp66w1b07rvriy.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%2Fp2fkm4wp66w1b07rvriy.gif" alt="Gemma parsing a routine in the browser and answering questions" width="640" height="360"&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%2Fd59m69rjxo15tn3itc4n.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%2Fd59m69rjxo15tn3itc4n.png" alt="The editable preview of Gemma's output" width="800" height="450"&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%2Fq1dssnue5861n34m5sfl.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%2Fq1dssnue5861n34m5sfl.png" alt="Asking " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same routine at night, and on a phone:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyyc9jl3v420f9nf2148.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%2Fjyyc9jl3v420f9nf2148.png" alt="Night view with all tasks done" 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%2Fr4kkchl3vunt09pvoq7l.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%2Fr4kkchl3vunt09pvoq7l.png" alt="Phone view" width="780" height="1688"&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/StarKnightt" rel="noopener noreferrer"&gt;
        StarKnightt
      &lt;/a&gt; / &lt;a href="https://github.com/StarKnightt/little-world" rel="noopener noreferrer"&gt;
        little-world
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A daily routine tracker on a small 3D island. Gemma runs in the browser through WebGPU, and nothing leaves the device.
    &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;Little World&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A daily routine tracker shown as a small 3D island.&lt;/strong&gt; Describe a routine in plain English ("Thyroid tablet when I wake up, Metformin before lunch and dinner, eye drops at 4 PM"). Gemma, running entirely in the browser, turns it into a validated schedule. Each task is placed on the rim of the island at its time of day, and the sun and moon follow the real clock. Mark a task as done when it is complete. Ask "What's next?" and Gemma answers using only the routine.&lt;/p&gt;
&lt;p&gt;Nothing leaves your device. No account, no server, no analytics. After the first load it works offline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://starknightt.github.io/little-world/" rel="nofollow noopener noreferrer"&gt;https://starknightt.github.io/little-world/&lt;/a&gt; (open &lt;code&gt;?demo&lt;/code&gt; for the instant demo, no download)&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/hero.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Fhero.gif" alt="Little World, a day passing on the island"&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;Day&lt;/th&gt;
&lt;th&gt;Night&lt;/th&gt;
&lt;th&gt;Phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/day.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Fday.png" alt="Day"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/night.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Fnight.png" alt="Night"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/mobile.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Fmobile.png" alt="Mobile"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Big view&lt;/th&gt;
&lt;th&gt;Family setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/bigview.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Fbigview.png" alt="What do I do now?"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/StarKnightt/little-world/docs/family.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FStarKnightt%2Flittle-world%2FHEAD%2Fdocs%2Ffamily.gif" alt="Send to another device, then open it there"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not medical advice.&lt;/strong&gt; Little World only repeats what you told it. It does not know about medicines, doses or interactions, and it is told to send those…&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/StarKnightt/little-world" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Built with Vite, TypeScript, Three.js, WebLLM and zod, under the MIT license. All code in the repository was written during the challenge weekend, starting October 4.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 2 2B&lt;/strong&gt; (&lt;code&gt;gemma-2-2b-it-q4f16_1-MLC&lt;/code&gt;), Google's open-weight model, quantized to 4 bits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebLLM&lt;/strong&gt; from MLC, which runs the model on the GPU through WebGPU. It runs in a Web Worker so the 3D scene stays responsive while the model is working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three.js&lt;/strong&gt; for the island, with a custom sky shader, bloom and HTML labels that avoid overlapping each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zod&lt;/strong&gt; to validate every routine before it is saved, and &lt;strong&gt;vite-plugin-pwa&lt;/strong&gt; for offline support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;routine text
  → Gemma 2 2B, constrained by a grammar
  → JSON, one object per task: quoted source text, name, dose, time anchor, food rule, icon
  → quote check (plain code)
  → zod validation (one retry if it fails)
  → time anchors converted to clock times
  → editable preview
  → saved routine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. A grammar instead of asking for JSON.&lt;/strong&gt; WebLLM can constrain decoding to a grammar. I wrote a small EBNF grammar for the exact routine format with fixed separators, so the model chooses only the content, never the structure. With it, every output in my test set was valid JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The model chooses a meaning, and code computes the time.&lt;/strong&gt; My first version asked Gemma for an hour and a minute, and it often returned &lt;code&gt;0:00&lt;/code&gt; for "after breakfast". Now the model chooses from a fixed list of anchors (&lt;code&gt;on_waking&lt;/code&gt;, &lt;code&gt;after_breakfast&lt;/code&gt;, &lt;code&gt;before_lunch&lt;/code&gt;, &lt;code&gt;bedtime&lt;/code&gt; and a few others), and code converts them to times such as 8:30, 12:30 and 22:00. The model returns a clock time only when the text contains one, such as "at 4 PM". The food rule is derived from the anchor as well, so "before lunch" cannot become "after food".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Quote first, then verify.&lt;/strong&gt; For each task the model must also copy the exact source text into a &lt;code&gt;said&lt;/code&gt; field. A short function then checks that quote with regular expressions. If the quote names exactly one time or food rule and the model chose something else, the quote takes precedence. A quote such as "before lunch and before dinner" is split into two tasks, and a dose containing a number that does not appear in the source text is removed. This check produced the largest accuracy improvement, in about 100 lines of code.&lt;/p&gt;

&lt;p&gt;Before anything is saved, the user sees a preview in which every time can be edited and every task can be removed. A 2B model will sometimes be wrong, so the person always has the final say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions&lt;/strong&gt; follow the same principle. Code first computes the facts: what is done, what is waiting and for how long, and what is next. Gemma only phrases those facts as a short answer, and it is instructed not to give medical advice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;I wrote 20 test routines with known correct answers. They cover medicines, insulin, an inhaler, eye drops, plant care, feeding a pet, explicit clock times and descriptive wording such as "the white tablet with breakfast, the pink tablet at bedtime". I scored Gemma on all of them in headless Chrome on my PC's RTX 4060.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma 2 2B&lt;/th&gt;
&lt;th&gt;Valid JSON&lt;/th&gt;
&lt;th&gt;Correct task count&lt;/th&gt;
&lt;th&gt;Correct times&lt;/th&gt;
&lt;th&gt;Correct food rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free text, no grammar&lt;/td&gt;
&lt;td&gt;14/20 (17/20 after one retry)&lt;/td&gt;
&lt;td&gt;13/20&lt;/td&gt;
&lt;td&gt;38/54&lt;/td&gt;
&lt;td&gt;16/21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grammar, model alone&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;36/54&lt;/td&gt;
&lt;td&gt;11/21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grammar + quote check (shipped)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17/20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47/54 (87%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17/21&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; 1.4 GB, once, in 42 parts. On the live site, the first run including the download took about 7.7 minutes on my connection. After that, loading from the browser cache takes about 4 to 6 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; about 40 to 55 tokens per second for generation and 500 to 700 tokens per second for reading the prompt. Short routines take 4 to 6 seconds. The six-line example above is 688 tokens in and 330 out, about 8 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions:&lt;/strong&gt; answers take about 1.5 to 3 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; on the live site with a fresh browser profile, I downloaded the model, created a routine, disconnected the network and reloaded. The app loaded from its own cache (0.9 MB), Gemma loaded from the browser cache, and a new routine was created in about 12 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First paint:&lt;/strong&gt; under one second on the live site at desktop and phone sizes. The CSS is inlined into the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated checks:&lt;/strong&gt; a headless test opens the live site at desktop and phone sizes, covers the large view, the share link round trip and a browser without WebGPU, and fails on any console error. All 28 checks pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What did not work
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B was my first choice.&lt;/strong&gt; At 537 MB it is a much smaller download. In the WebLLM build I used, its sliding-window attention configuration either failed to load or, once forced, produced unusable output. Without the grammar it produced no valid routines in 3 test cases. With the grammar it produced valid JSON for all 5 cases, but none of the 11 times were correct. I switched to Gemma 2 2B, at the cost of a download nearly three times larger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON schema mode looped in early tests.&lt;/strong&gt; With a plain JSON schema, the small models sometimes generated whitespace until they reached the token limit. The custom grammar with fixed separators avoids this. In the final evaluation, schema mode was also valid for all 20 cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Times were the hardest part.&lt;/strong&gt; Asking for hours and minutes directly produced many wrong times, often midnight. The anchor list plus the quote check brought it to 47 of 54.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model still misses tasks.&lt;/strong&gt; In the example routine above, Gemma leaves out the walk. I kept that in the demo deliberately, because it shows why the editable preview matters. During the offline test it also produced a dose of "500 mg" for a Vitamin D tablet whose dose I had not written, which is why the dose rule exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bug in my own regular expressions.&lt;/strong&gt; I generated part of the quote checker with a script that turned every &lt;code&gt;\b&lt;/code&gt; (word boundary) into a literal backspace character. Nothing matched, and accuracy dropped until I found it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing a 1.4 GB model is slow.&lt;/strong&gt; Each test with a fresh browser profile includes an eight-minute download. A force-closed Chrome once left the model cache half-written, and the app then waited indefinitely while checking the GPU. That check now times out after 3 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every device can run the model.&lt;/strong&gt; It needs WebGPU with 16-bit float support and a few GB of GPU memory, and I only verified it on my own GPU. Phones get the full app with the demo routine and can open a routine sent from a computer. When the model cannot run, the first screen says so and suggests that option.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A daily routine, especially one with medicines in it, is private. It shows what a person is treated for, how often, and how consistently they keep up. My grandmother should not have to send that to a company's server to get a reminder or an answer to "Did I take the evening one?"&lt;/p&gt;

&lt;p&gt;Because Gemma's weights are open, the whole model can run inside the page. The routine, the completed tasks and the questions stay in one browser on one device. There is no API key to leak, no usage bill, and no dependency on a service that could change its terms or shut down. Once loaded, it keeps working without a network connection.&lt;/p&gt;

&lt;p&gt;Open weights also meant I could control how the model behaves, not only what I ask it. WebLLM let me constrain decoding with my own grammar, which raised JSON validity from 14 out of 20 to 20 out of 20. I could switch from Gemma 3 1B to Gemma 2 2B with a one-line change when the first did not work, and I could run the 20-case evaluation repeatedly on my own GPU at no cost. With a closed API, I would have been limited to adjusting the prompt.&lt;/p&gt;

&lt;p&gt;The trade-off is a 1.4 GB first download and a WebGPU requirement. For a health routine I think that is the right trade, and smaller open models will reduce it further.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma&lt;/strong&gt;: Gemma 2 2B runs entirely in the browser through WebLLM and WebGPU and is the core of the app. It turns a plain-language description into the routine and answers questions using only that routine.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I built a 163-page paycheck calculator that costs $0 a month to run</title>
      <dc:creator>StarKnight</dc:creator>
      <pubDate>Thu, 02 Jul 2026 07:39:03 +0000</pubDate>
      <link>https://dev.to/starknight/i-built-a-163-page-paycheck-calculator-that-costs-0-a-month-to-run-355d</link>
      <guid>https://dev.to/starknight/i-built-a-163-page-paycheck-calculator-that-costs-0-a-month-to-run-355d</guid>
      <description>

&lt;p&gt;Earlier this month I shipped &lt;a href="https://paybrackets.com" rel="noopener noreferrer"&gt;PayBrackets&lt;/a&gt;, a free calculator that tells Americans what they actually keep from their paycheck after federal tax, state tax, and Social Security take their cut. It covers all 50 states plus D.C., it has no database, no backend, no APIs, and my total running cost is the domain.&lt;/p&gt;

&lt;p&gt;I want to walk through how it's built, because the interesting part isn't the code. It's how much you can get out of very little code if the data underneath is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a tax calculator, of all things
&lt;/h2&gt;

&lt;p&gt;Every month, millions of people type things like "75000 after taxes texas" or "25 an hour is how much a year" into Google. I looked at what they find. Mostly it's ten-year-old calculators buried under popups, or freshly generated AI pages where the numbers are confidently wrong.&lt;/p&gt;

&lt;p&gt;That gap felt workable for one person. Salaries and taxes are pure math. The rules are public. Nothing about it needs a server, an account system, or content writers. It needs someone to sit down, get the rules right once, and present them clearly.&lt;/p&gt;

&lt;p&gt;So the whole plan was: encode the 2026 tax rules properly, build one calculation engine, and let that engine generate every page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous part was the whole ballgame
&lt;/h2&gt;

&lt;p&gt;There's no tax API behind the site. The source of truth is a set of TypeScript files I wrote by hand, encoding the 2026 federal brackets from the IRS, the Social Security wage base from the SSA, and every state's brackets, deductions, exemptions, and phaseouts from Tax Foundation data. It looks like this:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;California&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;california&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;graduated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.133&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;brackets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;single&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="nx"&gt;_079&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="c1"&gt;// ...8 more brackets up to 13.3% at $1M&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;standardDeduction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;single&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_540&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;married&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="nx"&gt;_080&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;payrollTaxes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CA SDI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.013&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="c1"&gt;// uncapped since 2024&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing this for 51 jurisdictions took me a full day, and it was the most valuable day of the project. States are genuinely weird. Maryland's counties add their own income tax on top of the state's. California's disability insurance tax lost its wage cap in 2024, so it applies to every dollar. Connecticut quietly claws back the benefit of its lower brackets once you earn enough. Nine states don't tax wages at all.&lt;/p&gt;

&lt;p&gt;If I had modeled states as "one tax rate" like most quick calculators do, every single page would be wrong in a different way. And where I chose not to model something, like those Maryland county rates, the page says so in a visible note instead of pretending.&lt;/p&gt;

&lt;h2&gt;
  
  
  One engine, checked against reality
&lt;/h2&gt;

&lt;p&gt;The heart of the site is embarrassingly small. Progressive taxes are just a walk over brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;marginalTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taxable&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;brackets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Bracket&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;brackets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;brackets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taxable&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;brackets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;brackets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taxable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Around that sits the composition: federal income tax after the standard deduction, Social Security up to the wage cap, Medicare plus the surtax on high earners, optional 401(k) deferrals, then the state layer with all its exceptions.&lt;/p&gt;

&lt;p&gt;Before launch I wrote a small check script with hand-verified salaries. It runs the engine against numbers I worked out independently and fails loudly if anything drifts. A typo in a bracket threshold is invisible in a code review. In a paycheck for a specific salary, it screams.&lt;/p&gt;

&lt;h2&gt;
  
  
  163 pages from one route
&lt;/h2&gt;

&lt;p&gt;The site is Next.js, fully static. All the calculator pages hang off a single dynamic route, and the slugs are literally the questions people search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;allProgrammaticSlugs&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;STATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-paycheck-calculator`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;// 51 pages&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;HOURLY_RATES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-an-hour-is-how-much-a-year`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 61 pages&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;SALARY_AMOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-after-taxes`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;// 35 pages&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At build time, each page computes its own numbers. The "$50,000 after taxes" page shows the exact take-home in all 51 jurisdictions. The Texas page shows what no state income tax is actually worth against California at the same salary. No two pages share their content, which is the difference between programmatic pages that help people and the thin templated stuff Google buries.&lt;/p&gt;

&lt;p&gt;The build produces 163 static pages in about half a minute, and the interactive calculator runs the same engine in the browser. Your salary never leaves the page, which was important to me. Nobody should upload their income to a stranger's server to learn what they already earn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The readers that aren't people
&lt;/h2&gt;

&lt;p&gt;A growing share of these questions never reach Google. People ask ChatGPT or Perplexity instead. So the site treats AI crawlers as an audience of their own.&lt;/p&gt;

&lt;p&gt;Every page opens with the question stated plainly and a two-sentence answer with real numbers, before any explanation. There's FAQ markup throughout, a methodology page naming every data source, and a robots.txt that explicitly welcomes the OpenAI, Anthropic, and Perplexity crawlers rather than blocking them.&lt;/p&gt;

&lt;p&gt;The bet is simple. Assistants quote pages that are structured like clean answers with checkable numbers. If an assistant answers "what's $70k after taxes in Florida" by citing my site, that citation is the new referral traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The launch video is also code
&lt;/h2&gt;

&lt;p&gt;One more thing that amused me. I wanted a proper product video for the launch, the smooth animated kind startups pay studios for. I don't own After Effects, so I built the video in Remotion instead. Every scene is a React component, the dollar figures in the animation come from the same tax engine as the site, and the final MP4 renders from the command line.&lt;/p&gt;

&lt;p&gt;There's something satisfying about a project where even the marketing assets compile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;The site has been live for a few weeks. It's indexing, impressions are climbing, and the first ad review is in progress. I deliberately have no revenue story to tell yet, and I'd rather say that than inflate it. Once there are real numbers, good or bad, I'll write the follow-up.&lt;/p&gt;

&lt;p&gt;The site is &lt;a href="https://paybrackets.com" rel="noopener noreferrer"&gt;paybrackets.com&lt;/a&gt; if you want to poke at it. Happy to go deeper on the tax data modeling, the static generation, or the video in the comments.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
