<?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: AsiMir</title>
    <description>The latest articles on DEV Community by AsiMir (@asiboy6789).</description>
    <link>https://dev.to/asiboy6789</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%2F237053%2F55ca5dc7-646d-46b1-beb0-aa0503ae15a4.jpeg</url>
      <title>DEV Community: AsiMir</title>
      <link>https://dev.to/asiboy6789</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/asiboy6789"/>
    <language>en</language>
    <item>
      <title>Four Birthdays, Two Family Dinners, and One Extremely Expensive December</title>
      <dc:creator>AsiMir</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:07:00 +0000</pubDate>
      <link>https://dev.to/asiboy6789/four-birthdays-two-family-dinners-and-one-extremely-expensive-december-5cga</link>
      <guid>https://dev.to/asiboy6789/four-birthdays-two-family-dinners-and-one-extremely-expensive-december-5cga</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;p&gt;December 19, last year. I am in the supermarket line, holding a very large watermelon.&lt;/p&gt;

&lt;p&gt;In two days, twelve people come to our place for Yalda, the longest night of the year.&lt;/p&gt;

&lt;p&gt;I open my bank app. I close it. I open it again, as if the number might change the second time.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;Payday is January 1. I do the math. Then I stop doing the math.&lt;/p&gt;

&lt;p&gt;This is That Day. Every family has one.&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%2Frulr45a2t0xaxjh1c921.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%2Frulr45a2t0xaxjh1c921.png" alt="The bot's /forecast reply on December 1: likely spending ₺42,400, budget ₺30,000, Wall Day around Dec 19, biggest reason Yalda Night" width="720" height="1012"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;🧱 The bot is less dramatic. It calls That Day "Wall Day": the day the budget runs out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Ledgerly is a Telegram bot for lazy families. Send it receipt photos and family dates. It tells you when the money will run out, while you can still change it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17kio00iu8yrevtpe41i.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%2F17kio00iu8yrevtpe41i.gif" alt="The chat at 1.5× speed: the events, /forecast with its chart, and the gift question" width="480" height="675"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sped up: I tell the bot the family events, ask for &lt;code&gt;/forecast&lt;/code&gt;, then ask about a gift. The chart shows the likely path, the range and the budget line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A note before the story: to keep my family private, I changed the names, the birthdays and the money. The demo bot uses a sample year of spending I made for the demo. Money is in Turkish lira (₺). The bot, the screenshots and every test number are real.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My friend Ryan has typed every receipt into a spreadsheet, by hand, for years. Ryan has never had That Day.&lt;/p&gt;

&lt;p&gt;So I tried Ryan's method. I lasted four days.&lt;/p&gt;

&lt;p&gt;Some days I had more than 30 receipts. Skip three days and you are looking at a pile of a hundred. That is when you quietly give up.&lt;/p&gt;

&lt;p&gt;Ryan's method works. It just needs a Ryan. He is the tortoise in the old story: slow and steady, every day. I am only slow. So I wanted a bot to be steady for me.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where the money really went
&lt;/h3&gt;

&lt;p&gt;I looked back at December. The supermarket was normal. The calendar was not.&lt;/p&gt;

&lt;p&gt;Four birthdays, one of them my nephew Arman's. Yalda Night at our place, for twelve people. We stay up late with watermelon, pomegranates and poems. One more family dinner.&lt;/p&gt;

&lt;p&gt;Gifts, cakes, and two big grocery runs in one week. By That Day, the money was already at the cake shop.&lt;/p&gt;

&lt;p&gt;I could not cut any of it. You do not cancel a nephew's birthday.&lt;/p&gt;

&lt;p&gt;My wife knew every one of these dates in November.&lt;/p&gt;

&lt;p&gt;My bank app knows I paid at a supermarket. It does not know it was a watermelon for Yalda. And it only looks back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Families spend by the calendar. I did not need to type more. I needed to see the future, and I was too lazy to type even the past.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The lazy first version
&lt;/h3&gt;

&lt;p&gt;So I asked a lazy person's question: why not give this job to an AI?&lt;/p&gt;

&lt;p&gt;The first version was not even an app. It was a folder of photos and one instruction file for Claude Code.&lt;/p&gt;

&lt;p&gt;Then I wanted it on my phone, at the checkout, with the receipt still warm. And I thought: other lazy people might want this too. So for this challenge, I started again from zero, with a new repo.&lt;/p&gt;

&lt;p&gt;Turkish receipts write &lt;code&gt;1.234,56&lt;/code&gt;. Read it the wrong way, and a ₺1,234 birthday cake costs ₺1.23. On a long receipt, it looks believable. It is very wrong.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ledgerly
&lt;/h3&gt;

&lt;p&gt;Now I send the photo to a Telegram bot and put my phone away. That is my whole job.&lt;/p&gt;

&lt;p&gt;Gemma reads it. Then plain code adds up the items, because of that cake. Only then is the receipt filed.&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%2Fswbq2125hhsegjaygald.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%2Fswbq2125hhsegjaygald.png" alt="A filed receipt in Telegram: six items, total ₺1,800, " width="720" height="1012"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Code checked the sum. Gemma only read it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When it cannot read the date, it just asks.&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%2Foldfmv90d39nk16d73tw.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%2Foldfmv90d39nk16d73tw.png" alt="The bot asks " width="720" height="1012"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No clear date, no guessing. "Yesterday" is a fine answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same photo twice? The bot says: "You already sent me this one 🦥"&lt;/p&gt;
&lt;h3&gt;
  
  
  Seeing That Day coming
&lt;/h3&gt;

&lt;p&gt;December has not come yet. I am too lazy to wait, so I set the demo bot's clock to December 1, payday.&lt;/p&gt;

&lt;p&gt;In October, my wife gave me the December list early. I passed it to the bot, the way I would tell a friend:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Arman's birthday is Dec 14. We're hosting Yalda for 12 people."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I ask for &lt;code&gt;/forecast&lt;/code&gt;. It gives me the reply at the top of this post.&lt;/p&gt;

&lt;p&gt;My wife reads it over my shoulder. "Yalda," she says. "Of course it is Yalda."&lt;/p&gt;

&lt;p&gt;The bot says Wall Day comes around December 19. Last year I found out on that day, in the supermarket line. This year I ask first:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Can I afford a ₺5,000 gift for Arman?"&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A ₺5,000 gift for Arman on Dec 14 moves Wall Day from Dec 19 to Dec 17. Likely spending becomes ₺47,400 of your ₺30,000 budget.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So Arman's gift gets a little smaller. The watermelon stays the same size.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

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

&lt;p&gt;The demo bot only answers me, because spending data is private. Your own copy is one button and three API keys away (Telegram, Gemini, TabPFN):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://render.com/deploy?repo=https://github.com/asi-mir/ledgerly" 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%2Frender.com%2Fimages%2Fdeploy-to-render-button.svg" alt="Deploy to Render" width="153" height="40"&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/asi-mir" rel="noopener noreferrer"&gt;
        asi-mir
      &lt;/a&gt; / &lt;a href="https://github.com/asi-mir/ledgerly" rel="noopener noreferrer"&gt;
        ledgerly
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Telegram bot for lazy families: Gemma reads receipts, TabPFN predicts the day the budget runs out. Deploy your own copy on Render.
    &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;Ledgerly 🦥&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A Telegram bot for people who are too lazy to track their spending.&lt;/p&gt;
&lt;p&gt;Send it a photo of a receipt. &lt;strong&gt;Gemma&lt;/strong&gt; reads it, and plain code checks that the items add
up before anything is filed. Tell it about family events in plain chat: "Arman's birthday is
Dec 14." &lt;strong&gt;TabPFN&lt;/strong&gt; reads the ledger and the calendar and tells you &lt;strong&gt;Wall Day&lt;/strong&gt;: the day the
budget runs out, while you can still change it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://render.com/deploy?repo=https://github.com/asi-mir/ledgerly" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c3053e93bc9f0a2cd84050a5ff9f07cc5e639621a72e50dce48781f4a38f10e2/68747470733a2f2f72656e6465722e636f6d2f696d616765732f6465706c6f792d746f2d72656e6465722d627574746f6e2e737667" alt="Deploy to Render"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/asi-mir/ledgerly/media/post/gif-3-forecast.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%2Fasi-mir%2Fledgerly%2FHEAD%2Fmedia%2Fpost%2Fgif-3-forecast.gif" width="360" alt="Ledgerly in Telegram: two family events, /forecast with Wall Day and a chart, then a gift question"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The demo copy on Dec 1, sped up. A made-up household: every amount is made up.&lt;/em&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Your own copy in 3 steps&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Each person runs their own bot and their own database. One bot = one household.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get three keys and your Telegram ID.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;A Telegram bot token from &lt;a href="https://t.me/BotFather" rel="nofollow noopener noreferrer"&gt;@BotFather&lt;/a&gt; (&lt;code&gt;/newbot&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Your numeric Telegram ID from &lt;a href="https://t.me/userinfobot" rel="nofollow noopener noreferrer"&gt;@userinfobot&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A Gemini API key from &lt;a href="https://aistudio.google.com/apikey" rel="nofollow noopener noreferrer"&gt;Google AI Studio&lt;/a&gt; (it serves Gemma).&lt;/li&gt;
&lt;li&gt;A TabPFN token…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/asi-mir/ledgerly" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT license. 165 tests, plus 5 live checks with real Gemma and TabPFN. The README shows where every measured number in this post came from.&lt;/p&gt;

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

&lt;p&gt;Each tool has one job. Gemma 4 on Google AI Studio reads. Plain Python checks. TabPFN, through the Prior Labs API, looks ahead. Render runs the bot, and Sentry watches it.&lt;/p&gt;

&lt;p&gt;On the way, I learned five things that work outside this project too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma reads, code checks, TabPFN looks ahead
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Let the model read. Let code do the math.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gemma turns photos into rows, and chat into small commands like "add this event". Language models are great readers and shaky accountants. So the sum check is plain Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠️ needs check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# after one retry
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole fix for the ₺1.23 cake. Gemma never predicts money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. With a tiny table, use a model that learns at prediction time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have about two months of real receipts, around 60 days. Most models need far more data and a training run.&lt;/p&gt;

&lt;p&gt;TabPFN was never trained on my receipts. It was pretrained on a huge number of made-up tables. Now it reads my small table and predicts in one go. It is a bit like an LLM that learns from examples in a prompt.&lt;/p&gt;

&lt;p&gt;One row is one day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Days to payday&lt;/th&gt;
&lt;th&gt;Birthday in next 3 days&lt;/th&gt;
&lt;th&gt;Guests&lt;/th&gt;
&lt;th&gt;Spent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nov 26&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;₺600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 13&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 21&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Past rows have the answer. Future rows have a question mark. TabPFN fills in the question marks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;past_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spent_per_day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fit() only stores the table. No weights change.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Only use what you will know on the day you predict.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Spent in the last 7 days" is a useful column. It is also a trap. On December 1, I cannot know what I will spend from December 13 to 19.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Known on Dec 1 for Dec 20?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Days to payday&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 guests for Yalda tomorrow&lt;/td&gt;
&lt;td&gt;Yes, I told the bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spent in the last 7 days&lt;/td&gt;
&lt;td&gt;No. So I freeze it at the Dec 1 value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A backtest hides past days and asks the model to guess them. Skip this rule, and the backtest looks great while the real forecast fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A range turns a guess into a decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TabPFN gives three numbers for each day: low, likely and high.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;likely&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantiles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wall Day is the first day where money already spent plus the likely days passes the budget. For the demo family, that is ₺30,000. The high numbers give "maybe as early as".&lt;/p&gt;

&lt;p&gt;"Can I afford it?" adds ₺5,000 on Arman's birthday to the same daily forecast and finds Wall Day again. One number is a guess. Two forecasts side by side are a choice.&lt;/p&gt;

&lt;p&gt;I also gave TabPFN an opponent: a typical day, the middle value of my past days. I hid two separate two-week stretches of my real spending. Then both guessed the total.&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;TabPFN&lt;/th&gt;
&lt;th&gt;A typical day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Error on the two-week total (lower is better)&lt;/td&gt;
&lt;td&gt;19.3%&lt;/td&gt;
&lt;td&gt;24.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Days inside the low-to-high range&lt;/td&gt;
&lt;td&gt;92.9%&lt;/td&gt;
&lt;td&gt;no range&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On average, TabPFN was closer on the total, and the total is what Wall Day needs. It won one test by a lot and lost the other by a little. Day by day, the typical day was slightly better. My real data has no family events, so quiet weeks are easy for a typical day. Two tests is a small test.&lt;/p&gt;

&lt;p&gt;Always give your model an opponent. Otherwise you cannot tell if it helps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where did the 34 seconds go?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;5. Trace one real turn before you guess where the time goes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Telegram message becomes one Sentry trace, with steps for Gemma, the tool call and, when it runs, TabPFN.&lt;/p&gt;

&lt;p&gt;My first &lt;code&gt;/forecast&lt;/code&gt; on Render took 37 seconds. TabPFN's step took 34 of them. The trace showed the wait came before TabPFN's first API call. It was Python libraries loading on the free server's small CPU.&lt;/p&gt;

&lt;p&gt;So now the libraries load when the bot starts. The forecast is computed in the background after every new receipt or event. On an awake bot, TabPFN's step takes 3 to 9 seconds, and the answer is ready before you ask.&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%2Fi48c10dnmvoa8hcqfw6y.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%2Fi48c10dnmvoa8hcqfw6y.png" alt="Sentry trace after the fix: the background TabPFN step takes 3.12 seconds for the demo family's 365 past days and 31 future days" width="799" height="571"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After the fix: TabPFN in the background, on the demo family's year.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the Arman question, after the fix.&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%2F9dkgnjkp4b85tw8uapaf.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%2F9dkgnjkp4b85tw8uapaf.png" alt="Sentry trace of the gift question: 2.11 seconds in total, with the Gemma call and the forecast tool call" width="800" height="476"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One question, 2.11 seconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Gemma took 1.59 seconds and 1,149 + 37 tokens to understand the question. The forecast tool took 0.07 seconds, because the forecast was already waiting.&lt;/p&gt;

&lt;p&gt;And the cost: $0. Gemma, TabPFN, Render and Sentry all run on free plans.&lt;/p&gt;

&lt;p&gt;No prompts and no images go to Sentry.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bot naps
&lt;/h3&gt;

&lt;p&gt;Render's free plan puts the bot to sleep after 15 quiet minutes. This morning, after 23 quiet minutes, a test &lt;code&gt;/forecast&lt;/code&gt; reached the sleeping bot. The first reply came after 43 seconds. The forecast came after 77. Sentry shows where the time went: right after a wake-up, TabPFN's step took 29 seconds again. I have not fixed this yet. Next, I will keep the last forecast in Postgres, so a wake-up does not wait for TabPFN.&lt;/p&gt;

&lt;p&gt;Telegram resends a message when the bot is slow. So the bot says "got it" at once, does the work after, and skips repeats. After a nap, it also says sorry for the wait. On a plan that sleeps, answer first and work after.&lt;/p&gt;

&lt;p&gt;One gotcha: leave out &lt;code&gt;plan: free&lt;/code&gt; in &lt;code&gt;render.yaml&lt;/code&gt;, and Render picks a paid plan. The same &lt;code&gt;render.yaml&lt;/code&gt; behind the Deploy button passes Render's Blueprint check. My copy was deployed from it through Render's API in about 4 minutes.&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%2Fsjyhw1536zr4rtsz86sm.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%2Fsjyhw1536zr4rtsz86sm.png" alt="Render dashboard: the ledgerly web service, deployed, and the ledgerly-db Postgres database, available, both in Frankfurt" width="800" height="164"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My copy on Render: one free web service and one free Postgres.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Receipts are private. They show where you shop, what you eat and whose birthday is next week.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your copy, your data.&lt;/strong&gt; Each family deploys its own bot and database. There is no central Ledgerly server with everyone's spending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private when you need it.&lt;/strong&gt; Gemma's weights are open: you can download the model and run it yourself. The free AI Studio tier may use your data to improve Google's products. Set &lt;code&gt;GEMMA_PROVIDER=ollama&lt;/code&gt; on a machine you control, and Gemma reads your receipts there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only numbers go to TabPFN.&lt;/strong&gt; One row per day: calendar counts and money totals. No shops, no items, no names.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With open weights, you choose where your receipts are read.&lt;/p&gt;

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

&lt;p&gt;Typing one receipt by hand takes me about a minute. Sending a photo takes about five seconds. The bot needs about ten more. On a busy day with 30 receipts, that is half an hour of typing, or under three minutes of photos. Ryan: still typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The event features are tested only on the demo family. My real data has no events.&lt;/li&gt;
&lt;li&gt;One household, about two months of data. Telegram only.&lt;/li&gt;
&lt;li&gt;The free Render database expires after 30 days. After a nap, the first answer takes about 40 seconds, and a forecast over a minute.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I wrote the story, the spec and the first receipt rules. Claude Code wrote most of the code, ran the tests and deployed the bot through Render's API. A review pass found a real bug. The chat agent moved last year's Arman birthday and Yalda dinner to this year. I tested the bot in Telegram and recorded the demo.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; Reads every receipt photo and every chat message, and turns them into rows or small commands. In the trace, it understood a question in 1.59 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN:&lt;/strong&gt; Low, likely and high spending inside a Gemma agent. Error on the two-week total: 19.3% vs 24.5% for a typical day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render:&lt;/strong&gt; The agent's runtime. One free web service runs the whole agent: the webhook, the Gemma and TabPFN calls, and the background forecast. A free Postgres holds the ledger. One &lt;code&gt;render.yaml&lt;/code&gt; and a Deploy button give each family its own copy, with its own data. My &lt;code&gt;render.yaml&lt;/code&gt; passes Render's Blueprint check. My copy was live in about 4 minutes, through Render's API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry Agent Tracing:&lt;/strong&gt; One trace per message: Gemma, tool call, TabPFN. It showed most of the 34 seconds came before TabPFN's first API call.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I am still lazier than Ryan. I do not type a single receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But it is only the first of December, and I already know when That Day is coming.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It may still come. It will not be a surprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ryan still types every receipt by hand. He says it is relaxing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which month is your family's That Day? Tell me in the comments.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your photo picks the next street: a walking mystery on open-weight Gemma</title>
      <dc:creator>AsiMir</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:23:32 +0000</pubDate>
      <link>https://dev.to/asiboy6789/your-photo-picks-the-next-street-a-walking-mystery-on-open-weight-gemma-33od</link>
      <guid>https://dev.to/asiboy6789/your-photo-picks-the-next-street-a-walking-mystery-on-open-weight-gemma-33od</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;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%2F3l73b5mmyxe2xrmmpsan.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%2F3l73b5mmyxe2xrmmpsan.gif" alt="A ten-second phone recording from the desk demo. At the first fork of the Karaköy case, two sample photos of one Ottoman fountain sit side by side, a gilded inscription and a brass spout, above a " width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The detail you photograph picks the path. Gemma reads the photo live in the desk demo. Photo: Francesco Bini, Wikimedia Commons, CC BY-SA 4.0, cropped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I live and write code in Istanbul. The city is full of old fountains with poems carved into the marble. Most of us walk past them and look at our phones.&lt;/p&gt;

&lt;p&gt;The Bereketzade Fountain faces Galata Tower. One side of it has a gilded inscription and a brass spout. In my game, this side of the fountain is a fork.&lt;/p&gt;

&lt;p&gt;If you photograph the inscription, Gemma sees &lt;em&gt;"gold calligraphy inscribed on a rectangular stone plaque"&lt;/em&gt;. The cue is &lt;em&gt;writing&lt;/em&gt;, and the next stop is a small mosque downhill. If you photograph the spout, Gemma sees &lt;em&gt;"a brass spout set into a carved marble fountain wall"&lt;/em&gt;. The cue is &lt;em&gt;water&lt;/em&gt;, and the next stop is a second fountain on a street corner.&lt;/p&gt;

&lt;p&gt;These are real readings from the desk demo. In 10 of 10 pairs of live runs, the two photos led to different stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forking Paths&lt;/strong&gt; is a walking mystery for your phone. Gemma 4, an open-weight model, writes a short case from facts about the streets around you. Every fact the narrator tells you comes with its link. The narrator reads each clue aloud, so your eyes stay on the street. At each fork, the detail you photograph picks where you walk next.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The demo video shows both ways through the fork, in the game's own ElevenLabs voice.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;You choose a walk of 15 or 25 minutes and tap &lt;strong&gt;Start the walk&lt;/strong&gt;. The narrator is an old Istanbul ferry captain who became an archivist. He asks one question, and the answer is a real fact at the last stop.&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%2Fsgcupvi6loukd1tpir85.jpg" 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%2Fsgcupvi6loukd1tpir85.jpg" alt="The briefing screen of case 60AB, titled " width="800" height="1731"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Each case asks one question. The answer to this one is at the end of this post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each clue names something you can see. It never uses a compass direction. Then the app says &lt;strong&gt;Lock your phone. Go.&lt;/strong&gt; and stays quiet, so you hear the street.&lt;/p&gt;

&lt;p&gt;At the place, you tap &lt;strong&gt;I'm here&lt;/strong&gt;. GPS alone is enough to pass the stop. Then the &lt;em&gt;look line&lt;/em&gt; tells you what to photograph. Gemma describes your detail, and the narrator tells you one fact about the place. The screen shows the source of the fact: Wikipedia, or a Google result that SerpApi fetched.&lt;/p&gt;

&lt;p&gt;At a fork, each way out has a &lt;em&gt;cue&lt;/em&gt;. A cue is one word, such as &lt;em&gt;writing&lt;/em&gt; or &lt;em&gt;water&lt;/em&gt;. The look line gives a hint, but it does not choose for you: &lt;em&gt;"Two things here keep a story: one you can read, one that flows. Photograph the one you trust, up close."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Gemma finds a cue in your photo, and the cue picks the next stop. The narrator's &lt;em&gt;bridge line&lt;/em&gt; then connects your detail to that stop.&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%2Feyr052whud243jk08scm.jpg" 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%2Feyr052whud243jk08scm.jpg" alt="Three phone screens side by side with arrows between them, under the heading " width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screens from a scripted browser run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All routes end at the same answer. At the end, the case file also shows the road you did not take.&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%2Fk5dcdwfeirh3f2s53pyn.jpg" 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%2Fk5dcdwfeirh3f2s53pyn.jpg" alt="Diagram titled " width="800" height="560"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Gemma's readings from a run of the app. Photos: Wikimedia Commons, CC BY-SA 4.0 (Francesco Bini, Basak, Ali Osman Dilekoğlu, Another Believer).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Forking Paths is for anyone who wants a reason to walk for 15 minutes and look closely at an old part of town.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The video at the top is the desk demo at phone size. It is 1 minute 54 seconds long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it from your desk:&lt;/strong&gt; clone the repo, run the app and open &lt;code&gt;/demo&lt;/code&gt;. The README shows how.&lt;/p&gt;

&lt;p&gt;You do not need an account or GPS. Play the saved Karaköy case with &lt;strong&gt;The gilded inscription&lt;/strong&gt;. Then play it again with &lt;strong&gt;The brass spout&lt;/strong&gt;, and the route changes. You can also tap &lt;strong&gt;Use your own photo&lt;/strong&gt;. If the live model fails, the sample photo uses its stored result, and the screen says so.&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%2Fkxyrttezuenikbjxdqpj.jpg" 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%2Fkxyrttezuenikbjxdqpj.jpg" alt="The desk demo page at phone size, titled " width="800" height="1731"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No walk needed. Photos: Francesco Bini, Wikimedia Commons, CC BY-SA 4.0, cropped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate a quest near me&lt;/strong&gt; writes a 15-minute case around you, or in a city you pick. It takes about two minutes. If a saved case starts within about 500 m, the app uses that case again. It has been tested in 8 cities: Istanbul, London, New York, Berlin, San Francisco, Lisbon, Rome and Prague. It works in city centres, where many places have facts. You can also pick one of the tested cities.&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/asi-mir" rel="noopener noreferrer"&gt;
        asi-mir
      &lt;/a&gt; / &lt;a href="https://github.com/asi-mir/forking-paths" rel="noopener noreferrer"&gt;
        forking-paths
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AI-powered, voice-first walking mystery built with Gemma 4, Mastra, and ElevenLabs. Deployed on Render.
    &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;Forking Paths&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A voice-first walking mystery for your phone, written by an open-weight model.&lt;/p&gt;
&lt;p&gt;Gemma writes a short case from facts about the streets near you, and every fact comes with its link
A narrator reads each clue, so you can lock your phone and walk
At each fork, the detail you photograph picks your next stop.
Every route ends at the same answer.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/asi-mir/forking-paths/media/post/00-fork.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%2Fasi-mir%2Fforking-paths%2FHEAD%2Fmedia%2Fpost%2F00-fork.gif" alt="The desk demo fork: a photo of a gilded inscription picks the next stop"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=i4n_3ieU8x0" rel="nofollow noopener noreferrer"&gt;YouTube&lt;/a&gt; · &lt;strong&gt;Write-up:&lt;/strong&gt; &lt;a href="https://dev.to/asiboy6789/your-photo-picks-the-next-street-a-walking-mystery-on-open-weight-gemma-33od" rel="nofollow"&gt;DEV&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it from your desk&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;You need Node 22, pnpm and Docker.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; apps/web
cp .env.example .env.local     &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; add GOOGLE_GENERATIVE_AI_API_KEY for live Gemma&lt;/span&gt;
pnpm install
docker run -d --name forking-paths-mongo -p 127.0.0.1:27017:27017 mongo:8
pnpm db:check                  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; connects and creates the indexes&lt;/span&gt;
pnpm dev                       &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; then open http://localhost:3000/demo&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The desk demo plays a saved case in Karaköy, Istanbul, with sample photos, so it needs no GPS
It also plays with no API keys: its voice clips are in the repo, and…&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/asi-mir/forking-paths" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The code is MIT licensed. 621 unit tests pass. 12 browser checks also pass against a production build at 390 × 844, with no paid API calls. The README explains how to run the game with hosted or local Gemma, and how to deploy it to Render.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frpx3qnwl0pycnnslwe8l.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%2Frpx3qnwl0pycnnslwe8l.png" alt="Architecture diagram titled " width="800" height="654"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;How the parts fit together.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Gemma does the language and the vision work. Research, the rules and the fork decision are plain code. MongoDB stores the cases and the walks. Sentry traces each request, and photos stay out of the traces.&lt;/p&gt;

&lt;p&gt;I designed the game and directed the build. Coding agents (Claude Code) wrote much of the code and ran the checks and evals behind the numbers in this post. Every language-model call in the product goes to Gemma.&lt;/p&gt;
&lt;h3&gt;
  
  
  The writer
&lt;/h3&gt;

&lt;p&gt;First, code fetches the facts, numbers them and measures possible routes. Each hop is at most 250 m. Then &lt;code&gt;gemma-4-31b-it&lt;/code&gt; writes the case in one call, with thinking on and no tools. It picks a route and returns the case as JSON. Every line that states a fact cites fact IDs.&lt;/p&gt;

&lt;p&gt;A validator of 665 lines then checks the case. It rejects unknown fact IDs, numbers that are not in the cited facts, compass words, unsafe wording and broken routes. If it finds a problem, it sends its messages back to Gemma for one repair:&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;// apps/web/src/quest/validate.ts (trimmed)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;factIds&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;factById&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&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="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;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;add&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="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cites &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;, which is not one of the facts.`&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;unknown&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// its numbers are checked once it cites real facts&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;num&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;numberTokens&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="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;known&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;num&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="nf"&gt;add&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="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; says &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, which is not in its cited facts (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;factIds&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;); use only numbers from those facts.`&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;51 broken quests test the validator, one for each rule. With the current prompt, 9 of 10 first drafts pass the first time. Almost all of the time to make a case is that one Gemma call:&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%2Fajlw1xixrzkaaceh3bth.jpg" 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%2Fajlw1xixrzkaaceh3bth.jpg" alt="Mastra Studio showing a finished run of the generate-quest workflow, marked Success in 1 minute 38 seconds. Four completed steps run top to bottom: research, " width="800" height="750"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A Prague case in Mastra Studio. The write step took 1 min 36 s of the 1 min 38 s total.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The voice
&lt;/h3&gt;

&lt;p&gt;The game talks at each stop. It is quiet while you walk.&lt;/p&gt;

&lt;p&gt;ElevenLabs voices the narrator. When a walk starts, the app voices each line with &lt;code&gt;eleven_v4&lt;/code&gt;. It caches the clips and keeps the voice under a daily cap. An ambience plays under the clues, and two short sounds play for "found" and "not here".&lt;/p&gt;

&lt;p&gt;The 21 clips of the demo case are in the repo, so the desk demo makes 0 ElevenLabs requests.&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%2Ftpua94hk06t8stjxoad7.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%2Ftpua94hk06t8stjxoad7.png" alt="A card with data from the ElevenLabs API, 11 Oct 2026, titled " width="800" height="436"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The voice budget of the demo case: 2,320 credits for 18 lines, a 3-line sample and 3 sounds.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Facts and their sources
&lt;/h3&gt;

&lt;p&gt;Research runs in code, around a start point rounded to about 100 m. First it uses the geo search of Wikipedia. Then SerpApi grounds the writer in fresh results: Google Maps, plus up to four Google searches for the places with the fewest facts. A case uses at most 6 searches.&lt;/p&gt;

&lt;p&gt;In a live run at the Karaköy start, 2 of 8 places came only from SerpApi: Galata Tower and the Camondo Stairs. (The demo case itself cites Wikipedia.) At the Pantheon in Rome, Maps raised the number of places with a fact from 11 to 21.&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%2Fhv0pa08wm1ckii164u3d.jpg" 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%2Fhv0pa08wm1ckii164u3d.jpg" alt="The facts list of a generated London case, under " title="" width="800" height="1731"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two sources. Each fact shows its query and its link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An automated check found all 30 sampled facts in their sources. 22 were word for word on the page. The other 8 were Google Maps descriptions in the stored SerpApi responses, and a coding agent matched them.&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%2Fjtakf2e1d2yfh2kmkln2.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%2Fjtakf2e1d2yfh2kmkln2.png" alt="A card with data from the SerpApi Account API, 10 Oct 2026, titled " width="800" height="539"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The SerpApi account counter: 34 searches before the live tests, 108 after.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The photo check and the fork
&lt;/h3&gt;

&lt;p&gt;At the fountain, the screen says &lt;strong&gt;This is a fork.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The phone makes your photo smaller (at most 1024 px) and re-encodes it, so no EXIF data leaves the phone. The server does not keep the photo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; gets the photo, the name of the stop and the cues of the fork. It answers in JSON. It says what it sees (never people), whether the photo can be from this place, how sure it is, and which offered cue it found.&lt;/p&gt;

&lt;p&gt;The cue field is an enum of the fork's cues. The enum is there because the small E4B model once answered &lt;code&gt;"crest"&lt;/code&gt;, a word from the hint, instead of &lt;code&gt;symbol-or-emblem&lt;/code&gt;. With the enum, it went from 15/20 to 18/20. Then code makes the choice:&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;// apps/web/src/mastra/agents/reader.ts: at a fork, the cue must be one of the offered cues, or null&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;offered&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;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offered&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Cue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Cue&lt;/span&gt;&lt;span class="p"&gt;[]]).&lt;/span&gt;&lt;span class="nf"&gt;nullable&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;null&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// apps/web/src/quest/fork.ts: null hands over to Gemma's pick by meaning, then to the nearest way&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideByRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Place&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="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&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;===&lt;/span&gt; &lt;span class="mi"&gt;1&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;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;only-link&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;byCue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkForCue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;place&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;cue&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;byCue&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;byCue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cue&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="kc"&gt;null&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;Gemma writes the bridge line from what it saw and from the facts of the next stop. The validator's text checks apply to this line too. Through the app, a fork took 3,722 ms at the median.&lt;/p&gt;

&lt;p&gt;One eval photo fooled both Gemma models. It shows the spout, but also the gilded panels above it. The 26B model saw &lt;em&gt;"a brass water spout"&lt;/em&gt; and still chose &lt;em&gt;writing&lt;/em&gt;. The E4B model on my laptop also chose &lt;em&gt;writing&lt;/em&gt;. Only 2 of the 34 eval photos fooled both models. So the look line now asks for the detail "up close". In the desk demo, the close crop read &lt;em&gt;water&lt;/em&gt; in every run.&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%2Frb896khf7o3hae8qhlpi.jpg" 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%2Frb896khf7o3hae8qhlpi.jpg" alt="The fork screen on the water way, case F2CC, stop 1 to 2. Under " width="800" height="1731"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After the spout photo, the cue is water, and the walk goes to a different street.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Paused walks
&lt;/h3&gt;

&lt;p&gt;Between stops, your phone screen is dark, and nothing runs on the server.&lt;/p&gt;

&lt;p&gt;Two Mastra workflows run the game. The &lt;code&gt;generate-quest&lt;/code&gt; workflow writes a case. The &lt;code&gt;play-quest&lt;/code&gt; workflow suspends at every stop, and your photo resumes it. In 2 of 2 scripted runs, a paused walk survived a server restart.&lt;/p&gt;

&lt;p&gt;Mastra saves where each walk is paused. The rules and the photo stay outside it: a paused snapshot is 2,071 bytes and has no image.&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%2Fv2z9fbzyn8emrbbu0aw1.jpg" 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%2Fv2z9fbzyn8emrbbu0aw1.jpg" alt="Mastra Studio showing a run of the play-quest workflow marked Suspended, paused for 38 minutes. The begin step, " width="800" height="550"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;code&gt;play-quest&lt;/code&gt; in Mastra Studio, waiting for a photo.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A prompt rule for legends
&lt;/h3&gt;

&lt;p&gt;At the small mosque, the narrator tells you about a secret passage.&lt;/p&gt;

&lt;p&gt;Old cities have many legends. A legend needs careful words, such as "is thought to", "says" or "circa". The validator checks IDs and numbers, but it cannot see when a careful word disappears. Gemma's first draft of the demo case dropped three of them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gemma's first draft&lt;/th&gt;
&lt;th&gt;Its next draft, with one new rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"They were designed to prevent the banker's children from falling …"&lt;/td&gt;
&lt;td&gt;"It is thought that the shape was designed to stop the children … from falling …"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"… built between 1870 and 1880 …"&lt;/td&gt;
&lt;td&gt;"… built circa 1870-1880 …"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"A secret passage once led from here to the harbor."&lt;/td&gt;
&lt;td&gt;"The mosque's inscription says that a secret passage … ran from Galata Tower … down to the old harbour at Karaköy."&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the new rule in the prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep their hedges ("is thought to", "says", "circa"): a legend stays a legend. Never claim that an inscription or a detail says something unless a fact says so.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next draft passed the first time. One line still said more than its facts (&lt;em&gt;"another hidden link between the lanes"&lt;/em&gt;), so it was rewritten by hand. The quest file lists all 8 hand edits.&lt;/p&gt;
&lt;h3&gt;
  
  
  Measuring the writer prompt
&lt;/h3&gt;

&lt;p&gt;The most common error in first drafts was a compass word (5 of 22 drafts). Gemma copied each one from its own facts. But &lt;em&gt;"the northwest corner"&lt;/em&gt; does not help when your phone is in your pocket. The fix was to name the compass word next to the fact in the prompt:&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;// apps/web/src/mastra/agents/writer.ts (trimmed): a note under each place's facts in the prompt&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;compass&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;`These facts use the compass word &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;compass&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;: cite them, but in clues, hints and link reasons name a landmark instead.`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three versions of the prompt ran with the same 10 starting points, the same facts and the same model. Runs with no draft are not counted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Writer prompt&lt;/th&gt;
&lt;th&gt;First drafts that passed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;As first merged&lt;/td&gt;
&lt;td&gt;2 of 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2: stricter rules on directions, hints and ties&lt;/td&gt;
&lt;td&gt;0 of 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3: each fact's compass words named; street names allowed&lt;/td&gt;
&lt;td&gt;8 of 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Version 2 got worse: its rule "Choose its answer place first" sent the start straight to the answer in 2 of 6 drafts. Version 3 removed that rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bugs and fixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tracing kept a photo.&lt;/strong&gt; Mastra's span store saved the image from a photo call. Now a span processor removes images first: 0 image bytes in 160 spans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first fork on the laptop ignored the photo.&lt;/strong&gt; The model was not loaded yet, so the read took 10,108 ms, over its 10 s budget. Now the app loads the model when the walk starts, and reads take 3.5 to 3.7 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Studio's free tier hid its wait time.&lt;/strong&gt; The 429 response had no Retry-After header. The wait, "Please retry in 46.1s.", was only in its body. Now the app reads it and waits once, inside its 240 s limit. Two more cases then finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rough GPS position counted as exact.&lt;/strong&gt; A position with 1 to 3 km of error would tell a player at the right place that they were "too far". Now a position worse than 150 m counts as no GPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I tested it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The desk-demo fork runs above: two photos, two stops, in 10 of 10 pairs.&lt;/li&gt;
&lt;li&gt;30 live case generations in 7 cities.&lt;/li&gt;
&lt;li&gt;34 labelled photos from Wikimedia Commons, through both photo readers, with 0 errors in 68 calls.&lt;/li&gt;
&lt;li&gt;8 unsafe test lines. The validator blocks all of them.&lt;/li&gt;
&lt;li&gt;12 browser checks, from both ways through the desk demo to a scripted play in street mode, with GPS set by the script.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Only one model call sees something private: the photo check. It gets a photo taken where you stand. ElevenLabs only gets the text of the case. SerpApi and Wikipedia only get a point rounded to about 100 m.&lt;/p&gt;

&lt;p&gt;The photo check can run on your own machine; the demo uses Google's API. When the app runs on your machine, one setting moves the photo check to Gemma 4 E4B on Ollama:&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;GEMMA_READER_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama   &lt;span class="c"&gt;# the photo check only; GEMMA_PROVIDER=ollama moves every role&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma4:e4b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is the same Gemma 4 family: 26B/31B in the cloud, E4B on my laptop. Both readers ran on the same 34 photos, on the same day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo check&lt;/th&gt;
&lt;th&gt;Gemma 4 26B, AI Studio free tier&lt;/th&gt;
&lt;th&gt;Gemma 4 E4B, Ollama, M1 Pro 16 GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right cue, 28 street details (chance: 12.3 of the 25 with a fitting cue)&lt;/td&gt;
&lt;td&gt;26 (93 %)&lt;/td&gt;
&lt;td&gt;22 (79 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plausible or not, right&lt;/td&gt;
&lt;td&gt;34 of 34&lt;/td&gt;
&lt;td&gt;34 of 34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per photo, p50 / p95&lt;/td&gt;
&lt;td&gt;3,592 / 5,573 ms&lt;/td&gt;
&lt;td&gt;4,178 / 5,953 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desk-demo fork, two photos to two stops&lt;/td&gt;
&lt;td&gt;10 of 10&lt;/td&gt;
&lt;td&gt;10 of 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who sees the photo&lt;/td&gt;
&lt;td&gt;Google; free-tier inputs may be used to improve its products&lt;/td&gt;
&lt;td&gt;my laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fklmqh58jh52b994jf8q4.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%2Fklmqh58jh52b994jf8q4.png" alt="A comparison card titled " width="800" height="596"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The prompt and the schema are the same. Only the place where the photo is read changes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; E4B is a much smaller model, and it runs on a busy 16 GB laptop. It got four more cues wrong. Its first read took 17.8 s while the model loaded. So the app loads it when the walk starts, and gives it 25 s per photo instead of 10. The writer stays on the 31B model in the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What open weights made possible.&lt;/strong&gt; With a closed vision API, every photo goes to one place, and the vendor picks the model. Gemma's open weights (Apache 2.0) let the photo check also run on my laptop. They also let me pick a model size for each job: 31B to write, 26B to read fast, and E4B where the photo stays on a machine I control. The eval is in the repo, so anyone can run it again.&lt;/p&gt;

&lt;p&gt;AI Studio listed 62 models for my key, and only 2 of them were Gemma. So "open weights only" is a check in the code:&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;// apps/web/src/lib/model.ts: AI Studio also serves closed Gemini models, so its IDs must be Gemma&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;provider&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^gemma-/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelId&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;ModelConfigError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Only Gemma models may run on Google AI Studio (got "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;modelId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ollama&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;:-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;cloud$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelId&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;ModelConfigError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Ollama cloud models are not allowed (got "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;modelId&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;/code&gt;&lt;/pre&gt;

&lt;/div&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 4 31B writes each case from numbered facts in one JSON call. 26B reads each photo, picks one cue from a fixed list and writes the bridge lines. With thinking set to &lt;code&gt;minimal&lt;/code&gt;, a one-word answer went from 5.5 s to 0.55 s. E4B runs the same photo check on a laptop through Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; ElevenLabs gives the open-source agent its voice. The ferry-captain narrator speaks with &lt;code&gt;eleven_v4&lt;/code&gt;, with a clip cache and a daily cap. ElevenLabs also made the ambience, the two short sounds and the narration of the video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi:&lt;/strong&gt; SerpApi grounds the writer in fresh Google Maps and Google Search results. A case uses at most 6 searches, and each fact shows its query and its link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra:&lt;/strong&gt; Mastra orchestrates the agent over open models. The &lt;code&gt;generate-quest&lt;/code&gt; workflow writes a case. The &lt;code&gt;play-quest&lt;/code&gt; workflow suspends the walk at every stop until a photo resumes it, even after a server restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Gemma 4 by Google (Apache 2.0), through Google AI Studio and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; Mastra, the AI SDK, the MongoDB driver, sharp and Playwright (Apache-2.0). Next.js, React, zod, Tailwind CSS, Vitest and the Sentry, ElevenLabs and SerpApi SDKs (MIT).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facts and voice:&lt;/strong&gt; Wikipedia (CC BY-SA 4.0, shortened or translated) and Google results through SerpApi. ElevenLabs (&lt;code&gt;eleven_v4&lt;/code&gt;, the premade voice George).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photos:&lt;/strong&gt; Wikimedia Commons, CC BY-SA 4.0, cropped: Francesco Bini, Basak, Ali Osman Dilekoğlu, Another Believer. The eval photos are credited in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts:&lt;/strong&gt; Geist, Fraunces, IBM Plex Mono (SIL OFL).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The answer
&lt;/h2&gt;

&lt;p&gt;Back to the question about the stairs. Both photos lead you to the Camondo Stairs at the end. The answer keeps its careful words: &lt;em&gt;"It is thought that the shape was designed to stop the children of the banker who built them from falling down the steps if they slipped."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then the case file shows the street you did not walk: &lt;em&gt;"Had you photographed the water, you would have gone to Hasan Ağa Fountain."&lt;/em&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%2Fn5gwunqdf1ytmneg5d2g.jpg" 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%2Fn5gwunqdf1ytmneg5d2g.jpg" alt="The case file's " width="800" height="1731"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photos: Wikimedia Commons, CC BY-SA 4.0, cropped (Francesco Bini, Basak, Another Believer).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run the desk demo and take the other photo at the fork. Then tell me in the comments what your own street would hide.&lt;/p&gt;

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