kissmarrykill

Kiss / Marry / Kill: AI

A live, public-opinion experiment for LLMs. Pick one to kiss, one to marry, one to kill. Watch the rankings change in real time. Monthly snapshots get frozen forever in the archive, with awards.

The digital descendant of a physical voting board from a summit, designed to feel less like a benchmark dashboard and more like a yearbook page the entire internet is signing.


Quick start (demo mode — no setup)

npm install
npm run dev

Open http://localhost:3000. You’ll see the full UI with seeded fake votes and reactions. The site runs in demo mode — votes, reactions, and even past months are stored in memory and reset whenever the server restarts. Perfect for iterating on design.

Production mode (Supabase)

  1. Create a project at https://supabase.com.
  2. In the Supabase SQL editor, paste and run supabase/schema.sql then supabase/seed.sql.
  3. Copy .env.local.example to .env.local and fill in:
    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
    • SUPABASE_SERVICE_ROLE_KEY (server-only, never expose)
    • CRON_SECRET (any long random string; protects the snapshot endpoint)
  4. Restart npm run dev. The demo-mode banner disappears and writes hit Postgres.

Features

Voting

Vote Card (share image)

Reaction system

Monthly Awards

Embeddable widget

Public read-only API (CORS-enabled, edge-cached)

Sound + confetti + easter eggs

Monthly snapshots

At 00:05 UTC on the 1st of each month, call:

POST /api/cron/snapshot
Authorization: Bearer <CRON_SECRET>

This invokes the snapshot_month() Postgres function which freezes the previous month’s leaderboard and awards into monthly_snapshots.

Easiest hosts to wire this up on:

Project layout

src/
  app/
    api/
      vote/                POST a vote
      leaderboard/         GET current month leaderboard
      reasons/             GET reasons feed (sort + category)
      reactions/           POST toggle a reaction
      archive/             GET frozen monthly snapshots
      share/og/            GET personalised Vote Card image
      award/og/            GET monthly award badge image
      cron/snapshot/       POST (cron-only) freeze previous month
      public/leaderboard/  GET (CORS) public read-only
      public/archive/      GET (CORS)
      public/models/       GET (CORS)
    embed/                 iframe-friendly leaderboard widget
    page.tsx               the whole site
    layout.tsx
    globals.css
  components/              all UI (Hero, VoteForm, ShareCard, Leaderboards, ReasonWall,
                           Archive (with awards), EmbedAndApi, Tribute, NavBar,
                           Footer, Toast, MuteToggle, LiveTicker, CountUp, BoardProvider)
  lib/
    models.ts              the LLM catalog (edit here to add/retire)
    awards.ts              award computation (mirrors the SQL function)
    storage/               pluggable backend (Supabase OR in-memory demo)
    voteToken.ts           cookie-based voter identity + IP hashing
    rateLimit.ts           in-process IP rate limit
    sound.ts               synthesised UI sounds + mute persistence
    confetti.ts            self-contained canvas confetti burst
    easterEggs.ts          combo → toast rules
    publicApi.ts           CORS / cache helpers for the public API
    month.ts               UTC month helpers
    types.ts               shared types
supabase/
  schema.sql               tables + RLS + snapshot_month() + reactions
  seed.sql                 model catalog seed (idempotent)

Casual vote enforcement (the trade-off)

This ships with the casual anti-double-vote tier:

Determined users can still cheat by clearing cookies + switching networks. To upgrade to strict (require sign-in), wire Supabase Auth and use the auth user id as voter_token. The schema already accommodates this.

Adding or removing a model

  1. Edit src/lib/models.ts.
  2. Re-run supabase/seed.sql (it upserts by slug).
  3. Done — no other code changes needed.

Origin / tribute section

The bottom of the page shows three placeholder photos for the original summit board. To swap in the real ones:

  1. Drop your images into public/origin/ (e.g. board.jpg).
  2. Edit src/components/Tribute.tsx and replace each <PlaceholderPhoto /> with <img src="/origin/...jpg" />.
  3. Update the captions.