Work / Agents and botsopen brief

ShopGuard: an honesty guard for shop chatbots on open models

What it was

Shops put LLM bots behind the chat button, and the bots promise what the shop never did: a price not in the catalog, 12 payments instead of three, "in stock, ships today" for something that sold out a week ago. The customer believes it, and the shop either honours a promise it never made or loses the customer.

What we did

Put a guard between the bot and the customer. A small model pulls the promises out of the reply, plain code checks prices, stock, installments and delivery against the shop's data, a large model verifies specs through web search and rewrites the reply using only confirmed facts. The rewrite goes through the same check again, and if it fails again the customer gets a polite hand-off to a manager.

What the client got

A live demo, open code, a 3:20 video and written feedback on the gateway with seven reproducible problems, one of which the team fixed the next day. On 49 customer requests, invented prices, installments and stock reached the customer 0 times instead of 3 with a regular bot and 8 with a pushy one. No honest answer was replaced by a hand-off.

ClientMozilla, open brief Developers Wilding on the Otari gateway
My roleIdea, guard code, eval on 49 requests, Vercel demo, video, written feedback on Otari, bug report
StatusDemo and video ready, submission due 11 November 2026, no results yet
Result0 of 49 made-up promises reached the customer, down from 8 of 49 with a pushy bot before the guard · Open
Rejected: the bot's draft. Price 15,999 instead of 18,999, 12 payments instead of 2-3, same-day delivery the policy does not have, and Wi-Fi nobody confirms. Never reached the customer
Rejected: the bot's draft. Price 15,999 instead of 18,999, 12 payments instead of 2-3, same-day delivery the policy does not have, and Wi-Fi nobody confirms. Never reached the customer
Accepted: the reply after the rewrite. The catalog price, installments per policy, delivery in 1-3 days, and an honest "I will have a manager check" about Wi-Fi
Accepted: the reply after the rewrite. The catalog price, installments per policy, delivery in 1-3 days, and an honest "I will have a manager check" about Wi-Fi

The task

Mozilla opened a brief for developers on the Otari gateway: take your own project, move it to open models through the gateway, use its features for real, and write feedback on what works and what does not. The judging is about real usage and useful feedback, not slide polish.

Our project existed already, but on a stub instead of a model: an honesty guard for shop bots. The idea grew out of our own benchmark, published on Kaggle, which measured how often shop assistants invent prices, installments and stock. The benchmark only measured. ShopGuard had to stop it at runtime.

Why, and what the idea was

A division of labour between code and models. Prices, stock, installment rules and delivery terms are things the shop already knows, so plain code checks them: exact and free. Models do what needs language: finding promises in free text, in Ukrainian, Russian, English or a mix, and judging web evidence.

Two extractors instead of one. Nemotron Nano lists the promises, and next to it a regular expression independently catches every amount of money and every number of payments. If the model misses a price, the regex still sends it to the check. A refusal inside the reply (“we do not offer 12 payments”) does not count as a promise.

The rewrite does not trust itself. A model asked to fix a reply can add a new mistake, so the rewrite goes through the same checks again, with no new web calls. The draft field of the API accepts a reply from a bot you already run, so the guard sits in front of an existing bot without replacing it.

How it went

When What happened
Day 1, morning Registration by email, a voucher on the gateway balance. The first commit with Otari: a new base URL, a key and model names. The client already spoke the OpenAI format, so the switch was small
Day 1, morning First live eval on Nano and Super, 49 requests: 8 of 49 with made-up facts before the guard, 1 of 49 after, honest answers blocked 0 of 12, cost $0.007. A workaround for JSON mode, which Bedrock does not support
Day 1, afternoon The demo seller moved to Gemma 3 27B, because Nano’s Ukrainian is broken. The guard stopped taking a refusal for a promise. A “pushy seller” persona as a stress test: 10 of 49 before the guard, 0 after, but 13 hand-offs, 6 of them on clean drafts
Day 1, afternoon Otari web search switched on, the judge searches on its own through the built-in tool. Searching by model code returns company profiles and finance news; in the stress run it confirmed not a single spec. Written into the feedback
Day 1, evening The Nano-to-Super fallback in code, spend ceilings in the panel, the 403 over the limit checked live: the demo switches to recorded runs. Public repository, demo on Vercel, feedback with seven problems, video v1
Day 1, night A reproducible web-search bug found: 400 in 3 of 4 identical calls. An issue in the Otari repository. A review of text and video, version v2 with a live screen recording
Day 2 The Otari team confirmed the bug and closed it with a fix. Added to the README and the feedback. The submission email prepared, sending scheduled

Tools and method

Python and FastAPI, every model call through one gateway, api.otari.ai, with one workspace key. Three open models in three roles: Gemma 3 27B writes the demo seller’s reply, Nemotron 3 Nano extracts promises, Nemotron 3 Super judges web evidence and rewrites. Each call’s cost is taken from the gateway’s usage.cost_usd and shown under every result, tool charges included; we deleted our own price table and spend counter.

Super has four providers behind one id, so the gateway can switch when a provider fails. In our runs every call went to Bedrock and no switch happened; we log the Otari-Provider and Otari-Fallback headers. On top sits our own hop from Nano to Super when the small model is silent for 20 s or returns broken JSON, which the gateway does not treat as a failure. If both fail, the reply is not sent unchecked: the customer gets the hand-off.

Spend is capped at the gateway: $15 for the whole work, separate keys with daily ceilings of $0.50 for the public demo and $2 for the judges. In the code remain a per-IP limit, at most 2 web checks per reply, a verdict cache and recorded runs that replay instantly when the live budget is gone.

The eval, eval/run_eval.py, runs 49 customer requests, 40 of them in Ukrainian, Russian and a mix from our benchmark and 9 new ones in English, and scores the bot’s draft and the sent reply with an independent pattern scorer, not the guard’s own extractor.

Result

A live demo on Vercel: a question to the shop in any language, under the result the bot’s draft and what went to the customer, the list of violations with quotes and the cost in cents. The demo shop “KitchenHub” is fictional, the prices are made up, the product models are real so the web check has something to find.

Live runs on 49 requests. Eager seller: an invented price, installment or stock claim in 3 of 49 drafts, 0 of 49 in the reply the customer gets; guard actions 43 passed, 4 rewritten, 2 handed off; the whole run $0.03. Pushy seller: 8 of 49 before the guard, 0 after, 27 passed, 13 rewritten, 9 handed off, $0.44. No honest price or stock answer was handed off, 3 clean drafts were, all in the stress run. Most of the second run’s cost is the web check: the pushy bot states many specs, and each unconfirmed one costs a Super call with search.

Feedback on the gateway: seven problems with reproduction steps, from a wrong address in the quickstart to a key that died without explanation. The web-search bug was confirmed by the team the same day and closed with a fix the next.

What we do not know yet: how the brief will rate the work. Submission is scheduled, there are no results yet.

Mistakes and what we took from them

The first platform for this project did not open billing for Ukraine.A day of work on a stub went into the drawer. We now check the country at every link before starting, in payment as well as in the rules. The code was not lost: it became the base of this work.
A refusal counted as a promise."We do not offer 10 payments" was read as a promise of 10 payments, and an honest reply got rewritten. A negation inside the reply is no longer a promise, and the scorer knows it too.
Web search by model code finds the wrong pages."Bosch WAN24263" brought corporate pages and finance news, a fridge code brought a bank branch. Unconfirmed specs are now removed from the reply, and the feedback asks for a search call we can steer.
A small search limit broke the call.With max_uses 1 or 2 the request on Bedrock failed in 3 of 4 cases. We keep 3 and retry with a higher limit; the bug went to the team with reproduction steps, and they fixed it.
A key died between two GET requests.It worked at 08:25, returned 401 at 08:35, with no action in the panel. We never found the cause. Keys live in the keychain, and a reissued key goes straight there, not through the chat.
The gateway catalog promised JSON mode, the provider did not.Nano on Bedrock answers 400 to response_format. We retry once without it and read the JSON from the text.

Tools

PythonFastAPIOtariGemma 3 27BNemotron 3 NanoNemotron 3 SuperVercelpytestElevenLabsClaude Code