# Project export: Bang4YourBuck

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: UC Berkeley AI Hackathon 2026
- Tagline: We'll get you a bang for your buck! Buy secondhand! Save the planet! Save your money in the process!
- Devpost: https://devpost.com/software/bang-4-your-buck
- GitHub: https://github.com/pranavgowrish/Dealbot
- Video: https://www.youtube.com/embed/WgWwGQ7PApY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Pranav Gowrishankar (10 commits), pratheek28 (5 commits), ssrin051 (4 commits)

## Devpost submission (written by the team)

### Inspiration

Last week, one of us spent three hours messaging eleven different sellers on Facebook Marketplace just to buy a desk. Four never responded. Two disappeared after we'd already agreed on a price. We hadn't bought a desk, we'd just wasted an afternoon scrolling and waiting. That frustration turned into a bigger question: why does secondhand shopping still work the same way it did a decade ago, even as the market around it explodes. The global secondhand market is now worth roughly $393 billion, growing nearly four times faster than retail, with Gen Z and millennials driving most of that growth. Secondhand already wins on price, the problem has always been finding the right item without losing hours to spam and unresponsive strangers. Everyone we knew had their own version of this story, students furnishing apartments, friends building PCs on a budget, resellers flipping for extra income. We also weren't alone in thinking AI could help, nearly two thirds of shoppers already say they're comfortable letting AI handle part of the buying process. So we set out to build something that doesn't just search for you, it goes out and gets the deal.

### What it does

Picture this: you fill out a quick form describing what you want, your budget, your location, your condition preferences. Seconds later, five AI agents are simultaneously messaging five different sellers across multiple marketplaces, each one reading the room and negotiating like a sharp human would. One agent realizes its seller mentioned moving out this weekend and leans into a same day cash offer. Another senses a firm, no nonsense seller and switches to a respectful, no haggle close. Meanwhile, a sixth agent, the manager, is watching all five conversations at once, deciding in real time which seller is actually worth pushing harder on and which negotiation isn't worth the effort. Bang4YourBuck is an autonomous shopping agent built for the messiest, most human corner of ecommerce: secondhand, peer to peer marketplaces, where there is no checkout API and no structured pricing, just a stranger on the other end of a chat. You approve the final price, we handle everything that gets you there.

### How we built it

This project would not have come together without leaning hard into our sponsor stack, and honestly, each piece earned its place. LangGraph is the backbone of our agent orchestration. It let us model our manager and worker agents as a real graph of coordinated state instead of a tangle of prompt chains, which is the only reason a six agent swarm was even manageable in a weekend. Browserbase gave us reliable, real browser automation against live marketplace listings, something that would have been a nightmare to hand roll with brittle scraping scripts. It just worked, even against sites that actively resist bots. Redis 8 is doing more heavy lifting in this project than any other single piece of infrastructure. We embed every listing as a vector and run native Redis vector similarity search to confirm two items are actually comparable before letting one agent's negotiated price become leverage for another, so a banged up item selling cheap never gets mistaken for a comp against a mint condition one. On top of that, Redis Pub/Sub broadcasts live price breakthroughs across all five agents instantly, while a Redis Stream keeps a permanent, replayable log of every single negotiation event. Vector search, real time messaging, and durable event logging, all from one platform, is genuinely what made our shared intelligence layer possible. Anthropic's Claude powers every agent's reasoning, from reading a seller's tone to deciding which negotiation tactic to deploy. The quality of judgment we got out of the model, distinguishing a motivated seller from a stubborn one, choosing when to bundle versus when to walk away, is what turned this from a scripted bot into something that actually feels like it is negotiating. Arize Phoenix, auto-instrumented on top of LangGraph and the Anthropic SDK, gave us full tracing of the entire swarm with almost no setup. Because we made sure each worker's negotiation is nested as its own clean subtree, we can click into any single agent mid demo and watch its exact conversation and Redis reads happening live. The first time we saw all five negotiations rendered as a clean execution tree, in real time, was the moment this stopped feeling like a hackathon project and started feeling like a real product.

### Challenges we ran into

Coordinating multiple agents without duplicated effort was harder than it sounds. Early on, our workers would sometimes chase the same listing or act on stale information, so we had to build real communication between the manager and workers instead of just letting them run independently. Designing negotiation logic that actually felt human, rather than a script repeating the same lines, took a lot of iteration against real listings. We also had to scope aggressively. We had bigger ideas than we had hours, so we made deliberate trade offs about which features mattered most and cut the rest so we could ship something that worked end to end rather than something half finished across too many features.

### Accomplishments we're proud of

Watching five agents negotiate live, each reading its seller differently and reaching for a different tactic, while a sixth agent dynamically reroutes strategy across all of them, is the most genuinely exciting thing any of us have built. It does not feel like five copies of the same chatbot, it feels like five negotiators on a call floor. We're especially proud of getting Redis vector search to do real condition aware leverage matching instead of a naive lowest number broadcast, and of getting full Phoenix tracing working cleanly across a parallel agent swarm, which let us prove on stage, not just claim, that the system is doing exactly what we say it's doing.

### What we learned

We learned that real multi-agent coordination means agents that actually change each other's behavior, not just agents that report back to a shared list. We learned how much a single platform like Redis can carry when you actually dig into its newer capabilities, vector search, Pub/Sub, and Streams covered three completely different problems for us without adding three different pieces of infrastructure. And we learned that observability is not a nice to have bolted on at the end, tracing our agents from day one made debugging faster and gave us our single best demo moment.

### What's next

We want to expand beyond furniture and electronics into more categories, and bring in more marketplaces beyond the ones we focused on this weekend. We're also interested in handling logistics after a deal closes, like coordinating pickup times or connecting with local delivery options. On the negotiation side, we want to keep sharpening our agents' tactics with more real world data so they get better at reading sellers and timing offers. We also want to build in real scam protection, flagging listings and sellers that show common fraud patterns before a user ever starts negotiating. On the search side, we want to combine keyword search with our image and description matching so results stay accurate even when a listing's photos or text don't fully line up. And we want our agents to actually assess the product itself, checking that the condition and details match what's described before any negotiation even begins, instead of taking a listing at face value. Longer term, we'd like to support frequent users like resellers and flippers with tools that go beyond a single purchase and help them track opportunities across the market over time.

## README (from the GitHub repository)

# Dealbot

## Detected evidence (automated analysis)

Indexed codebase: 33 recognized source files, 291 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (40 of 40)

```
.gitignore
backend/_graph_smoketest.py
backend/agent_events.py
backend/agent_graph.py
backend/agent_initializer.py
backend/browserb.py
backend/example.py
backend/instrumentation.py
backend/login_facebook.py
backend/main_backend.py
backend/memory.py
backend/orchestration.py
backend/requirements.txt
docker-compose.yml
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/public/.gitkeep
frontend/src/App.tsx
frontend/src/components/AgentNegotiationGraph.tsx
frontend/src/components/AgentStatusLog.tsx
frontend/src/components/JourneyStepper.tsx
frontend/src/components/ListingAmbientBackground.tsx
frontend/src/components/LoadingProgress.tsx
frontend/src/data/ambientListings.ts
frontend/src/hooks/useAgentGraphStream.ts
frontend/src/hooks/useAgentStream.ts
frontend/src/hooks/useTypewriterPlaceholder.ts
frontend/src/index.css
frontend/src/main.tsx
frontend/src/styles/base.css
frontend/src/styles/forms.css
frontend/src/styles/layout.css
frontend/src/vite-env.d.ts
frontend/tailwind.config.js
frontend/temp
frontend/tsconfig.json
frontend/tsconfig.tsbuildinfo
frontend/vite.config.ts
README.md
```

### Dependencies

- backend/requirements.txt: arize-otel, browserbase, dotenv, fastapi, langchain@>=1.3.10, langchain-anthropic@>=1.4.6, langchain-core@>=1.4.7, langgraph@>=1.2.5, openinference-instrumentation-langchain, paho-mqtt, playwright, pydantic, python-dotenv, redis, redisvl, sentence-transformers, stagehand, uvicorn
- frontend/package.json: @types/react@^19.1.8, @types/react-dom@^19.1.6, @vitejs/plugin-react@^4.7.0, autoprefixer@^10.4.21, framer-motion@^12.23.12, mqtt@^5.14.1, postcss@^8.5.6, postcss-import@^16.1.1, react@^19.1.0, react-dom@^19.1.0, tailwindcss@^3.4.17, typescript@~5.8.3, vite@^7.0.4

### Recent commits (newest first)

- Merge pull request #1 from pranavgowrish/sanvi
- blah
- Agent Traversal
- update to pratheek
- push
- latest update v2.0 but bb not working for message
- combined frontend to backend
- fixing merge
- Langgraph AI Agent
- merged pranavs branch into mine
- merged in pranavs
- Initial agent stuff with redis works
- arize logic
- ui as of now is done
- initilaize browsers
- merged
- Browserbase works
- Band AI Agent
- Frontend changes
- Initial changes

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

### docker-compose.yml

```yaml
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - dealbot-redis-data:/data
    command: redis-server --save 60 1 --loglevel warning
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  dealbot-redis-data:

```

### backend/requirements.txt

```
fastapi 
uvicorn 
pydantic

arize-otel 
openinference-instrumentation-langchain
langchain>=1.3.10
langchain-core>=1.4.7
langgraph>=1.2.5
langchain-anthropic>=1.4.6

browserbase
playwright
stagehand
dotenv
python-dotenv
redis
redisvl
sentence-transformers
paho-mqtt

```

### frontend/package.json

```
{
  "name": "dealbot-frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "framer-motion": "^12.23.12",
    "mqtt": "^5.14.1",
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  },
  "devDependencies": {
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "@vitejs/plugin-react": "^4.7.0",
    "autoprefixer": "^10.4.21",
    "postcss": "^8.5.6",
    "postcss-import": "^16.1.1",
    "tailwindcss": "^3.4.17",
    "typescript": "~5.8.3",
    "vite": "^7.0.4"
  }
}

```

### frontend/src/main.tsx

```typescript
/* ==========================================================================
   main.tsx — Application entry point
   Mounts the React root, wraps App in StrictMode, loads global styles.
   ========================================================================== */

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

```

### frontend/src/App.tsx

```typescript
/* ==========================================================================
   App.tsx — One-flow landing interface (root view)
   Flow: idle → fastapi_loading → show_listings → success_confirmed
   ========================================================================== */

import { useEffect, useRef, useState, type FormEvent } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { JourneyStepper } from './components/JourneyStepper';
import { ListingAmbientBackground } from './components/ListingAmbientBackground';
import { LoadingProgress } from './components/LoadingProgress';
import { AgentNegotiationGraph } from './components/AgentNegotiationGraph';

const LOGO_SRC = '/LOGO_SRC.png';
const API_BASE = 'http://localhost:8000';
/** Business rule — max picks before confirm. Change if product allows more/fewer. */
const MAX_LISTING_SELECTIONS = 5;

/** UI-only — quick-fill chips for the search form. */
const PRODUCT_CHIPS = ['Couch', 'iPhone 14 Pro', 'Electric guitar', 'Office chair'];
const BUDGET_CHIPS = ['$300', '$600', '$1000', '$2000'];

type LoadingState =
  | 'idle'
  | 'fastapi_loading'
  | 'show_listings'
  | 'success_confirmed';

type Listing = {
  id: number;
  title: string;
  price: string;
  image_url?: string | null;
  listing_url: string;
  area: string;
  source: string;
};

function parsePrice(value: string): number {
  const parsed = parseFloat(value.replace(/[$,]/g, '').trim());
  return Number.isFinite(parsed) ? parsed : 0;
}

function journeyIndex(state: LoadingState): number {
  switch (state) {
    case 'idle':
      return 0;
    case 'fastapi_loading':
      return 1;
    case 'show_listings':
      return 2;
    case 'success_confirmed':
      return 3;
  }
}

/** Root application view — orchestrates form input, listing approval, and confirmation. */
export default function App() {
  const [product, setProduct] = useState('');
  const [price, setPrice] = useState('');
  const [location, setLocation] = useState('');
  const [dateListed, setDateListed] = useState('');
  const [condition, setCondition] = useState('');
  const [color, setColor] = useState('');

  const [loadingState, setLoadingState] = useState<LoadingState>('idle');
  const [listings, setListings] = useState<Listing[]>([]);
  const [selectedListings, setSelectedListings] = useState<number[]>([]);
  const [searchError, setSearchError] = useState<string | null>(null);
  const [orchestrateError, setOrchestrateError] = useState<string | null>(null);
  const [isConfirming, setIsConfirming] = useState(false);
  const [activeJobId, setActiveJobId] = useState<string | null>(null);
  /** UI-only — controls optional filters accordion; not sent anywhere special. */
  const [showOptionalFilters, setShowOptionalFilters] = useState(false);
  /** UI-only — loader animation driven by elapsed time during search. */
  const [loaderProgress, setLoaderProgress] = useState(0);
  const [loaderStep, setLoaderStep] = useState(0);
  const [loaderListingCount, setLoaderListingCount] = useState(0);

  const loaderIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const isFormVisible = loadingState === 'idle';
  const isFastapiLoading = loadingState === 'fastapi_loading';
  const isShowListings = loadingState === 'show_listings';
  const isSuccessConfirmed = loadingState === 'success_confirmed';
  const isLogoExpanded = isFormVisible;

  useEffect(() => {
    return () => {
      if (loaderIntervalRef.current) clearInterval(loaderIntervalRef.current);
    };
  }, []);

  useEffect(() => {
    if (loadingState !== 'fastapi_loading') {
      setLoaderProgress(0);
      setLoaderStep(0);
      setLoaderListingCount(0);
      if (loaderIntervalRef.current) {
        clearInterval(loaderIntervalRef.current);
        loaderIntervalRef.current = null;
      }
      return;
    }

    const start = Date.now();
    loaderIntervalRef.current = setInterval(() => {
      const elapsed = Date.now() - start;
      const ratio = Math.min(1, elapsed / 120_000);
      setLoaderProgress(ratio * 95);
      setLoaderStep(Math.min(5, Math.floor(ratio * 6)));
      setLoaderListingCount(Math.min(10, Math.floor(ratio * 12)));
    }, 120);

    return () => {
      if (loaderIntervalRef.current) {
        clearInterval(loaderIntervalRef.current);
        loaderIntervalRef.current = null;
      }
    };
  }, [loadingState]);

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    const parsedPrice = parsePrice(price);
    if (!product.trim() || !location.trim() || parsedPrice <= 0) return;

    setSearchError(null);
    setOrchestrateError(null);
    setSelectedListings([]);
    setListings([]);
    setLoadingState('fastapi_loading');

    try {
      const response = await fetch(`${API_BASE}/api/v1/search`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          product: product.trim(),
          price: parsedPrice,
          location: location.trim(),
          dateListed: dateListed.trim() || undefined,
          condition: condition.trim() || undefined,
          color: color.trim() || undefined,
        }),
      });

      if (!response.ok) {
        const body = await response.json().catch(() => null);
        const detail =
          body && typeof body.detail === 'string'
            ? body.detail
            : `Search failed (${response.status})`;
        throw new Error(detail);
      }

      const data = (await response.json()) as { listings: Listing[] };
      setLoaderProgress(100);
      setLoaderListingCount(data.listings.length);
      setListings(data.listings);

      if (data.listings.length === 0) {
        setSearchError('No listings matched your search. Try adjusting your budget or filters.');
        setLoadingState('idle');
        return;
      }

      setLoadingState('show_listings');
    } catch (error) {
      console.error('Marketplace search failed', error);
      setSearchError(
   
[truncated — 19473 more characters]
```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    'postcss-import': {},
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
});

```

### frontend/tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Plus Jakarta Sans', 'sans-serif'],
        display: ['Bricolage Grotesque', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
    },
  },
  plugins: [],
};

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DealBot</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,700;12..96,800&family=JetBrains+Mono:wght@500&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### backend/example.py

```python
# example.py

# Importing instrumentation first ensures tracing is set up
# before `langgraph` is imported.
from instrumentation import tracer_provider

from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city. Returns a short string."""
    if city.lower() in ("sf", "san francisco"):
        return "It's 60 degrees and foggy in San Francisco."
    return f"It's 75 degrees and sunny in {city}."


# ChatAnthropic reads ANTHROPIC_API_KEY from the environment.
agent = create_react_agent(
    model=ChatAnthropic(model="claude-sonnet-4-6"),
    tools=[get_weather],
)

result = agent.invoke({
    "messages": [("user", "What's the weather in San Francisco?")],
})

print(result["messages"][-1].content)
```

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