<?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>Forem: Jean Michael Mayer</title>
    <description>The latest articles on Forem by Jean Michael Mayer (@jeanmmayer).</description>
    <link>https://forem.com/jeanmmayer</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1217275%2F15947f82-c327-4584-978d-001e526a46e1.jpeg</url>
      <title>Forem: Jean Michael Mayer</title>
      <link>https://forem.com/jeanmmayer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jeanmmayer"/>
    <language>en</language>
    <item>
      <title>Show Dev: I Built an AI Gift Whisperer in a Weekend</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Fri, 24 Apr 2026 17:09:10 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-i-built-an-ai-gift-whisperer-in-a-weekend-4eng</link>
      <guid>https://forem.com/jeanmmayer/show-dev-i-built-an-ai-gift-whisperer-in-a-weekend-4eng</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody wants to admit
&lt;/h2&gt;

&lt;p&gt;Gift-giving is a search problem with terrible inputs. You know your sister is 34, into pottery, allergic to cilantro (somehow relevant), and hates anything monogrammed. What you &lt;em&gt;don't&lt;/em&gt; know is what to buy her that won't end up in a drawer.&lt;/p&gt;

&lt;p&gt;Existing "gift finder" sites are SEO farms with affiliate links and a quiz that asks if the recipient is "fun" or "practical." Cool, thanks.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://gift-whisperer.edgecasefactory.com" rel="noopener noreferrer"&gt;The Gift Whisperer&lt;/a&gt;. You describe someone you love in plain English, and it returns 12 real gift ideas, an illustrated card, and a message that actually sounds like you wrote it (but better).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's a separate service
&lt;/h2&gt;

&lt;p&gt;I run a few small apps under one umbrella. My first instinct was to bolt this onto the monolith as another route. I didn't.&lt;/p&gt;

&lt;p&gt;The Gift Whisperer does three things that the rest of my stack doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-running LLM calls (sometimes 15–30 seconds for the full payload)&lt;/li&gt;
&lt;li&gt;Image generation that spikes memory&lt;/li&gt;
&lt;li&gt;Bursty traffic around holidays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sharing a process with my normal CRUD apps means one slow gift request blocks everything else. So it lives in its own Railway service with its own scaling rules and its own crash blast radius. If it dies on Mother's Day, the rest of my stuff keeps humming.&lt;/p&gt;

&lt;p&gt;Isolation is underrated. Microservices are overrated. The truth is boring: &lt;strong&gt;put the weird workload in its own box.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt is the product
&lt;/h2&gt;

&lt;p&gt;The actual interesting engineering here isn't the framework choice (Next.js, shocker). It's the prompt pipeline.&lt;/p&gt;

&lt;p&gt;A single "give me 12 gifts" prompt produces garbage — either generic Amazon bestseller slop or bizarre hallucinations ("a handcrafted ferret hammock"). So I split it into stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract traits&lt;/strong&gt; from the free-text description into a structured profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate candidates&lt;/strong&gt; across price tiers and categories in parallel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-rank and dedupe&lt;/strong&gt; against the original description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate the card + message&lt;/strong&gt; using the refined profile, not the raw input&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stage 2 looks roughly 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;tiers&lt;/span&gt; &lt;span class="o"&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;under_25&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;under_75&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;splurge&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;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tiers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;openai&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="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json_object&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="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;system&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;GIFT_SYSTEM_PROMPT&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;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;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&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="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;ideas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="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="nx"&gt;gifts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parallelizing per-tier cut total latency roughly in half and — more importantly — forces variety. A single call to "give me 12 ideas across budgets" consistently clusters around the middle tier. Splitting the constraint into separate calls fixes that without any clever reranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The illustrated card was the hard part
&lt;/h2&gt;

&lt;p&gt;LLMs are great at text. Image models are great at images. Getting them to agree on &lt;em&gt;what the gift actually looks like&lt;/em&gt; is the hard part.&lt;/p&gt;

&lt;p&gt;I pass the top-ranked gift idea plus a stripped-down style prompt to the image model. No recipient details, no names, no "make it feel warm and personal" — image models interpret that as cursed stock photography. Constraints beat vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "a message that slaps" matters
&lt;/h2&gt;

&lt;p&gt;The gift ideas are table stakes. The note is the moat. I spent more time tuning the message prompt than anything else, because a good gift with a generic card is still forgettable. The message prompt explicitly avoids words like &lt;em&gt;journey&lt;/em&gt;, &lt;em&gt;cherish&lt;/em&gt;, and &lt;em&gt;blessed&lt;/em&gt;. You're welcome.&lt;/p&gt;

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

&lt;p&gt;Got someone hard to shop for? Go describe them:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://gift-whisperer.edgecasefactory.com" rel="noopener noreferrer"&gt;gift-whisperer.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free to try. Takes about 30 seconds. Bring receipts if it nails it — I want to know what worked and what didn't.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Show Dev: An Idle Tycoon Game Where You Can't Click</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Thu, 23 Apr 2026 23:52:01 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-an-idle-tycoon-game-where-you-cant-click-59gi</link>
      <guid>https://forem.com/jeanmmayer/show-dev-an-idle-tycoon-game-where-you-cant-click-59gi</guid>
      <description>&lt;h2&gt;
  
  
  The dumbest game I've ever shipped
&lt;/h2&gt;

&lt;p&gt;I made a tycoon game where clicking does nothing. There are no upgrade buttons, no prestige loops, no "slap the cookie" dopamine. You open the tab, and numbers go up. That's it. That's the game.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;The Lazy Tycoon&lt;/strong&gt;, and it's the purest expression of the idle genre: the player has been removed entirely.&lt;/p&gt;

&lt;p&gt;This started as a bit. I was complaining that "idle" games aren't really idle — they demand more clicks per minute than most action games. So I built the logical endpoint: a game that plays itself while you watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why remove the player?
&lt;/h2&gt;

&lt;p&gt;Every idle game I've played eventually turns into spreadsheet homework. You sit there tapping a prestige button every 47 minutes because the meta demands it. The "idle" part is a lie told by the onboarding tutorial.&lt;/p&gt;

&lt;p&gt;By removing input entirely, a few interesting things happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The game has to be &lt;strong&gt;legible&lt;/strong&gt;. If you can't steer, every number on screen has to explain itself.&lt;/li&gt;
&lt;li&gt;The pacing has to feel &lt;strong&gt;alive&lt;/strong&gt; without rewarding attention. Watching should be optional.&lt;/li&gt;
&lt;li&gt;There's no failure state to design around. Just vibes and compounding interest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It turns out watching numbers grow is surprisingly relaxing when you've accepted you can't do anything about it. It's the Bob Ross of incremental games.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing is AI-generated
&lt;/h2&gt;

&lt;p&gt;I didn't hand-write the game logic. The entire app — the economy curves, the business names, the tick loop, the UI — was generated and then iterated on with an LLM in the loop. I gave it constraints ("no player input, must feel alive, numbers must compound believably") and let it cook.&lt;/p&gt;

&lt;p&gt;This is part of a larger experiment: I run a little factory of these apps, each one generated and deployed on its own isolated &lt;strong&gt;Railway&lt;/strong&gt; service. One app, one container, one subdomain. If a generation goes sideways, the blast radius is one silly game.&lt;/p&gt;

&lt;p&gt;The tick loop is about as boring as you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nf"&gt;setEmpire&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businesses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cash&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;income&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;businesses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;maybeAutoUpgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businesses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cash&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;income&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="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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only "decision" the game makes is &lt;code&gt;maybeAutoUpgrade&lt;/code&gt; — a tiny heuristic that reinvests cash into whichever business has the best ROI. It's a fake CEO running on a single &lt;code&gt;setInterval&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weird choices I'd defend in a code review
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No persistence.&lt;/strong&gt; Close the tab, lose your empire. This is a feature — it forces the app to be an experience, not an obligation. No FOMO, no save-scumming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One service per app.&lt;/strong&gt; Each silly thing I generate gets its own Railway deployment. Overkill? Absolutely. But it means I can nuke or redeploy one without touching the others, and cold-start cost is basically zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-only state.&lt;/strong&gt; No backend. The economy lives entirely in React state. If you open two tabs you get two universes, which is philosophically correct for a game about doing nothing.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;When you take away interaction, UI design becomes really honest. Every pixel has to earn its place because the player has nothing to do except look at it. I ended up cutting about half the HUD I originally generated — the remaining half got better for it.&lt;/p&gt;

&lt;p&gt;Also: generating small, weird apps and shipping each to its own isolated service is &lt;em&gt;way&lt;/em&gt; more fun than maintaining one big monorepo of jokes.&lt;/p&gt;

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

&lt;p&gt;Open the tab. Do nothing. Get rich (in fake money).&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://lazy-tycoon.edgecasefactory.com" rel="noopener noreferrer"&gt;lazy-tycoon.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you find yourself instinctively reaching for the mouse, congratulations — you've identified the problem the game is satirizing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Show Dev: A boring corporate site hiding 30 easter eggs</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:12:56 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-a-boring-corporate-site-hiding-30-easter-eggs-5pm</link>
      <guid>https://forem.com/jeanmmayer/show-dev-a-boring-corporate-site-hiding-30-easter-eggs-5pm</guid>
      <description>&lt;p&gt;I built a website that looks like the most generic corporate homepage you've ever scrolled past. Stock photos of people in blazers shaking hands. "Synergy-driven solutions." A newsletter signup nobody asked for.&lt;/p&gt;

&lt;p&gt;It's also hiding 30 easter eggs. Your job is to find them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build a boring website on purpose
&lt;/h2&gt;

&lt;p&gt;Most portfolio and marketing sites try to impress you in the first two seconds. Parallax, WebGL, a hero animation that eats your GPU. I wanted to do the opposite: build something so aggressively mid that your brain glazes over it — and then reward the people who poke at it anyway.&lt;/p&gt;

&lt;p&gt;It's basically a love letter to the weird HTML comments, Konami codes, and hidden dev-console messages that used to be all over the web. The premise is simple: everything that looks suspicious probably is. Click the logo five times. Look at the page source. Read the 404 page carefully. Try entering &lt;code&gt;up up down down&lt;/code&gt; somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack is dumb on purpose
&lt;/h2&gt;

&lt;p&gt;This is a Next.js app deployed as its own service on Railway, sitting behind a subdomain of my side-project umbrella. No database. No auth. Progress is tracked in &lt;code&gt;localStorage&lt;/code&gt;, which means yes — you can cheat, and I don't care.&lt;/p&gt;

&lt;p&gt;The egg registry is a single typed object. Each egg has an id, a trigger, and a discovery payload. The UI subscribes to a global &lt;code&gt;EggContext&lt;/code&gt; so anywhere in the app can fire a discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEggs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
  &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logo-clicker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-bold"&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  MegaCorp Global Solutions
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context handles deduping, the toast animation, and persisting the found set. Adding a new egg is basically one entry in the registry plus a trigger wired up wherever it lives — a component, a keyboard listener, a route, a response header, whatever.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI did most of the heavy lifting
&lt;/h2&gt;

&lt;p&gt;I'll be honest: I did not hand-write 30 easter eggs and a full fake corporate site. The scaffolding, the filler copy ("Empowering tomorrow's tomorrow, today"), the stock-photo-shaped placeholders, the testimonials from "Jennifer, VP of Alignment" — all AI-generated, then curated. The eggs themselves were a back-and-forth: I'd prompt for a category (keyboard, DOM, network, cursor, timing) and pick the ones that actually made me smile.&lt;/p&gt;

&lt;p&gt;This is where AI code generation shines, by the way. Not "build me a SaaS." More like "give me 10 weird but tasteful ways to hide a secret in a React page," then you, the human, act as the taste filter. The ratio was roughly 70% generated, 30% me fixing the three that didn't actually work and rewriting the jokes that weren't funny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases I enjoyed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One egg lives in a custom HTTP response header. You have to open the Network tab.&lt;/li&gt;
&lt;li&gt;One requires you to do &lt;em&gt;nothing&lt;/em&gt; for 90 seconds.&lt;/li&gt;
&lt;li&gt;One is in the &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One involves the page title when the tab is backgrounded.&lt;/li&gt;
&lt;li&gt;One is only reachable on mobile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few of them are genuinely mean. I'm sorry. Not that sorry.&lt;/p&gt;

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

&lt;p&gt;Go hunt: &lt;strong&gt;&lt;a href="https://easter-egg-excavation.edgecasefactory.com" rel="noopener noreferrer"&gt;easter-egg-excavation.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting to 30 is harder than it sounds. If you find them all, the final screen is worth it — or at least, I think it is. Let me know in the comments how many you got without opening DevTools. No judgment if that number is zero.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Show Dev: I made a glassblowing sim you blow into</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Thu, 23 Apr 2026 15:21:56 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-i-made-a-glassblowing-sim-you-blow-into-d0</link>
      <guid>https://forem.com/jeanmmayer/show-dev-i-made-a-glassblowing-sim-you-blow-into-d0</guid>
      <description>&lt;h2&gt;
  
  
  Why blow into your laptop?
&lt;/h2&gt;

&lt;p&gt;I've been building a series of tiny, weird web toys under the banner of "edge case factory" — apps that exist mostly because nobody asked for them. The latest one is &lt;a href="https://glassblowers-breath.edgecasefactory.com" rel="noopener noreferrer"&gt;Glassblower's Breath&lt;/a&gt;, a browser-based glassblowing simulator where you shape a virtual vase by literally exhaling into your microphone.&lt;/p&gt;

&lt;p&gt;One breath. One vase. No undo.&lt;/p&gt;

&lt;p&gt;The whole thing started from a dumb question: what if the input to a creative tool was something you can't really control precisely? Mouse input is too deliberate. Keyboards are too discrete. But breath? Breath is messy, noisy, and deeply analog. It felt like the right kind of input for molten glass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning breath into geometry
&lt;/h2&gt;

&lt;p&gt;The mic pipeline is embarrassingly simple. Grab an audio stream, run it through an &lt;code&gt;AnalyserNode&lt;/code&gt;, and sample the low-frequency RMS to estimate breath intensity. Plosives and speech get filtered out by looking at spectral flatness — breath is broadband noise, speech has formants.&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&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;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMediaStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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;analyser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnalyser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&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;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sampleBreath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFloatTimeDomainData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&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;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rms&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;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;buf&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="c1"&gt;// map rms to radial displacement on the current vase ring&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rms&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&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;Each frame, the current "ring" of the vase (it's built bottom-up, lathe-style) expands proportional to your breath. Stop breathing and the ring sets. The next ring starts slightly above. After ~15 seconds you've got a silhouette that's unmistakably yours — shaky inhales become pinched necks, a strong exhale makes a bulb.&lt;/p&gt;

&lt;p&gt;The mesh is a revolved spline in Three.js with a refractive shader that cheats hard: a cubemap of a studio environment, a fresnel term, and some chromatic aberration on the edges. It's not physically accurate glass. It looks like glass from across a room, which is all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it lives on its own Railway service
&lt;/h2&gt;

&lt;p&gt;This app was almost entirely AI-generated — I scaffolded it by describing the behavior I wanted and iterating on the shader and the breath-detection heuristics. That workflow produces code fast, but it also produces code I don't fully trust to share a process with anything important.&lt;/p&gt;

&lt;p&gt;So every edge-case-factory app gets its own Railway service on its own subdomain. Isolated deploys, isolated dependencies, isolated blast radius. If &lt;code&gt;glassblowers-breath&lt;/code&gt; leaks memory or the shader pins a GPU somewhere, it doesn't take down the neighbors. Each app is a Next.js project with essentially zero shared code — I gave up on the monorepo dream after the second app.&lt;/p&gt;

&lt;p&gt;The tradeoff is obvious: more services, more cold starts, more dashboards. The upside is that I can ship a weird thing in an afternoon and genuinely not care if it catches fire at 3am. For toys, that calculus is correct. For a real product, it wouldn't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one UX decision I'm proud of
&lt;/h2&gt;

&lt;p&gt;There's no "try again" button during a blow. Once you start, you're committed for the full duration. You can save the result or discard it, but you can't pause mid-breath to reconsider. This is annoying. It's also the entire point — real glassblowers can't pause either. The constraint is the feature.&lt;/p&gt;

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

&lt;p&gt;Put on headphones (mic feedback is real), find a quiet room, and make a vase with your lungs:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://glassblowers-breath.edgecasefactory.com" rel="noopener noreferrer"&gt;glassblowers-breath.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you make something beautiful or cursed, I'd love to see it.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Show Dev: I turned screen time reports into 3D trophies</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Mon, 20 Apr 2026 16:45:16 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-i-turned-screen-time-reports-into-3d-trophies-4hf0</link>
      <guid>https://forem.com/jeanmmayer/show-dev-i-turned-screen-time-reports-into-3d-trophies-4hf0</guid>
      <description>&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;I was doom-scrolling at 1am, and iOS politely informed me I'd spent 4h 12min on Reddit that day. Instead of, you know, changing my life, I built an app that makes fun of me for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Feed Autopsy&lt;/strong&gt; takes your screen time screenshot, has an AI gossip columnist read you for filth, and then mints a little 3D shame trophy you can spin around in your browser. It's therapy, but worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a gossip columnist?
&lt;/h2&gt;

&lt;p&gt;Most "digital wellness" apps are insufferable. They chirp things like &lt;em&gt;"You used your phone 23% more this week! Let's set a goal! 🌱"&lt;/em&gt; and I want to throw my laptop into the sea.&lt;/p&gt;

&lt;p&gt;Shame, delivered with theatrical flair, works better on me. So the prompt doesn't ask for advice — it asks for &lt;em&gt;dirt&lt;/em&gt;. The model plays a columnist who has seen your TikTok time and is now whispering about it at brunch. The tone constraint ended up being the hardest part of the prompt engineering: "mean but fun" is a narrow band. Too soft and it's a Hallmark card. Too harsh and it's genuinely upsetting at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vision pipeline
&lt;/h2&gt;

&lt;p&gt;Screen time reports are images, and the formats vary wildly (iOS, Android, different locales, people cropping weirdly). Instead of OCR + regex hell, I just hand the image to a multimodal model and ask it to extract structured JSON:&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="s2"&gt;/api/autopsy&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="s2"&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;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the screenshot&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;apps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalMinutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trophyPrompt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call returns both the extracted data &lt;em&gt;and&lt;/em&gt; the roast, which keeps latency down to one round trip. A second prompt generates a short &lt;code&gt;trophyPrompt&lt;/code&gt; — a physical description of what the shame trophy should look like, based on the worst offender. ("A bronze thumb, calloused, mounted on a marble base engraved INSTAGRAM 2h 47m.")&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3D trophy
&lt;/h2&gt;

&lt;p&gt;The trophy renders with &lt;strong&gt;react-three-fiber&lt;/strong&gt;. I'm not generating mesh geometry from scratch — that way lies madness. Instead I have a small library of base trophy shapes (pedestal, plaque, figure slot) and the AI's description drives materials, colors, engraved text, and which figure sits on top. It's basically a Mad Libs for 3D.&lt;/p&gt;

&lt;p&gt;You can drag to spin it. That's the whole feature. It is deeply unserious and I love it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why its own Railway service?
&lt;/h2&gt;

&lt;p&gt;The main site is a Next.js app, but Feed Autopsy runs as an isolated service on Railway with its own subdomain. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius.&lt;/strong&gt; Image uploads + LLM calls + occasional 3D asset generation is a different risk profile than my blog. If someone figures out how to make it expensive, I want to kill &lt;em&gt;that&lt;/em&gt; service, not the whole house.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start honesty.&lt;/strong&gt; Each experiment gets its own container, its own logs, its own budget alarm. When I inevitably abandon it in six months, I can nuke it without touching anything else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each app in my little factory of weird projects is deployed this way. It's more infra than a monorepo on Vercel would be, but it's made me way more willing to ship dumb ideas, because dumb ideas can't hurt the smart ones.&lt;/p&gt;

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

&lt;p&gt;Upload a screen time screenshot. Get roasted. Keep the trophy.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://feed-autopsy.edgecasefactory.com" rel="noopener noreferrer"&gt;feed-autopsy.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No signup, no email capture, no "premium tier." Just vibes and mild psychological damage. Tell me what the columnist said about you — I want to know if it's landing or if I need to make it meaner.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Show Dev: I Built a Dither Oracle That Roasts Your Soul</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Mon, 20 Apr 2026 16:31:14 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-i-built-a-dither-oracle-that-roasts-your-soul-2fgm</link>
      <guid>https://forem.com/jeanmmayer/show-dev-i-built-a-dither-oracle-that-roasts-your-soul-2fgm</guid>
      <description>&lt;h2&gt;
  
  
  The problem: I can't decide what to eat for lunch
&lt;/h2&gt;

&lt;p&gt;I have a well-documented inability to make small decisions. Big ones I'm fine with — career moves, moving countries, sure. But "Thai or Vietnamese?" at 12:47pm? I will stand in the middle of the sidewalk until I dissolve.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://dither-oracle.edgecasefactory.com" rel="noopener noreferrer"&gt;The Dither Oracle&lt;/a&gt;. You roll some dice, it gives you an answer, and then — because an answer alone is not enough for anyone in 2025 — it generates a dithered, black-and-white "soul portrait" to accompany the verdict. It's marketed, per the website, "AS SEEN ON TV!" It has not actually been seen on TV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why dithering?
&lt;/h2&gt;

&lt;p&gt;Because color is a commitment and I am already paralyzed enough.&lt;/p&gt;

&lt;p&gt;More seriously: dithering is one of those aesthetic constraints that makes cheap things look intentional. A crisp 1-bit image with Floyd–Steinberg or Atkinson diffusion has this early-Macintosh, zine-photocopy energy that masks a lot of sins. Including, say, the sins of a generative model that occasionally produces sixth fingers.&lt;/p&gt;

&lt;p&gt;The pipeline is roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User rolls dice → seed + vibe string&lt;/li&gt;
&lt;li&gt;Prompt gets assembled and sent to an image model&lt;/li&gt;
&lt;li&gt;Image comes back, gets downscaled and quantized to 1-bit&lt;/li&gt;
&lt;li&gt;Error-diffusion dither on a Canvas, then blitted to the page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dithering itself runs client-side. There's no reason to pay for CPU cycles to push pixels around when the user's laptop is sitting there idle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollOracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dice&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;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="s2"&gt;/api/oracle&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="s2"&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="nx"&gt;dice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bitmap&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;loadImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&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="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;atkinsonDither&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The weird infrastructure choice
&lt;/h2&gt;

&lt;p&gt;The Oracle lives in its own Railway service, separate from the rest of my apps. This was not the plan. The plan was "throw it on the same Next.js app as everything else."&lt;/p&gt;

&lt;p&gt;But image generation is bursty and slow. A single roll can tie up a request for 5–15 seconds waiting on the model. If I colocate that with a normal webapp, one viral tweet means everybody's unrelated dashboards start timing out because the Node process is babysitting dice rolls.&lt;/p&gt;

&lt;p&gt;So: isolated service, its own queue, its own env, its own crash radius. When the Oracle falls over (and it does — models hiccup, rate limits happen), the blast zone is exactly one dumb dice app. The main site stays up. This is the boring-but-correct answer nobody tells you at the start of a side project, and I only learned it after the third time I DDoS'd myself with my own hobby traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  On letting the AI be weird
&lt;/h2&gt;

&lt;p&gt;I had a choice: tightly constrain the prompts so the output is "on brand," or leave enough slack for the model to surprise me. I went with slack. The Oracle's verdicts range from gently encouraging to mildly menacing, and the portraits are sometimes a crow, sometimes a hand holding a peach, sometimes a geometry problem. That variance &lt;em&gt;is&lt;/em&gt; the product. A deterministic oracle is just a function. An unpredictable one is a bit.&lt;/p&gt;

&lt;p&gt;The dither pass flattens all of this into the same aesthetic register, which is the other reason I chose it. No matter how feral the model gets, it comes out looking like it belongs on a flyer stapled to a telephone pole in 1994.&lt;/p&gt;

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

&lt;p&gt;Roll the dice here: &lt;strong&gt;&lt;a href="https://dither-oracle.edgecasefactory.com" rel="noopener noreferrer"&gt;dither-oracle.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's free. It's fast. It will tell you whether to text them back. I take no responsibility for the outcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Show Dev: A Dragon That Burns Your Village If You Misspell</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Sun, 19 Apr 2026 20:35:05 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-a-dragon-that-burns-your-village-if-you-misspell-37db</link>
      <guid>https://forem.com/jeanmmayer/show-dev-a-dragon-that-burns-your-village-if-you-misspell-37db</guid>
      <description>&lt;h2&gt;
  
  
  The pitch
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://dragons-dictionary.edgecasefactory.com" rel="noopener noreferrer"&gt;The Dragon's Dictionary&lt;/a&gt;: a bilingual (English/Portuguese) word game where a hoarding dragon eats words you feed it. Spell them right, the dragon is pleased and adds them to its hoard. Spell them wrong, and it torches your village.&lt;/p&gt;

&lt;p&gt;It started as a joke about how Duolingo's owl is menacing but polite. I wanted something that felt genuinely unhinged — a creature that cares more about its word collection than your feelings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a dragon, why two languages
&lt;/h2&gt;

&lt;p&gt;I'm learning Portuguese. Most vocab apps treat bilingual learners like monolinguals who happen to own a dictionary. I wanted a game where switching languages mid-session is the point, not a settings toggle buried three menus deep.&lt;/p&gt;

&lt;p&gt;The dragon also solves a UX problem I keep hitting with language apps: stakes. Flashcards have no stakes. A burning village has stakes. The cognitive load of "don't kill the villagers" turns out to be a surprisingly effective mnemonic device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird technical choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's almost entirely AI-generated.&lt;/strong&gt; Not "AI-assisted" — I mean the dragon's reactions, the villager names, the insults it hurls when you fail, are generated on demand. I keep a small corpus of base words and let the model riff on reactions per session. This is why the dragon feels inconsistent in a charming way: it is inconsistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs in its own Railway service.&lt;/strong&gt; I have a small constellation of side projects under &lt;code&gt;edgecasefactory.com&lt;/code&gt;, and I used to cram them all into one monorepo behind one Next.js deployment. That was a mistake. One project's dependency bump would break two others. Now each dragon, factory, and whatever-else gets its own Railway service with its own lifecycle. Deploys take 40 seconds and I stopped dreading &lt;code&gt;npm update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No database for game state.&lt;/strong&gt; The dragon's hoard lives in &lt;code&gt;localStorage&lt;/code&gt;. I considered Postgres, Redis, SQLite-on-a-volume, the works. Then I remembered this is a game about a dragon and nobody needs their village-burning history to sync across devices. Deleting the database was the single best architectural decision I made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring loop
&lt;/h2&gt;

&lt;p&gt;Here's the core validation call. The model grades the word, returns a reaction, and decides the dragon's mood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;feedDragon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&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;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/feed&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="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hoard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getHoard&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;villagersLost&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;addToHoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;burnVillage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;villagersLost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server-side check does two things: a dictionary lookup (cheap, deterministic) and an LLM call for the reaction text (expensive, flavorful). If the LLM times out, the dragon just grunts. Graceful degradation, dragon-style.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;Portuguese has a lot of accented characters that are easy to get wrong on a US keyboard. I ended up adding a "close enough" mode because forcing learners to type &lt;code&gt;ção&lt;/code&gt; correctly on first try felt more like a bureaucratic form than a game. The dragon now accepts &lt;code&gt;cao&lt;/code&gt; but makes fun of you for it.&lt;/p&gt;

&lt;p&gt;Also: players burn their own village on purpose. A lot. I did not design for this. I now embrace it.&lt;/p&gt;

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

&lt;p&gt;🐉 &lt;strong&gt;&lt;a href="https://dragons-dictionary.edgecasefactory.com" rel="noopener noreferrer"&gt;dragons-dictionary.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feed the dragon. Don't feed it gibberish. Or do — the villagers signed waivers.&lt;/p&gt;

&lt;p&gt;Feedback welcome, especially from Portuguese speakers who want to tell me my conjugation tables are wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sideprojects</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Show Dev: Your Face Is Now a Music Generator</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Sun, 19 Apr 2026 19:27:43 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-your-face-is-now-a-music-generator-4pm7</link>
      <guid>https://forem.com/jeanmmayer/show-dev-your-face-is-now-a-music-generator-4pm7</guid>
      <description>&lt;h2&gt;
  
  
  The Dumbest Idea I've Shipped This Month
&lt;/h2&gt;

&lt;p&gt;I built a thing called &lt;a href="https://mood-ring-playlist.edgecasefactory.com" rel="noopener noreferrer"&gt;The Mood Ring Playlist&lt;/a&gt;. It points your webcam at your face, reads your emotions, and generates a live music track that shifts as you do.&lt;/p&gt;

&lt;p&gt;Smile? Tempo kicks up, major key, bright pads. Scowl at your code review? Minor key, slower, a little more low end. Zone out completely? It drifts into ambient territory.&lt;/p&gt;

&lt;p&gt;This article is the "why would you do this" post, plus a couple of technical choices that turned out weirder than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build This
&lt;/h2&gt;

&lt;p&gt;I wanted to play with two things at once: browser-based face detection and procedural audio. Every "AI music" demo I've seen is either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a 30-second clip, wait, listen, regenerate.&lt;/li&gt;
&lt;li&gt;Upload a giant model to the browser and pray the user's laptop doesn't melt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted something continuous. Not "click to generate a song" but "the song &lt;em&gt;is&lt;/em&gt; you, right now." That framing changed every architectural decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Face In The Browser, Music On The Server
&lt;/h2&gt;

&lt;p&gt;Face detection runs entirely client-side. Webcam frames → face landmarks → a small vector of emotion probabilities (happy, sad, angry, surprised, neutral, etc.). None of that leaves your machine. This is both a privacy win and a latency win — I don't want to ship webcam frames across the internet just to find out you're mildly annoyed.&lt;/p&gt;

&lt;p&gt;The music generation, though, lives in its own Railway service. That was deliberate. The main Next.js app is a thin frontend. The generator is a separate long-running Node service that holds state, manages the synthesis graph, and streams parameter updates based on the emotion vector it receives.&lt;/p&gt;

&lt;p&gt;Why split it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts would kill the vibe.&lt;/strong&gt; Serverless functions booting mid-song is not a vibe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's stateful.&lt;/strong&gt; The generator needs memory of what was just playing so transitions don't sound like someone slapping the radio dial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I can redeploy the UI without killing the audio.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client just posts emotion snapshots and receives back instructions for what the Web Audio API should do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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="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;emotions&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="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;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/mood&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="nx"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&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="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;tempo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layers&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="nx"&gt;mixer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tempo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;250&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 250ms debounce matters. Early versions updated every frame and the music sounded like a panic attack. Humans don't actually change mood 60 times a second — who knew.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Part, Honestly
&lt;/h2&gt;

&lt;p&gt;The "AI" here isn't a giant model generating waveforms. It's a smaller system that maps an emotion vector to musical parameters: scale, tempo range, instrument layer weights, chord progression tendencies. Think of it as a learned policy over a procedural music engine, not Suno-in-a-box. That's how it stays real-time.&lt;/p&gt;

&lt;p&gt;I tried the "just call a big model every few seconds" route first. It sounded great when it worked and terrible when it stitched. Seams everywhere. The procedural-with-a-smart-director approach wins because continuity matters more than novelty in ambient music.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known Weirdness
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you wear glasses with heavy reflections, it thinks you're surprised a lot.&lt;/li&gt;
&lt;li&gt;If you laugh hard enough to close your eyes, it briefly assumes you left.&lt;/li&gt;
&lt;li&gt;Lighting matters more than I'd like. Sorry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Give it your face for a minute. See what you sound like today.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://mood-ring-playlist.edgecasefactory.com" rel="noopener noreferrer"&gt;mood-ring-playlist.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Headphones recommended. Webcam required. Judgment optional.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
