WallpaperMachine
The wallpapers you love. A new home on your Mac.
Plans for groups, grounded in what each friend actually loves. Built on the Qloo taste graph.
LifestyleTypeScript · JavaScriptGitHub @pravoobiOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A generic AI guesses what your group might like; Common Ground knows what each of you loves and finds the plan where those tastes overlap.
Live demo · Sample group · Without-Qloo comparison · Make your own group · The story · How it's built
Built for the Qloo Agentic Hackathon. MIT licensed. No sign-up, no keys, nothing to install.
Sample pages replay real recorded runs. They load instantly and make no Qloo or model calls. The people in them are fictional; their tastes went through the real agent and real Qloo data.
Saturday, 4:10 pm, a WhatsApp group of four. "Where are we going tonight?" Forty messages later: one friend is vegetarian, one is on a budget, one wants a brewery, one says "anything is fine" and then vetoes everything. Someone always ends up organising, and they're guessing what everyone else likes.
Recommenders are built for one person. A group needs a place that works for everyone, including the friend whose taste is least like the others, plus one line for each friend on why it works for them, so the organiser can paste it into the chat and be done.
| Step | What happens | Qloo capability |
|---|---|---|
| Who's coming | 2–4 friends, a few things each one loves (artists, films, shows, places, dishes), veg and budget limits | — |
| Resolving tastes | Each taste is matched to a Qloo entity or tag. Ambiguous names ("Anirudh") are resolved and stated; misses ("Sid Sriram's new song") are retried and stated, never guessed | /search, /v2/tags |
| Finding overlap | Candidate restaurants come from everyone's tastes combined, plus each friend's own top places so a minority taste gets a say. Fast-food chains are dropped by tag | /v2/insights with location, category, price and popularity filters |
| Scoring for each friend | Every candidate is scored for each friend from that friend's own tastes, along with which of their tastes drove the match | /v2/insights with filter.results.entities and feature.explainability |
| Picking fairly | Hard limits first (veg, budget). Then the place whose least-happy friend is happiest wins, with a tie-break on the average | Deterministic code, not the model |
| Building the plan | One sentence per friend, citing only evidence Qloo returned; a WhatsApp-ready message | Explainability, place tags and properties |
flowchart TD
U[Browser: /new, /g/id] -->|POST group, capped| API[/api/plan, streams steps/]
U -->|GET sample| R[(Upstash Redis: recorded runs, private)]
API --> A[Claude agent, Tool Runner]
A -->|tools| T[resolve_tastes · choose_option · find_candidates · rank_for_group · explain_pick]
T --> Q[Qloo client: cache, 10 req/s throttle, retries, trace]
Q --> QA[(Qloo API: /search, /v2/tags, /v2/insights)]
Q <--> C[(Redis cache, 7-day TTL)]
T --> S[score.ts + constraints.ts: percentile per friend, maximin, hard limits]
A --> P[Plan: pick from the fair ranking + reasons checked against evidence ids]
The model never produces a number. Claude (claude-sonnet-5-5, effort medium) decides which tools to call, classifies each taste (artist, film, show, place, food), handles ambiguous and unmatched tastes, and writes one sentence per friend. Matching, scoring, constraints and the pick itself are plain TypeScript with tests. Before the plan is shown, the server keeps only the evidence ids that explain_pick actually returned for that friend. A reason with none left is labelled "unverified".
| # | Qloo capability | Exactly what we send | Code |
|---|---|---|---|
| 1 | Entity search | GET /search?query=<taste as typed>&types=urn:entity:{artist,movie,tv_show,book,place}&take=3, then a name-match guard (search always returns something) | lib/qloo/client.ts, lib/qloo/resolve.ts |
| 2 | Tag search (food and drink) | GET /v2/tags?filter.query=<dish>&take=5 → e.g. urn:tag:specialty_dish:place:filter_coffee | client.ts, steps.ts |
| 3 | Group candidate pool | GET /v2/insights?filter.type=urn:entity:place&filter.location.query=<city>&filter.tags=urn:tag:category:place:restaurant&filter.price_level.min=1&filter.price_level.max=<tightest budget>&filter.popularity.min=0.3&signal.interests.entities=<everyone's>&signal.interests.tags=<everyone's food tags>&take=25 | steps.ts |
| 4 | Each friend's own top places | Same as #3 with one friend's signals, take=10; their top 3 join the pool | steps.ts |
| 5 | Per-friend scoring of a fixed pool | Same filters, one friend's signals, filter.results.entities=<pool ids>&feature.explainability=true → query.affinity and which of their tastes drove it |
Every response is validated with zod, cached in Redis by request hash, and logged in a per-run trace that the X-ray shows (never the key). A cold plan for four friends makes 18–22 Qloo calls; reruns hit the cache for most of them.
| Tool | What it does | Number source |
|---|---|---|
resolve_tastes | Free text → Qloo entities or tags; first lookup must use the words as typed (enforced), retries are kept as a trail | Qloo |
choose_option | Resolves an ambiguous taste to one of the options Qloo returned, and only those | Qloo |
find_candidates | Group pool + each friend's top 3, fast food dropped | Qloo |
rank_for_group | Per-friend scores, hard limits, fair ranking, and what averaging would have picked | Qloo + score.ts |
explain_pick | Numbered evidence per friend and per place | Qloo |
Qloo's affinities for one person across a pool sit close together (0.87–0.94 in our spikes), so we first turn each friend's scores into a percentile within the pool (100 = their favourite there). Then we pick the place whose least-happy friend is happiest (maximin), with a tie-break on the average.
From the recorded birthday sample (three flatmates who like bars, and Amma, who likes Carnatic music and filter coffee):
| Kabir | Tanvi | Sameer | Amma | Least happy | Average | |
|---|---|---|---|---|---|---|
| Averaging picks The 13th Floor | 74 | 52 | 100 | 87 | 52 | 78 |
| Fairness picks SkyDeck by Sherlock's | 87 | 57 | 61 | 83 | 57 | 72 |
Averaging chases Sameer's 100 and leaves Tanvi with her weakest match; fairness gives up Sameer's favourite to lift the friend at the bottom. The gap is modest, and we report it as measured.
What it took to see any difference at all: when the pool came from one combined Qloo call, averaging and fairness agreed on every sample. The combined call follows the majority's tastes, so places only the odd one out loves never got considered. Adding each friend's own top places to the pool fixed that. That's a product fix, not a demo trick: the friend least like the others should get a say in what's on the table.
Without Qloo vs with Qloo (same model, same group, same instructions, no tools; generated once, stored and shown on each sample page). Each pick is scored for each friend with Qloo over the same pool; the figure is the least-happy friend's percentile.
| Sample | Without Qloo | Least happy | With Qloo | Least happy |
|---|---|---|---|---|
| College friends (4) | Meghana Foods | 0 | The 13th Floor | 71 |
| Couple (2) | Matsuri | 0 (Nisha 100, Arjun 0) | Don't Tell Mama | 81 |
| Birthday with Amma (4) | Windmills Craftworks | 0 | SkyDeck by Sherlock's | 58 |
Read this with the caveat: the yardstick is Qloo's own taste model and the pool is Qloo's recommendations, so a pick from outside it tends to rank low. It measures distance from each friend's taste profile, not whether real people would enjoy the night more. A blind check with real people is the stronger test and hasn't been done yet. What needs no Qloo score: the no-Qloo plan for Amma's birthday is priced above her stated budget; its reasons include details nothing backs up ("live jazz vibe", "soft and melodic" music); and it put Matsuri in Indiranagar, while the only Matsuri Qloo knows is on Lavelle Road.
Failure → recovery, shown on purpose in the samples:
More, with dates: docs/evidence.md and SPIKES.md.
Per live plan (measured 2026-10-22, claude-sonnet-5-5) | |
|---|---|
| Time | 11–31 s |
| Model | 5–7 turns, 24k–42k input / 0.7k–2k output tokens, about $0.05–0.10 |
| Qloo | 11–22 calls; on reruns, half to all of them come from the Redis cache |
retry-after./new runs the real agent (3 runs per visitor per day). "Fill in an example" fills the form.Requires Node.js 20+, a Qloo hackathon key, an Anthropic API key and (optionally) an Upstash Redis database.
npm install
cp .env.example .env.local # fill in the keys
npm run dev # http://localhost:3000, /new works without Redis
npm run plan -- data/groups/college-friends.json # one live run in the terminal
npm run record -- college-friends # record a sample into Redis
npm run baseline -- college-friends # its without-Qloo comparison
Checks: npm test (77 tests on hand-written fixtures, no network), npm run typecheck, npm run lint, npm run build. npm run test:live makes a few real Qloo and Upstash calls.
.env.local is never committed./new aren't stored; server logs record counts and timings only.Every group-booking product makes the organiser choose first and book second; the choosing is where groups stall. Common Ground is the choosing step. Who pays: group-booking and event apps (a per-plan API that turns "who's coming" into one place everyone likes), and venues and aggregators that want to be the answer for mixed groups. Paid placement would break the fairness promise, so venues would pay for clearly labelled offers attached to a fair pick, never for the pick itself. Why now: taste graphs like Qloo's make per-person affinity across music, film and food available through an API, and agents can now call such tools reliably. An LLM alone writes a confident guess; grounded per-person scores turn it into a plan you can defend in the group chat.
Built with Claude Code (Opus 5.5): plan first (docs/plan.md), then spikes against the real Qloo API written up in SPIKES.md before any product code. The process rules in CLAUDE.md shaped it: deterministic scoring with tests before the agent, interesting failures logged in docs/evidence.md before fixing them (the junk search match, the "Toit Bengaluru" over-cleaning, Burger King as a date-night pick, the majority-biased pool), a security check before every commit, and nothing invented: when Qloo has no data, the UI says so.
steps.ts| 6 | Explainability → reasons | query.explainability["signal.interests.entities"] plus place tags, specialty dishes and descriptions become numbered evidence | steps.ts |
| 7 | Tags as constraints | dietary_option:qloo:vegetarian / non_vegetarian_restaurant decide veg; fast_food, pizza_takeout etc. are dropped | steps.ts |
| 8 | Scoring the no-Qloo pick | The baseline's restaurant is found with #1 (right city, right branch) and scored like #5 | lib/agent/compare.ts |