<?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: Gavin Rose</title>
    <description>The latest articles on DEV Community by Gavin Rose (@gavin_rose).</description>
    <link>https://dev.to/gavin_rose</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%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png</url>
      <title>DEV Community: Gavin Rose</title>
      <link>https://dev.to/gavin_rose</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gavin_rose"/>
    <language>en</language>
    <item>
      <title>OnBlink: Communicate with minimal movement</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Mon, 09 Nov 2026 18:57:17 +0000</pubDate>
      <link>https://dev.to/gavin_rose/onblink-communicate-with-minimal-movement-3ag3</link>
      <guid>https://dev.to/gavin_rose/onblink-communicate-with-minimal-movement-3ag3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/presage-2026-11-04"&gt;Presage SmartSpectra SDK Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;OnBlink&lt;/strong&gt; lets someone speak a phrase or open an app using only their eyes and a regular webcam.&lt;/p&gt;

&lt;p&gt;A blink signal opens a menu. The highlight steps through phrases like "Yes", "I need help" or "I'm thirsty", and a second signal picks one. The Mac says it out loud, or opens an app. It's built for people who can't easily speak or type, and for the people around them.&lt;/p&gt;

&lt;p&gt;Highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two ways to select.&lt;/strong&gt; Double blink (SmartSpectra's built-in blink detection) or long blink (close your eyes for about a second), so people can use whichever signal is easiest for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own phrases and apps.&lt;/strong&gt; Add, edit, reorder and test menu items. Each one speaks a sentence or opens an app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heart rate check-ins (optional).&lt;/strong&gt; If someone's heart rate stays well above their own baseline level from earlier in the session, OnBlink offers a short check-in: "I am fine", "I need a break" or "I need help".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private option prompts (optional).&lt;/strong&gt; Options can be read softly, or sent to headphones or a single earbud, while the chosen phrase is spoken out loud for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guided setup.&lt;/strong&gt; A 10-step tutorial, a framing guide that waits until your face is in position, and a practice run.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/6Tld-F33S_A" rel="noopener noreferrer"&gt;Check out OnBlink&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/gavin-rose/onblink" rel="noopener noreferrer"&gt;https://github.com/gavin-rose/onblink&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setup takes three commands (&lt;code&gt;npm install&lt;/code&gt;, &lt;code&gt;npm start&lt;/code&gt;, then paste a free Presage API key in the tutorial). The README covers requirements, usage, settings, data handling and how the detector was tuned. OnBlink runs on macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Responds to Human Signals
&lt;/h2&gt;

&lt;p&gt;OnBlink uses five live SmartSpectra signals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;How OnBlink uses it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blinking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Double blink input: blink, wait for "Blink again", blink once more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Face landmarks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Long blink input, measured from how open each eye is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pulse rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live heart rate display and optional check-ins, using only readings the SDK marks stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Talking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ignores eye closures while the person is speaking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Validation status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drives the framing guide, pauses input when the person moves out of view, and pauses check-ins during motion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Integrating the SDK.&lt;/strong&gt; OnBlink is an Electron app built on the SmartSpectra Node SDK. The renderer requests &lt;code&gt;faceMetrics&lt;/code&gt; and &lt;code&gt;cardioMetrics&lt;/code&gt;, decodes each metrics message, and feeds blinks, landmarks, talking and pulse into small, separate modules: the menu, the long blink detector and the check-in logic. The main process handles settings, speech and opening apps, and the page can only ask it to run a saved action by its id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long blink.&lt;/strong&gt; SmartSpectra's blink signal is a short detection pulse, which is perfect for quick double blinks. For hold-based input, I measured eye openness directly from the face landmarks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each eye is compared to its own recent level, so it works for people whose eyes naturally open different amounts.&lt;/li&gt;
&lt;li&gt;A closure that lasts 0.7 to 2.5 seconds counts as a long blink. It's ignored if the head moves or the person is talking.&lt;/li&gt;
&lt;li&gt;Some people find a deliberate one-second close easier than a quick double blink, and in my own testing setup it was the more dependable option. It's also very hard to trigger by accident, which matters when a false trigger means the computer speaks for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tuned the thresholds with labeled recordings: I pressed a key after each blink, long blink and wink, then replayed the recordings through the detector. It caught all 5 deliberate long blinks and didn't fire on the 15 other labeled actions. The analysis scripts are in the repo's &lt;code&gt;research&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check-ins.&lt;/strong&gt; During the first minute of steady readings, OnBlink learns the person's usual heart rate. If stable readings stay well above that level (15 bpm or 20% for 45 seconds on the default setting), it offers a check-in through the same menu. It only reacts to rises, waits until the person is in position and not using the menu, and appears at most once every 10 minutes. It's an offer to talk, never a health judgment.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Thoughtful Real-World Use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The need.&lt;/strong&gt; People with limited speech or movement often have the hardest time asking for the simplest things: a drink, help, or a break. Dedicated eye-tracking devices exist, but they're expensive and need special hardware. OnBlink only needs a laptop webcam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designed for the people using it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A choice of signal and pace.&lt;/strong&gt; Double blink with relaxed or quick spacing, or long blink, plus an adjustable menu speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard to trigger by accident.&lt;/strong&gt; Single blinks are ignored, input pauses when the person is out of view, and the menu closes on its own if nothing is picked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick phrases first.&lt;/strong&gt; Yes and No come first, then urgent needs, so common answers take the fewest steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speaking up without the effort of starting.&lt;/strong&gt; The check-in gives someone an easy way to speak up when their body suggests something might be wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Considerate of the room.&lt;/strong&gt; Option prompts can stay quiet or go to an earbud, so only the chosen phrase is spoken out loud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doesn't stop on its own.&lt;/strong&gt; The screen stays awake during a session, and auto stop is off by default, so nobody has to restart it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy for a caregiver to set up.&lt;/strong&gt; A tutorial with a practice run, plus a settings screen with a Test button for every phrase.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Consent and Data Handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consent first.&lt;/strong&gt; Before the first session, OnBlink shows a plain-language consent screen, also part of the tutorial, explaining what the camera is used for. Nothing starts until the person accepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera.&lt;/strong&gt; On only after pressing Start, and off when pressing Stop or at an optional time limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK telemetry.&lt;/strong&gt; Turned off (&lt;code&gt;enableTelemetry: false&lt;/code&gt;). Presage's &lt;a href="https://github.com/Presage-Security/SmartSpectra/blob/main/docs/telemetry-and-privacy.md" rel="noopener noreferrer"&gt;telemetry and privacy document&lt;/a&gt; says the SDK transmits no telemetry when it's off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage.&lt;/strong&gt; OnBlink saves only the menu items, settings and the API key (encrypted with the macOS Keychain), locally on the user's computer. Deleting the app's settings folder removes everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never stored or sent.&lt;/strong&gt; Video, eye or face data, and heart rate. The heart rate level used for check-ins is kept in memory only and discarded when the session stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharing and AI services.&lt;/strong&gt; OnBlink shares nothing and uses no AI services. Phrases are spoken locally by macOS, and only when the person picks them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional features.&lt;/strong&gt; Check-ins are off until the person turns them on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a medical device.&lt;/strong&gt; OnBlink is a communication aid. It does not diagnose, treat or monitor any condition, and it should not be relied on in an emergency. Check-ins are only an offer to talk.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I built OnBlink with Claude Code: exploring what the SDK could measure, recording and analyzing my own blink data, and building and refining the app. The SDK was pretty easy to understand and use but I definitely leaned hard into Claude for the visuals as that isn't normally my wheelhouse.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>presagechallenge</category>
    </item>
    <item>
      <title>Try playing my game!</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Wed, 28 Oct 2026 19:28:09 +0000</pubDate>
      <link>https://dev.to/gavin_rose/try-playing-my-game-4670</link>
      <guid>https://dev.to/gavin_rose/try-playing-my-game-4670</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8" class="crayons-story__hidden-navigation-link"&gt;Plinth! My first arcade-style game.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/gavin_rose" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" alt="gavin_rose profile" class="crayons-avatar__image" width="512" height="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/gavin_rose" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Gavin Rose
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Gavin Rose
                
                
              
              &lt;div id="story-author-preview-content-4830647" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/gavin_rose" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" class="crayons-avatar__image" alt="" width="512" height="512"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Gavin Rose&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8" id="article-link-4830647"&gt;
          Plinth! My first arcade-style game.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/android"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;android&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/flutter"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;flutter&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              3&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>gamedev</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Haunt Engine</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:42:34 +0000</pubDate>
      <link>https://dev.to/gavin_rose/haunt-engine-1c04</link>
      <guid>https://dev.to/gavin_rose/haunt-engine-1c04</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Haunt Engine&lt;/strong&gt; is a game where you get to write a prompt that fuels the haunted house you play.&lt;/p&gt;

&lt;p&gt;You type a place: "a cozy bakery", "the last video rental store on earth", "the classroom where my grandmother taught". A model running on your own laptop designs a building from it, room by room. It names the rooms and furnishes them. It casts the people who never left, gives them something to say, hides the keys and decides what hunts you. The game builds that design into a top-down level and drops you in the front door with a lantern.&lt;/p&gt;

&lt;p&gt;You can talk to anyone you find. The model plays them in character, so a ghost might tell you where the key is, if you ask the right way. Get caught too many times and you die. The model then writes your epitaph from what actually happened: who took you, in which room, and what you were holding. Your headstone stays in that haunt, on the tile where you fell. Next time you go in, you walk past your own grave.&lt;/p&gt;

&lt;p&gt;I took the theme literally. The ghost in the machine is the thing that designed the building you're trapped in, and it's also the voice of everyone inside it.&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%2F2ntdjusa5ejm9345bbxo.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%2F2ntdjusa5ejm9345bbxo.gif" alt=" " width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2F2mwphs9o7kbt8y1nk1tw.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%2F2mwphs9o7kbt8y1nk1tw.gif" alt=" " width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Haunt Engine runs entirely on your machine, so there's no hosted link; the repo below gets you playing in three commands. The GIF above is the hand-written demo haunt, &lt;strong&gt;St. Agnes Elementary, After Hours&lt;/strong&gt;, on the easiest difficulty, where marigold petals lead you to your next objective.&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/gavin-rose" rel="noopener noreferrer"&gt;
        gavin-rose
      &lt;/a&gt; / &lt;a href="https://github.com/gavin-rose/haunt-engine" rel="noopener noreferrer"&gt;
        haunt-engine
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;Haunt Engine&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Describe a place. Walk through what's waiting in it.&lt;/p&gt;
&lt;p&gt;You type a premise ("a cozy bakery", "the classroom where my grandmother taught"). An
open-weight model (Gemma, running on your own machine) turns it into a haunted building: rooms
furniture, characters with things to say, a chain of objectives, something that hunts you
The game builds that into a top-down horror level you play in the browser. When you die, the
model carves your epitaph, and your grave stays in that haunt for your next visit.&lt;/p&gt;
&lt;p&gt;Everything runs locally. There are no accounts, no API keys and no server besides your own.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What you need&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js 20.9 or newer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; 0.30.5 or newer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About 10 GB of free memory and 8 GB of disk&lt;/strong&gt; for the 12B model. Developed on an Apple
Silicon MacBook Pro with 24 GB of memory.&lt;/li&gt;
&lt;li&gt;A browser with WebGL (any current Chrome, Firefox, Safari…&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/gavin-rose/haunt-engine" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run setup:model   &lt;span class="c"&gt;# pulls Gemma 4 12B and gives it a 16k context&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. The art keeps its own CC-BY-SA license, and every artist is credited in the game.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The model&lt;/strong&gt; is Gemma 4 12B (the quantization-aware &lt;code&gt;gemma4:12b-it-qat&lt;/code&gt; build) which I ran Ollama locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule I built everything around: the model designs, code builds.&lt;/strong&gt; The model never places a tile or touches game state. It writes a &lt;em&gt;blueprint&lt;/em&gt;, a JSON document of rooms, doors, items, characters, objectives and scripted events. Ollama's structured output holds it to a JSON schema generated from Zod. Then deterministic code turns that blueprint plus a random seed into a level and enforces every rule.&lt;/p&gt;

&lt;p&gt;That split is what made a 12B model good enough. A few things made it work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Closed vocabularies for anything that gets drawn.&lt;/strong&gt; My first blueprints asked for furniture I had no art for. Now every floor, wall, piece of furniture and item of clothing is an enum over the art I actually have. The model chooses from 61 props, and it dresses characters from LPC sprite layers: body, hair, coat, hat, wounds. The browser composites and recolors those layers, so the model can invent a character nobody drew and still get a sprite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair instead of retry.&lt;/strong&gt; Small models get references wrong. They connect a door to a room that doesn't exist, or lock a key behind its own door. Rather than asking again (which costs minutes on a laptop), &lt;code&gt;repair.ts&lt;/code&gt; fixes the blueprint deterministically. It reconnects orphaned rooms and moves keys until a fixpoint search proves every room is reachable. It also tops up haunts that are too easy. Early on, Gemma wrote a haunt with two objectives and no way out, and it was over almost as soon as it began. Now every haunt has at least three objectives, ends in an escape from the room farthest from the start, and has something hunting you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A generator that respects the art.&lt;/strong&gt; Rooms are laid out by walking the connection graph and drawn in 3/4 view, with real wall faces. Corner autotiling handles the ceiling trim. Furniture has placement rules: desks go in rows and mirrors hang on walls. Lighting is per-tile line of sight, blurred into soft darkness under a cold moonlight grade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never pay for the same haunt twice.&lt;/strong&gt; At about 11 tokens a second, a summon takes five to eight minutes. Every haunt is saved the moment it exists. Typing a prompt again loads it instantly, and a summon keeps generating even if you close the tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two Ollama details did make it difficult to work with. Gemma 4 thinks before answering by default, which made every request far slower; &lt;code&gt;reasoning_effort: "none"&lt;/code&gt; turns that off. And Ollama's default 4k context truncates a whole building, so &lt;code&gt;npm run setup:model&lt;/code&gt; creates a &lt;code&gt;haunt-gemma&lt;/code&gt; alias with 16k.&lt;/p&gt;

&lt;p&gt;The rest of the stack is open source, too: Next.js 16, Phaser 4, Zod, and Liberated Pixel Cup art. The sound is synthesized live with the Web Audio API: wind, footsteps that change with the floor, and a heartbeat that quickens as something gets closer. That means there are no recordings to license.&lt;/p&gt;

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

&lt;p&gt;Because a game like this is a terrible fit for an API bill.&lt;/p&gt;

&lt;p&gt;Every haunt is roughly 5,500 characters of JSON, and every conversation and epitaph is another call. With open weights, a player who spends an evening summoning twenty haunted houses costs nothing, and nobody needs an account or a key. Clone the repo and the haunted house lives on your machine, along with your graves.&lt;/p&gt;

&lt;p&gt;Open weights also let me make the model fit the job rather than the other way round. I could pin the exact build, give it a bigger context window and switch off its thinking. It's held to a schema, and when it slips, the code repairs the blueprint and moves on. None of that depends on a provider keeping a model alive or changing its behavior under me. The haunts you summon this October will be the same buildings next October.&lt;/p&gt;

&lt;p&gt;And the art could only come from an open commons. Every room, prop and person in the game was drawn by the Liberated Pixel Cup community and shared so that projects like this could exist. The model can dress a character in anything from that commons because the commons is open.&lt;/p&gt;

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

&lt;p&gt;Día de los Muertos falls right after Halloween, and the game borrows from it. On the easiest difficulty, a trail of marigold petals leads you through the dark to your next objective. Marigolds are what families scatter to guide the dead home, and here they guide the living.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Bastion, the Local Helper</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:15:03 +0000</pubDate>
      <link>https://dev.to/gavin_rose/bastion-the-local-helper-3516</link>
      <guid>https://dev.to/gavin_rose/bastion-the-local-helper-3516</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Bastion&lt;/strong&gt; is a local agent that does chores based on the prompt you give it.&lt;/p&gt;

&lt;p&gt;You describe a chore in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every Friday, go through my Downloads folder and move all .jar files into a folder called minecraft.&lt;/p&gt;

&lt;p&gt;Every morning at 9:30, check the news on artificial intelligence and summarize anything new.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A local model writes a small Python script for it, tests it as a dry run in a sandbox, and shows you exactly what it will be allowed to do. It slowly fixes itself if any issues are present, then, you only have to approve it once. From then on the chore runs on schedule as plain code. No model is involved, unless you asked for the results to be summarized. Every run after that is fast, cheap and predictable, and the only thing you do is read the result.&lt;/p&gt;

&lt;p&gt;It's built for people who don't write code. You never have to edit a script. You read a plain list of permissions ("can reach news.google.com", "can move files in ~/Downloads and to the Trash"), look at a sample result, and click approve.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The open pieces&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 12B&lt;/strong&gt; (Google's QAT build) writes and repairs scripts, run locally with &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; writes the daily summaries.&lt;/li&gt;
&lt;li&gt;Bastion talks to Ollama over &lt;strong&gt;stdio&lt;/strong&gt;: the prompt goes in on stdin, a JSON schema goes in with &lt;code&gt;ollama run --format&lt;/code&gt;, and structured JSON comes back on stdout. The grammar keeps the model's output valid, and length limits in the schema stop runaway loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir / Phoenix LiveView&lt;/strong&gt; runs the background service and the dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How a chore gets made&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"every Friday, move .jar files from Downloads into Downloads/minecraft"
        │
        ▼
 look around (read-only, sandboxed): list folders, fetch the feed or API, check tools exist
        │
        ▼
 Gemma 4 12B plans (JSON): permissions, schedule, which library example to start from
        │
        ▼
 Gemma 4 12B writes the script (plain Python on top of a small tested helper kit)
        │
        ▼
 dry-run test in the sandbox ──fails──► look again, read the error, revise (up to 2 times)
        │ passes
        ▼
 you approve, in plain English
        │
        ▼
 one supervised process per chore runs it on schedule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OTP does the boring, important parts.&lt;/strong&gt; Every approved chore gets its own GenServer under a DynamicSupervisor. Runs happen in unlinked tasks, so a crashing script never takes its scheduler down. Failures retry with backoff (seconds for a broken run, 10 to 30 minutes when a site is rate limiting). A janitor process marks interrupted runs as stopped, and every run can be stopped from the dashboard, which kills the actual sandboxed process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sandbox is enforced by the kernel, not by asking nicely.&lt;/strong&gt; Every script runs under macOS &lt;code&gt;sandbox-exec&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; blocked, except for a per-run proxy that only forwards to the domains you approved. It logs anything it blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes:&lt;/strong&gt; blocked, except for the chore's own memory folder and the folders you approved. Test runs are dry runs where even those are read-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programs:&lt;/strong&gt; nothing runs except Python and the commands you approved. No shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets:&lt;/strong&gt; SSH keys, cloud credentials, the Keychain, browser data, Mail, shell history and &lt;code&gt;.env&lt;/code&gt; files can't be read at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleting:&lt;/strong&gt; scripts never delete. Anything removed goes to the Trash, and any run that moved files has an &lt;strong&gt;Undo&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It respects scraping and other bot rules.&lt;/strong&gt; Scraped pages respect &lt;code&gt;robots.txt&lt;/code&gt;, feeds and public APIs are preferred, rate limits are waited out instead of hammered, and sites that send &lt;code&gt;Content-Signal: ai-input=no&lt;/code&gt; (like Lobsters) never have their content passed to a model. You still get the plain list, just not an AI summary of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned about small local models&lt;/strong&gt; (this was the most interesting part)&lt;/p&gt;

&lt;p&gt;I tested three 12B models on the same chores, drafted from scratch with no example to copy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 12B, community "abliterated" build&lt;/td&gt;
&lt;td&gt;Garbled text: &lt;code&gt;retmah=10&lt;/code&gt;, &lt;code&gt;efetch.fcji&lt;/code&gt;, invented hostnames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 12B (official)&lt;/td&gt;
&lt;td&gt;Clean text, but sent the same failing request five times in a row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma 4 12B (official)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read the API's error messages and corrected its own requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of them can reliably learn an unfamiliar web API on its own, so Bastion leans on things code can check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;probes verify every URL before it's used;&lt;/li&gt;
&lt;li&gt;Bastion, not the model, decides whether a request actually worked;&lt;/li&gt;
&lt;li&gt;when an API rejects a parameter, Bastion reads that API's docs page and hands the model the real names;&lt;/li&gt;
&lt;li&gt;the model adapts small, tested library examples instead of starting from nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where the models do shine is on your own machine. Cleanup chores worded the way a non-programmer would ask (duplicate files, screenshot filing, old installers, big untouched files) passed on the first try, 4 out of 4.&lt;/p&gt;

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

&lt;p&gt;For this project it's not a nice-to-have, it's the whole design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your files stay yours.&lt;/strong&gt; Bastion's models see folder listings, git repos, the names of things in your Downloads. With open weights running locally, none of that leaves the laptop. The only network traffic is the requests a chore makes to the sites you approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doing nothing costs nothing.&lt;/strong&gt; A chore that runs every day for a year would be hundreds of paid API calls. Here the model runs once, and daily runs are just Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could swap and tune models freely.&lt;/strong&gt; The three-way comparison above took an afternoon: pull a model and change one line of config. When the community build kept garbling text, I switched to Google's official one. When a model's default 8K context was too small for my prompts, a three-line Ollama Modelfile gave it 16K. A closed API wouldn't let me do any of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output at the grammar level.&lt;/strong&gt; Ollama enforces the JSON schema while the model generates, and it enforces length limits too. That's what stopped a small model's repetition loops from wrecking a run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local chores work offline.&lt;/strong&gt; Tidying folders, finding duplicates, checking git repos: none of it needs the internet at all.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I've been running it on my own machine this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily news chores&lt;/strong&gt; (AI news, and research) turn a feed of 30 to 100 headlines into a handful of relevant bullets, with Markdown and PDF export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A weekly Downloads tidy&lt;/strong&gt; for Minecraft &lt;code&gt;.jar&lt;/code&gt; files, set up in one sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test runs on my real folders&lt;/strong&gt; showed what the cleanup chores would handle: 423 screenshots and screen recordings to file into monthly folders, 134 old installers and archives to send to the Trash (one of them a 91.8 GB zip from 97 days ago), and duplicate sets across about 35,000 files. Each of those is a chore I'd been putting off for months, and now it's one sentence and one click.&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%2Fhe6557zve8yihkq6b1g4.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%2Fhe6557zve8yihkq6b1g4.png" alt=" " width="800" height="617"&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%2Fj18kbo4ucrljv3e4qr9s.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%2Fj18kbo4ucrljv3e4qr9s.png" alt=" " width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Magen: Local Penetration Testing</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Mon, 19 Oct 2026 00:44:06 +0000</pubDate>
      <link>https://dev.to/gavin_rose/magen-local-penetration-testing-c9h</link>
      <guid>https://dev.to/gavin_rose/magen-local-penetration-testing-c9h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Magen is a local-only security testing copilot. It captures traffic while you have the proxy running, flags leaked secrets and personal data, and uses a local open-weight model to reason about what it found and to write and run discovery probes. The one thing that defines it: every part runs on your machine. Nothing sensitive leaves the laptop.&lt;/p&gt;

&lt;p&gt;The problem it solves is specific. When you security test a real application you are handling live session tokens, .env files, and other people's personal data. You cannot paste any of that into a hosted LLM without creating a new liability. A model running locally removes that constraint, so the tool can reason over the real data instead of a redacted shadow of it.&lt;/p&gt;

&lt;p&gt;Who would pay for it: freelance security testers, small teams, and bug bounty hunters who want an AI assistant but cannot ship client data to a third party.&lt;/p&gt;

&lt;p&gt;What it does today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scope-locked intercepting proxy (built on mitmproxy) that only records hosts you explicitly opt in.&lt;/li&gt;
&lt;li&gt;Deterministic secret and PII detection with stable redaction, so a report can reference a secret by a token like «jwt:7c5b0229» instead of the value.&lt;/li&gt;
&lt;li&gt;Local anomaly detection across captured flows, using robust statistics and no model at all, to surface the one request that behaves unlike its siblings.&lt;/li&gt;
&lt;li&gt;Local-model triage, plus an agentic "explore" mode that writes standard-library Python probes, runs them in a guarded sandbox, and scores findings from the evidence they produce.&lt;/li&gt;
&lt;li&gt;A loopback-only dashboard to drive all of it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The open pieces are the project, not an accessory to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inference: Gemma (gemma3:12b-it-qat, with gemma3:4b as a fast fallback) running locally through Ollama.&lt;/li&gt;
&lt;li&gt;Capture: mitmproxy as the interception engine.&lt;/li&gt;
&lt;li&gt;Everything else is the Python standard library, on purpose, so the tool works offline with almost no dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design choice I am happiest with is how the agent handles secrets. When the model writes a probe to test an endpoint, Magen injects the real captured token into the probe's process through an environment variable at run time. The model never sees the token and never writes it into code, yet the probe still authenticates for real. The model reasons, the machine holds the secret, and nothing leaves the device.&lt;/p&gt;

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

&lt;p&gt;This is the whole reason the project exists. Three things a closed API could not give me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I can hand the model real credentials and real responses, because they never leave the machine. That is the core capability, and for a security tool it is not a tuning preference, it is a hard requirement. Cloud reasoning over live secrets is a non-starter, not a trade-off.&lt;/li&gt;
&lt;li&gt;It costs nothing per run and does not depend on anyone's pricing or uptime.&lt;/li&gt;
&lt;li&gt;I can swap models freely and, in principle, fine-tune one for this exact task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running an open model locally was not a nice-to-have here. It was the only honest way to build something that touches credentials. While a hosted open-weight model could have done work here, I felt it went against the principals of what I was trying to accomplish, and that meant making external requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Worked, What Didn't, and What I Would Change
&lt;/h2&gt;

&lt;p&gt;This project ended up being more of a learning experience than a finished product, and I think that is worth noting.&lt;/p&gt;

&lt;p&gt;The deterministic parts worked well: capture, secret and PII detection, redaction, and the statistics-based anomaly detection. Those are reliable, genuinely useful on their own, and they need no model.&lt;/p&gt;

&lt;p&gt;The local-model layer is where I fell short. The models that run comfortably on my hardware were not quite strong enough for the agentic security reasoning I was asking for. The small model is fast but shallow. The larger one reasons better but takes close to a minute per step and was still inconsistent; it would reach for libraries that were not available, pick weak endpoints to probe, or misjudge whether a response was actually a finding. That is not the model being bad. It is a mismatch between a general-purpose local model with no task-specific tuning and the fairly precise job I wanted it to do.&lt;/p&gt;

&lt;p&gt;Two things would have closed most of that gap, and I had time for neither: fine-tuning a model for this narrow task, and simply iterating longer on the prompts and the evidence format the model has to produce.&lt;/p&gt;

&lt;p&gt;The bigger lesson was about scope. I started too broad. Under time pressure I kept adding features: a service manager, a one-click demo, a managed test browser, multiple probe goals. Each one widened the surface area, and each one introduced bugs I then had to chase instead of improving the core. A tighter starting scope, something like "capture traffic, scan it for secrets locally, and produce a redacted report," would have been a smaller and far more solid product, with the local model doing one job it could actually do well. Improper scoping at the very start was probably the single biggest thing working against me.&lt;/p&gt;

&lt;p&gt;So I do not read this as a failure. The privacy thesis holds up, the deterministic tooling is good, and I now know exactly where a local-first AI tool earns its keep and where it still needs either a stronger model or real tuning time. That is a useful thing to carry into the next build.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Magen runs Gemma locally through Ollama as its reasoning and discovery engine.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Plinth! My first arcade-style game.</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 23:58:37 +0000</pubDate>
      <link>https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8</link>
      <guid>https://dev.to/gavin_rose/plinth-my-first-arcade-style-game-ji8</guid>
      <description>&lt;p&gt;I just shipped &lt;strong&gt;Plinth&lt;/strong&gt;, a small one-tap arcade game for Android. You stand on a column, a new column rises next to you, and you tap to stop it, ideally at your height. If you stop it too high, the extra gets sliced off, so every column after that is shorter. Stop it too low and you fall.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://play.google.com/store/apps/details?id=com.gtfrexo.plinth" rel="noopener noreferrer"&gt;Get Plinth on Google Play&lt;/a&gt; · &lt;a href="https://plinth-mobile.web.app" rel="noopener noreferrer"&gt;plinth-mobile.web.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's built with Flutter and &lt;a href="https://flame-engine.org/" rel="noopener noreferrer"&gt;Flame&lt;/a&gt;. Here's the story!&lt;/p&gt;

&lt;h2&gt;
  
  
  Faking 3D to promote performance
&lt;/h2&gt;

&lt;p&gt;Plinth looks 3D, but it's plain 2D canvas drawing. Every column is a box in isometric projection: three filled polygons (top, left side, right side) computed from grid coordinates.&lt;/p&gt;

&lt;p&gt;The only real trick is draw order. Boxes further "back" have to be painted first, so each frame I sort everything by &lt;code&gt;gx + gy&lt;/code&gt; and paint back to front (the painter's algorithm). The player gets drawn last, except when falling into a gap, where it slots into the sort so the columns in front correctly cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Game feel is a pile of tiny things
&lt;/h2&gt;

&lt;p&gt;The first playable version worked but felt dead. What fixed it was a slew of small touches, none of which were too hard to implement...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manipulating the character&lt;/strong&gt; on the character: a little crouch before a hop, a stretch in the air, a squash on landing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rising chime for perfect stops&lt;/strong&gt; that climbs a scale with your streak, so a streak of perfects sounds like a melody.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen shake and debris&lt;/strong&gt; when a column gets sliced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "NEW BEST!" moment mid-run&lt;/strong&gt; whenever you beat your previous best score, complete with confetti.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the sound effects were generated with a short Python + numpy script (sine waves, envelopes, some filtered noise).&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncing coins across devices without a backend
&lt;/h2&gt;

&lt;p&gt;I wanted coins and unlocks to follow players between phones, without running a server. Google Play Games' &lt;strong&gt;Saved Games&lt;/strong&gt; stores a blob per player for free, so I only needed a merge strategy for when a local copy and the cloud copy disagree.&lt;/p&gt;

&lt;p&gt;A plain "take the max coins" merge is wrong: spend coins on one phone, sync an older copy, and you get the coins back. Instead, coins are stored as per-device running totals of earned and spent:&lt;/p&gt;

&lt;p&gt;Each device only ever increases its own counters, so merging is just "take the max per device". Owned items merge as a union.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejection that taught me to test release builds
&lt;/h2&gt;

&lt;p&gt;My first production submission was rejected: &lt;strong&gt;"Plinth keeps stopping."&lt;/strong&gt; It had never crashed for me once. Here is where I used Claude Code to find a quick fix. &lt;/p&gt;

&lt;p&gt;The cause: release builds run R8 (code shrinking), debug builds don't. The ads SDK pulls in an old version of WorkManager, which uses Room, which finds its generated database class by name at startup. R8 renamed it, so the app crashed before drawing a single frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to create an instance of androidx.work.impl.WorkDatabase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was a keep rule plus a newer WorkManager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# android/app/proguard-rules.pro
&lt;/span&gt;&lt;span class="err"&gt;-keep&lt;/span&gt; &lt;span class="err"&gt;class&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="err"&gt;extends&lt;/span&gt; &lt;span class="err"&gt;androidx.room.RoomDatabase&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;init&amp;gt;();&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/app/build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.work:work-runtime:2.11.2"&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;The real lesson: before every upload, install a &lt;strong&gt;release&lt;/strong&gt; build fresh on an emulator and actually play it. &lt;code&gt;flutter run&lt;/code&gt; in debug mode tells you nothing about R8.&lt;/p&gt;

&lt;p&gt;While testing that, I found a second problem: on a phone with no Google account signed in (like many review devices), my startup code triggered the &lt;em&gt;interactive&lt;/em&gt; Play Games sign-in, which opened Google's full account login screen. Now the app only silently checks sign-in status at launch, and the interactive flow lives behind a "Sign in" button on the leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two small gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AdMob's app-ads.txt check vs Firebase Hosting.&lt;/strong&gt; &lt;code&gt;firebase init&lt;/code&gt; offers a single-page-app rewrite that sends every unknown URL to &lt;code&gt;index.html&lt;/code&gt; with a 200 status. That means &lt;code&gt;/app-ads.txt&lt;/code&gt; "exists" but returns HTML, and AdMob reports a mismatch. Use real files with &lt;code&gt;cleanUrls&lt;/code&gt; and no catch-all rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The consent form is not optional in the EU and UK.&lt;/strong&gt; Google's UMP SDK ships inside &lt;code&gt;google_mobile_ads&lt;/code&gt;. Request consent before initializing ads, and add a "Privacy options" entry so players can change their choice later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monetization: rewarded only
&lt;/h2&gt;

&lt;p&gt;Every ad in Plinth is opt-in: continue a run once, double your coins, or earn coins in the shop. No banners and no interstitials. It earns less per player than forced ads would, but I'd rather people keep playing than bounce off an ad between every run.&lt;/p&gt;

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

&lt;p&gt;Plinth is free on Android: &lt;strong&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.gtfrexo.plinth" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;&lt;/strong&gt;. If you give it a go, I'd love to hear your best score, and any feedback on how it feels. And if you're building something similar in Flutter and Flame, ask away in the comments.&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Rockhounds should try this!</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 23:05:47 +0000</pubDate>
      <link>https://dev.to/gavin_rose/rockhounds-should-try-this-53lo</link>
      <guid>https://dev.to/gavin_rose/rockhounds-should-try-this-53lo</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-story__hidden-navigation-link"&gt;Pocket rockhound buddy that works anywhere&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/gavin_rose" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" alt="gavin_rose profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/gavin_rose" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Gavin Rose
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Gavin Rose
                
                
              
              &lt;div id="story-author-preview-content-4828855" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/gavin_rose" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1552594%2F9e23f0d7-6a72-4d7c-9916-47952f3e2088.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Gavin Rose&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" id="article-link-4828855"&gt;
          Pocket rockhound buddy that works anywhere
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hf26challenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hf26challenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>mobile</category>
      <category>showdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>Pocket rockhound buddy that works anywhere</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:27:51 +0000</pubDate>
      <link>https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod</link>
      <guid>https://dev.to/gavin_rose/pocket-rockhound-buddy-that-works-anywhere-2dod</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I love to explore nature, and while I'm familiar with the minerals, fossils, and stones around my area, I wanted something that could help people that may not know as much, and more importantly, something that could be used completely offline so that it would still work even if you are far into a trail.&lt;/p&gt;

&lt;p&gt;The idea is that a photo alone isn't normally enough to identify a rock, and even using flagship close models like gemini can give mixed results. There are several tests you can perform at the site or at home such as  scratching it, weighing it in their hand, or putting vinegar on it to see if there is a reaction. This app takes a photo for a first guess, then walks you through those same physical tests one at a time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can your fingernail scratch it? A coin? A nail?&lt;/li&gt;
&lt;li&gt;Rub it on an unglazed tile. What color is the powder?&lt;/li&gt;
&lt;li&gt;Does a magnet stick to it?&lt;/li&gt;
&lt;li&gt;Put a drop of vinegar on it. Does it fizz?&lt;/li&gt;
&lt;li&gt;Is it a shell with two halves? Is each half symmetrical?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each answer updates the odds, and the app always asks whichever test will narrow things down the most next. Most identifications take 5 to 8 tests. Between questions the phone is in your pocket and the rock is in your hand, which is the part I like best: &lt;strong&gt;the screen is the shortest part of the experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model knows 94 things: 53 minerals, 22 rocks and 19 fossils. It also knows where you're standing. It looks up the bedrock under you from open geologic map data, so in limestone country it leans toward calcite and crinoids, on granite toward feldspar and mica. It even knows the age of the rock. A trilobite can't turn up in rock that's 100 million years too young.&lt;/p&gt;

&lt;p&gt;When you're done, you get a specimen label (type, hardness, streak, where to look for more) and can save the find with its GPS location. Finds export as GeoJSON, so you can map everything you've found if desired.&lt;/p&gt;

&lt;p&gt;It's for anyone who picks up rocks on a hike, at a creek or on a beach and wonders what they're holding. That covers kids, rockhounds, and people like me with pockets full of crinoids.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; [&lt;a href="https://blue-math-567b.gavin-rose.workers.dev/" rel="noopener noreferrer"&gt;https://blue-math-567b.gavin-rose.workers.dev/&lt;/a&gt;]. Open it in Safari or Chrome on your phone and add it to your home screen. The first launch downloads the model (about 63 MB, Wi-Fi is recommended). After that, it works wherever you go! While a connection is available, it will use a larger hosted open weight model to give better results more quickly, and will fall back to the lightweight model when necessary or if you toggle the other one off.&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%2Fe01hy4o2chai4mc5oi47.webp" 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%2Fe01hy4o2chai4mc5oi47.webp" alt=" " width="710" height="1536"&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%2Fir6xsby05a313igo9fhq.webp" 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%2Fir6xsby05a313igo9fhq.webp" alt=" " width="710" height="1536"&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%2F8ju4xdi1mvzqm2vdriht.webp" 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%2F8ju4xdi1mvzqm2vdriht.webp" alt=" " width="710" height="1536"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I went out on a few walks near home, where the bedrock is shale and sandstone, and here's how it went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crinoids, three different ways.&lt;/strong&gt; Loose stem discs in my palm: crinoid stems, 92%. A stem lying along a rock face: 87%. A slab of fossil hash: 88%. Each one took about five tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It got things wrong too.&lt;/strong&gt; A small white chip came back as calcite, and it wasn't. One find only reached 19% for "conglomerate", which is really a shrug dressed up as an answer. And when I photographed nothing to test the app, it confidently named a rock anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It crashed.&lt;/strong&gt; On my iPhone the app downloaded the model, then Safari killed the page. In the field I had to time my taps to get in before it went down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The questions felt off.&lt;/strong&gt; The fossil-shape question ("stacked discs? coiled shell?") kept coming up before anything suggested a fossil, so for most rocks I was tapping "None of these".&lt;/p&gt;

&lt;p&gt;Every one of those turned into a fix, covered below. Taking it outside was the most useful testing I did.&lt;/p&gt;

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

&lt;p&gt;It is an installable web app (PWA) written in TypeScript with Vite. There's no app store and no account. The AI runs directly in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The photo model: SigLIP 2, on the phone
&lt;/h3&gt;

&lt;p&gt;The photo step uses &lt;strong&gt;&lt;a href="https://huggingface.co/onnx-community/siglip2-base-patch16-224-ONNX" rel="noopener noreferrer"&gt;SigLIP 2&lt;/a&gt;&lt;/strong&gt;, Google's open-weight image and text model, running fully on-device through &lt;strong&gt;&lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;ONNX Runtime Web&lt;/strong&gt;. On phones it runs as a 4-bit model (63 MB) on the CPU. On desktops with WebGPU it uses the fp16 version on the GPU.&lt;/p&gt;

&lt;p&gt;I didn't start with SigLIP. I started with CLIP, then benchmarked five open backbones on the same images and the same held-out split, training the same small classifier on top of each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backbone&lt;/th&gt;
&lt;th&gt;Top-1&lt;/th&gt;
&lt;th&gt;Top-3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLIP ViT-B/32 (where I started)&lt;/td&gt;
&lt;td&gt;43.5%&lt;/td&gt;
&lt;td&gt;61.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLIP ViT-B/16&lt;/td&gt;
&lt;td&gt;48.6%&lt;/td&gt;
&lt;td&gt;66.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DINOv2 small&lt;/td&gt;
&lt;td&gt;47.7%&lt;/td&gt;
&lt;td&gt;62.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DINOv3 small&lt;/td&gt;
&lt;td&gt;51.2%&lt;/td&gt;
&lt;td&gt;67.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SigLIP 2 base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SigLIP 2 won clearly, at the same 63 MB on the phone. Swapping it in took about an hour. That's only possible because every one of these models is open and runs in the same runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Training it on rocks
&lt;/h3&gt;

&lt;p&gt;Off the shelf, SigLIP 2 manages 29% top-1 across all 94 classes. I trained a small classification layer on top of it, using about 5,000 openly licensed images:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wikimedia Commons&lt;/strong&gt;: Creative Commons photos of minerals and rocks, with attribution saved for every file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GBIF&lt;/strong&gt;: CC0 and CC-BY photos of fossil specimens from natural history museums. These were the key to fossils, including plenty of the loose crinoid discs I actually find.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own field photos&lt;/strong&gt;: always trusted and never filtered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Web data is messy. A search for "granite" returns mountains, statues and kitchen counters. So the training script uses the model itself as a filter: it drops any image that looks more like a landscape, building or diagram than a rock specimen. Museum photos had the opposite problem: a tiny fossil next to a big ruler and a color card. For those, the script tries a grid of crops and keeps the one that looks most like a fossil and least like a ruler.&lt;/p&gt;

&lt;p&gt;The trained model gets &lt;strong&gt;62.3% top-1 and 78.6% top-3&lt;/strong&gt; on held-out images, up from 29% and 45% for the untrained model. Top-3 matters most, because the photo is only a starting point and the physical tests do the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tests: odds that update with every answer
&lt;/h3&gt;

&lt;p&gt;Each of the 94 entries has its properties written down: hardness range, streak color, luster, how it breaks, whether it's magnetic or fizzes, and for fossils, shape and age. The app keeps a probability for every candidate and updates it with each answer. Answers are treated as noisy, because people misjudge luster and vinegar is a weak acid.&lt;/p&gt;

&lt;p&gt;To pick the next question, it calculates which one is expected to shrink the uncertainty most (information gain). A few rules keep it sensible. It always opens with a broad "What did you find?", and fossil-only questions wait until the evidence actually leans toward a fossil.&lt;/p&gt;

&lt;h3&gt;
  
  
  Geology from where you're standing
&lt;/h3&gt;

&lt;p&gt;With your GPS position (GPS works without signal), Streak looks up the bedrock in &lt;strong&gt;&lt;a href="https://macrostrat.org" rel="noopener noreferrer"&gt;Macrostrat&lt;/a&gt;&lt;/strong&gt;, an open geologic map database. It turns rock types into odds: a granite map unit boosts quartz, feldspar and mica, and "crinoidal limestone" in the description boosts crinoids directly.&lt;/p&gt;

&lt;p&gt;It never rules anything out. Creeks, glaciers and landscaping gravel carry rocks for miles, so local geology only boosts candidates, and there's a "Found it somewhere else?" switch. Fossils also get an age check: outside their time range they're down-weighted, unless you're standing on river gravel, which can hold fossils of any age. Before a trip you can save the geology for about 20 km around you, so it works offline too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional online mode: Llama 4 Scout
&lt;/h3&gt;

&lt;p&gt;With signal, Streak can also send the photo to &lt;strong&gt;Llama 4 Scout&lt;/strong&gt;, Meta's open-weight vision model, running on Cloudflare Workers AI. The request goes through the app's own Cloudflare Worker, so the phone only ever talks to the app's domain. It sends the photo and the name of the bedrock, never your coordinates. The larger model's answer is blended with the on-device one, and if anything fails, it quietly falls back to offline. A toggle on the home screen turns it off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing what the field test found
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The crash:&lt;/strong&gt; iOS was killing the page because loading the 176 MB fp16 model needed several copies in memory at once. Phones now get the 4-bit model, and there's a crash guard: if the page dies while loading the model, the next launch automatically drops to the lightest setting instead of crashing in a loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"That's a rock" about nothing:&lt;/strong&gt; before identifying, the app now checks whether the photo shows a rock, mineral or fossil at all, by comparing it against descriptions of both specimens and non-specimens (an empty hand, the sky, a room). If not, it says so. Any result under 35% confidence now shows as &lt;strong&gt;Unknown&lt;/strong&gt; with its closest matches, instead of a confident wrong name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fossil questions too early:&lt;/strong&gt; fixed by the question ordering described above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rare minerals coming up too often:&lt;/strong&gt; a rarity prior. Halite dissolves in rain, so it almost never lies on the ground outside deserts, and the app shouldn't jump to it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this app, open models weren't just a nicer choice. Several features only work because of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with no signal.&lt;/strong&gt; Good rocks are in road cuts, creek beds and canyons, where you usually have no bars. A closed vision API is useless there. Because SigLIP 2 is open, its weights sit on the phone and run in the browser. The geology lookup is open data too, cached ahead of time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could host the weights myself, and that saved the project.&lt;/strong&gt; On the first real test, the model wouldn't download. My network was blocking huggingface.co entirely. With a closed API that would have been the end of it. Because the weights are open, I bundled them into the app and served them from my own Cloudflare site, split into 20 MB pieces the browser stitches back together. Now the app works on any network that can load it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could swap models and measure.&lt;/strong&gt; I benchmarked five open backbones on my own data and switched to the winner for an 11-point gain, without asking anyone's permission or changing any API contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could train it on my own rocks.&lt;/strong&gt; Museum and web photos don't look like a phone photo of a dusty pebble in your palm. With open weights I train my own layer on top of the model and keep adding my own field photos. Every crinoid I photograph makes the next ID better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your location stays on your phone.&lt;/strong&gt; Offline mode sends nothing anywhere. Online mode sends a photo and a bedrock name to a model I chose and deployed, not your coordinates to a company whose retention policy I'd have to trust. Where you find good rocks is something collectors like to keep to themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; On-device inference is free. Hosting is on Cloudflare's free tier, and online mode fits in Workers AI's free daily allowance, roughly 120 to 160 photo IDs a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything underneath is open.&lt;/strong&gt; Open models (SigLIP 2, Llama 4 Scout), open runtimes (Transformers.js, ONNX Runtime), open data (Macrostrat, GBIF, Wikimedia Commons). It's also honest about its limits: it shows you the odds, the closest alternatives, and when it doesn't know.&lt;/p&gt;

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

&lt;p&gt;I built the app with the help of Claude, where I asked the agent to do the user interface and wiring so I could focus on the logic and the AI under the hood.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Stay on topic with trance</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 03 Oct 2026 18:04:59 +0000</pubDate>
      <link>https://dev.to/gavin_rose/stay-on-topic-with-trance-2kb5</link>
      <guid>https://dev.to/gavin_rose/stay-on-topic-with-trance-2kb5</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;&lt;strong&gt;trance&lt;/strong&gt; is a small Mac app that keeps you focused on what matters most to you in the moment.&lt;/p&gt;

&lt;p&gt;To start, just type what you're doing ("answer emails", "study for my upcoming assignment", "do anything except doomscroll"), pick a track, and get on with your day. Every 45 seconds, a Gemma model running locally looks at what you've actually been doing. From there, it decides whether to stay quiet, nudge you back, or tell you it's time for five minutes away from the screen.&lt;/p&gt;

&lt;p&gt;I built this for my friend that has mentioned previously having a hard time staying on track. He was looking for something that could help him focus, provide insights, and gently keep him on track. &lt;/p&gt;

&lt;p&gt;Most focus apps block the websites or applications you don't want to visit: you list the bad sites and they slam the door. In my opinion, that doesn't fit how people actually drift away from their focused tasks. YouTube is a distraction until it's the lecture you need. Slack is a distraction until you're clearing your inbox. This app doesn't block anything. Instead, it tries to understand what you're doing, compared with what you said you wanted to do.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notices drift.&lt;/strong&gt; Each window you land on gets labeled against your intent: on-task or off-task, plus what kind of thing it is (work, reference, feed, video, game, etc.). The live view shows the label a few seconds after you switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nudges.&lt;/strong&gt; When you continue drifting, a small card slides into view, just like a notification. This nudge is intended to bring you back on track without disruption, but also helps the model judge better throughout the session if you tell it that whatever you're doing is actually on-task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommends breaks.&lt;/strong&gt; Each track has a working period and a recommended break period. If you remain on task, this app will let you continue working. If you start drifting, tab switching, or seem otherwise unfocused, it will suggest taking a break and show a nice calming visual during the break. Breaks are 5 minutes, but you can come back early or add time if you think you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracks.&lt;/strong&gt; Deep Work, Study, Create, Admin Sprint and Cruise each set what on-task looks like, how much switching is normal and how often breaks come. You can also make your own track if you need something a little more customized, along with setting the intent for the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recaps.&lt;/strong&gt; Whenever you end a session, Gemma writes a short summary of how the session went, any potential time sinks, what you did well, and what you could do better next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjpeoin1gzdlq2x71xnro.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%2Fjpeoin1gzdlq2x71xnro.png" alt=" " width="800" height="1340"&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%2F1o599gicb54t3ogojzs0.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%2F1o599gicb54t3ogojzs0.png" alt=" " width="800" height="1340"&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%2Fl5noqjzlt6wg6bzjynqc.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%2Fl5noqjzlt6wg6bzjynqc.png" alt=" " width="800" height="1340"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The open pieces:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; 4B (instruction-tuned), running locally in &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, inside a &lt;a href="https://tauri.app" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt; app written in Rust.&lt;/p&gt;

&lt;p&gt;Gemma reads, then the Rust code decides. Every 45 seconds the model returns a structured opinion of how your focus is going. I used Ollama's &lt;code&gt;format&lt;/code&gt; field, which takes a JSON schema, so that the answer always parses properly. Then plain code applies the rules: drift has to show up in two reads in a row before trance says anything, nudges have cooldowns, and flow is never interrupted.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a 4B model got wrong, and what fixed it
&lt;/h3&gt;

&lt;p&gt;This is the part that required the most time and attention. My first version of the prompt gave Gemma all of the raw activity logs and asked for a verdict. On a 12B Gemma model, this was a pretty good approach. The problem is that a 12B model running constantly while you're doing other work eats a lot of resources, and some people may not have the computer to handle that. When I switched to a 4B model and did a test run, the model decided that me spending 90% of my time on distractions instead of the task at hand was on-task. To combat this, the model now gets the window information and labels it, the Rust code calculates switch rates, scroll rates, typing, on-task time, etc., and the model then gets this structured information rather than being expected to do all of the calculations itself and getting less than ideal results.&lt;/p&gt;

&lt;p&gt;Another problem was stale labels. Each window gets labeled the first time you open it, and that label sticks for the session. A browser card game got marked as work and stayed that way, making me look far more on-task than I was. Now the labeler also gets a screenshot of the window, and if a later check-in sees the screen and disagrees with the label, the label is corrected on the spot.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Because of what trance has to look at.&lt;/strong&gt; To do its job, trance sees every window title you have open, which sites you're on, and how much you're typing and scrolling. With the glance feature on, it sees a capture of your screen about every 45 seconds. With an open model in Ollama, the most private data trance handles never leaves the workstation or laptop. Screenshots are read and deleted, and session logs are plain JSONL files in the user's own directories. No third parties peeking at your usage, no training being done on your data, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost and customization.&lt;/strong&gt; If you're doing a full work day session, you're probably going to leave this running in the background, even while away from the computer. Running locally, each check-in costs nothing but a bit of battery; on a metered API, a full work day in the background adds up fast. Using Ollama also gives you access to a whole plethora of open models, so you can swap them on command to see what works best for your situation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model would have been easier:&lt;/strong&gt; To be honest, a big hosted model would have needed fewer guardrails and probably would have produced better results more quickly. Most of my weekend went into making a 4B model reliable, but the solutions to those pesky edge cases turned into features that otherwise might not have been considered. The rules that decide whether trance interrupts you are just code that anyone can read: no black box, and no server calls.&lt;/p&gt;

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

&lt;p&gt;I've always had a weak spot with frontend development and making things look professional, clean, and polished. Because I used Claude Code to create the user interface, I was able to spend my time where I shine best, building the functionality. Claude also helped me track down a few bugs with rendering and permission issues.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 4B runs locally through Ollama. It reads user intent, labels windows (with vision), judges focus every 45 seconds, and writes the session recap, all with no cloud calls!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Atlas</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sun, 19 Apr 2026 19:25:20 +0000</pubDate>
      <link>https://dev.to/gavin_rose/atlas-learn-about-the-earth-cio</link>
      <guid>https://dev.to/gavin_rose/atlas-learn-about-the-earth-cio</guid>
      <description>&lt;h1&gt;
  
  
  Explore Earth, One Swipe at a Time
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Some of us spend hours every day scrolling. The format is compelling, and the habit is already formed. The question we asked ourselves was what if that scroll actually taught you something?&lt;/p&gt;

&lt;p&gt;Atlas takes the interaction pattern everyone already knows and fills it with things worth knowing about the planet we live on... things like geology, ecology, ocean science, extreme weather, wildlife, and geography. No headlines, no opinions. Just Earth facts, beautifully presented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Atlas is a vertical swipe feed of Earth fact cards, built and deployed as a Flutter web app.  Beyond the content, Atlas has a full progression system aimed to keep people engaged as they learn about the Earth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XP and levels&lt;/strong&gt;: you earn XP for every new card you read, every quiz you reveal, and every card you save. The XP system has eight explorer levels from Seedling to World Wonder, and each level unlocks a new ambient visual theme (Ocean, Forest, Dusk, Aurora, Glacier, Desert, Volcanic).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievements&lt;/strong&gt;: ten unlockable achievements tied to meaningful milestones; exploring five nature cards, saving five cards, revealing five quizzes, exploring all categories, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily streak&lt;/strong&gt;: the app tracks consecutive days of use and surfaces your streak in the HUD once it hits day two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic content&lt;/strong&gt;: the feed mixes 60+ curated cards with live data pulled from four public APIs, refreshed every 24 hours: NASA APOD (astronomy images), USGS Significant Earthquakes (real-time seismic events), Wikipedia article summaries (25 curated Earth topics, four chosen at random per session), and Open Trivia DB (science and geography questions turned into Real or Fake cards).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The entire app is written in Flutter, hosted on Firebase Hosting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack/Packages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flutter/Dart&lt;/li&gt;
&lt;li&gt;Provider for state management&lt;/li&gt;
&lt;li&gt;SharedPreferences for local persistence&lt;/li&gt;
&lt;li&gt;http for API calls&lt;/li&gt;
&lt;li&gt;flutter_animate for card entry animations and toast transitions&lt;/li&gt;
&lt;li&gt;share_plus for native share sheet&lt;/li&gt;
&lt;li&gt;Firebase Hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges We Ran Into
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Swipe-one-card-at-a-time.&lt;/strong&gt; Flutter's PageView will happily let you queue multiple &lt;code&gt;animateToPage&lt;/code&gt; calls, which causes the view to snap to the wrong card when the user swipes quickly. The fix was disabling the PageView's built-in gesture handling entirely and replacing it with a manual drag detector and an animation lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accomplishments We're Proud Of
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A scroll experience that feels genuinely close to TikTok or Reels, built entirely in Flutter for web, with no JavaScript.&lt;/li&gt;
&lt;li&gt;A progression system that changes the visual environment of the app as you level up, so the reward is aesthetic and immediate, not just a number going up.&lt;/li&gt;
&lt;li&gt;Live data from four different public APIs, unified into the same card format as the curated content, with graceful offline fallback.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Short form content, especially as text, forces content to be better. Writing 60+ cards where every headline had to land in under 15 words was harder than any of the actual programming or engineering! XD&lt;/p&gt;

&lt;p&gt;We observed that while typically Flutter can be lacking for web development, when developing for designs that are mobile-first, Flutter was still able to shine although it felt like it took a little longer than doing something react-based may have taken.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-generated cards&lt;/strong&gt;: a simple submission form where anyone can contribute a fact, queued for moderation before it enters the live feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More API sources&lt;/strong&gt;: NOAA for weather extremes, iNaturalist for real-time species observations, NASA Earth imagery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personalisation&lt;/strong&gt; — mood-based filtering (Relax / Curious / Mind-Blown tags are already on every card, just not yet surfaced in the UI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native mobile builds&lt;/strong&gt;: the codebase is already cross-platform; the only web-specific concession is SharedPreferences over localStorage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Built With
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flutter&lt;/code&gt; &lt;code&gt;dart&lt;/code&gt; &lt;code&gt;firebase&lt;/code&gt; &lt;code&gt;nasa-api&lt;/code&gt; &lt;code&gt;usgs-api&lt;/code&gt; &lt;code&gt;wikipedia-api&lt;/code&gt; &lt;code&gt;open-trivia-db&lt;/code&gt; &lt;code&gt;provider&lt;/code&gt; &lt;code&gt;shared-preferences&lt;/code&gt; &lt;code&gt;flutter-animate&lt;/code&gt; &lt;code&gt;glassmorphism&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;The repo is publicly available &lt;a href="https://github.com/gavin-rose/Atlas" rel="noopener noreferrer"&gt;here&lt;/a&gt; &lt;br&gt;
The build itself can be found &lt;a href="https://atlas-26.web.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Scarab | Weekend Data</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Sat, 18 Apr 2026 12:48:38 +0000</pubDate>
      <link>https://dev.to/gavin_rose/scarab-weekend-data-38i7</link>
      <guid>https://dev.to/gavin_rose/scarab-weekend-data-38i7</guid>
      <description>&lt;p&gt;Scarab can give you a summary on what data is missing from your data with only a few clicks&lt;br&gt;
&lt;a href="https://debugr.gumroad.com/l/scarab" rel="noopener noreferrer"&gt;Download Now&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4b01u06r9ic36k4d33i1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4b01u06r9ic36k4d33i1.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>datascience</category>
      <category>data</category>
      <category>rust</category>
    </item>
    <item>
      <title>The new way to explore data</title>
      <dc:creator>Gavin Rose</dc:creator>
      <pubDate>Thu, 16 Apr 2026 17:12:37 +0000</pubDate>
      <link>https://dev.to/gavin_rose/the-new-way-to-explore-data-1i2</link>
      <guid>https://dev.to/gavin_rose/the-new-way-to-explore-data-1i2</guid>
      <description>&lt;p&gt;Scarab was built to allow people with no programming or statistical language experience analyze their data. It produces code snippets for those that want to bring insights into their own program, and can help gain valuable insights from your data as you progress. Free while in the testing phase!&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://debugr.gumroad.com/l/scarab" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;debugr.gumroad.com&lt;/span&gt;
          

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


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq7lezp679wovmwj3m6sd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq7lezp679wovmwj3m6sd.png" alt="Natural language analysis" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>data</category>
      <category>statistics</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
