Inspiration
What it does
happy2 — Find out if AI recommends you
Inspiration
Shopping is quietly changing shape. People used to open ten tabs; now they ask one question — "find me trail shoes under $200 for humid weather" — and an agent hands back a single answer. By the time a human sees a storefront, the decision has usually already been made.
That breaks something for merchants. Every storefront on the web is built for eyes: specs baked into product photos, prices injected after hydration, add-to-cart wired to a div with a click handler. A person sees a product page. An agent sees nothing.
The part that bothered us most is that there is no analytics surface for "did an AI assistant recommend me." Search has rank trackers. Ads have dashboards. This channel has nothing. A brand can be systematically invisible to every assistant on the market and the only signal it ever gets is revenue that quietly doesn't arrive.
So we built the missing instrument.
What it does
happy2 points a population of real shopping agents at a store and watches them try to buy something.
- Input — you submit a storefront URL, optionally a sitemap, an agent endpoint, and test SKUs.
- Check — we snapshot the catalogue, run a site audit, and launch agents across four surfaces: web search, browser agents on your actual pages, machine-readable guides (
llms.txt, sitemap, JSON-LD), and agent protocols (ACP / UCP / MCP). Everything streams live — every query issued, every URL fetched, every probe response. - Recommend — findings ranked by how many agents each fix unblocks, each one citing the specific agent runs that produced it and carrying a pasteable snippet.
- Dashboard — where agents dropped out, surface scores, and run history.
The scoring
The core discipline: a model never decides the verdict. A match is settled by URL — a cited URL on the brand's domain is a domain hit; one resolving to the targeted product page is a product hit. Deterministic, reproducible, arguable.
For $N$ shopper agents, let $D$ be the set that discovered the brand and $R \subseteq D$ the set that actually recommended it:
$$ \text{discovery rate} = \frac{|D|}{N} \qquad \text{hit rate} = \frac{|R|}{N} \qquad \bar{r} = \frac{1}{|D|}\sum_{i \in D} r_i $$
The interesting quantity is the gap:
$$ \Delta = \frac{|D| - |R|}{N} $$
$\Delta$ is the whole diagnosis. Agents outside $D$ never found you — that's a discoverability problem, fixed with sitemaps, feeds, and llms.txt; nothing you write on the page helps, because nothing reads the page. Agents in $D \setminus R$ read your product and chose a competitor anyway — that's a structured data and content problem, and more sitemap entries change nothing.
Two failures that look identical in a revenue chart, with opposite fixes. Separating them is the single most diagnostic thing the tool does.
How we built it
Contracts first. Before either service existed we froze shared/contracts/ — the CheckResult document, an enumerated failure-code registry, and runtime validators imported by both sides. Check calls assertCheckResult before writing; Evaluate calls it on read. A schema mismatch became a compile error instead of a 3am integration surprise, and it let the frontend and the findings engine both be built to completion against a fixture before the agent runner existed.
Frontend — Next.js 15 App Router, TypeScript, CSS Modules. Four screens as real routes rather than a step state machine, so a URL always restores the view. The design came in as a high-fidelity HTML prototype; we rebuilt it rather than copying it, keeping one load-bearing motif — a chevron track, clip-path-clipped, with an amber→red ramp where chevron $i$ of $n$ takes hue
$$ H_i = 88 - 62 \cdot \frac{i}{n-1} $$
Backend — Bun + Hono. Catalogue snapshot, site audit, persona generation, the agent population, scoring, and an SSE event bus. The stream replays everything that already happened on connect, so joining late still yields the whole run.
Real agents, not scripts. Browserbase + Stagehand drive actual headless browsers against the live store. Personas and written findings come from Cloudflare Workers AI; the browser agents' act/observe calls route through Browserbase's Model Gateway or our own OpenAI key.
One hard rule that shaped everything: no product category may appear in code. Personas are generated per catalogue from category-agnostic intent archetypes. A failure code is MISSING_ATTRIBUTE_EVIDENCE with attribute: "waterproof", never MISSING_WATERPROOF_EVIDENCE. The validator rejects an attribute-scoped code with no attribute, so the rule enforces itself rather than relying on discipline. Soap and running shoes take the same path.
Challenges we ran into
Free-tier browser concurrency. Browserbase gives 3 concurrent browsers and one browser-hour per account. Ten agents don't fit. We ended up pooling comma-separated API keys, each spending its own account's quota — with $k$ keys you get $3k$ real agents and script the remainder, degrading gracefully rather than failing.
Real agents are slow and flaky. A live run takes minutes, and any of it can time out mid-demo. This forced a hard architectural split: the fast, free, deterministic measurement (site audit, catalogue crawl, protocol probes) runs independently of the slow, expensive, unreliable one (browser agents), so a run always produces evidence even when the agents struggle.
Two pipelines, one product. We built a spec-shaped orchestrator and a pragmatic one that shipped, and for a while both existed — 800 lines of the "correct" implementation sitting unreachable while the live path did something subtly different. An import walk from the entrypoint (56 of 66 modules reached) made the dead half visible. The lesson was uncomfortable: a tested, validated, beautifully-typed module that nothing calls is worth exactly zero.
Frontend and backend drifted anyway. Despite the shared contracts, the live event stream and the contract's StageName model diverged — the frontend still speaks a six-stage journey while the contract defines four stages plus a discovered/recommended split. Contracts prevent silent drift; they don't prevent drift.
A CSS bug that taught us something. Animations on the live agent view silently stopped working. CSS Modules scopes @keyframes names — a module referencing a keyframe declared in a global stylesheet gets a renamed reference that matches nothing, and the animation just doesn't run. No error, no warning. Keyframes now live in the same module as the classes that use them.
What we learned
- Evidence in, evidence out. Content generators produce plausible copy against no measurement; audit tools produce checklists against no verification. Every finding here names the agent runs that produced it and the failure codes it addresses. The validator walks every reference and rejects any that doesn't resolve, so a finding cannot be generic advice wearing a finding's clothes.
- Derived values should never be stored. A stored copy of a computed number drifts from it. Scores are recomputed during validation and must agree to within $0.001$ or the document is rejected.
- Design the demo failure mode first. "What does this look like when the network is hostile" is an architecture question, not a polish question, and we asked it too late.
What's next
- Wire the baseline re-run so a fix is proven, not just recommended — a child run carrying
baseline_report_id, with the addressed failure codes verified to stop appearing. - Surface the site-audit probe table in the UI. It's the cheapest, most trustworthy evidence we collect and it's currently invisible.
- Bring-your-own-key tier, and hosted artifacts so adopting a fix is a paste rather than a project.
Built with: typescript, next.js, react, bun, hono, browserbase, stagehand, cloudflare-workers-ai, anthropic-claude, openai, zod, css-modules, tailwind, tremor, recharts, server-sent-events, postgresql, sqlite, docker, render
Built With
- anthropic-claude
- browserbase
- bun
- cloudflare-workers-ai
- css-modules
- docker
- hono
- next.js
- openai
- postgresql
- react
- recharts
- server-sent-events
- sqlite
- stagehand
- tailwind
- tremor
- typescript
- zod
Log in or sign up for Devpost to join the conversation.