# Project export: Vetted

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: The agentic shopping team that vets every product before you see it.
- Devpost: https://devpost.com/software/vetted-j92ynh
- GitHub: https://github.com/garysun1/TreeHacksProject
- Video: https://www.youtube.com/embed/ouTFAkL_HAI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Gary Sun (8 commits)

## Devpost submission (written by the team)

### Inspiration

Online shopping is highly suboptimal. As a consumer, you might spend hours bouncing between platforms like Amazon, eBay, Walmart, or Best Buy comparing prices. Or read hundreds of reviews without knowing which ones are fake and miss coupon codes buried across webpages. And when you find something on Facebook Marketplace, you have no idea if the seller may be legitimate. We realized that every pain point in online shopping--whether it be discovery, trust, pricing, or savings--is fundamentally a research problem. Since research is exactly what AI agents are built to do, we were inspired to build a shopping experience where every product is vetted before you ever see it.

### What it does

Vetted is an agentic shopping platform where you describe what you want in natural language, and a specialized multi-agent team handles the rest. The Search Agent scours sites like Amazon, Walmart, Facebook Marketplace, and Craigslist simultaneously. The Trust Agent verifies every seller, detects fake review patterns, and flags potential scams. The Price Agent researches price history and finds competitor prices. The Savings agent surfaces active coupon codes/cash-back opportunities for retail platforms and negotiation strategies for marketplace listings.

### How we built it

Frontend UI Built with TypeScript, Next.js, and Tailwind CSS Designed to feel like a familiar shopping platform by encapsulating search functionalities and product listings Features data gathered on trust, pricing, and savings for each product Real-time pipeline tracker shower agent progress Backend API Built with Python, FastAPI, and Pydantic Error handling on external API calls with third-party tools RESTful endpoints for session management and search Multi-Agent Orchestration Built with LangGraph StateGraph for pipeline orchestration with parallel execution and state checkpointing Bright Data for Search Agent Perplexity Sonar for Trust Agent and Price Agent OpenAI API for Savings Agent

### Challenges we ran into

LangGraph orchestration: getting the multi-agent orchestration correct with conditional edges, parallel execution, and human-in-the-loop required careful state management Rate limiting: running trust and price analysis in parallel for 15+ products meant sending concurrent requests to the Perplexity Sonar API in rapid succession

### Accomplishments we're proud of

Establishing an end-to-end workflow across all agents for any particular product search Developing a frontend that emulates a real shopping platform Building as a solo hacker!

### What we learned

Multi-agent systems require careful thought into what should be automated vs. user-triggered (certain tradeoffs exist between speed and relevance) The right tool for each job matters much more than applying one tool everywhere (Bright Data for structured scraping, Perplexity Sonar for research, OpenAI for persuasive writing)

### What's next

Expanding negotiation capabilities to voiced-based and automated workflows (one that can act autonomously on your behalf) Introducing a social layer where users can share a community-driven shopping intelligence network

## README (from the GitHub repository)

# Vetted

Multi-agent AI shopping assistant that searches across platforms, verifies sellers, analyzes prices, and helps you negotiate — all from a single query. Built for **TreeHacks 2026**.

## How It Works

```
User: "I need a good camera for travel, not too heavy, under $800"
                    │
                    ▼
            ┌──────────────┐
            │ Intent Agent │  ← Understands what you need
            │ (Claude SDK) │
            └──────┬───────┘
                    ▼
            ┌──────────────┐
            │ Search Agent │  ← Amazon, Walmart, Best Buy,
            │ (Bright Data)│    FB Marketplace, Craigslist
            └──────┬───────┘
                    ▼
         ┌──────────┴──────────┐
         ▼                      ▼
  ┌─────────────┐      ┌──────────────┐
  │ Trust Agent │      │  Price Agent │  ← Run in parallel
  │   (Sonar)   │      │    (Sonar)   │
  └──────┬──────┘      └──────┬───────┘
         └──────────┬──────────┘
                    ▼
            ┌──────────────┐
            │   Ranking    │  ← Weighted scoring
            └──────┬───────┘
                    ▼
              Results Ready!
                    │
        ┌───────────┴───────────┐
        ▼                       ▼
   Marketplace?              Retail?
        │                       │
        ▼                       ▼
 ┌──────────────┐      ┌──────────────┐
 │  Negotiate   │      │ Find Savings │
 │  (OpenAI)    │      │ (Price Data) │
 └──────────────┘      └──────────────┘
  On-demand:             On-demand:
  • Haggling messages    • Coupon codes
  • Offer strategies     • Cashback portals
  • Counter-offer tips   • Price-match scripts
```

## Architecture

**Automatic pipeline** runs on every search:
- **Intent Agent** (Anthropic Claude) — Synthesizes vague input into structured product requirements
- **Search Agent** (Bright Data) — Searches Amazon, Walmart, Best Buy, Facebook Marketplace, and Craigslist in parallel
- **Trust Agent** (Perplexity Sonar) — Verifies seller reputation, detects fake reviews, flags scams
- **Price Agent** (Perplexity Sonar) — Compares prices across retailers, checks price history, finds coupons and cashback

**On-demand features** triggered by the user:
- **Negotiate** (OpenAI GPT-4o) — For marketplace listings (FB Marketplace, Craigslist, eBay). Generates persuasive negotiation messages in three tones using competitor prices and price history as leverage.
- **Find Savings** — For retail listings (Amazon, Walmart, Best Buy). Surfaces coupon codes, cashback portals, price-match opportunities, and buy/wait recommendations.

Orchestrated by **LangGraph** with parallel execution, state checkpointing, and streaming status updates.

## Quick Start

```bash
cd shopagent
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env  # Fill in your API keys
uvicorn main:app --reload
```

Frontend:
```bash
cd frontend
npm install
npm run dev
```

API docs at `http://localhost:8000/docs`

## API

```bash
# Create a session
curl -X POST http://localhost:8000/api/sessions \
  -H "Content-Type: application/json" \
  -d '{"query": "I need a camera under $800 for travel"}'

# Send a message to the intent agent
curl -X POST http://localhost:8000/api/sessions/{id}/message \
  -H "Content-Type: application/json" \
  -d '{"message": "Under $800, prefer Sony, need it this week"}'

# Run the full pipeline
curl -X POST http://localhost:8000/api/sessions/{id}/search

# Get ranked results
curl http://localhost:8000/api/sessions/{id}/candidates

# On-demand: negotiate a marketplace listing
curl -X POST http://localhost:8000/api/sessions/{id}/negotiate/{candidate_id}

# On-demand: get savings breakdown for a retail listing
curl http://localhost:8000/api/sessions/{id}/savings/{candidate_id}
```

## Tech Stack

| Component | Technology | Purpose |
|-----------|-----------|---------|
| Backend | FastAPI | API + WebSocket streaming |
| Orchestration | LangGraph | Agent pipeline with parallel execution |
| Intent Agent | Anthropic Claude | Conversational requirement synthesis |
| Search Agent | Bright Data | Multi-platform product scraping |
| Trust Agent | Perplexity Sonar | Seller verification and review analysis |
| Price Agent | Perplexity Sonar | Price comparison, history, coupons, cashback |
| Negotiation Agent | OpenAI GPT-4o | Marketplace haggling strategy and messages |
| Frontend | Next.js + Tailwind | Product search UI deployed on Vercel |
| Data Models | Pydantic v2 | Validation across the pipeline |

## Detected evidence (automated analysis)

Indexed codebase: 78 recognized source files, 404 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — 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
- AI coding agent: Claude Code — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (89 of 89)

```
.env.example
.gitignore
agents/__init__.py
agents/base.py
agents/intent_agent.py
agents/negotiation_agent.py
agents/price_agent.py
agents/search_agent.py
agents/trust_agent.py
api/__init__.py
api/routes.py
api/websocket.py
CLAUDE.md
config.py
frontend/.env.local.example
frontend/.eslintrc.json
frontend/.gitignore
frontend/components.json
frontend/next.config.mjs
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/src/app/checkout/page.tsx
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/AgentActivityIndicator.tsx
frontend/src/components/CartPanel.tsx
frontend/src/components/CheckoutModal.tsx
frontend/src/components/Header.tsx
frontend/src/components/HeroSection.tsx
frontend/src/components/HowItWorksModal.tsx
frontend/src/components/NegotiatePanel.tsx
frontend/src/components/PipelineTracker.tsx
frontend/src/components/ProductCard.tsx
frontend/src/components/ProductDetailPanel.tsx
frontend/src/components/ProductGrid.tsx
frontend/src/components/SavingsBanner.tsx
frontend/src/components/SavingsPanel.tsx
frontend/src/components/SearchSection.tsx
frontend/src/components/SmartFilters.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/progress.tsx
frontend/src/components/ui/sheet.tsx
frontend/src/components/ui/slider.tsx
frontend/src/components/ui/tabs.tsx
frontend/src/components/ui/tooltip.tsx
frontend/src/lib/api.ts
frontend/src/lib/cart-context.tsx
frontend/src/lib/mock-data.ts
frontend/src/lib/toast-context.tsx
frontend/src/lib/types.ts
frontend/src/lib/utils.ts
frontend/tailwind.config.ts
frontend/tsconfig.json
main.py
models/__init__.py
models/candidates.py
models/negotiation.py
models/price.py
models/requirements.py
models/state.py
models/trust.py
orchestrator/__init__.py
orchestrator/graph.py
orchestrator/nodes.py
orchestrator/pipeline.py
orchestrator/ranking.py
README.md
requirements.txt
test_e2e.py
test_negotiation_agent.py
test_price_agent.py
test_search_agent.py
test_trust_agent.py
tests/__init__.py
tests/test_graph.py
tests/test_intent_agent.py
tests/test_models.py
tests/test_pipeline.py
tests/test_search_agent.py
tools/__init__.py
tools/bright_data.py
tools/browserbase.py
tools/elastic.py
tools/perplexity.py
```

### Dependencies

- frontend/package.json: @radix-ui/react-dialog@^1.1.15, @radix-ui/react-progress@^1.1.8, @radix-ui/react-slider@^1.3.6, @radix-ui/react-slot@^1.2.4, @radix-ui/react-tabs@^1.1.13, @radix-ui/react-tooltip@^1.2.8, @types/node@^20, @types/react@^18, @types/react-dom@^18, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^8, eslint-config-next@14.2.35, framer-motion@^12.34.0, lucide-react@^0.564.0, next@14.2.35, postcss@^8, react@^18, react-dom@^18, recharts@^3.7.0, tailwind-merge@^3.4.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5
- requirements.txt: anthropic@>=0.39.0, fastapi@>=0.115.0, httpx@>=0.28.0, langchain-core@>=0.3.0, langgraph@>=0.2.0, langgraph-checkpoint@>=2.0.0, openai@>=1.50.0, pydantic@>=2.10.0, pydantic-settings@>=2.6.0, pytest@>=8.0.0, pytest-asyncio@>=0.24.0, python-dotenv@>=1.0.0, rich@>=13.9.0, tenacity@>=9.0.0, uvicorn[standard]@>=0.32.0, websockets@>=14.0

### Recent commits (newest first)

- added pictures and quality-of-life updates
- transformed negotiation agent into savings agent depending on platform and rebranded product name to Vetted
- improved rendering of products
- updated backend to reflect frontend changes
- refactored frontend with product tags and checkout functionality to emulate real-world online shopping experience
- added Next.js minimal frontend with search page and product listings
- wired up negotiation agent with OpenAI GPT-4o for strategy selection and message generation
- wired up price agent with Perplexity Sonar for price comparison, history, coupons, and cashback
- wired up trust agent with Perplexity Sonar for seller and authenticity verification
- wired up search agent with Bright Data API + mock fallback
- scaffolded multi-agent workflow with langgraph orchestration
- initial commit

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

### CLAUDE.md

```markdown
# Vetted — Multi-Agent AI Shopping Assistant

> TreeHacks 2026 hackathon project

## Purpose
Vetted helps users go from a vague shopping intent to the best possible purchase by orchestrating 5 specialized AI agents in a pipeline.

## Architecture
Five agents orchestrated by a LangGraph StateGraph:

1. **Intent Agent** — Multi-turn conversation → structured `ProductRequirements`
2. **Search Agent** — Fans out across Amazon, Walmart, Best Buy → `ProductCandidate` list
3. **Trust Agent** — Verifies seller reputation, review authenticity → `TrustScore` per candidate
4. **Price Agent** — Price history, coupons, cashback, deal quality → `PriceAnalysis` per candidate
5. **Negotiation Agent** — Price-match, direct negotiation → `NegotiationResult`

Trust + Price agents run **in parallel** via `asyncio.gather` in the `analyze` node.

## Tech Stack
- **Backend:** FastAPI + uvicorn
- **Orchestration:** LangGraph StateGraph with MemorySaver checkpointing
- **Agents:** Anthropic Claude SDK (each agent has its own system prompt + tools)
- **Data Models:** Pydantic v2
- **External APIs:** Bright Data (scraping), Browserbase/Stagehand (automation), Perplexity Sonar (research)
- **Utilities:** httpx, tenacity (retries), rich (logging)

## LangGraph Graph Structure
```
START → intent (loops via conditional edge until requirements_finalized)
      → search
      → analyze (trust + price in parallel)
      → rank
      → negotiate (conditional — skipped if enable_negotiation=False)
      → END
```
- Node names: `intent`, `search`, `analyze`, `rank`, `negotiate`
- Conditional edges: `should_continue_intent`, `should_negotiate`
- Checkpointing: `MemorySaver` for in-memory session persistence
- Interrupt: `interrupt_before=["intent"]` for human-in-the-loop intent flow

## Conventions
- All agent logic is `async`
- Pydantic v2 models for all data structures
- Type hints on every function
- LangGraph nodes are thin wrappers in `orchestrator/nodes.py` that call `agent.run()`
- Python `logging` module throughout (no print statements)
- External API calls use `tenacity` retry with exponential backoff

## How to Run
```bash
cd shopagent
cp .env.example .env   # Fill in API keys
pip install -r requirements.txt
uvicorn main:app --reload
```

## API Overview
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/sessions` | Create session, start intent conversation |
| POST | `/api/sessions/{id}/message` | Send message to intent agent |
| POST | `/api/sessions/{id}/search` | Trigger full search pipeline |
| GET | `/api/sessions/{id}` | Get session state |
| GET | `/api/sessions/{id}/candidates` | Get ranked candidates |
| WS | `/ws/{id}` | Stream real-time agent updates |
| GET | `/health` | Health check |

## Key Files
- `main.py` — FastAPI app entry point
- `config.py` — pydantic-settings configuration
- `models/state.py` — `SharedState` (central pipeline state)
- `agents/base.py` — `BaseAgent` abstract class
- `orchestrator/graph.py` — LangGra
[truncated — 293 more characters]
```

### requirements.txt

```
anthropic>=0.39.0
openai>=1.50.0
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
pydantic>=2.10.0
pydantic-settings>=2.6.0
python-dotenv>=1.0.0
httpx>=0.28.0
websockets>=14.0
tenacity>=9.0.0
rich>=13.9.0
langgraph>=0.2.0
langgraph-checkpoint>=2.0.0
langchain-core>=0.3.0
pytest>=8.0.0
pytest-asyncio>=0.24.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.8",
    "@radix-ui/react-slider": "^1.3.6",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-tooltip": "^1.2.8",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "framer-motion": "^12.34.0",
    "lucide-react": "^0.564.0",
    "next": "14.2.35",
    "react": "^18",
    "react-dom": "^18",
    "recharts": "^3.7.0",
    "tailwind-merge": "^3.4.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.35",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### main.py

```python
"""FastAPI application entry point for Vetted."""

import logging

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from rich.logging import RichHandler

from api.routes import router
from api.websocket import ws_router
from config import settings

# Configure logging
logging.basicConfig(
    level=getattr(logging, settings.log_level.upper(), logging.INFO),
    format="%(name)s - %(message)s",
    handlers=[RichHandler(rich_tracebacks=True)],
)

app = FastAPI(
    title="Vetted",
    description="Multi-agent AI shopping assistant — TreeHacks 2026",
    version="0.1.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(router)
app.include_router(ws_router)


@app.get("/health")
async def health() -> dict[str, str]:
    """Health check endpoint."""
    return {"status": "ok", "service": "vetted"}

```

### frontend/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { TooltipProvider } from "@/components/ui/tooltip";
import { CartProvider } from "@/lib/cart-context";
import { ToastProvider } from "@/lib/toast-context";
import "./globals.css";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });

export const metadata: Metadata = {
  title: "Vetted — Search smarter. Shop better.",
  description: "Search across Amazon, eBay, Walmart, Best Buy, Facebook Marketplace, Craigslist and more. AI-verified deals, trust scores, and negotiation strategies.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${inter.variable} font-sans antialiased`}>
        <CartProvider>
          <TooltipProvider delayDuration={200}>
            <ToastProvider>
              {children}
            </ToastProvider>
          </TooltipProvider>
        </CartProvider>
      </body>
    </html>
  );
}

```

### frontend/src/app/page.tsx

```typescript
"use client";

import { useReducer, useCallback, useState, useRef } from "react";
import { AlertCircle, Wifi, WifiOff } from "lucide-react";
import { Header } from "@/components/Header";
import { HeroSection } from "@/components/HeroSection";
import { HowItWorksModal } from "@/components/HowItWorksModal";
import { CartPanel } from "@/components/CartPanel";
import { PipelineTracker } from "@/components/PipelineTracker";
import { SmartFilters } from "@/components/SmartFilters";
import { ProductGrid } from "@/components/ProductGrid";
import { ProductDetailPanel } from "@/components/ProductDetailPanel";
import { NegotiatePanel } from "@/components/NegotiatePanel";
import { SavingsPanel } from "@/components/SavingsPanel";
import { AgentActivityIndicator } from "@/components/AgentActivityIndicator";
import { AppState, Filters, PipelineStage, Product, NegotiateResponse, SavingsResponse } from "@/lib/types";
import { defaultPipelineStages, defaultFilters } from "@/lib/mock-data";
import { createSession, sendMessage, triggerSearch, getSessionState, simulatePipeline, negotiateCandidate, getSavingsDetail } from "@/lib/api";

// ── Named constants ──────────────────────────────────────────────────
const STAGE_SEARCH_DELAY_MS = 5_000;
const STAGE_ANALYZE_DELAY_MS = 12_000;
const POLL_INTERVAL_MS = 2_000;
const POLL_SAFETY_TIMEOUT_MS = 60_000;

type DataSource = "live" | "demo" | null;

type Action =
  | { type: "SEARCH_START"; query: string }
  | { type: "PIPELINE_UPDATE"; stages: PipelineStage[] }
  | { type: "FILTERS_READY"; filters: Filters }
  | { type: "FILTERS_CHANGE"; filters: Filters }
  | { type: "PRODUCTS_READY"; products: Product[] }
  | { type: "SEARCH_COMPLETE" }
  | { type: "SEARCH_ERROR"; message: string }
  | { type: "SELECT_PRODUCT"; product: Product }
  | { type: "CLOSE_DETAIL" }
  | { type: "DISMISS_ERROR" }
  | { type: "RESET" };

interface ExtendedState extends AppState {
  searchError: string | null;
}

const initialState: ExtendedState = {
  searchQuery: "",
  isSearching: false,
  searchSubmitted: false,
  pipelineStages: defaultPipelineStages,
  filters: defaultFilters,
  candidates: [],
  selectedProduct: null,
  isDetailOpen: false,
  totalSavings: 0,
  flaggedSellers: 0,
  bestDeal: null,
  searchError: null,
};

function computeSavingsInfo(products: Product[]) {
  const totalSavings = products.reduce((sum, p) => sum + p.price.savings, 0);
  const flaggedSellers = products.filter((p) => p.trust.overall < 75).length;
  const best = products.reduce<Product | null>(
    (best, p) => (!best || p.price.savings > best.price.savings ? p : best),
    null
  );
  const bestDeal = best
    ? {
        name: best.name.length > 40 ? best.name.slice(0, 40) + "..." : best.name,
        price: best.price.effectivePrice,
        discount: Math.round(((best.price.originalPrice - best.price.effectivePrice) / best.price.originalPrice) * 100),
      }
    : null;
  return { totalSavings, flaggedSellers, bestDeal };
}

function reducer(state: ExtendedState, action: Action): ExtendedState {
  switch (action.type) {
    case "SEARCH_START":
      return {
        ...initialState,
        searchQuery: action.query,
        isSearching: true,
        searchSubmitted: true,
        searchError: null,
        pipelineStages: defaultPipelineStages.map((s) => ({ ...s })),
      };
    case "PIPELINE_UPDATE":
      return { ...state, pipelineStages: action.stages };
    case "FILTERS_READY":
      return { ...state, filters: action.filters };
    case "FILTERS_CHANGE":
      return { ...state, filters: action.filters };
    case "PRODUCTS_READY": {
      const info = computeSavingsInfo(action.products);
      return { ...state, candidates: action.products, ...info };
    }
    case "SEARCH_COMPLETE":
      return { ...state, isSearching: false };
    case "SEARCH_ERROR":
      return { ...state, isSearching: false, searchError: action.message };
    case "SELECT_PRODUCT":
      return { ...state, selectedProduct: action.product, isDetailOpen: true };
    case "CLOSE_DETAIL":
      return { ...state, isDetailOpen: false, selectedProduct: null };
    case "DISMISS_ERROR":
      return { ...state, searchError: null };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

export default function Home() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [dataSource, setDataSource] = useState<DataSource>(null);
  const [howItWorksOpen, setHowItWorksOpen] = useState(false);
  const [cartOpen, setCartOpen] = useState(false);
  const abortRef = useRef<AbortController | null>(null);
  const sessionIdRef = useRef<string | null>(null);

  // ── Negotiate panel state ─────────────────────────────────────────
  const [negotiateOpen, setNegotiateOpen] = useState(false);
  const [negotiateData, setNegotiateData] = useState<NegotiateResponse | null>(null);
  const [negotiateLoading, setNegotiateLoading] = useState(false);
  const [negotiateError, setNegotiateError] = useState(false);
  const [negotiateProduct, setNegotiateProduct] = useState<Product | null>(null);
  const [negotiateLoadingId, setNegotiateLoadingId] = useState<string | null>(null);

  // ── Savings panel state ───────────────────────────────────────────
  const [savingsOpen, setSavingsOpen] = useState(false);
  const [savingsData, setSavingsData] = useState<SavingsResponse | null>(null);
  const [savingsLoading, setSavingsLoading] = useState(false);
  const [savingsError, setSavingsError] = useState(false);
  const [savingsProduct, setSavingsProduct] = useState<Product | null>(null);
  const [savingsLoadingId, setSavingsLoadingId] = useState<string | null>(null);

  const handleSearch = useCallback(async (query: string) => {
    // Cancel any in-flight search
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;
    const signal = controller.signal;

    dispatch({ type: "SEARCH_START", query });
    setDataSource(null);
    sessionIdRef.curren
[truncated — 12901 more characters]
```

### frontend/src/app/checkout/page.tsx

```typescript
"use client";

import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import {
  ShoppingBag,
  ChevronLeft,
  Lock,
  CreditCard,
  Truck,
  ShieldCheck,
  CheckCircle2,
  X,
  Tag,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useCart } from "@/lib/cart-context";

// ── Platform badge colors ────────────────────────────────────────────
const platformColors: Record<string, string> = {
  Amazon: "bg-amber-100 text-amber-800",
  eBay: "bg-blue-100 text-blue-800",
  Walmart: "bg-blue-100 text-blue-700",
  "Best Buy": "bg-yellow-100 text-yellow-800",
  "Facebook Marketplace": "bg-sky-100 text-sky-800",
  Craigslist: "bg-violet-100 text-violet-800",
};

export default function CheckoutPage() {
  const { items, removeFromCart, subtotal, totalSavings, clearCart } = useCart();
  const [orderPlaced, setOrderPlaced] = useState(false);

  const shippingCost = 0; // free shipping
  const taxRate = 0.0875;
  const tax = subtotal * taxRate;
  const total = subtotal + shippingCost + tax;

  if (orderPlaced) {
    return (
      <div className="min-h-screen bg-gray-50">
        {/* Header */}
        <header className="bg-white border-b border-gray-200 sticky top-0 z-50">
          <div className="max-w-[1100px] mx-auto px-4 py-3 flex items-center gap-3">
            <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
              <ShoppingBag className="h-5 w-5 text-emerald-600" />
              <span className="text-lg font-semibold tracking-tight text-gray-900">
                Vetted
              </span>
            </Link>
            <div className="flex items-center gap-1 ml-auto text-xs text-gray-400">
              <Lock className="h-3 w-3" />
              Secure Checkout
            </div>
          </div>
        </header>

        <div className="max-w-[600px] mx-auto px-4 py-20 text-center">
          <div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-6">
            <CheckCircle2 className="h-10 w-10 text-emerald-600" />
          </div>
          <h1 className="text-2xl font-bold text-gray-900 mb-2">Order Confirmed!</h1>
          <p className="text-gray-500 mb-1">
            Your order of {items.length} item{items.length !== 1 ? "s" : ""} has been placed.
          </p>
          <p className="text-sm text-gray-400 mb-2">Order #SA-{Math.random().toString(36).slice(2, 8).toUpperCase()}</p>
          {totalSavings > 0 && (
            <p className="text-sm font-semibold text-emerald-600 mb-8">
              You saved ${totalSavings.toFixed(2)} with Vetted!
            </p>
          )}
          <Link href="/" onClick={() => clearCart()}>
            <Button className="bg-emerald-600 hover:bg-emerald-700 text-white px-8 h-11">
              Continue Shopping
            </Button>
          </Link>
        </div>
      </div>
    );
  }

  if (items.length === 0) {
    return (
      <div className="min-h-screen bg-gray-50">
        {/* Header */}
        <header className="bg-white border-b border-gray-200 sticky top-0 z-50">
          <div className="max-w-[1100px] mx-auto px-4 py-3 flex items-center gap-3">
            <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
              <ShoppingBag className="h-5 w-5 text-emerald-600" />
              <span className="text-lg font-semibold tracking-tight text-gray-900">
                Vetted
              </span>
            </Link>
          </div>
        </header>

        <div className="max-w-[600px] mx-auto px-4 py-20 text-center">
          <ShoppingBag className="h-16 w-16 text-gray-200 mx-auto mb-4" />
          <h1 className="text-xl font-bold text-gray-900 mb-2">Your cart is empty</h1>
          <p className="text-sm text-gray-500 mb-6">Add some products before checking out.</p>
          <Link href="/">
            <Button className="bg-emerald-600 hover:bg-emerald-700 text-white px-8">
              Back to Shopping
            </Button>
          </Link>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-gray-50">
      {/* ── Checkout header ──────────────────────────────────────── */}
      <header className="bg-white border-b border-gray-200 sticky top-0 z-50">
        <div className="max-w-[1100px] mx-auto px-4 py-3 flex items-center justify-between">
          <div className="flex items-center gap-4">
            <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
              <ShoppingBag className="h-5 w-5 text-emerald-600" />
              <span className="text-lg font-semibold tracking-tight text-gray-900">
                Vetted
              </span>
            </Link>
            <span className="text-gray-300">|</span>
            <h1 className="text-sm font-medium text-gray-700">Checkout</h1>
          </div>
          <div className="flex items-center gap-1 text-xs text-gray-400">
            <Lock className="h-3 w-3" />
            Secure Checkout
          </div>
        </div>
      </header>

      {/* ── Back link ────────────────────────────────────────────── */}
      <div className="max-w-[1100px] mx-auto px-4 pt-5 pb-2">
        <Link href="/" className="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 transition-colors">
          <ChevronLeft className="h-4 w-4" />
          Back to results
        </Link>
      </div>

      {/* ── Main content ─────────────────────────────────────────── */}
      <div className="max-w-[1100px] mx-auto px-4 pb-12">
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">

          {/* ── Left column: Shipping + Payment ───────────────────── */}
          <div className="lg:col-span-2 space-y-6">

            {/* Shipping */}
            <section className="bg-white rounded-xl border border-gray-200 p-6">
              <div className="flex items-center gap-2 mb-5">
 
[truncated — 10919 more characters]
```

### config.py

```python
"""Application configuration loaded from environment variables."""

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    """Central configuration for Vetted."""

    # Required API keys
    anthropic_api_key: str = ""
    brightdata_api_key: str = ""
    # Optional Bright Data: zone for SERP/Request API; dataset IDs for Web Scraper API
    brightdata_zone: str = ""
    brightdata_amazon_search_dataset_id: str = "gd_lwdb4vjm1ehb499uxs"  # Amazon keyword search
    brightdata_amazon_product_dataset_id: str = "gd_l7q7dkf244hwjntr0"  # Amazon product page
    brightdata_amazon_reviews_dataset_id: str = "gd_le8e811kzy4ggddlq"  # Amazon reviews
    browserbase_api_key: str = ""
    browserbase_project_id: str = ""
    perplexity_api_key: str = ""
    openai_api_key: str = ""  # OpenAI GPT-4o for negotiation agent

    # Optional
    elastic_cloud_id: str = ""
    elastic_api_key: str = ""

    # App config
    log_level: str = "INFO"
    agent_timeout: int = 120
    enable_negotiation: bool = True

    model_config = {
        "env_file": (".env", "../.env"),  # check shopagent/.env first, then repo root
        "env_file_encoding": "utf-8",
    }


settings = Settings()

```

### test_search_agent.py

```python
"""Quick end-to-end test for the SearchAgent."""

import asyncio
import logging
import sys

# Load .env before anything else imports settings
from dotenv import load_dotenv
load_dotenv()

from config import settings
from models.requirements import ProductRequirements
from models.state import SharedState
from agents.search_agent import SearchAgent

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")


async def main() -> None:
    # 1. Check API key status
    has_bright = bool(settings.brightdata_api_key and settings.brightdata_api_key.strip())
    print(f"\n=== Search Agent E2E Test ===")
    print(f"Bright Data API key configured: {has_bright}")
    print(f"Bright Data zone: {settings.brightdata_zone or '(none)'}")
    print(f"Amazon search dataset ID: {settings.brightdata_amazon_search_dataset_id or '(none)'}")
    print(f"Amazon product dataset ID: {settings.brightdata_amazon_product_dataset_id or '(none)'}")
    print(f"Amazon reviews dataset ID: {settings.brightdata_amazon_reviews_dataset_id or '(none)'}")

    # 2. Build requirements
    reqs = ProductRequirements(
        category="mirrorless camera",
        description="mirrorless camera for travel photography",
        must_have=["mirrorless", "4K video"],
        nice_to_have=["image stabilization", "weather sealed", "compact"],
        dealbreakers=["DSLR only"],
        budget_min=None,
        budget_max=800.0,
        brand_preferences=["Sony", "Fujifilm"],
        brand_exclusions=[],
        use_case="travel photography",
        urgency="this_month",
        condition="new",
    )

    # 3. Minimal shared state
    state = SharedState(
        session_id="test-001",
        user_query="mirrorless camera, budget $500-800, Sony or Fujifilm, for travel photography",
        requirements=reqs,
        requirements_finalized=True,
        status="searching",
    )

    # 4. Run the search agent
    from agents.search_agent import _build_search_query
    print(f"\nBuilt search query: \"{_build_search_query(reqs)}\"")

    agent = SearchAgent()
    print("Running SearchAgent...")
    result = await agent.run(state)

    # 5. Print results
    candidates = result.get("candidates", [])
    print(f"\n--- Results ---")
    print(f"Status: {result.get('status')}")
    print(f"Candidates returned: {len(candidates)}")

    if result.get("errors"):
        print(f"Errors: {result['errors']}")

    for i, c in enumerate(candidates, 1):
        print(f"\n  {i}. {c.name}")
        print(f"     Brand: {c.brand} | Price: ${c.price:.2f} | Platform: {c.platform}")
        print(f"     Rating: {c.rating} | Reviews: {c.review_count}")
        print(f"     URL: {c.url}")
        if c.matched_requirements:
            print(f"     Matched reqs: {c.matched_requirements}")

    # 6. Detect mock vs real
    mock_indicators = sum(1 for c in candidates if "Mock" in c.name)
    if mock_indicators == len(candidates) and candidates:
        print(f"\n>>> Data source: MOCK (all {len(candidates)} candidates are mock data)")
    elif mock_indicators > 0:
        print(f"\n>>> Data source: MIXED ({len(candidates) - mock_indicators} real, {mock_indicators} mock fallback)")
    else:
        print(f"\n>>> Data source: REAL API (no mock candidates detected)")


if __name__ == "__main__":
    asyncio.run(main())

```

### test_trust_agent.py

```python
"""Quick end-to-end test for the TrustAgent with real Perplexity Sonar API."""

import asyncio
import logging

from dotenv import load_dotenv
load_dotenv()

from config import settings
from models.candidates import ProductCandidate
from models.state import SharedState
from agents.trust_agent import TrustAgent

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")


async def main() -> None:
    has_pplx = bool(settings.perplexity_api_key and settings.perplexity_api_key.strip())
    print(f"\n=== Trust Agent E2E Test ===")
    print(f"Perplexity API key configured: {has_pplx}")

    # Build a small set of realistic candidates (2 real-ish, 1 suspicious)
    candidates = [
        ProductCandidate(
            id="c001",
            name="Sony Alpha ZV-E10 Mirrorless Camera",
            brand="Sony",
            price=798.00,
            url="https://www.amazon.com/dp/B0FLSPG85G",
            platform="amazon",
            seller_name="Amazon.com",
            rating=4.6,
            review_count=905,
        ),
        ProductCandidate(
            id="c002",
            name="Canon EOS R100 Mirrorless Camera Kit",
            brand="Canon",
            price=715.89,
            url="https://www.amazon.com/dp/B0FP496DLW",
            platform="amazon",
            seller_name="Camera Photo Photo",
            rating=4.5,
            review_count=35,
        ),
        ProductCandidate(
            id="c003",
            name="ProShot 8K Ultra Camera AMAZING DEAL",
            brand="Unknown",
            price=29.99,
            url="https://www.amazon.com/dp/B0FAKE12345",
            platform="amazon",
            seller_name="BestDealzXtreme",
            rating=5.0,
            review_count=12000,
        ),
    ]

    state = SharedState(
        session_id="trust-test-001",
        user_query="mirrorless camera for travel",
        candidates=candidates,
        status="analyzing",
    )

    agent = TrustAgent()
    print(f"\nRunning TrustAgent on {len(candidates)} candidates...\n")
    result = await agent.run(state)

    trust_scores = result.get("trust_scores", {})
    print(f"--- Results: {len(trust_scores)} trust scores ---\n")

    for cid, score in trust_scores.items():
        c = next((x for x in candidates if x.id == cid), None)
        name = c.name[:50] if c else cid
        print(f"  [{cid}] {name}")
        print(f"    Overall: {score.overall_score}/100")
        print(f"    Seller:  {score.seller_score}/100")
        print(f"    Reviews: {score.review_authenticity_score}/100")
        print(f"    Legit:   {score.product_legitimacy_score}/100")
        if score.flags:
            for f in score.flags:
                print(f"    FLAG [{f.severity}] {f.category}: {f.description}")
        print(f"    Reasoning: {score.reasoning[:200]}...")
        print(f"    Sources: {score.sources_checked[:3]}")
        print()

    # Check if we got real or mock data
    mock_count = sum(
        1 for s in trust_scores.values() if "Mock" in s.reasoning
    )
    if mock_count == len(trust_scores):
        print(">>> Data source: MOCK (all scores are mock)")
    elif mock_count > 0:
        print(f">>> Data source: MIXED ({len(trust_scores) - mock_count} real, {mock_count} mock)")
    else:
        print(">>> Data source: REAL API (all scores from Perplexity Sonar)")


if __name__ == "__main__":
    asyncio.run(main())

```

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