Skip to content

Repository files navigation

INFINITE

An infinite AI-generated cartoon livestream. Y2K / early-Adult-Swim-style animation roasting bitcoin, freedom tech, and AI. Viewers pay sats over Lightning to put their ideas on the broadcast.

Built at ATL BitLab.

How it works

viewer idea ──► durable "preparing" job ──► UI shows AI REVIEW immediately
│ post-response worker; cron repairs interruptions
▼
Claude vibe-check + scriptwriting
│ complete prompt stored before payment
▼
Lightning invoice (Voltage) ──► QR code
│ paid
▼
fal.ai render ──► scheduled playlist ──► "synthetic live" player
  • Synthetic live: no streaming servers. Every clip gets an air slot; /api/now computes the current clip + offset from wall-clock time, so every viewer sees the same broadcast. When nothing fresh is scheduled, a deterministic rerun loop plays the library.
  • House content: when the library is thin — or the newest clip is older than HOUSE_FRESH_HOURS (default 4) — a viewer's player triggers an auto-written clip (locked + capped by MAX_DAILY_HOUSE_CLIPS so it can't burn money). Generation is always viewer-triggered: nobody watching, nothing spent.
  • Moderation before payment: Claude rejects mean-spirited stuff up front, so nobody pays for a clip that won't air.
  • Responsive submission: /api/submit stores the pitch and returns its job ID before Claude runs. The UI shows the AI review stage while a post-response task writes the script. Voltage receives an invoice request only after the complete render prompt is stored. A submit-flow header makes older open tabs refresh instead of interpreting the new preparation state as invoice latency.
  • Mock mode: with no env vars at all, the app runs on sample videos, canned ideas, and fake auto-settling invoices — full flow, zero spend.
  • Live chat (flagged): NEXT_PUBLIC_CHAT_ENABLED=1 (or ?chat=1 on any URL) shows a chat panel next to the player. It free-rides on nostr: every visitor already has a per-browser nostr key, so messages are NIP-53 live-chat events (kind 1311) bound to the station's room address and read straight from public relays — no chat server. Every message has a ⚡ FUND tap that drops its text into the pitch form, so it goes through the same AI review + Lightning checkout as any submission (fresh job id, moderation re-run, price recomputed; nothing in the chat event is trusted). Anyone with a nostr key can post, so there is a per-device mute list and a local send throttle, but no server-side moderation of chat text.

Economics

fal MiniMax H3 Max list price is $0.05/sec @ 480P, $0.08/sec @ 768P (promo rates until 2026-09-07), so a 15s 768P clip costs ≈ $1.20 plus ~a cent of LLM. Default submission price is $2.00 in sats per 15s (converted at spot, PRICE_USD/PRICE_SATS to tune), scaling linearly with length. House filler is capped per day; the more people submit, the less filler you need.

Prices never fall below cost: lib/price.ts estimates what each length costs us (fal seconds at FAL_VIDEO_USD_PER_SEC / FAL_DIRECTOR_USD_PER_SEC, the 60s director minimum, per-scene ffmpeg calls, sandbox time, LLM) and sells at no less than that × PRICE_MARKUP (default 1.5). When fal changes its rates, change the env vars; nothing else needs to move.

Long episodes: H3 Max Director (optional)

Episodes up to 45s are rendered as frame-chained scenes through fal's queue API. Above that, character and plot continuity falls apart, so the optional director tier sells 46–120s episodes as ONE continuous take from minimax/h3-max/director, which keeps up to two minutes of story context natively. (fal ends a session after 13 chunks, about 103s of video, so 120s purchases currently deliver ~103s.)

Director is a live WebRTC stream, not a file API, so those episodes are recorded by the worker in recorder/ rather than inside /api/generate. Enable it with DIRECTOR_MAX_DURATION=120 and a shared RECORDER_SECRET, then give the station a recorder in one of two ways:

  • Sandbox (default for Vercel): set RECORDER_SANDBOX=1. Every paid director job spawns one Vercel Sandbox microVM that clones this repo at the deployed commit, runs recorder --once for that job, and is stopped when the episode lands. Nothing runs while nobody is buying. Optionally bake the ~60s of VM setup into a snapshot with node recorder/sandbox-snapshot.mjs and set RECORDER_SANDBOX_SNAPSHOT. Sandbox auth is automatic on Vercel (OIDC).
  • Long-lived worker: run the recorder anywhere with FAL_KEY (see recorder/Dockerfile). The long durations only appear on the slider while it is polling.

Sessions bill a 60s minimum ($0.02/s promo until 2026-09-14, $0.08/s list), and the writers' room writes a show bible plus one beat per 10s chunk.

Clip archive (Cloudflare R2)

fal keeps generated media on its CDN with a configurable retention and no documented default. With R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET and R2_PUBLIC_URL set, every finished mp4 (house, chained and director) is copied into the bucket before the clip is scheduled and served from there; the fal URL is kept as sourceUrl. A copy failure never blocks an episode: the clip airs from fal and

curl -H "Authorization: Bearer $CRON_SECRET""https://<host>/api/cron/archive-clips?limit=5"

archives whatever is still on fal (run it repeatedly until remaining is 0; this is also the backfill for the pre-archive library). R2 egress is free, so serving from the bucket costs nothing per view.

Develop

npm install
npm run dev

Open http://localhost:3000. Copy .env.example to .env.local and fill keys to go live service-by-service (each one falls back to mock independently).

Deploy (Vercel)

  1. Import the repo into Vercel.
  2. Add an Upstash Redis integration (or set UPSTASH_REDIS_REST_URL/TOKEN) — required in prod; the in-memory store is dev-only.
  3. Set FAL_KEY, ANTHROPIC_API_KEY, the four Voltage account variables, CRON_SECRET, and VOLTAGE_WEBHOOK_ID / VOLTAGE_WEBHOOK_SECRET as described below. For the director tier add DIRECTOR_MAX_DURATION, RECORDER_SECRET and RECORDER_SANDBOX=1; for the clip archive the five R2_* variables.
  4. Deploy. The generate route sets maxDuration = 300.

Voltage webhook

Invoice generation and payment completion use one environment-level webhook. Create it once after the site has a public HTTPS URL:

export INFINITE_URL="https://your-deployment.example"export VOLTAGE_WEBHOOK_ID="$(uuidgen)"export VOLTAGE_API_URL="${VOLTAGE_API_URL:-https://voltageapi.com/v1}"
curl --fail-with-body \
--request POST \
--header "x-api-key: $VOLTAGE_API_KEY" \
--header "Content-Type: application/json" \
"$VOLTAGE_API_URL/organizations/$VOLTAGE_ORG_ID/environments/$VOLTAGE_ENV_ID/webhooks" \
--data "{\"id\": \"$VOLTAGE_WEBHOOK_ID\",\"url\": \"$INFINITE_URL/api/voltage/webhook\",\"name\": \"INFINITE payments\",\"events\": [ {\"receive\": \"generated\"}, {\"receive\": \"completed\"}, {\"receive\": \"expired\"}, {\"receive\": \"failed\"} ] }"

The 202 response contains a one-time shared_secret. Save its id as VOLTAGE_WEBHOOK_ID and shared_secret as VOLTAGE_WEBHOOK_SECRET in the deployment environment, then redeploy. Point production Voltage directly at the deployed endpoint; deliveries have a two-second response deadline.

For local payload inspection, a Smee channel can forward to the route:

npx smee-client \
--url https://smee.io/YOUR_CHANNEL \
--target http://localhost:3000/api/voltage/webhook

Register the Smee URL on a development Voltage environment only. Smee is not a production relay, and JSON reserialization may prevent raw-body signature verification; use a deployed preview or raw-byte-preserving HTTPS tunnel when testing signatures end to end.

Payment reconciliation

The signed Voltage webhook is the fast path. receive.generated writes the BOLT11 directly to Redis, receive.completed records payment, and the browser observes either update through its short local poll without waiting on the Payments API.

Reconciliation is only the recovery path. vercel.json invokes /api/cron/reconcile-payments once per minute. The job repairs missed payment events, retries a bounded exact-ID invoice request after persistent 404 responses, and recovers one interrupted AI preparation per run. Set a random CRON_SECRET of at least 16 characters in Vercel. Vercel supplies it as the route's bearer token automatically.

The worker claims at most 25 pending jobs per run, checks five concurrently, and rotates unresolved jobs to prevent one stale payment from blocking newer ones. Per-minute cron schedules require a Vercel Pro or Enterprise project; on Hobby, invoke the same authenticated route from an external scheduler or use a schedule allowed by that plan.

A completed payment atomically enters the existing generation queue. The active browser starts rendering immediately; if it goes away, a viewer's background drain picks the paid job up later without requiring another payment event.

Roadmap

  • Simulcast to YouTube/X via restream.io (tiny 24/7 ffmpeg relay playing /api/now)
  • Nostr zaps / comments overlay
  • Mirror fal-hosted clips to durable storage (Vercel Blob)

Releases

Packages

Contributors

Languages