<?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: ADK BBX</title>
    <description>The latest articles on DEV Community by ADK BBX (@adk_bbx).</description>
    <link>https://dev.to/adk_bbx</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%2F4158217%2F579edfae-f460-4c4b-8838-05a5f09ff6bb.png</url>
      <title>DEV Community: ADK BBX</title>
      <link>https://dev.to/adk_bbx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adk_bbx"/>
    <language>en</language>
    <item>
      <title>NapQuest: a sloth game you can hand over to a local AI</title>
      <dc:creator>ADK BBX</dc:creator>
      <pubDate>Mon, 26 Oct 2026 01:40:48 +0000</pubDate>
      <link>https://dev.to/adk_bbx/napquest-a-sloth-game-you-can-hand-over-to-a-local-ai-58j8</link>
      <guid>https://dev.to/adk_bbx/napquest-a-sloth-game-you-can-hand-over-to-a-local-ai-58j8</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;For the past three weeks, I've been building fairly serious apps for these challenges. There was one for &lt;a href="https://dev.to/adk_bbx/phone-calls-are-the-boss-fight-of-living-in-japan-1h6h"&gt;getting through phone calls in Japan&lt;/a&gt;, one for &lt;a href="https://dev.to/adk_bbx/i-asked-my-junk-mail-one-question-are-you-worth-the-walk-41p8"&gt;working out whether junk mail was worth a walk&lt;/a&gt;, and last week's &lt;a href="https://dev.to/adk_bbx/building-software-got-easy-finding-the-first-customer-didnt-398i"&gt;tool for finding a product's first customers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This week I wanted to make something fun and light. The prompt was about laziness, so a sloth seemed like a reasonable place to start.&lt;/p&gt;

&lt;p&gt;I called him Mallow. He lives in a woodland village, and his first problem is a bell that won't let him sleep. Somehow this turned into gathering wood, taking a job with bees, building a better house and defending it from geese.&lt;/p&gt;

&lt;p&gt;I also wanted to be able to stop doing all the little jobs myself. That's where Laya comes in.&lt;/p&gt;

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

&lt;p&gt;NapQuest is a small village-building game. You can play it yourself, or give Mallow a goal and let a local AI choose what he does next.&lt;/p&gt;

&lt;p&gt;If I ask him to earn ten coins, he can find work or gather berries to sell. If I ask him to upgrade his home, he has to get the materials, pay for the upgrade and keep himself fed along the way. I can take control again whenever I want.&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%2Fizwnaw4eonyksy9sdyl3.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%2Fizwnaw4eonyksy9sdyl3.png" alt="Mallow standing outside his level-five woodland home, surrounded by rabbit guards, production buildings, watchtowers and Kevin's market" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the supplied level-five practice village. It has its own save and prepared resources so you can try the later parts of the game.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first versions weren't very entertaining. Mallow would walk around while the journal told me what had happened. If a goose was supposed to be chasing him, I wanted to see the goose chasing him. The same went for the bees. Reading about a consequence wasn't enough.&lt;/p&gt;

&lt;p&gt;Now stealing a picnic starts a chase on the map. Taking a honey job brings bee coworkers along for a shift. Trees leave stumps you can clear for building space. A nap takes a few seconds and ends with Mallow coming back outside, ready to continue.&lt;/p&gt;

&lt;p&gt;The village has five home levels, with a different building at each level. You can add resource mines, plant food, buy tools and train defenders. Walls and gates slow enemies down while watchtowers and acorn cannons fire at them. The last fight is against the Goose Captain.&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%2Fusiafjprv3h7youxk0m0.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%2Fusiafjprv3h7youxk0m0.gif" alt="Rabbit guards, hedgehog rangers, towers and cannons defending the supplied village against the Goose Captain's raid, with the journal closed" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A recording of the practice siege. The prepared defenses win this run and earn the Acorn crest.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://adkbbx-napquest.hf.space/play/" rel="noopener noreferrer"&gt;Play NapQuest in your browser&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Online demo added October 27, after the Week 3 submission. You can play by hand without using AI quota. Laya runs on a shared free GPU, so automatic play can queue or pause at the guest limit. The local version is still available for longer AI sessions. Online goals and action descriptions go to the server; your village saves in this browser.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://adkbbx-napquest.hf.space/play/?defense=5" rel="noopener noreferrer"&gt;Try the supplied level-five practice village&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/adkbbx/NapQuest/blob/main/submission/media/laya-gameplay.mp4" rel="noopener noreferrer"&gt;Watch the MP4 gameplay recording&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc3d0y1zd0h2l6n35vp8.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%2Fhc3d0y1zd0h2l6n35vp8.gif" alt="Mallow following a real Laya goal to earn ten coins, travelling to the bee workplace and stopping after the goal counter reaches ten" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This shows the first 24 seconds of a local Laya run, followed by its later completion. The practice world supplies the starting village. The recording is silent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The downloadable version runs locally with Python and a browser. Once you've downloaded the model, you can run it offline. To try automatic play, open Laya, choose "Earn 10 coins" and select "Let Laya take over." Clicking the map gives control back to you.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/adkbbx/NapQuest" rel="noopener noreferrer"&gt;Source code on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source package includes the browser game, Python model adapter, setup scripts, tests and the decision records from the runs below. The application code is MIT licensed. The model downloads separately during setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://huggingface.co/convaiinnovations/laya" rel="noopener noreferrer"&gt;Convai Innovations' Laya&lt;/a&gt;, an open-weight decision model, running on the CPU. The build pins model revision &lt;code&gt;7b928d828b7b0e022f929d9bd2e44165aa270148&lt;/code&gt; so I can repeat tests against the same checkpoint.&lt;/p&gt;

&lt;p&gt;Laya chooses from a set of described actions. The game works out which actions are legal, sends those choices to the model and carries out its answer. Movement, resource costs and combat follow the game's rules. The model chooses what to attempt; the game checks whether it can happen.&lt;/p&gt;

&lt;p&gt;One bug was especially annoying. Asking Mallow to earn money didn't work because earning wasn't a proper goal type yet. I added that goal and a counter for new income, rather than checking how much money was already in his wallet. I also had to split crowded action descriptions into smaller questions. Moving home near the market needed separate questions for what to move and where to put it.&lt;/p&gt;

&lt;p&gt;The browser game uses JavaScript, HTML and Canvas. Terrain generates in seeded chunks as you explore, and browser storage saves your inventory and changes to the village. The painted characters use layered cutout animation. The added combat units use simpler whole-sprite movement.&lt;/p&gt;

&lt;p&gt;Sound made a big difference. I used my ElevenLabs subscription to generate 21 recordings for the forest, footsteps, chopping, mining, doors, bees, goose honks and combat. The game plays those files locally. I added separate controls for ambience and effects, plus a mute button in the header.&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%2Fc31b3d00e863qfdev94o.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%2Fc31b3d00e863qfdev94o.png" alt="NapQuest's Store open on a phone-sized screen, with item prices, purchase buttons and the main navigation within reach" width="390" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The phone layout keeps the map and controls on one screen. Store, Bag, Village and Settings open as panels. You can tap objects to gather or interact instead of finding an action at the bottom of a long page.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does open innovation matter?
&lt;/h2&gt;

&lt;p&gt;I wanted to put the model in the game loop without charging for every decision. With Laya running locally, I can leave a goal going without a hosted inference bill. In the local version, the player's requests stay on their machine. The later online demo sends requests to its model server.&lt;/p&gt;

&lt;p&gt;It also made debugging practical. I could save the available choices and the model's answer, change an action description and run the same situation again. There is still a setup download and some waiting for CPU inference, and Laya can misunderstand a request. The game handles one main goal at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lazy payoff
&lt;/h2&gt;

&lt;p&gt;In one fresh-world test, I gave Laya the home-level-five goal and it completed it in 40 world actions without supplied resources. A separate run earned ten new coins in 33 actions. Those are recorded runs, rather than a promise that every goal will take the same route.&lt;/p&gt;

&lt;p&gt;That's the part I wanted from this project. I can do the gathering and building when I feel like playing, or give Mallow a job and watch him work through it.&lt;/p&gt;

&lt;p&gt;The original submission build passed 48 gameplay tests and nine Python tests. I've also checked the game in the browser, including purchases, moving home, clearing stumps, automatic waking and a ten-coin goal that stopped when it was finished. There is more I'd like to do with the story, but this version has the bell-and-nap quest, the five-level village and the goals you set yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize categories
&lt;/h2&gt;

&lt;p&gt;I'm entering Best Use of ElevenLabs for the generated game audio. The repository's audio manifest lists the recordings and the prompts used to make them. The files were generated during development and are included with the game.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I used AI coding and image-generation tools while building NapQuest, and ElevenLabs for the audio. I directed the build, tested the game and kept revising the parts that weren't fun. AI also helped prepare this post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building software got easy. Finding the first customer didn't.</title>
      <dc:creator>ADK BBX</dc:creator>
      <pubDate>Mon, 19 Oct 2026 01:23:31 +0000</pubDate>
      <link>https://dev.to/adk_bbx/building-software-got-easy-finding-the-first-customer-didnt-398i</link>
      <guid>https://dev.to/adk_bbx/building-software-got-easy-finding-the-first-customer-didnt-398i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;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%2Fnle09lpyk69jych7s4vr.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%2Fnle09lpyk69jych7s4vr.gif" alt="The GTM Shortlist landing page at eygentic.tech: " width="600" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GTM Shortlist, live at &lt;a href="https://eygentic.tech" rel="noopener noreferrer"&gt;eygentic.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI has made building software cheap and fast, and you can see it in the numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In 2025, Apple received about &lt;a href="https://thenextweb.com/news/vibe-coding-apple-app-store-surge-crackdown" rel="noopener noreferrer"&gt;557,000 new app submissions&lt;/a&gt;, the most since 2016.&lt;/li&gt;
&lt;li&gt;In December 2025, new iPhone apps were &lt;a href="https://the-decoder.com/vibe-coding-may-be-behind-a-60-percent-spike-in-new-ios-apps/" rel="noopener noreferrer"&gt;up 60% on a year earlier&lt;/a&gt;, after three years of flat numbers (a16z's analysis, which also says this doesn't prove AI coding tools are the cause).&lt;/li&gt;
&lt;li&gt;On GitHub, developers created &lt;a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/" rel="noopener noreferrer"&gt;more than 230 new projects every minute&lt;/a&gt;, and over 36 million new developers joined in a year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Customers didn't multiply the same way. When CB Insights looked at &lt;a href="https://www.cbinsights.com/research/startup-failure-reasons-top/" rel="noopener noreferrer"&gt;431 venture-backed startups that shut down since 2023&lt;/a&gt;, the most common underlying problem was poor product-market fit, in 43% of the cases they could explain. In plain words, the product got built, and not enough of the right people bought it.&lt;/p&gt;

&lt;p&gt;I've felt this myself. This month I shipped an app every week for these challenges, with two coding agents doing most of the typing. Building isn't the hard part anymore. The hard part starts the day after you ship: who do you sell it to?&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://eygentic.com" rel="noopener noreferrer"&gt;Eygentic&lt;/a&gt;, a company that runs customer-acquisition experiments for B2B and SaaS teams: it tests which customers, messages and channels actually work before anyone spends money on growth. Every one of those experiments starts with the same boring thing, a list of companies that might buy. Most prospecting tools I looked at sell you access to a big database of companies and contacts, and you have to trust that each row is right. That's fine if you already have a sales budget. A two-person AI startup that just shipped usually doesn't.&lt;/p&gt;

&lt;p&gt;If finding customers is now the hard part of starting a software company, I don't think it should only be possible for companies that can already afford a sales team. So I built the tool I wanted for my own first list, made it open source, and put it online. It's not complete yet, but this challenge gave me the motivation to finally get started on it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;GTM Shortlist.&lt;/strong&gt; GTM means "go to market", the work of getting a product in front of people who will buy it. You give the app your company's website, and it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads your website&lt;/strong&gt; and writes a short description of what you sell and who might buy it. You fix anything it got wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Searches Google&lt;/strong&gt; for companies that match. Nothing is searched until you've confirmed who you're looking for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visits each company's own website&lt;/strong&gt; and looks for proof that it matches, like a sentence on its product page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gives you a short list.&lt;/strong&gt; Next to each company it shows the exact sentence from that company's site that backs up the match. If it can't find proof, it says "unknown" instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lets you decide.&lt;/strong&gt; You mark each company yes or no, download the list as a spreadsheet, and can start a first email that points to what you found. It never sends anything for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg46592kgb0juuqpkujki.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%2Fg46592kgb0juuqpkujki.gif" alt="The demo at eygentic.tech/?demo=1, with invented companies: the app understands the offer, explores the market, waits for the target to be confirmed, checks candidates, shows the exact passage behind a match, and ends with accept or reject" width="720" height="737"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every step, using the demo on eygentic.tech. The companies and sentences are made up for the demo, so you can try it without an account or spending anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A company is only marked as a fit when every must-have is backed by a sentence found word for word on its own site, and a second check by the model agrees that the sentence really says that. Your yes or no is kept separate from the app's opinion, and a fit never means "this company wants to buy".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for this?&lt;/strong&gt; Founders of small B2B software and AI companies who do their own sales, and the consultants and agencies who build a new list of target companies for every campaign. The hosted version gives you your first three companies free, with no card. After that, research runs on credits, one per company researched, and paying for more credits isn't built yet. If you run it yourself, it's free apart from your own search and AI costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real run, and what it got wrong
&lt;/h2&gt;

&lt;p&gt;On 16 October I tried the hosted app on my own website, &lt;a href="https://eygentic.com" rel="noopener noreferrer"&gt;eygentic.com&lt;/a&gt;, looking for "B2B AI SaaS companies". To find companies, the app runs a few Google searches one after another. The first search worked and found 8 companies, but the next one failed. The app had a rule that every search had to work before it used any of the results, so it threw away all 8 and stopped with nothing. It gave my three credits back, but I still had no list.&lt;/p&gt;

&lt;p&gt;So I changed the rule: if one search fails, the app now keeps the companies the other searches already found and carries on. I picked the same run back up from where it stopped, and it researched three of the 8 companies it had saved, without searching again. That gave me my first real shortlist.&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%2Fr88auv4p2r1j95x7rjv9.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%2Fr88auv4p2r1j95x7rjv9.png" alt="Review your shortlist: three researched companies. NachoNacho needs verification, builtin.com has no company description established, and DealHub fits the required criteria. A notice says discovery was incomplete" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first real shortlist. DealHub matched. NachoNacho needs more checking. builtin.com isn't a company I could sell to at all: it's a website that writes lists of other companies, and it used up one of my three research slots.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The app didn't make anything up about Built In. It just said "unknown" for everything. But I'd still spent a credit researching a website that was never a customer. So I looked back at every test run I had saved, without paying for any new searches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;18 test runs had researched 19 companies, and &lt;strong&gt;not one of them came out as a real match&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;My searches were bad. I was searching Google for my whole target sentence, and Google mostly returned how-to guides, "top 10" lists, reviews, YouTube videos, job ads and dictionary pages, not companies.&lt;/li&gt;
&lt;li&gt;I was asking for things websites don't say. Whether a company is privately owned, how much funding it has, or what its internal problems are is almost never on its own website, so those answers were always "unknown".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I changed four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search for companies, not articles.&lt;/strong&gt; It now searches with a short phrase like "B2B SaaS", plus words you only find on a company's own site, like "official website" or "pricing".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip pages that aren't companies before opening them.&lt;/strong&gt; Videos, job boards, review sites, directories and "top 10" lists are dropped straight from the search results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warn me when I ask for something a website won't say,&lt;/strong&gt; and let me turn it into a nice-to-have with one click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never research the same company twice&lt;/strong&gt; for the same business.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To check the search fix, I took real search results that my earlier test runs had saved, marked each one as either a company's own website or something else by looking at its address and title, and ran them through the old version and the new one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real company websites:&lt;/strong&gt; there were 21. The old version let 16 of them through to research. The new one lets all 21 through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages that weren't companies&lt;/strong&gt; (job boards, directories, review sites and so on): there were 46. The old version let 5 of them slip through. The new one lets 2 through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages opened for nothing:&lt;/strong&gt; the old version opened 39 pages only to throw them away afterwards. The new one throws them away before opening them, so it wastes none.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running my old research through the new rules, 18 of the 22 companies I had already researched would have been skipped: 6 because they weren't companies at all, and 12 because they were the same company researched again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real websites are messy
&lt;/h2&gt;

&lt;p&gt;Testing on more real websites turned up more problems, each with its own fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invisible characters.&lt;/strong&gt; One site hides invisible spaces between its words, so a sentence the model copied didn't look like the same sentence to the app. Now the app ignores invisible characters, but every real word still has to match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menus instead of content.&lt;/strong&gt; On some pages, most of the text was the list of countries and languages, so the model quoted the menu as proof. Long menus are now removed before the model reads the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A menu link isn't proof.&lt;/strong&gt; One company counted as a match because of a single link in its menu. One or two words from a menu no longer count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Suggests" isn't proof.&lt;/strong&gt; When the model says a page "suggests" or "may" show something, that no longer counts as a yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages it couldn't open.&lt;/strong&gt; Some homepages send you on to another address, and some are very large. Both used to be skipped, and both can be read now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The best page was ignored.&lt;/strong&gt; Search often finds the exact page that proves the point, but research used to start from the homepage instead. Now it reads the page search found as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A small model on my laptop vs a big one on DigitalOcean
&lt;/h2&gt;

&lt;p&gt;After all those fixes, I wanted to know whether the list was finally useful for my own business. I took the 12 companies a saved web search had found for Eygentic on 13 October, looking for private AI software startups that sell to businesses, and researched them again with no new searches.&lt;/p&gt;

&lt;p&gt;First I used small open models on my laptop: Gemma 4 E2B and Qwen 3.5 4B. With the fixes in, they stopped wasting money on pages that were never going to work, but they rarely copied the proof from a page, so &lt;strong&gt;none of the product companies came out as a match&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then I used Gemma 4 31B, the bigger version of Gemma, on DigitalOcean. It took 18 AI calls for all 12:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DelegateZero: a match.&lt;/strong&gt; Its own site says "DelegateZero - AI Decision Proxy for B2B SaaS Founders", and its pricing page lists monthly plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prismatic: a match&lt;/strong&gt; ("Embedded iPaaS for B2B Software Companies"), after one more fix. The model sometimes answered a check with a word like "cleared" instead of yes or no, and the app threw the whole company away. It now reads those words properly, and re-reading the answer the model had already given turns Prismatic into a match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smeron and BackOfficeBoss: set aside.&lt;/strong&gt; They build custom software for clients rather than selling a product, so they aren't buyers for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six others: need more checking.&lt;/strong&gt; The app couldn't find enough proof on their own websites, so it says so instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two: refused.&lt;/strong&gt; The app couldn't confirm who owns the website.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So 2 of the 12 are real leads for Eygentic. It's a short list, but every name on it comes with its proof, and the small models couldn't get there at all.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://eygentic.tech" rel="noopener noreferrer"&gt;eygentic.tech&lt;/a&gt;&lt;/strong&gt;: type your company's website or name. Your first three companies are free and no card is needed.&lt;/p&gt;

&lt;p&gt;If you'd rather not sign up, tap &lt;strong&gt;Try the demo&lt;/strong&gt; on the start page, the walkthrough in the GIF above. You can also install it from the code and choose &lt;strong&gt;Try it with sample data&lt;/strong&gt;, which runs the whole thing on three made-up companies with no accounts or keys.&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%2Fiq2u36yf9xn11i37iju3.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%2Fiq2u36yf9xn11i37iju3.png" alt="Architecture: your browser, a Render web service at eygentic.tech serving the API and UI, Supabase for sign-in and Postgres, a Render background worker, and the worker's calls to SerpApi, the companies' own websites and Gemma 4 31B on DigitalOcean. The same code runs on a laptop with SQLite and Ollama" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app is a website (React) with a Python server behind it, plus a separate background worker that does the slow research. Before the worker does anything that costs money, like a search, a page visit or a call to the AI model, it writes it down first. If the server restarts halfway through, it carries on from its notes instead of paying for the same thing twice, and if it can't tell whether a request went through, it stops and tells you instead of trying again. Every run has hard limits: at most 4 searches, 8 AI calls and 3 pages per company.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  SerpApi: the Google search
  &lt;p&gt;SerpApi gives the app Google search results, at most 4 searches per run. My first real search took about 29 seconds and timed out, so the app now waits up to 60 seconds, and a search that times out is never retried automatically, because it may already count against my monthly limit.&lt;/p&gt;

&lt;p&gt;I'm on SerpApi's free plan, 250 searches a month, so my coding agents had to ask me before spending any. While building, the app used saved search results instead of new ones, and SerpApi's free account check confirms the key works without using up a search. On 16 October I still had 236 of my 250 searches left.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  Gemma on DigitalOcean: the AI model
  &lt;p&gt;The hosted app uses Gemma 4 31B, an open model from Google, on DigitalOcean's hosted AI service, so I don't run any servers for it. It does four jobs: describing your business from your homepage, suggesting campaign ideas, finding quotes on each company's site, and double-checking each quote. On 30 test cases for that double-check (29 written for the test, 1 from a real website) it got all 30 right. That's a check of one job, not an independent benchmark.&lt;/p&gt;

&lt;p&gt;On my laptop, the first runs gave useless answers. The program running the model was quietly cutting my instructions short, keeping about 2,000 pieces of text out of 6,000 to 12,800, so the model never saw what I'd asked for. Shorter instructions and a bigger limit fixed it.&lt;/p&gt;

&lt;p&gt;The model never has the last word. Ordinary code checks that each quote really is on the page, that the page belongs to that company and that every must-have is covered, and it decides the order of the list.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  Render: running the app online
  &lt;p&gt;Render runs two pieces from the same code: the website and the background worker. Passwords and keys live in Render's dashboard, never in the code. I only release a version after the automatic tests pass. During the first real run I released an update halfway through, and the new worker picked up exactly where the old one stopped, without searching again. Afterwards I restarted everything on purpose, and the exported results were exactly the same.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  eygentic.tech: the web address
  &lt;p&gt;&lt;a href="https://eygentic.com" rel="noopener noreferrer"&gt;eygentic.com&lt;/a&gt; is the company. &lt;a href="https://eygentic.tech" rel="noopener noreferrer"&gt;eygentic.tech&lt;/a&gt; is the tool, open for anyone to use. When I first connected the domain to Render, the app refused it with "400 Invalid host header", because it only trusted its original Render address. Now it accepts a fixed list of addresses, each with its own sign-in link, and still refuses anything else.&lt;/p&gt;



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

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

&lt;p&gt;Finding customers is becoming the expensive part of starting a software company, and right now the answer is mostly rented: you pay for access to someone else's database, every month. I want the method to be something anyone can read, run and change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Open?&lt;/th&gt;
&lt;th&gt;If you don't want it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GTM Shortlist&lt;/td&gt;
&lt;td&gt;the whole app&lt;/td&gt;
&lt;td&gt;open source (MIT licence)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4&lt;/td&gt;
&lt;td&gt;reads websites, writes, double-checks&lt;/td&gt;
&lt;td&gt;open model&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;runs Gemma online&lt;/td&gt;
&lt;td&gt;paid service running an open model&lt;/td&gt;
&lt;td&gt;run Gemma on your own computer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;Google search results&lt;/td&gt;
&lt;td&gt;paid service with a free plan&lt;/td&gt;
&lt;td&gt;Tavily, or saved search results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;runs the app online&lt;/td&gt;
&lt;td&gt;paid service&lt;/td&gt;
&lt;td&gt;any server, or your own computer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;sign-in and database&lt;/td&gt;
&lt;td&gt;open source&lt;/td&gt;
&lt;td&gt;a simple file on your computer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's what being open actually changed for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your list stays yours.&lt;/strong&gt; Who you're trying to sell to is private business information. If you run it yourself with a local model, your research stays in a file on your own computer. The only things that leave it are the searches and the visits to the companies' own websites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's cheap enough for one person.&lt;/strong&gt; Run it yourself and you only pay for searches and the model. At 4 searches a run, SerpApi's free plan covers dozens of lists a month. That's what "accessible to everyone" means to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could check its work.&lt;/strong&gt; Every problem above was found by reading the saved pages and the model's saved answers, not by guessing. Because the model is open, I could try the small version on my laptop and the big version online on exactly the same pages, and see for myself that this job needs the big one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules are visible.&lt;/strong&gt; What counts as a match, which pages get skipped and how quotes are checked are all in the code, so you can disagree with them and change them.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't find email addresses&lt;/strong&gt; or contact details. It finds companies and the proof that they fit. Reaching out is still up to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some things stay unknown.&lt;/strong&gt; Funding, ownership and budgets are rarely on a company's own website, so the app leaves them as "unknown" instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models aren't good enough yet.&lt;/strong&gt; On my laptop they couldn't produce a single real match for my own business. Research needs a bigger model, which means a hosted service or a powerful computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs are small.&lt;/strong&gt; Up to three companies at a time, so it's a shortlist, not a database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't buy more credits yet.&lt;/strong&gt; The free three work, but paying for more isn't built.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The first user is me. The first real shortlist was for Eygentic, the company I'm building, and DealHub was the first company that matched what I was looking for. The bigger model later added DelegateZero and Prismatic. I haven't sold GTM Shortlist to anyone yet. What I want to find out next is whether a founder who has just launched will trust a list that says "unknown" a lot, instead of one that gives every company a confident score.&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/adkbbx" rel="noopener noreferrer"&gt;
        adkbbx
      &lt;/a&gt; / &lt;a href="https://github.com/adkbbx/GTM_Shortlist" rel="noopener noreferrer"&gt;
        GTM_Shortlist
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;GTM Shortlist&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Open-source company discovery and research for B2B sellers. Confirm your offer and targeting criteria, discover candidates, inspect dated source passages, and review/export a shortlist. Human acceptance never means buying intent.&lt;/p&gt;
&lt;p&gt;Created by Eygentic. Every seller uses the same workflow; Eygentic profiles are optional examples and test inputs, not product defaults.&lt;/p&gt;
&lt;p&gt;This is an early local implementation. Hosted accounts, external signup, deployment, real provider benchmarks and observed user testing remain release gates in &lt;a href="https://github.com/adkbbx/GTM_Shortlist/docs/IMPLEMENTATION.md" rel="noopener noreferrer"&gt;the checklist&lt;/a&gt;. &lt;a href="https://github.com/adkbbx/GTM_Shortlist/docs/PROGRESS.md" rel="noopener noreferrer"&gt;Progress&lt;/a&gt; records what has actually passed.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it with sample data&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;No provider keys are needed. Install from source below, open the local app, and choose &lt;strong&gt;Try it with sample data&lt;/strong&gt; on the setup card. Confirm the sample target, inspect the three invented companies and their evidence, accept or reject a result, and export CSV, JSON or Excel. Every screen and export labels the sample data. Sample source links are plain text because…&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/adkbbx/GTM_Shortlist" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Open source under the MIT licence and built during the challenge: the first commit was on 14 October, and there are 188 so far. 838 automated tests on the server side and 173 browser tests at phone and laptop sizes pass, and every real test I ran with paid services is written down in &lt;code&gt;docs/PROGRESS.md&lt;/code&gt;, including the ones that failed.&lt;/p&gt;

&lt;p&gt;Claude Code wrote the plans, reviewed the changes and ran the tests on my machine, and Codex wrote much of the code from those plans. The project's rules say no paid search or AI call without my approval for that specific test, which is why so many of the fixes above came from re-checking saved runs instead of paying for new searches.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of SerpApi.&lt;/strong&gt; SerpApi is how the hosted app finds companies at all. The app sends short Google searches aimed at companies' own websites, skips results that aren't companies before opening them, and keeps every run to 4 searches. Each search is written down before it's sent, a slow search gets up to 60 seconds, and a failed one is never retried automatically, so the free plan's 250 searches a month go a long way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of DigitalOcean.&lt;/strong&gt; Every AI call the hosted app makes goes to Gemma 4 31B (&lt;code&gt;gemma-4-31B-it&lt;/code&gt;) on DigitalOcean's hosted AI service: describing the business, suggesting campaign ideas, finding quotes and double-checking each one. On the same 12 saved companies for my own business, the small models on my laptop found no product company that matched, and Gemma 4 31B on DigitalOcean found two, each with proof from its own website, in 18 AI calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma does every AI job in the hosted app: it reads your homepage and drafts your offer and must-haves, suggests campaign ideas, finds quotes on each company's own site, and double-checks every quote. The same code runs Gemma 4 31B on DigitalOcean or Gemma 4 E2B on a laptop through Ollama, and running both on the same 12 companies showed this job needs the bigger one: two real matches with proof, against none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Render.&lt;/strong&gt; Render runs the website and the background research worker from the same code, with passwords kept in the dashboard. When I released an update in the middle of a real run, the worker carried on from its notes with no repeated searches and no double charge, and restarting both afterwards left the results exactly the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of a .tech Domain.&lt;/strong&gt; The app is live at &lt;a href="https://eygentic.tech" rel="noopener noreferrer"&gt;eygentic.tech&lt;/a&gt;, the .tech domain I claimed through Hacktoberfest, running on Render. eygentic.com is the company and eygentic.tech is the open tool, and both GIFs in this post were recorded on the live domain. Getting there meant fixing a real "400 Invalid host header" error: the app now accepts a fixed list of addresses, gives each its own sign-in link, and still refuses anything else.&lt;/p&gt;

&lt;p&gt;Built solo.&lt;/p&gt;

&lt;p&gt;If you've shipped something and don't know who to sell it to, put your website into &lt;a href="https://eygentic.tech" rel="noopener noreferrer"&gt;eygentic.tech&lt;/a&gt; and tell me in the comments which company it got wrong.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I asked my junk mail one question: are you worth the walk?</title>
      <dc:creator>ADK BBX</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:21:18 +0000</pubDate>
      <link>https://dev.to/adk_bbx/i-asked-my-junk-mail-one-question-are-you-worth-the-walk-41p8</link>
      <guid>https://dev.to/adk_bbx/i-asked-my-junk-mail-one-question-are-you-worth-the-walk-41p8</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%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanz4qnfs9bxm3mnldgp3.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%2Fanz4qnfs9bxm3mnldgp3.jpg" alt="One week of flyers from my mailbox, fanned out in my hand: a drugstore price list on top, a cleaner's sale, a KUMON leaflet and a meal-delivery coupon behind it" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one week of my mailbox. A drugstore price list with 52 products on it, a cleaner's half-price sale, a pilates studio's trial, KUMON, a meal-delivery coupon, gold buyers, junk removal. Most of it goes straight into paper recycling. A few of these are probably worth leaving the house for. I just never know which ones, because I don't read Japanese well enough to go through a stack like this every week.&lt;/p&gt;

&lt;p&gt;So I built an app that reads them for me.&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%2Fq2fovuvwdgf1jp1b5p26.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%2Fq2fovuvwdgf1jp1b5p26.jpg" alt="Five flyers laid out on a mat, each tagged with what the app decided: two worth a walk, two to use at home, one to recycle" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Five of them, tagged with what the app decided. The pilates studio and the cleaner are worth a walk. The sushi menu and the meal kit are things to use at home. The gold buyer goes in the recycling.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Worth the Walk?&lt;/strong&gt; You photograph your flyers, one photo per page. Gemma 4 reads each page and the app sorts every flyer into one of four piles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pile&lt;/th&gt;
&lt;th&gt;What goes there&lt;/th&gt;
&lt;th&gt;What happens to it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Worth a walk&lt;/td&gt;
&lt;td&gt;a place you can go to: a cleaner's sale, a studio trial, a shop&lt;/td&gt;
&lt;td&gt;it becomes a stop on today's walk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use at home&lt;/td&gt;
&lt;td&gt;delivery menus, coupons&lt;/td&gt;
&lt;td&gt;kept, with its deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep, not an ad&lt;/td&gt;
&lt;td&gt;anything that looks like a bill or a notice&lt;/td&gt;
&lt;td&gt;nothing from it is stored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recycle&lt;/td&gt;
&lt;td&gt;gold buyers, junk removal, the rest&lt;/td&gt;
&lt;td&gt;paper recycling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer looks like a train ticket for today. The places worth a walk sit on a route line in walking order, starting from where you are or from a station you pick. Each one shows its best printed deal on a yellow sticker, the same way the flyers do it (&lt;em&gt;Trial ¥1,000, was ¥3,300&lt;/em&gt;). The app picks the stops itself. If there's one you don't want, you remove it, and that choice (along with every stamp you collect) is what TabPFN learns from.&lt;/p&gt;

&lt;p&gt;When you start the walk, the screen turns into the display above a train door: &lt;em&gt;NEXT 次は&lt;/em&gt;, the place, a little line map of your stops, and how to use the deal. At each stop you tap &lt;strong&gt;I'm here&lt;/strong&gt;. One GPS reading, checked on the phone, gives you an ink stamp like the ones at Japanese train stations, with a different ink for each kind of place. Tap what you actually used and the savings add up, but only from prices printed on the flyer. At the end the ticket comes back punched, with a 済 ("done") stamp and the day's totals.&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%2Fnopwzhsiiigp09n8gm70.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%2Fnopwzhsiiigp09n8gm70.png" alt="Six phone screens from left to right: photograph the pile, Gemma 4 reads a real drugstore flyer, a ticket for today, walk mode, an ink stamp at a stop, and the finished walk" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no account. Photos are read and thrown away, and everything else stays on your phone. You can add it to your home screen, and a walk you've started will reopen even with no signal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Taking it outside
&lt;/h2&gt;

&lt;p&gt;On Monday afternoon I put the drugstore flyer from that pile through the live app. It was the only place on the day's ticket: a Welcia three minutes away, 211 m. I switched to walk mode and went.&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%2F71cb1zt6mlndx5mpeqd0.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%2F71cb1zt6mlndx5mpeqd0.jpg" alt="The app in walk mode at the drugstore: stop 1 of 1, 3 minutes, 211 m, and the stamp" width="800" height="1778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fieu2e3fdbh6oq0cmqcfs.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%2Fieu2e3fdbh6oq0cmqcfs.jpg" alt="The drugstore from the street, on a clear afternoon" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stamp is the dashed kind, the one the app gives when it stamps without a GPS reading.&lt;/p&gt;

&lt;p&gt;It's a short walk, which is kind of the point. That flyer had been sitting in my mailbox all week. It took three minutes to go and look.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it got here
&lt;/h2&gt;

&lt;p&gt;My first plan for this challenge was a grocery app: 547 supermarket branches around 74 stations, which point cards each one takes, and when the bonus days are. The next step was going to be scanning supermarket flyers for deals. Then two things happened on Sunday.&lt;/p&gt;

&lt;p&gt;First I tested the models. Gemma 4 E2B, running on my laptop, read a whole supermarket page and got 0 of 8 items right. It put real prices next to products that weren't on the page. Gemma 4 31B on DigitalOcean got a cropped section of the same page 4 out of 4, member and non-member prices included.&lt;/p&gt;

&lt;p&gt;Then I emptied my mailbox. Fifteen flyers, and not one of them from a supermarket. Gold buyers, junk removal, a hot-yoga trial, an aikido dojo, sushi delivery, a cleaner's half-price sale. Supermarket flyers do come some weeks. Most weeks it looks like this.&lt;/p&gt;

&lt;p&gt;So I rebuilt the app around the mailbox I actually have. Any flyer goes in, the app sorts it, and the places worth visiting become a walk. The grocery data stayed on as station locations and an optional grocery stop on the way.&lt;/p&gt;

&lt;p&gt;The first phone test failed in a boring way. I took seven photos, tapped Read at the bottom of the screen, and nothing seemed to happen, because the progress line was at the top and off screen. I refreshed and lost every photo. That's why there's now a reading card with a progress bar, cards that appear as each flyer finishes, photos that wait on the phone until they've been read, and a screen that stays on while it works.&lt;/p&gt;

&lt;p&gt;Then ten real photos went through, and the real problems showed up.&lt;/p&gt;
&lt;h3&gt;
  
  
  The flyers that fooled it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A logo isn't text.&lt;/strong&gt; The sushi menu's back page only has the shop name as a brush-style logo. Gemma read it as two different restaurants on two runs, so the back page never joined its front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One shop, two spellings.&lt;/strong&gt; The cleaner's front page says "Good!" and the back says "クリーニング Good!". Pages now group when one name sits inside the other. On one run Gemma wrote the name in katakana instead and they split again, so now you can tap a page and pick which flyer it belongs to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A phone number cut in half.&lt;/strong&gt; A big 0120 number fell across two tiles, and each tile only read half of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model copied my example.&lt;/strong&gt; My prompt said "phones, e.g. 0120-123-456", and that exact number turned up on a gold buyer's flyer. It also broke the grouping, because it looked like another shop's phone. The prompt has no example digits now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Almost-JSON.&lt;/strong&gt; Gemma sometimes drops a quote before a key. The parse failed, the retry doubled the cost, and the second reading lost the comparison prices. The parser fixes that now before giving up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Class times look like opening hours.&lt;/strong&gt; A dojo prints "毎週土曜 10:00～12:00". At 4 pm it counted as closed and dropped out of every walk. Lessons and events never count as closed now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The drugstore that had no prices.&lt;/strong&gt; On Monday I photographed the drugstore flyer: 52 products, each with a price before and after tax. The app found the flyer and showed none of the prices. The first pass reads the whole page, and when a page is a dense price list it's told to skip the items and describe them instead. A rule in code then decides whether to read the page again in tiles, and that rule didn't recognise this page as a price list. Supermarket and drugstore flyers now always get the second pass, and the same photo comes back with all 52 products and their prices.&lt;/p&gt;

&lt;p&gt;Here's how one real mailbox scored on Sunday evening:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flyer&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Business name&lt;/th&gt;
&lt;th&gt;Pile&lt;/th&gt;
&lt;th&gt;Offer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dinner delivery&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right (home)&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sushi delivery&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;front right, back wrong (logo)&lt;/td&gt;
&lt;td&gt;right (home)&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dry cleaner&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right (walk)&lt;/td&gt;
&lt;td&gt;¥297 → ¥98, ¥987 → ¥326&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pilates studio&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right (walk)&lt;/td&gt;
&lt;td&gt;¥3,300 → ¥1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gold buyer&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;right, pages grouped&lt;/td&gt;
&lt;td&gt;right (recycle)&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Junk removal&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right (recycle)&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Appliance pickup&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;td&gt;right (recycle)&lt;/td&gt;
&lt;td&gt;right&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every pile and offer was right. Business names were right apart from the logo.&lt;/p&gt;

&lt;p&gt;Late on Sunday it worked, and I still wouldn't have used it. Every flyer was a card with every field on it, in four tabs, with thumbs up and down buttons. Looking at it on my phone, I wouldn't have read any of it. So it became one answer: the app picks the stops, draws them as a ticket, puts the best printed price on a sticker, and all you do is remove what you don't want. Then you walk.&lt;/p&gt;

&lt;p&gt;Early on Monday I opened it like someone who had never seen it, and it was a station picker and a camera button with no explanation. Now the first screen shows what happens, it only asks where you're starting when it needs to, and &lt;strong&gt;Try a sample mailbox&lt;/strong&gt; runs the whole thing with eight made-up flyers, so you can try it without any Japanese mail.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&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%2F7mblgr8kh4jxyfegnhoz.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%2F7mblgr8kh4jxyfegnhoz.png" alt="Architecture: the phone, a Node server on Render running the Mastra workflows, and the models and services around it, with the open pieces marked" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Reading flyers: whole pages first, tiles only for price lists (Gemma 4 + Mastra)
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B (local, Ollama)&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;td&gt;0/8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B&lt;/td&gt;
&lt;td&gt;one section&lt;/td&gt;
&lt;td&gt;2/4 (若鶏もも肉 became "sheep wool meat")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B (DigitalOcean)&lt;/td&gt;
&lt;td&gt;full page&lt;/td&gt;
&lt;td&gt;wrong prices, invented eggs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;one section&lt;/td&gt;
&lt;td&gt;4/4, both prices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tiles fixed the accuracy, but six calls per page made the first real test take minutes. Most of my mailbox isn't price lists, though. A gold buyer, a junk-removal service or a yoga studio is big text, a phone number and one offer, and read whole, all four of those came back right in one call each (4 to 26 s).&lt;/p&gt;

&lt;p&gt;So reading is adaptive. The phone crops each photo to the flyer and sends the whole page first. A Mastra &lt;code&gt;readPage&lt;/code&gt; workflow reads it and the card shows up within seconds. A rule in code then decides whether the page is a price list worth a closer look (a supermarket, a drugstore, a cleaner's price table, a takeout menu). Only those get a second pass as a 2×3 grid of overlapping tiles, read in parallel and streamed back while the card says "Reading prices…".&lt;/p&gt;

&lt;p&gt;A text-only &lt;code&gt;mergeFlyer&lt;/code&gt; call combines the readings from every page of one flyer, since the offer is often on the front and the address on the back. Price lists are merged in code, which took merging from 232 s to 87 s on my test pile. Then I timed the whole thing through the real app and found the merge was still the slow part: five single-page flyers took four and a half minutes, because each one waited for a merge call that had nothing to merge. A flyer that was read whole in one call is now sorted in code. The same five pages now go from photos to a ticket in 38 seconds, into the same piles.&lt;/p&gt;

&lt;p&gt;The model only copies what's printed. Code decides the pile, which pages belong together, the deadline, and what counts as a saving.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  Finding the places: GSI, SerpApi and OpenStreetMap
  &lt;p&gt;Printed addresses go to &lt;a href="https://msearch.gsi.go.jp/" rel="noopener noreferrer"&gt;GSI&lt;/a&gt; (国土地理院), Japan's free government geocoder, which needs no API key. When a flyer worth a walk has no usable address, &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; looks the business up on Google Maps. If Google's phone number matches the one on the flyer, the card says "phone matches the flyer". If SerpApi finds nothing, the app tries OpenStreetMap by name.&lt;/p&gt;

&lt;p&gt;The free SerpApi plan has 250 searches a month, so only walkable flyers without an address can trigger a search, results are cached for 30 days, every search is counted before it's made with daily and monthly caps and a reserve of 20, and the tests run against a network that throws, so a test can't spend one.&lt;/p&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8etifxcrlhw9mbe5a87v.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%2F8etifxcrlhw9mbe5a87v.png" alt="SerpApi dashboard: 10 searches out of 250 this month" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi after the whole build: 10 searches out of 250, none failed, and the busiest day stopped at 8, the app's own daily cap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Learning what you go to: TabPFN through Prior Labs' MCP
  &lt;p&gt;The places you remove and the stamps you collect train &lt;a href="https://priorlabs.ai" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; through Prior Labs' remote MCP server. Mastra's MCP client does upload, fit and predict, and reuses the fit until your choices change. TabPFN only sees a row of numbers per flyer: category, a distance bucket, offer type, whether it's a free trial, printed value, days left and three yes/no flags. No names, addresses or flyer text. On a synthetic check it scored a nearby free trial at 0.9996 and a far-away flyer with no offer at 0.0004, in 11.6 s. With fewer than six choices, or if anything fails, simple rules take over.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  Watching the agent: Sentry traces with no flyer content
  &lt;p&gt;The Mastra Sentry exporter keeps the model, tokens, cost and timing, and drops prompts, outputs, images and any URL with a query string, because these are photos of someone's mail. Sentry's cost numbers put one tile at about $0.00044, so reading a page costs about a quarter of a cent.&lt;/p&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs28los3cozjovhz3g9pw.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%2Fs28los3cozjovhz3g9pw.png" alt="A readPage trace in Sentry: eight Gemma 4 calls for one page, with tokens and cost, and no input stored" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One &lt;code&gt;readPage&lt;/code&gt; trace in Sentry: eight Gemma 4 calls for one drugstore page, mostly in parallel, 29.8 s in all. Every pink &lt;code&gt;chat gemma-4-31B-it&lt;/code&gt; bar is one call to Gemma 4 31B on DigitalOcean's serverless inference. The selected span shows the agent, the model, 1.7K tokens in and 497 out, a cost under a cent, and "No input for this span". The flyer itself never reaches Sentry.&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%2Ffg4hcant0ps9joqmfq8h.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%2Ffg4hcant0ps9joqmfq8h.png" alt="Sentry's AI Agents view: three agents, tokens and cost per run, and merges that cost nothing" width="799" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sentry's AI Agents view: the page reader, tile reader and merger, every run's tokens and cost, and no errors. The &lt;code&gt;mergeFlyer&lt;/code&gt; rows with 0 tokens and $0 are flyers sorted in code instead of by the model.&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%2F7n9c3k41sb6lqh3x8iw9.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%2F7n9c3k41sb6lqh3x8iw9.png" alt="A mergeFlyer trace: the workflow, its merge-and-locate step, the flyer-merger agent and its Gemma 4 call" width="799" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Mastra structure as Sentry records it: the &lt;code&gt;mergeFlyer&lt;/code&gt; workflow, its &lt;code&gt;merge-and-locate&lt;/code&gt; step, the &lt;code&gt;flyer-merger&lt;/code&gt; agent and its Gemma 4 call. 1.2K tokens, under a cent, 2.89 s against this agent's 23.6 s average, and again no input stored.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Walks, opening hours and stamps
  &lt;p&gt;The station data came from the grocery app that didn't work out. Stops are ordered by trying every order, and the lowest-priority stop is dropped until the walk fits your time. The hours parser handles 83 store formats: &lt;code&gt;25:00&lt;/code&gt;, &lt;code&gt;〜&lt;/code&gt; and &lt;code&gt;－&lt;/code&gt;, full-width digits, 平日 vs 土日祝, "closes at 21:00 on weekends", public holidays. A stamp is one GPS reading, compared on the phone, and the location never leaves it. If you start from where you are, the server only ever gets that start rounded to about 1 km, for address lookups.&lt;/p&gt;



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

&lt;p&gt;&lt;/p&gt;
  Running it: Render
  &lt;p&gt;One Node web service from a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint. Keys are kept in the dashboard, not the repo. Each IP gets hourly limits on reading, merging, lookups and ranking, and &lt;code&gt;X-Forwarded-For&lt;/code&gt; is only trusted behind Render's proxy, so the limits can't be dodged with a fake header.&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%2Fljminxkv8gsquyfh5bz5.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%2Fljminxkv8gsquyfh5bz5.png" alt="Render environment variables, every value masked" width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuv4zdvq6fqn7mitltvw.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%2Fcuv4zdvq6fqn7mitltvw.png" alt="Render: the Blueprint-managed worth-the-walk service and its auto-deploys" width="799" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Render: one Blueprint-managed Node service on the Starter plan. Every push deployed in about a minute, 30 times over the build.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Not everything in this app is open, so it's worth being clear about which parts are. The open pieces do the core work. Gemma 4 (open weights, Apache 2.0) reads every flyer and merges the multi-page ones. Mastra (Apache 2.0) runs every workflow and agent. The services around them are closed, and each one can be switched off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Open?&lt;/th&gt;
&lt;th&gt;Without it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B / E2B&lt;/td&gt;
&lt;td&gt;reads, translates and merges every flyer&lt;/td&gt;
&lt;td&gt;open weights, Apache 2.0&lt;/td&gt;
&lt;td&gt;no app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;workflows, agents, the MCP client, the trace exporter&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;no app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;hosts Gemma 4 31B&lt;/td&gt;
&lt;td&gt;hosted service, open model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LOCAL_LLM=1&lt;/code&gt; runs Gemma 4 E2B on my laptop with Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN via Prior Labs' MCP&lt;/td&gt;
&lt;td&gt;ranks flyers from my choices and stamps&lt;/td&gt;
&lt;td&gt;open-source code, hosted API&lt;/td&gt;
&lt;td&gt;simple ranking rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;finds places that print no address&lt;/td&gt;
&lt;td&gt;hosted service&lt;/td&gt;
&lt;td&gt;OpenStreetMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenStreetMap (Nominatim)&lt;/td&gt;
&lt;td&gt;finds a shop by name when nothing else did&lt;/td&gt;
&lt;td&gt;open data, free, no key&lt;/td&gt;
&lt;td&gt;printed addresses only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSI address search&lt;/td&gt;
&lt;td&gt;turns printed addresses into map points&lt;/td&gt;
&lt;td&gt;free public service&lt;/td&gt;
&lt;td&gt;not needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;traces every model call&lt;/td&gt;
&lt;td&gt;hosted service&lt;/td&gt;
&lt;td&gt;no tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;hosts the web app&lt;/td&gt;
&lt;td&gt;hosted service&lt;/td&gt;
&lt;td&gt;any Node host, or &lt;code&gt;npm start&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I tested that last column on Monday morning with the finished app. With every key removed and &lt;code&gt;LOCAL_LLM=1&lt;/code&gt;, three real flyers from my mailbox went from photos to a ticket in 35 seconds on my laptop. GSI placed a printed address, OpenStreetMap found a chain store by name, and the ranking ran on rules. It cost nothing and needed no account. The only things that left my machine were an address for GSI and a shop name for OpenStreetMap.&lt;/p&gt;

&lt;p&gt;Here's what being open actually changed for me.&lt;/p&gt;

&lt;p&gt;I get to choose where my mail is read. Hosted, the photos go to DigitalOcean to be read and the app never stores them. With &lt;code&gt;LOCAL_LLM=1&lt;/code&gt; they never leave my laptop. A closed API doesn't give you that option.&lt;/p&gt;

&lt;p&gt;I could test the models on my own flyers before trusting them. That 0 out of 8 against 4 out of 4 decided the whole design: price lists are read in tiles, everything else in one call.&lt;/p&gt;

&lt;p&gt;It's cheap enough that I didn't need accounts. A fully tiled page costs about a quarter of a cent and a whole mailbox about five cents, so there's no sign-up and no subscription.&lt;/p&gt;

&lt;p&gt;The local version works, but it's worse. E2B put two of those three flyers in the right pile (it sent a dinner-delivery menu to recycling) and made up the yoga studio's prices: ¥500, ¥150 and ¥100 for a flyer that prints a ¥3,000 trial. So local mode tells you to check every price. Running it at home is a real option, not a good one yet, and a bigger Gemma 4 is one setting away (&lt;code&gt;LOCAL_LLM_MODEL&lt;/code&gt;) if your machine can hold it. I only know all this because the weights are open.&lt;/p&gt;

&lt;p&gt;The code is MIT licensed.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/nWed_wsqI28" width="710" height="399"&gt;
  &lt;/iframe&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%2Fy1odkoq0yagbfgvl4880.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%2Fy1odkoq0yagbfgvl4880.gif" alt="Trying the sample mailbox: a ticket of four made-up places, walk mode, an ink stamp at each stop, and the finished ticket" width="300" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://worth-the-walk.onrender.com" rel="noopener noreferrer"&gt;worth-the-walk.onrender.com&lt;/a&gt;&lt;/strong&gt;, no sign-up. If you don't have any Japanese flyers, tap &lt;strong&gt;Try a sample mailbox&lt;/strong&gt;. It places made-up shops near you so you can see the ticket, walk mode and a stamp in under a minute.&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/adkbbx" rel="noopener noreferrer"&gt;
        adkbbx
      &lt;/a&gt; / &lt;a href="https://github.com/adkbbx/worth-the-walk" rel="noopener noreferrer"&gt;
        worth-the-walk
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Worth the Walk?&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Photograph the flyers in your mailbox, read what each one is in English, and plan a walk from where you are (or a station you pick) to the places worth visiting. Each stop gives you an ink stamp.&lt;/p&gt;
&lt;p&gt;No Japanese flyers to hand? Open the app and tap &lt;strong&gt;Try a sample mailbox&lt;/strong&gt;: eight made-up flyers, placed around your start, with no model calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install and offline:&lt;/strong&gt; Worth the Walk? is installable on your phone. After an online visit, the app shell is cached network-first so a walk in progress reopens without signal, with your saved walk and stamps intact. Reading flyers still needs a connection.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Photograph the pages. Gemma 4 reads each page whole first (one call), so its card appears within seconds. Only walkable price lists (supermarkets, drugstores, cleaners, menus) get a second pass in six overlapping tiles for exact prices.&lt;/li&gt;
&lt;li&gt;The…&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/adkbbx/worth-the-walk" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed and built during the challenge week. There are 147 offline tests and a Playwright run at phone and desktop size, in light and dark. The Playwright run covers scanning, sorting, ranking, planning, both kinds of stamp, savings, a reload in the middle of reading, a second browser that sees none of the first one's data, going offline mid-walk, a rate-limited read, and contrast, focus and 200% text checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it with two agents
&lt;/h2&gt;

&lt;p&gt;Claude Code was the architect. It wrote the specs and the contracts between modules, reviewed every diff, ran the tests and checked the screenshots. Codex CLI wrote most of the modules from those specs. On the last night I left them running in a loop for five hours: Claude wrote a spec per round (onboarding, visual polish, the walk, error states, accessibility, offline, speed), Codex built it, and Claude ran the browser tests, looked at the screenshots and sent back what was wrong. When Codex hit its usage limit around 6:30 a.m., Claude carried on with the smaller fixes.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Sentry Agent Tracing.&lt;/strong&gt; Every page read and every merge is one trace: the &lt;code&gt;readPage&lt;/code&gt; workflow, the &lt;code&gt;read-tiles&lt;/code&gt; step, the &lt;code&gt;flyer-reader&lt;/code&gt; agent and its Gemma 4 call, with model, tokens, cost and timing. Since these are photos of people's mail, a span processor (&lt;code&gt;omit-flyer-payloads&lt;/code&gt;) drops every prompt, output, image and query-string URL before export, and an allow-list keeps only attributes I added on purpose: page and tile counts, whether a flyer was sorted in code or by the model (&lt;code&gt;chirashi.merge&lt;/code&gt;), its pile, and where its location came from (&lt;code&gt;gsi&lt;/code&gt;, &lt;code&gt;serpapi&lt;/code&gt; or &lt;code&gt;osm&lt;/code&gt;). The cost per tile in this post comes from Sentry's numbers. The three Sentry screenshots above show it running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Mastra.&lt;/strong&gt; Mastra runs the reading pipeline. The &lt;code&gt;readPage&lt;/code&gt; workflow streams tiles back while the card says "Reading prices…", and the &lt;code&gt;mergeFlyer&lt;/code&gt; workflow merges, sorts and locates a flyer in one step. Three agents (page reader, tile reader, merger) share one prompt schema, Mastra's MCP client talks to TabPFN, and its Sentry exporter produces the traces above. When a flyer was read whole in one call, the workflow skips the merger agent and sorts it in code, which is what took five real flyers from four and a half minutes to 38 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of SerpApi.&lt;/strong&gt; Only flyers worth a walk that print no address get looked up, by business name near your start (rounded to about 1 km). If Google's phone number matches the flyer's, the card says so, which is also a decent way to tell a real shop from a door-to-door buyer. On the free plan every search is counted before it's made, with daily and monthly caps, a reserve, a 30-day cache, and tests that can't spend a search. The whole build used 10 of 250. When SerpApi finds nothing, OpenStreetMap is the fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN.&lt;/strong&gt; The places you remove and the stamps you collect are the labels, so there's nothing extra to rate. Through Prior Labs' remote MCP, TabPFN ranks the mailbox from nine numbers per flyer, never a name, address or flyer text, and reuses the fit until your choices change. I've only checked it on synthetic rows so far (0.9996 for a nearby free trial, 0.0004 for a far flyer with no offer, in 11.6 s), and with fewer than six choices simple rules take over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 31B reads Japanese flyers into English and structured prices (all 52 products on one drugstore page), and Gemma 4 E2B runs the same app on a laptop with no keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of DigitalOcean.&lt;/strong&gt; Every flyer the hosted app reads goes to Gemma 4 31B (&lt;code&gt;gemma-4-31B-it&lt;/code&gt;) on DigitalOcean's serverless inference, through its OpenAI-compatible endpoint: every page read, tile read and model merge. Mastra uses it like any other provider, and &lt;code&gt;LOCAL_LLM=1&lt;/code&gt; swaps in Ollama with no code changes. DigitalOcean is also where I ran the test that shaped the app: 31B there against E2B on my laptop, on the same pages. On my 10-page pile a whole page comes back in 4 to 30 s (about 7 s typical), and Sentry's cost numbers put a tile at about $0.00044, so a whole mailbox costs about five cents. That price is why the app has no sign-up.&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%2Fy8ecql1k70vic7ig4o55.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%2Fy8ecql1k70vic7ig4o55.png" alt="DigitalOcean serverless inference dashboard for the last 7 days: one model, gemma-4-31B-it, with input and output tokens and token usage over time" width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;DigitalOcean's serverless inference dashboard: one model, &lt;code&gt;gemma-4-31B-it&lt;/code&gt;. The climb on the right, 11 and 12 October, is this app being built and tested on real flyers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Render.&lt;/strong&gt; One Node service from a Blueprint, with secrets in the dashboard, per-IP limits that respect the proxy, compression and caching, a service worker so a walk reopens without signal, and a deploy on every push.&lt;/p&gt;

&lt;p&gt;Built solo.&lt;/p&gt;

&lt;p&gt;If your mailbox looks like mine, photograph it and tell me which flyer it got wrong.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Phone calls are the boss fight of living in Japan. 🗼</title>
      <dc:creator>ADK BBX</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:42:34 +0000</pubDate>
      <link>https://dev.to/adk_bbx/phone-calls-are-the-boss-fight-of-living-in-japan-1h6h</link>
      <guid>https://dev.to/adk_bbx/phone-calls-are-the-boss-fight-of-living-in-japan-1h6h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&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%2Fjtp63ghvcqumhy7x8rr3.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%2Fjtp63ghvcqumhy7x8rr3.gif" alt="A practice call in Before I Call: a Japanese question with furigana, a word's meaning on hover, then a plain-English explanation of the question" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've lived alone in Tokyo for four years now, but when my washing machine started leaking, I stared at the building manager's number for twenty minutes before I pressed call.&lt;/p&gt;

&lt;p&gt;My friend ran into the same thing when she had to cancel her internet contract by phone. The provider's explanation sounded alien to her. The Japanese businesses use on the phone is not the Japanese you use every day, and on a call there's no face to read and no time to look anything up.&lt;/p&gt;

&lt;p&gt;So I built Before I Call, an app where you practice the call with an AI before you make the real one. I made it for myself and for friends in the same situation, and sent it to one of them to see if it actually helps. Their reply is further down.&lt;/p&gt;

&lt;p&gt;Here it is in action:&lt;/p&gt;

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

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

&lt;p&gt;You describe the call you need to make, or pick one of the examples (home repairs, the clinic, a missed delivery, the city office, dietary requests, lost property, bills). Then you talk to an AI that plays the person on the other end, like a receptionist or your building manager. It speaks polite Japanese, asks one question at a time and waits for your answer. There's no score.&lt;/p&gt;

&lt;p&gt;If you get stuck, you don't have to hang up. &lt;strong&gt;Explain question&lt;/strong&gt; opens a helper next to the call with what the question means and a reply you could give. The AI on the call doesn't see it, so the role-play carries on. &lt;strong&gt;Slow replay&lt;/strong&gt; plays the question again at 0.7x speed, and &lt;strong&gt;Repeat question&lt;/strong&gt; asks the AI to say it again. Kanji have furigana, every line has romaji, and known words show their meaning when you hover or tap.&lt;/p&gt;

&lt;p&gt;When you say goodbye, the AI hangs up and you get a call card: the whole conversation with readings and meanings, and a list of useful words from your call. You can download it as a PDF and keep it next to you when you make the real call.&lt;/p&gt;

&lt;p&gt;It won't make up dates, prices or availability, and it can't book anything for you. It's only for practice.&lt;/p&gt;

&lt;p&gt;Setting up a call. You can type or dictate it in English or Japanese, or start from an example. &lt;strong&gt;Enhance prompt&lt;/strong&gt; tidies up rough notes without adding facts you didn't give it.&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%2Fytxcbot3v0454u2tctqn.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%2Fytxcbot3v0454u2tctqn.png" alt="The setup form: a washing-machine leak typed in English, with Start from an example, Enhance prompt, Dictate situation and Start voice practice" width="800" height="876"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Asking for help in the middle of the 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%2F9rkr0mhmh6tyk6aii5ed.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%2F9rkr0mhmh6tyk6aii5ed.png" alt="The explanation dialog: the question's meaning, a note on どうされましたか, and a suggested reply with furigana" width="800" height="822"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The word list on the call card:&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%2Fwh3dmo649m9jukkb83yo.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%2Fwh3dmo649m9jukkb83yo.png" alt="The call card's useful words: 管理会社, 田中 and 洗濯機 with furigana, romaji and English meanings" width="800" height="1011"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What my friend said
&lt;/h3&gt;

&lt;p&gt;I sent the link to my friend on WhatsApp and asked them to let me know if it helps. This is what came back:&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%2Frkznedsexrxwtt7iz67l.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%2Frkznedsexrxwtt7iz67l.png" alt="WhatsApp chat: I share the Before I Call link with my friend. They reply " width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I didn't expect the PDF to be the part they liked. I made it as a cheat sheet for the real call, but for someone studying for the JLPT it's also a word list from a conversation they actually had.&lt;/p&gt;

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

&lt;p&gt;Try it at &lt;a href="https://before-i-call.onrender.com" rel="noopener noreferrer"&gt;before-i-call.onrender.com&lt;/a&gt;. There's no sign-up. Voice practice needs a microphone. Without one, tap &lt;strong&gt;Play guided example&lt;/strong&gt; to watch a recorded call in Japanese or English.&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%2F2ky1gf298ag9sk66wstt.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%2F2ky1gf298ag9sk66wstt.png" alt="The PDF call card: the situation, each turn as a speech bubble with furigana, romaji and meaning" width="800" height="1132"&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/adkbbx" rel="noopener noreferrer"&gt;
        adkbbx
      &lt;/a&gt; / &lt;a href="https://github.com/adkbbx/before_I_call" rel="noopener noreferrer"&gt;
        before_I_call
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/adkbbx/before_I_call/docs/images/logo.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fadkbbx%2Fbefore_I_call%2FHEAD%2Fdocs%2Fimages%2Flogo.svg" width="76" alt="Before I Call logo"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Before I Call&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Rehearse the phone call you've been putting off.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;A patient AI voice partner for everyday Japanese and English calls, with furigana on every word,&lt;br&gt;
help that never interrupts the conversation, and a call card to take with you.&lt;br&gt;
Use it in the cloud, or &lt;b&gt;free and offline on your own computer&lt;/b&gt; with Gemma 4, Whisper and Kokoro.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://before-i-call.onrender.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/73e3fad786c3d941e2b2cf53d224bdb71b0c835aca00f8c398a0a5d0d3b3b520/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5472795f69745f6c6976652d6265666f72652d2d692d2d63616c6c2e6f6e72656e6465722e636f6d2d3331356234393f7374796c653d666f722d7468652d6261646765" alt="Try it live"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://deepmind.google/models/gemma/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/078f8dc5422b0d1ea9b38b5a6b6c2cf2fd6c5479f3843b538607706d6de97423/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f47656d6d615f342d6f70656e5f776569676874732d3161373365383f7374796c653d666c61742d737175617265" alt="Gemma 4 open weights"&gt;&lt;/a&gt;
&lt;a href="https://elevenlabs.io/agents" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6fe212177af7800b3b9bba35df7a8e02e43a2a7ca1acd47de3bb249896e5aed9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f456c6576656e4c6162732d4167656e74732d3030303030303f7374796c653d666c61742d737175617265" alt="ElevenLabs Agents"&gt;&lt;/a&gt;
&lt;a href="https://docs.digitalocean.com/products/gradient-ai-platform/how-to/use-serverless-inference/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/662624bae698400e417d417d9ea67a611aa3e82d45b324652098e6a227f8cf6e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4469676974616c4f6365616e2d7365727665726c6573735f696e666572656e63652d3030383066663f7374796c653d666c61742d737175617265" alt="DigitalOcean serverless inference"&gt;&lt;/a&gt;
&lt;a href="https://docs.sentry.io/product/insights/ai/agents/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e005abc142c60935dff2440bcb899265550ac0978fe2eb52f1cbc991bf6d63d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53656e7472792d6167656e745f74726163696e672d3336326435393f7374796c653d666c61742d737175617265" alt="Sentry agent tracing"&gt;&lt;/a&gt;
&lt;a href="https://render.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dfc69bd58faad7e0a384a6da7a417d3f61354a98794e6533b3f2517dfa704578/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52656e6465722d6465706c6f7965642d3436653362373f7374796c653d666c61742d737175617265" alt="Deployed on Render"&gt;&lt;/a&gt;
&lt;a href="https://github.com/adkbbx/before_I_call/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e04ea5c0d2a58a0233a89eda6eb5b38d982abc818aa2bdb30142f21a1eff0f1b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d3331356234393f7374796c653d666c61742d737175617265" alt="MIT license"&gt;&lt;/a&gt;
&lt;a href="https://github.com/adkbbx/before_I_call#free-local-mode-on-your-own-computer" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/da7581c0e6ea9ed2949c5267fb431ed06f53f9f67871eb3304b6e7e8e548537c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f667265655f6c6f63616c5f6d6f64652d576869737065725fc2b75f47656d6d615fc2b75f4b6f6b6f726f2d6530613532363f7374796c653d666c61742d737175617265" alt="Free local mode with Whisper, Gemma and Kokoro"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/e4f202733aede36bad72b88e8ebe6482bc52a311a8cdebcd913cd87b3dbb969c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d38375f70617373696e672d3331356234393f7374796c653d666c61742d737175617265"&gt;&lt;img src="https://camo.githubusercontent.com/e4f202733aede36bad72b88e8ebe6482bc52a311a8cdebcd913cd87b3dbb969c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d38375f70617373696e672d3331356234393f7374796c653d666c61742d737175617265" alt="87 tests passing"&gt;&lt;/a&gt;
&lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/a5340b509445a5480c2fab7253ba6e314df05f1d4dd45982775e981a0b92cc36/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4861636b746f626572666573742d323032362d3364356635383f7374796c653d666c61742d737175617265" alt="Hacktoberfest 2026"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://before-i-call.onrender.com" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Live app&lt;/strong&gt;&lt;/a&gt; · &lt;a href="https://github.com/adkbbx/before_I_call#how-a-practice-call-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; · &lt;a href="https://github.com/adkbbx/before_I_call#free-local-mode-on-your-own-computer" rel="noopener noreferrer"&gt;Free local mode&lt;/a&gt; · &lt;a href="https://github.com/adkbbx/before_I_call/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/adkbbx/before_I_call#run-it-locally" rel="noopener noreferrer"&gt;Run it locally&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;

  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fadkbbx%2Fbefore_I_call%2FHEAD%2Fdocs%2Fimages%2Fhome-light.png" width="900" alt="Before I Call home page: practice a phone call in Japanese or English, or play a guided example"&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A phone call is the hardest everyday conversation in a second language. There is no face to read and no time to look anything up, and the other person speaks at native speed. People who live in Japan put off calling the building manager, the clinic or the delivery company, not because they can't manage the conversation, but because the call itself feels risky.&lt;/p&gt;
&lt;p&gt;Phrasebooks help with the first sentence. They don't…&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/adkbbx/before_I_call" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed and built during the challenge. It has 87 tests (80 Python, 7 Node), and the README explains how to run it locally.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The call runs on ElevenLabs Agents
&lt;/h3&gt;

&lt;p&gt;The live call is an ElevenLabs agent. It handles all the audio: the WebRTC connection, speech recognition, working out when you've finished speaking, interruptions, and the voice. I use one agent for every scenario. When a call starts, the app sends that call's prompt, first line, language and voice as overrides, so the same agent can be a Japanese building manager or an English clinic receptionist.&lt;/p&gt;

&lt;p&gt;I slowed the agent's speech to 0.85x for learners and capped calls at two minutes, with a daily limit on the server so the credits can't all go in one day. Hanging up uses the built-in &lt;code&gt;end_call&lt;/code&gt; tool. Its description tells the model to use it only after your question is dealt with and you've said you don't need anything else, not when you just say thanks. &lt;strong&gt;Explain question&lt;/strong&gt; opens a second, text-only session with a helper prompt, so the explanation never ends up in the call transcript. &lt;strong&gt;Slow replay&lt;/strong&gt; uses ElevenLabs text to speech (Flash v2.5 at 0.7x).&lt;/p&gt;

&lt;p&gt;The agent has had 113 conversations so far. ElevenLabs groups them by topic on its own, and the top three match the examples in the app: booking an appointment (27), the washing machine (19) and a missed delivery (17).&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%2Fa5lc0pv9s443vs3nf7aw.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%2Fa5lc0pv9s443vs3nf7aw.png" alt="ElevenLabs agent dashboard: 113 conversations, with topics grouped as Appointment Booking and Scheduling (27), Washing Machine Issue Resolution (19) and Missed Delivery Redelivery Request (17)" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I switched to a custom LLM
&lt;/h3&gt;

&lt;p&gt;When I started, the agent used one of the LLMs hosted by ElevenLabs, Qwen3.5-397B-A17B. Its usage came out of the same ElevenLabs credits I got for Hacktoberfest, and it was going through them fast. ElevenLabs lists it at about $0.016 a minute and $0.003 a message, on top of the voice. A custom LLM costs nothing on the ElevenLabs side:&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%2Ft9semi240lwnyjkulteu.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%2Ft9semi240lwnyjkulteu.png" alt="The ElevenLabs LLM picker: hosted models with their prices, including Qwen3.5-397B-A17B at about $0.0159 per minute and $0.0030 per message, and Custom LLM at $0.0000" width="437" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wanted those credits for the voice, so I switched the agent to a custom LLM and pointed it at Gemma 4 31B on DigitalOcean, which bills my DigitalOcean account instead. With the per-token prices my tracing code uses for Gemma ($0.18 per million input tokens and $0.50 per million output tokens), the turn in the trace further down cost about $0.0002. The ElevenLabs credits now only pay for the audio.&lt;/p&gt;

&lt;p&gt;At first the custom LLM pointed straight at DigitalOcean. Later I put my own server in between so I could trace and check every reply. This is the agent's LLM setting now:&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%2Fzqy1dx7uofaix46lqauc.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%2Fzqy1dx7uofaix46lqauc.png" alt="The agent's Custom LLM setting in ElevenLabs: Chat Completions, server URL https://before-i-call.onrender.com/llm/v1/ and model ID gemma-4-31B-it" width="434" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On each turn, the agent sends the conversation to my FastAPI server on Render, the server forwards it to Gemma 4 31B on DigitalOcean's serverless inference, and the response streams straight back, so the voice can start as soon as the first words arrive.&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%2Fihxfyvrlids4vsau6p4s.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%2Fihxfyvrlids4vsau6p4s.png" alt="Architecture: the browser talks to ElevenLabs over WebRTC; ElevenLabs calls the Gemma proxy on Render, which streams Gemma 4 31B from DigitalOcean and sends traces to Sentry" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The proxy on Render
&lt;/h3&gt;

&lt;p&gt;The custom LLM URL points at my own server: a FastAPI app that Render runs as one Docker web service. The same service serves the React app, the API and the proxy. The whole setup is a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint in the repo (trimmed here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;before-i-call&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;starter&lt;/span&gt;
    &lt;span class="na"&gt;healthCheckPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api/health&lt;/span&gt;
    &lt;span class="na"&gt;disk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;usage-data&lt;/span&gt;
      &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/data&lt;/span&gt;
      &lt;span class="na"&gt;sizeGB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;envVars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LLM_PROXY_KEY&lt;/span&gt;
        &lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GRADIENT_MODEL&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemma-4-31B-it&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MAX_CONCURRENT_CALLS&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MAX_CALL_SECONDS&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;120'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MAX_CALLS_PER_VISITOR_DAY&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys are &lt;code&gt;sync: false&lt;/code&gt;, so they're set in the Render dashboard and never committed. The limits that protect my credits are plain environment variables: two calls at once, two minutes each, three calls per visitor a day. The 1 GB disk holds the SQLite files for those limits and the analytics, so they survive redeploys.&lt;/p&gt;

&lt;p&gt;The proxy is a single route. It accepts only the agent's key, refuses every model except Gemma 4 31B, and passes DigitalOcean's stream through unchanged while it reads each chunk for tracing and the rule checks (trimmed):&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="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/llm/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_completions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# The key is never usable for other, more expensive models.
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Only &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is available.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aiter_bytes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;media_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;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Cache-Control&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no-cache&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Accel-Buffering&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An always-on web service suits this. Each reply is a streamed response that stays open while Gemma talks. The server reuses one HTTPS client for DigitalOcean, so a turn doesn't pay for a new TLS handshake. And when you interrupt the AI, ElevenLabs drops the request, so the proxy closes the stream and records the turn as cancelled in Sentry.&lt;/p&gt;

&lt;p&gt;Here's the proxy at work in Render's logs, one line per Gemma request from the agent:&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%2Fup9w3t6i5dpvgdhpwmzy.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%2Fup9w3t6i5dpvgdhpwmzy.png" alt="Render application logs filtered to POST /llm/v1/chat/completions: a steady list of 200 OK responses on October 4 and 5" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service is managed by the Blueprint, which stays synced to the repo, and every push to main deploys on its own:&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%2Fc7j667vtciwcaf9ajyih.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%2Fc7j667vtciwcaf9ajyih.png" alt="Render Blueprints page: before-i-call, synced with adkbbx/before_I_call on main" width="799" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5qpdffdna3conozbvnee.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%2F5qpdffdna3conozbvnee.png" alt="Render events for the before-i-call web service (Docker, Starter, Blueprint managed, live): deploys started by " width="799" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing how numbers are read
&lt;/h3&gt;

&lt;p&gt;The first version read 7時 as "nana-ji". Nobody says that. Japanese numbers change their reading depending on the counter after them: 7時 is しちじ, 10分 is じゅっぷん, 4月1日 is しがつついたち and 2人 is ふたり. Dates, times and the number of people are exactly what you say on a phone call, so this had to be right.&lt;/p&gt;

&lt;p&gt;I fixed it with a pronunciation dictionary: 118 entries I wrote by hand plus 5,989 generated ones for dates, clock times, durations and counters, 6,107 in total. A script builds the rules and uploads them to the ElevenLabs agent as a pronunciation dictionary, and the app uses the same list for slow replay and the recorded demos. The text on screen doesn't change. This is the dictionary on the agent:&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%2Fhqhetmcb99e2dyr9jv40.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%2Fhqhetmcb99e2dyr9jv40.png" alt="ElevenLabs agent settings: the pronunciation dictionary " width="444" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs2tp1dwigc5iwkzv7luj.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%2Fs2tp1dwigc5iwkzv7luj.png" alt="Japanese voices trip over dates and counters, so I wrote 6,107 fixes: 7時 is shichiji, not nana-ji" width="800" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tuning the prompt for Gemma
&lt;/h3&gt;

&lt;p&gt;Switching to Gemma meant rewriting the prompt. I tested it with text-only runs of the agent. The old prompt never called &lt;code&gt;end_call&lt;/code&gt; (0 of 2 scenarios), and the tuned one hung up in all 4. It also keeps ordinary words in kanji and writes numbers in kana so the voice reads them correctly, and it asks for practice details instead of your real ones. It still missed the hang-up sometimes in real calls, and Sentry caught that. The start of the tuned prompt:&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%2Fqz6ak1gyrnawv8cenotg.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%2Fqz6ak1gyrnawv8cenotg.png" alt="The start of the agent's system prompt in ElevenLabs: polite spoken Japanese, one question per turn, kanji for ordinary words, numbers with counters in hiragana such as 7時→しちじ, and never inventing dates, prices or availability" width="762" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking every reply with Sentry
&lt;/h3&gt;

&lt;p&gt;Since every turn goes through my server, I can check each reply against the rules of the practice call. If you said goodbye and the AI didn't hang up, if it "confirmed" a booking, or if it put digits or English letters into Japanese speech, that becomes a Sentry issue tagged with the prompt version and turn number. Each request is also a &lt;code&gt;gen_ai&lt;/code&gt; trace with time to first token, token counts and estimated cost. After a call you can tap &lt;strong&gt;Report this reply&lt;/strong&gt;, and the report shows up next to that turn's trace. Conversation text only goes to Sentry if you choose to attach it.&lt;/p&gt;

&lt;p&gt;Here's a real one. On October 3, the practice partner didn't hang up after the learner said goodbye, and Sentry caught it 8 times in four minutes. Each rule gets its own issue:&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%2F5wphabsqbkzn8v5rtg3c.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%2F5wphabsqbkzn8v5rtg3c.png" alt="Sentry issue feed showing one unresolved issue, Practice partner: missed hang up, on /llm/v1/chat/completions with 8 events" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The issue lists every time it happened and which release it came from:&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%2F3qvsbv911uq67nkpqwih.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%2F3qvsbv911uq67nkpqwih.png" alt="The missed hang up issue in Sentry: 8 events between 5:57 and 6:01 PM UTC on October 3, all on the same release in production" width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For comparison, here's a turn where the partner did hang up. The &lt;code&gt;execute_tool end_call&lt;/code&gt; span is the hang-up. The Gemma call took 1.27 s (Sentry's average for it is 1.49 s), used about 1,100 input tokens and 43 output tokens, and cost less than a cent. Traces don't include the conversation text, so the Input tab is empty.&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%2Fr216xseyxbgjo70k5uaz.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%2Fr216xseyxbgjo70k5uaz.png" alt="A Sentry trace of one practice turn: invoke_agent, chat gemma-4-31B-it taking 1.27 s, the end_call tool and the request to DigitalOcean, with 1.1K input tokens, 43 output tokens and a cost under $0.01" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of those Gemma calls run on DigitalOcean's serverless inference, so I never had to run a GPU server. In two days, October 3 and 4, the app sent about 1.28 million input tokens to Gemma 4 31B and got about 75,000 back. Most of it is input because every turn sends the instructions and the conversation so far, while the replies are short, like the 43 tokens in the trace above.&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%2Fh4c1w707sh96pv9yp65t.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%2Fh4c1w707sh96pv9yp65t.png" alt="DigitalOcean Serverless Inference, Analyze tab: 1,278,483 input tokens and 74,719 output tokens, all used on October 3 and 4" width="800" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bugs I hit along the way
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gemma sometimes wrote voice directions like &lt;code&gt;[calm]&lt;/code&gt; or &lt;code&gt;[pause]&lt;/code&gt; into its replies. The app strips them from the text on screen.&lt;/li&gt;
&lt;li&gt;Small models sometimes say a tool call out loud, like &lt;code&gt;end_call(reason="done")&lt;/code&gt;, instead of calling the tool. The app removes that text before it's spoken and treats it as a hang-up.&lt;/li&gt;
&lt;li&gt;Running Gemma 4 locally, the first replies came back empty because the model spent all its tokens on hidden reasoning. Setting &lt;code&gt;reasoning_effort: "none"&lt;/code&gt; fixed it, and replies now take about half a second on my laptop's GPU.&lt;/li&gt;
&lt;li&gt;Kokoro's Japanese voice wouldn't install on Windows, because pyopenjtalk needs CMake to build. pyopenjtalk-plus has prebuilt wheels, so I switched to that.&lt;/li&gt;
&lt;li&gt;Romaji showed 10月4日 as "10tsuki4hi". Romaji and furigana now use the same reading list as the voice, so it reads &lt;em&gt;juugatsu yokka&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The hosted version runs on paid APIs. I also wanted a version that's free and private, so the same code can run entirely on a laptop. With &lt;code&gt;LOCAL_VOICE=1&lt;/code&gt;, Whisper handles speech recognition, Gemma 4 E2B (4.3 GB, through Ollama) plays the other person, and Kokoro-82M speaks. Explanations, slow replay, Enhance prompt, the word list, furigana and the PDF all still work. Nothing is sent to a provider, it works offline, and you can switch models with one setting (&lt;code&gt;LOCAL_LLM_MODEL&lt;/code&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%2Fx5cw1xbzbmhd035ce2sw.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%2Fx5cw1xbzbmhd035ce2sw.png" alt="Same app, two brains: Gemma 4 31B on DigitalOcean with ElevenLabs, or Gemma 4 E2B on a laptop with Whisper and Kokoro" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On my laptop (Ryzen 9 5900HS, RTX 3060 with 6 GB), the AI greets you in 2.4 s, answers what you say in about 6 s, and an explanation takes 1.4 s.&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%2Fb012bqobkf76hoyugtik.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%2Fb012bqobkf76hoyugtik.png" alt="A free local practice call: Gemma's reply with furigana and romaji, a tap-to-speak microphone and the call controls" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The local version isn't as good. The ElevenLabs voice sounds more natural, responds faster and lets you interrupt it. Locally you tap to talk, and the small model doesn't follow the role-play rules as closely as the 31B. It's possible at all because Gemma's weights are open: the same model family runs on DigitalOcean and on my laptop, with the same prompt, rules and pronunciation list. Furigana and romaji don't need a model. The open-source pykakasi and Janome libraries handle them.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; one agent over WebRTC for every call, with per-call prompt, first line, language and voice overrides, the built-in &lt;code&gt;end_call&lt;/code&gt; tool, a custom LLM pointed at Gemma, a separate text-only session for explanations, text to speech for slow replay, and a 6,107-entry pronunciation dictionary on the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing:&lt;/strong&gt; every Gemma request is a &lt;code&gt;gen_ai&lt;/code&gt; trace with time to first token, tokens and cost. Replies are checked against the role-play rules, and user reports link to the exact turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of DigitalOcean:&lt;/strong&gt; serverless inference runs Gemma 4 31B for every call turn, explanation, prompt enhancement and word list, with no GPU to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma does four jobs (the other person on the call, the explanation helper, the prompt enhancer and the word picker), as 31B in the cloud and E2B on a laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; one Docker web service from a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint serves the app, the API and the custom LLM proxy that streams every Gemma reply to ElevenLabs, with a persistent disk for usage limits and analytics, keys kept out of the repo, and the credit limits as environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built solo.&lt;/p&gt;

&lt;p&gt;If you live in Japan and put off phone calls too, try it and tell me in the comments what's missing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>Hello world!</title>
      <dc:creator>ADK BBX</dc:creator>
      <pubDate>Fri, 02 Oct 2026 18:16:26 +0000</pubDate>
      <link>https://dev.to/adk_bbx/hello-world-58d3</link>
      <guid>https://dev.to/adk_bbx/hello-world-58d3</guid>
      <description></description>
    </item>
  </channel>
</rss>
