# Project export: Timbermarket

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

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: You have opinions about who deserves to win TreeHacks, how many llamas will show up, if someone will boil the ocean. We built a prediction market for hackers so you can put fake money on it, together.
- Devpost: https://devpost.com/software/timbermarket
- GitHub: https://github.com/Di10n/timbermarket
- Demo: https://timbermarket.lol/
- Video: https://www.youtube.com/embed/4167g2r_lh0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Di10n (19 commits)

## Devpost submission (written by the team)

### Overview

“I was asking around for cool projects and everyone has been talking about Timbermarket!” -Dagsen (Soon Technology) “Oh shit you guys are the Timbermarket guys!” -Many, many people “Can I pleaseeee have 1000 more leaves, I lost it to insider trading…” -Anonymous In the first 12 hours, we got our core infra running and launched to 1,000 hackers Saturday 9am. In 36 hours, we got more than 250 users, 20+ markets, and over 6k trades, getting interviews with Soon Technology, the CEO of freeCodeCamp, and drawing attention from CMU hackers and Paradigm folks! What it does TimberMarket is a live prediction market built exclusively for TreeHacks. Traders get a starting balance of 1,000 “leaves” to trade contracts on existing markets or propose new markets. Our team travels between events to verify the ground truth and resolve each market for payouts. We have a live-tv style view for monitoring the situation, which we stream out inside of Huang’s basement to hackers as they pass by.

### Inspiration

Hackathons are wonderfully chaotic, and we built Timbermarket to extract the wisdom of the crowd and connect the hacker community. With predictions, we wanted to evaluate hackathon outcomes and judge fairness while building a social layer to engage project discussions, meme debates, friend making, and community building.

### How we built it

At the core of TimberMarket is an Automated Market Maker (AMM) inspired by Maniswap, which is a variant of Uniswap v2. AMM is key to making markets work in lower-liquidity environments. How Our Markets Work Specifically, the core equation that an AMM preserves across all trades is a weighted geometric mean, \(k = y^pn^{(1-p)},\) where \(y, n\) are the number of yes/no token reserves, \(p\) is a fixed probability parameter set at market creation, and \(k\) is the invariant constant that must stay the same after each trade, and we update \(y, n\) as needed. The probability being displayed is equal to $$\frac{pn}{y(1-p)+pn},$$ which can be understood as the proportion of the no values to the yes ones. Buying shares is pretty simple. If you bet on yes, it adds that amount to the no pool and calculates the new yes pool, and gives you the difference between the updated number of shares and the old number of shares. Selling is harder, where selling yes shares means you buy the same number of no shares, redeem both the yes and no shares at the same amount, and the payout is the difference between the redeemed leaves and the leaves you spent to buy the no shares. Note that buying the no shares is not analytically invertible (we are trying to find the price to buy a fixed number of shares, which is the inverse problem), so we use binary search to get an approximate solution within ~1e-8. Ultimately, we went with this design because we didn’t need an order book & updating the database was simple. TV View Also, we also spent a LOT of time designing our TV view, as we knew that it was our unique way of showing ourselves to the rest of the hackers. It has a privileged connection to our database so that the live view can update in real time, showing off recent trades, featured markets, probabilities, number of traders, and comments. Without it, we wouldn’t have made something that felt exciting. Challenges and Fun Stories We got a lot more than we bargained for. All of it fun! Beginning Development We knew that an important part of this project would be getting it out to the hackers as soon as humanly possible. We started at around 10 PM sharp and locked ourselves in the basement until we got our website up after about 11 hours of planning, coding, and designing. Design was an incredibly fun part of the night. We knew that if we had no traders, we wouldn’t have anything! So, we spent a bit of time designing posters during the first night with fun questions to post up on the walls and hand to hackers, which are attached as an image on the devpost! Another constraint that we had to face was that of moderating users so we didn’t have suspicious trades. As a result, we spent a lot of time trying to figure out auth. At first, we wanted to use TreeHack's own QR codes, since they would’ve been unique, but other people who didn’t have one (sponsors, organizers, etc.) also wanted to trade together, so we ended up using phone numbers for verification instead. Then, after we had finished our first version, we went to hand out the posters. Sadly, this was shut down by one of the organizers, so we instead used scraped data from Slack to email blast all of the participants. This grew our site to over a hundred users in only a few hours. Then we launched to hackers and more via Slack, X, and Fizz, drawing in attention from CMU cybersecurity mains who DDoSed us, media teams, the CEO of FreeCodeCamp, and people at Paradigm! CMU Hackers… After we posted our project on Twitter, it gained attention from people both inside and outside of treehacks, and we began to experience attacks on our system. All of Saturday was spent hardening our security against experienced CTF players who took it upon themselves to attempt to exploit our site by slamming our database and launching DDoS attacks. Insider Trading! During the course of the day, the treehacks organizers started joining in on the fun. At first, it was simple markets, with “Will there be at least 3 llamas at treehacks?” Then we got a little more creative, with “Will 6 TreeHacks Organizers form a human pyramid,” which they excitedly (insider) traded, and also “Will a treehacks organizer do more than 40 push-ups,” in which Shrish came into our room, bought a ton of YES shares, and ripped them out. “Will You Find Love At TreeHacks?” Organizers also found ways to spice up live events with our prediction markets. At the Valentines dating game show we made this market: “Will Michael find a date during the V-day event?” Quickly after the event started, we realized that Michael had donned a wig to become Michelle, which we promptly updated on the site. In the end, the pool of contestants was not appealing enough to Michelle, who rejected all of them, though Michelle did attempt to date one of the TreeHackers organizers at the end, so we resolved the market as N/A to reset the market and refund all users, which we implemented for purposes like this. By the end of the second night, we found a TV to show our real-time trades in the Huang basement. We were excited to meet many of the other hackers who scanned our QR code to trade for fun between their intense building hours.

### What we learned

There were two main things we learned. In no particular order, publicity and GTM really matters with a project like this, and when deploying at scale you will get hacked. For publicity, it was extremely important that we get the link out to as many people as possible, but more importantly we needed each trader to feel like they were included. They would need a market that related to them, so we spent a lot of the time just brainstorming markets and adding them in as they came. We also worked on our TV view to reflect on this so you could see live trades happening! For security, we learned that there are always more vulnerabilities than you think, and that if the site reaches a large enough scale, people will always try to find them. Tightening the security around a site takes deliberate planning and care. Vibecoding is an awesome tool, but if you don’t spend the time to understand your own protocol, then you’re at risk!!

### What's next

? The backend is currently moderated by us. We would want to have a better mechanism for creating popular markets without needing at least one of us to be awake, and we’d also like to have a better way to check resolution criteria. We also want to expand to everyone at Stanford. Timbermarket can help party or event organization as a great turnout indicator and encourage more students to discuss and participate in bonding activities like sports games. In particular, connecting the Timbermarket to Fizz posts, where people anonymously post events and questions, seems fun and scalable.

## README (from the GitHub repository)

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details!


## Detected evidence (automated analysis)

Indexed codebase: 89 recognized source files, 345 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (101 of 101)

```
.gitignore
apply_public_access.sql
CLAUDE.md
eslint.config.mjs
MIGRATION_INSTRUCTIONS.md
next.config.ts
package.json
postcss.config.mjs
README.md
src/app/(app)/admin/page.tsx
src/app/(app)/layout.tsx
src/app/(app)/leaderboard/leaderboard-entry.tsx
src/app/(app)/leaderboard/page.tsx
src/app/(app)/markets/[id]/page.tsx
src/app/(app)/portfolio/page.tsx
src/app/(app)/profile/[username]/page.tsx
src/app/(app)/tv/page.tsx
src/app/(auth)/forgot-password/page.tsx
src/app/(auth)/layout.tsx
src/app/(auth)/login/page.tsx
src/app/(auth)/signup/page.tsx
src/app/(auth)/verify/page.tsx
src/app/api/admin/create-market/route.ts
src/app/api/admin/resolve-market/route.ts
src/app/api/admin/rollback-trade/route.ts
src/app/api/admin/set-featured-market/route.ts
src/app/api/auth/forgot-password/reset/route.ts
src/app/api/auth/forgot-password/send/route.ts
src/app/api/auth/forgot-password/verify/route.ts
src/app/api/auth/signup/route.ts
src/app/api/comments/[id]/route.ts
src/app/api/comments/route.ts
src/app/api/market-ideas/route.ts
src/app/api/trade/route.ts
src/app/api/verify-phone/bypass/route.ts
src/app/api/verify-phone/check/route.ts
src/app/api/verify-phone/send/route.ts
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/components/admin-panel.tsx
src/components/footer.tsx
src/components/forest-footer.tsx
src/components/home-carousel.tsx
src/components/home-top-bar.tsx
src/components/leaderboard.tsx
src/components/leaf-icon.tsx
src/components/logout-button.tsx
src/components/market-card.tsx
src/components/market-carousel-card.tsx
src/components/market-comments.tsx
src/components/nav-link.tsx
src/components/navbar.tsx
src/components/paginated-market-list.tsx
src/components/portfolio-tabs.tsx
src/components/probability-chart.tsx
src/components/probability-sparkline.tsx
src/components/recent-comments.tsx
src/components/recent-trades.tsx
src/components/suggest-market.tsx
src/components/theme-toggle.tsx
src/components/trade-panel.tsx
src/lib/amm.ts
src/lib/reset-token.ts
src/lib/supabase/client.ts
src/lib/supabase/middleware.ts
src/lib/supabase/server.ts
src/lib/types.ts
src/lib/utils.ts
src/middleware.ts
supabase/.temp/cli-latest
supabase/.temp/gotrue-version
supabase/.temp/postgres-version
supabase/.temp/rest-version
supabase/.temp/storage-migration
supabase/.temp/storage-version
supabase/migrations/001_create_tables.sql
supabase/migrations/002_create_rls_policies.sql
supabase/migrations/003_create_functions.sql
supabase/migrations/004_add_notes_to_approved_codes.sql
supabase/migrations/005_allow_profile_reads.sql
supabase/migrations/006_fix_sell_rounding.sql
supabase/migrations/007_create_comments.sql
supabase/migrations/008_enable_realtime.sql
supabase/migrations/009_phone_verification.sql
supabase/migrations/010_allow_public_read_access.sql
supabase/migrations/011_get_market_trade_counts.sql
supabase/migrations/012_add_is_featured_to_markets.sql
supabase/migrations/013_auto_redeem_positions.sql
supabase/migrations/014_fix_sell_total_invested.sql
supabase/migrations/015_fix_total_invested_tracking.sql
supabase/migrations/016_enable_comments_realtime.sql
supabase/migrations/017_create_market_ideas.sql
supabase/migrations/018_restrict_phone_number_access.sql
supabase/migrations/019_secure_rpc_functions.sql
supabase/migrations/020_trade_rollback.sql
supabase/migrations/021_fix_rollback_total_invested.sql
supabase/migrations/022_fix_profile_update_and_admin_rpc.sql
supabase/migrations/023_lock_trade_functions_to_service_role.sql
tsconfig.json
ui_elements/Untitled
```

### Dependencies

- package.json: @supabase/ssr@^0.8.0, @supabase/supabase-js@^2.95.3, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, react@19.2.3, react-dom@19.2.3, recharts@^3.7.0, supabase@^2.76.8, tailwindcss@^4, twilio@^5.12.1, typescript@^5

### Recent commits (newest first)

- final commit
- security
- verified users
- increase limit
- fix balance display
- trade count
- carousel
- security
- rollback
- rollback improvements
- rollback
- fix
- fix
- tighten onboarding
- fix count
- security
- features
- show resolved
- icon
- better bars

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

### MIGRATION_INSTRUCTIONS.md

```markdown
# Database Migration Instructions

## What this does
This migration allows unauthenticated users (visitors who haven't logged in) to view:
- Markets
- Trades
- Leaderboard (profiles)
- Probability charts

Trading, portfolio, and admin features remain protected and require authentication.

## How to apply this migration

### Option 1: Supabase Dashboard (Recommended)

1. Open your Supabase project at https://supabase.com/dashboard
2. Navigate to **SQL Editor** in the left sidebar
3. Click **+ New query**
4. Copy the entire contents of `apply_public_access.sql` and paste it into the editor
5. Click **Run** (or press Cmd/Ctrl + Enter)
6. You should see "Success. No rows returned" - that's good!

### Option 2: Supabase CLI

If you have the Supabase CLI installed and linked to the project:

```bash
supabase db execute -f apply_public_access.sql
```

Or manually:

```bash
supabase login
cd /path/to/timbermarket
psql $DATABASE_URL < apply_public_access.sql
```

## Verification

After applying the migration, test by:
1. Opening the site in an incognito/private browser window (not logged in)
2. You should now see markets, trades, and leaderboard data
3. Trying to access `/portfolio` or `/admin` should still require login

## Troubleshooting

If you still see "No markets yet" after applying:
1. Make sure the migration ran successfully (no errors in SQL Editor)
2. Try refreshing the page (Cmd/Ctrl + Shift + R for hard refresh)
3. Check the browser console for any errors
4. Verify RLS policies were updated:
   ```sql
   SELECT schemaname, tablename, policyname
   FROM pg_policies
   WHERE schemaname = 'public'
   ORDER BY tablename, policyname;
   ```

## Need help?

If issues persist, share:
- Any error messages from the SQL Editor
- Browser console errors (F12 > Console tab)
- Output from the verification query above

```

### CLAUDE.md

```markdown
# Timbermarket - Prediction Market Platform
 
## Project Overview
Timbermarket is a prediction market platform built for TreeHacks 2026 hackathon. Users buy and sell shares on yes/no questions using an Automated Market Maker (AMM) algorithm. The platform uses "leaves" as its currency.

**Domain**: timbermarket.lol

## Tech Stack
- **Frontend**: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4
- **Backend**: Next.js API Routes
- **Database**: Supabase (PostgreSQL)
- **Auth**: Supabase Auth
- **Deployment**: Vercel
- **Phone Verification**: Twilio Verify API
- **Charts**: Recharts

## Branch Strategy
- **Main branch**: `main` (production)
- **Current development branch**: `gino`
- Development work happens on feature branches that merge into `main`
- Using shared Supabase dev database

## Core Features

### 1. Authentication & Onboarding
- Users sign up with username and password (no email)
- Synthetic email (`{username}@timbermarket.lol`) used internally for Supabase Auth
- Phone number verification via Twilio Verify API required for approval
- Approved users receive 1000 starting leaves
- Location: `src/app/(auth)/`

### 2. Markets
- List of active and resolved markets
- Each market is a yes/no question with probability
- Market detail page shows probability chart, trade panel, recent trades
- Location: `src/app/(app)/markets/`

### 3. Trading (Maniswap Algorithm)
- **Algorithm**: Maniswap CPMM (Constant Product Market Maker)
- **Invariant**: `k = y^p × n^(1-p)`
  - `y` = YES pool reserves
  - `n` = NO pool reserves
  - `p` = probability parameter
- **Probability**: `p_market = (p × n) / ((1-p) × y + p × n)`
- **Initial liquidity**: 500 units per market recommended
- **Atomicity**: All trades via `execute_trade()` and `execute_sell()` PostgreSQL functions with row locks
- Location: `src/lib/amm.ts` (client-side calculations), `supabase/migrations/003_create_functions.sql` (server-side execution)

### 4. Portfolio
- Shows user's balance and total portfolio value
- Toggle between positions and recent trades views
- Position value = YES shares × probability + NO shares × (1 - probability)
- Location: `src/app/(app)/portfolio/`

### 5. Leaderboard
- Ranks users by total portfolio value (balance + position values)
- Shows position breakdown per user
- Calculates portfolio value in real-time based on current market probabilities
- Location: `src/app/(app)/leaderboard/`

### 6. Admin Panel
- Restricted to users with `is_admin = true`
- Create new markets with initial probability and ante
- Resolve markets to: YES, NO, N/A, or percentage
- Location: `src/app/(app)/admin/`

## Database Schema

### Tables
- **profiles**: User accounts (balance, username, phone_number, is_admin, is_approved)
- **markets**: Questions, pool state, probability, status, resolution
- **trades**: Immutable ledger of all buy/sell actions
- **positions**: Aggregated per-user-per-market holdings
- **probability_history**: Time series for charts

### Key Functions (SECURI
[truncated — 4425 more characters]
```

### package.json

```
{
  "name": "timbermarket",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@supabase/ssr": "^0.8.0",
    "@supabase/supabase-js": "^2.95.3",
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "recharts": "^3.7.0",
    "supabase": "^2.76.8",
    "twilio": "^5.12.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### src/app/layout.tsx

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

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

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

const gaegu = Gaegu({
  variable: "--font-gaegu",
  weight: ["400", "700"],
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Timbermarket",
  description: "Prediction market platform",
  icons: {
    icon: "/timbermarket_logo.svg",
  },
  openGraph: {
    images: ["/timbermarketbackground.jpg"],
  },
};

export const viewport = {
  width: "device-width",
  initialScale: 1,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `(function(){try{var s=localStorage.getItem('theme');var d=s==='dark'||(!s&&window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);}catch(e){}})();`,
          }}
        />
      </head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${gaegu.variable} antialiased bg-background text-foreground min-h-screen`}
      >
        {children}
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
import { redirect } from "next/navigation";
import { createClient, createServiceClient } from "@/lib/supabase/server";
import HomeTopBar from "@/components/home-top-bar";
import HomeCarousel from "@/components/home-carousel";
import Leaderboard from "@/components/leaderboard";
import RecentTrades from "@/components/recent-trades";
import MarketCard from "@/components/market-card";
import SuggestMarket from "@/components/suggest-market";
import RecentComments from "@/components/recent-comments";
import Footer from "@/components/footer";
import type { Market, Trade } from "@/lib/types";

export const dynamic = "force-dynamic";

export default async function Home() {
  const supabase = await createClient();
  const serviceClient = await createServiceClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  // Logged in but not approved: send to verify
  if (user) {
    const { data: profile } = await supabase
      .from("profiles")
      .select("username, balance, is_approved")
      .eq("id", user.id)
      .single();

    if (!profile?.is_approved) {
      redirect("/verify");
    }
  }

  const [marketsResult, tradesResult, profileResult, allProfilesResult, allPositionsResult, userPositionsResult, recentCommentsResult] =
    await Promise.all([
      supabase
        .from("markets")
        .select("*")
        .in("status", ["active", "resolved"])
        .order("volume", { ascending: false })
        .limit(30),
      supabase
        .from("trades")
        .select("*, profiles(username), markets(question)")
        .eq("is_rolled_back", false)
        .order("created_at", { ascending: false })
        .limit(15),
      user
        ? supabase
            .from("profiles")
            .select("username, balance, is_admin")
            .eq("id", user.id)
            .single()
        : { data: null },
      supabase
        .from("profiles")
        .select("id, username, balance")
        .eq("is_approved", true),
      serviceClient
        .from("positions")
        .select("user_id, market_id, yes_shares, no_shares, markets(probability, status)")
        .or("yes_shares.gt.0,no_shares.gt.0"),
      user
        ? supabase
            .from("positions")
            .select("market_id")
            .eq("user_id", user.id)
            .or("yes_shares.gt.0,no_shares.gt.0")
        : { data: [] },
      supabase
        .from("comments")
        .select("id, content, created_at, market_id, profiles(username), markets(question)")
        .order("created_at", { ascending: false })
        .limit(10),
    ]);

  const topMarkets = (marketsResult.data ?? []).sort((a: Market, b: Market) => {
    if (a.status === "resolved" && b.status !== "resolved") return 1;
    if (a.status !== "resolved" && b.status === "resolved") return -1;
    return 0; // preserve volume order within each group
  }) as Market[];
  const recentTrades = (tradesResult.data ?? []) as (Trade & {
    profiles?: { username: string };
    markets?: { question: string } | null;
  })[];
  const profile = profileResult.data as { username: string; balance: number; is_admin?: boolean } | null;

  // Build leaderboard with portfolio values (mirrors leaderboard page logic)
  const recentComments = ((recentCommentsResult.data ?? []) as any[]).map((c) => ({
    id: c.id as string,
    content: c.content as string,
    created_at: c.created_at as string,
    market_id: c.market_id as string,
    profiles: c.profiles as { username: string },
    markets: c.markets as { question: string },
  }));

  const allProfiles = (allProfilesResult.data ?? []) as { id: string; username: string; balance: number }[];
  const allPositions = (allPositionsResult.data ?? []) as any[];
  const leaderList = allProfiles
    .map((p) => {
      const userPos = allPositions.filter((pos: any) => pos.user_id === p.id && pos.markets?.status === 'active');
      const posValue = userPos.reduce((sum: number, pos: any) => {
        const prob = pos.markets?.probability ?? 0.5;
        return sum + pos.yes_shares * prob + pos.no_shares * (1 - prob);
      }, 0);
      return { username: p.username, portfolio_value: p.balance + posValue };
    })
    .sort((a, b) => b.portfolio_value - a.portfolio_value)
    .slice(0, 5);

  // Count traders per market from positions
  const traderCountMap = new Map<string, number>();
  for (const pos of allPositions) {
    traderCountMap.set(pos.market_id, (traderCountMap.get(pos.market_id) ?? 0) + 1);
  }

  // Build carousel: up to 3 markets the user has traded on, then featured market, then fill to 5 with top-volume
  const userMarketIds = new Set(
    ((userPositionsResult.data ?? []) as { market_id: string }[]).map((p) => p.market_id)
  );
  const userTradedMarkets = topMarkets
    .filter((m) => userMarketIds.has(m.id))
    .slice(0, 3);
  const carouselIds = new Set(userTradedMarkets.map((m) => m.id));

  const featuredMarket = topMarkets.find((m) => m.is_featured && !carouselIds.has(m.id));
  if (featuredMarket) {
    carouselIds.add(featuredMarket.id);
  }

  const remainingSlots = 5 - userTradedMarkets.length - (featuredMarket ? 1 : 0);
  const volumeMarkets = topMarkets
    .filter((m) => !carouselIds.has(m.id))
    .slice(0, remainingSlots);
  const carouselMarkets = [...(featuredMarket ? [featuredMarket] : []), ...userTradedMarkets, ...volumeMarkets];

  const marketIds = topMarkets.map((m) => m.id);

  // Fetch probability history and comment counts in parallel
  const [historyResult, commentCountsResult] = await Promise.all([
    marketIds.length > 0
      ? supabase
          .from("probability_history")
          .select("market_id, probability, created_at")
          .in("market_id", marketIds)
          .order("created_at", { ascending: true })
          .limit(10000)
      : { data: [] },
    marketIds.length > 0
      ? supabase
          .from("comments")
          .select("market_id")
          .in("market_id", marketIds)
      : { data: [] },
  ]);

  // Count comments per market
  c
[truncated — 2735 more characters]
```

### src/app/(auth)/layout.tsx

```typescript
import Footer from "@/components/footer";

export default function AuthLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="min-h-screen bg-background text-foreground flex flex-col">
      <div className="flex-1">{children}</div>
      <Footer />
    </div>
  );
}

```

### src/app/(app)/layout.tsx

```typescript
import Navbar from "@/components/navbar";
import Footer from "@/components/footer";

export const dynamic = "force-dynamic";

export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="min-h-screen bg-background text-foreground flex flex-col">
      <Navbar />
      <main className="max-w-6xl mx-auto px-4 py-8 flex-1 w-full">{children}</main>
      <Footer />
    </div>
  );
}

```

### src/lib/supabase/server.ts

```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing sessions.
          }
        },
      },
    }
  );
}

export async function createServiceClient() {
  const { createClient } = await import("@supabase/supabase-js");
  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );
}

```

### src/app/(app)/admin/page.tsx

```typescript
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
import AdminPanel from "@/components/admin-panel";
import type { Market } from "@/lib/types";

export default async function AdminPage() {
  const supabase = await createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) redirect("/login");

  const { data: profile } = await supabase
    .from("profiles")
    .select("is_admin")
    .eq("id", user.id)
    .single();

  if (!profile?.is_admin) redirect("/");

  const [{ data: markets }, { data: featured }, { data: allMarkets }] =
    await Promise.all([
      supabase
        .from("markets")
        .select("*")
        .eq("status", "active")
        .order("created_at", { ascending: false }),
      supabase
        .from("markets")
        .select("id")
        .eq("is_featured", true)
        .eq("status", "active")
        .maybeSingle(),
      supabase
        .from("markets")
        .select("*")
        .order("created_at", { ascending: false }),
    ]);

  const featuredMarketId =
    (featured as { id: string } | null)?.id ?? null;

  return (
    <div>
      <h1 className="text-xl font-bold mb-6">Admin</h1>
      <AdminPanel
        activeMarkets={(markets ?? []) as Market[]}
        allMarkets={(allMarkets ?? []) as Market[]}
        featuredMarketId={featuredMarketId}
      />
    </div>
  );
}

```

### src/app/api/market-ideas/route.ts

```typescript
import { NextResponse } from "next/server";
import { createClient, createServiceClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  const supabase = await createClient();

  const {
    data: { user },
    error: authError,
  } = await supabase.auth.getUser();

  if (authError || !user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Check approval
  const { data: profile } = await supabase
    .from("profiles")
    .select("is_approved")
    .eq("id", user.id)
    .single();

  if (!profile?.is_approved) {
    return NextResponse.json(
      { error: "Account not approved" },
      { status: 403 }
    );
  }

  const { question } = await request.json();

  if (!question || typeof question !== "string") {
    return NextResponse.json(
      { error: "Question is required" },
      { status: 400 }
    );
  }

  const trimmedQuestion = question.trim();
  if (trimmedQuestion.length === 0 || trimmedQuestion.length > 500) {
    return NextResponse.json(
      { error: "Question must be 1-500 characters" },
      { status: 400 }
    );
  }

  const serviceClient = await createServiceClient();

  const { data, error } = await serviceClient
    .from("market_ideas")
    .insert({
      user_id: user.id,
      question: trimmedQuestion,
    })
    .select()
    .single();

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json({ data });
}

```

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