# Server NODE_ENV=development PORT=4000 # F-017: DATABASE_URL points at a real Postgres. Leave it EMPTY for local/dev to # use the embedded PGlite store (server/.data/pglite) with no external DB needed. # Only set a real, reachable URL here; a placeholder host (e.g. "host") will fail # the connection probe and (outside production) fall back to PGlite with a warning. # In production a set DATABASE_URL that is unreachable is a hard boot error (no # silent fallback) — that is intentional so real money/state is never misrouted. # # Neon (console.neon.tech): copy the POOLED connection string and KEEP its query # string, i.e. ...?sslmode=require. Pasted without sslmode the driver attempts a # plaintext connection and Neon rejects it; db/index.js now infers verified TLS for # *.neon.tech / *.supabase.co hosts, so either form boots. Use the pooled (-pooler) # endpoint: it survives Neon's compute restarts. Keep `neondb` as the database name # unless you created another one. DATABASE_URL= # Boot patience for a suspended Neon compute (it wakes on first connection, which # can take seconds). Optional: unset values resolve to production 10000ms x 3 # attempts, local 2500ms x 1 attempt. A malformed value degrades to the default # rather than crashing boot. # PG_CONNECT_TIMEOUT_MS=10000 # PG_CONNECT_ATTEMPTS=3 ALLOW_PGLITE_IN_PROD=false JWT_SECRET=change-me-to-at-least-32-chars-long-random-string CORS_ORIGIN=http://localhost:5173 PANTA_MODE=hybrid PANTA_BASE_URL=https://live-api.panta.market/api/v1 PANTA_API_KEY= SOLANA_RPC_URL= # Panta real-time integration (Door 1: real catalog/prices/outcomes + Panta's # settlement LOGIC, bet with our SIMULATED $100 — nothing touches the chain). OFF by # default: it only starts when BOTH this flag is true AND PANTA_API_KEY above is set, # so an accidental boot never polls Panta or spends a key. Use a pk_test_ sandbox key # for dev. When Panta is unreachable the last-known price is kept and honestly labeled # 'stale' — prices are NEVER fabricated (ToU §5). The key stays server-only. PANTA_REALTIME_ENABLED=false # Poll cadence for the single process-wide Panta read (list endpoint batches many # markets per call; Panta's read budget is 120 req/60s, so keep this coarse). PANTA_POLL_INTERVAL_MS=15000 # A mirrored Panta price older than this is relabeled 'stale_panta' in the UI. PANTA_PRICE_STALE_AFTER_MS=60000 SIM_FEE_BPS=200 SIM_CREATOR_SHARE_BPS=2500 SIM_GRADUATION_VOLUME=100 SIM_LIQUIDITY_B=50 SIM_CREATE_FEE=1 # Bounded band (basis points from the flat 0.5 mid) for OUR OWN simulated price # movement where a Panta market has no live price. Clamps drift so a simulated number # can never masquerade as a real market signal. 2000 bps = ±20%. SIM_DRIFT_MAX_BPS=2000 # Kick Developer API — the PRIMARY real-stream provider (Twitch was retired as a # provider in Oct 2026; its TWITCH_* env vars and code are gone). # Register an app at https://dev.kick.com and use the client-credentials app token # (POST https://id.kick.com/oauth/token with grant_type=client_credentials). Both # Client ID + Secret are required for REAL Kick livestream/channel calls. Leave both # blank to run in labeled demo/fallback mode — the app degrades gracefully and never # crashes (same policy as PANTA_API_KEY). NEVER commit real creds; placeholders only. KICK_CLIENT_ID= KICK_CLIENT_SECRET= # A reliably-live 24/7 channel slug optionally bound to the seed gaming room so # "always live" is genuinely real. Leave blank to disable the binding. KICK_FALLBACK_CHANNEL= # Kick's official player iframe (https://player.kick.com/) needs NO `parent` # domain handshake (unlike the old Twitch embed), so there is no parent-domain var. # Server-side cache TTL for the /api/kick/live browse list (avoid hammering Kick). KICK_CACHE_TTL_MS=45000 # Multi-source live feed — GET /api/live (docs/live-aggregation-spec.md §7). # Merges Kick → YouTube → Floor(Livepeer) into one LiveChannel[] with a # never-empty ladder. EACH source is optional and creds-gated exactly like Kick: # a source is enabled purely by whether its creds/config exist (no kill-switch # flag). Missing creds → that source contributes nothing and the grid still loads # (never blank, never a crash). NEVER commit real creds. # YouTube Data API v3 — create an API key in Google Cloud (YouTube Data API v3 # enabled). YOUTUBE_QUERY seeds the currently-live broadcast search. YOUTUBE_API_KEY= YOUTUBE_QUERY=live # Optional Tier-B multi-query (REPLACES YOUTUBE_QUERY when set). Pipe-separated, # each with an optional operator label: "football live:Football|cricket live". # Each query costs 100 search units per refresh — keep the list short. # YOUTUBE_QUERIES= YOUTUBE_CACHE_TTL_MS=45000 # Floor (Livepeer) — the guaranteed last rung of the ladder. FLOOR_FALLBACK_URL is # a self-hosted / always-live HLS playback URL; setting it is what lets /api/live # promise a NON-EMPTY grid even when every other source is down and nothing is # cached. The other FLOOR_FALLBACK_* fields are cosmetic labels for that one card # (no thumbnail is configurable — §1/§4 forbid fabricating a preview URL). # LIVEPEER_API_KEY is optional (enables Livepeer Studio live discovery). FLOOR_PROVIDER=livepeer LIVEPEER_API_KEY= FLOOR_HLS_BASE=https://stream.livepeer.com FLOOR_FALLBACK_URL= FLOOR_FALLBACK_TITLE= FLOOR_FALLBACK_CHANNEL= FLOOR_FALLBACK_CATEGORY= FLOOR_CACHE_TTL_MS=45000 # Grid-level cache TTL for the merged /api/live result (§3 step 4). LIVE_CACHE_TTL_MS=30000 # Privy auth (server) — docs/PRIVY_AUTH_SPEC.md. Lead-managed secrets; leave blank. # PRIVY_APP_SECRET never leaves the server. The web app reads VITE_PRIVY_APP_ID from # this ROOT .env via vite.config.js (envDir: '..' — only VITE_* prefixed vars are # exposed to the browser), NOT from a web/.env.local file. PRIVY_APP_ID= PRIVY_APP_SECRET= # LLM Failover Chain (Broadcast Market Engine) # Ordered by reliability/speed: Groq (PRIMARY) -> OpenRouter -> Gemini -> Cloudflare -> # GitHub Models (emergency). All keys are optional: missing credentials degrade safely. GROQ_API_KEY= GROQ_MODEL=qwen/qwen3.8-27b GROQ_FALLBACK_MODEL=openai/gpt-oss-120b GROQ_BUDGET_PER_DAY=300 GEMINI_API_KEY= GEMINI_MODEL=gemini-3.8-flash GEMINI_BUDGET_PER_DAY=40 CLOUDFLARE_API_TOKEN= CLOUDFLARE_ACCOUNT_ID= CLOUDFLARE_AI_MODEL=@cf/meta/llama-3.3-70b-instruct-fp8-fast CLOUDFLARE_BUDGET_PER_DAY= OPENROUTER_API_KEY= OPENROUTER_MODEL=meta-llama/llama-3.3-70b-instruct:free OPENROUTER_BUDGET_PER_DAY= GITHUB_MODELS_TOKEN= GITHUB_MODEL=gpt-4o GITHUB_BUDGET_PER_DAY=100 LLM_BUDGET_PER_DAY= SIM_BET_WINDOW_MIN= # Web (Vite) — only VITE_* vars reach the browser (read from this ROOT .env via # vite.config.js envDir: '..', baked in AT BUILD TIME, not runtime). # In production VITE_API_URL must be the deployed backend origin (else the app # falls back to http://localhost:4000 and every API/SSE call fails in prod). VITE_API_URL=http://localhost:4000 # Privy web SDK needs the app id exposed to the client. Without this the deployed # sign-in UI cannot mount Privy (server uses PRIVY_APP_ID/PRIVY_APP_SECRET above). VITE_PRIVY_APP_ID=