# Copy to .env and fill in. The loop runs WITHOUT a key using a mock reasoner. # --- LLM provider --- # LLM_PROVIDER: openai | gemini | anthropic (auto-detected from whichever key is # set if blank; OpenAI-compatible preferred). Provider w/ a missing key => mock. LLM_PROVIDER=openai # OpenAI-compatible endpoint — works with OpenAI, OpenRouter, Together, Groq, # vLLM, Ollama, LM Studio, etc. Just point these three at any /chat/completions API: OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_API_KEY= OPENAI_MODEL=gpt-4o-mini OPENAI_JSON_MODE=1 # set 0 if the endpoint rejects response_format=json_object # There is NO mock fallback. On HTTP 429 the loop WAITS this long and retries # (honors Retry-After if present), so it grinds through tight rate limits. LLM_RATELIMIT_WAIT_SECONDS=900 LLM_RATELIMIT_MAX_RETRIES=24 # Other providers: GEMINI_API_KEY= GEMINI_MODEL=gemini-2.5-flash ANTHROPIC_API_KEY= LLM_MODEL=claude-sonnet-4-6 # Scoring thresholds (design Phase 5) PRIORITY_THRESHOLD=0.72 STANDARD_THRESHOLD=0.45 # --- Exa.ai news/web harvester --- # Without a key, the news connector falls back to data/fixtures.json. EXA_API_KEY= EXA_ENABLED=1 # Hard ceilings to stay inside the free plan (~20,000 req/month). The connector # refuses to call Exa once either cap is reached. Monthly count persists in # output/state/exa_usage.json and resets at the start of each calendar month. EXA_MONTHLY_REQUEST_CAP=1000 EXA_PER_RUN_REQUEST_CAP=25 EXA_RESULTS_PER_QUERY=5 EXA_LOOKBACK_DAYS=45 # Exa content category. "news" keeps out careers/marketing landing pages — the # biggest signal-quality lever. "" disables category filtering. EXA_CATEGORY=news # Scope to the account's own domain (1) or broad-web news about it (0, default). # With category=news, broad web finds actual coverage; domain scoping finds the # company's own newsroom. Override per run: loop --no-domain-filter / --domain-filter EXA_DOMAIN_FILTER=0 # Hosts to always exclude (comma-separated), e.g. talent.example.com,careers.example.com EXA_EXCLUDE_DOMAINS=