-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.env.example
More file actions
737 lines (654 loc) · 35.8 KB
/
Copy path.env.example
File metadata and controls
737 lines (654 loc) · 35.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# ============================================================
# Social Poster Agent — Environment Variables Template
# Copy to .env and fill in real values. NEVER commit .env.
# ============================================================
# === Social credentials (NEVER commit) ===
# 2FA is disabled on all accounts (OQ-3 resolved) — no TOTP/SMS vars needed
SOCIAL_X_USERNAME=your-username
SOCIAL_X_PASSWORD=
# Email IMAP — for automatic 2FA/verification code retrieval from Gmail
# Gmail: enable 2FA → generate App Password at https://myaccount.google.com/apppasswords
EMAIL_IMAP_HOST=imap.gmail.com
EMAIL_IMAP_PORT=993
EMAIL_USER= # your@gmail.com
EMAIL_PASSWORD= # 16-char App Password (no spaces)
EMAIL_FROM_FILTER=x.com # sender filter for verification emails
EMAIL_IMAP_IDLE_TIMEOUT_MS=300000 # close idle IMAP connection after 5 min
SOCIAL_THREADS_USERNAME= # = Instagram username (Threads = IG account)
SOCIAL_THREADS_PASSWORD= # = Instagram password
SOCIAL_FACEBOOK_EMAIL=
SOCIAL_FACEBOOK_PASSWORD=
SOCIAL_FACEBOOK_PAGE_SLUG= # business page slug for navigation (OQ-1)
# === Social cookies (optional — more stable than username/password login) ===
# If set, cookie-based auth is tried BEFORE username/password login.
# Format: "name1=value1; name2=value2" (same as browser cookie header)
# Extract from browser DevTools: F12 → Application → Cookies
# X: requires auth_token + ct0 (e.g., "auth_token=xxx; ct0=yyy")
# Threads: requires sessionid (e.g., "sessionid=xxx")
# Facebook: requires c_user + xs (e.g., "c_user=xxx; xs=yyy")
SOCIAL_X_COOKIES=
SOCIAL_THREADS_COOKIES=
SOCIAL_FACEBOOK_COOKIES=
# === F20: Session Warm-up Mode (opt-in per account) ===
# Set to true for NEW accounts to enable browse-only → gradual ramp (reduces ban risk).
# Warm-up starts when the account is first seeded on startup.
# Days 1-2: browse-only, Days 3-5: light interactions, Days 6-7: moderate, Day 8+: full.
SOCIAL_X_WARMUP=false
SOCIAL_THREADS_WARMUP=false
SOCIAL_FACEBOOK_WARMUP=false
WARMUP_DAYS_TOTAL=7 # total warm-up days (default 7)
# === LLM providers (reused from content-agent-platform/.env — OQ-6 resolved) ===
# Multi-provider fallback chain (FREE-FIRST, matching CAP's cheap-tier strategy):
# Groq → SambaNova → Cerebras → OpenRouter → DeepSeek → Anthropic → OpenAI → Google
# → NVIDIA → GitHub Models → Mistral → HuggingFace → Together → Cohere → Ollama
# All providers expose OpenAI-compatible API → ChatOpenAI works for all
# (Anthropic via its OpenAI-compatible endpoint https://api.anthropic.com/v1/).
# Only providers with their API key env var set are included in the chain.
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
OPENAI_MODEL=gpt-5-nano
# Shared LLM cache: Redis-backed by default (multi-instance safe). Set to false to use in-memory.
LLM_CACHE_SHARED=true
LLM_CACHE_KEY_PREFIX=spa:cache:llm
LLM_CACHE_TTL_MS=300000
LLM_CACHE_MAX_SIZE=100
LLM_DAILY_BUDGET_PER_ACCOUNT_USD=0 # 0 = unlimited; conservative pre-call reservation
LLM_PROMPT_COMPRESSION_ENABLED=false
LLM_PROMPT_COMPRESSION_URL=
LLM_PROMPT_COMPRESSION_MIN_TOKENS=500
LLM_PROMPT_COMPRESSION_TIMEOUT_MS=1500
# Q1: Per-role provider routing — creative roles get stronger models, analytical
# roles stay cheap. Format: role=provider1,provider2;role2=...
# Roles: draft, hook, critique, judge, facts, utility. Unset = single shared chain.
# If the preferred providers for a role are not configured, the code falls back to the full chain.
LLM_ROLE_CHAINS=draft=anthropic,google,openai;hook=anthropic,google,openai;critique=groq,cerebras,sambanova;judge=groq,cerebras,sambanova;facts=groq,cerebras,sambanova;utility=groq,cerebras,sambanova
LLM_COST_ROUTER_ENABLED=false # opt-in; promotion still requires quality/cost evidence
# Temperature overrides for LLM generation stages. Higher = more creative/varied,
# lower = more deterministic. Reasoning models (gpt-5-nano, o1, o3, o4-mini) ignore
# temperature by design. Defaults are tuned for human-like, creative output.
GENERATION_TEMPERATURE_HOOK=0.95
GENERATION_TEMPERATURE_DRAFT=0.8
GENERATION_TEMPERATURE_REFINE=0.6
REPLIES_TEMPERATURE=0.6
ENGAGEMENT_COMMENT_TEMPERATURE=0.8
ENGAGEMENT_QUOTE_TEMPERATURE=0.8
# Q2: Global LLM concurrency cap (parallel graph branches otherwise burst
# 9-15 concurrent calls and trip free-tier rate limits) + 429 retry delay.
LLM_MAX_CONCURRENT=4
LLM_RATE_LIMIT_RETRY_MS=2500
# Q2: Rate-limit "penalty box" for LLM providers.
# When a provider keeps returning 429, we skip it for a calculated cooldown instead of retrying every 2.5s.
LLM_RATE_LIMIT_MAX_COOLDOWN_MS=7200000 # 2h cap for any provider cooldown
LLM_RATE_LIMIT_BASE_BACKOFF_MS=10000 # 10s starting exponential backoff
LLM_RATE_LIMIT_STRIKE_WINDOW_MS=600000 # 10m sliding window for 429 strikes
LLM_RATE_LIMIT_STRIKE_THRESHOLD=3 # 3 429s in window triggers sustained cooldown
LLM_RATE_LIMIT_STRIKE_PENALTY_MS=1800000 # 30m sustained cooldown
LLM_RATE_LIMIT_RETRY_AFTER_MAX_MS=10000 # only retry same provider if Retry-After <= 10s
# Q8: Judge-gated refine loop — posts with anti_ai_tone below this threshold
# get ONE extra refine pass with the judge's feedback. 0 disables the loop.
JUDGE_REFINE_THRESHOLD=0.6
# Stage 2: Batched LLM-as-a-Judge hard-fail threshold. If the lowest of
# (anti_ai_tone, factual_accuracy, character_limit) is below this, the post is
# dropped and not saved. The judge now runs once per topic for all networks.
JUDGE_HARD_FAIL_THRESHOLD=0.25
# Optional per-dimension overrides. Fall back to JUDGE_HARD_FAIL_THRESHOLD when not set.
JUDGE_HARD_FAIL_ANTI_AI=0.2
JUDGE_HARD_FAIL_FACTUAL=0.2
JUDGE_HARD_FAIL_CHARACTER=0.25
# Skip A/B variant generation when the minimum judge score is below this threshold.
JUDGE_SKIP_AB_THRESHOLD=0.6
# Groq — FREE, fast inference (primary)
GROQ_API_KEY=
GROQ_MODEL=meta-llama/llama-4-scout-17b-16e-instruct
# OpenRouter — FREE models available (but limited to 1000 requests/day)
# For production, add credits or use a paid model to avoid rate limits
OPENROUTER_API_KEY=
OPENROUTER_MODEL=meta-llama/llama-4-maverick:free
# DeepSeek — cheap
DEEPSEEK_API_KEY=
DEEPSEEK_MODEL=deepseek-chat
# Cerebras — FREE, fast
CEREBRAS_API_KEY=
CEREBRAS_MODEL=gpt-oss-120b
# SambaNova — FREE 20M tokens/day, no credit card (best free-tier quota)
# Signup: cloud.sambanova.ai → email → API Keys → Create
SAMBANOVA_API_KEY=
SAMBANOVA_MODEL=Meta-Llama-3.3-70B-Instruct
# GitHub Models — FREE 150 RPD, no credit card (GPT-5, Llama, DeepSeek, Mistral)
# Needs GitHub PAT with models:read scope. Signup: github.com/settings/tokens
GITHUB_TOKEN=
GITHUB_MODEL=meta-llama/Llama-4-Scout-17B-16E-Instruct
# Mistral AI — Free mode, no credit card (EU-hosted, strong multilingual)
# Signup: console.mistral.ai → API Keys → Create
MISTRAL_API_KEY=
MISTRAL_MODEL=mistral-small-latest
# Hugging Face Inference Providers — $0.10/mo free, auto-failover to 15+ partners
# Signup: huggingface.co/settings/tokens → fine-grained token → Inference Providers scope
HF_TOKEN=
HF_MODEL=meta-llama/Llama-4-Scout-17B-16E-Instruct
# Together AI — $25 free credits, no credit card (68 free models, no expiry)
# Signup: api.together.ai → Settings → API Keys → Create
TOGETHER_API_KEY=
TOGETHER_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo-Free
# Cohere — Trial key: 1000 calls/mo, 20 RPM, no credit card (not for production)
# Signup: cohere.com → Dashboard → API Keys → Trial key
COHERE_API_KEY=
COHERE_MODEL=command-r7b
# === Ollama (local LLM for F1/F3 decision-making — OQ-19 resolved) ===
OLLAMA_URL=http://localhost:11434
OLLAMA_DEFAULT_MODEL=gemma4
# === Orchestrator ban detection ===
# H9: 5 consecutive FAILED posts within this window → ban detected → WAIT.
# Without a time window, old FAILED posts block the orchestrator indefinitely.
BAN_DETECTION_WINDOW_HOURS=2
# === Database (Docker local — OQ-5 resolved) ===
# PostgreSQL via infra/docker-compose.yml on port 5433 (avoids system PG conflict)
DATABASE_URL=postgresql://spa:spa@localhost:5433/social_poster
# Prisma connection pool tuning. connection_limit is per process; pool_timeout is how
# long Prisma waits for a free connection before throwing P2024. transaction_timeout is
# passed to $transaction for long batches (e.g., multi-post generation).
PRISMA_CONNECTION_LIMIT=20
PRISMA_POOL_TIMEOUT_MS=30000
PRISMA_TRANSACTION_TIMEOUT_MS=30000
SLOW_QUERY_THRESHOLD_MS=500 # log Prisma queries at or above this duration
# === P0-H3: Session Encryption (AES-256-GCM) ===
# Encrypts Playwright storageState at rest (cookies, localStorage).
# Generate with: openssl rand -hex 32 (produces 64 hex chars = 32 bytes)
# If empty, encryption is disabled (passthrough mode — NOT recommended for production).
SESSION_ENCRYPTION_KEY=
# === UI auth (JWT cookie) ===
# When AUTH_ENABLED=true, all backend routes require a valid JWT (issued by POST /auth/login)
# except /auth/login and /health. The JWT is transported in an httpOnly cookie (`spa_token`).
# The UI has a login page at /login; the admin account is bootstrapped from ADMIN_USERNAME/
# ADMIN_PASSWORD on startup (created if missing, password updated if env changes).
# When AUTH_ENABLED=false (default), the guard is pass-through (dev / VPN-only / tests).
# Enabled but empty JWT_SECRET = fail-closed (everything denied).
# In production with AUTH_ENABLED=true, JWT_SECRET must be ≥32 chars.
AUTH_ENABLED=false
JWT_SECRET= # generate: openssl rand -hex 32
ADMIN_USERNAME=admin
ADMIN_PASSWORD= # set a strong password; env is source of truth
# Comma-separated list of additional CORS origins for cross-origin UI deployments
# (e.g. Vercel: https://your-project-ui.vercel.app)
CORS_ORIGIN=
# === Redis / BullMQ (Docker local — port 6381) ===
# Redis via infra/docker-compose.yml on port 6381 (avoids system Redis conflict)
REDIS_URL=redis://localhost:6381
# === Rate limits (configurable — changed without code) ===
# Raised for autonomous human-like operation: more posts/day, longer weekly caps,
# but the min delay stays ≥5min so bursts never look bot-like.
RATE_LIMIT_X_MAX_PER_DAY=5
RATE_LIMIT_X_MAX_PER_WEEK=25
RATE_LIMIT_THREADS_MAX_PER_DAY=4
RATE_LIMIT_THREADS_MAX_PER_WEEK=20
RATE_LIMIT_FACEBOOK_MAX_PER_DAY=2
RATE_LIMIT_FACEBOOK_MAX_PER_WEEK=10
RATE_LIMIT_MIN_DELAY_MS=300000 # 5 min min delay between posts (human-like floor)
# === BullMQ retry config ===
BULLMQ_MAX_RETRIES=3
BULLMQ_RETRY_DELAY_MS=60000 # 1min → 5min → 15min (exponential)
BULLMQ_POSTING_MAX_RETRIES=8 # posting gets more retries for session recovery
BULLMQ_POSTING_RETRY_DELAY_MS=120000 # 2min → 4min → 8min → 16min... (exponential)
BULLMQ_QUEUE_PREFIX=spa # spa:posting-x, spa:posting-threads, ...
BULLMQ_CONCURRENCY_PER_QUEUE=1 # one action per network at a time
BULLMQ_REMOVE_ON_COMPLETE=100 # keep last N completed jobs per queue (Redis memory)
BULLMQ_REMOVE_ON_FAIL=100 # keep last N failed jobs per queue (DLQ audit)
BULLMQ_EVENTS_MAX_LENGTH=100 # limit the BullMQ events stream per queue (default 10 000). Set 0 to disable.
# === F2 Multi-stage thread posting ===
THREAD_CONTINUATION_DELAY_MS=1800000 # 30min between thread replies (position × delay)
# === Content source ===
# CAP path: if set and the runs/ directory exists, ContentReader (filesystem) is used.
# If not set or directory doesn't exist, DbContentReader (DB-backed, LLM-generated) is used.
# On Railway/production, leave empty to use DB-backed topics (no sibling repo needed).
CONTENT_AGENT_PLATFORM_PATH=
SITE_BLOG_PATH=../../../content-agent-platform/content/blog/en
# === Brand / domain context (open-source) ===
# Generic brand metadata used by prompts, Swagger, and canonical URLs.
BRAND_NAME=Social Poster Agent
BRAND_DESCRIPTION=AI-assisted multi-network social posting system
DOMAIN=your product or topic area
BRAND_VOICE_PATH=../brand-voice.md
# Optional YAML-frontmatter/JSON Markdown containing deployment persona seeds.
# Leave empty to avoid shipping in-source editorial defaults.
PERSONA_PROFILES_PATH=
DOMAIN_PROMPT_DIR=config/prompts
TOPIC_CATEGORIES=
CONTENT_PILLARS_PATH=
CONTENT_STYLES_PATH=
HUMOR_MECHANICS_PATH=
SLOP_LEXICON_PATH=
TRENDING_NICHES_PATH=
TRENDING_EVENTS_PATH=
TRENDING_KEYWORD_OVERRIDES_PATH=
VISUAL_STYLES_PATH=
# MEDIA-101: image generation is disabled until a provider adapter is configured.
IMAGE_GENERATION_ENABLED=false
GEMINI_API_KEY=
IMAGE_GENERATION_TIMEOUT_MS=90000
IMAGE_GENERATION_MODEL=gemini-3.1-flash-lite-image
IMAGE_GENERATION_RESOLUTION=1K
IMAGE_GENERATION_DAILY_LIMIT_PER_ACCOUNT=3
IMAGE_GENERATION_COST_BUDGET_USD_PER_DAY=1.0
IMAGE_OUTPUT_DIR=./spa-images
# === Topic generation (DB-backed content source) ===
# LLM generates topic ideas and stores them in the Topic table.
# Active when CONTENT_AGENT_PLATFORM_PATH is empty or points to a non-existent dir.
TOPIC_GENERATION_ENABLED=true
TOPIC_GENERATION_CRON=0 */2 * * * # generate topics every 2 hours
TOPIC_POOL_MIN=30 # generate when active topics < this
TOPIC_BATCH_SIZE=20 # how many topics to generate per batch
# === Browser (Camoufox — stealth Firefox fork, C++ level anti-detect) ===
# Camoufox binary downloaded via `npx camoufox-js fetch` (postinstall)
CAMOUFOX_HEADLESS=true # true | false | virtual (virtual = headful on a virtual display, useful in Docker)
CAMOUFOX_HUMANIZE=true # human-like mouse movement (built-in)
CAMOUFOX_GEOIP=true # geolocation/timezone/locale spoofing (built-in)
CAMOUFOX_LOCALE=en-US # target locale for fingerprint
CAMOUFOX_OS=windows # windows | macos | linux (target OS for fingerprint)
CAMOUFOX_INSTALL_DIR= # optional custom install path (containers/CI)
# P0: residential/mobile proxy is strongly recommended for production. Either set
# a static rotating proxy URL, or use proxy rotation below. Datacenter IPs = high ban risk.
CAMOUFOX_PROXY_URL= # optional static proxy URL for Camoufox (e.g. rotating residential endpoint)
# PROXY_ROTATION_ENABLED=false # enable per-network proxy rotation (requires PROXY_LIST or PROXY_GATEWAY_URL)
# PROXY_LIST= # comma-separated proxy URLs, e.g. http://user:pass@host:port,http://...
# PROXY_GATEWAY_URL= # single rotating gateway URL that rotates IPs server-side
# PROXY_STICKY_MINUTES=10 # how long a network keeps the same proxy before rotating
# MEM: memory-saving firefox_user_prefs applied at Camoufox launch (cache caps,
# session history, JS GC tuning, image decode chunk, telemetry off). Saves
# ~50-100 MB RSS per Firefox process. Safe for automation — does NOT touch
# dom.ipc.processCount (fission isolation kept for anti-detect). Disable for
# debugging/benchmarking: CAMOUFOX_MEMORY_PREFS=false
CAMOUFOX_MEMORY_PREFS=true
# MEM: image decode chunk size (bytes). Fix for camoufox#87 OOM on scroll —
# default 32768 causes excessive memory on media-heavy feeds. 8192 is the
# current factory default (lower = less memory, slower image render).
CAMOUFOX_IMAGE_DECODE_CHUNK=8192
# MEM: block image requests in read-only contexts (engagement scroll, trending
# scrape, verifyPosted) via page.route(). Posting path keeps images for visual
# verification. Media + fonts are always blocked regardless of this flag.
CAMOUFOX_BLOCK_IMAGES_READONLY=true
# PRV: block WebRTC to avoid local IP leaks (X/Threads/FB do not need WebRTC).
CAMOUFOX_BLOCK_WEBRTC=false
# PRV: block WebGL. Can reduce fingerprinting surface, but may look suspicious.
CAMOUFOX_BLOCK_WEBGL=false
# Disable Cross-Origin-Opener-Policy (only enable if cross-origin iframes need clicks).
CAMOUFOX_DISABLE_COOP=false
# Enable main-world JS evaluation (page.evaluate("mw:...")).
CAMOUFOX_MAIN_WORLD_EVAL=false
# Print the Camoufox launch config to stderr (debug only).
CAMOUFOX_DEBUG=false
# Pin Camoufox/Firefox version (e.g. 150). Leave empty to use the latest fetched build.
CAMOUFOX_FF_VERSION=
# Fixed window size "width,height" or "widthxheight" (e.g. 1280,720). Leave empty for random.
CAMOUFOX_WINDOW=
# Screen constraints: JSON {minWidth,maxWidth,minHeight,maxHeight} or CSV minW,maxW,minH,maxH.
CAMOUFOX_SCREEN=
# Path to a BrowserForge fingerprint JSON file to pin identity across launches.
CAMOUFOX_FINGERPRINT_FILE=
# Comma-separated list of custom Firefox addon paths to load.
CAMOUFOX_ADDONS=
# Comma-separated list of default addons to exclude (currently: UBO).
CAMOUFOX_EXCLUDE_ADDONS=
# Xvfb display number for headless="virtual" (e.g. ":99"). Auto-created if empty.
CAMOUFOX_VIRTUAL_DISPLAY=
# SEC2: persistent browser profile dir holds plaintext auth cookies (esp. FB c_user+xs)
# outside the DB encryption. The dir is chmod 0700, but for real at-rest protection in
# production point this at a restricted/encrypted volume — NOT world-readable /tmp.
CAMOUFOX_PROFILE_DIR=/tmp/spa-profiles
# SEC2: MAX_PARALLEL_BROWSERS is deprecated — the code uses BROWSER_POOL_SIZE only.
# BROWSER_POOL_SIZE=1 is safe for posting (concurrency=1 per network); raise only if
# you enable parallel engagement/browsing.
BROWSER_POOL_SIZE=1
# === SE1: session re-login hardening ===
# Enabled for autonomous operation: posting never form-logs-in inline (defers +
# retries), and refreshSessionsCron performs the controlled out-of-band re-login.
# This avoids suspicious inline login attempts during posting (more human-like).
SESSION_DEFERRED_LOGIN=true
SESSION_RELOGIN_CRON=*/15 * * * * # out-of-band re-login schedule (only active when SESSION_DEFERRED_LOGIN=true)
FORM_LOGIN_COOLDOWN_MS=1800000 # 30 min between username/password form logins per network (avoids login spam)
BROWSER_POOL_ACQUIRE_TIMEOUT_MS=60000 # timeout for waiting on a pooled context
SPA_SCREENSHOT_DIR=/tmp/spa-screenshots # screenshots for debugging login/posting
# === EVAL-501: durable human review feedback ===
# Keep false for legacy API clients; enable once the operator UI supplies reason codes.
REVIEW_FEEDBACK_ENFORCE_REASONS=false
REVIEW_FEEDBACK_SYNC_CRON=*/5 * * * *
REVIEW_FEEDBACK_SYNC_BATCH_SIZE=50
REVIEW_FEEDBACK_SYNC_MAX_ATTEMPTS=8
REVIEW_FEEDBACK_SYNC_ORCHESTRATOR_INTERVAL_MS=300000
# === EVAL-702: online evaluator sampling and alert window ===
ONLINE_EVAL_SEMANTIC_SAMPLE_RATE=0.05
ONLINE_EVAL_WINDOW_MS=86400000
ONLINE_EVAL_MAX_OBSERVATIONS=10000
ONLINE_EVAL_ALERT_COOLDOWN_MS=1800000
# === REL-102: jittered recovery probes ===
RESILIENCE_PROBE_JITTER_MS=30000
RESILIENCE_LLM_PROBE_INTERVAL_MS=300000
RESILIENCE_SESSION_PROBE_INTERVAL_MS=900000
RESILIENCE_QUEUE_PROBE_INTERVAL_MS=300000
# === LangGraph Checkpoint (Redis) ===
# 1 hour is enough for crash-resume/pause. Manual HITL resume that takes longer
# can be supported by setting a larger value (e.g. 18000 for 5h) per environment.
CHECKPOINT_TTL_SECONDS=3600 # 1 hour — checkpoints auto-expire
CHECKPOINT_PREFIX=spa:checkpoint # Redis key prefix for checkpoints
# Optional: use a separate Redis instance for checkpoints. This lets you set
# maxmemory-policy=allkeys-lru on the checkpoint Redis while keeping BullMQ's
# Redis on noeviction. Falls back to REDIS_URL if unset.
CHECKPOINT_REDIS_URL=
# === SSE (Server-Sent Events) ===
SSE_CHANNEL=spa:events # Redis Pub/Sub channel for SSE events
# === Rate Limit (Redis) ===
RATE_LIMIT_PREFIX=spa:ratelimit # Redis key prefix for rate limit counters
# === Cron ===
# 2x/day generation for HITL (09:00 and 21:00 UTC). Lower burst reduces ban risk.
# Jitter adds a random delay up to N minutes after the cron fires to avoid exact-minute spikes.
CRON_GENERATION_SCHEDULE=0 9,21 * * *
CRON_GENERATION_JITTER_MINUTES=30
HEALTH_MONITOR_SCHEDULE=0 * * * * # hourly health check (F21)
RECONCILIATION_SCHEDULE=30 * * * * # hourly reconciliation (B3)
HEALTH_MONITOR_BAN_THRESHOLD=5 # consecutive failures → BANNED
# === F6: Metrics scraping ===
METRICS_SCRAPER_ENABLED=false # enable daily metrics collection cron
METRICS_SCRAPER_SCHEDULE=0 6 * * * # daily at 6:00 AM
METRICS_SCRAPER_LOCK_KEY=spa:lock:metrics-scraper
METRICS_SCRAPER_LOCK_TTL_MS=600000 # 10 min lock to prevent overlapping cron runs
# === F22: Trending sources (Google Trends + X) ===
TRENDING_SCRAPING_ENABLED=true # master switch for all trending scrapers
TRENDING_SCRAPER_SCHEDULE=0 */2 * * * # refresh cache every 2 hours
TRENDING_CACHE_TTL_MS=900000 # cache time-to-live (default 15 min)
X_TRENDS_SCRAPING_ENABLED=true # enable X/Threads browser scraping
TRENDING_LLM_FILTER_ENABLED=true # run niche relevance LLM filter on trends
# Optional programmatic Google Trends proxy (e.g. SerpApi, RapidAPI, internal gateway).
# If both are set, the proxy is tried first; on failure it falls back to public RSS.
# If only one is set, a warning is logged and RSS is used.
TRENDING_GOOGLE_API_URL= # JSON endpoint returning [{ topic, rank?, url?, traffic? }]
TRENDING_GOOGLE_API_KEY= # sent as Authorization: Bearer <key>
# === F13: Content recycling (RC2) ===
# Enabled for autonomous content volume — recycles under-performing posts weekly.
RECYCLING_CRON_ENABLED=true
RECYCLING_CRON_SCHEDULE=0 8 * * 1 # weekly, Monday 08:00
# === Session warm-up (F20) ===
WARMUP_DAYS_TOTAL=7 # default warm-up period for new accounts
# === API/UI ===
SPA_API_PORT=3100
SPA_UI_PORT=3101
SPA_API_PREFIX=/api/v1
SPA_SWAGGER_PATH=docs # Swagger UI at /docs
# Auth: see AUTH_ENABLED / JWT_SECRET / ADMIN_* at the top of this file.
# When AUTH_ENABLED=false (default) the API is pass-through (VPN-only / dev / tests).
# === F1 Autonomous Agent (Phase 2 — decision engine ready) ===
# Engagement module is gated behind ENGAGEMENT_ENABLED feature flag.
# When false (default), engagement routes are NOT registered — no endpoints exposed.
# To enable: set ENGAGEMENT_ENABLED=true and configure F1_* limits below.
# Default limits are tuned for a cautious, human-like daily pace:
# ~20 likes/day, ~5 comments/day, ~2 discussions/day (repost + quote).
# P0: keep engagement off until the core posting loop is stable — it is the highest ban-risk feature.
ENGAGEMENT_ENABLED=false
# === ADR-006: Autonomous Agent ===
# When true, the autonomous runner cron generates, checks, approves, and posts
# content without operator intervention. Frontend becomes a monitoring dashboard.
# P0: keep false by default — HITL (cron generates, human reviews, agent posts) is the safe default.
AUTONOMOUS_RUNNER_ENABLED=false
AUTONOMOUS_RUNNER_SCHEDULE=0 */4 * * * # every 4 hours
AUTONOMOUS_POSTS_PER_RUN=3 # topics per autonomous cycle
AUTONOMOUS_TARGET_NETWORKS=X,THREADS
ENABLED_NETWORKS=X,THREADS # which networks are active (Facebook disabled by default — session instability)
DEDUP_SINCE_DAYS=14 # topic dedup window (days) — skip topics posted within this period
AUTONOMOUS_POSTING_DELAY_MIN_MS=600000 # 10 min delay before posting (human-like)
AUTONOMOUS_POSTING_DELAY_MAX_MS=3600000 # 1 hour max delay
# Auto-approve gate: quality score thresholds (1-10)
# P0: keep false by default — require human review before posting.
AUTO_APPROVE_ENABLED=false
AUTO_APPROVE_MIN_SCORE=7 # ≥ this → auto-approve
AUTO_APPROVE_REVIEW_SCORE=4 # ≥ this → human review (below = reject)
AUTO_APPROVE_REJECT_STREAK_ALERT=3 # alert on N consecutive rejects in 1h
# P1: LLM-as-a-Judge gate. Auto-approve also requires judge dimensions to be ≥ these thresholds.
AUTO_APPROVE_USE_JUDGE_SCORES=true # when true, judge scores gate auto-approval
AUTO_APPROVE_MIN_JUDGE_ANTI_AI=0.7 # anti-AI tone (0.0-1.0)
AUTO_APPROVE_MIN_JUDGE_FACTUAL=0.6 # factual accuracy (0.0-1.0)
AUTO_APPROVE_MIN_JUDGE_HOOK=0.6 # hook strength (0.0 = disabled)
AUTO_APPROVE_MIN_JUDGE_CHARACTER=0.8 # character limit (0.0 = disabled)
AUTO_APPROVE_REJECT_JUDGE_ANTI_AI=0.3 # below this always rejects
AUTO_APPROVE_REJECT_JUDGE_FACTUAL=0.3 # below this always rejects
# AUTO_CHECK_MIN_QUALITY_SCORE retired (A1/BUG-12): score decision is owned solely
# by AUTO_APPROVE_MIN_SCORE / AUTO_APPROVE_REVIEW_SCORE above. AutoCheck is now a
# pure content-safety gate (bait / char-limit / forbidden phrases / dedup).
# === LLM-in-the-loop queue triage (CONTROL-001) ===
# When true, POST /api/v1/queue/:network/triage uses an LLM to decide whether to
# RETRY, REQUEUE_DELAY, REJECT, or ESCALATE each failed BullMQ posting job.
LLM_QUEUE_TRIAGE_ENABLED=false
LLM_QUEUE_TRIAGE_MAX_JOBS=20 # max failed jobs per triage call
LLM_QUEUE_TRIAGE_MAX_TOKENS=800 # output token budget for the triage LLM
# P1: Full LLM-in-the-loop orchestrator mode.
# When true, the orchestrator always uses the LLM for decisions (not rules fallback).
# When the hourly LLM decision budget is exhausted, it falls back to rules.
LLM_FULL_LOOP_ENABLED=false
LLM_FULL_LOOP_MAX_DECISIONS_PER_HOUR=60
# P0: Token/cost budgets. 0 = unlimited. When exceeded, orchestrator falls back to
# rules and generation run aborts the current network branch.
ORCHESTRATOR_TOKEN_BUDGET_PER_HOUR=0
ORCHESTRATOR_COST_BUDGET_PER_HOUR=0
ORCHESTRATOR_WATCHDOG_LOCK_KEY=spa:orchestrator:watchdog-lock
ORCHESTRATOR_WATCHDOG_LOCK_TTL_MS=60000
GENERATION_TOKEN_BUDGET_PER_RUN=0
GENERATION_COST_BUDGET_PER_RUN=0
# === Orchestrator (LangGraph agent loop — replaces all crons) ===
# When true, a single LangGraph orchestrator runs an adaptive loop
# (OBSERVE → DECIDE → EXECUTE → EVALUATE) replacing all @Cron jobs.
# When false, the old cron-based scheduling is used.
ORCHESTRATOR_ENABLED=false
ORCHESTRATOR_LLM_ENABLED=true # use LLM for soft decisions (false = rules only)
ORCHESTRATOR_LLM_TIMEOUT_MS=30000 # timeout for LLM decision calls (default 30s, was 10s)
ORCHESTRATOR_MAX_ACTIONS_PER_HOUR=60 # guardrail: max actions per hour (one per minute cycle)
ORCHESTRATOR_HEARTBEAT_TTL_MS=600000 # 10 min — watchdog restarts if stale
ORCHESTRATOR_RESTART_DELAY_MS=3000 # delay between stop() and start() on API restart
ORCHESTRATOR_WATCHDOG_RESTART_DELAY_MS=5000 # graceful shutdown window before watchdog restart
# === Posting Windows (Smart — data-driven from engagement metrics) ===
POSTING_WINDOW_MIN_SAMPLES=10 # min posts before using smart windows
POSTING_WINDOW_TOP_HOURS=3 # how many top hours to recommend
POSTING_WINDOW_DECAY_DAYS=30 # weight recent posts more (exponential decay)
POSTING_WINDOW_FALLBACK_HOURS=9,12,18,21 # cold-start fallback hours (UTC)
POSTING_WINDOW_BYPASS=false # when true, ignore posting windows and post whenever approved drafts exist
# Conservative Phase 2 engagement limits — human-like and low ban-risk.
# With 5 browsing sessions/day the targets are ~20 likes, ~5 comments, ~2 discussions.
# NOTE: these are per-session soft targets, clamped to the global daily hard limits below.
F1_LIKES_MAX_PER_DAY=4
F1_COMMENTS_MAX_PER_DAY=1
F1_REPOSTS_MAX_PER_DAY=1
F1_QUOTES_MAX_PER_DAY=1
F1_DISCUSSIONS_MAX_PER_DAY=2
F1_BROWSING_SESSIONS_PER_DAY=5
F1_BROWSING_SESSION_MINUTES=15
F1_MAX_POSTS_PER_SESSION=40
# P0: engagement-first guardrail. Higher = more likely to override POST/GENERATE with BROWSE.
# Formula: engagement.debt * ENGAGEMENT_PRIORITY_WEIGHT > approvedDrafts. 0 = disabled.
ENGAGEMENT_PRIORITY_WEIGHT=1.0
# F1 daily hard limits (per account, across all browsing sessions).
# Per-session soft targets above are clamped to these global totals.
F1_MAX_LIKES_PER_DAY_GLOBAL=20
F1_MAX_COMMENTS_PER_DAY_GLOBAL=5
F1_MAX_REPOSTS_PER_DAY_GLOBAL=1
F1_MAX_QUOTES_PER_DAY_GLOBAL=1
# === Human Behavior Engine (LLM-driven engagement emulation) ===
# Targeting sources — weighted rotation across browsing sessions
# Multilingual hashtags — agent engages with posts in different languages and replies in the same language
ENGAGEMENT_HASHTAGS=#ai,#opensource,#tech,#news,#community
ENGAGEMENT_COMPETITORS=competitor1,competitor2,competitor3
ENGAGEMENT_WEIGHT_HOME_FEED=40
ENGAGEMENT_WEIGHT_HASHTAG=25
ENGAGEMENT_WEIGHT_COMPETITOR=15
ENGAGEMENT_WEIGHT_EXPLORE=10
ENGAGEMENT_WEIGHT_NOTIFICATIONS=10
# Engagement scheduler — autonomous browsing sessions (cron with jitter)
# Enabled + 5 sessions/day spread across waking hours for continuous human-like activity.
# P0: keep false by default — engagement only runs when ENGAGEMENT_ENABLED=true.
ENGAGEMENT_SCHEDULER_ENABLED=false
ENGAGEMENT_SCHEDULE_CRON=0 0 * * * # BUG-2: daily re-scheduling (midnight) so engagement doesn't stop after day 1
ENGAGEMENT_SESSIONS_PER_DAY=5
ENGAGEMENT_SESSION_WINDOWS=08:00,11:00,14:00,17:00,20:00 # HH:MM, comma-separated; malformed entries are ignored (BUG-10)
ENGAGEMENT_JITTER_MINUTES=45 # wider jitter → less predictable (more human-like)
ENGAGEMENT_NETWORKS=X,THREADS
# === F2 Multi-stage posting (Phase 1.5) ===
F2_DELAY_BETWEEN_STAGES_MS=1800000 # 30 minutes
F2_MAX_STAGES=3
# === F4 Adaptive replies (Phase 2) ===
# Enabled for autonomous engagement. Real knobs are REPLIES_* (F4_* are vestigial
# and not read by code). Polling every 4h, up to 3 replies/post, human-like delays.
# P0: keep false by default — replies are the highest ban-risk engagement action.
REPLIES_ENABLED=false
REPLIES_CRON_SCHEDULE=0 */4 * * * # poll for new comments every 4 hours
REPLIES_MAX_PER_POST=5 # total auto-replies per post (raised to allow more conversations)
REPLIES_MAX_CONVERSATION_DEPTH=3 # hard limit: agent replies per dialogue chain
REPLIES_QUESTION_TEMPERATURE=0.3 # classifier LLM temperature
REPLIES_DELAY_MIN_MS=300000 # 5 min min delay between replies
REPLIES_AUTO_DELAY_MIN_MS=300000 # 5 min min delay for auto-replies
REPLIES_AUTO_DELAY_MAX_MS=1800000 # 30 min max delay for auto-replies
REPLIES_AUTO_REPLY_COMPLEXITY=medium # low/medium/high — threshold for human review
REPLIES_SAFETY_TEMPERATURE=0.2 # LLM safety classifier temperature
REPLIES_MAX_PER_DAY=10 # daily auto-reply budget per network (prevents bot-like over-replying)
# === App ===
NODE_ENV=development
# === Dry-Run Mode (CLI: pnpm dry-run) ===
# Set to true by the dry-run CLI automatically. When true, DryRunBrowserPort
# wraps the real BrowserFactory — browser opens and navigates for real, but
# the final submit click is intercepted (screenshot + synthetic URL, no posting).
SPA_DRY_RUN=false
SPA_DEBUG_DIR=/tmp/spa-debug
# === Monitoring (optional — all env-gated, no-op if not set) ===
# Sentry — error tracking (https://sentry.io)
# Set SENTRY_DSN to your project DSN, or leave empty to disable.
# SENTRY_ORG, SENTRY_PROJECT, and SENTRY_AUTH_TOKEN are used by the sentry:sourcemaps script.
SENTRY_DSN=
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_TRACES_SAMPLE_RATE=1.0 # 100% of transactions traced
SENTRY_PROFILES_SAMPLE_RATE=0.1 # 10% of profiles sampled
SPA_RELEASE=spa@0.5.2 # release version tag
# Langfuse — LLM observability (tracing, prompt management, evaluation)
# Auto-enable: tracing activates when LANGFUSE_PUBLIC_KEY is set.
# When absent/empty, Langfuse is a no-op (zero overhead, no network calls).
# Get keys from Langfuse UI → Settings → API Keys.
# 🇪🇺 EU: https://cloud.langfuse.com | 🇺🇸 US: https://us.cloud.langfuse.com | self-hosted URL
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_BASE_URL=https://us.cloud.langfuse.com
# Prompt version label for Langfuse Prompt Management. Defaults to 'latest'.
# Override per prompt with PROMPT_VERSION_<NAME>, e.g.:
# PROMPT_VERSION_DRAFT_POST=experimental
# PROMPT_VERSION_HOOK_GENERATION=v2
PROMPT_VERSION=latest
# ── Link attribution (ROADMAP_V2 Z4 — my_zodiac_ai/back client) ──
# When ZODIAC_API_URL is empty, posts fall back to direct UTM-tagged destination URLs.
ZODIAC_API_URL=
# Shared secret for /internal/attribution-links (MAIN_BACKEND_INTERNAL_TOKEN on zodiac-back)
ZODIAC_INTERNAL_TOKEN=
ZODIAC_DEFAULT_DESTINATION_URL=https://quiz.my-zodiac-ai.com
ZODIAC_TIMEOUT_MS=5000
# NewRelic APM (optional — install newrelic package and require first)
# NEW_RELIC_LICENSE_KEY=
# NEW_RELIC_APP_NAME=spa-backend
# NEW_RELIC_LOG_ENABLED=true
# PostHog product analytics (optional — for UI usage tracking)
# POSTHOG_API_KEY=
# POSTHOG_API_URL=https://app.posthog.com
# ── Extended LLM providers ──
# Anthropic Claude — strong multilingual, reliable backstop
ANTHROPIC_MODEL=claude-haiku-4-5
# Google Gemini — free tier (1500 RPD), strong multilingual
GOOGLE_API_KEY=
GOOGLE_MODEL=gemini-2.5-flash
# MiMo (Xiaomi) — paid, strong multilingual
MIMO_API_KEY=
MIMO_MODEL=mimo-v2.5
# NVIDIA NIM — free ~40 req/min, general multilingual
NVIDIA_API_KEY=
NVIDIA_MODEL=meta/llama-4-scout-17b-16e-instruct
# ── Multi-instance distribution (multi-container / process safety) ──
# Leave INSTANCE_ID empty to auto-generate a UUID per container.
INSTANCE_ID=
INSTANCE_HEARTBEAT_TTL_MS=30000
INSTANCE_HEARTBEAT_INTERVAL_MS=10000
# Orchestrator leader election: only one instance runs the graph loop.
ORCHESTRATOR_LEADER_KEY=spa:orchestrator:leader
ORCHESTRATOR_LEADER_TTL_MS=30000
ORCHESTRATOR_LEADER_RENEW_INTERVAL_MS=10000
# Engagement distributed lock: only one browsing session runs at a time.
ENGAGEMENT_LOCK_KEY=spa:lock:engagement
ENGAGEMENT_LOCK_TTL_BUFFER_MS=300000
ENGAGEMENT_LOCK_ACQUIRE_RETRY_MS=1000
# ── Syndication (Phase 0+ — cross-platform content syndication) ──
# Feature flag — when false, SyndicationModule + ParticipationModule are not registered.
# All 11 new platforms (Dev.to, Hashnode, LinkedIn, Bluesky, Mastodon, Telegram,
# Medium, Substack, Reddit, Quora, Pinterest) are gated behind this flag.
# No API keys needed (except Telegram Bot API) — all via Camoufox browser automation.
SYNDICATION_ENABLED=false
# Blog base URL for canonical URLs (POSSE — Publish Own Site, Syndicate Elsewhere)
BLOG_BASE_URL=https://your-domain.com
# Article generation cron schedule (default: weekly Monday 9am)
CRON_ARTICLE_GENERATION_SCHEDULE=0 9 * * 1
# Telegram (only API-based platform — Bot API is free, no approval, instant setup)
# Create bot via @BotFather → /newbot → get token → create channel → add bot as admin
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHANNEL_ID=
# Operator control bot (TGBOT-101): separate token, chat allowlist, default off.
# Commands: /status /pending /approve /reject /pause /resume — see docs/roadmap/16
CONTROL_BOT_ENABLED=false
TELEGRAM_CONTROL_BOT_TOKEN=
TELEGRAM_CONTROL_CHAT_IDS=
# Account credentials for Camoufox login (email/password).
# These are the actual values used for browser login. In DB, only the env var
# NAME is stored (credentials_ref), never the secret itself.
DEVTO_EMAIL=
DEVTO_PASSWORD=
HASHNODE_EMAIL=
HASHNODE_PASSWORD=
LINKEDIN_EMAIL=
LINKEDIN_PASSWORD=
BLUESKY_HANDLE=
BLUESKY_APP_PASSWORD=
BLUESKY_TRANSPORT=api # api (default) or browser rollback
BLUESKY_SERVICE_URL=https://bsky.social
BLUESKY_PUBLIC_API_URL=https://public.api.bsky.app
MASTODON_INSTANCE=mastodon.social
MASTODON_BASE_URL= # optional full instance URL; defaults from MASTODON_INSTANCE
MASTODON_TRANSPORT=api # api (default) or browser rollback
MASTODON_ACCESS_TOKEN=
MASTODON_USERNAME=
MASTODON_VISIBILITY=public
MASTODON_EMAIL=
MASTODON_PASSWORD=
MEDIUM_EMAIL=
MEDIUM_PASSWORD=
SUBSTACK_PUBLICATION=
SUBSTACK_EMAIL=
SUBSTACK_PASSWORD=
REDDIT_USERNAME=
REDDIT_PASSWORD=
QUORA_EMAIL=
QUORA_PASSWORD=
PINTEREST_EMAIL=
PINTEREST_PASSWORD=
# Per-platform auto-approve thresholds (fallback to AUTO_APPROVE_MIN_SCORE=7)
# Reddit is strictest (9) — Reddit bans for self-promo
AUTO_APPROVE_MIN_SCORE_DEVTO=7
AUTO_APPROVE_MIN_SCORE_HASHNODE=7
AUTO_APPROVE_MIN_SCORE_LINKEDIN=7
AUTO_APPROVE_MIN_SCORE_BLUESKY=7
AUTO_APPROVE_MIN_SCORE_MASTODON=7
AUTO_APPROVE_MIN_SCORE_TELEGRAM=7
AUTO_APPROVE_MIN_SCORE_MEDIUM=7
AUTO_APPROVE_MIN_SCORE_SUBSTACK=7
AUTO_APPROVE_MIN_SCORE_REDDIT=9
AUTO_APPROVE_MIN_SCORE_QUORA=8
AUTO_APPROVE_MIN_SCORE_PINTEREST=7
# Per-platform rate limits (daily)
RATE_LIMIT_DAILY_DEVTO=3
RATE_LIMIT_DAILY_HASHNODE=3
RATE_LIMIT_DAILY_LINKEDIN=2
RATE_LIMIT_DAILY_BLUESKY=5
RATE_LIMIT_DAILY_MASTODON=5
RATE_LIMIT_DAILY_TELEGRAM=10
RATE_LIMIT_DAILY_MEDIUM=2
RATE_LIMIT_DAILY_SUBSTACK=2
RATE_LIMIT_DAILY_REDDIT=2
RATE_LIMIT_DAILY_QUORA=2
RATE_LIMIT_DAILY_PINTEREST=3