This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

Everybody has one. The charge you meant to dispute. The subscription that renewed after you cancelled. The bill that went up $30 with no warning. You know exactly what you want. You just don't want to sit through the phone tree, explain it three times, and remember to ask for a reference number.

TODO (Penta): 2–3 sentences about YOUR errand. Which one had been sitting on your list, and for how long? This is the line judges will remember. Keep it true.

Before I built anything this week, I had an agent go read what people actually complain about online. The loudest category wasn't sleep or inboxes. It was dealing with companies. A February 2026 Groundwork Collaborative study, reported by Fortune, puts the "annoyance economy" at about $165 billion a year and finds Americans spend 60% more time on customer-service calls than 20 years ago. On Trustpilot, Calm sits at 1.4 out of 5, and the reviews are mostly billing traps: "They charged me $85 for a free trial and then refused a refund."

Then I checked this challenge's tag. Around 545 Week 3 posts: a lot of file sorters, a lot of inbox digests, and four bedtime companions, which was the idea I'd planned to build. Nobody had taken the customer's side of the phone call.

Please Hold does. You type one sentence:

Lumen Fitness charged me $49.99 on Oct 3 even though I cancelled on Sept 28. Member #448210. I want it refunded.

and get back the whole errand:

  • A phone script in your voice: the first thing to say, the exact ask, three polite lines for when they say no, and how to close (agent's name, reference number, what was agreed).
  • An email and a chat message, ready to send.
  • Your leverage. Short, sourced consumer-rights pointers for the US, UK and EU that match the errand. I wrote these by hand. The model never writes them.
  • Hold mode. A full-screen card with your script, a hold timer, and calm hold music generated on your device. Your hold music, not theirs.
  • After the call. Type the agent's name and reference number, and it writes the confirmation email and a calendar reminder to check they actually did it. No model in this step, so nothing gets made up.

Gemma runs on your own computer through Ollama. Your account numbers, your address, the money you're owed: none of it leaves your machine. If you just want to try it on a phone, there's an opt-in cloud mode, clearly labeled.

Demo

Live: https://please-hold.dankpenta.workers.dev

To keep everything local, install Ollama, run ollama pull gemma3:4b, and start it with the site allowed:

OLLAMA_ORIGINS=https://please-hold.dankpenta.workers.dev ollama serve 

Or pick Gemma 4 26B on Cloudflare to try it on any device right now.

A finished kit: receipts summary, the ask, and the phone script

Hold mode: timer, your script, your own hold music

After the call: a confirmation email written from your own notes

Screenshots from a run on my laptop with gemma3:4b in Ollama. The ledger numbers are from that test run.

TODO (Penta): optional 30-second screen recording of one real run.

Code

Repo: https://github.com/5thlegend/please-hold

TODO (Penta): swap this line for the GitHub embed (github tag + 5thlegend/please-hold) once the repo is pushed.

MIT licensed. Plain HTML, CSS and ES modules, no build step. One small Cloudflare Worker for the opt-in cloud route.

How I Built It

The model writes words, never facts

Small models are helpful in the worst way. Ask one for a cancellation email and it may add a support phone number, a "within 5 business days" promise, or a consumer protection act. None of that came from you, and some of it is invented. On a customer-service call that's not a style problem. Read a made-up policy to an agent and you've lost the call.

So Please Hold never trusts the model's output. It runs two passes, then checks the receipts:

  1. Pass 1, extract. Gemma pulls out the company, the errand type, and the facts, each with a quote from your sentence. A fact survives only if its quote really is your words, or if every number in it matches what you typed and most of its words are yours. Anything else is dropped before a word gets written.
  2. Pass 2, write. Gemma writes the email, chat message and phone script from the surviving facts only, in your voice. Unknowns become [ORDER NUMBER]-style blanks.
  3. The receipts check. Plain JavaScript, no model, finds every checkable claim in the output (phone numbers, URLs, emails, amounts, dates, durations, percentages, account numbers, legal-sounding phrases) and compares each one against your own words.
// public/js/verify.js (trimmed) for (const c of findClaims(output)) { const status = isVerified(c.kind, c.text, idx) ? "verified" // it's in what you typed : c.catchable ? "caught" // phone/URL/email you never gave: replaced : "unverified"; // number, date, deadline, law: highlighted segments.push({ text: status === "caught" ? PLACEHOLDER[c.kind] : c.text, status }); } 

On screen, anything from your words gets a dotted underline. An invented phone number is removed and replaced with [COMPANY PHONE — copy it from your bill]. Anything else you didn't say is highlighted yellow so you check it before you read it to a stranger.

Both passes use structured output: the JSON schema goes to the model, so it can only produce the fields the app expects (Ollama's format, Workers AI's response_format).

The parts I wrote by hand

Anything that has to be right isn't generated. That covers the consumer-rights pointers, each checked against an official page (FTC, Your Europe, Citizens Advice). It also covers the "getting to a human" tips, the follow-up email built from your call log, and the .ics reminder. The hold music is four slow major-seventh chords and a soft bell, generated live with the Web Audio API. No audio files.

Where Gemma runs

Option Model Where your text goes
Your computer (default when found) any Gemma in Ollama, e.g. gemma3:4b Nowhere
Cloudflare (opt-in) Gemma 4 26B A4B on Workers AI Cloudflare. The app doesn't store or log it

What broke: I tried to put Gemma in the browser

My plan was for Gemma to run inside the tab with nothing to install. I spent most of an afternoon on it, and it didn't ship. Here's what happened on my laptop (RTX 4050 6 GB, 16 GB RAM, Chrome):

  • Gemma 4 E2B on WebLLM 0.2.86, the first WebLLM release with Gemma 4, lost the WebGPU device on the first token. So did every other model on 0.2.86. 0.2.85 is stable but has no Gemma 4.
  • Gemma 4 E2B on Transformers.js 4.3.0 downloaded all ~3.3 GB of weights, then the tab crashed loading them.
  • Gemma 3 1B on WebLLM 0.2.85 runs, but it's too small for this job. Under a JSON grammar it printed whitespace until it hit the token limit. Without one, it echoed my prompt back, or worse: it answered with a refund for "$10.99 on Oct 28." The user had typed $49.99 and Oct 3.

That last one is the whole argument for the receipts check, made by the model itself. It's still in the app behind ?lab=1 if you want to watch it struggle.

Two bugs from that detour that you might hit too:

  • Hugging Face rejects *.workers.dev referrers. WebLLM said Failed to execute 'open' on 'CacheStorage', then Failed to fetch. Chrome said CORS, and curl said everything was fine. Replaying Chrome's exact request one header at a time showed it: Hugging Face returns 404 with no CORS headers to any model download whose Referer is a *.workers.dev page. The fix is one line, <meta name="referrer" content="no-referrer"> (plus the matching Referrer-Policy header).
  • WebLLM's prebuilt Gemma 3 1B config rejects itself. It sets both a 4096 context and a 512 sliding window, and WebLLM refuses that combination. chatOpts: { context_window_size: 4096, sliding_window_size: -1 } gets it to load.

So the local path is Ollama. It's one install, and the model then runs on your own GPU with native CUDA.

Does it actually behave?

I wrote 24 realistic errands (refunds, cancellations, a lost suitcase, a pharmacy overcharge, a security deposit, a free trial about to convert, an errand where the person doesn't know their account number), ran each through the full pipeline on both backends, and counted what the receipts check found.

Model Runs on Errands Claims checked Matched your words Flagged for you Invented contact details Avg time per kit
Gemma 4 26B Cloudflare Workers AI 24/24 169 165 4 0 12.8 s
Gemma 3 4B my laptop, via Ollama 24/24 155 151 4 0 16.7 s

All eight flags were Gemma doing arithmetic. "My bill went from $65 to $95" became "the $30 increase," and "$120 for a prescription that's supposed to be $10" became "a $110 refund." The math is right, but you never said $30, so it's highlighted for you to confirm. The check doesn't decide what's true. It shows you what didn't come from you.

Neither model invented a phone number on this set. I'd still never ship without the check, after watching the 1B model invent a whole charge on its first try. The local model also had 6 extracted facts dropped for not matching the user's words, before they could reach the email.

The Lazy Payoff

Across the 24 errands, you type about 19 words and Please Hold writes about 220: an email, a chat message, and a four-part phone script with your facts already in it. Each kit takes 13–17 seconds on average.

TODO (Penta): run one real errand of yours and fill this in honestly:

  • How long you'd been putting it off:
  • Time from typing to kit:
  • Time on hold (the app tracks it):
  • Outcome, and money recovered if any:

The app keeps a lazy ledger in your browser: errands finished, time spent on hold, words you didn't have to write, and money recovered.

Why Does Open Innovation Matter?

  • The sensitive part is the data. A customer-service errand is account numbers, addresses, and money owed. With open weights on your own machine, that text is never sent anywhere. It isn't a "we don't store it" promise.
  • No subscription, for an app about subscription traps. The thing people hate most about the incumbents is getting charged. A model on your own GPU costs nothing per errand, forever.
  • I can hold it to account because I can run it. Open weights plus open runtimes let me force a JSON schema, run the same model family locally or in the cloud, and check every output with my own code. The honesty system is mine, not a vendor setting.
  • You choose where it runs. The same Gemma family runs on a laptop GPU through Ollama, or on Cloudflare when you're on a phone. Open weights make that a radio button, not a migration.
  • Where open beat closed, and where it didn't. A frontier API would write smoother emails. It would also send your account details to one company so you can argue with another. And open isn't magic: the in-browser route wasn't ready on my hardware this week. Small, local, and checked was the right trade for this job.

What it doesn't do

It doesn't make the call for you, and it doesn't give legal advice. The rights pointers are general information with links to the official source. The local 4B model writes plainer prose than the cloud one, and that's why the checks exist.

My Agent Session

I built this with an AI agent in one long sitting, from "go research what people actually complain about" to deploy. The session shows the research that killed my first idea, the rule that the model writes words and never facts, the Referer bug, and the in-browser attempts that didn't make it.

Please Hold: from 'what are people complaining about?' to a local Gemma errand kit
You

Bring my Hacktoberfest run up to date for the week of 2026-10-20 and ship this week's entry. Week 3 'Slow and Steady' is open: build something with open-weight models / open-source AI that rewards laziness. Pitch 3 concepts that fit my strengths (local-first open-weight AI like Storyhouse/Lore Walk), let me pick, build it, deploy it, save the session, draft the DEV post. Never publish or register without my yes.

Agent
▸ get_challenges DevRelay
DevRelay
Week 3 'Slow and Steady' (Oct 19-25, due Sat 11:59 PM PDT). Writing quality weighted most. Post must explain why open innovation matters and show the lazy payoff.
▸ search_mlh_events Open-Source AI Challenge
Open-Source AI Challenge
MLH event 'Hacktoberfest Open-Source AI Challenge: Week 3' exists; not registered yet.

Pitched three: an offline wind-down companion that slows down with you, a local file tidier, and fridge-photo-to-grocery-list. Recommended the wind-down companion.

You

Go out on the internet and deep research what people are complaining about in this area or theme and we can come up with ideas to reward their laziness.

Agent

Ran four parallel research tracks (life admin, household/food, sleep and wind-down, developer drudgery plus the competition), then a report writer combined them into one ranked report.

▸ research: life admin HN, r/ADHD snapshots, Trustpilot, surveys
HN, r/ADHD snapshots, Trustpilot, surveys
Top pains: customer-service calls and hold time, insurance/medical paperwork, subscription cancelling. Groundwork Collaborative (Feb 2026): $165B/yr 'annoyance economy', call time up 60% in 20 years.
▸ research: sleep Trustpilot, app stores, AASM, Talker Research
Trustpilot, app stores, AASM, Talker Research
Calm 1.4/5 on Trustpilot (492 reviews, 78% one-star), mostly billing traps: 'They charged me $85 for a free trial and then refused a refund'.
▸ research: competition dev.to #hf26challenge
dev.to #hf26challenge
~545 Week 3 entries. ~119 file sorters, ~80 summarizers, ~48 inbox/chat digests.
▸ search_dev_to_semantic hf26challenge bedtime sleep wind-down offline
hf26challenge bedtime sleep wind-down offline
Sloth Companion, SnoozeScape, Compline, SlumberSafe: the bedtime idea is already taken.
▸ search_dev_to_semantic hf26challenge fridge / timesheet / insurance letter
hf26challenge fridge / timesheet / insurance letter
Fridge: 4+ this week. Timesheet: 'lowkey' did it in Week 2. Letter decoder: Week 2.
▸ search_dev_to_semantic hf26challenge cancel subscription customer service call script
hf26challenge cancel subscription customer service call script
No Week 3 entry on the consumer side of customer-service errands.

My original pick was already taken four times. The most-complained-about pain (calling a company to cancel, refund, or dispute) had no Week 3 entry. New pitch: Please Hold. One sentence in, the whole errand kit out, written on your device.

You

Build Please Hold — the local customer-service errand kit.

Agent
▸ Bash HF API: onnx-community + mlc-ai gemma models; npm view @mlc-ai/web-llm
$ HF API: onnx-community + mlc-ai gemma models; npm view @mlc-ai/web-llm
gemma-4-E2B-it-q4f16_1-MLC (2.8 GB, 3.4 GB VRAM) is in WebLLM 0.2.86 but not 0.2.85. gemma3-1b-it (0.6 GB) in both. Workers AI has @cf/google/gemma-4-26b-a4b-it.

Decision: on-device Gemma 4 E2B through WebLLM as the default (pinned and vendored 0.2.86), Gemma 3 1B as a light option, the user's own Ollama, and an opt-in cloud fallback (Gemma 4 26B on Workers AI) for browsers without WebGPU. Same open model family, the user picks where it runs.

Design rule: the model writes words, never facts. Pass 1 extracts facts and must quote the user word for word. Pass 2 writes the kit only from verified facts. Then a deterministic 'receipts check' scans every phone number, URL, email, amount, date, duration, percentage, account number and legal-sounding phrase, and compares it to the user's own text. Invented contact details get replaced with a placeholder; other unverified claims are highlighted. Consumer-rights pointers, IVR tips, the follow-up email and the calendar reminder are written by hand, with no model involved.

▸ Bash node test/verify.test.mjs
$ node test/verify.test.mjs
verify: all checks passed
▸ Bash Workers AI REST: gemma-4-26b-a4b-it with json_schema response_format
$ Workers AI REST: gemma-4-26b-a4b-it with json_schema response_format
200 in 5.6s. Clean JSON, but facts came back as fragments ('on Oct 3'). Fixed the prompt: each fact is one short third-person sentence.
▸ WebFetch consumer.ftc.gov/articles/using-credit-cards-and-disputing-charges
consumer.ftc.gov/articles/using-credit-cards-and-disputing-charges
'Send your letter so that it reaches the issuer within 60 days after the first bill with the error was sent to you.' Rewrote the US leverage text to match. Two Citizens Advice URLs 404'd; swapped them for a live Citizens Advice page that states the 14-day and 30-day rules.
▸ Bash npx wrangler deploy
$ npx wrangler deploy
https://please-hold.dankpenta.workers.dev
▸ Bash node test/eval-cloud.mjs (24 realistic errands)
$ node test/eval-cloud.mjs (24 realistic errands)
24/24 ok. 178 checkable claims: 174 matched the user's words, 0 invented contact details, 4 flagged ($110, a difference Gemma calculated from $120 and $10, which isn't in the user's words). Avg 17s per kit, ~216 words written per errand.
▸ browser on-device run in the in-app browser
on-device run in the in-app browser
Blocked: the sandboxed browser pane can't reach huggingface.co for the weights. Switched to Playwright driving real Chrome on the RTX 4050.
▸ Playwright CreateMLCEngine('gemma-4-E2B-it-q4f16_1-MLC')
CreateMLCEngine('gemma-4-E2B-it-q4f16_1-MLC')
UnknownError: Failed to execute 'open' on 'CacheStorage'. My IndexedDB fallback used the old option name (useIndexedDBCache); WebLLM 0.2.86 renamed it to cacheBackend: 'indexeddb'. Fixed and redeployed.
▸ Playwright fetch(huggingface.co/.../mlc-chat-config.json) from the app
fetch(huggingface.co/.../mlc-chat-config.json) from the app
Still 'Failed to fetch': CORS, no Access-Control-Allow-Origin. The cache error had been a red herring. Direct navigation to the same URL works.
▸ Bash curl with Chrome's exact headers, varying one at a time
$ curl with Chrome's exact headers, varying one at a time
Referer: https://please-hold.dankpenta.workers.dev/ -> 404 from CloudFront (no CORS headers). No Referer -> 302. Referer: https://example.com/ -> 302. Hugging Face rejects downloads referred by *.workers.dev pages.

Fix: <meta name="referrer" content="no-referrer"> plus a Referrer-Policy: no-referrer header in public/_headers. One line each. Re-probe: all three files 200.

▸ Playwright Gemma 4 E2B on WebLLM 0.2.86, RTX 4050, real Chrome
Gemma 4 E2B on WebLLM 0.2.86, RTX 4050, real Chrome
Model loaded (428s, 2.8 GB). First generated token: 'Failed to execute mapAsync on GPUBuffer: A valid external Instance reference no longer exists.' The WebGPU device was lost. Same with Gemma 3 1B on 0.2.86, even for a 40-token plain-text reply.
▸ Playwright Same Gemma 3 1B on WebLLM 0.2.85
Same Gemma 3 1B on WebLLM 0.2.85
Works: 2.2s plain, 5.3s with a JSON schema. 0.2.86 is the regression, and it's the only version with Gemma 4.
▸ Playwright Gemma 3 1B on the real facts prompt (341 tokens)
Gemma 3 1B on the real facts prompt (341 tokens)
Under the JSON grammar: '{"company"' followed by 1,200 characters of whitespace. Without a grammar: echoes '---' or the prompt. With a system role it answered about a refund 'of $10.99 on Oct 28'. The user typed $49.99 and Oct 3. Too small for this job, so it moved behind ?lab=1.
▸ Playwright Gemma 4 E2B via Transformers.js 4.3.0 (+esm build)
Gemma 4 E2B via Transformers.js 4.3.0 (+esm build)
All weights downloaded (~3.3 GB), then the tab crashed while loading them. 16 GB RAM, 2.5 GB free.

Decision: the local path is Ollama (native CUDA). It's the default whenever the site detects it, the cloud is opt-in, and the in-browser path stays behind a flag with an honest write-up. Added an in-app message for GPU resets and a copyable OLLAMA_ORIGINS command for Mac/Linux and Windows.

▸ Bash node test/eval-ollama.mjs gemma3:4b (first run)
$ node test/eval-ollama.mjs gemma3:4b (first run)
24/24, 110 of 111 claims matched, 0 invented contacts, but 10 facts dropped. They were mostly false positives: the model 'quoted' its own paraphrase ('They were charged $120 for a prescription').

Loosened checkFacts: a fact is kept if its quote really is the user's words, or if every number in it checks out and at least 60% of its content words are the user's. Added unit tests for both, including one where a fact with an invented $140 still gets dropped.

▸ Read docs/shots/02-kit.png
docs/shots/02-kit.png
The phone opener came out in the agent's voice: 'Thank you for contacting Lumen Fitness... I'm here to assist you today.' Fixed the prompt to say everything is in the CUSTOMER's voice, said TO the company. The goal and fallback are now short phrases, and the 'have these ready' items are plain lowercase.
▸ Bash final evals, both backends, final prompts
$ final evals, both backends, final prompts
Gemma 4 26B (Workers AI): 24/24, 165 of 169 claims matched, 4 flagged, 0 invented contacts, 12.8s avg. Gemma 3 4B (Ollama, RTX 4050): 24/24, 151 of 155 matched, 4 flagged, 0 invented contacts, 6 facts dropped, 16.7s avg. All 8 flags were Gemma's own arithmetic ($95 - $65 = '$30', $120 - $10 = '$110'), so they're highlighted for the user to confirm.

Prize Categories

  • Best Use of Gemma: Gemma is the whole engine. Gemma 3 4B runs locally through Ollama, and Gemma 4 26B on Workers AI is the opt-in fallback. Both are held to the same receipts check, and both are measured on the same 24 errands above.