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.
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/nowcomputes 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 byMAX_DAILY_HOUSE_CLIPSso 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/submitstores 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=1on 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.
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.
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, runsrecorder --oncefor 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 withnode recorder/sandbox-snapshot.mjsand setRECORDER_SANDBOX_SNAPSHOT. Sandbox auth is automatic on Vercel (OIDC). - Long-lived worker: run the recorder anywhere with
FAL_KEY(seerecorder/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.
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.
npm install
npm run devOpen 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).
- Import the repo into Vercel.
- Add an Upstash Redis integration (or set
UPSTASH_REDIS_REST_URL/TOKEN) — required in prod; the in-memory store is dev-only. - Set
FAL_KEY,ANTHROPIC_API_KEY, the four Voltage account variables,CRON_SECRET, andVOLTAGE_WEBHOOK_ID/VOLTAGE_WEBHOOK_SECRETas described below. For the director tier addDIRECTOR_MAX_DURATION,RECORDER_SECRETandRECORDER_SANDBOX=1; for the clip archive the fiveR2_*variables. - Deploy. The generate route sets
maxDuration = 300.
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/webhookRegister 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.
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.
- 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)