Inspiration
Ask any chatbot how to market a café and you get the same placeless advice: a signature latte, post more reels, run a loyalty card. It would say the same thing about a café in Riyadh, Dubai or Portland, because it has no idea who walks past that particular door.
The hard part of marketing a small venue was never the copywriting — it is knowing the audience. That is a data problem, and it is exactly the problem Qloo's taste graph solves. So I built the thing that sits between the two: an agent whose recommendations are not allowed to exist unless Qloo data backs them.
What it does
You give Hayy a venue and a neighborhood. It returns:
- who the people around that venue are and what they are into,
- three menu moves,
- a six-artist in-store playlist,
- three collaboration partners,
- five ready-to-post social posts, in Saudi-dialect Arabic and English,
- and an evidence list mapping each claim back to the Qloo entity and affinity it came from.
Two things make it more than a prompt:
It shows its work. The page runs the same brief twice, side by side — once with a plain LLM and no tools, once with Hayy — and then prints the full Qloo call trace: every tool call, its arguments, the HTTP status, how many entities came back. You watch the recommendations get earned instead of asserted.
It is allowed to fail. If Qloo returns no taste data for a venue, Hayy refuses to write a brief rather than quietly falling back on the model's own priors. A grounded product that cannot say "I don't know" is not grounded.
How I built it
One Cloudflare Worker, one static HTML page. No build step, no dependencies, no framework.
The Worker exposes three tools to the model and lets it plan its own path through them:
| tool | Qloo endpoint | purpose |
|---|---|---|
qloo_search |
/search |
resolve the venue to an entity id |
qloo_insights |
/v2/insights |
the core call: signal with entity ids, get affinity-ranked artists, brands, films, places |
qloo_tags |
/v2/tags |
resolve cuisines and genres to tag ids |
The key move is using the venue's own entity id as the taste signal
(signal.interests.entities), so what comes back is what this audience likes rather than the city's
generic top ten. Tool results are trimmed to names, ids, affinity and a few tags before they reach the
model, which keeps a nine-call loop inside a sane context.
Challenges I ran into
Qloo's quiet failure mode cost me the most time. Passing location together with a non-place
filter_type — artist, brand, movie — returns HTTP 200 with zero results rather than an error. My
agent could not tell "nobody likes this" from "you asked wrong", so it retried variations and burned its
budget. Riyadh looked great and Dubai looked broken, and for a while I believed that was a data-coverage
gap in Qloo rather than a bug in my own querying.
Reading the call traces of the failed runs is what found it. The rule — location only with
urn:entity:place, signal taste with interest_entities instead — is now in the tool description and
the system prompt. Measured on identical briefs:
| city | before | after |
|---|---|---|
| Downtown Dubai | 9 calls, 4 with data, 141s | 5 calls, 5 with data, 47s |
| San Francisco | 8 calls, 5 with data, 151s | 3 calls, 3 with data, 49s |
Empty-result calls went to zero in every city.
Two smaller ones, both only visible once it was really running: models sometimes return the final brief as a tool call named "JSON", which the gateway rejects before my code ever sees it — so the loop now retries that step with no tools, which forces the answer into content. And a free LLM tier limits requests per minute, so the page asks for the two briefs sequentially; firing them in parallel tripped the quota on the very first click.
A note on the hosted demo
Every free LLM tier runs out — Cloudflare Workers AI caps a day at 10,000 neurons, Gemini's free tier at 20 requests — and one brief spends about eight calls. Rather than let the demo answer a judge with a stack trace, it falls through a list of OpenAI-compatible providers, and if they are all spent it serves a real saved run from this same code, labelled on screen as a saved run. So you may occasionally see that banner instead of a live call; it is a deliberate floor, not a failure, and it never passes saved output off as live. There is also a cap of 6 briefs per visitor per 10 minutes.
What I learned
A 200 is not a success. The most expensive bug here was an API answering cheerfully with nothing, and
the only reason I caught it was that I had built the call trace into the UI for the judges' benefit — it
ended up being the debugging tool. Making an agent show its work is not just a demo feature; it is how
you find out what it is actually doing.
What's next
Arabic-first venue search, a weekly refresh so a café can watch its neighborhood's taste drift, and per-branch briefs for small chains.
Built With
- ai-agents
- arabic
- cloudflare-workers
- javascript
- llm
- marketing
- qloo
- saudi-arabia
- taste-graph
- workers-ai
Log in or sign up for Devpost to join the conversation.