<?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: Gabriel Almeida</title>
    <description>The latest articles on DEV Community by Gabriel Almeida (@ghalmeida).</description>
    <link>https://dev.to/ghalmeida</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%2F4167080%2F6668579c-5b68-4bee-9799-7e47772ae2b3.jpg</url>
      <title>DEV Community: Gabriel Almeida</title>
      <link>https://dev.to/ghalmeida</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ghalmeida"/>
    <language>en</language>
    <item>
      <title>TouchGrass API: outdoor missions from local Gemma 3 and the weather</title>
      <dc:creator>Gabriel Almeida</dc:creator>
      <pubDate>Tue, 06 Oct 2026 18:19:44 +0000</pubDate>
      <link>https://dev.to/ghalmeida/touchgrass-api-outdoor-missions-from-local-gemma-3-and-the-weather-1bof</link>
      <guid>https://dev.to/ghalmeida/touchgrass-api-outdoor-missions-from-local-gemma-3-and-the-weather-1bof</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;&lt;strong&gt;TouchGrass API&lt;/strong&gt; is a small backend that answers one question: &lt;em&gt;"I have 20 free minutes. What should I do outside?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You send it a city and the minutes you have. It looks up the current weather, hands that to a local open-weight model, and returns one short, concrete outdoor mission that fits both the sky and your schedule. Drizzle gets a "stand under an awning and listen to the rain" mission. A clear afternoon gets a walk with a destination. Do the mission, check in, and the API tracks your streak of consecutive days.&lt;/p&gt;

&lt;p&gt;It is three endpoints on purpose, so the screen stays the shortest part of the experience: ask, read one paragraph, go outside.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /mission&lt;/code&gt; takes &lt;code&gt;{ city, minutes }&lt;/code&gt; and returns a mission.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /checkin&lt;/code&gt; marks a mission as done. It is idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /streak&lt;/code&gt; returns &lt;code&gt;{ current, longest, completedToday }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is built for anyone who wants a nudge off the screen without installing another app that wants their data. Because it is an API, it can sit behind a CLI, a shortcut, a widget or a bot.&lt;/p&gt;

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

&lt;p&gt;This is a real response from my machine, with the model running locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/mission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"city":"Curitiba","minutes":20}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Observação da Garoa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saia e observe a garoa que cobre Curitiba. Concentre-se nos sons da chuva, nas gotas que escorrem pelas folhas e na sensação do ar úmido no rosto. [...]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weather"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Curitiba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"temperatureC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"precipitationMm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windKmh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isDay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"garoa"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Curitiba was drizzling at 14 °C when I called it, and the model proposed a rain-listening mission instead of a jog. That is the point: the weather changes the mission.&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/GAlmeidaDev" rel="noopener noreferrer"&gt;
        GAlmeidaDev
      &lt;/a&gt; / &lt;a href="https://github.com/GAlmeidaDev/TouchGrass-API" rel="noopener noreferrer"&gt;
        TouchGrass-API
      &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;🌱 TouchGrass API&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Fecha o notebook. Abre a porta. &lt;strong&gt;Vai pegar sol.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;API em Node/TypeScript que olha o clima da sua cidade e pede a um modelo de IA &lt;strong&gt;open-weight rodando na sua máquina&lt;/strong&gt; uma missão curta ao ar livre, do tamanho do tempo que você tem. Feita para o desafio &lt;em&gt;Touch Grass&lt;/em&gt; do Hacktoberfest 2026 Open-Source AI Challenge (DEV).&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Como funciona&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;POST /mission {city, minutes}
   │
   ├─► Open-Meteo (geocoding + clima atual, sem chave de API)
   └─► Gemma 3 4B via Ollama (local) ─► missão em JSON validado
         │
         └─► SQLite (histórico de missões)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;O que faz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /mission&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Gera uma missão para a cidade e o tempo livre informados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /checkin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Marca a missão como cumprida (&lt;code&gt;{"missionId": 1}&lt;/code&gt;); idempotente&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /streak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Dias seguidos com missões cumpridas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Como rodar&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Requisitos: Node 20+ e &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/GAlmeidaDev/TouchGrass-API" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;The open-weight model.&lt;/strong&gt; Missions come from &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; served by &lt;strong&gt;Ollama&lt;/strong&gt;, running on my own machine. It is about 3.3 GB and fast enough on a regular laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output instead of prompt-and-pray.&lt;/strong&gt; A 4B model is happy to ramble. I pass a JSON schema (generated from a zod schema) in Ollama's &lt;code&gt;format&lt;/code&gt; field, so the model has to answer &lt;code&gt;{ "title", "description" }&lt;/code&gt;. The reply is then parsed with the same zod schema, and anything invalid becomes a clean &lt;code&gt;503&lt;/code&gt; rather than garbage in the database. The system prompt asks for pt-BR, a mission that fits the time including the walk back, no equipment, and a safe alternative when there is rain, a storm, extreme cold or darkness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weather without an API key.&lt;/strong&gt; &lt;a href="https://open-meteo.com" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; handles geocoding and current conditions, so there is nothing to sign up for. I map the WMO weather code to a readable condition, and the model sees temperature, feels-like, rain, wind and day or night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaks.&lt;/strong&gt; Missions live in SQLite (&lt;code&gt;better-sqlite3&lt;/code&gt;). Check-ins store the local day, and the streak is computed from distinct completed days. It stays alive if your last completed day was yesterday, so you have until the end of today to keep it, and it resets after a skipped day while preserving your longest run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testable by design.&lt;/strong&gt; The weather provider, the mission generator and the clock are injected into the Fastify app. The 12 tests run with no network and no Ollama, and the clock injection let me test consecutive days and a month rollover without waiting.&lt;/p&gt;

&lt;p&gt;Stack: Node, TypeScript, Fastify, zod, SQLite, Ollama, Gemma 3 4B, Open-Meteo.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The model runs on your machine. Where you live and how often you go outside is not sent to an AI provider. The only outbound request is the weather lookup, which carries a city name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-capable.&lt;/strong&gt; Inference needs no internet. Only the forecast does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost.&lt;/strong&gt; No API key, quota or per-token bill, so a habit app can call the model every day for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable.&lt;/strong&gt; Open weights mean one environment variable (&lt;code&gt;OLLAMA_MODEL&lt;/code&gt;) swaps the model. A closed API can change or retire a model under you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't tried the closed-API version of this, so I won't claim it is worse. For a tool about personal routine, keeping the data local was the deciding factor.&lt;/p&gt;

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

&lt;p&gt;This is the backend only. Next I want a tiny client (CLI or phone shortcut) and per-user time zones. Right now days follow the server's time zone, which fits the local, single-user setup.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 4B runs locally through Ollama and generates every mission.&lt;/p&gt;

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