DEV Community

Cover image for Please Hold: the customer-service call you keep putting off, written by a Gemma that isn't allowed to make things up
Penta Dank
Penta Dank

Posted on

Please Hold: the customer-service call you keep putting off, written by a Gemma that isn't allowed to make things up

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

Code

Please Hold

Type the customer-service errand you've been avoiding. Get everything you need to finish it in one call.

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

You write one sentence, like "Lumen Fitness charged me $49.99 on Oct 3 even though I cancelled on Sept 28. Member #448210. I want it refunded." Please Hold gives you back:

  • A phone script: the first thing to say, the exact ask, three polite lines for when they say no, and how to close the call.
  • An email and a chat message, ready to send.
  • Your leverage: hand-written, sourced consumer-rights pointers (US, UK, EU) that match the errand. These are never written by the model.
  • Hold mode: a full-screen card with your script, a hold timer, and calm hold music generated on your device.
  • After the call: log the agent's name and reference number, then get a confirmation email and a calendar reminder to check they actually did…

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 });
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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.

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.

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.

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.

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.

Top comments (0)