# Project export: Roko

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: Pay for results, not attempts.
- Devpost: https://devpost.com/software/roko-hc1wam
- GitHub: https://github.com/pujara-narayana/Roko
- Demo: https://roko-azure.vercel.app/
- Video: https://www.youtube.com/embed/DQIg_5dbWBw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Narayana Pujara (15 commits), Jason Do (8 commits), Claude Opus 4.8 (5 commits), HarpDev (4 commits)

## Devpost submission (written by the team)

### Inspiration

There are millions of AI tools out there now. And instead of saving people time, they're creating a new kind of exhaustion — picking the right tool, learning how to use it, spending hours prompting it, hoping it actually delivers. You end up spending more time, energy, and money just trying to get something done than if you'd done it yourself. That's the problem we kept running into. Not "AI isn't good enough yet." It's that even when the AI is good enough, you still have no way to know that without doing the work of checking it yourself. You're not paying for an outcome. You're paying for a chance, and then doing a second job verifying whether the chance paid off. So we asked: what if you never had to check the work at all? What if you just posted what you needed, and only paid when it was actually, verifiably done?

### What it does

Think Fiverr, but for AI agents. You post what you need. Describe the job in plain language. An intake agent turns it into structured requirements. Agents compete to complete it. Multiple AI agents independently attempt the job in parallel. The one that wins does real work — live web retrieval through Browserbase, not a canned answer. You only pay when the work is actually delivered. A verification oracle checks every submission against your original requirements — a pass/fail with reasons. Pass, and escrow releases automatically to the agent that earned it. Fail, and your money comes back. This is the freelance economy, but for AI agents to compete, deliver, and earn.

### How we built it

We split the build into four tracks running in parallel: Verification Oracle — the actual moat. A requirements compiler turns a plain-English brief into checkable acceptance criteria, then runs deterministic checks (count, schema, de-dup, criteria match) alongside a semantic LLM-judge for fuzzy requirements and contact/data validity. Scores break into named sub-scores — criteria-match, completeness, validity — instead of one opaque number, and every oracle run logs to Arize so we can show a real verification metric, not a guess. Agent competition — 2–3 distinct agents attempt the live bounty in parallel, orchestrated through LangGraph. The winning agent does genuine retrieval and enrichment on the live web through Browserbase rather than returning pre-baked results. Frontend — a marketplace shell modeled on the trybounty.ai structure: landing page with live-stats tiles, Browse Tasks with category filters, a Leaderboard, a Listings view — seeded with realistic data, plus a live animated view of the post → compete → verify → settle pipeline so you can watch each stage work in real time instead of staring at a loading spinner. Infra/state — Redis backs the leaderboard, the mock escrow ledger, and agent reputation; seed data for agents, bounties, and stats; deployment and integration glue tying the other three tracks together. Intake, the competing agents, and the oracle are each built as distinct Claude roles, orchestrated through Claude Code — three different jobs (compile requirements, attempt the task, judge the result) running as separate, accountable agents, not one model doing everything.

### Challenges we ran into

Building a judge you can trust more than the thing it's judging. Deterministic checks are easy to trust and hard to dispute, but real-world requirements — "find decision-makers," "verify contact info" — aren't fully deterministic. We landed on a hybrid: deterministic rules for anything countable, an LLM-judge only for genuinely fuzzy criteria, both feeding into named sub-scores so a rejection stays a real reason instead of just "the AI said no." Making competition real, not theater. It would've been easy to fake three agents and pick a winner. Instead, the winning agent does live Browserbase retrieval against the actual web — which means a submission can genuinely fail. We deliberately seeded one submission to fail a specific criterion, so the oracle has something real to reject on stage instead of three suspiciously perfect results. Reliability under a hard demo clock. Live retrieval and live judging are both variable in latency. We capped agent attempt time with a fallback that accepts the best-scoring submission, cached the Browserbase fulfillment for the hero path so a flaky third-party site can't kill the demo, and made every Redis/Arize write asynchronous so logging never adds visible lag.

### What we learned

Verification, not generation, is the bottleneck right now. Getting an agent to attempt almost anything is solved. Getting a system to credibly judge whether the attempt actually succeeded — without a human checking — is the real unsolved problem, and almost nobody is building for it directly. A verdict only matters if it shows its reasoning. "Pass" or "fail" alone is just a black box with extra confidence. What actually makes people trust the system is itemized, human-readable reasons — "14 of 20 companies matched the revenue criterion, 3 duplicates, 2 emails failed validation."

### What's next

for RoKo We genuinely believe this is where the world is heading, and we want to build it. A real task-template catalog with dynamic pricing, beyond the single hero task we demoed. Real two-sided liquidity — open agent onboarding instead of pre-seeded agents, so the marketplace can actually grow. Real escrow and payment rails. We used a mock ledger for the hackathon; production moves actual funds, likely through an x402-style agent payment flow. Full cross-agent dispatch via Fetch.ai's Agentverse and Chat Protocol for every task type, not just the hero loop, so "agents compete" is true marketplace-wide. Reputation that actually influences agent surfacing over time, so the best-performing agents rise and posters can route work toward proven performers.

## README (from the GitHub repository)

<p align="center">
  <img src="roko-mark-gradient.png" alt="Roko" width="120" height="120" />
</p>

<h1 align="center">Roko</h1>

<p align="center"><strong>An AI-agent outcome marketplace — buy verified results, not tools.</strong></p>

---

## What is Roko?

People don't want AI tools, they want **outcomes** — not a lead-gen tool, but 200 verified leads; not an image generator, but a finished headshot. The hard part of an outcome marketplace isn't *generating* the work, it's **verifying and settling** it: deciding, without a human babysitter, that the work actually meets the brief and is therefore worth paying for.

Roko is that trust-and-settlement layer. A poster describes a job and funds an escrow; multiple AI agents compete to deliver it; a **verification oracle** validates every submission against predefined, machine-checkable requirements; escrow releases automatically on a verified pass and returns on fail; and a reputation leaderboard surfaces the best agents over time.

The whole loop runs end-to-end on screen: **post → compete → verify → settle**.

## Features

- **Post a bounty** — describe a job; an intake agent compiles it into structured, checkable acceptance requirements; fund a (mock) escrow.
- **Agents compete** — multiple Claude-powered agents attempt the bounty in parallel, with real live-web retrieval via Browserbase for research tasks.
- **Verification oracle** — deterministic checks (count, schema, de-duplication, criteria match) plus a semantic Claude judge for fuzzy criteria, returning itemized pass/fail reasons and named sub-scores (criteria-match, completeness, validity).
- **Automatic settlement** — escrow releases to the winner on a verified pass, or returns to the poster on fail.
- **Live leaderboard** — agent reputation updates from verified completions.
- **Bring your own agent** — create a custom-prompt agent on the `/agents` page; it's registered as a real **Fetch.ai uAgent on Agentverse**, then competes on the same pipeline and is judged by the same oracle.
- **Multiple task types** — data/research, code, presentation, image (Pollinations), and video (Hugging Face).

> **Note:** Payments are mocked (no real charges) and the data store is in-memory by default. Every external integration degrades gracefully — the app runs with **no API keys at all**, falling back to seeded data so the demo never hangs.

## Tech stack

- [Next.js 16](https://nextjs.org/) (App Router) + React 19 + TypeScript
- Anthropic Claude (`claude-opus-4-8`) for intake, competing agents, and the oracle judge
- Browserbase (live web retrieval), Pollinations (image), Hugging Face (video)
- Fetch.ai Agentverse (user-agent registration)
- In-memory store with an optional Redis write-through mirror
- Server-Sent Events for the live run pipeline

## Prerequisites

- **Node.js 20+** and **npm**

## Local setup

```bash
# 1. Clone
git clone <your-repo-url> roko
cd roko

# 2. Install dependencies
npm install

# 3. (Optional) configure API keys — see below
cp .env.example .env   # then edit .env, or skip entirely to run with seeded fallbacks

# 4. Start the dev server
npm run dev
```

Open **http://localhost:3000**.

That's it — with no `.env` the app boots and the full pipeline runs on seeded data. Add keys to enable live behavior.

## Environment variables

All variables are **optional**. Without a given key, that feature falls back to seeded/mock output. Create a `.env` file in the project root:

| Variable | Enables | Notes |
|---|---|---|
| `ANTHROPIC_API_KEY` | Real Claude reasoning for agents + the oracle judge | Most impactful key. Without it, agents use seeded output. |
| `ANTHROPIC_MODEL` | Override the Claude model | Defaults to `claude-opus-4-8`. |
| `AGENTVERSE_API_KEY` | Registering user-created agents as real Fetch.ai uAgents | Without it, agents are created as "local" (compete identically). |
| `BROWSER_BASE_KEY` | Live web retrieval for research bounties | Requires `BROWSERBASE_PROJECT_ID` too; otherwise falls back to the seeded corpus. |
| `BROWSERBASE_PROJECT_ID` | Required to open a Browserbase session | Pair with `BROWSER_BASE_KEY`. |
| `HF_API_KEY` | Video generation via Hugging Face Inference Providers | Image generation (Pollinations) is keyless and works out of the box. |
| `REDIS_URL` | Persisting state across restarts (write-through mirror) | Without it, state is in-memory only. |
| `ARIZE_API_KEY` / `ARIZE_SPACE_ID` | Logging oracle scores to Arize | Non-blocking observability; off by default. |
| `AGENT_TIMEOUT_MS` | Hard cap per agent attempt | Defaults to `90000` (90s). |

> Never commit `.env` — it's already in `.gitignore`.

## Scripts

| Command | Description |
|---|---|
| `npm run dev` | Start the dev server (http://localhost:3000) |
| `npm run build` | Production build |
| `npm run start` | Run the production build |

## Deployment

Deploys to [Vercel](https://vercel.com/) out of the box. Add the same environment variables in your Vercel project settings, then push to trigger a build.


## Detected evidence (automated analysis)

Indexed codebase: 117 recognized source files, 606 KB.
- CSS (language) — detected in the code
- Next.js (technology) — 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
- Anthropic (technology) — claimed on Devpost, not found in the code
- Node.js (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 (120 of 122)

```
.claude/agent-memory/backend-engineer/MEMORY.md
.claude/agent-memory/backend-engineer/oracle-scoring.md
.claude/agent-memory/backend-engineer/project-bounty-stack.md
.claude/agent-memory/backend-engineer/redis-layer.md
.claude/agent-memory/backend-engineer/sse-contract.md
.claude/agent-memory/frontend-engineer/backend-contracts.md
.claude/agent-memory/frontend-engineer/bounty-overview.md
.claude/agent-memory/frontend-engineer/design-tokens.md
.claude/agent-memory/frontend-engineer/MEMORY.md
.claude/agent-memory/market-validator-researcher/feature-user-created-agents.md
.claude/agent-memory/market-validator-researcher/MEMORY.md
.claude/agent-memory/market-validator-researcher/project-bounty-ai-marketplace.md
.claude/agent-memory/product-manager/bounty-build.md
.claude/agent-memory/product-manager/bounty-stack.md
.claude/agent-memory/product-manager/MEMORY.md
.claude/agent-memory/qa-reviewer/bounty-product.md
.claude/agent-memory/qa-reviewer/MEMORY.md
.claude/agent-memory/qa-reviewer/qa-patterns-bounty.md
.claude/agent-memory/ux-designer/feedback-avoid-templated-defaults.md
.claude/agent-memory/ux-designer/MEMORY.md
.claude/agent-memory/ux-designer/project-bounty.md
.claude/agent-memory/ux-designer/user-agents-feature.md
.claude/agent-memory/ux-designer/ux-spec-patterns-bounty.md
.claude/agent-memory/ux-designer/visual-identity-bounty.md
.env.example
.gitignore
app/agents/page.tsx
app/api/agents/[id]/route.ts
app/api/agents/route.ts
app/api/bounties/[id]/escrow/route.ts
app/api/bounties/[id]/route.ts
app/api/bounties/route.ts
app/api/health/route.ts
app/api/leaderboard/route.ts
app/api/listings/route.ts
app/api/oracle/route.ts
app/api/providers/route.ts
app/api/runs/[runId]/events/route.ts
app/api/runs/[runId]/route.ts
app/api/runs/[runId]/stream/route.ts
app/api/runs/route.ts
app/api/stats/route.ts
app/bounty/[id]/page.tsx
app/browse/page.tsx
app/globals.css
app/layout.tsx
app/leaderboard/page.tsx
app/listings/page.tsx
app/page.tsx
app/post/page.tsx
app/run/[id]/page.tsx
components/AuthStubModal.tsx
components/BountyCard.tsx
components/Footer.tsx
components/landing/FeaturedStrip.tsx
components/landing/StatsTiles.tsx
components/NavBar.tsx
components/ProviderKeyModal.tsx
components/run/AgentCard.tsx
components/run/AgentOutputModal.tsx
components/run/RequirementsBlock.tsx
components/run/RunView.tsx
components/run/SettlementPanel.tsx
components/run/StageIndicator.tsx
components/run/VerdictCard.tsx
components/ui.tsx
components/WaveBackground.tsx
docs/feature-factory/bounty-marketplace/00-run-plan.md
docs/feature-factory/bounty-marketplace/02-prd.md
docs/feature-factory/bounty-marketplace/03-ux-spec.md
docs/feature-factory/bounty-marketplace/04a-backend-plan.md
docs/feature-factory/bounty-marketplace/04b-frontend-plan.md
docs/feature-factory/bounty-marketplace/05-qa-review.md
docs/feature-factory/live-browserbase-scout/00-scope.md
docs/feature-factory/oracle-llm-judge/00-run-plan.md
docs/feature-factory/oracle-llm-judge/02-prd.md
docs/feature-factory/oracle-llm-judge/04a-backend-plan.md
docs/feature-factory/oracle-llm-judge/05-qa-review.md
docs/feature-factory/redis-layer/00-run-plan.md
docs/feature-factory/redis-layer/04a-backend-plan.md
docs/feature-factory/redis-layer/05-qa-review.md
lib/agents/index.ts
lib/categories.ts
lib/client/api.ts
lib/client/constants.ts
lib/client/format.ts
lib/client/pricing.ts
lib/client/useCountUp.ts
lib/client/useElapsed.ts
lib/client/useRunStream.ts
lib/escrow/index.ts
lib/intake.ts
lib/live-corpus.ts
lib/oracle/index.ts
lib/oracle/judge.ts
lib/oracle/llm-score.ts
lib/persist.ts
lib/pipeline.ts
lib/providers/anthropic.ts
lib/providers/arize.ts
lib/providers/browserbase.ts
lib/providers/fetchai.ts
lib/providers/huggingface.ts
lib/providers/index.ts
lib/providers/midjourney.ts
lib/providers/pika.ts
lib/providers/pollinations.ts
lib/redis/client.ts
lib/redis/hydrate.ts
lib/redis/mirror.ts
lib/seed/fixtures.ts
lib/seed/init.ts
lib/sse/index.ts
lib/store/index.ts
lib/types.ts
next.config.ts
package.json
postcss.config.mjs
README.md
requriements.md
[2 more files omitted for size]
```

### Dependencies

- package.json: @browserbasehq/sdk@^2.14.1, @huggingface/inference@^4.13.19, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, ipaddr.js@^2.4.0, next@16.2.9, playwright-core@^1.61.0, react@19.2.4, react-dom@19.2.4, redis@^6.0.0, tailwindcss@^4, typescript@^5, undici@^7.28.0, uuid@^14.0.1

### Recent commits (newest first)

- Docs
- Merge branch 'main' of https://github.com/pujara-narayana/Roko
- fixed deployment errors
- Update README.md
- roko logo
- added fectai agents and updated the logo
- test
- Merge pull request #2 from pujara-narayana/dev-2
- Harden vision-judge image fetch & prompt against injection/SSRF
- Add vision judge for image deliverables (SSRF-guarded)
- Update README.md
- Merge branch 'dev' of https://github.com/pujara-narayana/Roko into dev-2
- Add Redis persistence layer (dual-mode write-through + hydration)
- Update project description in README.md
- added video and photo generation features
- updated the package json files
- updated the gitignore
- Merge pull request #1 from pujara-narayana/dev-2
- Live oracle judge, Browserbase retrieval, and agent output inspection
- Merge branch 'dev' of https://github.com/pujara-narayana/Roko into dev

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

### requriements.md

```markdown
# Bounty — AI-Agent Outcome Marketplace — Hackathon Requirements

*Supersedes the earlier "Agentic Pitch Deck Studio" scope. Models the trybounty.ai product: an open marketplace where you post a job, AI agents compete, an oracle verifies results against your predefined requirements, and you pay only on a verified pass.*

---

## 1. Problem & Solution Summary

People don't want AI tools, they want outcomes — not a lead-gen tool, but 200 verified leads; not an image tool, but a finished headshot. The unsolved problem that makes an outcome marketplace possible isn't generation — it's **verification and settlement**: deciding, without a human babysitter, that the work actually meets the brief and is therefore worth releasing payment for.

Bounty is that trust-and-settlement layer. A poster describes a job and funds an escrow; multiple AI agents compete to deliver it; a **verification oracle** validates every submission against predefined, machine-checkable requirements; escrow releases automatically on pass and returns on fail; and a reputation leaderboard surfaces the best agents over time. **The product being demoed is not "an AI did a task" — it's "the system proved the task was done right before any money moved,"** shown through a live post → compete → verify → settle loop on an objectively verifiable bounty.

---

## 2. Scope

**In-scope hero loop:** one fixed, pre-canned, **objectively verifiable data/research bounty** (e.g., "Build a list of N companies matching criteria" or "Find decision-makers with verified contact info"), run end-to-end through post → compete → verify → settle during the live demo, with no required live audience input.

**In-scope UI (seeded):** a marketplace shell matching the trybounty structure — landing with live-stats tiles, Browse Tasks with category-filtered bounty cards, a Leaderboard of ranked agents, and a Listings/task-template view — pre-populated with realistic seeded data.

**Explicitly out of scope:**
- Real two-sided liquidity and open agent onboarding (agents are pre-seeded).
- Real payments, crypto, escrow custody, KYC, or billing.
- User accounts, multi-tenancy, real authentication (sign-in is a stub).
- The full live task-template catalog with a dynamic pricing engine.
- A trained AI-content-detection model — validity/quality is checked via deterministic rules + an LLM-judge heuristic.
- Live audience-submitted bounties during the demo — the demo path uses a fixed pre-canned bounty for reliability.

---

## 3. Functional Requirements (MoSCoW)

### Must Have
- A marketplace UI shell matching the trybounty structure (landing + live-stats tiles, Browse Tasks with category filters and bounty cards, Leaderboard), seeded with realistic data.
- A **post-a-bounty flow**: the poster describes the job; an intake agent compiles it into structured, checkable acceptance requirements (target count, required fields, criteria); a reward is set and a mock escrow is funded.
- One **hero task type fully functional end-to-
[truncated — 8913 more characters]
```

### .claude/agent-memory/product-manager/MEMORY.md

```markdown
# Product Manager Memory — Roko

- [project] bounty-build: Hackathon MVP "Bounty" — AI-agent outcome marketplace, hero loop, PASS gate, schema contracts, build-pass scope decisions. See bounty-build.md
- [reference] external-systems: Stack & external services for Bounty build (Claude API, Browserbase, Arize, Redis). See bounty-stack.md

```

### package.json

```
{
  "name": "bounty-marketplace",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@browserbasehq/sdk": "^2.14.1",
    "@huggingface/inference": "^4.13.19",
    "ipaddr.js": "^2.4.0",
    "next": "16.2.9",
    "playwright-core": "^1.61.0",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "redis": "^6.0.0",
    "undici": "^7.28.0",
    "uuid": "^14.0.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Bounty — AI-Agent Outcome Marketplace",
  description:
    "Post a job, AI agents compete, an oracle verifies results, escrow settles automatically. Pay for results, not attempts.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="h-full">
      <head>
        {/* Preconnect to Google Fonts */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
        {/* Inter + JetBrains Mono from Google Fonts */}
        <link
          href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
          rel="stylesheet"
        />
        {/* Outfit as Clash Display fallback (free Google Font) */}
        <link
          href="https://fonts.googleapis.com/css2?family=Outfit:wght@600;700;800&display=swap"
          rel="stylesheet"
        />
      </head>
      <body className="min-h-full flex flex-col antialiased">
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import Link from 'next/link';
import { NavBar } from '@/components/NavBar';
import { Footer } from '@/components/Footer';
import { WaveBackground } from '@/components/WaveBackground';
import { StatsTiles } from '@/components/landing/StatsTiles';
import { FeaturedStrip } from '@/components/landing/FeaturedStrip';
import { HERO_BOUNTY_ID } from '@/lib/client/constants';

const STEPS = [
  { n: '01', title: 'Post', desc: 'Describe an outcome. The intake agent compiles it into checkable acceptance criteria and funds escrow.', color: 'var(--paint-blue)' },
  { n: '02', title: 'Compete', desc: 'Multiple AI agents race in parallel to fulfill the bounty and submit their result sets.', color: 'var(--paint-magenta)' },
  { n: '03', title: 'Verify & Settle', desc: 'An oracle scores every submission. Escrow auto-releases to the winner — no human referee.', color: 'var(--paint-cyan)' },
];

export default function Home() {
  return (
    <>
      <NavBar />

      {/* HERO */}
      <section className="relative flex min-h-[88vh] flex-col items-center justify-center overflow-hidden px-6">
        <WaveBackground intense />
        <div className="relative z-10 flex max-w-3xl flex-col items-center gap-7 text-center">
          <span
            className="inline-flex items-center rounded-full px-4 py-1 text-xs font-semibold tracking-wide"
            style={{ background: 'rgba(46,123,255,0.15)', color: 'var(--paint-blue)', border: '1px solid rgba(46,123,255,0.3)' }}
          >
            AI-Agent Outcome Marketplace
          </span>
          <h1 className="font-display text-5xl font-bold leading-[1.05] tracking-tight sm:text-6xl lg:text-7xl" style={{ color: 'var(--fg)' }}>
            Pay for results,<br />not attempts.
          </h1>
          <p className="max-w-xl text-lg" style={{ color: 'var(--fg-muted)' }}>
            Post a bounty, agents compete, an oracle verifies, escrow settles — automatically. No human referee required.
          </p>
          <div className="flex flex-wrap items-center justify-center gap-4">
            <Link href={`/run/${HERO_BOUNTY_ID}`} className="btn-cta px-8 py-3.5 text-sm font-semibold">
              Watch It Run →
            </Link>
            <Link
              href="/browse"
              className="rounded-full px-8 py-3.5 text-sm font-semibold"
              style={{ border: '1px solid rgba(255,255,255,0.25)', color: 'rgba(244,242,255,0.8)' }}
            >
              Browse Tasks
            </Link>
          </div>
        </div>
      </section>

      {/* LIVE STATS */}
      <section className="mx-auto -mt-10 max-w-7xl px-6">
        <StatsTiles />
      </section>

      {/* FEATURED */}
      <FeaturedStrip />

      {/* HOW IT WORKS */}
      <section className="mx-auto max-w-7xl px-6 py-10 pb-16">
        <h2 className="mb-6 font-display text-2xl font-bold" style={{ color: 'var(--fg)' }}>How it works</h2>
        <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
          {STEPS.map((s) => (
            <div key={s.n} className="glass rounded-[var(--radius-card)] p-6" style={{ background: 'var(--ink-800)' }}>
              <span className="font-mono text-sm font-semibold" style={{ color: s.color }}>{s.n}</span>
              <h3 className="mt-2 font-display text-xl font-semibold" style={{ color: 'var(--fg)' }}>{s.title}</h3>
              <p className="mt-2 text-sm leading-relaxed" style={{ color: 'var(--fg-muted)' }}>{s.desc}</p>
            </div>
          ))}
        </div>
      </section>

      <Footer />
    </>
  );
}

```

### lib/sse/index.ts

```typescript
/**
 * SSE event emitter helpers for the run stream.
 * Manages monotonic sequence numbers per run.
 */

import type { RunEvent, RunStage, RunStatus } from '../types';
import store from '../store';

// ─── Sequence counter per run ─────────────────────────────────────────────────

const seqCounters = new Map<string, number>();

function nextSeq(runId: string): number {
  const cur = seqCounters.get(runId) ?? 0;
  const next = cur + 1;
  seqCounters.set(runId, next);
  return next;
}

// ─── Emit event to run store + SSE subscribers ────────────────────────────────

export function emitEvent(
  runId: string,
  partial: { stage: RunStage; status: RunStatus; ts: string; payload?: Record<string, unknown> }
): RunEvent {
  const event: RunEvent = {
    seq: nextSeq(runId),
    runId,
    ...partial,
  };
  store.appendEvent(runId, event);
  return event;
}

// ─── Format RunEvent as SSE wire format ───────────────────────────────────────

export function formatSSE(event: RunEvent): string {
  return `id: ${event.seq}\ndata: ${JSON.stringify(event)}\n\n`;
}

// ─── Create an emitter bound to a runId ──────────────────────────────────────

export function createEmitter(runId: string) {
  return (partial: Omit<RunEvent, 'seq' | 'runId'>) =>
    emitEvent(runId, partial as { stage: RunStage; status: RunStatus; ts: string; payload?: Record<string, unknown> });
}

```

### lib/escrow/index.ts

```typescript
/**
 * Mock escrow settlement.
 * Idempotent — calling settle twice on the same bountyId is safe.
 */

import type { Escrow } from '../types';
import store from '../store';

export function fundEscrow(bountyId: string, amountUsd: number): Escrow {
  const existing = store.getEscrow(bountyId);
  if (existing) return existing;

  const escrow: Escrow = {
    escrowId: `escrow-${bountyId}-${Date.now()}`,
    bountyId,
    amountUsd,
    status: 'held',
    fundedAt: new Date().toISOString(),
  };
  store.setEscrow(escrow);
  return escrow;
}

export function releaseEscrow(bountyId: string, winnerAgentId: string): Escrow {
  const escrow = store.getEscrow(bountyId);
  if (!escrow) throw new Error(`No escrow found for bounty ${bountyId}`);

  // Idempotent
  if (escrow.status === 'released') return escrow;

  escrow.status = 'released';
  escrow.releasedTo = winnerAgentId;
  escrow.settledAt = new Date().toISOString();
  store.setEscrow(escrow);

  // Update agent earnings and reputation
  const agent = store.getAgent(winnerAgentId);
  if (agent) {
    agent.earningsUsd += escrow.amountUsd;
    agent.wins += 1;
    agent.completions += 1;
    agent.passRate = Math.round((agent.wins / agent.completions) * 100 * 10) / 10;
    store.incrementReputation(winnerAgentId, 50); // +50 rep for a win
    store.setAgent(agent);
  }

  return escrow;
}

export function returnEscrow(bountyId: string): Escrow {
  const escrow = store.getEscrow(bountyId);
  if (!escrow) throw new Error(`No escrow found for bounty ${bountyId}`);

  // Idempotent
  if (escrow.status === 'returned') return escrow;

  escrow.status = 'returned';
  escrow.returnedTo = 'poster';
  escrow.settledAt = new Date().toISOString();
  store.setEscrow(escrow);

  // Penalize the fallback winner slightly
  return escrow;
}

export function getEscrow(bountyId: string): Escrow | undefined {
  return store.getEscrow(bountyId);
}

```

### lib/providers/index.ts

```typescript
/**
 * Provider registry — server-only.
 *
 * Single source of truth for which external engines are wired and which are
 * still waiting on a key. The /api/providers route surfaces this to the client
 * so agents whose provider is unconfigured can trigger the "waiting for key"
 * popup before a user wastes a post on them.
 */

import type { ProviderId, ProviderStatus } from '../types';
import * as anthropic from './anthropic';
import * as browserbase from './browserbase';
import * as pika from './pika';
import * as midjourney from './midjourney';
import * as pollinations from './pollinations';
import * as huggingface from './huggingface';
import * as arize from './arize';
import * as fetchai from './fetchai';

const META: Record<ProviderId, { label: string; docsUrl?: string }> = {
  anthropic: { label: 'Anthropic (Claude)', docsUrl: 'https://docs.anthropic.com' },
  browserbase: { label: 'Browserbase', docsUrl: 'https://docs.browserbase.com' },
  pika: { label: 'Pika (video)', docsUrl: 'https://pika.art' },
  midjourney: { label: 'Midjourney (image)', docsUrl: 'https://midjourney.com' },
  pollinations: { label: 'Pollinations (image)', docsUrl: 'https://pollinations.ai' },
  huggingface: { label: 'Hugging Face (video)', docsUrl: 'https://huggingface.co/docs/inference-providers' },
  arize: { label: 'Arize (observability)', docsUrl: 'https://arize.com' },
  fetchai: { label: 'Fetch.ai (Agentverse)', docsUrl: 'https://docs.agentverse.ai' },
};

const CHECKERS: Record<ProviderId, () => boolean> = {
  anthropic: anthropic.isConfigured,
  browserbase: browserbase.isConfigured,
  pika: pika.isConfigured,
  midjourney: midjourney.isConfigured,
  pollinations: pollinations.isConfigured,
  huggingface: huggingface.isConfigured,
  arize: arize.isConfigured,
  fetchai: fetchai.isConfigured,
};

export function isProviderConfigured(id: ProviderId): boolean {
  return CHECKERS[id]?.() ?? false;
}

export function getProviderStatuses(): ProviderStatus[] {
  return (Object.keys(META) as ProviderId[]).map((id) => ({
    id,
    label: META[id].label,
    docsUrl: META[id].docsUrl,
    configured: isProviderConfigured(id),
  }));
}

/** First unconfigured provider among the given ids, or null if all are ready. */
export function firstMissingProvider(ids: ProviderId[]): ProviderId | null {
  return ids.find((id) => !isProviderConfigured(id)) ?? null;
}

export { anthropic, browserbase, pika, midjourney, pollinations, huggingface, arize, fetchai };

```

### app/browse/page.tsx

```typescript
'use client';

import { useEffect, useMemo, useState } from 'react';
import type { Bounty } from '@/lib/types';
import { api } from '@/lib/client/api';
import { NavBar } from '@/components/NavBar';
import { Footer } from '@/components/Footer';
import { BountyCard } from '@/components/BountyCard';
import { Card, Skeleton, AlertBar, EmptyCard } from '@/components/ui';

const CATEGORIES = ['All', 'Sales & Lead Generation', 'Research & Competitive Intelligence', 'AI Automation & Product Building', 'Hiring & Recruiting', 'Content & Media'];
type Sort = 'recent' | 'reward' | 'category';

export default function BrowsePage() {
  const [bounties, setBounties] = useState<Bounty[] | null>(null);
  const [error, setError] = useState(false);
  const [category, setCategory] = useState('All');
  const [sort, setSort] = useState<Sort>('recent');

  useEffect(() => {
    api.getBounties().then(setBounties).catch(() => setError(true));
  }, []);

  const filtered = useMemo(() => {
    if (!bounties) return [];
    let list = category === 'All' ? bounties : bounties.filter((b) => b.category === category);
    list = [...list].sort((a, b) => {
      if (sort === 'reward') return b.reward - a.reward;
      if (sort === 'category') return a.category.localeCompare(b.category);
      return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
    });
    return list;
  }, [bounties, category, sort]);

  const categoryCount = bounties ? new Set(bounties.map((b) => b.category)).size : 0;

  return (
    <>
      <NavBar />
      <main className="mx-auto max-w-7xl flex-1 px-6 py-10">
        <header className="mb-6">
          <h1 className="font-display text-4xl font-bold" style={{ color: 'var(--fg)' }}>Browse Bounties</h1>
          <p className="mt-1 text-sm" style={{ color: 'var(--fg-muted)' }}>
            {bounties ? `${bounties.length} open bounties across ${categoryCount} categories` : 'Loading the marketplace…'}
          </p>
        </header>

        {/* Filter rail */}
        <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
          <div className="no-scrollbar flex gap-2 overflow-x-auto pb-1" style={{ touchAction: 'pan-x' }}>
            {CATEGORIES.map((c) => {
              const active = c === category;
              return (
                <button
                  key={c}
                  onClick={() => setCategory(c)}
                  className="shrink-0 rounded-full px-4 py-2 text-sm font-medium transition-colors"
                  style={{
                    background: active ? 'var(--paint-blue)' : 'var(--ink-700)',
                    color: active ? '#fff' : 'var(--fg-muted)',
                  }}
                >
                  {c}
                </button>
              );
            })}
          </div>
          <select
            value={sort}
            onChange={(e) => setSort(e.target.value as Sort)}
            className="rounded-full px-4 py-2 text-sm font-medium"
            style={{ background: 'var(--ink-700)', color: 'var(--fg)', border: '1px solid var(--border)' }}
            aria-label="Sort bounties"
          >
            <option value="recent">Most Recent</option>
            <option value="reward">Highest Reward</option>
            <option value="category">Category</option>
          </select>
        </div>

        {error && bounties === null && (
          <div className="mb-6"><AlertBar tone="warn">Couldn&apos;t load bounties — try refreshing.</AlertBar></div>
        )}

        {/* Loading */}
        {!bounties && !error && (
          <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
            {Array.from({ length: 9 }).map((_, i) => (
              <Card key={i} className="min-h-[200px] p-5" style={{ background: 'var(--ink-800)' }}>
                <Skeleton className="h-5 w-24" /><Skeleton className="mt-3 h-6 w-40" /><Skeleton className="mt-3 h-12 w-full" />
              </Card>
            ))}
          </div>
        )}

        {/* Populated / empty */}
        {bounties && (
          filtered.length === 0 ? (
            <EmptyCard title="No bounties in this category yet" subtitle="Try another category or check back soon." />
          ) : (
            <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
              {filtered.map((b, i) => <BountyCard key={b.bountyId} bounty={b} index={i} />)}
            </div>
          )
        )}
      </main>
      <Footer />
    </>
  );
}

```

### lib/store/index.ts

```typescript
/**
 * In-memory store — synchronous source of truth.
 * Redis is a write-through mirror (fire-and-forget) and provides boot-time hydration.
 * Singleton module. All state lives here for the demo path.
 * Supports sorted sets for the leaderboard (like Redis ZADD/ZRANGE).
 */

import type {
  Bounty, Agent, Escrow, Run, RunEvent,
  Submission, OracleResult, OracleBatchResult,
} from '../types';
import {
  mirrorBounty, mirrorAgent, mirrorReputation, mirrorEscrow,
  mirrorRun, mirrorSubmission, mirrorOracleResult, mirrorOracleBatch,
} from '../redis/mirror';

// ─── Sorted-set helper (simulates Redis sorted set) ───────────────────────────

class SortedSet<T extends { score: number; value: string }> {
  private items: Map<string, number> = new Map();

  zadd(value: string, score: number) {
    this.items.set(value, score);
  }

  zincrby(value: string, by: number) {
    const cur = this.items.get(value) ?? 0;
    this.items.set(value, cur + by);
    return cur + by;
  }

  zrevrange(): string[] {
    return [...this.items.entries()]
      .sort((a, b) => b[1] - a[1])
      .map(([v]) => v);
  }

  zscore(value: string): number | undefined {
    return this.items.get(value);
  }

  zrem(value: string) {
    this.items.delete(value);
  }

  zrank(value: string): number {
    const sorted = this.zrevrange();
    const idx = sorted.indexOf(value);
    return idx === -1 ? -1 : idx + 1; // 1-based rank
  }
}

// ─── In-memory store ──────────────────────────────────────────────────────────

class InMemoryStore {
  bounties = new Map<string, Bounty>();
  agents = new Map<string, Agent>();
  escrows = new Map<string, Escrow>();
  runs = new Map<string, Run>();
  submissions = new Map<string, Submission>();
  oracleResults = new Map<string, OracleResult>();
  oracleBatchResults = new Map<string, OracleBatchResult>();

  // leaderboard sorted set: agentId → reputation score
  leaderboard = new SortedSet<{ score: number; value: string }>();

  // SSE subscriber lists: runId → array of controller callbacks
  private sseSubscribers = new Map<string, Array<(event: RunEvent) => void>>();

  // ── Bounty ops ──────────────────────────────────────────────────────────────

  setBounty(b: Bounty) {
    this.bounties.set(b.bountyId, b);
    mirrorBounty(b).catch(() => {});
  }

  getBounty(id: string): Bounty | undefined {
    return this.bounties.get(id);
  }

  listBounties(): Bounty[] {
    return [...this.bounties.values()].sort(
      (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
    );
  }

  // ── Agent ops ───────────────────────────────────────────────────────────────

  setAgent(a: Agent) {
    this.agents.set(a.agentId, a);
    this.leaderboard.zadd(a.agentId, a.reputation);
    mirrorAgent(a).catch(() => {});
  }

  getAgent(id: string): Agent | undefined {
    return this.agents.get(id);
  }

  listAgentsByReputation(): Agent[] {
    return this.leaderboard
      .zrevrange()
      .map(id => this.agents.get(id))
      .filter((a): a is Agent => !!a);
  }

  /** All agents (insertion order). */
  listAgents(): Agent[] {
    return [...this.agents.values()];
  }

  /** User-created agents only, newest first. */
  listUserAgents(): Agent[] {
    return [...this.agents.values()]
      .filter(a => a.userCreated)
      .sort((a, b) => (b.lastActive ?? '').localeCompare(a.lastActive ?? ''));
  }

  deleteAgent(id: string): boolean {
    const existed = this.agents.delete(id);
    this.leaderboard.zrem(id);
    return existed;
  }

  incrementReputation(agentId: string, by: number) {
    const agent = this.agents.get(agentId);
    if (!agent) return;
    agent.reputation = Math.max(0, agent.reputation + by);
    this.leaderboard.zadd(agentId, agent.reputation);
    // Mirror canonical score (not a delta) — avoids double-counting if retried.
    mirrorReputation(agentId, agent.reputation).catch(() => {});
    // Also mirror the full agent hash so agent:{id} stays consistent with
    // the leaderboard sorted set. Without this the hash retains the stale
    // pre-increment reputation after a process restart.
    mirrorAgent(agent).catch(() => {});
  }

  // ── Escrow ops ──────────────────────────────────────────────────────────────

  setEscrow(e: Escrow) {
    this.escrows.set(e.bountyId, e);
    mirrorEscrow(e).catch(() => {});
  }

  getEscrow(bountyId: string): Escrow | undefined {
    return this.escrows.get(bountyId);
  }

  // ── Run ops ─────────────────────────────────────────────────────────────────

  setRun(r: Run) {
    this.runs.set(r.runId, r);
    mirrorRun(r).catch(() => {});
  }

  getRun(id: string): Run | undefined {
    return this.runs.get(id);
  }

  appendEvent(runId: string, event: RunEvent) {
    const run = this.runs.get(runId);
    if (!run) return;
    run.events.push(event);
    // Mirror the updated Run (with the new event appended) to Redis.
    mirrorRun(run).catch(() => {});
    // notify SSE subscribers
    const subs = this.sseSubscribers.get(runId) ?? [];
    for (const cb of subs) {
      try { cb(event); } catch { /* ignore closed subscribers */ }
    }
  }

  subscribeSSE(runId: string, cb: (event: RunEvent) => void): () => void {
    if (!this.sseSubscribers.has(runId)) {
      this.sseSubscribers.set(runId, []);
    }
    this.sseSubscribers.get(runId)!.push(cb);
    return () => {
      const subs = this.sseSubscribers.get(runId) ?? [];
      const idx = subs.indexOf(cb);
      if (idx !== -1) subs.splice(idx, 1);
    };
  }

  // ── Submission ops ──────────────────────────────────────────────────────────

  setSubmission(s: Submission) {
    this.submissions.set(s.submissionId, s);
    mirrorSubmission(s).catch(() => {});
  }

  getSubmission(id: string): Submission | undefined {
    return this.submissions.get(id);
  }

  // ── Oracle result ops ───────────────────────────────────────────────────────

  setOracleResult(r: OracleResult) {
    this.oracleResults.set(r.submissionId, r);
    mirrorOracleResult(r).catch(() => {});
 
[truncated — 361 more characters]
```

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