Inspiration

Outside the big cities there is no delivery app. The restaurants down the road are on no platform, so finding out who delivers to your street, what it actually costs at the door, and how long it takes means calling them one after another — and starting over each time the answer is no.

That is not a hard problem. It is a tedious one, which is worse: people stop asking and order from the same single place forever. For anyone who dreads phone calls, or has a speech or hearing impairment, the tedium is a wall.

The interesting part is that "call candidates until one fits" is not about food at all. It is the shape of an urgent dentist appointment, a brake repair before the weekend, a spare part in stock, a respite care slot. We wanted the pattern, and used food to demonstrate it.

What it does

  • Takes one brief — what you want, where you are, what you will pay — and turns it into a sequential cascade of calls to ranked local candidates.
  • Before dialling anything it geocodes the address, drops businesses that are closed at that hour, and orders the rest by craving and distance.
  • Enforces a hard doorstep budget. If the total at your door exceeds the cap, the agent declines politely and moves to the next candidate. It cannot spend authority it was not given.
  • Refuses a price it would have to guess. "About 30 euros, depends on the driver" is recorded as price_known: false and rejected — a guessed price is not a price.
  • Holds concessions back. What you would settle for (a longer ETA for a lower price, an indoor table when the terrace is full) is only played when the primary path has failed, and the result states which step was used.
  • Stops the entire cascade the second a candidate meets every condition. The remaining candidates are never called.
  • Keeps a timestamped transcript of what was agreed, as evidence of the spoken contract, next to a masked callback number.
  • Runs all of it with no account, no network and no telephone, against bundled fixtures.

The one thing to try first

One command, about thirty seconds, no account and no API key:

  • pip install -e .
  • hungrycall demo

It runs the whole cascade: candidate 1 rejected for exceeding the budget, candidate 2 rejected for quoting a vague price, candidate 3 accepted and ordered, the remaining candidates never dialled — and it prints the transcript as proof of what was agreed.

Note: python -m hungrycall does not work; the package has no __main__ entry point. Install first, then use the hungrycall command.

Why not just use the CALL-E app?

Use it. For a single call the CALL-E chat is faster than anything we could build, and HungryCall does not try to replace it.

The difference is the search, not the call:

  • The app calls one number you already chose. This ranks a list and works down it.
  • The app leaves the judgement to you after each call. This is given the criteria in advance and applies them during the conversation.
  • The app has no spending limit, because a human is reading along. This has a cap the agent cannot exceed.
  • The app returns prose. This returns a schema-validated result, so "does it fit" is a decision rather than an impression.
  • The app stops when you stop. This stops itself the moment the goal is met, which is also what keeps it cheap.

How we built it

  • Python 3.11+, FastAPI, HTMX, SQLite and Leaflet. No bundler, no build step, no CDN.
  • The cascade engine is the whole product; the three modes (delivery, table, pickup) are different criteria on the same engine, not three code paths.
  • Agent authority is modelled in four tiers, and this is the part meant to be reused: musts that cannot be traded, hard boundaries that force a polite refusal, tiered concessions that stay hidden until the primary attempt fails, and wishes that only reorder the candidate list. The pattern is written up separately in MUSTER.md.
  • Schema-validated results come from the REST API, because that is the only path that has them.
  • The dry run is a real fixture framework, not a stub: user input is interpolated into transcripts, activity logs and summaries, so the offline run exercises the same parsing a live call would.
  • Safety is code, not documentation: E.164 validation, phone masking in every output, deterministic idempotency keys, and a content guard that rejects medical, legal, financial and emergency briefs before a call is ever planned.

Challenges we ran into

Everything below was measured against the real service in a real call, and several of these contradict the documentation:

  • status is useless as a progress indicator. It stayed on PREPARING for an entire conversation and only moved to COMPLETED after the call had ended. Live progress had to be read from activity instead.
  • Speech recognition streams and then corrects itself. The same line arrives twice, a rough version and a correction moments later, so intermediate drafts have to be de-duplicated or the live log becomes unreadable.
  • About 40 seconds of every call is dialling before a word is spoken, independent of how long anyone talks. Billing is per call; time is not.
  • Result schemas are REST-only. plan_call over MCP/CLI has no result_schema, and a call started over MCP is not retrievable over REST at all — separate ID spaces, shared billing.
  • Extending agent authority from time to money turned out to need a second rule nobody writes down: the agent must also refuse unclear prices. A cap alone is not enough if the agent is willing to estimate.

Accomplishments that we're proud of

  • 170 tests, all green, all in the dry run, none needing an account or a network.
  • The concession mechanism is tested in both directions: an agent that spends authority it was not granted has its result rejected, and the same call succeeds once the concession is granted — with the result naming which step it used.
  • A 30-second demo that shows the whole argument (budget rejection, vague-quote rejection, success, early exit, transcript proof) without any access.
  • A web interface that is honest where you are working: real calls are locked, and it says so on screen instead of offering a "go live" switch that does nothing.
  • Both languages complete, enforced by a test that fails the build if a key is missing in either one or a placeholder is lost in translation.

What we learned

  • Monetary limits are a workable form of agent authority — but only paired with a rule that forbids acting on an estimate. Otherwise the cap is advisory.
  • Design the result schema before the prose. What the agent must fill in is what it must find out during the call.
  • Streaming speech recognition needs de-duplication before it is shown to anyone.
  • The cheapest optimisation was the early exit. Stopping at the first fit is both the correct behaviour and the one that saves the most money.

What's next for HungryCall

  • Packaging the cascade engine as a standalone library, so the medical, mechanic and spare-part cases are configuration rather than forks.
  • A field test with real businesses, with disclosure. It has not happened yet.
  • Multilingual negotiation prompts for providers outside German-speaking regions.
  • Verifying the live OpenStreetMap candidate search, which exists in code but is never entered by the dry run.

Links

Update — live field trial and retest (Aug 11 + Aug 22)

Since recording the demo video, HungryCall went through a full live field trial with real outbound calls (2026-08-11 and 2026-08-22) plus a targeted retest of every finding. The chain-ordering flow now enforces a hard stop and states the consequence out loud the moment a restaurant's prices exceed the budget cap, instead of quietly placing an order anyway; the bot now speaks the full order aloud, item by item, before ever asking for a total — and by deliberate design decision it no longer does its own arithmetic: it asks the restaurant to state the total and only sanity-checks wildly implausible figures. All ten polish findings from the trial (clearer budget copy, auto-saved successful orders, localized rejection reasons, a clear "not reached" vs. "declined" distinction in the call history, and more) are shipped and covered by tests. The project now ships an honest "How We Tested" section in the README, describing the field-trial setup, call counts, and the find-fix-retest cycle — not just a scripted demo. Current test suite: 368 passing.

Built With

Share this project:

Updates