Jello
a shopping agent you text
An AI agent in iMessage and WhatsApp that searches 36 shops and marketplaces. Prices are written by code, never by the model
A startup with paying users.
- 1stplace, Hackathon of Hackathon Winners
- 52,380lines of code
- 36shop and marketplace adapters
- 585commits in ten days
- 15 minhard ceiling on a deep search
Try it
Two real searches, recorded on 29 September 2026 and replayed with compressed timing; picks, prices and counts are as recorded. Live search stays off on this site to keep costs capped.
Architecture
Connections
- Shopper on iMessage or WhatsApp → Chat agent: understands the ask: text, photo or link
- Chat agent: understands the ask → Planner: queries in five languages: structured request
- Planner: queries in five languages → Search rounds: shops, marketplaces, web: parallel queries
- Search rounds: shops, marketplaces, web → Stop rules and hard caps (code): may we stop?
- Stop rules and hard caps (code) → Search rounds: shops, marketplaces, web: missing work
- Stop rules and hard caps (code) → LLM judge and photo check: shortlist
- LLM judge and photo check → Live check of each product page: relevant listings
- Live check of each product page → Reply: links and prices rendered by code: up to three live picks
- Reply: links and prices rendered by code → Shopper on iMessage or WhatsApp: message bubbles
A message arrives on iMessage or WhatsApp and reaches a chat agent, which turns it into a structured request: what the person wants, hard constraints such as size, condition or budget, and, for a request like “a gift for dad, he likes coffee and fishing”, one aspect per interest.
The search engine is a Python package with no dependencies outside the standard library. It runs in two modes. A quick search takes about 15 to 25 seconds: one page from the matching platforms, one web search, a relevance check, a live check of each candidate, then the reply. A deep search runs for about five minutes and never more than fifteen: a planner writes query variants in Danish, Swedish, Norwegian, German and English, rounds of parallel searches follow with pagination and price-sorted passes, and a critic looks for gaps after each round.
Each marketplace and shop is an adapter behind one interface, discovered automatically. Every number the engine uses lives in one configuration file.
Before a reply goes out, a language model judges the shortlist for relevance, and for clothing and furniture a vision model checks the photos for the asked fit, pattern or colour. Each remaining candidate’s product page is fetched live to confirm price and stock. The reply is assembled by code: links, titles and prices come only from verified candidates, and the writing model supplies only an intro, one comment per pick and a closing line.
A model must never decide when a search is finished
Constraint. Left to itself, a language model stops when it feels done, which is usually too early on hard requests and too late on easy ones. A deep search also costs money on every call.
Options. Let the model call a “finish” tool, or let code decide from what has actually been searched.
Decision. Code decides. A set of pure functions over the search ledger lists what is still missing: a platform never queried, a primary platform with too few query variants, a full results page still yielding new candidates, a price-sorted pass not yet run, results still moving between rounds, fewer than three verified live picks. Each missing item becomes the next round’s work. Hard caps override everything: 8 rounds, 40 model calls and $0.20 per deep search, plus an early stop when two rounds in a row add nothing that qualifies.
Result. A deep search has a known ceiling on time and cost, and it can’t quit before the platforms that matter have been searched.
No made-up product facts
Constraint. A shopping reply with a wrong price or a dead link is worse than no reply.
Decision. Every candidate is verified live on its product page before it can be a pick. Links, titles and prices in the reply are rendered by code. Any line from the writing model that contains a URL, or a price that isn’t one of the picks’ prices, is rejected and replaced by a fixed line. A line that is too long counts as a miss too.
Result. The model can choose the words around the facts, never the facts themselves.
Relevance is harder than search
Constraint. Keyword search returns coincidences. “Mac” found an air fryer and LEGO. A coffee-flavoured fishing lure matches “coffee and fishing”.
Decision. Brand and family requests are expanded into concrete product types that fit the budget, and the bare word is never searched. The judge drops keyword coincidences and generic novelty gifts. When the person names a look, only listings whose photo shows it can be picks. Fewer real matches means fewer picks, never padding with a near neighbour.
Result. Jello would rather say it found one match, or none, than send three that don’t fit.
Code
The hard caps. The first one that hits forces the search to finish. Unedited.
brain/jello/gates.py
def hard_cap(led, cfg: dict) -> str | None:
"""Caps override everything and force the beast-mode finalise. Returns the reason or None."""
m = led.meter
checks = [(led.elapsed() >= cfg["finalise_s"], "time"),
(led.rounds >= cfg.get("max_rounds", 1), "rounds"),
(led.count("platform") >= cfg.get("max_platform_fetches", 10**9), "platform fetches"),
(led.count("web") >= cfg.get("max_web_searches", 10**9), "web searches"),
(m.get("llm_calls", 0) >= cfg.get("max_llm_calls", 10**9), "LLM calls"),
(m.get("cost_usd", 0) >= cfg.get("max_spend_usd", 10**9), "spend"),
(dry(led, cfg), "nothing new turning up")]
return next((why for hit, why in checks if hit), None)Stop early when a hard request has gone dry. Unedited.
brain/jello/gates.py
def dry(led, cfg: dict) -> bool:
"""Nothing turning up: the last K rounds added no qualifying candidate and there still aren't enough for the
picks. More rounds of the same won't fix a hard ask; stop early and answer with what there is."""
k = cfg.get("dry_rounds")
h = led.top_history
if not k or len(h) < k + 1 or led.rounds < cfg.get("min_rounds", 1):
return False
before = h[-k - 1].get("n_q", 0)
return all(x.get("n_q", 0) <= before for x in h[-k:]) and h[-1].get("n_q", 0) < cfg.get("verified_min", 3)The writer may not invent a link or a price. Unedited excerpt from a longer function.
brain/jello/loop.py
def foreign_money(s):
for m in _MONEY.finditer(s):
try:
if int(float(re.sub(r"[.,](?=\d{3}\b)", "", m.group(1) or m.group(3)).replace(",", "."))) not in allowed:
return True
except ValueError:
return True
return False
def clean(s, fallback, max_words=None):
s = re.sub(r"[*_#`>\[\]]", "", str(s or "")).strip()
# Too long counts as a miss too: a text reply, not an essay (see writer.md).
if _URL.search(s) or foreign_money(s) or not s or (max_words and len(s.split()) > max_words):
if s:
meta["rejected"].append(s[:120])
return fallback
return sBackground
Buying something specific online means checking brand shops, big retailers and second-hand marketplaces one by one, in several languages, and still not knowing whether the cheapest listing is real, in stock or even the right product. Chat assistants answer confidently, but they invent prices, links and product details.
Jello is a free shopping assistant people text on iMessage and WhatsApp. In the team’s own words, from the partners page:
Who we are Jello won 1st place at the Hackathon of Hackathon Winners in September 2026. We built it there and have kept building since.
Jello is a free shopping assistant people text on iMessage and WhatsApp. They ask for a specific product, and Jello replies with a few matching picks and a direct link to the shop that sells them.
No made-up product facts. Price, stock and details come from your product page or feed.
Jello is launching soon. My part was most of the search engine: the loop that decides what to search, when to stop, what is relevant and what is safe to say.
What I’d do differently
- Fewer special cases, more data. Rules for sizes, refurbished units and brand families each fixed a real failure, and together they make the search loop a file of over 2,500 lines. Several of them belong in per-category data rather than in code.
- Measure the whole search’s cost, not only its parts. The per-call caps hold, but the full cost of an average deep search is still an estimate.
Deep dives
In the order the system was hard to build.