# Project export: Sniff

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Your loyal AI shopping guard dog. It fetches the deals. It sniffs out the scams.
- Devpost: https://devpost.com/software/sniff-l9xk1q
- GitHub: https://github.com/thenerd31/sniff
- Demo: https://sniff-treehacks.vercel.app/
- Video: https://www.youtube.com/embed/JK2X10VwiBE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Aswin Surya (23 commits), Davyn Paringkoan (14 commits), manrajmondair (6 commits), Yifan Knag (1 commits)

## Devpost submission (written by the team)

### Inspiration

Online shopping has become a high-stakes trust decision made in seconds with almost no usable information. Is this store real? Is this "sale" legitimate? Is the seller verified? The penalty for guessing wrong isn't a mild inconvenience — it's a drained bank account, identity theft, or a package that never ships. And the impacts are very real: $12.5B lost to fraud in 2024 (25% increase year-to-year) 859,000 complaints with $16B in losses recorded by the Internet Crime Complaint Center 1 in 5 US adults have lost money to an online scam 75% of victims never report it to law enforcement People over 60 suffered nearly $5B in losses alone What makes this uniquely dangerous is that scam sites don't look like scams anymore. They have polished storefronts, convincing checkout flows, and social proof indistinguishable from the real thing. "Safe shopping" today requires the user to cross-reference domain age, SSL validity, safety databases, community sentiment, and seller verification — work most people don't have the time or expertise to do under pressure from countdown timers and urgency language. We built Sniff to close that gap: turn safety from a burden into an automatic agentic layer of commerce without sacrificing price intelligence. What It Does Just tell Sniff what you want. It goes to work. Guided Discovery: Sniff starts a conversation, not a search. It asks the right clarifying questions (gender, style, budget, brand) to understand exactly what you need before searching. Multi-Retailer Search: Searches across Google Shopping, pulling real product listings from Amazon, Best Buy, Walmart, Nordstrom, and dozens more. Dead links get filtered out before you ever see them. 5 Parallel Fraud Checks per Listing: Retailer Reputation: WHOIS domain age, registrar signals, suspicious TLDs Safety Database: Google Safe Browsing + ScamAdviser threat intelligence Community Sentiment: Reddit post mentions, scam reports, real user experiences Brand Impersonation: Detects typosquatting and lookalike domains Page Red Flags: Urgency tactics, missing policies, suspicious payment methods Retailer Reputation: WHOIS domain age, registrar signals, suspicious TLDs Safety Database: Google Safe Browsing + ScamAdviser threat intelligence Community Sentiment: Reddit post mentions, scam reports, real user experiences Brand Impersonation: Detects typosquatting and lookalike domains Page Red Flags: Urgency tactics, missing policies, suspicious payment methods Live-Streaming Results: Products appear, fraud checks fill in, verdicts land — all in real time via Server-Sent Events. You watch the investigation happen. Trust Verdict: Every listing gets a verdict (trusted/caution/danger) and a trust score (0–100). Sniff recommends the best pick — the cheapest option among the safest options. Dramatic Cleanup: Tap "PURGE CURSED" and flagged listings get eliminated in a black hole animation. Remaining safe results shuffle into price order. The best deal gets crowned. How We Built It Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS, Framer Motion, Zustand, Vercel Backend: Next.js API Routes with Server-Sent Events streaming. Each investigation spawns a parallel tool pipeline — WHOIS lookup, Google Safe Browsing, ScamAdviser, Reddit search, brand impersonation detection, page scraping — that streams structured findings as they complete. AI: OpenAI powers the core agent loop. gpt-5-mini handles structured fraud analysis and tool orchestration via function calling. gpt-4o-mini drives the multi-turn query refinement engine. Every tool call returns structured output that the agent reasons over before deciding what to do next. Web Data: Bright Data SERP API for Google Shopping results with real product images and prices. Browserbase + Stagehand for AI-powered browser automation — navigating real retailer pages, extracting structured product data, and verifying prices without brittle CSS selectors. Perplexity Sonar for cross-retailer price research when SERP data isn't enough. Fraud Scoring: 150+ verified retailers allowlist for instant trust decisions. Weighted scoring with normalization across 5 independent signals. Fatal flags for malware/phishing short-circuit to danger regardless of other signals. Early URL validation filters dead links before users ever see them. The Agent Loop Sniff is a multi-turn agent that accumulates context and adapts across steps. Turn 1 — Understand Intent: The query refiner decides if the user's request is specific enough to search, or asks the highest-information-gain clarifying question. Each user answer is fed back as context for the next decision. The agent tracks which dimensions it's already covered (budget, brand, style) and picks the most useful unanswered one. Turn 2 — Search & Validate: Once intent is clear, the agent searches across retailers, validates every URL for reachability, and streams results. It holds all products in working memory while fraud checks run in parallel. Turn 3 — Investigate & Score: For each listing, the agent orchestrates 5 independent tools concurrently, collects their structured outputs, and computes a weighted trust verdict. It uses the full set of results as context — price anomaly detection compares each listing against the trusted-retailer median, so a $49 pair of headphones only gets flagged if trusted retailers sell it for $250+. Turn 4 — Recommend: The agent synthesizes everything — trust scores, prices, fraud signals — and recommends the best deal. The entire chain streams to the frontend as it happens. Challenges We Ran Into Unreliable web data: Listings vary by site and many are JS-rendered, so we had to combine search APIs, scraping, proxies, and browser automation while still normalizing everything into clean, typed results. Real-time agent orchestration: Coordinating a multi-step pipeline (search → checks → verdict → stream) to stay fast, stable, and understandable under SSE was non-trivial. Animation under React Compiler: With reactCompiler: true, re-render-based animations broke, forcing us to re-engineer complex sequences (BlackHoleWipe, ShuffleSort) in pure CSS keyframes. Accomplishments We're Proud Of End-to-end agentic commerce pipeline: multi-turn clarify → streaming search → parallel fraud validation → price-optimized recommendation Modular fraud toolkit (WHOIS, SSL, Safe Browsing, Reddit, seller verification, brand impersonation) wired through a shared type contract Real-time SSE streaming where users watch the investigation happen, not just a loading spinner Polished pixel-art UI with a persistent saved-items dashboard that stores full fraud-report snapshots across sessions What We Learned Fraud detection is fundamentally a calibration problem — the hard part isn't catching scams, it's not flagging legitimate businesses Multi-turn query refinement dramatically improves search relevance vs. raw keyword search Real-time SSE streaming with complex frontend state machines requires careful orchestration — out-of-order events and race conditions are the norm Pixel art is surprisingly time-consuming What's Next for Sniff Chrome Extension — Investigate any link in browser before you buy Image Search — Snap a photo, Sniff finds it and validates sellers Deeper Seller Verification — Crawl seller profiles with Stagehand and Browserbase, verify physical addresses, extract business registration data Price Tracking — Monitor prices over time and alert when trusted deals drop

## README (from the GitHub repository)

<p align="center">
  <img src="https://img.shields.io/badge/TreeHacks-2026-orange?style=for-the-badge" alt="TreeHacks 2026" />
  <img src="https://img.shields.io/badge/Next.js-16-black?style=for-the-badge&logo=next.js" alt="Next.js 16" />
  <img src="https://img.shields.io/badge/React-19-61dafb?style=for-the-badge&logo=react" alt="React 19" />
  <img src="https://img.shields.io/badge/TypeScript-strict-3178c6?style=for-the-badge&logo=typescript" alt="TypeScript" />
  <img src="https://img.shields.io/badge/OpenAI-gpt--5--mini-412991?style=for-the-badge&logo=openai" alt="OpenAI" />
  <img src="https://img.shields.io/badge/Deployed-Vercel-000?style=for-the-badge&logo=vercel" alt="Vercel" />
</p>

<h1 align="center">Sniff</h1>
<h3 align="center">Your loyal AI shopping guard dog.<br/>It fetches the deals. It sniffs out the scams.</h3>

<p align="center">
  <a href="https://github.com/thenerd31/sniff"><strong>GitHub</strong></a> &middot;
  <a href="#demo"><strong>Live Demo</strong></a> &middot;
  <a href="#how-it-works"><strong>How It Works</strong></a>
</p>

---

## The Problem

Online shopping is a trust decision made in seconds with almost no usable information.

- **$12.5B** lost to online fraud in 2024 - up 25% year-over-year (FTC)
- **859,000** complaints with $16B in losses (Internet Crime Complaint Center)
- **1 in 5** US adults have lost money to an online scam
- **75%** of victims never report it

Scam sites don't look like scams anymore. They have polished storefronts, real-looking checkout flows, and fake social proof indistinguishable from the real thing. "Safe shopping" today requires cross-referencing domain age, SSL validity, safety databases, community sentiment, and seller verification - work nobody has time to do while a countdown timer pressures them to buy.

**Sniff closes that gap.** Safety becomes an automatic layer of commerce, not a burden.

---

## What It Does

Tell Sniff what you want. It goes to work.

### Guided Discovery
Sniff starts a conversation, not a search. It asks the right clarifying questions - gender, style, budget, brand - to understand exactly what you need before searching. Each answer feeds back into the agent's understanding.

### Multi-Retailer Search
Searches across Google Shopping, pulling real listings from Amazon, Best Buy, Walmart, Nordstrom, and dozens more. Dead links get filtered before you ever see them.

### 5 Parallel Fraud Checks Per Listing

| Check | What It Does |
|---|---|
| **Retailer Reputation** | WHOIS domain age, registrar signals, suspicious TLDs |
| **Safety Database** | Google Safe Browsing + ScamAdviser threat intelligence |
| **Community Sentiment** | Reddit mentions, scam reports, real user experiences |
| **Brand Impersonation** | Detects typosquatting and lookalike domains |
| **Page Red Flags** | Urgency tactics, missing policies, suspicious payment methods |

### Real-Time Streaming
Products appear, fraud checks fill in, verdicts land - all live via Server-Sent Events. You watch the investigation happen, not a loading spinner.

### Trust Verdict
Every listing gets a verdict (**trusted** / **caution** / **danger**) and a trust score (0-100). Sniff recommends the **best pick** - the cheapest option among the safest options.

### Purge & Sort
Tap **PURGE CURSED** and flagged listings spiral into a black hole. Safe results shuffle into price order. The best deal gets crowned.

---

## How It Works

```
User Query
    │
    ▼
┌─────────────────────┐
│   Guided Discovery   │  gpt-4o-mini decides: specific enough to search,
│   (Multi-Turn Agent) │  or ask the highest-info-gain clarifying question?
└─────────┬───────────┘
          │  query refined
          ▼
┌─────────────────────┐
│   Product Search     │  Bright Data SERP → Google Shopping
│                      │  Perplexity Sonar fallback
└─────────┬───────────┘
          │  N products
          ▼
┌─────────────────────────────────────────────┐
│         Parallel Fraud Validation (×N)       │
│                                              │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│  │  WHOIS   │ │  Safe    │ │  Reddit      │ │
│  │  Lookup  │ │  Browsing│ │  Sentiment   │ │
│  └──────────┘ └──────────┘ └──────────────┘ │
│  ┌──────────────┐ ┌────────────────────────┐ │
│  │  Brand       │ │  Page Red Flags        │ │
│  │  Impersonation│ │  (Scraping)           │ │
│  └──────────────┘ └────────────────────────┘ │
└─────────────────────┬───────────────────────┘
                      │  scored & verdicted
                      ▼
              ┌───────────────┐
              │  Best Pick     │  cheapest × safest
              │  Recommendation│
              └───────────────┘
```

All steps stream to the frontend as they happen via SSE.

---

## The Agent Loop

Sniff isn't a single API call - it's a **multi-turn agent** that accumulates context and adapts across steps.

**Turn 1 - Understand intent:** The query refiner decides if the request is specific enough to search, or asks the highest-information-gain clarifying question. It tracks which dimensions it's covered (budget, brand, style) and picks the most useful unanswered one.

**Turn 2 - Search & validate:** Searches across retailers, validates every URL for reachability, streams results. Holds all products in working memory while fraud checks run in parallel.

**Turn 3 - Investigate & score:** Orchestrates 5 independent tools concurrently per listing, computes weighted trust verdicts. Price anomaly detection compares each listing against the trusted-retailer median - a $49 pair of headphones only gets flagged if trusted retailers sell it for $250+.

**Turn 4 - Recommend:** Synthesizes trust scores, prices, and fraud signals to recommend the best deal. The entire chain streams live.

---

## Tech Stack

| Layer | Technology |
|---|---|
| **Framework** | Next.js 16 (App Router), React 19, TypeScript (strict) |
| **Styling** | Tailwind CSS 4, Framer Motion, pixel-art game aesthetic |
| **State** | Zustand |
| **AI** | OpenAI gpt-5-mini (fraud analysis, tool orchestration), gpt-4o-mini (query refinement) |
| **Search** | Bright Data SERP API (Google Shopping), Perplexity Sonar (price research fallback) |
| **Scraping** | Bright Data Web Unlocker, Browserbase + Stagehand (AI browser automation) |
| **Safety APIs** | Google Safe Browsing, ScamAdviser |
| **Deployment** | Vercel (SSE streaming, edge-optimized) |

**React Compiler** enabled for production performance. **150+ verified retailer allowlist** for instant trust decisions on major domains.

---

## Architecture

```
src/
├── app/
│   └── api/
│       ├── shop/           POST - SSE stream: product search + fraud checks
│       │   └── refine/     POST - JSON: guided discovery query refinement
│       ├── investigate/    POST - SSE stream: full URL investigation
│       ├── compare/        POST - SSE stream: agentic price comparison
│       └── deepen/         POST - SSE stream: drill deeper on a finding
├── lib/
│   ├── shopping-agent.ts          Multi-step shopping pipeline orchestrator
│   ├── agent.ts                   OpenAI Responses API agent loop
│   ├── query-refiner.ts           Guided Discovery engine
│   ├── scoring.ts                 Threat score calculation
│   ├── known-domains.ts           150+ trusted retailer allowlist
│   ├── stream.ts                  SSE streaming utilities
│   └── tools/
│       ├── validate-product.ts    5-check fraud validation per product
│       ├── fraudCheck.ts          Fraud check orchestrator
│       ├── serpSearch.ts          Bright Data SERP → Google Shopping
│       ├── seller-check.ts        Two-step seller page extraction
│       ├── whois.ts               Domain registration analysis
│       ├── ssl.ts                 Certificate validation
│       ├── safe-browsing.ts       Google Safe Browsing API
│       ├── scamadviser.ts         ScamAdviser trust score
│       ├── reddit.ts              Reddit sentiment search
│       ├── brand-impersonation.ts Typosquatting / l

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 92 recognized source files, 652 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (100 of 100)

```
.env.example
.gitignore
backend.md
CLAUDE.md
eslint.config.mjs
instructions.md
new_backend.md
next.config.ts
package.json
postcss.config.mjs
README.md
scripts/modal_hello.py
scripts/test-api.ts
scripts/test-pipeline.ts
scripts/test-seller-check.ts
scripts/verify-keys.ts
src/app/animationtesting/page.tsx
src/app/api/clarify/route.ts
src/app/api/compare/route.ts
src/app/api/deepen/route.ts
src/app/api/investigate-demo/route.ts
src/app/api/investigate/route.ts
src/app/api/report/route.ts
src/app/api/search/route.ts
src/app/api/shop/refine/route.ts
src/app/api/shop/route.ts
src/app/board_test/page.tsx
src/app/column_design/clarify/clarify.css
src/app/column_design/clarify/page.tsx
src/app/column_design/dashboard/dashboard.css
src/app/column_design/dashboard/page.tsx
src/app/column_design/page.tsx
src/app/column_design/results.css
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/app/results/page.tsx
src/components/clarify/ClarifyingQuestions.tsx
src/components/clarify/CompletionPopup.tsx
src/components/clarify/PixelDogMascot.tsx
src/components/clarify/QuestionCard.tsx
src/components/clarify/ThinkingSummary.tsx
src/components/dashboard/DashboardHeader.tsx
src/components/dashboard/EmptyDashboard.tsx
src/components/dashboard/ItemReport.tsx
src/components/dashboard/SavedDashboard.tsx
src/components/dashboard/SavedItemCard.tsx
src/components/results/BlackHoleWipe.tsx
src/components/results/CardBack.tsx
src/components/results/CardFront.tsx
src/components/results/FlipCard.tsx
src/components/results/HorizontalResultsList.tsx
src/components/results/LootFlipCard.tsx
src/components/results/ResultsContainer.tsx
src/components/results/ShuffleSort.tsx
src/components/results/TrustBadge.tsx
src/components/results/TwoColumnLayout.tsx
src/components/UnlockAnimation.tsx
src/example/mcp_agent.py
src/example/web_scrape.py
src/hooks/useResultsStream.ts
src/lib/agent.ts
src/lib/hooks/useInvestigation.ts
src/lib/investigationStore.ts
src/lib/known-domains.ts
src/lib/query-refiner.ts
src/lib/reportGenerator.ts
src/lib/scoring.ts
src/lib/shopping-agent.ts
src/lib/stream.ts
src/lib/tools/brand-impersonation.ts
src/lib/tools/brightdata.ts
src/lib/tools/browserbase.ts
src/lib/tools/error-classify.ts
src/lib/tools/fraudCheck.ts
src/lib/tools/index.ts
src/lib/tools/priceSearch.ts
src/lib/tools/queryUnderstand.ts
src/lib/tools/reddit.ts
src/lib/tools/safe-browsing.ts
src/lib/tools/scamadviser.ts
src/lib/tools/scraper.ts
src/lib/tools/seller-check.ts
src/lib/tools/serpSearch.ts
src/lib/tools/ssl.ts
src/lib/tools/validate-product.ts
src/lib/tools/webSearch.ts
src/lib/tools/whois.ts
src/stores/resultsStore.ts
src/stores/savedDashboardStore.ts
src/styles/results.css
src/types/clarify.ts
src/types/dashboard.ts
src/types/index.ts
summary_of_backend.md
test-data/generate-urls.ts
test-data/mass_test.py
test-data/urls-live.json
test-data/urls.json
tsconfig.json
```

### Dependencies

- package.json: @browserbasehq/sdk@^2.6.0, @browserbasehq/stagehand@^3.0.8, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-slot@^1.2.4, @radix-ui/react-tooltip@^1.2.8, @reactflow/background@^11.3.14, @reactflow/controls@^11.2.14, @reactflow/core@^11.11.4, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, babel-plugin-react-compiler@1.0.0, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.1.6, framer-motion@^12.34.0, lucide-react@^0.564.0, next@16.1.6, openai@^6.22.0, puppeteer-core@^24.37.3, react@19.2.3, react-dom@19.2.3, reactflow@^11.11.4, tailwind-merge@^3.4.0, tailwindcss@^4, typescript@5.9.3, uuid@^13.0.0, whois@^2.16.1, zod@^4.3.6, zustand@^5.0.11

### Recent commits (newest first)

- Merge branch 'aswin/agent'
- remove scam listing injection from search results
- add custom favicon
- fix README formatting
- add project README
- fix: skip reachability filter for known dangerous domains
- feat: add known dangerous domain blocklist for instant fraud flagging
- fix: wrap useSearchParams in Suspense for Vercel build
- feat: add missing retailer domains to fraud allowlist
- Merge remote-tracking branch 'origin/davyn/frontend' into aswin/agent
- integrated dashboard with the rest of the pages
- Merge remote-tracking branch 'origin/davyn/frontend' into aswin/agent
- updated landing page dog
- Merge remote-tracking branch 'origin/davyn/frontend' into aswin/agent
- fixed card flipping
- fixed card flipping functionality
- fix: update GitHub link to new repo name
- bump: max SERP results 12 → 18 to account for dead link filtering
- fix: dead link filtering, temperature errors, landing page copy
- Merge remote-tracking branch 'origin/manraj/integrations' into aswin/agent

## Key source files (fetched from GitHub, selected and truncated for size)

### instructions.md

```markdown
Yifan
You own everything in src/app/api/ and src/lib/tools/. You build the API routes that Davyn's frontend calls, and the tool functions that Aswin's agent invokes.
Shared Types (Everyone reads this first)
Create src/types/index.ts — this is the CONTRACT between frontend and backend.
// src/types/index.ts

export type CardSeverity = "critical" | "warning" | "info" | "safe";

export type CardType =
  | "domain"
  | "ssl"
  | "scam_report"
  | "review_analysis"
  | "price"
  | "seller"
  | "business"
  | "alert"
  | "email"
  | "alternative"
  | "coupon";

export interface EvidenceCard {
  id: string;
  type: CardType;
  severity: CardSeverity;
  title: string;        // Short: "Domain registered 6 days ago"
  detail: string;       // Longer: "Registered Feb 7, 2026 via NameCheap in Lagos, Nigeria"
  source: string;       // "WHOIS Lookup" | "Google Safe Browsing" | "Reddit" | etc.
  confidence: number;   // 0.0 - 1.0
  connections: string[]; // IDs of cards this connects to
  metadata: Record<string, any>; // Flexible extra data
}

export interface PriceCard extends EvidenceCard {
  type: "price";
  metadata: {
    retailer: string;
    price: number;
    currency: string;
    url: string;
    inStock: boolean;
    priceHistory?: { date: string; price: number }[];
  };
}

export interface InvestigationState {
  id: string;
  url: string;
  cards: EvidenceCard[];
  connections: { from: string; to: string; label?: string }[];
  threatScore: number;       // 0-100
  savingsAmount?: number;    // for price comparison mode
  status: "investigating" | "complete" | "error";
  turn: number;
}

// API Request/Response types
export interface InvestigateRequest {
  url: string;
}

export interface DeepenRequest {
  investigationId: string;
  focus: "seller" | "reviews" | "business" | "alternatives" | "price_history";
}

export interface CompareRequest {
  productUrl: string;
}

// Server-Sent Event types (streamed to frontend)
export type SSEEvent =
  | { event: "card"; data: EvidenceCard }
  | { event: "connection"; data: { from: string; to: string; label?: string } }
  | { event: "threat_score"; data: { score: number } }
  | { event: "narration"; data: { text: string } }
  | { event: "done"; data: { summary: string } }
  | { event: "error"; data: { message: string } };

DAVYN — Frontend Step-by-Step
You own everything in src/components/, src/app/page.tsx, and src/stores/. You talk to Yifan's API routes via fetch + SSE. You never write backend code.

ASWIN — Agent Architecture Step-by-Step
You own the agent system prompt, tool definitions, and orchestration logic. You work closely with Yifan (he builds the tools, you decide how the agent uses them).


```

### summary_of_backend.md

```markdown
# Sentinel Backend Architecture

## One endpoint: `POST /api/investigate`

User pastes **any URL**. The agent decides what to do.

---

## Decision Tree

```
User pastes URL
      |
      +-- Known retailer? (Amazon, Walmart, Best Buy, etc.)
      |   +-- PATH A: Skip investigation -> price comparison
      |
      +-- Unknown site?
          +-- PATH B: Full investigation -> then decide
              |
              +-- Dangerous + selling a product?
              |   +-- "THE TURN" -- warn about scam + find legit alternatives
              |
              +-- Dangerous + not a product?
              |   +-- Warn: "Do not enter payment info"
              |
              +-- Safe + product page?
              |   +-- Compare prices across retailers
              |
              +-- Safe + not a product?
                  +-- Done -- just show investigation results
```

---

## PATH A: Known Retailer (e.g. amazon.com/airpods)

1. **Detect** -- URL matches retailer domain list -> threat score 0, skip fraud tools
2. **Price comparison** -- `runPriceComparison()`:
   - Scrape original URL price (Bright Data -> Browserbase fallback)
   - Perplexity finds same product on other retailers (URLs only)
   - Scrape each retailer URL in parallel (Bright Data -> Browserbase)
   - Fallback: Perplexity price search if scraping fails
3. **Savings card** -- cheapest price, spread, connections to all price cards
4. **Done** -- "Compared 5 prices (5 verified via live scraping)"

**Typical result:** 5 price cards + savings card + connections. ~60s.

---

## PATH B: Unknown Site (e.g. luxurydesigneroutlet.shop)

### Phase 1 -- Parallel Tool Scan (~10s)

8 tools run simultaneously, cards stream in real-time:

| Tool | What it checks | Example output |
|---|---|---|
| WHOIS Lookup | Domain age, registrar, country | "Registered 6 days ago in Nigeria" |
| SSL Analysis | Certificate validity, issuer, mismatch | "Self-signed cert / Let's Encrypt" |
| Google Safe Browsing | Known malware/phishing database | "Not flagged" or "MALWARE detected" |
| Brand Impersonation | Typosquatting, lookalike domains (GPT-4o-mini) | "Likely impersonating Nike" |
| Reddit Search | Scam reports in r/Scams, r/IsItAScam | "12 scam reports, 847 upvotes" |
| Page Scanner | Urgency tactics, missing policies, fake discounts | "No return policy, countdown timer" |
| ScamAdviser | Trust score, risk category | "Trust score: 12/100" |
| Web Search (Perplexity) | General reputation research | "Zero legitimate presence online" |

Each card streams immediately with severity + confidence. Incremental threat score updates live.

### Phase 2 -- Veto Scoring

After all tools finish, `computeFinalThreatScore()` applies a tiered hierarchy:

| Tier | Condition | Score |
|---|---|---|
| Tier 4 | Known-safe domain (Tranco ~150 allowlist) | -> 0 |
| Tier 1 | Brand impersonation (conf >= 0.8) OR Safe Browsing flagged | -> 100 |
| Tier 2 | Young domain (<30d) AND free SSL cert | -> 75 |
| Tier 3 | Scraper red flags 
[truncated — 3971 more characters]
```

### package.json

```
{
  "name": "sentinel",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@browserbasehq/sdk": "^2.6.0",
    "@browserbasehq/stagehand": "^3.0.8",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@reactflow/background": "^11.3.14",
    "@reactflow/controls": "^11.2.14",
    "@reactflow/core": "^11.11.4",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "framer-motion": "^12.34.0",
    "lucide-react": "^0.564.0",
    "next": "16.1.6",
    "openai": "^6.22.0",
    "puppeteer-core": "^24.37.3",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "reactflow": "^11.11.4",
    "tailwind-merge": "^3.4.0",
    "uuid": "^13.0.0",
    "whois": "^2.16.1",
    "zod": "^4.3.6",
    "zustand": "^5.0.11"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "5.9.3"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Plus_Jakarta_Sans, Geist_Mono, Playfair_Display } from "next/font/google";
import "./globals.css";

const jakarta = Plus_Jakarta_Sans({
  variable: "--font-jakarta",
  subsets: ["latin"],
  weight: ["400", "500", "600", "700", "800"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

const playfair = Playfair_Display({
  variable: "--font-playfair",
  subsets: ["latin"],
  style: ["normal", "italic"],
});

export const metadata: Metadata = {
  title: "Sniff - Your Personal Shopping Assistant",
  description:
    "Find the best deals, uncover hidden markups, and save money on every purchase with AI-powered price discovery.",
  icons: {
    icon: [
      { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
      { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
    ],
    apple: "/apple-icon.png",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${jakarta.variable} ${geistMono.variable} ${playfair.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### src/types/index.ts

```typescript
// src/types/index.ts
// SHARED CONTRACT — Frontend and Backend must agree on these types.

export type CardSeverity = "critical" | "warning" | "info" | "safe";

export type CardType =
  | "domain"
  | "ssl"
  | "scam_report"
  | "review_analysis"
  | "price"
  | "seller"
  | "business"
  | "alert"
  | "email"
  | "alternative"
  | "coupon";

export interface EvidenceCard {
  id: string;
  type: CardType;
  severity: CardSeverity;
  title: string;        // Short: "Domain registered 6 days ago"
  detail: string;       // Longer: "Registered Feb 7, 2026 via NameCheap in Lagos, Nigeria"
  source: string;       // "WHOIS Lookup" | "Google Safe Browsing" | "Reddit" | etc.
  confidence: number;   // 0.0 - 1.0
  connections: string[]; // IDs of cards this connects to
  metadata: Record<string, any>; // Flexible extra data
}

export interface PriceCard extends EvidenceCard {
  type: "price";
  metadata: {
    retailer: string;
    price: number;
    currency: string;
    url: string;
    inStock: boolean;
    priceHistory?: { date: string; price: number }[];
  };
}

export interface InvestigationState {
  id: string;
  url: string;
  cards: EvidenceCard[];
  connections: { from: string; to: string; label?: string }[];
  threatScore: number;       // 0-100
  savingsAmount?: number;    // for price comparison mode
  status: "investigating" | "complete" | "error";
  turn: number;
}

// API Request/Response types
export interface InvestigateRequest {
  url: string;
}

export interface DeepenRequest {
  investigationId: string;
  focus: "seller" | "reviews" | "business" | "alternatives" | "price_history";
}

export interface CompareRequest {
  productUrl: string;
}

/** Raw product result from Google Shopping (serpSearch.ts). */
export interface ProductResult {
  id: string;
  title: string;
  price: number;          // USD number, 0 if unavailable
  currency: string;
  retailer: string;       // "Amazon", "Walmart", etc.
  domain: string;         // "amazon.com"
  url: string;
  imageUrl?: string;
  rating?: number;
  reviewCount?: number;
  snippet?: string;       // shipping tag, promo label, etc.
}

export interface ShoppingRequest {
  /** Free-text search query or product description (optional). */
  text?: string;
  /** Base64-encoded image bytes or full data-URI (optional). */
  imageBase64?: string;
  /** MIME type of the image, e.g. "image/jpeg" (optional). */
  imageMediaType?: string;
}

// Server-Sent Event types (streamed to frontend)
export type SSEEvent =
  | { event: "card"; data: EvidenceCard }
  | { event: "connection"; data: { from: string; to: string; label?: string } }
  | { event: "threat_score"; data: { score: number } }
  | { event: "narration"; data: { text: string } }
  | { event: "done"; data: { summary: string } }
  | { event: "error"; data: { message: string } };

// ── Shopping Agent Types ─────────────────────────────────────────────────

export type FraudCheckName =
  | "Retailer Reputation"
  | "Safety Database"
  | "Community Sentiment"
  | "Brand Impersonation"
  | "Page Red Flags"
  | "Seller Verification"
  | "Link Verification";

export type FraudCheckStatus = "passed" | "warning" | "failed" | "pending";

export interface FraudCheck {
  name: FraudCheckName;
  status: FraudCheckStatus;
  detail: string;
  severity: number; // 0.0 - 1.0
}

export type ProductVerdict = "trusted" | "caution" | "danger";

export interface ProductWithVerdict extends ProductResult {
  checks: FraudCheck[];
  verdict: ProductVerdict;
  trustScore: number; // 0-100
}

export interface SearchRequest {
  query?: string;
  image?: string; // base64 encoded
  url?: string;   // product URL to find alternatives for
  searchQueries?: string[]; // post-refinement query variants
}

// ── Query Refiner Types ───────────────────────────────────────────────────────

export interface ConversationMessage {
  role: "user" | "assistant";
  content: string;
}

export interface RefinementOption {
  /** Short vivid label, e.g. "The Streetwear Look" */
  label: string;
  /** One-line description of this lifestyle/use-case path */
  description: string;
  /** Keywords to append to the query if the user picks this option */
  value: string;
}

export type RefineResult =
  | {
      type: "question";
      question: string;
      options: RefinementOption[];
      internalReasoning: string;
    }
  | {
      type: "ready";
      refinedQuery: string;
      searchQueries: string[];
      internalReasoning: string;
    };

// ── Results UI Phase (animation state machine) ───────────────────────────

export type ResultsPhase =
  | "hidden"
  | "two-columns"
  | "wiping"
  | "shuffling"
  | "final-list"
  | "done";

export type SearchSSEEvent =
  | { event: "narration"; data: { text: string } }
  | { event: "product"; data: ProductResult }
  | { event: "fraud_check"; data: { productId: string; check: FraudCheck } }
  | { event: "verdict"; data: { productId: string; verdict: ProductVerdict; trustScore: number } }
  | { event: "all_products"; data: { count: number } }
  | { event: "best_pick"; data: { productId: string; savings?: number } }
  | { event: "done"; data: { summary: string; totalProducts: number; trustedCount: number; flaggedCount: number } }
  | { event: "error"; data: { message: string } };

```

### src/app/animationtesting/page.tsx

```typescript
"use client";

import { useState, useEffect, useCallback } from "react";
import UnlockAnimation from "@/components/UnlockAnimation";

export default function AnimationTestingPage() {
  const [unlock, setUnlock] = useState(false);
  const [finished, setFinished] = useState(false);
  const [instanceKey, setInstanceKey] = useState(0);

  // Auto-trigger after a short pause so you see the locked state first
  useEffect(() => {
    const timer = setTimeout(() => setUnlock(true), 600);
    return () => clearTimeout(timer);
  }, [instanceKey]);

  const handleReset = useCallback(() => {
    setUnlock(false);
    setFinished(false);
    // Increment key → full remount → all internal state resets
    setInstanceKey((k) => k + 1);
  }, []);

  return (
    <div className="relative flex min-h-screen flex-col items-center justify-center gap-6 bg-[#0a1628]">
      {/* Phase label */}
      <p className="text-sm font-medium tracking-widest uppercase text-white/40">
        {!unlock ? "Locked" : finished ? "Unlocked" : "Unlocking\u2026"}
      </p>

      {/* Animation */}
      <UnlockAnimation
        key={instanceKey}
        unlock={unlock}
        onComplete={() => setFinished(true)}
      />

      {/* Status text */}
      <p className="text-base text-white/30">
        {finished
          ? "Investigation complete."
          : "Investigating best prices\u2026"}
      </p>

      {/* Reset button */}
      <button
        onClick={handleReset}
        className="rounded-lg border border-white/10 bg-white/5 px-6 py-2.5 text-sm font-medium text-white/60 transition-colors hover:border-white/20 hover:bg-white/10 hover:text-white/80"
      >
        Reset
      </button>
    </div>
  );
}

```

### src/lib/tools/index.ts

```typescript
import { whoisLookup } from "./whois";
import { sslAnalysis } from "./ssl";
import { safeBrowsingCheck } from "./safe-browsing";
import { scrapeForRedFlags } from "./scraper";
import { redditSearch } from "./reddit";
import { scamadviserCheck } from "./scamadviser";
import { priceSearch } from "./priceSearch";
import { brandImpersonationCheck } from "./brand-impersonation";
import { sellerCheck } from "./seller-check";
import type { EvidenceCard } from "@/types";

function defineTool(name: string, description: string) {
  return {
    type: "function" as const,
    name,
    description,
    strict: false as const,
    parameters: {
      type: "object" as const,
      properties: {
        url: { type: "string" as const, description: "The URL to investigate" },
      },
      required: ["url"] as const,
      additionalProperties: false as const,
    },
  };
}

// OpenAI function definitions for the agent
export const toolDefinitions = [
  defineTool(
    "whois_lookup",
    "Performs a WHOIS lookup on the target URL's domain. Returns domain registration date, registrar, country, and organization. Young domains are a major scam indicator."
  ),
  defineTool(
    "ssl_analysis",
    "Analyzes the SSL/TLS certificate of the target URL. Checks issuer, validity, expiration, and whether the certificate is self-signed. Missing or invalid SSL is a red flag."
  ),
  defineTool(
    "safe_browsing_check",
    "Checks the URL against Google Safe Browsing database for known malware, phishing, and social engineering threats."
  ),
  defineTool(
    "scrape_red_flags",
    "Scrapes the webpage and analyzes content for scam red flags: fake urgency timers, missing return/refund policies, suspicious payment methods, extreme discount claims."
  ),
  defineTool(
    "reddit_search",
    "Searches Reddit for discussions about the domain, including scam reports, reviews, and user experiences."
  ),
  defineTool(
    "scamadviser_check",
    "Queries ScamAdviser for the domain's trust score and risk assessment based on their database of known scam sites."
  ),
  defineTool(
    "price_search",
    "Searches for the same product across legitimate retailers to compare prices. Use this for price comparison investigations and to find better deals."
  ),
  defineTool(
    "brand_impersonation_check",
    "Uses an LLM to detect whether the URL is impersonating a well-known brand. Catches typosquatting (amaz0n, paypai), keyword stuffing (apple-support-team.com), and lookalike TLDs that Levenshtein-based checks miss."
  ),
  defineTool(
    "seller_check",
    "For marketplace URLs (Amazon, eBay, Etsy), scrapes the product page and uses an LLM to extract the third-party seller's name, rating, review count, and account age, then flags bad sellers. Also analyzes review authenticity for any e-commerce page, detecting fake review patterns like time-clustered posts and generic language."
  ),
];

// Tool executor map
export async function executeTool(
  name: string,
  args: Record<string, string>
): Promise<EvidenceCard | EvidenceCard[]> {
  switch (name) {
    case "whois_lookup":
      return whoisLookup(args.url);
    case "ssl_analysis":
      return sslAnalysis(args.url);
    case "safe_browsing_check":
      return safeBrowsingCheck(args.url);
    case "scrape_red_flags":
      return scrapeForRedFlags(args.url);
    case "reddit_search":
      return redditSearch(args.url);
    case "scamadviser_check":
      return scamadviserCheck(args.url);
    case "price_search":
      return priceSearch(args.url);
    case "brand_impersonation_check":
      return brandImpersonationCheck(args.url);
    case "seller_check":
      return sellerCheck(args.url);
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

```

### src/app/results/page.tsx

```typescript
"use client";

import { Suspense, useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useResultsStore, useProductsWithVerdicts } from "@/stores/resultsStore";
import { ResultsContainer } from "@/components/results/ResultsContainer";
import type { SearchSSEEvent } from "@/types";
import { ArrowLeft, Loader2 } from "lucide-react";
import Link from "next/link";

export default function ResultsPage() {
  return (
    <Suspense>
      <ResultsPageInner />
    </Suspense>
  );
}

function ResultsPageInner() {
  const searchParams = useSearchParams();
  const query = searchParams.get("q") || "";
  const [narration, setNarration] = useState("Starting search...");
  const [isStreaming, setIsStreaming] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const store = useResultsStore();
  const startedRef = useRef(false);

  useEffect(() => {
    if (!query || startedRef.current) return;
    startedRef.current = true;
    store.reset();
    setIsStreaming(true);

    const abortController = new AbortController();

    (async () => {
      try {
        const res = await fetch("/api/search", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ query }),
          signal: abortController.signal,
        });

        if (!res.ok || !res.body) {
          setError("Search failed. Please try again.");
          setIsStreaming(false);
          return;
        }

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split("\n");
          buffer = lines.pop() || "";

          for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
            try {
              const parsed = JSON.parse(line.slice(6)) as { event: string; data: unknown };
              const evt = parsed as SearchSSEEvent;

              switch (evt.event) {
                case "narration":
                  setNarration((evt.data as { text: string }).text);
                  break;
                case "product":
                  store.addProduct(evt.data as Parameters<typeof store.addProduct>[0]);
                  break;
                case "fraud_check": {
                  const fc = evt.data as { productId: string; check: Parameters<typeof store.addFraudCheck>[1] };
                  store.addFraudCheck(fc.productId, fc.check);
                  break;
                }
                case "verdict": {
                  const v = evt.data as { productId: string; verdict: Parameters<typeof store.setVerdict>[1]; trustScore: number };
                  store.setVerdict(v.productId, v.verdict, v.trustScore);
                  break;
                }
                case "best_pick": {
                  const bp = evt.data as { productId: string };
                  store.setBestPick(bp.productId);
                  break;
                }
                case "done": {
                  const d = evt.data as { summary: string };
                  store.setDoneSummary(d.summary);
                  store.setPhase("two-columns");
                  setIsStreaming(false);
                  break;
                }
                case "error": {
                  const e = evt.data as { message: string };
                  setError(e.message);
                  setIsStreaming(false);
                  break;
                }
              }
            } catch {
              // skip unparseable lines
            }
          }
        }
      } catch (err) {
        if (!abortController.signal.aborted) {
          setError("Connection lost. Please try again.");
          setIsStreaming(false);
        }
      }
    })();

    return () => {
      abortController.abort();
    };
  }, [query, store]);

  return (
    <div className="min-h-screen bg-[#FFFAF5]">
      {/* Top bar */}
      <div className="sticky top-0 z-50 border-b border-gray-100 bg-white/80 backdrop-blur-md">
        <div className="mx-auto flex max-w-6xl items-center gap-4 px-6 py-3">
          <Link href="/" className="flex items-center gap-2 text-sm text-muted hover:text-foreground transition-colors">
            <ArrowLeft className="h-4 w-4" />
            Back
          </Link>
          <div className="h-4 w-px bg-gray-200" />
          <p className="text-sm font-medium text-foreground truncate">
            {query}
          </p>
          <div className="flex-1" />
          {isStreaming && (
            <div className="flex items-center gap-2 text-sm text-brand">
              <Loader2 className="h-4 w-4 animate-spin" />
              {narration}
            </div>
          )}
        </div>
      </div>

      {/* Error state */}
      {error && (
        <div className="mx-auto max-w-2xl px-6 pt-20 text-center">
          <p className="text-red-500 font-medium">{error}</p>
          <Link href="/" className="mt-4 inline-block text-sm text-brand hover:underline">
            Try another search
          </Link>
        </div>
      )}

      {/* Streaming narration (while loading, before results appear) */}
      {isStreaming && Object.keys(store.verdicts).length === 0 && !error && (
        <div className="mx-auto max-w-2xl px-6 pt-32 text-center">
          <Loader2 className="mx-auto h-8 w-8 animate-spin text-brand" />
          <p className="mt-4 text-lg font-medium text-foreground">{narration}</p>
          <p className="mt-2 text-sm text-muted">Searching across retailers and running security checks...</p>
        </div>
      )}

      {/* Results — Manraj's animation pipeline */}
      <ResultsContainer />
    </div>
  );
}

```

### src/app/column_design/page.tsx

```typescript
"use client";

import { useEffect, useState } from "react";
import { useResultsStore } from "@/stores/resultsStore";
import { ResultsContainer } from "@/components/results/ResultsContainer";
import type { ProductResult, FraudCheck, ProductVerdict } from "@/types";
import "./results.css";

// ── Mock Data — Simulates what the agent SSE would stream ────────────────

const mockProducts: ProductResult[] = [
  {
    id: "p1",
    title: "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
    price: 278.0,
    currency: "USD",
    retailer: "Amazon",
    domain: "amazon.com",
    url: "https://amazon.com/dp/B09XS7JWHH",
    imageUrl: "https://m.media-amazon.com/images/I/51aYfwjGRZL._AC_SL1500_.jpg",
    rating: 4.7,
    reviewCount: 12453,
    snippet: "Industry-leading noise cancellation with Auto NC Optimizer",
  },
  {
    id: "p2",
    title: "Sony WH-1000XM5 Headphones - Brand New Sealed",
    price: 189.99,
    currency: "USD",
    retailer: "BestAudioDeals",
    domain: "bestaudiodeals.shop",
    url: "https://bestaudiodeals.shop/sony-xm5",
    imageUrl: "https://m.media-amazon.com/images/I/51aYfwjGRZL._AC_SL1500_.jpg",
    rating: 4.9,
    reviewCount: 23,
    snippet: "Unbeatable price! Limited stock available",
  },
  {
    id: "p3",
    title: "Sony WH-1000XM5 Wireless Headphones Black",
    price: 295.0,
    currency: "USD",
    retailer: "Best Buy",
    domain: "bestbuy.com",
    url: "https://bestbuy.com/sony-wh1000xm5",
    imageUrl: "https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6505/6505727_sd.jpg",
    rating: 4.6,
    reviewCount: 8921,
    snippet: "Free shipping, 15-day return policy",
  },
  {
    id: "p4",
    title: "XM5 Sony Headphones CHEAP!! Free AirPods included",
    price: 129.99,
    currency: "USD",
    retailer: "DealzKing",
    domain: "dealzking.xyz",
    url: "https://dealzking.xyz/sony-xm5-bundle",
    imageUrl: "",
    rating: 5.0,
    reviewCount: 3,
    snippet: "Best deal online! Buy now before it's gone!",
  },
  {
    id: "p5",
    title: "Sony WH-1000XM5/S Headphones Silver",
    price: 289.99,
    currency: "USD",
    retailer: "B&H Photo",
    domain: "bhphotovideo.com",
    url: "https://bhphotovideo.com/sony-xm5",
    imageUrl: "https://m.media-amazon.com/images/I/41K5m5LHXAL._AC_SL1500_.jpg",
    rating: 4.7,
    reviewCount: 3412,
    snippet: "Authorized Sony dealer, full warranty",
  },
  {
    id: "p6",
    title: "Sony WH1000XM5 - Refurbished Like New",
    price: 159.0,
    currency: "USD",
    retailer: "QuickFlipElectronics",
    domain: "quickflipelectronics.co",
    url: "https://quickflipelectronics.co/xm5",
    imageUrl: "",
    rating: 4.2,
    reviewCount: 8,
    snippet: "Refurbished with 30-day guarantee",
  },
  {
    id: "p7",
    title: "Sony WH-1000XM5 Noise Cancelling Wireless",
    price: 269.0,
    currency: "USD",
    retailer: "Walmart",
    domain: "walmart.com",
    url: "https://walmart.com/ip/sony-wh1000xm5",
    imageUrl: "https://m.media-amazon.com/images/I/51aYfwjGRZL._AC_SL1500_.jpg",
    rating: 4.5,
    reviewCount: 6234,
    snippet: "Free next-day delivery on orders over $35",
  },
  {
    id: "p8",
    title: "SONY XM5 Premium Headphones - LOWEST PRICE GUARANTEED",
    price: 99.99,
    currency: "USD",
    retailer: "ElectroSavvyDeals",
    domain: "electrosavvydeals.net",
    url: "https://electrosavvydeals.net/xm5-deal",
    imageUrl: "",
    rating: 4.8,
    reviewCount: 7,
    snippet: "We beat any price! 100% satisfaction guaranteed!",
  },
];

const mockChecks: Record<string, FraudCheck[]> = {
  p1: [
    { name: "Retailer Reputation", status: "passed", detail: "Amazon is a globally trusted marketplace with buyer protection.", severity: 0.05 },
    { name: "Safety Database", status: "passed", detail: "No flags in Google Safe Browsing or PhishTank databases.", severity: 0.02 },
    { name: "Community Sentiment", status: "passed", detail: "Overwhelmingly positive sentiment across Reddit and forums.", severity: 0.08 },
    { name: "Brand Impersonation", status: "passed", detail: "Sold and shipped by Amazon.com. Verified business entity.", severity: 0.03 },
  ],
  p2: [
    { name: "Retailer Reputation", status: "warning", detail: "Domain registered 12 days ago. No established reputation.", severity: 0.72 },
    { name: "Safety Database", status: "failed", detail: "Domain flagged by 2 community scam databases.", severity: 0.85 },
    { name: "Community Sentiment", status: "failed", detail: "Multiple Reddit posts warning about this domain.", severity: 0.9 },
    { name: "Brand Impersonation", status: "failed", detail: "No business registration found. WHOIS privacy enabled.", severity: 0.88 },
  ],
  p3: [
    { name: "Retailer Reputation", status: "passed", detail: "Best Buy is a Fortune 100 company with 1,000+ stores.", severity: 0.03 },
    { name: "Safety Database", status: "passed", detail: "Clean across all safety databases.", severity: 0.01 },
    { name: "Community Sentiment", status: "passed", detail: "Strong positive reputation in consumer electronics.", severity: 0.05 },
    { name: "Brand Impersonation", status: "passed", detail: "Publicly traded company (NYSE: BBY). Verified entity.", severity: 0.02 },
  ],
  p4: [
    { name: "Retailer Reputation", status: "failed", detail: "Domain registered 3 days ago via anonymous registrar.", severity: 0.95 },
    { name: "Safety Database", status: "failed", detail: "Flagged by Google Safe Browsing as deceptive.", severity: 0.98 },
    { name: "Community Sentiment", status: "failed", detail: "Zero legitimate mentions. Suspicious social media ads only.", severity: 0.92 },
    { name: "Brand Impersonation", status: "failed", detail: "No business entity. Uses .xyz TLD common in scam sites.", severity: 0.96 },
  ],
  p5: [
    { name: "Retailer Reputation", status: "passed", detail: "B&H Photo is a trusted NYC-based electronics retailer since 1973.", severity: 0.04 },
    { name: "Safety Database", status: "passed", detail: "No flags in a
[truncated — 5018 more characters]
```

### src/app/api/report/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { generateReport } from "@/lib/reportGenerator";
import type { ReportInput } from "@/lib/reportGenerator";

export async function POST(req: NextRequest) {
  try {
    const input: ReportInput = await req.json();

    if (!input.url || !Array.isArray(input.cards)) {
      return NextResponse.json(
        { error: "Missing required fields: url, cards" },
        { status: 400 }
      );
    }

    const report = generateReport({
      url: input.url,
      cards: input.cards,
      connections: input.connections || [],
      threatScore: input.threatScore ?? 0,
      savingsAmount: input.savingsAmount,
    });

    return NextResponse.json(report);
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to generate report" },
      { status: 500 }
    );
  }
}

```

[80 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]