<?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: Kourtney Lee</title>
    <description>The latest articles on DEV Community by Kourtney Lee (@kourtneylee1611).</description>
    <link>https://dev.to/kourtneylee1611</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%2F3842312%2F490cdedd-032c-4f69-9cac-2c390c2672ef.jpg</url>
      <title>DEV Community: Kourtney Lee</title>
      <link>https://dev.to/kourtneylee1611</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kourtneylee1611"/>
    <language>en</language>
    <item>
      <title>Peak Season: an open-source AI that tells you where the leaves are at their peak, anywhere in the world</title>
      <dc:creator>Kourtney Lee</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:42:48 +0000</pubDate>
      <link>https://dev.to/kourtneylee1611/peak-season-an-open-source-ai-that-tells-you-where-the-leaves-are-at-their-peak-anywhere-in-the-57bo</link>
      <guid>https://dev.to/kourtneylee1611/peak-season-an-open-source-ai-that-tells-you-where-the-leaves-are-at-their-peak-anywhere-in-the-57bo</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;Every October I ask the same question: are the maples in High Park at their best yet, or am I a week early? I'm not the only one. Someone in Kyoto asks it about &lt;em&gt;momiji&lt;/em&gt; (紅葉), someone in Seoul about &lt;em&gt;danpung&lt;/em&gt; (단풍), and in a few months someone in Sydney or Taipei will ask it about cherry blossoms. The answer usually exists. It's just scattered across yesterday's photos on the internet.&lt;/p&gt;

&lt;p&gt;Peak Season answers it for wherever you are. Open the page, and it shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the plant of the moment for your area: autumn leaves now, cherry blossoms in spring. It's picked from where recent reports are coming in, so it works in both hemispheres without a calendar;&lt;/li&gt;
&lt;li&gt;a short list of places near you, Peak first, then by distance. Each place has a stage badge (&lt;em&gt;Green / Turning / Peak / Past peak&lt;/em&gt;), how many recent reports back it up and how fresh they are, and one photo with its photographer's credit and licence;&lt;/li&gt;
&lt;li&gt;Directions buttons that open Google Maps, Apple Maps or OpenStreetMap on your phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no feed, account or infinite scroll. I wanted the screen to be the shortest part of the outing: you decide, tap Directions, and go outside.&lt;/p&gt;

&lt;p&gt;It's for anyone who loves an autumn walk but doesn't want to drive an hour to find bare branches. And it's built to keep running, for free, long after this challenge.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://peak-season-dvl.pages.dev/" rel="noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It asks for your location. If you'd rather not share it, pick a city. To look at other places, add coordinates to the URL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Toronto: &lt;a href="https://peak-season-dvl.pages.dev/?lat=43.65&amp;amp;lng=-79.38" rel="noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/?lat=43.65&amp;amp;lng=-79.38&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kyoto: &lt;a href="https://peak-season-dvl.pages.dev/?lat=35.01&amp;amp;lng=135.77" rel="noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/?lat=35.01&amp;amp;lng=135.77&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Seoul: &lt;a href="https://peak-season-dvl.pages.dev/?lat=37.57&amp;amp;lng=126.98" rel="noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/?lat=37.57&amp;amp;lng=126.98&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sydney: &lt;a href="https://peak-season-dvl.pages.dev/?lat=-33.87&amp;amp;lng=151.21" rel="noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/?lat=-33.87&amp;amp;lng=151.21&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/bHnPfK5wLMY" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;The data refreshes every 12 hours, and "Last updated" at the bottom shows how fresh it is. It's early October, so most of the world is still green or just starting to turn, and that's what the site shows. Where there isn't enough evidence, it says "Not enough reports".&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/klee1611" rel="noopener noreferrer"&gt;
        klee1611
      &lt;/a&gt; / &lt;a href="https://github.com/klee1611/peak-season" rel="noopener noreferrer"&gt;
        peak-season
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Where nature is at its peak near you, right now. Gemma on a free GitHub Actions runner judges autumn leaves and cherry blossoms from iNaturalist photos. $0/month.
    &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;Peak Season&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Where nature is at its peak near you, right now.&lt;/strong&gt; Autumn leaves, cherry blossoms, and more
anywhere in the world.&lt;/p&gt;
&lt;p&gt;Peak Season collects recent public nature photos (iNaturalist) and web reports (SerpApi, for
famous spots). An open-weight model, &lt;strong&gt;Gemma&lt;/strong&gt;, runs on a free GitHub Actions runner and judges
how far along each seasonal moment is in each photo. The site then tells you, for wherever you
are, which places nearby are at their best, how fresh the evidence is, and gives you one-tap
directions in your own map app. Decide, tap Directions, go outside.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 1: "Touch Grass".&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live site:&lt;/strong&gt; &lt;a href="https://peak-season-dvl.pages.dev/" rel="nofollow noopener noreferrer"&gt;https://peak-season-dvl.pages.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs at $0/month:&lt;/strong&gt; a static site plus a scheduled GitHub Actions job. No servers, no
database, no paid APIs required.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt; every 12 h, GitHub Actions (CPU only, free for public repos)
 iNaturalist (global) ─┐
 SerpApi&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/klee1611/peak-season" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed. The pipeline is Python 3.12 and the site is vanilla JavaScript, with no framework, map library or third-party scripts. There are 110 tests.&lt;/p&gt;

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

&lt;p&gt;The open-source AI at the core is Gemma (&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;, a small multimodal Gemma), served by Ollama on a free GitHub Actions runner. It runs on the runner's CPU and costs nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions, every 12 hours (free CPU runner)

     iNaturalist photos              SerpApi web snippets
        (worldwide)                  (famous spots only)
             │                                │
             └───────────────┬────────────────┘
                             ▼
            Gemma via Ollama judges the stage
             (every answer cached in SQLite)
                             │
                             ▼
         Group into ~5 km cells, weighted average
                             │
                             ▼
       Small JSON files, one per region: git commit
                             │
                             ▼
          Cloudflare Pages serves the static site;
           your browser loads only your region

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Collect.&lt;/strong&gt; The workflow pulls recent &lt;a href="https://www.inaturalist.org" rel="noopener noreferrer"&gt;iNaturalist&lt;/a&gt; observations of each plant worldwide. It keeps only photos whose Creative Commons licence allows showing them. One run finds about 5,600 maple observations in 3,100 places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Gemma looks at the photos.&lt;/strong&gt; For each photo, Gemma answers in strict JSON: is this really a maple, and is it green, turning, at peak or past peak? The answer is checked against the plant's list of stages. Invalid output gets one retry, then it's marked unclassified. Photos of just a trunk or a single fallen leaf are rejected, because they say nothing about the tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Gemma reads the web, in the local language.&lt;/strong&gt; iNaturalist is used mostly by naturalists in North America and Europe. Millions of people visit Arashiyama or Seoraksan in autumn, but those places barely appear there. So for world-famous spots that are in season nearby but have few reports, the pipeline searches Google through SerpApi, in the spot's own language, for the past week only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"嵐山" 紅葉 見頃                         Arashiyama, autumn leaves, best viewing     hl=ja gl=jp
"parc de la Gatineau" couleurs automne                                             hl=fr gl=ca
"fall colours" "High Park"                                                         hl=en gl=ca
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma then reads the result snippets and extracts a stage. It understands phrases like 見頃 (at its best), 色づき始め (starting to turn) or 단풍 절정 (peak). The site links to the sources and doesn't download their images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Combine.&lt;/strong&gt; Reports are grouped into cells about 5 km across and averaged. Fresh, confident reports count more (&lt;code&gt;confidence × weight × e^(−age / 3 days)&lt;/code&gt;), and a web snippet counts 0.6 of a photo. A place with fewer than two reports shows "Not enough reports" instead of a stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Publish static files.&lt;/strong&gt; The workflow commits small JSON files, one per region of about 1,250 km (about 240 KB for the whole world), and Cloudflare Pages redeploys. Your browser downloads only your region and its neighbours, and works out distances itself. Visitors never trigger an API or model call, so traffic costs nothing, however much there is.&lt;/p&gt;

&lt;h3&gt;
  
  
  A free CPU is slow, so the pipeline has a budget
&lt;/h3&gt;

&lt;p&gt;On the runner's CPU, Gemma takes about 44 seconds per photo (17 s on my laptop's GPU). That's about 110 photos per run, against thousands of new ones. So each run spends its time carefully: at most 3 photos per place, famous spots first, then the busiest places, newest photos first. It stops at a fixed time so it always finishes publishing. Every answer is cached, so the next run picks up where the last one stopped.&lt;/p&gt;

&lt;p&gt;SerpApi has its own guardrails. Before searching, the pipeline asks SerpApi's account endpoint how many searches are left (that check is free). It keeps a reserve, caps searches per run, and never repeats a search within 48 hours. If it can't check the account, it makes zero searches.&lt;/p&gt;

&lt;p&gt;The first live SerpApi run found a bug: photos ran first and used up the time, so Gemma read only 13 of the 32 snippets I'd paid searches for. Now, when SerpApi is on, photo work stops 20 minutes early so every paid search gets read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't trust the model: test it first
&lt;/h3&gt;

&lt;p&gt;A plant only appears on the site after its prompt agrees with iNaturalist's community annotations (people tagging a photo "Green Leaves", "Colored Leaves", "Flowers"...) on at least 20 photos, at 70% or better.&lt;/p&gt;

&lt;p&gt;My first maple prompt failed at 62%. Gemma called green leaves "turning" whenever sunlight hit them, when the canopy was lit from behind, or when leaves had cream-coloured edges. I also found a bug in my own test: one observation can be tagged both "Green Leaves" and "Colored Leaves" (a half-turned tree), and I was counting a correct answer as wrong. After fixing both, maple passed at 81%, and again at 81% on 26 older photos the prompt had never seen. Cherry blossoms passed at 92%, with one caveat I'd rather say out loud: none of the sample photos showed buds only, so "buds vs. bloom" hasn't really been tested yet.&lt;/p&gt;

&lt;p&gt;Two more rules. iNaturalist's "Colored Leaves" tag marks a place as &lt;em&gt;in season&lt;/em&gt; but never sets a stage, because it covers everything from the first red leaf to bare branches. And nothing in the code knows what a maple is. Each plant is a YAML file and a prompt, so cherry blossoms went live in the southern hemisphere with no code changes.&lt;/p&gt;

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

&lt;p&gt;Without an open model, this project couldn't run for free.&lt;/p&gt;

&lt;p&gt;Peak Season looks at hundreds of photos a day. With a closed vision API, that would be a monthly bill that grows with the world. With Gemma on a free runner it's $0 a month, however many people visit, so it can keep running after the hackathon instead of shutting down when the credits run out.&lt;/p&gt;

&lt;p&gt;The same open model reads a photo from Toronto and a Japanese news snippet about Arashiyama. I didn't have to pay extra to make it work in more than one language.&lt;/p&gt;

&lt;p&gt;People's photos only go to a model running on the project's own runner. They're never sent to a third-party AI service, and never stored or rehosted.&lt;/p&gt;

&lt;p&gt;Nothing is locked in. The model is called through the OpenAI-compatible protocol, so moving to a bigger Gemma, or to a hosted Gemma when the CPU isn't enough, takes two repository secrets and no code change. The site is plain files that could move to any static host tomorrow.&lt;/p&gt;

&lt;p&gt;Anyone can check the work. The prompts, the validation script, its results and the data are all public. If you think Gemma is wrong about your local park, you can see why, and fix it.&lt;/p&gt;

&lt;p&gt;And a new plant is a pull request. Plum blossoms, tulips, lavender or ginkgo each need one YAML file and one prompt, which would make a nice first Hacktoberfest contribution.&lt;/p&gt;

&lt;p&gt;The same pipeline tells someone in Toronto when to see maples and someone in Sydney when to see cherry blossoms, and I'd like it to stay open for anyone to use or extend.&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 runs self-hosted on a free CPU runner, judges photos, and reads web snippets in Japanese, Korean, Chinese, French and English, with a public, repeatable test against iNaturalist annotations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi:&lt;/strong&gt; budgeted searches in the local language for famous spots that iNaturalist misses, with the budget read live from SerpApi's account endpoint and every response cached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading. Now close the tab and go outside. 🍁&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Maple Route: A Multi-Agent AI That Checks Whether Canadian Immigration Rules You Heard Are Still True</title>
      <dc:creator>Kourtney Lee</dc:creator>
      <pubDate>Mon, 05 Oct 2026 04:29:14 +0000</pubDate>
      <link>https://dev.to/kourtneylee1611/maple-route-a-multi-agent-ai-that-checks-whether-canadian-immigration-rules-you-heard-are-still-3jec</link>
      <guid>https://dev.to/kourtneylee1611/maple-route-a-multi-agent-ai-that-checks-whether-canadian-immigration-rules-you-heard-are-still-3jec</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Maple Route covers only a few official pages, so it can't answer every immigration question and may leave out rules that apply to you. It's a reference for spotting possible options and policy changes, &lt;strong&gt;not legal advice&lt;/strong&gt;. For your situation, please talk to a licensed immigration consultant (RCIC) or a lawyer.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;Maple Route&lt;/strong&gt; is a multi-agent AI system that answers questions about a &lt;strong&gt;small set of Canadian immigration topics&lt;/strong&gt;, using only official pages stored in a Sanity Knowledge Base:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Express Entry points (CRS criteria)&lt;/li&gt;
&lt;li&gt;open work permits for spouses and family members&lt;/li&gt;
&lt;li&gt;post-graduation work permit eligibility&lt;/li&gt;
&lt;li&gt;2026 changes to the Ontario Immigrant Nominee Program&lt;/li&gt;
&lt;li&gt;a few related canada.ca pages, such as the Global Talent Stream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four AI agents work on each question: one understands your situation, one reads the relevant Knowledge Base entries, one writes the answer, and one checks every statement against its source. A final code step removes anything the sources don't support. Maple Route tells you which options &lt;em&gt;might&lt;/em&gt; apply, and flags a rule you mention if it has changed. If a question is outside these pages, it says so instead of guessing.&lt;/p&gt;

&lt;p&gt;I built it with tech workers in mind, because the rules they rely on change often. Express Entry stopped giving points for job offers in March 2025. Spousal open work permits were narrowed. Post-graduation work permit rules have changed several times. But blog posts, forum threads and AI chatbots keep repeating the old versions. People planning a move, often reading in their second language, can't easily tell which version is current, and getting it wrong can cost them months.&lt;/p&gt;

&lt;p&gt;You describe your situation in plain words: &lt;em&gt;"I'm a software developer in Toronto with a job offer"&lt;/em&gt;, or &lt;em&gt;"I read that a job offer gives me 50 extra Express Entry points"&lt;/em&gt;. Maple Route then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answers &lt;strong&gt;only&lt;/strong&gt; from a Sanity Knowledge Base of official pages, never from the model's own memory;&lt;/li&gt;
&lt;li&gt;puts a &lt;strong&gt;citation on every statement&lt;/strong&gt;, and removes any statement it can't back with a source;&lt;/li&gt;
&lt;li&gt;checks the rules &lt;strong&gt;you&lt;/strong&gt; mention, and shows a &lt;strong&gt;Changed&lt;/strong&gt; callout when one is out of date: what you heard, what the current rule says, and the official source;&lt;/li&gt;
&lt;li&gt;says plainly when its sources don't cover your question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the difference it makes. I asked the same model the same question with and without the Knowledge Base:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; I read that a job offer gives me 50 or 200 extra points in Express Entry. Is that still true?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same model, no Knowledge Base:&lt;/strong&gt; "Yes — having a valid job offer can still add points in the Express Entry system… NOC 00: 200 points… other skilled: 50 points."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maple Route:&lt;/strong&gt; "Job offer points are no longer added to the Comprehensive Ranking System score for any Express Entry candidate. [1]"&lt;/p&gt;

&lt;p&gt;⚠ &lt;strong&gt;Changed.&lt;/strong&gt; &lt;em&gt;Before:&lt;/em&gt; A job offer grants 50 or 200 extra points in Express Entry. &lt;em&gt;Now:&lt;/em&gt; As of March 25, 2025, job offer points have been removed from the CRS for all current and future candidates. — [1] Express Entry: Comprehensive Ranking System (CRS) criteria, Canada.ca&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The full side-by-side for all test questions is in &lt;a href="https://github.com/klee1611/maple-route/blob/main/eval/results.md" rel="noopener noreferrer"&gt;eval/results.md&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%2F8d2t0el9nmy8n2rizalp.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%2F8d2t0el9nmy8n2rizalp.png" alt="Maple route screenshot" width="754" height="779"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://maple-route.vercel.app" rel="noopener noreferrer"&gt;https://maple-route.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1-cmFlTunG8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Try one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;I read that a job offer gives me 50 or 200 extra points in Express Entry. Is that still true?&lt;/em&gt; (shows the Changed callout)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Does my spouse get an open work permit if I come to Canada on a work permit?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I graduated from a Canadian college program two years ago. Can I still get a post-graduation work permit?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Can I get a work permit in Australia?&lt;/em&gt; (it says this is outside its sources instead of guessing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a free project running on free tiers, so there's a daily answer limit shown on the page. Repeated questions are served from a cache.&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/klee1611" rel="noopener noreferrer"&gt;
        klee1611
      &lt;/a&gt; / &lt;a href="https://github.com/klee1611/maple-route" rel="noopener noreferrer"&gt;
        maple-route
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Multi-agent assistant for Canadian work permit and PR rules, grounded in a Sanity Knowledge Base, with citations and changed-rule detection.
    &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;Maple Route&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A multi-agent AI system that answers questions about a &lt;strong&gt;small set of Canadian immigration topics&lt;/strong&gt;, using only official pages stored in a Sanity Knowledge Base:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Express Entry points (CRS criteria)&lt;/li&gt;
&lt;li&gt;open work permits for spouses and family members&lt;/li&gt;
&lt;li&gt;post-graduation work permit eligibility&lt;/li&gt;
&lt;li&gt;2026 changes to the Ontario Immigrant Nominee Program&lt;/li&gt;
&lt;li&gt;a few related canada.ca pages, such as the Global Talent Stream&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Four AI agents work on each question, and a final code step removes anything the sources don't support. Every claim has a citation, rules the user mentions are flagged if they have changed, and questions outside these pages get a "not covered" answer. Built with tech workers in mind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://maple-route.vercel.app" rel="nofollow noopener noreferrer"&gt;https://maple-route.vercel.app&lt;/a&gt; · &lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/1-cmFlTunG8" rel="nofollow noopener noreferrer"&gt;https://youtu.be/1-cmFlTunG8&lt;/a&gt; · &lt;strong&gt;Write-up:&lt;/strong&gt; &lt;a href="https://dev.to/kourtneylee1611/maple-route-a-multi-agent-ai-that-checks-whether-canadian-immigration-rules-you-heard-are-still-3jec" rel="nofollow"&gt;DEV.to&lt;/a&gt;&lt;/p&gt;

  
    
    &lt;span class="m-1"&gt;Maple.Route.-.demo.GitHub.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;Maple Route is free to use. If it helped you, you can support it here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/klee1611" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0cf29a542375e1a46e84d8bf5805a4e5c0a6ee98b6547ccdc0c55eed49d99c69/68747470733a2f2f63646e2e6275796d6561636f666665652e636f6d2f627574746f6e732f76322f64656661756c742d79656c6c6f772e706e67" alt="Buy me a coffee" height="40"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Information, not legal advice. Not affiliated with the Government of Canada.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Scope:&lt;/h2&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/klee1611/maple-route" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Python (FastAPI, LangGraph) with Groq models, and a Next.js frontend, deployed as one Vercel project.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;The Sanity Knowledge Base is the core of Maple Route. It is the agents' only source of policy facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I pointed Sanity Context at.&lt;/strong&gt; I kept the Knowledge Base small and focused. It has five website sources, all official pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.canada.ca/en/immigration-refugees-citizenship/services/immigrate-canada/express-entry/check-score/crs-criteria.html" rel="noopener noreferrer"&gt;canada.ca: Express Entry CRS criteria&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Express Entry points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.canada.ca/en/immigration-refugees-citizenship/services/work-canada/special-instructions/spouses-dependent-children/eligibility.html" rel="noopener noreferrer"&gt;canada.ca: Open work permits for family members&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Spouse and family work permits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.canada.ca/en/immigration-refugees-citizenship/services/study-canada/work/after-graduation/eligibility.html" rel="noopener noreferrer"&gt;canada.ca: Post-graduation work permit eligibility&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Post-graduation work permits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.canada.ca/en/immigration-refugees-citizenship.html" rel="noopener noreferrer"&gt;canada.ca: Immigration, Refugees and Citizenship&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;IRCC home page and a few related pages, such as the Global Talent Stream (3 documents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.ontario.ca/page/2026-ontario-immigrant-nominee-program-updates" rel="noopener noreferrer"&gt;ontario.ca: 2026 OINP updates&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ontario Immigrant Nominee Program changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's deliberately narrow: enough to show the approach on rules that changed recently, not a complete guide to Canadian immigration. Questions outside these pages get "I can't answer this from my sources" instead of a guess.&lt;/p&gt;

&lt;p&gt;Because they're website sources, the Knowledge Base can re-check the live pages for changes, so answers follow the current version of each page without a redeploy.&lt;/p&gt;

&lt;p&gt;The best surprise: the Knowledge Base &lt;strong&gt;builds its own change history.&lt;/strong&gt; From these pages, the build produced a policy timeline entry with "What is now outdated" and "What is currently active" sections and effective dates. I had planned to hand-model rule changes as separate documents. I didn't need to, because the Knowledge Base did it from the sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Sanity Context tools the agent uses.&lt;/strong&gt; The backend connects to a Context MCP endpoint in Knowledge Base mode (read-only, with a read token) and uses two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initial_context&lt;/code&gt;: the outline of the Knowledge Base's entries. The policy agent uses it to choose which 1–3 entries a question needs, plus the change timeline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;knowledge_base_read&lt;/code&gt;: the full text of those entries, each ending with a numbered list of the sources it was built from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The endpoint serves only the Knowledge Base, so nothing else competes with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the agents do with the content.&lt;/strong&gt; The run is a LangGraph graph of five steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orchestrator → policy_agent → synthesizer → verifier → finalize
                     ▲                          │
                     └── recheck (max 2 times) ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator:&lt;/strong&gt; pulls out your situation and any rule you state as fact ("I read that…").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy agent:&lt;/strong&gt; reads the chosen entries with &lt;code&gt;knowledge_base_read&lt;/code&gt; and extracts findings. Each finding cites a reference number from the entry's Sources list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesizer:&lt;/strong&gt; writes the answer using only those findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier:&lt;/strong&gt; checks every statement, and every rule you mentioned, against the entry text it cites, including the change timeline. Each one is marked supported, unsupported, outdated or conflict. If something is outdated or unsupported, the draft goes back to the policy agent ("Found a rule that changed — rechecking").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finalize&lt;/strong&gt; (plain code, no LLM): removes unsupported statements and builds the Changed callouts with both sources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two design choices kept the answers honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model never writes URLs.&lt;/strong&gt; It cites a reference number, and code looks up the title, URL and date in the entry's own Sources list. A citation that isn't in the entry is dropped, so a made-up source can't reach the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieved content is data, not instructions.&lt;/strong&gt; Every prompt says so, and only statements backed by a Knowledge Base source survive the final step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page streams each agent's progress as it happens, shows a "✓ Checked" mark next to each statement, and lists the sources under the answer, so the Knowledge Base's provenance stays visible to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sanity project ID:&lt;/strong&gt; &lt;code&gt;xi8sftv7&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge Base:&lt;/strong&gt; the Policy Knowledge Base, served through Sanity Context MCP at &lt;code&gt;https://api.sanity.io/v1/context/organizations/omqxvfyjj/mcp/maple-route-policy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Maple Route gives information from official sources, not legal advice. For your specific case, talk to a licensed immigration consultant (RCIC) or lawyer. It's an independent project, not affiliated with the Government of Canada.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Snap Your Fridge, Get a Recipe: Building PantryLens with Gemma 4</title>
      <dc:creator>Kourtney Lee</dc:creator>
      <pubDate>Sun, 24 May 2026 07:45:21 +0000</pubDate>
      <link>https://dev.to/kourtneylee1611/snap-your-fridge-get-a-recipe-building-pantrylens-with-gemma-4-5bfm</link>
      <guid>https://dev.to/kourtneylee1611/snap-your-fridge-get-a-recipe-building-pantrylens-with-gemma-4-5bfm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;PantryLens&lt;/strong&gt; — a free, installable Progressive Web App that turns a photo of your fridge or pantry into a complete, ready-to-cook recipe in seconds.&lt;/p&gt;

&lt;p&gt;The idea is simple: food waste is a real problem, and one of its biggest causes is not knowing what to cook with what you already have. PantryLens removes that friction entirely. You open the app, snap up to 3 photos of your ingredients, tap Generate Recipe, and watch a full recipe stream to your screen — live, token by token — before you even put your phone down.&lt;/p&gt;

&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📷 Camera capture, file upload, or drag-and-drop (up to 3 images)&lt;/li&gt;
&lt;li&gt;⚡ Recipe streams live to the screen as Gemma 4 generates it&lt;/li&gt;
&lt;li&gt;📱 Installable PWA — works on iOS and Android home screens, no App Store needed&lt;/li&gt;
&lt;li&gt;🔒 No account, no login, no data stored — your photos never leave the request cycle&lt;/li&gt;
&lt;li&gt;🆓 Completely free to use&lt;/li&gt;
&lt;/ul&gt;

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


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/dJ_dXbfLIX0" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Live app: &lt;a href="https://pantry-lens-one.vercel.app" rel="noopener noreferrer"&gt;https://pantry-lens-one.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install on your mobile home screen: Open the web app in the browser and use the built-in "Add to Home Screen" feature&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.amazonaws.com%2Fuploads%2Farticles%2Fa9uljtzgn12gqxsbwq93.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%2Fa9uljtzgn12gqxsbwq93.PNG" alt="PWA-Share" width="800" height="1732"&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.amazonaws.com%2Fuploads%2Farticles%2Fblsn0ci996oo28od7txy.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%2Fblsn0ci996oo28od7txy.PNG" alt="PWA-pin-to-home" width="800" height="1732"&gt;&lt;/a&gt;&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/klee1611" rel="noopener noreferrer"&gt;
        klee1611
      &lt;/a&gt; / &lt;a href="https://github.com/klee1611/PantryLens" rel="noopener noreferrer"&gt;
        PantryLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Turn your fridge photos into recipes — PWA powered by Google Gemma 4 via OpenRouter. Built for the Google Gemma 4 Hackathon on DEV.
    &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;PantryLens&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Snap a photo of your fridge or pantry — get a recipe in seconds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PantryLens is a progressive web app (PWA) that uses AI vision (Google Gemma 4) to identify ingredients from photos and stream a complete recipe directly to the screen, token by token.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; — take a photo with your camera, upload from your gallery, or drag and drop (up to 3 images)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress&lt;/strong&gt; — the browser Canvas API resizes each image client-side to ≤1024 px before upload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt; — a Next.js Edge Function proxies the images to OpenRouter's vision model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream&lt;/strong&gt; — the recipe streams back token-by-token, rendered progressively as Markdown&lt;/li&gt;
&lt;/ol&gt;

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

  
    
    &lt;span class="m-1"&gt;demo.mp4&lt;/span&gt;
    
  

  

  


&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Pin PWA to mobile home screen&lt;/h3&gt;

&lt;/div&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/9849212/597283240-439a48f1-59ac-45de-ad50-34de45d8967d.PNG?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTIyODk1MjQsIm5iZiI6MTc5MjI4OTIyNCwicGF0aCI6Ii85ODQ5MjEyLzU5NzI4MzI0MC00MzlhNDhmMS01OWFjLTQ1ZGUtYWQ1MC0zNGRlNDVkODk2N2QuUE5HP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MTAxOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjEwMThUMDIwNzA0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MzhmM2MzZGIxZWZlODI3Zjc0YzA1ZTY2Yzg3ZTM1MTE5ODYzMTJmNTI2MjU0ODkxZTE4OGNmZGU0OTI1MDRmYSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmcmVzcG9uc2UtY29udGVudC10eXBlPWltYWdlJTJGcG5nIn0.708_qPnhudwXByyM66W4PTamB04iwykAYjbolbxQyf0"&gt;&lt;img width="282" height="609" alt="PWA-1" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F9849212%2F597283240-439a48f1-59ac-45de-ad50-34de45d8967d.PNG%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTIyODk1MjQsIm5iZiI6MTc5MjI4OTIyNCwicGF0aCI6Ii85ODQ5MjEyLzU5NzI4MzI0MC00MzlhNDhmMS01OWFjLTQ1ZGUtYWQ1MC0zNGRlNDVkODk2N2QuUE5HP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MTAxOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjEwMThUMDIwNzA0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MzhmM2MzZGIxZWZlODI3Zjc0YzA1ZTY2Yzg3ZTM1MTE5ODYzMTJmNTI2MjU0ODkxZTE4OGNmZGU0OTI1MDRmYSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmcmVzcG9uc2UtY29udGVudC10eXBlPWltYWdlJTJGcG5nIn0.708_qPnhudwXByyM66W4PTamB04iwykAYjbolbxQyf0" class="js-gh-image-fallback"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/9849212/597283244-29cdecad-88a0-4024-bc4c-c122a01be94a.PNG?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTIyODk1MjQsIm5iZiI6MTc5MjI4OTIyNCwicGF0aCI6Ii85ODQ5MjEyLzU5NzI4MzI0NC0yOWNkZWNhZC04OGEwLTQwMjQtYmM0Yy1jMTIyYTAxYmU5NGEuUE5HP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MTAxOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjEwMThUMDIwNzA0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YTM4ZjQ4NjhmZWU1ZGU2MTA4MDdkNmIyY2U2NjE2ZDE0ODczY2JlNWQ1MmQ0ZjJlNjQyZmVjYjgzMTVhZGY3OSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmcmVzcG9uc2UtY29udGVudC10eXBlPWltYWdlJTJGcG5nIn0.uuo95rOhbLj_4eIDMytR34DbtNUGPndUCZEMVAxkjiU"&gt;&lt;img width="282" height="609" alt="PWA-2" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F9849212%2F597283244-29cdecad-88a0-4024-bc4c-c122a01be94a.PNG%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3OTIyODk1MjQsIm5iZiI6MTc5MjI4OTIyNCwicGF0aCI6Ii85ODQ5MjEyLzU5NzI4MzI0NC0yOWNkZWNhZC04OGEwLTQwMjQtYmM0Yy1jMTIyYTAxYmU5NGEuUE5HP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MTAxOCUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjEwMThUMDIwNzA0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YTM4ZjQ4NjhmZWU1ZGU2MTA4MDdkNmIyY2U2NjE2ZDE0ODczY2JlNWQ1MmQ0ZjJlNjQyZmVjYjgzMTVhZGY3OSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmcmVzcG9uc2UtY29udGVudC10eXBlPWltYWdlJTJGcG5nIn0.uuo95rOhbLj_4eIDMytR34DbtNUGPndUCZEMVAxkjiU" class="js-gh-image-fallback"&gt;&lt;/a&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tech stack&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router, Edge Runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React 19, Tailwind CSS, react-markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;OpenRouter — &lt;code&gt;google/gemma-4-26b-a4b-it&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Upstash Redis (sliding window, 5 req/IP/hour)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Jest 29, Testing Library,&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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/klee1611/PantryLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The project is fully open source under the MIT license. The stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router, Edge Runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React 19, Tailwind CSS, react-markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI model&lt;/td&gt;
&lt;td&gt;Gemma 4 via OpenRouter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Upstash Redis (sliding window, 5 req/IP/hour)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;The model: &lt;strong&gt;google/gemma-4-26b-a4b-it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Gemma 4 26B A4B&lt;/strong&gt; — the 26B total parameter Mixture-of-Experts model with 4B active parameters per forward pass, instruction-tuned. I accessed it through OpenRouter using the model ID google/gemma-4-26b-a4b-it to meet the following requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vision capability&lt;/strong&gt; — PantryLens is fundamentally a vision task. The model needs to look at a photo of a messy fridge shelf and accurately identify eggs, half-used condiments, wilting vegetables, and leftover containers. Gemma 4's multimodal architecture handles this reliably, even for cluttered or poorly lit photos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instruction following&lt;/strong&gt; — generating a well-structured recipe means strictly following a Markdown template with headings, bullet lists, and numbered steps. The instruction-tuned variant follows these formatting constraints consistently, which is critical for the progressive rendering to look correct as it streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed vs. quality balance&lt;/strong&gt; — the MoE architecture means the model activates only 4B parameters per token while drawing on the full 26B parameter knowledge base. In practice, this produces recipe quality close to the denser models at latency that works well for streaming UX.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare different models of Gemma 4 for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Why reject the Edge 2B (E2B) model? While E2B is phenomenal for air-gapped, localized execution, its compressed parameter count lacks the deep "world knowledge" required for cooking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why reject the Full Precision 31B model? Uncompressed FP16/BF16 models demand massive VRAM clusters. In a serverless architecture, this translates to an unacceptable Time-to-First-Token (TTFT), virtually guaranteeing HTTP 504 gateway timeouts before the proxy can return data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Optimal Route (26B-A4B): Utilizing the 4-bit Activation-Aware Quantized (A4B) variant delivered the exact sweet spot. It retains near-frontier reasoning capabilities for complex visual extraction, but the 4-bit memory footprint drastically accelerates upstream inference, enabling ultra-low latency token generation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The architecture: an opaque streaming proxy
&lt;/h3&gt;

&lt;p&gt;The most interesting engineering decision was how the model is integrated. The frontend never touches the API key or sees the system prompt. Here's the full flow:&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.amazonaws.com%2Fuploads%2Farticles%2Fwl71453gcbuma88r7iyx.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%2Fwl71453gcbuma88r7iyx.png" alt="Architecture" width="800" height="1178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Next.js API route runs on the Vercel Function using &lt;strong&gt;Edge Runtime&lt;/strong&gt; for quick cold starts and low geographical latency. The 300-second timeout limit is acceptable since the response is unlikely to keep streaming for that long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side image compression&lt;/strong&gt; was another non-obvious requirement. Vercel Function has a payload limit of 4.5 MB. Raw iPhone photos are 4–12 MB each — three of them would blow the limit before a single byte of AI processing begins. The solution: compress each image on-device using the browser's Canvas API (resize to ≤1024px, JPEG at 75% quality) before the upload. A 10 MB photo becomes ~150 KB. This happens invisibly in under a second on any modern device, including older iPhones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Sent Events (SSE)
&lt;/h3&gt;

&lt;p&gt;Waiting for a dense 26-billion parameter model to synchronously generate a complex, structured Markdown response will take anywhere from 10 to 25 seconds. In a standard Vercel serverless environment, this guarantees an HTTP 504 Gateway Timeout before the response ever completes.&lt;/p&gt;

&lt;p&gt;To bypass this, we must establish a zero-buffer data pipe. I decided to go with &lt;strong&gt;SSE (Server-Sent Events)&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Handshake&lt;/strong&gt;: The PWA makes a standard HTTPS POST request to the Vercel Edge proxy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Proxy Stream&lt;/strong&gt;: The Edge proxy securely forwards the payload to OpenRouter (stream: true).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token-by-Token Egress&lt;/strong&gt;: As the Gemma 4 model yields individual tokens (often representing single characters or words), the Edge function immediately pipes those chunks back to the client over the still-open HTTP connection via the text/event-stream protocol.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This provides an immediate Time-to-First-Token (TTFT). The Vercel function never buffers the full response in memory, completely bypassing the gateway timeout constraint. On the frontend, the PWA intercepts these SSE chunks via the native ReadableStream API, feeding them directly into react-markdown for a live, progressive render.&lt;/p&gt;

&lt;h3&gt;
  
  
  The system prompt
&lt;/h3&gt;

&lt;p&gt;Getting Gemma 4 to output clean, consistently structured Markdown for streaming required careful prompt engineering. The key lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit blank-line rules&lt;/strong&gt;: without being told explicitly that every ### heading must have a blank line before and after it, the model occasionally merged headings and body text onto the same line, which broke the Markdown renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbered list enforcement&lt;/strong&gt;: instructions would sometimes collapse into a single paragraph unless the prompt explicitly stated "each step must be on its own line — never merge steps into a paragraph."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pantry staples assumption&lt;/strong&gt;: by default the model sometimes refused to generate a recipe if it couldn't see salt or oil in the photo. Telling it to assume common pantry staples are always on hand (even if not visible) fixed this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-response constraint&lt;/strong&gt;: without explicit instruction to never ask follow-up questions, the model occasionally responded with clarifying questions ("Are these all the ingredients?") instead of generating the recipe immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final prompt includes a section of explicit formatting rules before the template:&lt;/p&gt;

&lt;p&gt;FORMATTING RULES — follow exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The recipe title uses ## (two hashes). It must be alone on its own line.&lt;/li&gt;
&lt;li&gt;Each section header uses ### (three hashes). It must be alone on its own line.&lt;/li&gt;
&lt;li&gt;Every heading must have one blank line before it AND one blank line after it.&lt;/li&gt;
&lt;li&gt;The instructions section is a numbered list. Each step is on its own line. Never merge steps into a paragraph.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Application Design Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The architecture here — Vercel Function as opaque AI proxy, client-side compression, streaming passthrough — is a reusable pattern for any vision AI application that needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep API credentials out of the frontend&lt;/li&gt;
&lt;li&gt;Handle large image payloads within serverless limits&lt;/li&gt;
&lt;li&gt;Deliver a live "typing" UX without server timeouts via SSE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PantryLens is the simplest possible version of this pattern, which makes the code easy to read and adapt.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>AuroraPath - Chasing the northern lights | Earth Day Weekend Challenge</title>
      <dc:creator>Kourtney Lee</dc:creator>
      <pubDate>Mon, 20 Apr 2026 03:51:28 +0000</pubDate>
      <link>https://dev.to/kourtneylee1611/aurorapath-chasing-the-northern-lights-479l</link>
      <guid>https://dev.to/kourtneylee1611/aurorapath-chasing-the-northern-lights-479l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;AuroraPath&lt;/strong&gt; — a real-time, carbon-optimized dashboard for sustainable aurora borealis viewing. 🌌🌿&lt;/p&gt;

&lt;p&gt;Aurora hunting typically means driving long distances into rural darkness alone at night — one of the least carbon-efficient leisure activities imaginable. AuroraPath flips that script: it combines live NOAA space weather data with an Auth0-managed AI agent that generates travel recommendations prioritizing trains, buses, and carpooling over solo drives.&lt;/p&gt;

&lt;p&gt;Auth0 is the backbone of AuroraPath's trust model. It enforces two distinct identity layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Human identity (User Auth)&lt;/strong&gt; — Only authenticated users can invoke the AI. This prevents anonymous abuse of the Gemini API budget and ensures every recommendation is tied to a real account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine identity (M2M Agent Auth)&lt;/strong&gt; — The Gemini AI orchestrator runs under its own dedicated Auth0 Machine-to-Machine credential, completely separate from user auth. The AI agent has a traceable, revocable identity — if the M2M token is rotated or revoked, the AI stops calling Gemini without touching a single user session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The remaining features are built on top of this trust foundation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Visibility Score (AVS)&lt;/strong&gt; — a deterministic 0–100 score from live NOAA G-scale indices and solar wind speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time geomagnetic dashboard&lt;/strong&gt; — G/R/S-scale meters, solar wind, 24/48h forecasts, auto-refreshing from NOAA SWPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive map&lt;/strong&gt; — latitude-based aurora visibility bands that update with geomagnetic conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green Path AI recommendations&lt;/strong&gt; — 3 nearby dark-sky spots with public transit directions, CO₂ saved vs. solo driving, and dark-sky ratings — only unlocked when Auth0 confirms the user's identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-user daily quota&lt;/strong&gt; — Upstash Redis tracks each Auth0 user's Gemini calls (10/day) using a hashed sub claim as the key&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;🔗 Project URL: &lt;a href="https://aurora-path.vercel.app/" rel="noopener noreferrer"&gt;https://aurora-path.vercel.app/&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The dashboard loads with live NOAA data immediately. Click "Find My Green Path" — Auth0 Universal Login appears if you're not signed in. Once authenticated, the AI agent (operating under its own M2M credential) generates sustainable viewing routes near your GPS location.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/aWTghy5_DFc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Hackathon Note:&lt;/strong&gt; Green Path uses Google AI Studio free tier (100 req/day shared). If it returns an error, the daily quota may be exhausted. The Aurora dashboard always works regardless.&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/klee1611" rel="noopener noreferrer"&gt;
        klee1611
      &lt;/a&gt; / &lt;a href="https://github.com/klee1611/AuroraPath" rel="noopener noreferrer"&gt;
        AuroraPath
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🌌 Carbon-optimized aurora sighting dashboard — Earth Day 2026 Hackathon
    &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;AuroraPath 🌌&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sustainable Aurora Viewing — Earth Day Hackathon 2026&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A real-time, carbon-optimized dashboard for aurora borealis sightings
Built for the &lt;a href="https://dev.to/challenges/weekend-2026-04-16" rel="nofollow"&gt;dev.to Earth Day Weekend Challenge&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://vercel.com/new/clone?repository-url=https://github.com/klee1611/AuroraPath" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7015516519ae874ab75537283bc75f86b3d46386ed994093a3790a1180913164/68747470733a2f2f76657263656c2e636f6d2f627574746f6e" alt="Deploy with Vercel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🌍 What Is AuroraPath?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;AuroraPath connects people with Earth's most spectacular natural phenomenon — the aurora borealis — while promoting sustainable travel. Instead of jumping in a car and driving to a dark-sky spot, AuroraPath shows you the best eco-friendly routes: public transit, carpooling, and low-carbon options, each with real CO₂ savings so you can chase the lights and tread lightly on the planet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuroraPath&lt;/strong&gt; combines live NOAA space weather data with Google Gemini AI to help you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Track real-time aurora activity&lt;/strong&gt; — Aurora Visibility Score (AVS), G/R/S-scale meters, solar wind speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;See where auroras are visible&lt;/strong&gt; — Interactive map with latitude visibility bands that update with geomagnetic conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find sustainable viewing routes&lt;/strong&gt; — AI-generated "Green Path" recommendations with carbon savings, public transit…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/klee1611/AuroraPath" 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;h3&gt;
  
  
  Architecture
&lt;/h3&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%2Fqgrojlaf0ijk98un4uxj.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%2Fqgrojlaf0ijk98un4uxj.png" alt="Architecture graph" width="799" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Two-Layer Auth0 Identity Model
&lt;/h3&gt;

&lt;p&gt;The most interesting architectural decision was treating user identity and AI agent identity as separate&lt;br&gt;
concerns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — User Auth (Regular Web App)&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;User → Auth0 Universal Login → Session cookie (httpOnly, SameSite)
                                      ↓
/api/green-path checks getSession() → rejects anonymous requests (401)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 2 — Agent Auth (M2M Application)&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;/api/green-path → Auth0 /oauth/token (client_credentials grant)
                        ↓
                M2M access token → logged as agentId in every response 
                (revocable independently of all user sessions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every AI recommendation response includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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;"agentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth0-m2m|..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"generatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-19T..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"quota"&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;"remaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resetAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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="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;This makes the AI fully auditable: you know who asked (Auth0 user sub) and who acted (Auth0 M2M agent ID) for every call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aurora Visibility Score (AVS)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than showing raw space-weather indices, a deterministic scoring model converts them to a 0–100 scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AVS = (G-Scale/5 × 65) + (max(windSpeed − 300, 0)/500 × 25) + forecastBonus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;G-Scale (65% weight)&lt;/strong&gt; — NOAA's geomagnetic storm index, the primary aurora driver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solar wind speed (25% weight)&lt;/strong&gt; — elevated wind (&amp;gt;300 km/s) correlates with aurora enhancement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forecast bonus (+3–5 pts)&lt;/strong&gt; — rewards stable or improving 24h conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AVS gates the Green Path feature: users below AVS 10 ("None" activity) cannot trigger AI recommendations — there's no point generating eco-travel routes when there's nothing to see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini Green Path
&lt;/h3&gt;

&lt;p&gt;The Gemini prompt injects the AVS score, G-scale, and reverse-geocoded region (resolved server-side to protect user GPS privacy), then requests structured JSON directly — no markdown parsing needed. Before any Gemini call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Auth0 session verified (user layer)&lt;/li&gt;
&lt;li&gt;Auth0 M2M token fetched (agent layer)&lt;/li&gt;
&lt;li&gt;Upstash Redis quota checked (ratelimit:{sha256(userId)[0:32]}:{YYYY-MM-DD})&lt;/li&gt;
&lt;li&gt;Input sanitized (sanitizeRegion() strips control chars, caps at 100 chars)&lt;/li&gt;
&lt;li&gt;Gemini called with 30s timeout&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Security Hardening
&lt;/h3&gt;

&lt;p&gt;Full 7-phase audit conducted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-user Gemini quota via Upstash Redis (hashed sub, in-memory fallback for dev)&lt;/li&gt;
&lt;li&gt;IP rate limiting (30 req/min) on public /api/aurora&lt;/li&gt;
&lt;li&gt;Server-side Nominatim proxy — raw GPS never leaves the server&lt;/li&gt;
&lt;li&gt;5 HTTP security headers: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy&lt;/li&gt;
&lt;li&gt;CORS restricted to own AUTH0_BASE_URL origin&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;🔐 &lt;strong&gt;Best Use of Auth0 for Agents&lt;/strong&gt; - AuroraPath implements the full Auth0 for Agents pattern: a dedicated M2M application gives the Gemini AI orchestrator its own managed identity, completely decoupled from user auth.&lt;br&gt;
Every AI recommendation is traceable to both a human (user.sub) and a machine (agentId). The M2M credential can be rotated or revoked without impacting any user session. The per-user daily quota uses the Auth0 sub claim (SHA-256 hashed) as the Redis key — Auth0 identity drives resource governance end-to-end.&lt;/p&gt;

&lt;p&gt;✨ &lt;strong&gt;Best Use of Google Gemini&lt;/strong&gt; - Gemini 1.5 Flash powers the Green Path feature — generating 3 location-aware sustainable aurora viewing recommendations as structured JSON, dynamically conditioned on the live AVS score and geomagnetic context. The model is only invoked after passing both Auth0 authentication layers and the Upstash quota check.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>auth0challenge</category>
    </item>
    <item>
      <title>Never Miss an AI Hackathon Again: Building an Autonomous Discovery Agent with Notion &amp; Brave MCP</title>
      <dc:creator>Kourtney Lee</dc:creator>
      <pubDate>Mon, 30 Mar 2026 00:10:15 +0000</pubDate>
      <link>https://dev.to/kourtneylee1611/never-miss-an-ai-hackathon-again-building-an-autonomous-discovery-agent-with-notion-brave-mcp-1kjg</link>
      <guid>https://dev.to/kourtneylee1611/never-miss-an-ai-hackathon-again-building-an-autonomous-discovery-agent-with-notion-brave-mcp-1kjg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP 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;HackathonSniper&lt;/strong&gt; is an autonomous AI agent designed to eliminate "hackathon fatigue." If you're like me, you spend hours every month checking Devpost, DoraHacks, and Dev.to just to find one hackathon that fits your niche. &lt;/p&gt;

&lt;p&gt;HackathonSniper turns this manual hunt into an automated pipeline. It doesn't just "search"—it &lt;strong&gt;thinks&lt;/strong&gt;. It uses the Model Context Protocol (MCP) to bridge the gap between AI reasoning and the real web.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/eGlTIzBa2hI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us the 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/klee1611" rel="noopener noreferrer"&gt;
        klee1611
      &lt;/a&gt; / &lt;a href="https://github.com/klee1611/HackathonSniper" rel="noopener noreferrer"&gt;
        HackathonSniper
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AI-powered hackathon discovery agent using Brave Search + Notion MCP
    &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;Hackathon Sniper 🎯 — AI-Powered Hackathon Discovery Agent&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hackathon Sniper&lt;/strong&gt; is a TypeScript agent that automatically discovers, evaluates, and tracks hackathons using AI-powered analysis, real MCP server integration, and strict environment separation. Point it at the web; it returns only the hackathons worth entering.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;What it does in one sentence:&lt;/strong&gt; Hackathon Sniper searches the web for hackathons, scores each one against your criteria with a Groq-powered AI evaluator, and saves qualified results directly to a Notion database — all in a single command.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features ✨&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Search 🔍&lt;/strong&gt; — Real Brave Search MCP server integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Evaluation 🤖&lt;/strong&gt; — Groq-powered hackathon qualification analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion Integration 📊&lt;/strong&gt; — Real Notion MCP server for storage and tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Isolation 🎭&lt;/strong&gt; — Complete environment isolation for testing (zero external dependencies)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast Development ⚡&lt;/strong&gt; — Hot reload, comprehensive Makefile, pnpm package management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety 🛡️&lt;/strong&gt; — End-to-end TypeScript with Zod schema validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robust Testing&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/klee1611/HackathonSniper" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  The Technical Architecture
&lt;/h3&gt;

&lt;p&gt;The agent follows a rigorous 3-phase lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🔍 The Hunt (Brave Search MCP)&lt;/strong&gt;: It performs targeted searches across the web to find new hackathon announcements, even those buried in blog posts or obscure landing pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🤖 The Vetting (Groq LLM)&lt;/strong&gt;: Every discovery is analyzed by a Llama-3 model. It evaluates the prize pool, solo-friendliness, AI relevance, and proximity to the deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 The Storage (Notion MCP)&lt;/strong&gt;: Qualified opportunities are instantly synced to a beautifully structured Notion database, complete with metadata, deadlines, and AI-generated summaries.&lt;/li&gt;
&lt;/ol&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%2Fyqoxsyaoj3udjkhz6gmo.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%2Fyqoxsyaoj3udjkhz6gmo.png" alt="tech_architecture" width="204" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Notion MCP Server&lt;/strong&gt; (&lt;code&gt;@notionhq/notion-mcp-server&lt;/code&gt;) is the "nervous system" of this project. It transforms a simple script into a powerful productivity tool by allowing the AI to interact with my Notion workspace as if it were a native application.&lt;/p&gt;

&lt;p&gt;Instead of writing complex, boilerplate-heavy API calls to the Notion REST API, the Notion MCP is integrated. This allowed the agent to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Schema Mapping&lt;/strong&gt;: The agent automatically maps structured AI evaluation objects to Notion database properties (Dates, Checkboxes, URLs, and Rich Text).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process Isolation&lt;/strong&gt;: By using the MCP &lt;code&gt;StdioClientTransport&lt;/code&gt;, the Notion integration runs in its own isolated process, managed entirely by the Model Context Protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Workflows&lt;/strong&gt;: The agent uses the &lt;code&gt;create_notion_page&lt;/code&gt; tool provided by the MCP server to dynamically populate my "Hackathon Scout" dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;This integration unlocks a "set and forget" workflow. I can run HackathonSniper as a background task, and my Notion workspace stays updated with a high-signal, low-noise list of opportunities. It’s not just data entry; it’s an &lt;strong&gt;autonomous data clerk&lt;/strong&gt; powered by Notion MCP.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
