# Project export: Traide: Sell Anything, Anywhere

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: TrAIde is the AI agent that sells your stuff for you. Just text a photo and price range and it lists, prices, and negotiates the sale across marketplaces. Turn your clutter into cash!
- Devpost: https://devpost.com/software/traid-h6db5a
- GitHub: https://github.com/rishabhroyy/traide
- Video: https://www.youtube.com/embed/VgXrJ3nY2rY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Use of Browserbase)
- Team: 4 GitHub contributor(s) — Rishabh (14 commits), advaith_appajodu (4 commits), Claude Opus 4.8 (1 commits), arjunj1407-creator (1 commits)

## Devpost submission (written by the team)

### Inspiration

We all have busy lives juggling work, school, and everything in between, and none of us actually enjoy the part of selling online where you fight off scammers, dodge lowball offers, and answer the same "is this still available?" message ten times a day. Reselling something should be easy, but the marketplace grind makes people leave money on the table or just never list their stuff at all. So we decided to take the fighting off your hands. trAIde does the haggling, the pricing, and the busywork for you, so you get the value of your items without the headache of the negotiation.

### What it does

trAIde is an AI agent that buys and sells on online marketplaces automatically, starting with eBay. You text it a picture of your item, a quick product description, and answer a couple of questions about your price range and how fast you want it gone. From there it takes over: it researches what comparable items are actually selling for, sets a smart price point, writes and publishes the listing, and then negotiates with buyers on your behalf, accepting fair offers and pushing back on lowballers. The entire experience happens over text, so there is no app to download and no dashboard you are forced to babysit. You just send a photo and let the agent close the deal.

### How we built it

We built trAIde as a texting-native autonomous reselling agent, wiring together four services around a Claude reasoning core. The front door is Poke: a user just texts a photo and a casual note like "sell this, want it gone fast," and our Python backend, a stateless MCP server that Poke connects to, picks it up and replies back through a Poke webhook, so the whole experience lives in chat. Behind that, we used Orkes (Conductor-style orchestration, run via the AgentSpan runtime) to model the agent's work as durable, multi-step tool workflows covering identify, research, price, draft, publish, and negotiate, so each stage is tracked and resumable rather than one fragile script. The hands-on work happens in Browserbase, a real cloud browser that the agent drives with Stagehand to scrape eBay comparables, fill out the listing wizard, and message buyers, with each run exposed as a live, embeddable session. Everything is persisted in Redis as the single source of truth, with RedisVL powering vector search over comparable listings and Redis Agent Memory holding durable seller preferences. Finally, we built a standalone dashboard that reads Redis and Browserbase directly (read-only) to visualize the agent live without touching that backend.

### Challenges we ran into

Getting Browserbase to reliably find and click the correct buttons on eBay's constantly shifting UI was harder than expected, since the listing flow does not always render the same way twice. We worked through it by debugging interactively with Claude Code until we landed on a reliable approach. Our second big hurdle was getting Poke to communicate with the rest of the agent, because the MCP server kept failing to connect. The root cause was a complicated monorepo with a lot of moving ports, so we unified everything behind a single start file that manages all of the ports for us, which made the connection stable. Finally, bypassing captchas and logging into online marketplaces was a real blocker, and we got past it by consulting a Browserbase representative for guidance on the right way to handle authenticated sessions.

### Accomplishments we're proud of

We are proud that we used multiple sponsor technologies in a way that genuinely fit the product, rather than bolting on features just to check a box. Every integration, from Poke to Orkes to Browserbase to Redis, earns its place in the workflow. We are also happy with how clean the user interface and the text-based input system turned out, making a fairly complex agent feel simple to use. Most of all, we are proud that we can take an item from a single text message all the way to a real, live eBay listing, which is the core promise of the whole project working end to end.

### What we learned

This project pushed us to pick up a lot of new tools quickly. We learned how to work with Claude Code as a development partner, how to orchestrate multi-step agent workflows with Orkes, how to use Redis (along with RedisVL and Agent Memory) as both a state store and a search layer, how to drive real browsers at scale with Browserbase, and how to build a texting-native interface on top of Poke. On top of the infrastructure, we also sharpened our UI design skills to turn all of that backend complexity into something clean and approachable.

### What's next

for trAIde: Sell Anything, Anywhere Because we built on Browserbase instead of relying on a single eBay integration, there is huge room to scale: the same agent that drives a real browser can be pointed at almost any marketplace. Next up is expanding beyond eBay to platforms like Facebook Marketplace and Depop, so users can list once and reach buyers everywhere. We also want to round out the experience by adding buying on top of selling, letting the agent hunt for deals and negotiate purchases for you the same way it already handles sales.

## README (from the GitHub repository)

# Berkeley AI Hackathon '26 Project by Rishabh Roy, Arjun Jadhav, and Advaith Appajodu
https://devpost.com/software/traid-h6db5a

# Traide — Text-to-Sell Resale Agent

Text a photo + a casual instruction ("Sell this keyboard, has a scratch, want it
gone quickly") to your **Poke** assistant. trAId identifies the item, researches
eBay with a real logged-in browser, texts you the comparable listings it found
(title + price), prices it, drafts a listing, asks you to approve over text,
publishes to eBay, then **autonomously negotiates** with buyers — only
interrupting you when a sale closes or a listing goes stale. It can also buy:
give it an item and a price cap and it hunts, haggles, and closes under budget.

Built for CalHacks AI Hackathon 2026 (24h). See [`PRD-trAId.md`](../PRD-trAId.md)
for the full spec and [`docs/demo_runbook.md`](docs/demo_runbook.md) for the demo.

## How the sponsors map in
| Sponsor | Role |
|---|---|
| **Poke** | The entire UI. Our `/mcp` server exposes `start_sale`, `approve_sale`, … ; we push proactive texts via a Poke **webhook trigger** (the `/inbound/api-message` endpoint 200s but silently drops messages). No app, no dashboard. |
| **AgentSpan** | Durable workflow runtime + the visual dashboard at `localhost:6767`. Survives restarts; real pause/resume at the approval step. **Also our only LLM path** — `Agent(model="anthropic/claude-…")`. |
| **Browserbase + Stagehand** | One persistent browser context (logged into eBay once in the console) drives research, publish, and offer threads with natural-language `act`/`extract`. |
| **Redis** | RedisVL hybrid comparable search + semantic cache, Redis Agent Memory for seller prefs, plain keys for state + the duplicate-publish lock. |

### One reasoning provider
Every Claude call — product ID, pricing bullets, copy, revision parsing,
negotiation decisions — goes through **AgentSpan's native model layer**
(`app/agents/llm.py`). Nothing in this repo imports the `anthropic` SDK directly.
Stagehand uses its own internal LLM to drive the browser; that's the automation
engine's requirement, separate from our reasoning layer.

## Architecture
```
Poke ──(MCP /mcp)──▶ FastAPI ──▶ AgentSpan sale workflow (durable, resumable)
  ▲                                identify→memory→eBay research→report comps→price→copy
  └──(Poke webhook: proactive)───  →propose→⏸approve→eBay publish→verify→notify
                                   │
            ┌──────────────────────┼───────────────────────────┐
            ▼                      ▼                            ▼
       AgentSpan(Claude)     Browserbase/Stagehand          Redis
                             (1 persistent context)    (RedisVL + Agent Memory
                                                        + state keys + lock)

Post-publish: a sell-side negotiation monitor + a buy-side purchase flow hang off
the same Redis state, the shared Browserbase context, and the Poke path.
```

## Layout
```
app/
  main.py               FastAPI: mounts MCP at /mcp, runs the negotiation monitor
  mcp_server.py         Poke-facing MCP tools
  poke_client.py        outbound proactive Poke messages
  config.py  models.py  settings + Pydantic contracts
  agents/   llm.py (AgentSpan Claude chokepoint), identify, pricing, copywriter,
            revision_parser, negotiator (hard floor/cap guardrails in code)
  browser/  session (one shared context), research(+ebay_research),
            publish(+ebay_publish), threads(+ebay_offers)
  memory/   comparables_index (RedisVL), seller_memory (Agent Memory)
  state/    redis_state (keys + publish lock), negotiation_state (policy+scheduler)
  workflow/ sale, management, negotiation, purchase
scripts/    seed_seller_memory
docs/       demo_runbook.md
```

## Run it
```bash
uv venv && source .venv/bin/activate
uv pip install -r requirements.txt
cp .env.example .env          # fill in keys

# In the Browserbase console: create a Context, open it, log into eBay by hand
# once, then paste its id into BROWSERBASE_CONTEXT_ID in .env. (No login code —
# the session cookies persist in that one context across every run.)

# prerequisites: Redis Stack, Redis Agent Memory Server, AgentSpan local server
python -m scripts.seed_seller_memory        # seed demo seller prefs

uvicorn app.main:app --port 8000
npx poke@latest tunnel http://localhost:8000/mcp -n "trAId"
```

## Build-time VERIFY notes
Grounded against live docs on 2026-06-20; pin exact signatures on Day 1:
- AgentSpan: `Agent`/`AgentRuntime`/`AgentHandle` kwargs, multimodal vision message
  shape in `agents/llm.py`, `runtime.start`/`handle.approve` return types.
- MCP SDK: `FastMCP.streamable_http_app()` mount path + `session_manager.run()`.
- RedisVL: `SearchIndex`, `VectorQuery`, `SemanticCache` import paths.
- Redis Agent Memory client: `search_long_term_memory` / `create_long_term_memory`
  signatures (wrapped defensively in `memory/seller_memory.py`).
- Stagehand Python: `StagehandConfig` fields + `page.act/extract` schema arg.
Each is isolated behind a thin adapter so a signature fix touches one file.


## Detected evidence (automated analysis)

Indexed codebase: 63 recognized source files, 293 KB.
- Express (technology) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- TypeScript (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (80 of 80)

```
.agents/skills/aidesigner-frontend/references/api.md
.agents/skills/aidesigner-frontend/references/frontend-rubric.md
.agents/skills/aidesigner-frontend/SKILL.md
.aidesigner/.gitkeep
.claude/agents/aidesigner-frontend.md
.claude/commands/aidesigner.md
.claude/launch.json
.claude/skills/aidesigner-frontend/references/api.md
.claude/skills/aidesigner-frontend/references/frontend-rubric.md
.claude/skills/aidesigner-frontend/SKILL.md
.env.example
.gitignore
.mcp.json
app/__init__.py
app/agents/__init__.py
app/agents/copywriter.py
app/agents/identify.py
app/agents/llm.py
app/agents/negotiator.py
app/agents/pricing.py
app/agents/revision_parser.py
app/browser/__init__.py
app/browser/ebay_offers.py
app/browser/ebay_publish.py
app/browser/ebay_research.py
app/browser/publish.py
app/browser/research.py
app/browser/session.py
app/browser/threads.py
app/config.py
app/images.py
app/main.py
app/mcp_server.py
app/memory/__init__.py
app/memory/comparables_index.py
app/memory/seller_memory.py
app/models.py
app/poke_client.py
app/state/__init__.py
app/state/negotiation_state.py
app/state/redis_state.py
app/workflow/__init__.py
app/workflow/management_workflow.py
app/workflow/negotiation_workflow.py
app/workflow/purchase_workflow.py
app/workflow/runtime.py
app/workflow/sale_workflow.py
dashboard/.aidesigner/design-artifact.html
dashboard/.gitignore
dashboard/README.md
dashboard/server/browserbaseSource.mjs
dashboard/server/env.mjs
dashboard/server/index.mjs
dashboard/server/package.json
dashboard/server/redisSource.mjs
dashboard/server/store.mjs
dashboard/web/index.html
dashboard/web/package.json
dashboard/web/src/api/client.ts
dashboard/web/src/App.tsx
dashboard/web/src/components/Header.tsx
dashboard/web/src/components/KeyspaceExplorer.tsx
dashboard/web/src/components/LiveFeedPanel.tsx
dashboard/web/src/components/RedisStory.tsx
dashboard/web/src/lib/format.ts
dashboard/web/src/main.tsx
dashboard/web/src/state/useDashboard.ts
dashboard/web/src/types.ts
dashboard/web/tsconfig.json
dashboard/web/tsconfig.node.json
dashboard/web/vite.config.ts
docs/demo_runbook.md
pyproject.toml
README.md
requirements.txt
scripts/__init__.py
scripts/login.py
scripts/seed_seller_memory.py
scripts/setup_browser_context.py
start.sh
```

### Dependencies

- dashboard/server/package.json: dotenv@^16.4.5, express@^4.19.2, redis@^4.7.0
- dashboard/web/package.json: @types/react@^18.3.12, @types/react-dom@^18.3.1, @vitejs/plugin-react@^4.3.4, react@^18.3.1, react-dom@^18.3.1, typescript@^5.6.3, vite@^5.4.11
- pyproject.toml: agent-memory-client@>=0.1, agentspan@>=0.1, browserbase@>=1.0, fastapi@>=0.115, fastmcp@>=2.12, httpx@>=0.27, mcp@>=1.2, pydantic@>=2.7, python-dotenv@>=1.0, redis@>=5.0, redis-agent-memory@>=0.0.4, redisvl@>=0.3, stagehand@>=0.4, uvicorn[standard]@>=0.30
- requirements.txt: agent-memory-client@>=0.1, agentspan@>=0.1, browserbase@>=1.0, fastapi@>=0.115, fastmcp@>=2.12, httpx@>=0.27, mcp@>=1.2, playwright@>=1.40, pydantic@>=2.7, python-dotenv@>=1.0, redis@>=5.0, redisvl@>=0.3, stagehand@>=0.4, uvicorn[standard]@>=0.30

### Recent commits (newest first)

- Revise README with project title and description
- fixed!
- traid->traide
- almost done w frontend
- Add trAId operations dashboard (live Browserbase feed + Redis state)
- add AIDesigner mcp
- working list function for ebay
- almost working item listing
- almost works on upload
- Simplified and added Ebay
- browserbase KIND OF works for auth
- Got to browserbase
- auto setup in start.sh
- Fixed Poke MCP connection
- Debugged Poke access
- FIX PLS
- stuff and things
- add features n shi
- MVP
- Initial commit

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

### docs/demo_runbook.md

```markdown
# trAId — Demo Runbook (§11)

Pin this during the live demo. The demo *is* the product: proof of work lives in
the Poke thread, the AgentSpan dashboard (`localhost:6767`), Browserbase Live
View, and Redis.

## Hours before (NOT minutes before)
1. `cp .env.example .env` and fill every key.
2. Start Redis Stack (or point `REDIS_URL` at Redis Cloud) and the Redis Agent
   Memory Server (`AGENT_MEMORY_URL`).
3. Start the AgentSpan local server + dashboard (`localhost:6767`).
4. In the Browserbase console: create one Context, open it, log into eBay by
   hand, then paste its id into `BROWSERBASE_CONTEXT_ID` in `.env`. The cookies
   persist in that context — the app never logs in itself.
5. `python -m scripts.seed_seller_memory` → seed 6 long-term seller memories.
6. `uvicorn app.main:app --port 8000`
7. `npx poke@latest tunnel http://localhost:8000/mcp -n "trAId"`
   (fallback: `ngrok http 8000` and register the `/mcp` URL at poke.com/integrations/new)
8. For the demo window only: set `NEGOTIATION_POLL_INTERVAL_SECONDS=20` and
   `STALE_CHECK_THRESHOLD_SECONDS=60`. Narrate the speed-up out loud.
9. Full dry run TWICE, including the revision and the planted-offer beat.
10. Cue the pre-recorded backup video to the negotiation beat.

## The script
| Time | Action | On screen |
|---|---|---|
| 0:00–0:25 | Narrate the problem | — |
| 0:25–0:45 | Text Poke the item + photos | Poke thread |
| 0:45–1:15 | `start_sale` fires → switch to AgentSpan dashboard | identify → memory → eBay research |
| 0:45–1:15 | Open the Browserbase Live View tab briefly | eBay search running |
| 1:15–1:30 | Poke texts the comps it found (title + price) | "🔎 Found N eBay comps…" |
| 1:30–1:50 | Poke sends the compact proposal | ID, price range, rec, memory callout |
| 1:50–2:00 | Text "raise it by $5" | draft updates; Poke re-sends the proposal |
| 2:00 | Text "Approved" | dashboard resumes past the paused node |
| 2:00–3:10 | Publish; show the Browserbase session | the eBay form filling |
| 3:10–3:35 | Poke: "Your listing is live" + URL | open the real listing |
| 3:35–4:00 | Text "Drop it by $5" | price updates; Poke confirms |
| 4:00–4:30 | Counterpart sends a real offer on the eBay listing | Poke: "Got an offer for $65 — accepted automatically. Sold!" (zero approval) |
| 4:30–4:50 (opt) | Buy-side: "Find me a used X under $50" + planted seller counters above cap | Poke: "Seller won't go below $60, cap is $50 — raise it?" → "yes" → confirmed |

If on a strict clock, keep the 4:00–4:30 sell-side offer beat; cut buy-side first.

## If something breaks
- Browser shows a login wall → the context cookies expired; re-open the Context
  in the Browserbase console and log into eBay again, then re-run.
- Publish image upload fails → trAId returns a Live View link; take over, upload,
  hand back.
- Live negotiation mistimed → cut to the backup video; research→report→publish→
  price-drop stands on its own.

```

### .claude/commands/aidesigner.md

```markdown
<!-- AUTO-GENERATED from packages/aidesigner-agent-skills/templates/claude-command.md — do not edit directly.
     Run `npx -y @aidesigner/agent-skills upgrade` to regenerate. -->

Use AIDesigner for the request in $ARGUMENTS.

## Scope Defaults

- Spend AIDesigner credits only when the user explicitly asked to use AIDesigner or clearly opted into that workflow.
- Prefer the connected `aidesigner` MCP server for `whoami`, `get_credit_status`, `generate_design`, and `refine_design`.
- Treat HTML as a design artifact first and implementation input second.

## Pre-Flight

1. Gather design context before spending credits:
   - Read `DESIGN.md`, `.aidesigner/DESIGN.md`, or `docs/design.md` if present.
   - Inspect theme files, tokens, fonts, shared components, and the target route or page.
2. Write a compact internal design brief from that repo context:
   - platform and surface
   - product goal and main user action
   - visual language to preserve, evolve, or reset
   - important repo patterns, constraints, and content types
   - concrete typography or tokens only if preserving the current repo aesthetic
3. Split the task into:
   - a visual reference prompt for AIDesigner
   - a detailed implementation spec you keep local
4. If this will use `mode: "clone"`, check for screenshot-capable browser tooling before spending credits.
   - If Puppeteer or equivalent browser automation is missing, install `puppeteer` in the repo with the repo package manager first.
   - Do not start clone generation when visual QA tooling is known to be unavailable.

## Workflow

### 1. Build The Visual Prompt

Rewrite `$ARGUMENTS` into a broad visual reference prompt before calling AIDesigner.

- Focus on product type, audience, UX priorities, overall feel, and non-negotiable constraints.
- Let AIDesigner choose the specific layout, supporting sections, composition, and most stylistic details.
- Avoid exact section orders, card counts, micro-copy, and detailed per-element placement unless the user explicitly requested them.
- Do not pass exhaustive content outlines, tables, command lists, API fields, or other documentation detail dumps into the prompt.
- If the user gave a very specific page spec, compress it into a smaller visual brief for AIDesigner and save the full spec for local implementation.
- If matching an existing aesthetic, it is fine to mention concrete repo colors, fonts, and tokens.
- If this is a new design or revamp, keep styling guidance broad and do not hard-code exact colors or gradients unless the user explicitly asked for them.

### 2. Generate Or Refine

- Keep MCP calls prompt-driven unless the user explicitly asked for a reference-URL workflow.
- Use `mode: "clone"` only for a near-1:1 recreation or faithful copy of a specific URL.
- Use `mode: "enhance"` only to improve or modernize a specific URL while preserving its content or intent.
- Use `mode: "inspire"` only for a new design inspired by a specific URL or its visual style.
- Before `mode: "c
[truncated — 5113 more characters]
```

### pyproject.toml

```
[project]
name = "traid"
version = "0.1.0"
description = "trAId — text-to-sell resale agent (CalHacks 2026, 24h build)"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115",
    "uvicorn[standard]>=0.30",
    "pydantic>=2.7",
    "python-dotenv>=1.0",
    "httpx>=0.27",
    "agentspan>=0.1",
    "fastmcp>=2.12",
    "mcp>=1.2",
    "stagehand>=0.4",
    "browserbase>=1.0",
    "redis>=5.0",
    "redisvl>=0.3",
    "agent-memory-client>=0.1",
    "redis-agent-memory>=0.0.4",
]

[tool.setuptools.packages.find]
include = ["app*"]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

```

### requirements.txt

```
# --- Core service ---
fastapi>=0.115
uvicorn[standard]>=0.30
pydantic>=2.7
python-dotenv>=1.0
httpx>=0.27

# --- Sponsor SDKs ---
agentspan>=0.1            # durable agent runtime + native multi-provider LLM (Anthropic Claude)
fastmcp>=2.12             # MCP server framework (Poke connects to /mcp); run stateless per Poke's examples
mcp>=1.2                  # MCP SDK (transitive via fastmcp; pinned for clarity)
stagehand>=0.4            # Browserbase + Stagehand browser automation
browserbase>=1.0          # Browserbase session/context management + Live View URLs
playwright>=1.40          # CDP file-chooser handling for eBay photo upload (no local browser needed; connects over CDP)

# --- Redis trio ---
redis>=5.0               # plain key/value state + publish lock (Redis Stack / Redis Cloud)
redisvl>=0.3             # vector + metadata hybrid search over comparables, semantic cache
agent-memory-client>=0.1 # Redis Agent Memory Server client (seller short/long-term memory)

```

### dashboard/server/package.json

```
{
  "name": "traid-dashboard-server",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "description": "Standalone read-only data layer for the trAId dashboard. Reads live state from Redis + Browserbase. Does NOT touch the Python MCP backend.",
  "scripts": {
    "start": "node index.mjs",
    "dev": "node --watch index.mjs"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "redis": "^4.7.0"
  }
}

```

### dashboard/web/package.json

```
{
  "name": "traid-dashboard-web",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.3",
    "vite": "^5.4.11"
  }
}

```

### app/main.py

```python
"""ASGI entrypoint: the stateless streamable-HTTP MCP app Poke connects to.

    ./start.sh          # starts everything
    # or manually:
    uvicorn app.main:app --port 8000
    npx poke@latest tunnel http://localhost:8000/mcp -n "trAId"

The MCP endpoint is served at /mcp and a plain GET /health is added for the
launcher's readiness check. The negotiation monitor runs via the server lifespan
(see app/mcp_server.py). Stateless HTTP is required for Poke's distributed
gateway — see the module docstring there.
"""
from __future__ import annotations

import logging
import mimetypes

from starlette.responses import FileResponse, Response
from starlette.types import ASGIApp, Receive, Scope, Send

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(name)s %(levelname)s %(message)s",
    datefmt="%H:%M:%S",
)

from app.images import IMAGE_DIR, verify_token
from app.mcp_server import mcp

IMAGE_DIR.mkdir(parents=True, exist_ok=True)


class _RewriteToMCP:
    """Route requests to the MCP app, with /images served separately.

    Poke does the handshake on /mcp, then routes later requests through a
    connection-specific path like /5bf67ab6-.../mcp. The MCP app only has a
    route at /mcp, so without this they 404. /health and /.well-known/* are
    MCP custom routes and pass through. /images/* is handled inline with
    HMAC-signed token verification. Non-http scopes (notably `lifespan`)
    are forwarded unchanged so the MCP session manager + monitor still start.
    """

    def __init__(self, app: ASGIApp) -> None:
        self._app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "http":
            path: str = scope["path"]

            if path.startswith("/images/"):
                await self._serve_image(path, scope, receive, send)
                return

            if not path.startswith(("/health", "/mcp", "/.")):
                scope = dict(scope, path="/mcp")

        await self._app(scope, receive, send)

    async def _serve_image(self, path: str, scope: Scope, receive: Receive, send: Send) -> None:
        relative = path[len("/images/"):]
        qs = scope.get("query_string", b"").decode()
        params = dict(p.split("=", 1) for p in qs.split("&") if "=" in p)
        token = params.get("token", "")

        if not relative or not verify_token(relative, token):
            resp = Response("Forbidden", status_code=403)
            await resp(scope, receive, send)
            return

        file_path = IMAGE_DIR / relative
        if not file_path.is_file() or not file_path.resolve().is_relative_to(IMAGE_DIR.resolve()):
            resp = Response("Not found", status_code=404)
            await resp(scope, receive, send)
            return

        resp = FileResponse(str(file_path))
        await resp(scope, receive, send)


app = _RewriteToMCP(mcp.http_app(stateless_http=True))

```

### dashboard/server/index.mjs

```
/**
 * trAId dashboard data layer — a standalone, read-only HTTP service.
 *
 * It reads live state from Redis + Browserbase (using credentials from the
 * repo-root .env) and exposes it to the React frontend over REST + SSE. It does
 * NOT import, run, or modify the Python MCP backend in app/.
 *
 *   GET /api/health         -> liveness + source connection status
 *   GET /api/state          -> full dashboard snapshot (one-shot)
 *   GET /api/live-session   -> latest Browserbase session + live-view URL (one-shot)
 *   GET /api/events         -> Server-Sent Events stream: `state` and `live` events
 */
import path from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import express from "express";
import { config } from "./env.mjs";
import { addClient, getSnapshot, getLive, startPolling, clientCount } from "./store.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();

app.get("/api/health", (_req, res) => {
  const snap = getSnapshot();
  res.json({
    ok: true,
    service: "traid-dashboard",
    sseClients: clientCount(),
    redisConnected: snap?.redisConnected ?? false,
    totalKeys: snap?.totals?.keys ?? null,
    browserbase: getLive()?.ok ?? false,
  });
});

app.get("/api/state", (_req, res) => {
  const snap = getSnapshot();
  if (!snap) return res.status(503).json({ error: "snapshot not ready yet" });
  res.json(snap);
});

app.get("/api/live-session", (_req, res) => {
  const live = getLive();
  if (!live) return res.status(503).json({ error: "live session not polled yet" });
  res.json(live);
});

app.get("/api/events", (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache, no-transform",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  });
  res.flushHeaders?.();
  res.write("retry: 3000\n\n");
  const remove = addClient(res);
  req.on("close", () => { remove(); res.end(); });
});

// In production, optionally serve the built frontend from web/dist.
const distDir = path.resolve(__dirname, "..", "web", "dist");
if (fs.existsSync(distDir)) {
  app.use(express.static(distDir));
  app.get("*", (_req, res) => res.sendFile(path.join(distDir, "index.html")));
}

startPolling();
app.listen(config.port, () => {
  console.log(`[traid-dashboard] data layer listening on http://localhost:${config.port}`);
  console.log(`[traid-dashboard]   REST:  /api/state  /api/live-session  /api/health`);
  console.log(`[traid-dashboard]   SSE:   /api/events`);
});

```

### dashboard/web/src/main.tsx

```typescript
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### dashboard/web/src/App.tsx

```typescript
import { useDashboard } from "./state/useDashboard";
import { Header } from "./components/Header";
import { LiveFeedPanel } from "./components/LiveFeedPanel";
import { RedisStory } from "./components/RedisStory";
import { KeyspaceExplorer } from "./components/KeyspaceExplorer";

// A standalone, read-only console. Top: the agent's live Browserbase viewport.
// Below: how trAId uses Redis (system of record, vector memory, agent memory)
// and a full explorer over the live keyspace. All data flows from the single
// useDashboard state layer — it talks ONLY to Redis + Browserbase, never to the
// Python MCP backend.
export default function App() {
  const { snapshot, live, status } = useDashboard();

  return (
    <div className="flex flex-col w-full h-screen overflow-hidden bg-zinc-50">
      <Header snapshot={snapshot} status={status} />

      <div className="flex-1 overflow-y-auto">
        <div className="p-5 lg:p-8 flex flex-col gap-8 max-w-6xl mx-auto w-full">
          {/* 1 — Live agent viewport (Browserbase) */}
          <LiveFeedPanel live={live} />

          {/* 2 — How we use Redis */}
          <section className="flex flex-col gap-4">
            <div className="flex items-center gap-2">
              <i className="ph-fill ph-database text-zinc-700 text-lg" />
              <h3 className="text-lg font-semibold tracking-tight text-zinc-900">Redis, beyond caching</h3>
            </div>
            <RedisStory snapshot={snapshot} />
          </section>

          {/* 3 — Full keyspace explorer */}
          <KeyspaceExplorer snapshot={snapshot} />

          <footer className="text-center text-[11px] text-zinc-400 font-mono pb-2">
            trAIde Redis console · reads Redis + Browserbase directly · no backend coupling
          </footer>
        </div>
      </div>
    </div>
  );
}

```

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