<?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: Shuvo</title>
    <description>The latest articles on DEV Community by Shuvo (@0shuvo0).</description>
    <link>https://dev.to/0shuvo0</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%2F728097%2F22055fc3-95c3-4684-8344-ac7727d2dc3a.png</url>
      <title>DEV Community: Shuvo</title>
      <link>https://dev.to/0shuvo0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0shuvo0"/>
    <language>en</language>
    <item>
      <title>Why I made an interactive Git learning platform</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Fri, 13 Nov 2026 04:37:39 +0000</pubDate>
      <link>https://dev.to/0shuvo0/why-i-made-an-interactive-git-learning-platform-16n8</link>
      <guid>https://dev.to/0shuvo0/why-i-made-an-interactive-git-learning-platform-16n8</guid>
      <description>&lt;p&gt;You've watched the Git tutorial.&lt;/p&gt;

&lt;p&gt;You nodded along. Everything made sense.&lt;/p&gt;

&lt;p&gt;Then you hit your first merge conflict at work and froze. 🥶&lt;/p&gt;

&lt;p&gt;I've been there. That's exactly why I built &lt;strong&gt;&lt;a href="https://gitmission.com" rel="noopener noreferrer"&gt;GitMission&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://gitmission.com" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2bp8j65zvozf67w60jw.png" alt="GitMission Hero Section Preview" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tutorials give you a false sense of progress
&lt;/h2&gt;

&lt;p&gt;Watching someone run &lt;code&gt;git rebase&lt;/code&gt; feels like learning.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;Here's what usually happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You follow along while the video is playing&lt;/li&gt;
&lt;li&gt;You close the tab&lt;/li&gt;
&lt;li&gt;A week later, you can't remember the commands
Your brain learned to &lt;strong&gt;recognize&lt;/strong&gt; Git. It never learned to &lt;strong&gt;use&lt;/strong&gt; it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are two very different skills.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitmission.com" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fug4wa5sur5tgbaf9rmwl.png" alt="Start Your First Mission" width="219" height="47"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Git is learned with your fingers, not your eyes
&lt;/h2&gt;

&lt;p&gt;Think about the last thing you got good at.&lt;/p&gt;

&lt;p&gt;Did you get good by watching, or by doing, failing, and trying again?&lt;/p&gt;

&lt;p&gt;Git works the same way. You need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type the commands&lt;/li&gt;
&lt;li&gt;Break things on purpose&lt;/li&gt;
&lt;li&gt;Fix what you broke
That's the loop that makes it stick.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  &lt;a href="https://www.gitmission.com/test-your-git-skills" rel="noopener noreferrer"&gt;Take the Git skills test&lt;/a&gt;
&lt;/h3&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  So I built a place where you can break things safely
&lt;/h2&gt;

&lt;p&gt;GitMission is an interactive Git platform that runs entirely in your browser.&lt;/p&gt;

&lt;p&gt;No install. No setup. No fear of wrecking a real repo.&lt;/p&gt;

&lt;p&gt;Here's what you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🖥️ &lt;strong&gt;A real in-browser Git terminal&lt;/strong&gt; with instant feedback on every command&lt;/li&gt;
&lt;li&gt;✏️ &lt;strong&gt;A built-in code editor&lt;/strong&gt;, so you practice the full flow from edit to commit&lt;/li&gt;
&lt;li&gt;👥 &lt;strong&gt;A virtual engineering team&lt;/strong&gt; that assigns tasks, sets context, and puts you in realistic situations&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Real-world scenarios&lt;/strong&gt; like merge conflicts, messy rebases, and recovering lost commits
Here's the kind of thing you'll see in a mission:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Marcus &lt;span class="o"&gt;(&lt;/span&gt;Senior Dev&lt;span class="o"&gt;)&lt;/span&gt;: Before I pick up the login work,
can you print the commits made so far?

$ git log --oneline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No multiple choice. You type the command. The platform checks it.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But I just use AI to write my code..."
&lt;/h2&gt;

&lt;p&gt;Let's talk about vibe coders. 👀&lt;/p&gt;

&lt;p&gt;AI can write your features in minutes. That's amazing.&lt;/p&gt;

&lt;p&gt;But AI also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rewrites files you didn't ask it to touch&lt;/li&gt;
&lt;li&gt;Introduces bugs you won't notice until later&lt;/li&gt;
&lt;li&gt;Makes big changes in a single shot
Without Git, you have &lt;strong&gt;no undo button&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Git, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Commit a working state before every AI experiment&lt;/li&gt;
&lt;li&gt;✅ See exactly what the AI changed with &lt;code&gt;git diff&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Roll back with &lt;code&gt;git revert&lt;/code&gt; or &lt;code&gt;git reset&lt;/code&gt; when things go sideways&lt;/li&gt;
&lt;li&gt;✅ Branch off and try risky ideas without touching &lt;code&gt;main&lt;/code&gt;
The faster you build, the more you need a safety net.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vibe coding without Git is driving fast with no brakes.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Git is not optional if you want a dev job
&lt;/h2&gt;

&lt;p&gt;Almost every software team uses Git.&lt;/p&gt;

&lt;p&gt;It's not a "nice to have" on a resume. It's assumed.&lt;/p&gt;

&lt;p&gt;In interviews, you'll often be asked about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Branching and merging&lt;/li&gt;
&lt;li&gt;Rebasing vs merging&lt;/li&gt;
&lt;li&gt;Resolving conflicts&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undoing mistakes&lt;br&gt;
And on day one of a real job, you'll be expected to:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a branch for your task&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open a pull request&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Respond to code review&lt;br&gt;
Knowing the definitions isn't enough.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need &lt;strong&gt;experience&lt;/strong&gt;, so your answers come from something you've actually done.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's inside GitMission
&lt;/h2&gt;

&lt;p&gt;8 missions. About 7 hours of hands-on practice. Zero fluff.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mission&lt;/th&gt;
&lt;th&gt;What you'll learn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Introduction to Git&lt;/td&gt;
&lt;td&gt;What Git is and how to set it up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Tracking Changes&lt;/td&gt;
&lt;td&gt;init, status, add, commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Working with History&lt;/td&gt;
&lt;td&gt;log, diff, .gitignore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Undoing Changes&lt;/td&gt;
&lt;td&gt;unstage, amend, reset, revert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Branching&lt;/td&gt;
&lt;td&gt;create, switch, merge, delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Advanced Git&lt;/td&gt;
&lt;td&gt;stash, rebase, cherry-pick, bisect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. GitHub Basics&lt;/td&gt;
&lt;td&gt;SSH keys, push, pull, clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Advanced GitHub &amp;amp; Collaboration&lt;/td&gt;
&lt;td&gt;forks, pull requests, code reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start at the beginning, or jump straight to the scary stuff like rebase and conflicts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who is it for?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌱 &lt;strong&gt;Complete beginners&lt;/strong&gt; who've never typed &lt;code&gt;git init&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Vibe coders&lt;/strong&gt; who want a safety net for AI-generated code&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;Job seekers&lt;/strong&gt; preparing for interviews and their first dev role&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - 😬 &lt;strong&gt;Devs who know &lt;code&gt;add&lt;/code&gt; and &lt;code&gt;commit&lt;/code&gt;&lt;/strong&gt; but panic at &lt;code&gt;rebase&lt;/code&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  First, how good are your Git skills right now?
&lt;/h2&gt;

&lt;p&gt;Before you start learning, find out where you stand.&lt;/p&gt;

&lt;p&gt;I made a free Git skills test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;30 questions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;20 seconds each&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 difficulty levels&lt;/strong&gt;, from beginner to advanced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No signup, no install&lt;/strong&gt;
You also get a topic-by-topic breakdown and explanations for every wrong answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think you'll ace it? 😏&lt;/p&gt;

&lt;h3&gt;
  
  
  👉 &lt;a href="https://www.gitmission.com/test-your-git-skills" rel="noopener noreferrer"&gt;Take the Git skills test&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Drop your score in the comments. I'm curious who gets 30/30. 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  Ready to start?
&lt;/h2&gt;

&lt;p&gt;Your first mission needs &lt;strong&gt;no login and no credit card&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Git fundamentals are free to try. The advanced missions are unlocked with Pro, which is a one-time purchase, not a subscription.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitmission.com" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fug4wa5sur5tgbaf9rmwl.png" alt="Start Your First Mission" width="219" height="47"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;You don't learn Git by watching someone else use it.&lt;/p&gt;

&lt;p&gt;You learn it by getting stuck, fixing it, and doing it again.&lt;/p&gt;

&lt;p&gt;That's the whole idea behind GitMission.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love your feedback. Tell me what worked, what didn't, and which Git command still scares you. 💬&lt;/p&gt;

&lt;p&gt;Happy committing!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit -m &lt;span class="s2"&gt;"start learning"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>git</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Junior Developer Comeback: Seniors will struggle to get a job?</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sat, 07 Nov 2026 12:52:41 +0000</pubDate>
      <link>https://dev.to/0shuvo0/the-junior-developer-comeback-seniors-will-struggle-to-get-a-job-1pc4</link>
      <guid>https://dev.to/0shuvo0/the-junior-developer-comeback-seniors-will-struggle-to-get-a-job-1pc4</guid>
      <description>&lt;p&gt;Right now, juniors are struggling.&lt;/p&gt;

&lt;p&gt;Whether you're trying to become a developer, a designer, or a data analyst, the entry-level door feels like it's closing. Seniors, on the other hand, are mostly getting hired or retained.&lt;/p&gt;

&lt;p&gt;I think that's about to reverse. Not this year, but within the next five to ten years, I believe &lt;strong&gt;juniors will do very well, and seniors will be the ones fighting for jobs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Learn Git by completing missions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F369ehfxm9dnl7m4p7jrp.png" alt="GitMission hero" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why juniors are struggling right now
&lt;/h2&gt;

&lt;p&gt;The current state of AI is good, but it isn't &lt;em&gt;senior-level&lt;/em&gt; good.&lt;/p&gt;

&lt;p&gt;It can write code, generate designs, and crunch data. But it still needs someone experienced to guide it, catch its mistakes, and make the calls that actually matter. That's exactly what a senior does.&lt;/p&gt;

&lt;p&gt;So companies are doing the logical thing for today's reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the seniors&lt;/strong&gt;, because they can steer AI and fix what it gets wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip the juniors&lt;/strong&gt;, because AI already covers a lot of what juniors used to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the world we're in right now, and it explains why the junior market feels so rough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens in 5 to 10 years?
&lt;/h2&gt;

&lt;p&gt;Now fast forward.&lt;/p&gt;

&lt;p&gt;By then, AI will probably be senior-level good. Not "impressive for a tool," but genuinely capable of senior-level work.&lt;/p&gt;

&lt;p&gt;Here's the part that matters: &lt;strong&gt;someone still has to operate it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with extremely capable AI, people will need &lt;em&gt;some&lt;/em&gt; technical skill to use it well. Not a lot, but some. You need enough understanding to describe what you want, to judge whether the output is right, and to know when something is off.&lt;/p&gt;

&lt;p&gt;And not everyone will have that.&lt;/p&gt;

&lt;p&gt;Most people won't have spent time learning graphic design, web development, or data analysis. The people who &lt;em&gt;did&lt;/em&gt; will have a real edge, because they understand the fundamentals well enough to operate the AI properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incentive to hire seniors disappears
&lt;/h2&gt;

&lt;p&gt;This is where the economics flip.&lt;/p&gt;

&lt;p&gt;Picture two candidates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Senior&lt;/th&gt;
&lt;th&gt;Junior (with some technical skill)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output with senior-level AI&lt;/td&gt;
&lt;td&gt;Great&lt;/td&gt;
&lt;td&gt;Just as great&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salary expectation&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Much lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If AI closes the gap in raw capability, then the senior's main advantage, years of hard-won technical expertise, matters a lot less. The junior with a bit of technical knowledge gets the same job done just as well.&lt;/p&gt;

&lt;p&gt;The senior costs more and delivers about the same result.&lt;/p&gt;

&lt;p&gt;From a business perspective, the decision becomes easy. Why pay more for a result you can get for less? The incentive to hire a senior drops, and hiring a junior with some technical knowledge starts to make far more sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I could be wrong
&lt;/h2&gt;

&lt;p&gt;I don't want to pretend this is a certainty. A few things could change the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seniors might shift their value.&lt;/strong&gt; If the gap in technical execution disappears, seniors could compete on things AI doesn't replace, like judgment, product sense, communication, and owning the consequences when something breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"A little technical skill" might not be enough.&lt;/strong&gt; If AI makes mistakes that are subtle or expensive, companies may still pay for deep expertise as insurance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timelines are uncertain.&lt;/strong&gt; Five to ten years is a guess, and AI progress has surprised people in both directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline problem.&lt;/strong&gt; If nobody hires juniors for years, fewer people will build the technical foundation that this whole argument depends on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even so, I think the core pressure is real: when capability gets cheap, the premium for experience shrinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're a junior:&lt;/strong&gt; Don't give up on learning the fundamentals. The market is hard right now, but the technical foundation you build is the thing that could set you apart later. Learn the craft, and learn to work &lt;em&gt;with&lt;/em&gt; AI, not against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a senior:&lt;/strong&gt; Don't rely on years of experience alone. Lean into what's hardest to automate, such as decision-making, system thinking, and being the person who can be trusted with outcomes, not just output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Today's market rewards seniors because AI isn't senior-level yet. Tomorrow's market might reward cheaper people who can operate a senior-level AI.&lt;/p&gt;

&lt;p&gt;The people who win won't necessarily be the most experienced. They'll be the ones who have &lt;em&gt;just enough&lt;/em&gt; technical skill to get the most out of the tools.&lt;/p&gt;

&lt;p&gt;I could be completely off, so I'd love to hear what you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you think juniors will bounce back once AI reaches senior-level ability, or will seniors always have the upper hand?&lt;/strong&gt; Let me know in the comments. 👇&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Séance Shell: A Local Ghost That Haunts Your Terminal</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sun, 01 Nov 2026 01:45:01 +0000</pubDate>
      <link>https://dev.to/0shuvo0/seance-shell-a-local-ghost-that-haunts-your-terminal-9a1</link>
      <guid>https://dev.to/0shuvo0/seance-shell-a-local-ghost-that-haunts-your-terminal-9a1</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;Séance Shell is a haunted terminal. A small ghost lives in your Bash or Zsh session, watches for the moments that matter, and murmurs a single eerie sentence about what just happened.&lt;/p&gt;

&lt;p&gt;It reacts to two kinds of events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Destructive-looking commands&lt;/strong&gt; like &lt;code&gt;git reset --hard&lt;/code&gt;, &lt;code&gt;git clean -fd&lt;/code&gt;, or &lt;code&gt;rm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed builds and tests&lt;/strong&gt; like &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt;, or &lt;code&gt;cargo build&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the command has finished, the ghost speaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD~1
ghost: the past has accepted your changes, and forgotten your future.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's the whole product. The ghost is a companion for reflection, not an assistant for execution. It never suggests commands, never runs anything, and never offers advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I read the theme:&lt;/strong&gt; the prompt suggested "a haunted terminal that possesses your shell and whispers back eerie commentary." I wanted that, but with restraint. Most AI tools are loud and eager to help. A good ghost lingers at the edge of the room, says one unsettling thing, and goes quiet. Séance Shell is a ghost that knows its place.&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/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/seance-shell" rel="noopener noreferrer"&gt;
        seance-shell
      &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;Séance Shell 👻&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A quiet, local-AI terminal companion that offers one short reflection after selected commands. The ghost is atmospheric, not an assistant: it gives no advice, runs no generated text, and never retries a command.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;$ git reset --hard HEAD~1
ghost: the past has accepted your changes, and forgotten your future
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Reflections are generated independently by Ollama on your machine with &lt;code&gt;gemma3:latest&lt;/code&gt;. Model output is validated before display. If the local model is unavailable, the reflection is skipped.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Notices selected destructive-looking commands and failed build/test commands.&lt;/li&gt;
&lt;li&gt;Reflects only after the shell has already completed a command; it never re-runs it.&lt;/li&gt;
&lt;li&gt;Sends Ollama a category, outcome, and—only for an explicit &lt;code&gt;seance reflect --context&lt;/code&gt; request—short sanitized context. Shell hooks do not send the command text to the model.&lt;/li&gt;
&lt;li&gt;Requires one sentence, at most 15 words and 180 characters; malformed output is discarded.&lt;/li&gt;
&lt;li&gt;Uses dim terminal text when…&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/0shuvo0/seance-shell" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;To run it locally you need Ollama and the &lt;code&gt;gemma3:latest&lt;/code&gt; model. The shell hook works without the model, so the ghost only goes silent if Ollama isn't running.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The open-source pieces are what make this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/strong&gt; runs inference locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 (&lt;code&gt;gemma3:latest&lt;/code&gt;)&lt;/strong&gt;, Google's open-weight model, is the default.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Bash/Zsh hook&lt;/strong&gt; detects events after the shell has finished a command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No cloud API, no hosted inference, no account. Three design decisions shaped the build.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The model never sees your command
&lt;/h3&gt;

&lt;p&gt;The shell doesn't send the raw command string to the model. It sends a structured description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;category:&lt;/strong&gt; destructive or failed-build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;status:&lt;/strong&gt; succeeded or failed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context:&lt;/strong&gt; optional, and sanitized first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model gets enough to set a mood but nothing it could be tricked by or leak. It is not a coding agent and can't be steered by arbitrary shell content.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The output is validated aggressively
&lt;/h3&gt;

&lt;p&gt;The model is asked for one sentence, at most 15 words and 180 characters. If the response is malformed, multi-line, or too long, it's discarded and the ghost stays silent. The text on screen is a constrained artifact, not whatever the model felt like saying.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A ghost, not an agent
&lt;/h3&gt;

&lt;p&gt;Séance Shell never retries commands, never executes anything, and never reinterprets what you did. It's a passive observer. That modesty is what makes it usable in a place where you're making decisions at speed.&lt;/p&gt;

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

&lt;p&gt;A terminal is about as personal as software gets. It holds your file paths, repo names, half-finished experiments, and the commands you'd rather not explain. An always-on ghost watching that is only acceptable if it lives on your own machine.&lt;/p&gt;

&lt;p&gt;Open-weight models made that possible in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It works with the lights off.&lt;/strong&gt; There is no network call in the loop, so the ghost haunts you on a plane, in a basement, or during an outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your context stays on your machine.&lt;/strong&gt; Even with command text withheld, I wouldn't want a terminal's activity flowing to a server I don't control. With a local model I don't have to take anyone's word for it, and I can read exactly what gets sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I control the personality.&lt;/strong&gt; The ghost's voice depends on the prompt, the validation rules, and the model. With open weights I can swap Gemma for another model or tighten the constraints without asking a vendor, and nothing can be deprecated out from under it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API could have written the sentences. It couldn't have given me a ghost that is private, offline, and mine, and that's the part that makes it feel like a haunting rather than a service.&lt;/p&gt;

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

&lt;p&gt;The first time it fired, I ran &lt;code&gt;git reset --hard HEAD~1&lt;/code&gt; on a throwaway branch and the ghost answered: &lt;em&gt;"the past has accepted your changes, and forgotten your future."&lt;/em&gt; It was a small moment, but the one-line reminder that I had just erased something did what no warning dialog ever has. It made me pause.&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 runs locally through Ollama as the model behind every one of the ghost's reflections.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Too Lazy to Write Alt Text for Images? Let Gemma Do It</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:14:14 +0000</pubDate>
      <link>https://dev.to/0shuvo0/im-too-lazy-to-write-alt-text-so-gemma-does-it-for-me-6ji</link>
      <guid>https://dev.to/0shuvo0/im-too-lazy-to-write-alt-text-so-gemma-does-it-for-me-6ji</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;Every codebase I've touched has the same line in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/imgs/cookie.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's rarely because we don't care about accessibility. Describing forty images is just the most boring job in front-end, so it gets pushed to "later."&lt;/p&gt;

&lt;p&gt;So I built a VS Code extension that does it for me. Hover over any &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with a missing or empty &lt;code&gt;alt&lt;/code&gt;, click &lt;strong&gt;Generate alt text&lt;/strong&gt;, and the description is written straight into your code.&lt;/p&gt;

&lt;p&gt;Feeling really lazy? Press &lt;code&gt;Cmd+Shift+A&lt;/code&gt; (&lt;code&gt;Ctrl+Shift+A&lt;/code&gt; on Windows/Linux) and it fills in every empty alt in the file at once.&lt;/p&gt;

&lt;p&gt;A few small rules keep it safe to trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;never overwrites&lt;/strong&gt; alt text you already wrote.&lt;/li&gt;
&lt;li&gt;Failures are reported per image, and the successful ones are still applied.&lt;/li&gt;
&lt;li&gt;Output is escaped, so a stray quote can't break your markup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66g5wwn8w1klvstnbrml.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%2F66g5wwn8w1klvstnbrml.gif" alt="Extension demo" width="600" height="367"&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/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/ai-img-alt-text-ext" rel="noopener noreferrer"&gt;
        ai-img-alt-text-ext
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Generate accessible image alt text in HTML, JSX, and TSX files using a locally running Ollama vision model. Image contents are sent only to the Ollama server you configure.
    &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;AI Image Alt Text&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Generate accessible image alt text in HTML, JSX, and TSX files using a locally running Ollama vision model. Image contents are sent only to the Ollama server you configure.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/ai-img-alt-text-ext/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fai-img-alt-text-ext%2FHEAD%2Fdemo.gif" alt="demo"&gt;&lt;/a&gt;&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;Hover over an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element with an empty or missing &lt;code&gt;alt&lt;/code&gt; attribute and select &lt;strong&gt;Generate alt text&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Cmd+Shift+A&lt;/strong&gt; (or &lt;strong&gt;Ctrl+Shift+A&lt;/strong&gt; on Windows/Linux) to fill empty or missing alt attributes in the current file.&lt;/li&gt;
&lt;li&gt;Supports multiline tags, other attributes, workspace-relative image files, and HTTP(S) image URLs.&lt;/li&gt;
&lt;li&gt;Existing non-empty alt text is never overwritten. Bulk generation reports individual image failures and applies successful results together.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://ollama.com/" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; running locally or at a server you can access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A vision-capable model installed in Ollama, for example:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:latest&lt;/pre&gt;

&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first generation prompts for the Ollama server URL (for example &lt;code&gt;http://localhost:11434&lt;/code&gt;) and model name. These values are saved in the VS Code user settings. To change them…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/ai-img-alt-text-ext" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;The whole thing runs on &lt;strong&gt;Gemma 3 through Ollama&lt;/strong&gt;, entirely on my own machine.&lt;/p&gt;

&lt;p&gt;When you click the button, the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the image. It checks the workspace root first, then common static folders like &lt;code&gt;public/&lt;/code&gt;, so &lt;code&gt;/imgs/cookie.png&lt;/code&gt; just works. Remote URLs work too.&lt;/li&gt;
&lt;li&gt;Sends it to the local Gemma model with a short prompt asking for a concise, useful description.&lt;/li&gt;
&lt;li&gt;Writes the result back into the &lt;code&gt;alt&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setup is two steps: &lt;code&gt;ollama pull gemma3:latest&lt;/code&gt;, then point the extension at your Ollama URL. The model name is a setting, so you can swap in any other vision model without touching the code.&lt;/p&gt;

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

&lt;p&gt;Alt text feels like a perfect job for a hosted API, until you think about what you'd be sending it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your images stay yours.&lt;/strong&gt; Client mockups, unreleased products and internal dashboards often sit in a repo long before anyone should see them. With a local model, nothing leaves the laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing per image.&lt;/strong&gt; A big project can have hundreds of images. Running them through Gemma costs electricity, not a bill that grows with your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's swappable.&lt;/strong&gt; If a better open vision model shows up next month, it's a one-line settings change. I'm not tied to one provider's pricing or availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest trade-off:&lt;/strong&gt; a local model is slower than a hosted API. But this is a job you start and walk away from, so for this use case slow and steady is a fine deal.&lt;/p&gt;

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

&lt;p&gt;I ran it on 20 images from one of my projects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; just 3 minutes, versus roughly 10 minutes writing them by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable as-is:&lt;/strong&gt; 18 out of 20 descriptions needed no editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it struggled:&lt;/strong&gt; Animated or vector images eg &lt;code&gt;gif&lt;/code&gt; files&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; every description is generated by Gemma 3, running locally through Ollama.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>programming</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>Chat with client In -&gt; Invoice Out</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/0shuvo0/chat-with-client-in-invoice-out-4odg</link>
      <guid>https://dev.to/0shuvo0/chat-with-client-in-invoice-out-4odg</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;Every freelancer knows this moment.&lt;/p&gt;

&lt;p&gt;The client says &lt;em&gt;"Great work, can you send the invoice?"&lt;/em&gt; and suddenly you're digging through 200 WhatsApp messages to find the rate, the hours, the email address, and that one message where the price changed from $250 to $300.&lt;/p&gt;

&lt;p&gt;Then you open a template, retype everything, export a PDF, and email it. Ten minutes gone, for something that's already written down in the chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat2Invoice&lt;/strong&gt; fixes that. You paste the conversation, and a local AI turns it into a ready-to-send invoice PDF.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Paste&lt;/strong&gt; your WhatsApp, Slack, iMessage, or email conversation (optionally attach a contract, a receipt, or a mockup).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; what the AI found. Anything missing gets flagged, and everything is editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send&lt;/strong&gt; a clean A4 PDF. Download it or email it straight to the client.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for this?&lt;/strong&gt; Anyone who gets paid directly, outside platforms that auto-generate invoices: freelance designers, developers, writers, consultants, and small agencies who negotiate in DMs and then have to turn those DMs into paperwork.&lt;/p&gt;

&lt;p&gt;The best part is that the chat never has to leave your machine.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 1: Paste the chat&lt;/strong&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%2Fb76grafz8w67knlli62f.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%2Fb76grafz8w67knlli62f.png" alt="Chat2Invoice step 1" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Review what the AI found&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI shows what it extracted and highlights anything it couldn't find. Here it caught one field was not found in chat.&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%2Fk5df8z838pdho11rsjka.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%2Fk5df8z838pdho11rsjka.png" alt="Chat2Invoice step 2" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Preview, download, send&lt;/strong&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%2Fuo885rsv223s5h2zyg9c.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%2Fuo885rsv223s5h2zyg9c.png" alt="Chat2Invoice step 3" width="799" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  Struggling to learn Git?
&lt;/h2&gt;

&lt;p&gt;Learn Git by solving problems on &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5629n4cm1n7gxrhe6fat.png" alt="GitMission preview" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&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/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/chat2invoice" rel="noopener noreferrer"&gt;
        chat2invoice
      &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;Chat2Invoice&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Paste a client chat → local AI (Ollama) extracts invoice details → review/edit → generate an A4 PDF → download or email it.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/chat2invoice/preview.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fchat2invoice%2FHEAD%2Fpreview.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Next.js 14 (App Router) + TypeScript + Tailwind CSS, jsPDF (PDF generation), pdfjs-dist (PDF text extraction), Inter font bundled locally (@fontsource).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; everything runs locally. Text/PDF reading and PDF generation happen in the browser; the AI runs in your local Ollama. Only "Send via Email" needs internet (Resend).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Node.js &lt;strong&gt;18.18+&lt;/strong&gt; (20 LTS recommended)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/download" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 1. Install Ollama, then pull the model (one time, ~3 GB)&lt;/span&gt;
ollama pull gemma3:latest

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 2. Install dependencies&lt;/span&gt;
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; chat2invoice
npm install

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 3. Environment&lt;/span&gt;
cp .env.example .env.local     &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; edit if needed&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;.env.local&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-dotenv notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;OLLAMA_AI_MODEL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;gemma3:latest&lt;/span&gt;
&lt;span class="pl-v"&gt;OLLAMA_HOST&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;http://127.0.0.1:11434&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Optional – the "Send Invoice via Email" section only appears when BOTH are set:&lt;/span&gt;
&lt;span class="pl-v"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;re_xxx&lt;/span&gt;
&lt;span class="pl-v"&gt;RESEND_SENDER_EMAIL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;invoices@your-verified-domain.com&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;(Shell environment variables work too.) RESEND_SENDER_EMAIL must…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/chat2invoice" 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-source AI part:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; running locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. No API key, no per-request bill, no server in the middle. The app talks to Ollama on &lt;code&gt;127.0.0.1:11434&lt;/code&gt;, and that's where the "understanding" happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router) + &lt;strong&gt;TypeScript&lt;/strong&gt; + &lt;strong&gt;Tailwind CSS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jsPDF&lt;/strong&gt; to generate the invoice PDF in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdfjs-dist&lt;/strong&gt; to read text out of attached PDF contracts, also in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inter&lt;/strong&gt; font bundled locally, so nothing is fetched from a CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; for the optional "Send via Email" feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input.&lt;/strong&gt; Paste the chat or hit "Paste Clipboard". An optional "Advanced &amp;amp; Attachments" section lets you add notes that &lt;em&gt;override&lt;/em&gt; the chat (like "Net 15, apply 10% holiday discount"), plus PDF/TXT files and images. Text is extracted in the browser, and images go to Gemma 3's vision capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; Missing required fields appear under &lt;strong&gt;Action Needed&lt;/strong&gt;, and found ones are shown as editable fields. Optional extras include discount, amount due now, tax note, currency, company names, and payment instructions. The invoice ID (like &lt;code&gt;INV-2026-OCT-13-JD2N&lt;/code&gt;) and issue date are auto-generated, and "Due Now" defaults to amount minus discount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready.&lt;/strong&gt; Preview the invoice, download the PDF, or send it to the client with an editable message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I intentionally put a human checkpoint in the middle. The AI proposes, and &lt;em&gt;you&lt;/em&gt; approve. A small local model will occasionally misread a message, and an invoice is a document with money on it, so the review step is a feature rather than a limitation. Missing details get flagged and asked for instead of invented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it yourself&lt;/strong&gt; takes a minute. Pull a model with Ollama, then create &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OLLAMA_AI_MODEL=gemma3:latest
OLLAMA_HOST=http://127.0.0.1:11434

# Optional: the "Send Invoice via Email" section only appears when BOTH are set
RESEND_API_KEY=re_xxx
RESEND_SENDER_EMAIL=invoices@your-verified-domain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RESEND_SENDER_EMAIL&lt;/code&gt; must be on a domain verified in your Resend account. Skip these two lines and the app simply hides the email section, so everything else works fully offline.&lt;/p&gt;

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

&lt;p&gt;Client chats are some of the most sensitive text a freelancer owns. They contain real names, email addresses, rates, project details, and sometimes things covered by an NDA. Pasting that into a closed cloud API just to get a PDF is a bad trade.&lt;/p&gt;

&lt;p&gt;Open-weight models changed the deal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by architecture.&lt;/strong&gt; The chat is processed by Gemma on your own machine. There's no "we promise not to store it" because there's no server to store it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costs nothing to run.&lt;/strong&gt; No tokens, no subscription, no usage cap. A tool for a side hustle shouldn't eat into the side hustle's income.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline.&lt;/strong&gt; Text extraction, AI, and PDF generation all happen locally. Only the optional email step needs internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; The model is one environment variable (&lt;code&gt;OLLAMA_AI_MODEL&lt;/code&gt;). If a better open model shows up next month, swapping it in is a one-line change, with no pricing changes or deprecations to worry about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API could do the extraction, but it couldn't make the promise that matters most to the person using this tool: &lt;em&gt;your client's conversation never left your laptop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;My friend who is in final year in college and he is super busy trying to manage study and freelance work at the same time. So this will save him a few hours monthly.&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 runs locally via Ollama and handles both text extraction and image understanding for receipts and mockups.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Group Chat Where Only AIs Can Talk (and They Roast Each Other)</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:10:34 +0000</pubDate>
      <link>https://dev.to/0shuvo0/i-built-a-group-chat-where-only-ais-can-talk-and-they-roast-each-other-2bfk</link>
      <guid>https://dev.to/0shuvo0/i-built-a-group-chat-where-only-ais-can-talk-and-they-roast-each-other-2bfk</guid>
      <description>&lt;p&gt;Imagine a group chat like WhatsApp or Discord, but no human can send messages. Only AI models can. They talk to each other, bully each other, roast each other, and react to each other's messages.&lt;/p&gt;

&lt;p&gt;That was my idea, and it turned out to be a lot of fun.&lt;/p&gt;

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

&lt;p&gt;Here's how I built it, including the one trick that made it far more entertaining: giving every model its own voice.&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/0shuvo0/AI-Group-Chat" rel="noopener noreferrer"&gt;github.com/0shuvo0/AI-Group-Chat&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;It's a custom app where I can add a bunch of AI models to a shared chat. Once they're in, they read what the others wrote and respond on their own. I just sit back and watch.&lt;/p&gt;

&lt;p&gt;The first question is how to get the models into the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Too many APIs
&lt;/h2&gt;

&lt;p&gt;I don't want to open chatgpt.com and paste messages by hand. I want my app to send a question to a model and get the answer back programmatically. That's what an &lt;strong&gt;API&lt;/strong&gt; is for. Gemini, Claude, Grok, and ChatGPT all have their own.&lt;/p&gt;

&lt;p&gt;The catch: I wanted &lt;em&gt;lots&lt;/em&gt; of models, and API access is paid. Five models would mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;five separate accounts,&lt;/li&gt;
&lt;li&gt;five separate billing setups,&lt;/li&gt;
&lt;li&gt;five API keys to manage,&lt;/li&gt;
&lt;li&gt;a minimum top-up on each, even though I'd barely use any tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's complicated and not cost effective.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: OpenRouter
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; gives you access to a huge catalog of models through a single API. You add credit to one account, use whichever models you like, and it bills you per model usage.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;one service, one API key, one balance&lt;/strong&gt;. Swapping or adding a model is just a change to the model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Getting them to actually chat
&lt;/h2&gt;

&lt;p&gt;Left alone, models will happily ramble about whatever they want, and that's rarely entertaining. So I added a &lt;strong&gt;theme&lt;/strong&gt;. When I open the app, I can give the chat a topic, and the models riff around it.&lt;/p&gt;

&lt;p&gt;Each model gets a prompt like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ai, chatting in a group chat with these AIs &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;others&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.
Talk the way people do in a casual group chat.

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\nTheme of this chat: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

Rules:
- Write only your own messages, never lines for the others.
- Send 1 to 3 short messages. Put each message on its own line.
- Keep each message to a sentence or two. Do not write paragraphs.
- No markdown, no quotes, and do not start with your name or a label.
- An emoji-only message is fine now and then.
- Respond to what others said, by name if it helps. Add something new instead of just agreeing or repeating.
- Do not say goodbye or try to wrap up the conversation.
- Talk like carefree young generation: relaxed, fun, expressive, emojis

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Chat so far:
&amp;lt;chat&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&amp;lt;/chat&amp;gt;

If you have nothing to add right now, reply with exactly: &amp;lt;SKIP&amp;gt;
Otherwise, send your next message(s).`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nobody has said anything yet, so you are sending the first message.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details make this work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role clarity.&lt;/strong&gt; Each model is told it's in a group chat and who else is there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict format rules.&lt;/strong&gt; Short messages, no markdown, no name labels. Without these, models write essays or script lines for the other participants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared history.&lt;/strong&gt; Previous messages are passed in a &lt;code&gt;&amp;lt;chat&amp;gt;&lt;/code&gt; block so replies actually respond to what was said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An escape hatch.&lt;/strong&gt; Models can answer &lt;code&gt;&amp;lt;SKIP&amp;gt;&lt;/code&gt; if they have nothing to add, which keeps the conversation from feeling forced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personality.&lt;/strong&gt; The casual, emoji-friendly tone instruction is what gives the chat its vibe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app itself was simple to build. I described what I wanted to Claude, and a few prompts later I had a working version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: It's too fast to read
&lt;/h2&gt;

&lt;p&gt;The app worked and looked good, but it wasn't as fun as I'd hoped. Messages arrive so quickly that reading them all becomes a chore.&lt;/p&gt;

&lt;p&gt;So I gave the models a voice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: Kokoro.js
&lt;/h3&gt;

&lt;p&gt;For text-to-speech I used &lt;strong&gt;&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt;&lt;/strong&gt;, an open model with just 82 million parameters, around 300 MB in size. That's small enough to run directly in the browser, so there are no extra server calls or per-character fees, and it's fast.&lt;/p&gt;

&lt;p&gt;I used its JavaScript library, &lt;a href="https://www.npmjs.com/package/kokoro-js" rel="noopener noreferrer"&gt;kokoro-js&lt;/a&gt;, and plugged it into the web app. Each incoming AI message is converted to audio locally and played back. Now the models don't just type, they &lt;em&gt;talk&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final result
&lt;/h2&gt;

&lt;p&gt;Watching a handful of AIs bicker, joke, and roast each other out loud is oddly entertaining.&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%2Fqw8hyc9qgwkqo3xk5psq.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%2Fqw8hyc9qgwkqo3xk5psq.png" alt="Chat demo" width="601" height="915"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiple AI models, one API&lt;/td&gt;
&lt;td&gt;&lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech in the browser&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt; via &lt;a href="https://www.npmjs.com/package/kokoro-js" rel="noopener noreferrer"&gt;kokoro-js&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffolding the app&lt;/td&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;The full code is on GitHub: &lt;a href="https://github.com/0shuvo0/AI-Group-Chat" rel="noopener noreferrer"&gt;0shuvo0/AI-Group-Chat&lt;/a&gt;. Clone it, add your OpenRouter key, pick a theme, and let the models loose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Struggling with git?
&lt;/h2&gt;

&lt;p&gt;Learn git by playing games and solving missions with &lt;a href="https://gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh8fieg1pcprtbas1rgi.png" alt="GitMission Preview" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>People Won't Go Outside so I made this</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 07 Oct 2026 01:38:29 +0000</pubDate>
      <link>https://dev.to/0shuvo0/people-wont-go-outside-so-i-made-this-20j4</link>
      <guid>https://dev.to/0shuvo0/people-wont-go-outside-so-i-made-this-20j4</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;iframe src="https://www.youtube.com/embed/3kbQoHIhxB0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://goseek.ai.studio/" rel="noopener noreferrer"&gt;GoSeek&lt;/a&gt;&lt;/strong&gt; is a mobile scavenger hunt that gets you outside by giving you a riddle instead of an instruction.&lt;/p&gt;

&lt;p&gt;The problem I started with: if an app says &lt;em&gt;"go outside!"&lt;/em&gt;, nobody goes. People don't like being told what to do, but they love solving puzzles. So GoSeek hands you this instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Small furry apex predator that domesticated itself, loves afternoon sunbeams, and leaves silent pawprints in the garden."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can't solve that from the couch. You have to go find the thing, and the app only accepts a photo taken live in the app. No gallery uploads.&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%2Fyqz5bd8kv3lcnwcdmavs.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%2Fyqz5bd8kv3lcnwcdmavs.gif" alt="App demo" width="480" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tap &lt;strong&gt;Touch Grass&lt;/strong&gt; and get a riddle for a random everyday object (a bench, a pigeon, a manhole cover).&lt;/li&gt;
&lt;li&gt;Go outside and work out what it is.&lt;/li&gt;
&lt;li&gt;Open the camera and take a photo.&lt;/li&gt;
&lt;li&gt;A vision model running &lt;strong&gt;on your phone&lt;/strong&gt; checks whether it matches.&lt;/li&gt;
&lt;li&gt;Match confirmed: confetti, points, and the photo joins your &lt;strong&gt;Trophy Gallery&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Harder objects are worth more points, and points level you up from &lt;em&gt;Wanderer&lt;/em&gt; to &lt;em&gt;Explorer&lt;/em&gt; to &lt;em&gt;Seeker&lt;/em&gt; to &lt;em&gt;Trailblazer&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is it for?&lt;/strong&gt; Anyone, especially city dwellers. The object list leans urban (street signs, power lines, shop signs), with nature finds like butterflies and pine cones as harder, higher-reward targets. The screen is the shortest part: read, walk, snap, done.&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%2F2rwacqx5m6by9rhnnhla.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%2F2rwacqx5m6by9rhnnhla.png" alt="GoSeek screens" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;🌿 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://goseek.ai.studio/" rel="noopener noreferrer"&gt;goseek.ai.studio&lt;/a&gt; (best on your phone)&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/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/goseek" rel="noopener noreferrer"&gt;
        goseek
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      GoSeek is a mobile-first outdoor scavenger hunt game. Each round gives you a riddle clue, and you must find the matching object in the real world by taking a photo with your camera or uploading an image.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/goseek/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fgoseek%2FHEAD%2Fdemo.gif" alt="Demo video"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;GoSeek&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;GoSeek is a mobile-first outdoor scavenger hunt game. Each round gives you a riddle clue, and you must find the matching object in the real world by taking a photo with your camera or uploading an image.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/goseek/demo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fgoseek%2FHEAD%2Fdemo.png" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it: &lt;a href="https://goseek.ai.studio/" rel="nofollow noopener noreferrer"&gt;goseek.ai.studio&lt;/a&gt;
&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The app picks a random object from &lt;code&gt;src/data/objects.json&lt;/code&gt; and shows a clue/riddle.&lt;/li&gt;
&lt;li&gt;You open the mission screen, then use the live camera or a file upload to capture a photo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/services/aiModel.ts&lt;/code&gt; validates the image against the target object using a local Hugging Face vision pipeline and a fallback visual heuristic.&lt;/li&gt;
&lt;li&gt;If the object matches, the app awards points, stores the discovery, and updates your run stats.&lt;/li&gt;
&lt;li&gt;Progress is saved in the browser using IndexedDB (&lt;code&gt;src/services/db.ts&lt;/code&gt;), so your streak, score, and discovery history persist locally.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Main flow&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/App.tsx&lt;/code&gt; manages the dashboard, active mission, score updates, and success/reset modals.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/ActiveMission.tsx&lt;/code&gt; handles the camera, verification loop, save…&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/0shuvo0/goseek" 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;
  
  
  1. Design: Google Stitch → Figma
&lt;/h3&gt;

&lt;p&gt;I described the app to &lt;a href="https://stitch.withgoogle.com/" rel="noopener noreferrer"&gt;Google Stitch&lt;/a&gt;, imported the generated design into Figma, simplified it, and edited the copy.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Stitch prompt
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP: GoSeek
1.
when user opens the app and clicks a button first we choose an everyday common object that can be easily found outside(We will have an array of those things with difficulty level, more common = less difficult and more difficult = more points)
2.
app uses ai to describe the object as a mystery/riddle eg. instead of saying 'cat' it says 'small furry apex predator that domesticated itself'
3.
the use has to go out find the object and take a photo(use can only take photo on the app, website not upload old or downloaded photos)
4.
then the app uses AI to analyze the image and tells use if the photo is of correct thing or not
if correct so a nice animation and add points to user

design a mvp for it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Google stitch output: &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbf7872j08xz4ghz9yjx.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%2Fcbf7872j08xz4ghz9yjx.png" alt="Google stitch output" width="799" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After simplifying on figma:&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%2Ff1tzq15axt8tz7aa1znm.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%2Ff1tzq15axt8tz7aa1znm.png" alt="After simplifying on" width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The architecture problem
&lt;/h3&gt;

&lt;p&gt;My first plan: keep a list of objects and have an on-device model write riddles on the fly. That fails. A model good enough for &lt;em&gt;clever&lt;/em&gt; riddles is too big for most phones, and this is an outdoor game that has to run on the device you actually carry.&lt;/p&gt;

&lt;p&gt;So I split the job and put each task where it fits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write creative riddles&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; (open-weight)&lt;/td&gt;
&lt;td&gt;My computer, via Ollama&lt;/td&gt;
&lt;td&gt;Once, ahead of time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge the photo&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SmolVLM-500M&lt;/strong&gt; (open-weight)&lt;/td&gt;
&lt;td&gt;The user's phone, in the browser&lt;/td&gt;
&lt;td&gt;Every play&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The heavy creative model runs &lt;strong&gt;once&lt;/strong&gt; and produces a static &lt;code&gt;objects.json&lt;/code&gt;. The tiny vision model handles the one thing that must happen live.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Generating riddles with Gemma
&lt;/h3&gt;

&lt;p&gt;I pulled &lt;code&gt;gemma3:4b&lt;/code&gt; with Ollama and wrote a small script that asks for five riddles per object. The key is the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You write riddles for an outdoor scavenger hunt game for all ages.
Write 5 different riddles, each describing "${obj.name}" WITHOUT ever saying the word "${obj.name}" (or its plural, or any word in it).
Rules:
- Each riddle is 1-2 sentences, maximum 30 words.
- Each riddle uses a different angle (looks, purpose, location, sound, behavior, feel).
- Family-friendly, no mention of people or danger.
- Do not start with "I am" every time.

Output ONLY a JSON array of 5 strings. No explanation, no markdown.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asking for &lt;strong&gt;a different angle per riddle&lt;/strong&gt; keeps replays fresh, and the "never say the word" rule stops riddles from giving the answer away.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Full generation script (ai.ts + index.ts)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ai.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;askGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:11434/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemma3:4b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Local AI request failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Make sure Ollama is running.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Local AI returned an empty recommendation. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;askGemma&lt;/span&gt; &lt;span class="p"&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 typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;askGemma&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Difficulty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GameObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Difficulty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tree&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OUTPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;objects.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Generating "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`...the prompt above...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/gi, "").trim();
    const riddles = JSON.parse(cleaned);

    results.push({ name: obj.name, difficulty: obj.difficulty, riddles });
  }

  await fs.writeFile(OUTPUT_FILE, JSON.stringify(results, null, 2), "utf8");
  console.log(`&lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt; &lt;span class="nx"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Saved&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`);
}

run().catch(console.error);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The output is a plain &lt;code&gt;objects.json&lt;/code&gt;:&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="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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"road"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"easy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"riddles"&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="s2"&gt;"A long ribbon of black stone that carries wheels from where you are to where you're going, never moving itself"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Painted with dashed lines and arrows, it tells travelers where to go, yet it has no voice"&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;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;h3&gt;
  
  
  4. Building the app in Google AI Studio
&lt;/h3&gt;

&lt;p&gt;I opened &lt;a href="https://aistudio.google.com/apps" rel="noopener noreferrer"&gt;Google AI Studio's app builder&lt;/a&gt;, attached the design screenshots and &lt;code&gt;objects.json&lt;/code&gt;, and gave it a single detailed prompt: game loop, data models, tech stack (React, TypeScript, Tailwind, Transformers.js, IndexedDB), and camera rules. A few follow-up prompts later the app worked, and one click on &lt;strong&gt;Publish&lt;/strong&gt; made it live.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  AI Studio prompt
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role &amp;amp; Goal: Act as an expert frontend developer. Build a mobile-first, PWA-ready web application called "GoSeek," an outdoor scavenger hunt game where users find real-world objects based on riddles.

UI/UX Design
Match the layout, dark theme, and typography strictly to the provided design mockups in attached photos. The app must be highly responsive on mobile devices.

Core Game Loop
Model Initialization: On first load, download and initialize the SmolVLM-500M AI model locally on the device (show a progress bar during this step).
Mission Generation: Select a random everyday outdoor object from objects.json. Display one of its associated riddles to the user (e.g., "small furry apex predator that domesticated itself" instead of "cat").
Capture: The user clicks "Open camera" to access the device's live camera. Constraint: Users must capture a live photo; file uploads from the gallery are strictly disabled.
Verification: Pass the captured frame to the local SmolVLM-500M model to verify if the object matches the riddle.
Success State: If matched, trigger a celebratory animation (e.g., confetti or a success modal) and award points based on the object's difficulty.
Gallery &amp;amp; State: Downscale the verified image and save it locally along with the object name, timestamp, and points earned. Display these in a scrollable "Trophy Gallery &amp;amp; Log" history section.

Data Structures &amp;amp; Logic
export type Difficulty = 'easy' | 'medium' | 'hard';
export interface GameObject {
  name: string;
  difficulty: Difficulty;
  riddles: string[];
}
export const points: Record&amp;lt;Difficulty, number&amp;gt; = { easy: 10, medium: 20, hard: 30 }
export const level: Record&amp;lt;string, number&amp;gt; = {
  wanderer: 0,      // 0-49 points
  explorer: 50,     // 50-99 points
  seeker: 100,      // 100-149 points
  trailblazer: 150  // 150+ points
}

Technical Requirements
Tech Stack: Use React with TypeScript and Tailwind CSS for styling.
AI Integration: Use Hugging Face transformers.js to load and run SmolVLM-500M entirely on-device via WebGPU/WebAssembly.
Storage: Use IndexedDB (via a wrapper like idb or localforage) rather than localStorage to save the downscaled user photos and history state without hitting quota limits.
Camera API: Use the HTML5 navigator.mediaDevices.getUserMedia API facing the environment (rear camera). Fall back to typical file upload if it fails.
Handle edge cases and figure stuff out if missing in description.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&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%2F9vniycqtcmrgd33z4n2z.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%2F9vniycqtcmrgd33z4n2z.png" alt="Google AI studio" width="800" height="693"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&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;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;Google Stitch → Figma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Riddle generation&lt;/td&gt;
&lt;td&gt;Gemma 3 4B via Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-device vision&lt;/td&gt;
&lt;td&gt;SmolVLM-500M via Transformers.js (WebGPU/WASM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React + TypeScript + Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;IndexedDB (photos + history)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getUserMedia&lt;/code&gt;, rear camera, live capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build + hosting&lt;/td&gt;
&lt;td&gt;Google AI Studio&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How It Went Outside
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;So I played with it outsider for 20-0 minutes and it was a blast&lt;/li&gt;
&lt;li&gt;One of the riddle I got was: &lt;em&gt;No back cushions, no screen, just a place to rest, feed the pigeons, or wait for someone&lt;/em&gt; and very interesting I must way&lt;/li&gt;
&lt;li&gt;The SmolVLM-500M is small but so far it has been working decent]&lt;/li&gt;
&lt;li&gt;It runs very smoothly on my iPhone 14&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Open models didn't just make GoSeek cheaper. They made the architecture possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos never leave your phone.&lt;/strong&gt; GoSeek points a camera at your street and neighborhood. Because SmolVLM-500M is open-weight and runs in the browser, verification happens on the device: no upload, no server, no account. A closed vision API would receive every photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works where the game happens.&lt;/strong&gt; After the one-time model download, checking a photo doesn't need a network round-trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per play.&lt;/strong&gt; Every verification is free, where a hosted vision API charges per image. Running Gemma locally made generating all the riddles free too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right model, right place.&lt;/strong&gt; Open weights let me run a bigger model once for a creative task and a tiny one live on the phone. That split is the core design decision of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is swappable.&lt;/strong&gt; A better small vision model next month? Swap it in. Riddles in another language or tone? Regenerate &lt;code&gt;objects.json&lt;/code&gt; with a new prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Honest trade-offs:&lt;/strong&gt; The first load need internet connection for downloading the AI model, but after that it can run completely offline.&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, run locally through Ollama, writes every riddle in the game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading, and now go touch some grass. 🌿&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Struggling to learn Git?
&lt;/h2&gt;

&lt;p&gt;Learn Git by solving problems on &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5629n4cm1n7gxrhe6fat.png" alt="GitMission preview" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>My friend's Game Boy got stolen so I built him one</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sat, 03 Oct 2026 06:45:29 +0000</pubDate>
      <link>https://dev.to/0shuvo0/my-friends-game-boy-got-stolen-so-i-built-him-one-3lnn</link>
      <guid>https://dev.to/0shuvo0/my-friends-game-boy-got-stolen-so-i-built-him-one-3lnn</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;p&gt;My friend's Game Boy Color got stolen. Rude. Then he went looking for a replacement and found out that decent used ones are now rare, overpriced, or "mint condition" in the way that means "mint-flavored lie."&lt;/p&gt;

&lt;p&gt;So I built him a new one that can't be stolen: a &lt;strong&gt;Game Boy Color in the browser&lt;/strong&gt;. It's teal, it has a D-pad that clicks, and it plays real Game Boy games.&lt;/p&gt;

&lt;p&gt;The twist is that he never has to scroll through a ROM list. He just tells it what he's in the mood for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to play a game involving Pokémon."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A small AI reads the request, picks a game from the collection, and boots it. He can type the request or tap the mic and say it out loud. A bit like a game store clerk who lives inside the console.&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%2Ff82oi9rintgdt2uyaqvb.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%2Ff82oi9rintgdt2uyaqvb.gif" alt="Game demo" width="600" height="481"&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/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/Game-boy-color-emulator" rel="noopener noreferrer"&gt;
        Game-boy-color-emulator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browser-based Gameboy Color Emulator with AI game search capabilities using Google gemma 4b
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/Game-boy-color-emulator/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2FGame-boy-color-emulator%2FHEAD%2Fdemo.gif" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Browser-based Gameboy Color Emulator&lt;/h1&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;with AI game search capabilities using Google gemma 4b&lt;/h2&gt;

&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: For the AI feature to work you need to have &lt;a href="https://ollama.com/library/gemma3:4b" rel="nofollow noopener noreferrer"&gt;https://ollama.com/library/gemma3:4b&lt;/a&gt; installed and running on your machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;in the package.json file there is a script called "start:ai" which will start the ollama server.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/Game-boy-color-emulator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emulator:&lt;/strong&gt; The mGBA core compiled to WebAssembly, wired up in a Vite + TypeScript project. It loads ROMs straight from &lt;code&gt;public/gameroms&lt;/code&gt;, including ones packed inside ZIP files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controls:&lt;/strong&gt; Keyboard (&lt;code&gt;Z&lt;/code&gt;/&lt;code&gt;X&lt;/code&gt;/&lt;code&gt;Enter&lt;/code&gt;/&lt;code&gt;Shift&lt;/code&gt;), touch buttons on the on-screen console, and gamepad support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI picker:&lt;/strong&gt; Gemma 3 (4B), running locally through &lt;strong&gt;Ollama&lt;/strong&gt;. When he makes a request, the app sends it along with the list of available games. Gemma picks the best match and the emulator loads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice:&lt;/strong&gt; The browser's built-in speech recognition fills in the prompt and submits it when he stops talking. It's not an AI model, but it makes the whole thing feel like magic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escape hatch:&lt;/strong&gt; If he knows what he wants, "Choose a game instead" opens a normal list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt is simple: "here's what the player wants, here's the game list, pick one." The game list is just a file (&lt;code&gt;src/games.ts&lt;/code&gt;), so adding a game means dropping a ROM in a folder and adding its name.&lt;/p&gt;

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

&lt;p&gt;This project needed an AI that could do one small job well: match a vibe to a filename. A giant cloud model would be overkill, and a cloud API would have made it worse in a few ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing.&lt;/strong&gt; No API key, no per-request bill, no "you've hit your limit" while someone is trying to play Pokémon. For a gift to a friend, that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs locally.&lt;/strong&gt; His requests never leave the machine. Nobody needs to know what he plays at 2 AM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's swappable.&lt;/strong&gt; Gemma 3 4B was small and fast enough. If a different model picks better, it's one line to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't break on me.&lt;/strong&gt; No deprecation emails, no pricing changes, no surprise outages. The model is on the disk, and it stays there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 4B open model on a normal machine turned out to be plenty for this. It does the job without anyone's permission or a credit card.&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) running locally through Ollama powers the natural-language game picker.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hacktoberfest 2026, what changed?</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:06:27 +0000</pubDate>
      <link>https://dev.to/0shuvo0/hacktoberfest-2026-what-changed-1g3g</link>
      <guid>https://dev.to/0shuvo0/hacktoberfest-2026-what-changed-1g3g</guid>
      <description>&lt;h1&gt;
  
  
  Hacktoberfest 2026 Is Not What You Remember (And That's Actually Good News)
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Last updated: October 1, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've done Hacktoberfest before, you probably remember the drill: open four pull requests, wait for the merge notifications, and wear your free T-shirt with quiet pride.&lt;/p&gt;

&lt;p&gt;Forget all of that. &lt;strong&gt;Pull requests no longer count toward Hacktoberfest rewards.&lt;/strong&gt; The whole event has been rebuilt from the ground up, and this year it's about learning and building with open-source AI.&lt;/p&gt;

&lt;p&gt;Don't close the tab yet. The new version is free, open to beginners, and has more ways to earn swag than the old one did. This guide walks through what changed, why, and exactly how to take part, online or in person.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 60-second version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pull requests are out.&lt;/strong&gt; Neither PRs nor merge requests count toward rewards anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theme is open-source AI.&lt;/strong&gt; The tagline is "AI belongs to everyone," and the focus is open-weight models, agents, and the tools around them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There are two ways to join:&lt;/strong&gt; 300+ one-day in-person events called &lt;strong&gt;Fests&lt;/strong&gt;, and an &lt;strong&gt;online program&lt;/strong&gt; with livestreams, challenges, and a week-long event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewards run on stickers.&lt;/strong&gt; You earn virtual stickers for activities, and real stickers get mailed to you once you collect enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's free&lt;/strong&gt;, and you need to be 13 or older.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MLH and DEV run it now&lt;/strong&gt;, with DigitalOcean (the original creator) as presenting partner.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Old vs. new: what actually changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;The old Hacktoberfest (2014-2025)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hacktoberfest 2026&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core task&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open 4 pull requests on participating repos&lt;/td&gt;
&lt;td&gt;Learn and build with open-source AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How progress is measured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PR count&lt;/td&gt;
&lt;td&gt;Virtual stickers from activities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Online, solo, on GitHub&lt;/td&gt;
&lt;td&gt;300+ in-person Fests plus an online program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reward&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;T-shirt (or a tree planted, in some years)&lt;/td&gt;
&lt;td&gt;Sticker pack by mail, with extra tiers up to a raffle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who runs it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;MLH and DEV, with DigitalOcean presenting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open-source contributions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The entire point&lt;/td&gt;
&lt;td&gt;Still welcome, just not rewarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community element&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mostly social media and Discord&lt;/td&gt;
&lt;td&gt;Real events in real cities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T-shirt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The headline prize&lt;/td&gt;
&lt;td&gt;Only at Fests, while supplies last, not guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small correction to a common memory: older editions let you contribute to &lt;em&gt;any&lt;/em&gt; public repo that opted in, not just a short curated list. The 2026 change is bigger than swapping the list. The counter itself is gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why they changed it
&lt;/h2&gt;

&lt;p&gt;Short answer: the spam got out of hand.&lt;/p&gt;

&lt;p&gt;The organizers say it's now easier than ever to churn out low-effort pull requests, and maintainers kept paying the price in review time and burnout. Their own history page describes the 2015-2025 era as one where thousands of people made a first contribution, but the flood of low-quality PRs eventually became too much for the people on the receiving end.&lt;/p&gt;

&lt;p&gt;So rather than patching the rules again, they dropped the incentive. The mission page puts it simply: open source was never defined by a PR counter, and this year they'd rather help people learn to build with open models and tools.&lt;/p&gt;

&lt;p&gt;You can agree or disagree with the call. But the reasoning is easy to follow, and anyone who has ever maintained a popular repo in October will probably nod along.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet the new cast: MLH, DEV, and DigitalOcean
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.mlh.com/" rel="noopener noreferrer"&gt;Major League Hacking (MLH)&lt;/a&gt;&lt;/strong&gt; runs the Fests and the sign-in system (MyMLH).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to"&gt;DEV&lt;/a&gt;&lt;/strong&gt; runs the online challenges and hands out badges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/strong&gt; started Hacktoberfest in 2014 and is still on board as presenting partner.&lt;/li&gt;
&lt;li&gt;A long sponsor list backs the swag and events, including GitHub, MongoDB, Snowflake, Google Cloud, IBM, Qualcomm, Arduino, and many others.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two ways to take part
&lt;/h2&gt;

&lt;p&gt;You can do either one, or both. They stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: Go to a Fest (in person)
&lt;/h3&gt;

&lt;p&gt;A Fest is a free, one-day, locally organized event. Almost all of them are free, though a few pop-ups happen inside ticketed conferences. There are two flavors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hack Day&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Meetup&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vibe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mini hackathon&lt;/td&gt;
&lt;td&gt;Community hangout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you do&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build a project with open-weight models or open-source AI tools, then demo it&lt;/td&gt;
&lt;td&gt;Talks, workshops, panels, and meeting people&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solo or any team size (prizes cover up to four members)&lt;/td&gt;
&lt;td&gt;No team needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prizes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, usually a swag bag&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Laptop and charger&lt;/td&gt;
&lt;td&gt;Curiosity, plus a laptop only if the host asks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginners are welcome.&lt;/strong&gt; You don't need to build an AI project to attend a Meetup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everyone on a team must register and check in.&lt;/strong&gt; One person submits the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects must be finished and submitted on the day,&lt;/strong&gt; before the host's deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every in-person attendee gets an official participation certificate,&lt;/strong&gt; available in your My Hacktoberfest dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swag varies by Fest.&lt;/strong&gt; T-shirts and other goodies are limited and not guaranteed. The local host decides how to hand them out. Select Hack Days also have Arduino boards available while supplies last.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to join a Fest:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Find a Fest&lt;/a&gt; and search by city, country, or name (or use your location).&lt;/li&gt;
&lt;li&gt;Open the Fest's page and read the details: date, venue, food, accessibility, and what to install or bring.&lt;/li&gt;
&lt;li&gt;Register on that Fest's own page with a free MyMLH account. Every Fest has a separate registration.&lt;/li&gt;
&lt;li&gt;Show up, check in with the host, and say hi to a stranger.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Contact Host&lt;/strong&gt; button on the page for anything about logistics, accommodations, or late arrival.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Path 2: Join online (from anywhere)
&lt;/h3&gt;

&lt;p&gt;The online program runs all month and has three main pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Livestreams.&lt;/strong&gt; Sessions on open-weight models, agents, and tools. You check in with a code shown on screen to collect a sticker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV Challenges.&lt;/strong&gt; A weekend kickoff challenge plus four weekly open-source AI rounds, all labeled as cash-prize challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Hack Week: Hacktoberfest.&lt;/strong&gt; A week-long virtual festival of livestreams and challenges, run by MLH.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are the dates as listed on DEV's challenge hub and in community guides. Always double-check the deadline printed on each official challenge page, because that's the one that counts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Dates (UTC, as reported)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Weekend Challenge&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 2 (02:00) to Oct 5 (06:59)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Open-Source AI Challenge, Week 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 5 to Oct 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://events.mlh.com/events/14553-global-hack-week-hacktoberfest" rel="noopener noreferrer"&gt;Global Hack Week&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 9 to Oct 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Open-Source AI Challenge, Week 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 12 to Oct 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Open-Source AI Challenge, Week 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 19 to Oct 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Open-Source AI Challenge, Week 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 26 to Oct 31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full online calendar lives on the &lt;a href="https://hacktoberfest.com/schedule/" rel="noopener noreferrer"&gt;official schedule page&lt;/a&gt; and shows times in your own time zone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; the exact prize amounts and the actual challenge prompts weren't public when I researched this. The prompts are revealed when each challenge launches, so don't start building a "winning entry" before you've read the real brief. Challenge announcements go out under DEV's &lt;a href="https://dev.to/t/hf26challenge"&gt;&lt;code&gt;#hf26challenge&lt;/code&gt; tag&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The sticker system, explained
&lt;/h2&gt;

&lt;p&gt;This is the new reward engine, and it's friendlier than it sounds.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;24 virtual stickers&lt;/strong&gt; in total: 22 you can earn online, plus 2 you earn in person (attend a Fest, or host one). Each one is tied to an activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reward tiers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stickers collected&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A real, mailed Hacktoberfest 2026 sticker pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A bonus holographic sticker added to your pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Completionist&lt;/strong&gt; status, plus automatic entry into a raffle for a Hacktoberfest 2026 T-shirt or an Arduino Uno Q board&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two stickers are &lt;strong&gt;required&lt;/strong&gt; before anything ships: signing in to Hacktoberfest.com, and adding your mailing address to your MyMLH account. Do those first. They also count toward your total, so you're already two-thirds of the way to the first reward after five minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the rest come from
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples of what earns a sticker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Livestreams&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check into 1, 3, or 5 livestreams, or the launch livestream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DEV Challenges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connect your DEV account, then submit to the Launch Weekend and Weeks 1 to 4 challenges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Global Hack Week&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complete registration challenges, check into a session, and hit 15, 30, or 75 points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install and log in to DevRelay, join the MLH Community Discord, connect your DigitalOcean account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Surveys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fill out the pre-event and post-event surveys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In person&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attend a Fest, or host one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full, up-to-date list is on the &lt;a href="https://hacktoberfest.com/activities/" rel="noopener noreferrer"&gt;Activities page&lt;/a&gt;, and your personal progress lives in &lt;a href="https://hacktoberfest.com/my/" rel="noopener noreferrer"&gt;My Hacktoberfest&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good to know about swag
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Online participants don't get T-shirts.&lt;/strong&gt; Those are a Fest thing, and even there they aren't guaranteed. Completionists can still win one in the raffle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailing takes a while.&lt;/strong&gt; The main site says stickers and prizes go out 8 to 12 weeks after Hacktoberfest ends, while the FAQ says packs should arrive at most destinations within 30 to 60 days of shipping starting. Plan for a wait either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity done early still counts.&lt;/strong&gt; The FAQ says eligible activity completed before milestones were announced is credited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing in alone won't earn you a pack.&lt;/strong&gt; You need to do at least a few activities.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Your quick-start checklist
&lt;/h2&gt;

&lt;p&gt;If you only do one section of this article, make it this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today (5 to 10 minutes):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in at &lt;a href="https://hacktoberfest.com/my/" rel="noopener noreferrer"&gt;My Hacktoberfest&lt;/a&gt; with a free MyMLH account.&lt;/li&gt;
&lt;li&gt;Add your mailing address to MyMLH.&lt;/li&gt;
&lt;li&gt;Connect your DEV account (optional, but it earns a sticker and a DEV badge, and it's needed for DEV Challenges credit).&lt;/li&gt;
&lt;li&gt;Join the &lt;a href="https://discord.mlh.io" rel="noopener noreferrer"&gt;MLH Community Discord&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This week:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register for &lt;a href="https://events.mlh.com/events/14553-global-hack-week-hacktoberfest" rel="noopener noreferrer"&gt;Global Hack Week: Hacktoberfest&lt;/a&gt;, which is free.&lt;/li&gt;
&lt;li&gt;Follow each &lt;a href="https://dev.to/challenges/hacktoberfest"&gt;DEV Hacktoberfest challenge&lt;/a&gt; so you get launch notifications.&lt;/li&gt;
&lt;li&gt;Check the &lt;a href="https://hacktoberfest.com/schedule/" rel="noopener noreferrer"&gt;schedule&lt;/a&gt; and mark a few livestreams.&lt;/li&gt;
&lt;li&gt;Look up &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Fests near you&lt;/a&gt; and register if one fits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;All month:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check into livestreams and submit to challenges as they open.&lt;/li&gt;
&lt;li&gt;Fill out the pre-event survey early and the post-event survey at the end.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What should you build? Ideas to get started
&lt;/h2&gt;

&lt;p&gt;The official line is that instead of counting PRs, you'll write your first &lt;code&gt;skills.md&lt;/code&gt;, build an open-source agent, fine-tune an open-weight model, or follow your curiosity. Here are some starter ideas by difficulty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beginner-friendly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reusable agent &lt;strong&gt;skill&lt;/strong&gt;: a folder with a &lt;code&gt;SKILL.md&lt;/code&gt;, a README, a license, and a couple of examples.&lt;/li&gt;
&lt;li&gt;A small local chatbot running an open-weight model through a local runtime such as Ollama or llama.cpp.&lt;/li&gt;
&lt;li&gt;A tiny &lt;strong&gt;evaluation script&lt;/strong&gt; that checks whether a model gives consistent answers to the same prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Intermediate&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small &lt;strong&gt;open-source agent&lt;/strong&gt; that combines a local model, a few tools (files, APIs, MCP servers), and simple memory.&lt;/li&gt;
&lt;li&gt;A CPU-friendly &lt;strong&gt;fine-tuning experiment&lt;/strong&gt; using Hugging Face's &lt;a href="https://github.com/huggingface/peft" rel="noopener noreferrer"&gt;PEFT&lt;/a&gt; or &lt;a href="https://github.com/huggingface/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt;. Keep it small. A tiny project that works beats a giant one that doesn't.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tool-call correctness test suite&lt;/strong&gt; or prompt regression tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ambitious&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improve an open-source &lt;strong&gt;agent harness or SDK&lt;/strong&gt;: better error messages, safer permissions, clearer setup on Windows, macOS, or Linux.&lt;/li&gt;
&lt;li&gt;Build an &lt;strong&gt;evaluation dashboard&lt;/strong&gt; that compares open-weight models side by side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good project checklist before you submit anything: what real problem does it solve, which license applies, which model and under what terms, can someone else run it without hidden steps, is there a short demo or screenshot, and are the limitations written down.&lt;/p&gt;




&lt;h2&gt;
  
  
  But I still want to contribute to open source!
&lt;/h2&gt;

&lt;p&gt;You absolutely can, and nobody is stopping you. The organizers say outright that open source runs 365 days a year and that you can contribute anytime. They just aren't giving swag for it anymore. They point newcomers to this &lt;a href="https://dev.to/opensauced/open-source-101-a-beginners-guide-to-getting-started-37fb"&gt;beginner's guide to getting started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're going to send a PR this October, here's how to be the kind of contributor maintainers love:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; first.&lt;/strong&gt; Some projects require you to be assigned the issue, get design approval, sign a CLA, or follow an AI-disclosure policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that the issue is still open and unclaimed,&lt;/strong&gt; with no competing PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the problem yourself.&lt;/strong&gt; A fix without a reproduction is a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the change small and focused,&lt;/strong&gt; and add a test that fails before your fix and passes after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest about AI help.&lt;/strong&gt; Many repos now ask for disclosure. Never submit code you can't explain or test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what you're about to do&lt;/strong&gt; before writing a lot of code. A short comment like "I reproduced this, here's my plan, anyone already on it?" saves everyone time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where to look
&lt;/h3&gt;

&lt;p&gt;Hacktoberfest has no official participating-repo list this year. These are active projects in the open-AI orbit that have accepted outside contributions. Treat them as places to browse, not guaranteed openings, since issues get claimed quickly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/modelcontextprotocol/typescript-sdk" rel="noopener noreferrer"&gt;MCP TypeScript SDK&lt;/a&gt; and &lt;a href="https://github.com/modelcontextprotocol/python-sdk" rel="noopener noreferrer"&gt;MCP Python SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/peft" rel="noopener noreferrer"&gt;Hugging Face PEFT&lt;/a&gt; and &lt;a href="https://github.com/huggingface/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Giskard-AI/giskard-oss" rel="noopener noreferrer"&gt;Giskard&lt;/a&gt; (AI evaluation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mdn/content" rel="noopener noreferrer"&gt;MDN content&lt;/a&gt; (documentation, with low setup cost)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/browser-use/browser-use" rel="noopener noreferrer"&gt;Browser Use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/PrefectHQ/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A smaller, well-scoped issue in a project with a clear guide is almost always a better bet than a big feature nobody has approved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to host a Fest?
&lt;/h2&gt;

&lt;p&gt;Anyone can. If your city has no Fest yet, it could be you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hack Day&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Meetup&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attendees build projects&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workshops and speakers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prizes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLH funding&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Applications are open and are confirmed on a rolling basis, within about a week of a completed application.&lt;/li&gt;
&lt;li&gt;Hosts get stickers, swag, and prizes for participants, a listing in the searchable directory, programming help, and (for Hack Days) funding with reimbursement for certain event expenses.&lt;/li&gt;
&lt;li&gt;Start here: &lt;a href="https://hacktoberfest.com/host/" rel="noopener noreferrer"&gt;Host a Fest&lt;/a&gt;, then read the &lt;a href="https://mlh.gitbook.io/mlh-hacktoberfest-organizer-guide" rel="noopener noreferrer"&gt;host handbook&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Hosting also earns you a sticker and a DEV badge.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do pull requests count at all?&lt;/strong&gt;&lt;br&gt;
No. They don't count toward rewards, and the organizers say that's deliberate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I still get a T-shirt?&lt;/strong&gt;&lt;br&gt;
Maybe, but only at a Fest while supplies last (the host decides), or through the Completionist raffle if you hit 17 stickers. Online participants aren't promised one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who can join?&lt;/strong&gt;&lt;br&gt;
People aged 13 and over, subject to country eligibility rules. Individual Fests can have their own age limits, and if you're under 18 you should ask the host about parental permission. DEV Challenges have separate contest eligibility rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it free?&lt;/strong&gt;&lt;br&gt;
Yes, including nearly all Fests and Global Hack Week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to build an AI project to take part?&lt;/strong&gt;&lt;br&gt;
No. You can watch livestreams, attend a Meetup, join the Discord, and do surveys. Those all count toward stickers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a GPU?&lt;/strong&gt;&lt;br&gt;
Not for everything. Plenty of beginner-friendly work (documentation, SDK bugs, evaluation tools, small fine-tuning experiments) runs fine on a laptop CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who do I contact with problems?&lt;/strong&gt;&lt;br&gt;
Use &lt;strong&gt;Contact Host&lt;/strong&gt; on a Fest's page for event questions, the relevant challenge page for DEV Challenge questions, and the support emails listed on the &lt;a href="https://hacktoberfest.com/questions/" rel="noopener noreferrer"&gt;official FAQ&lt;/a&gt; for swag, prizes, or safety concerns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;It's okay to feel a little nostalgic. The four-PR challenge got a lot of people into open source, and it's strange to see it retired.&lt;/p&gt;

&lt;p&gt;But the new format has a lot going for it. Maintainers get a break from the spam. Beginners get real events and a clear, low-stakes path to a reward. And if you've been meaning to try out open-weight models or build your first agent, October now comes with a built-in excuse, a community, and stickers.&lt;/p&gt;

&lt;p&gt;So sign in, add your address, grab those first two stickers, and see where the month takes you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/" rel="noopener noreferrer"&gt;Hacktoberfest 2026 home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/questions/" rel="noopener noreferrer"&gt;Official FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hacktoberfest.com/online/" rel="noopener noreferrer"&gt;Attend online&lt;/a&gt; and the &lt;a href="https://hacktoberfest.com/activities/" rel="noopener noreferrer"&gt;Activities page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hacktoberfest.com/in-person/" rel="noopener noreferrer"&gt;Attend in person&lt;/a&gt; and &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Find a Fest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/host/" rel="noopener noreferrer"&gt;Host a Fest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/mission/" rel="noopener noreferrer"&gt;Mission and history&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/challenges/hacktoberfest"&gt;DEV Hacktoberfest challenges&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/aniruddhaadak/hacktoberfest-2026-what-online-builders-can-join-win-and-claim-4fge"&gt;Hacktoberfest 2026: what online builders can join, win, and claim&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hacktoberfest</category>
      <category>hackathon</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>You Can't Even Make an AI Wrapper Chat App 😆🫵</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Fri, 05 Jun 2026 05:48:23 +0000</pubDate>
      <link>https://dev.to/0shuvo0/you-cant-even-make-an-ai-wrapper-chat-app-3ph5</link>
      <guid>https://dev.to/0shuvo0/you-cant-even-make-an-ai-wrapper-chat-app-3ph5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"It's just a fetch call, bro." — Every junior dev, moments before learning a hard lesson.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you want to build a ChatGPT wrapper. Fair enough. It sounds embarrassingly simple — you've got an API key, a text box, and ambition. How hard can it be?&lt;/p&gt;

&lt;p&gt;Turns out: pretty hard. Not &lt;em&gt;rocket science&lt;/em&gt; hard, but &lt;em&gt;"oh god why is my chat broken on refresh"&lt;/em&gt; hard.&lt;/p&gt;

&lt;p&gt;Let's walk through the three stages of enlightenment.&lt;/p&gt;




&lt;h2&gt;
  
  
  🟢 Stage 1: The Junior Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Ship it. It works on my machine."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The junior dev opens a blank React project, installs &lt;code&gt;axios&lt;/code&gt; (or doesn't, and just uses &lt;code&gt;fetch&lt;/code&gt;), and writes something like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;result&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;Done. Deployed. LinkedIn post drafted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem?&lt;/strong&gt; You're waiting for the &lt;em&gt;entire&lt;/em&gt; AI response to generate before anything shows up on screen. For a short reply, fine. For a longer one? You're staring at a blank screen for 10–15 seconds while your users quietly close the tab.&lt;/p&gt;

&lt;p&gt;It feels like dial-up internet. In 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🟡 Stage 2: The Mid-Level Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"I've heard of this 'streaming' thing."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The mid-level dev has seen the ChatGPT UI. Tokens appear one by one, like the AI is &lt;em&gt;thinking&lt;/em&gt;. That's streaming — and it's a vastly better experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;&lt;strong&gt;Frontend:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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;Now tokens stream in real-time. The app &lt;em&gt;feels&lt;/em&gt; alive. Users are happy. You are happy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem?&lt;/strong&gt; Hit refresh. &lt;/p&gt;

&lt;p&gt;Gone. All of it. Every conversation, vanished. The app has the memory of a goldfish. Each new page load is a blank slate — no history, no context, no continuity.&lt;/p&gt;

&lt;p&gt;You've built a chat app that forgets you exist the moment you leave. That's not a chat app. That's a very expensive &lt;code&gt;alert()&lt;/code&gt; box.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔴 Stage 3: The Senior Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Streaming is table stakes. Persistence is the real work."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The senior dev knows that a chat app without history isn't a product — it's a demo. Real users expect to close the tab, come back tomorrow, and pick up where they left off. They expect their conversation to &lt;em&gt;exist&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This means the stream itself needs to be saved &lt;em&gt;as it arrives&lt;/em&gt; — not after it's done, not on the next request, but &lt;strong&gt;token by token&lt;/strong&gt;, persisted to a database in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend — save while streaming:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Save user message immediately&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Save complete assistant response after stream finishes&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;&lt;strong&gt;Frontend — load history on mount:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/conversations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/messages`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when a user refreshes? Their chat is still there. When they come back on mobile? Still there. When they share a conversation link? You get the idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But wait, there's more.&lt;/strong&gt; The senior dev also thinks about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending the full conversation history back to the AI (so it has &lt;em&gt;actual&lt;/em&gt; context)&lt;/li&gt;
&lt;li&gt;Handling mid-stream disconnects gracefully (what if the user closes the tab at 50%?)&lt;/li&gt;
&lt;li&gt;Debouncing saves so you're not hammering your DB on every single token&lt;/li&gt;
&lt;li&gt;Auth, so users only see &lt;em&gt;their&lt;/em&gt; conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is glamorous. It's just... what production looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What they build&lt;/th&gt;
&lt;th&gt;What's missing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Junior&lt;/td&gt;
&lt;td&gt;Request → Response&lt;/td&gt;
&lt;td&gt;Speed, UX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;td&gt;Streaming response&lt;/td&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senior&lt;/td&gt;
&lt;td&gt;Streaming + saved history&lt;/td&gt;
&lt;td&gt;(probably something else they'll discover next week)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between a junior and senior implementation isn't really about the AI part at all — the API call is the easy bit. It's about treating the &lt;em&gt;surrounding infrastructure&lt;/em&gt; with the same seriousness as the feature itself.&lt;/p&gt;

&lt;p&gt;So next time someone says "I built an AI chat app over the weekend" — ask them what happens when they refresh the page.&lt;/p&gt;

&lt;p&gt;The silence will tell you everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Now go add persistence to your side project. You know who you are.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Before AI we had ...</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Fri, 22 May 2026 04:12:37 +0000</pubDate>
      <link>https://dev.to/0shuvo0/before-ai-we-had--5a0g</link>
      <guid>https://dev.to/0shuvo0/before-ai-we-had--5a0g</guid>
      <description>&lt;p&gt;Nowadays you will hear a lot of people saying &lt;strong&gt;Why should I hire a software developer when I can vibecode it my self with about $100 worth of AI tools?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Well they aren't wrong about that!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But getting an app made and running was never the hard part. Before &lt;strong&gt;AI or vibe coding&lt;/strong&gt; we had platforms like &lt;strong&gt;Fiverr/UpWork&lt;/strong&gt; where people could just pay a third world country developer &lt;strong&gt;$50-$100&lt;/strong&gt; to build them their app.&lt;/p&gt;

&lt;p&gt;So getting a &lt;strong&gt;working APP&lt;/strong&gt; or at least &lt;strong&gt;MVP&lt;/strong&gt; was never really very hard or expensive rather its the &lt;strong&gt;distribution&lt;/strong&gt; part that kills 99% of the apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And for handling complex apps or codebases &lt;strong&gt;cheap developers&lt;/strong&gt; were not reliable then, &lt;strong&gt;AI tools&lt;/strong&gt; are not reliable now!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>discuss</category>
      <category>coding</category>
    </item>
    <item>
      <title>Frontend rate limiting can save you $10,000</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 21 May 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/0shuvo0/frontend-rate-limiting-can-save-you-10000-272l</link>
      <guid>https://dev.to/0shuvo0/frontend-rate-limiting-can-save-you-10000-272l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Fastest Requests Are the Ones You Never Make&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While backend rate limiting is absolutely crucial, &lt;strong&gt;it comes at a cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For backend rate limiting, we usually store a user's IP and request timestamps in &lt;strong&gt;memory&lt;/strong&gt; or a &lt;strong&gt;Redis database&lt;/strong&gt;. If your app has millions of users, you end up storing a lot of data in memory or a database — and that can get expensive fast.&lt;/p&gt;

&lt;p&gt;But what if &lt;strong&gt;we block requests &lt;em&gt;before&lt;/em&gt; they ever leave the browser/client&lt;/strong&gt;? By handling rate limiting on the client side, we can save a significant number of unnecessary request and read/write operations on the backend. Depending on how many users your app has, this can translate to anywhere between &lt;strong&gt;hundreds to thousands of dollars saved per year&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1: Basic frontend rate limiter example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/rate-limiter.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Maximum number of requests allowed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Time window in milliseconds (1 minute)&lt;/span&gt;

&lt;span class="c1"&gt;// Store timestamps of recent requests&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="c1"&gt;// Check if a new request is within the allowed limit&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove timestamps that have fallen outside the time window&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Drop-in fetch wrapper with rate limiting&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rate limit exceeded. Please try again later.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Better rate limiter with cooldown time and localstorage implementation so user can't just reset limit by refreshing the page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Front end rate limiter&lt;/span&gt;
&lt;span class="c1"&gt;// Max number of requests allowed per time window&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Maximum number of requests allowed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Time window in milliseconds (e.g., 1 minute)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COOLDOWN_TIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Time to wait after hitting the rate limit before requests are allowed again&lt;/span&gt;

&lt;span class="c1"&gt;// localStorage keys for persisting state across page refreshes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limiter_timestamps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limiter_cooldown_until&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- localStorage helpers ---&lt;/span&gt;

&lt;span class="c1"&gt;// Load request timestamps from localStorage, falling back to an empty array&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Persist the current timestamps array to localStorage&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Silently fail if localStorage is unavailable (e.g. private browsing restrictions)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Load the cooldown expiry timestamp (ms since epoch), or 0 if none is set&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Persist the cooldown expiry timestamp to localStorage&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Silently fail if localStorage is unavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- Core rate-limit logic ---&lt;/span&gt;

&lt;span class="c1"&gt;// Function to check if a request can be made&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// If an active cooldown exists, block all requests until it expires&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cooldownUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cooldownUntil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cooldownUntil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Load persisted timestamps so a page refresh doesn't reset the counter&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove timestamps that are outside the time window&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Persist the cleaned-up timestamps&lt;/span&gt;
    &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the number of requests in the time window is less than the maximum allowed&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Function to make a request&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Inform the caller how long (in seconds) they need to wait&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rate limit exceeded. Please try again in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Load, update, and persist timestamps before making the request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// If this request tips us over the limit, start a cooldown period.&lt;/span&gt;
    &lt;span class="c1"&gt;// The cooldown begins NOW and lasts COOLDOWN_TIME ms, after which&lt;/span&gt;
    &lt;span class="c1"&gt;// canMakeRequest() will unblock and the sliding window will have cleared.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;saveCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;COOLDOWN_TIME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Make the actual fetch request&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Whenever you need to make a request to the backend, just use &lt;code&gt;$fetch&lt;/code&gt; instead of the native &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example usage&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utils/rate-limiter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/get-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; This will prevent the vast majority of users from flooding your backend, but a determined minority and AI bots may attempt to bypass client-side checks entirely. &lt;strong&gt;Backend rate limiting must still be in place&lt;/strong&gt; as your true line of defense. Think of frontend rate limiting as an optimization layer, not a security layer.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  When to Use Frontend Rate Limiting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your app has a high volume of users and you want to reduce unnecessary backend load&lt;/li&gt;
&lt;li&gt;The endpoint being called is expensive (e.g., hits a third-party API, runs a heavy DB query)&lt;/li&gt;
&lt;li&gt;You want to improve UX by giving users instant feedback instead of waiting for a backend &lt;code&gt;429&lt;/code&gt; response&lt;/li&gt;
&lt;li&gt;You want to reduce cloud costs on services that charge per request or per DB operation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;Not&lt;/em&gt; to Use Frontend Rate Limiting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;As a &lt;strong&gt;replacement&lt;/strong&gt; for backend rate limiting — it is never a substitute&lt;/li&gt;
&lt;li&gt;For protecting sensitive or security-critical endpoints (authentication, payments, etc.)&lt;/li&gt;
&lt;li&gt;When your users are primarily API consumers or developers who interact directly with your backend&lt;/li&gt;
&lt;li&gt;When the client environment is untrusted (e.g., mobile apps that can be reverse-engineered or patched)&lt;/li&gt;
&lt;li&gt;When accurate rate limiting is a hard requirement — client-side state can be reset simply by refreshing the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>coding</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
