Skip to content

Add /ai-debate with a no-login two-turn mini AI round (draft) - #298

Merged
Programcode merged 2 commits into
mainfrom
t3/ai-debate-landing-mini-round
Nov 2, 2026
Merged

Programcode merged 2 commits into
mainfrom
t3/ai-debate-landing-mini-round

Conversation

@Programcode

@Programcode Programcode commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Task 1 from the 2026-11-02 organic spec. Draft: do not merge. Adam approved a draft PR only. Nothing here is deployed: no migration applied, no Edge Function deployed, no env vars set on Supabase or Vercel.

What it adds

/ai-debate is a prerendered landing page for "AI debate partner" searches. It uses the spec's title, meta description, H1 and first sentence.

At the top is a mini round. The visitor picks a motion (5 presets or their own, up to 200 characters) and a side, then writes an argument (up to 600 characters). They get one AI rebuttal (capped at 150 words) and a one-line judge note naming the weakest link in their argument, then one follow-up reply. After the second turn the page shows "Run a full timed round: start your 3-day trial", linking to /login, with "Then $5/month + tax. Cancel anytime." under it.

Without JavaScript, or while the demo is off, the same spot shows a static sample exchange and the trial button.

Below it: how it works, a worked example on an invented motion about an invented town, what the full app adds (Sparring, Crossfire, Case generator, Case attacker), the formats Sparring supports (School 4-6-6-3, Lincoln-Douglas, Public Forum, checked in packages/core/src/sparring/formats.ts), a paragraph on general chatbots linking to /best-ai-for-debate, and the 5-question FAQ. The FAQ says the demo is free and the app is a 3-day trial, then $5/month. It never calls the demo a free plan.

Schema: WebApplication (EducationalApplication; Offer at 5 USD, with the trial in the offer description), WebPage, FAQPage and BreadcrumbList. The page has its own 1200x630 share image, public/og/ai-debate.png. scripts/prerender.mjs now swaps og:image and twitter:image for any page whose head names an image.

Links to it: a secondary "Try an AI debate" button in the hero on the English home page only (/hu and /es keep one button, since this page is English), a footer link, the first chip in the home page's "Practice with AI" group, and llms.txt. The sitemap picks the page up from dist/. The page has 24 internal inlinks.

Server side: the demo-rebuttal Edge Function

The spec asked for POST /api/demo/rebuttal. The site is a static Vite build on Vercel with no API routes, and every server call already goes through Supabase Edge Functions, so this uses an Edge Function instead.

Limits

  • Server off switch. DEMO_ENABLED turns the function on only when it is exactly true. The default is off.
  • Browser off switch. The form shows only when the build sets both VITE_DEMO_ENABLED=true and VITE_TURNSTILE_SITE_KEY. The default is off, and the page shows the sample exchange.
  • Bot check. Cloudflare Turnstile, verified on the server before a session starts. Without TURNSTILE_SECRET_KEY the function stays off.
  • Origin. Only the origins in _shared/cors.ts get through; anything else gets 403.
  • Per IP. 3 sessions per IP per UTC day.
  • Per session. 2 turns.
  • Global daily cap. DEMO_DAILY_LIMIT model calls per UTC day across all visitors. The default is 500.
  • Model. DEFAULT_MODEL (Claude Haiku 5.5), routed through openRouterProviderOptions so it stays on the Anthropic BYOK key, with max_tokens 450, no reasoning and no retries.

The per-IP, per-session and global checks happen in one transaction in claim_demo_round_turn, a service-role-only RPC. It takes a per-IP advisory lock, so parallel requests can't both take the last slot, and it writes nothing when it refuses. A turn counts against the limits before the model call, so a failed call still uses up its slot and can never push spend past the cap.

Moderation. The model classifies each input in the same call it answers. Sexual content, violence, self-harm, hate against a protected group, targeting a private person, or text that isn't a debate argument returns allowed: false, which the function turns into a 422 and the page into "That motion or argument cannot be used in the demo". On top of that: the length caps, an 8 KB body cap, and Anthropic's own safety. There is no separate moderation API.

Privacy. The tables hold a salted SHA-256 of day + IP and counts. Nothing the visitor types is stored. Logs record only the outcome and the turn number ([DEMO] status=ok turn=1). The salt is SPARRING_INTEGRITY_SECRET, or the service-role key when that isn't set, the same fallback the ai function uses. Sessions older than yesterday are deleted on every claim.

Cost per call

OpenRouter's listed price for anthropic/claude-haiku-5.5, fetched today: $0.10 per million input tokens, $0.50 per million output tokens.

  • Input: the system prompt plus the motion, the first exchange and the follow-up, at most about 1,500 tokens = $0.00015.
  • Output: max_tokens 450 = at most $0.000225.
  • Worst case about $0.0004 per call. A full session (2 calls) costs about $0.0008.
  • At the default cap of 500 calls a day, the ceiling is about $0.19 a day.

Billing goes to the Anthropic BYOK key at Anthropic's price. Past the monthly BYOK allowance OpenRouter adds its fee, which these numbers don't include.

Deploy steps (not done here)

  1. Apply supabase/migrations/20261102120000_demo_rounds.sql (staging first, then production).
  2. In Cloudflare, create a Turnstile widget for debateowl.com (plus the staging and preview hosts if wanted). This creates a secret, so Adam does it.
  3. Set on the Supabase project: TURNSTILE_SECRET_KEY, DEMO_ENABLED=true, and optionally DEMO_DAILY_LIMIT (500 if unset).
  4. Deploy the function: npx supabase functions deploy demo-rebuttal. supabase/config.toml sets verify_jwt = false for it.
  5. Set on Vercel (production): VITE_DEMO_ENABLED=true and VITE_TURNSTILE_SITE_KEY, then redeploy.

The CSP in vercel.json already allows https://challenges.cloudflare.com for script-src and frame-src, and the CSP guard test is updated to match.

Kill switch. Set DEMO_ENABLED to anything but true. The function answers 503 demo_disabled and the page switches back to the sample exchange without a redeploy. Unsetting VITE_DEMO_ENABLED hides the form too, but needs a redeploy.

Before merging. The meta description, intro and FAQ promise "one free exchange with no account" in the static HTML. If this merges while the demo is off, the page promises something it can't deliver. Do steps 1 to 5 before or right at merge.

Checks

Run locally:

  • npm run verify at the final head: all 9 steps pass (docs, jargon, lint with knip and squawk, typecheck, guards, Edge tests, smoke, build with prerender, site and link checks, service-worker offline).
  • ./.superset/db-gate.sh --db-only: pgTAP passes, including the new supabase/tests/demo_rounds_test.sql. It covers the rate limits: 3 sessions per IP and a 4th refused, 2 turns and a 3rd refused, a session not continuable from another IP, the global cap refusing without counting, client roles with no access, and old rows deleted.
  • e2e/ai-debate.spec.ts, desktop and mobile Chromium, with Turnstile and the function mocked: two turns end in the trial button, and a 503 demo_disabled falls back to the sample exchange. Not tagged @smoke, so CI doesn't run it.
  • The function run locally with deno run: a wrong origin gets 403, the off state 503, a bad body 400, a missing Turnstile token 403. With Cloudflare's always-pass test secret, a real siteverify call passed and the request failed closed at the database step, since there was no database.
  • Lighthouse 12 on vite preview of /ai-debate: SEO 100, accessibility 100.
  • The prerendered dist/ai-debate/index.html contains the sample exchange and no form, so the page works without JavaScript.

Not run:

  • A real model call end to end. The verify stack's live mode doesn't include this function, and nothing is deployed.
  • Deno unit tests for the function, per Adam's no-unit-tests rule.
  • The Vercel preview is pinned to staging, which has none of the new env vars, so it shows the off state (the sample exchange). That's expected. Preview, behind the Vercel login: https://debatelab-staging-ohix4klzm-adam-gaborjanis-projects.vercel.app/ai-debate

Notes

  • The copy below the mini round comes to about 970 words, slightly over the spec's 600 to 900. Most of it is the worked example.
  • The hero's secondary button shows on the English home page only. The footer link appears in all three languages and points to the English page, as the footer's existing "Compare AI debate tools" link does.

🤖 Generated with Claude Code

Programcode and others added 2 commits November 2, 2026 17:43
/ai-debate is a prerendered landing page for "AI debate partner" searches.
At the top, a visitor picks a motion and a side, writes an argument, and
gets one AI rebuttal with a one-line judge note, then one follow-up. After
the second turn the page offers the 3-day trial. Without JavaScript, or
while the demo is off, the same spot shows a static sample exchange.

The demo-rebuttal Edge Function serves the round. It stays off unless
DEMO_ENABLED=true and TURNSTILE_SECRET_KEY is set, accepts only our
origins, checks a Turnstile token before a session starts, and reserves
each model call through claim_demo_round_turn: 3 sessions per IP per UTC
day, 2 turns per session, and DEMO_DAILY_LIMIT calls per day overall (500
by default). It stores a salted per-day IP hash and counts, nothing the
visitor typed. Each turn is one Claude Haiku 5.5 call with max_tokens 450.

The client shows the live form only when the build sets VITE_DEMO_ENABLED
and VITE_TURNSTILE_SITE_KEY. The CSP allows challenges.cloudflare.com for
the Turnstile script and frame. The page has WebApplication, FAQPage and
BreadcrumbList data and its own share image, and is linked from the hero
(English), the footer, the home page's Practice group and llms.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A follow-up from a different IP (a phone moving between networks) cannot
continue its session. It used to show the generic "unavailable" message
under a reply box that could never succeed; it now says why and ends the
round. The e2e also checks that the FAQ heading renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 2, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

This PR changes supabase/. Its debatelab-staging preview talks to staging, which runs main's migrations and Edge Functions, so this PR's backend changes are not there yet. The VPS staging sync applies them within about 5 minutes of the merge. To try them before merging, use the verify-debatelab skill against a local stack. See docs/staging.md.

@Programcode
Programcode marked this pull request as ready for review November 2, 2026 18:02
Copilot AI balanced review requested due to automatic review settings November 2, 2026 18:02
@Programcode
Programcode merged commit f9405c2 into main Nov 2, 2026
6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants