MINARA

Environment Variables

How the agent consumes secrets, and the convention for adding new ones

This page covers the convention for how minara-agent-v2 loads env vars and the required steps for introducing new ones. For the per-variable reference with defaults, formats, consumer files, and "what breaks if unset" for every variable the agent reads, see Reference → Environment Variables.

The canonical template lives at .env.example at the project root. Keep that file in sync with both pages.

MINARA_ is reserved for Minara-platform-specific variables (MINARA_API_KEY, MINARA_BASE_URL, MINARA_SKIP_FUND_CONFIRM, MINARA_DATA_DIR, MINARA_OPENAI_BASE_URL, MINARA_OPENAI_CHAT_PATH). Agent-loop infrastructure variables (gateway, log, model, scenario, memory knobs) drop the prefix.

TL;DR Convention

Any new skill or tool that needs an API key, secret, token, or overridable URL MUST:

  1. Read it via process.env.<NAME> at factory/initializer time. Never hard-code secrets. Never accept them as CLI args you echo.

  2. Append a documented entry to .env.example in the same commit. The entry must spell out: what it controls, which skill/tool consumes it, when an operator would set it, the default behavior when unset, and the accepted value format. One-line stubs (# FOO_KEY=) are rejected — the template is the first thing a new operator reads. Update this page in the same commit so the published docs stay in sync.

  3. For domain skills (apps/agent/src/skills/builtin/*.ts or apps/agent/src/skills/external/<id>/), declare the var in requires_env so the SkillRegistry hides the skill entirely when the var is missing:

    export const myNewSkill: DomainSkill = {
      id: "research.my_provider",
      // ...
      requires_env: ["MY_PROVIDER_API_KEY"],
    };
  4. For tools (apps/agent/src/tools/*.ts), the factory should return an empty ToolEntry[] when the env var is missing. The tool registry silently excludes unregistered names, so downstream skill tool_names references will gracefully degrade:

    export function createMyProviderTools(): ToolEntry[] {
      const apiKey = process.env.MY_PROVIDER_API_KEY;
      if (!apiKey) return [];
      // ...
    }
  5. Never commit a real secret. .env is git-ignored; .env.example is the committed template with blank values.

How .env loading works

Loading is handled by apps/agent/src/config/load-env.ts — a side-effect module that calls Node 22's built-in process.loadEnvFile(".env"). It's imported as the very first line of every entrypoint:

  • apps/agent/src/gateway/cli.ts — REPL mode
  • apps/agent/src/gateway/server.ts — HTTP mode

ESM evaluation order guarantees the loader runs before any downstream module that reads process.env.<NAME> at import time.

Precedence: variables already exported by your shell / CI / systemd win over values in .env. The loader does not overwrite existing keys. This matches Node's default behavior and is the safest rule (no surprise masking of CI-injected secrets).

No dotenv dependency. We rely purely on process.loadEnvFile, stable since Node 22.5. Missing .env is a no-op (not an error).

Inventory

For detailed per-variable documentation (defaults, formats, consumer files, "what breaks if unset"), see Reference → Environment Variables. The tables below are a quick-reference summary.

LLM providers

At least one must resolve (directly or via a saved OAuth profile from minara login) or the agent refuses to start.

VariableDefaultFormatEffect
ANTHROPIC_API_KEYsk-ant-...Primary Claude credential. Unset: falls through to stored OAuth, then OpenRouter.
OPENROUTER_API_KEYsk-or-...Alternative multi-model router. Unset: tried only if Anthropic paths fail.
OPENAI_API_KEYsk-...Image generation, KB embeddings, TTS, and (opt-in) LLM. Setting this alone does NOT auto-select OpenAI as the LLM — run minara login openai --api-key $OPENAI_API_KEY to enable the LLM path; the env var still powers image/audio/KB tools by itself.
OPENAI_BASE_URLhttps://api.openai.com/v1URLOptional override for the OpenAI API base URL — set when targeting an Azure-compatible gateway or a corporate proxy.
OPENAI_ORG_IDorg-...Optional OpenAI-Organization header for billing routing.
XAI_API_KEYopaqueNative xAI (Grok) LLM provider key — direct calls to api.x.ai/v1 via the xai-api-key provider. Lowest precedence in auto-select; never displaces existing Anthropic/OpenRouter setups.
XAI_BASE_URLhttps://api.x.ai/v1URLOptional override for the xAI API base URL.
MINARA_XAI_OAUTH_CLIENT_IDxAI Grok-CLI public idUUIDOverride the public OAuth client_id sent to auth.x.ai. Default reuses Hermes Agent's shared id per RFC 8252 §8.4; xAI may revoke without notice.

Extended thinking

Claude 3.7+ / Sonnet 4.x / Opus 4.x / Haiku 4.5+ support the thinking API parameter. The model decides per-turn how much (if any) of the budget to actually spend reasoning — simple queries spend ~0 thinking tokens; complex synthesis can use the full budget. Other providers (OpenAI o1/o3) reason automatically; the param is accept-and-ignore for them.

The web-ui's existing ReasoningBlock (collapsed by default with a streaming preview) renders the trace for both regular chat and the institution-mode persona cards. For models WITHOUT native extended thinking, the chat input toolbar shows a manual Thinking toggle that prepends a "think step by step" instruction to the user message.

VariableDefaultFormatEffect
THINKING_ENABLEDtruetrue | falseMaster switch. When false, no agent surface enables thinking (the manual toggle still works for non-native models, but the prefix becomes a no-op too).
THINKING_BUDGET_TOKENS4000positive integerMax thinking tokens per LLM turn. Anthropic charges only for tokens the model actually used.
INSTITUTION_THINKING_BUDGET_TOKENSTHINKING_BUDGET_TOKENSpositive integerOverride scoped to institution-mode role calls. Useful for giving the multi-agent pipeline more reasoning room without raising the global default.
DEEP_RESEARCH_THINKING_BUDGET_TOKENSTHINKING_BUDGET_TOKENSpositive integerOverride scoped to the deep-research synthesis turns.

web_search provider chain

web_search walks a provider chain (first non-empty wins). Native LLM-provider search runs first when configured, then external HTTP providers, then the always-available DuckDuckGo Lite fallback.

Each provider is "transient-failure transparent": exceptions or 0-result responses fall through to the next chain entry. The web_search tool also accepts an explicit engine parameter (auto / exa / firecrawl / anthropic_native / tavily / google / brave / duckduckgo) for ops debugging.

VariableDefaultFormatEffect
EXA_API_KEYExa API keyHighest-priority provider. Runs first in the chain when set, ahead of Firecrawl and every other provider. Neural search index; each result carries highlight snippets. Get a key at https://dashboard.exa.ai/api-keys.
FIRECRAWL_API_KEYFirecrawl API keyRuns second in web_search (after Exa) and first in web_extract (ahead of Tavily). Combined search + clean-markdown extract from one key. Free 500 credits/month. Get a key at https://www.firecrawl.dev/app/api-keys.
WEB_SEARCH_ANTHROPIC_NATIVE_ENABLEDtruetrue | falseAnthropic native runs first when an ANTHROPIC_API_KEY env var OR a Claude.ai OAuth session is active. Set false to skip even when the credential is present. Uses Haiku 4.5 + the web_search_20250305 server tool, ~$0.001 per call.
TAVILY_API_KEYTavily API keyStrong external choice with LLM-generated answer summaries. Get one at https://tavily.com.
GOOGLE_SEARCH_API_KEYGoogle Cloud keyGoogle Custom Search via Programmable Search Engine. Free 100 queries/day, then $5/1000. Requires GOOGLE_SEARCH_CX set together.
GOOGLE_SEARCH_CXProgrammable Search Engine IDThe CX (engine id) from https://programmablesearchengine.google.com/.
BRAVE_SEARCH_API_KEYBrave subscription tokenIndependent index — best modern substitute for the retired Bing Search API. Free 2000 queries/month, then $0.05/1000. Get a key at https://api.search.brave.com/app/dashboard.

Minara core

VariableDefaultFormatEffect
MINARA_API_KEYopaque stringMinara REST credential. Unset: falls back to a stored OAuth profile (web-UI sign-in).
MINARA_BASE_URLhttps://api.minara.aiabsolute URLBackend API origin. Override only when targeting a staging environment or local mock.
MINARA_FRONTEND_BASE_URLhttps://minara.aiabsolute URLWeb frontend origin used for token:// / address:// deep links in terminal hyperlinks and deep-research reports. Override when targeting a staging frontend or local Next.js dev server.
AGENT_MODELclaude-sonnet-4-6model idMain agent-loop model. Also settable via /model.
MINARA_DATA_DIR~/.minaraabsolute pathRoot for SQLite + sandbox + auth + logs.
MINARA_TERMINAL_CWDabsolute pathOverride the agent's working directory (file reads/writes + shell commands). Resolution order: per-session override → this var → process.cwd(). Set by gateway/cron entrypoints whose launch dir differs from the work dir; the local CLI leaves it unset and falls back to the shell cwd. A non-existent path is ignored.
FILES_URL_BASE/v1/filesURL or pathPublic file link prefix (behind reverse proxy).
OFFLINE_MODEoff1/true/yes/onHard-disable outbound HTTP from sandbox tools.
LOG_LEVELinfodebug|info|warn|errorLogger threshold. debug is safe but ~4x volume.

HTTP gateway

VariableDefaultFormatEffect
GATEWAY_PORT8080integerTCP port the gateway binds.
GATEWAY_AUTH_TOKENopaque stringBearer token on every /v1/... route. Unset: auth disabled.
WEB_UI_DIST_DIRabsolute pathBuilt web-ui dist/ served same-origin at / (desktop / single-port). Unset: no static UI (API only).
WEBHOOK_PORTintegerOptional dedicated inbound-webhook port.

Safety and fund-moving

VariableDefaultFormatEffect
DISABLE_STRICT_PLAYBOOKunset (strict on)1/true to disableReverts buildPlaybookBlock to the legacy soft-advisory header. Default behavior renders the imperative "AUTHORITATIVE specification for this turn" checklist tone.
DISABLE_METHODOLOGY_INJECTIONunset (injection on)1/true to disableOff switch for all three on-demand methodology paths (scenario placeholder, tool-output <methodology_reminder>, methodology_lookup tool). Default behavior queries the store at the graduated tier (Wilson ≥ 0.55).
DISABLE_METHODOLOGY_INSTANCE_DISPATCHunset (dispatch on)1/true to disableOff switch for BO-tuned methodology instance overrides. Default behavior merges instance thresholds onto template defaults; disabled = template-only resolution.
DISABLE_KNOWLEDGE_BUDGETunset (budget on)1/true to disableOff switch for the knowledge-budget negotiator. Default: trim combined scenario / memory / role blocks to KNOWLEDGE_BUDGET_TOKENS. Disabled = emit full length.
DISABLE_ROLE_FAILURE_MODES_INJECTunset (inject on)1/true to disableOff switch for the failureModes fallback. Default: when BM25 returns zero case-memory hits for a role, inject its declared failureModes list as warning anchors. Disabled = "hits or nothing".
DISABLE_PARALLEL_TOOL_CALLSunset (parallel on)1/true to disableOff switch for per-turn parallel tool dispatch in the agent-loop. Default: READ_ONLY tier tools not in the META_UNSAFE denylist (activate_skills) run concurrently via Promise.all; CONFIRM_ONCE+ tier tools (fund-moving / writes / withdraws) always stay serial. Disabled = every tool call runs one at a time like the legacy for-await loop. tool_result message order is preserved either way.
CHAT_INTENT_GATING_ENABLEDunset (off)1/true to enableIntent-gated personalization injection. On: the cached prefix keeps the always-inject personalization core (the trading summary, the core knowledge / risk tags, recent memories, custom prompt, active preferences), and a one-shot classifier appends only the remaining behavioural-tag dimensions a turn needs to the volatile block, so they never perturb the cache. Off: the full personalization block is injected into the cached prefix every turn (legacy). Costs one short extra classifier call per turn when on.
SHADOW_MODEsampledoff|sampled|onA/B observation recorder. Writes (current, proposed) variant pairs at classifier / memory-snapshot / role-hints decision points to the shadow_runs SQLite table.
SHADOW_SAMPLE_RATE0.1[0, 1]Sampling probability when SHADOW_MODE=sampled.
SHADOW_RETENTION_DAYS30positive integerDays of shadow_runs rows to keep. One-shot prune runs at boot.
MEMORY_SNAPSHOT_PREF_LIMIT50non-negative integerQuota for preference-category rows in the session memory snapshot.
MEMORY_SNAPSHOT_STRAT_LIMIT30non-negative integerQuota for strategy-category rows.
MEMORY_SNAPSHOT_TRADE_LIMIT50non-negative integerQuota for trade_note-category rows.
MEMORY_SNAPSHOT_OBS_LIMIT30non-negative integerBase quota for observation rows. Unused slots from the pref / strategy / trade buckets overflow into this bucket.
MEMORY_REFRESH_WRITES3non-negative integerWrites-since-rebuild threshold for soft snapshot refresh.
MEMORY_REFRESH_TURNS10non-negative integerTurns-since-rebuild threshold for soft snapshot refresh. Both thresholds (AND) must be met for a mid-session rebuild. Set to 0 to disable refresh entirely (snapshot stays session-frozen).
MEMORY_WRITE_MAX_LEN2000positive integer (200–8000)Max characters kept when the agent saves a memory via memory_write (observation / preference / trade_note / strategy). Longer content is trimmed before it is stored, so one oversized entry cannot flood the prompt or the search index. Also editable at runtime as the memory.writeMaxLen preference. The stricter personalization-fact path keeps its own shorter limit and is unaffected.
KNOWLEDGE_BUDGET_TOKENS15000non-negative integerCap on combined tokens across scenario playbook + memory context + role hints. When exceeded, trims from lowest priority (role → memory → scenario). 0 disables the cap. Set DISABLE_KNOWLEDGE_BUDGET=1 to bypass trimming entirely.
KNOWLEDGE_SOURCE_TAGSfalsetrue|falsePrepend each dynamic knowledge block with an HTML-comment provenance tag. For human / log audit only.
PREFERENCE_LEARNING00/1Master switch for the M2 preference-evolution flow (periodic LLM proposer + in-chat graduation card). When 0, the M1 PreferenceStore + REPL/CLI/REST manual management remain available; the proposer never fires and no card is injected.
PREFERENCE_PROPOSER_INTERVAL30positive integerTurns between consecutive proposer fires. Fire-and-forget async — the proposer runs after the user-visible response is sent, so this is amortized cost, not user latency.
PREFERENCE_WEEKLY_QUOTA3positive integerMax graduations allowed in any rolling 7-day window. Once hit, the proposer skips its cycle. Manual /preferences approve overrides the quota. Mirrors AutoClaw's "1-3 deep evolutions per week" principle.
PREFERENCE_DEDUP_THRESHOLD0.85[0, 1]TF-IDF cosine score above which a candidate is treated as a duplicate of an existing live preference (state ∈ active/proposed/deprecated) and dropped before persisting.
PREFERENCE_PROPOSER_BATCH_SIZE200positive integerMax recent candidates pulled into a single proposer LLM call.
PREFERENCE_MIN_CLUSTER_SIZE3positive integer (≥ 2)Minimum number of supporting candidate messages the proposer's LLM must report for one cluster before the proposal is persisted. Floor of 3 keeps singletons out of the queue.
PREFERENCE_ASK_COOLDOWN_HOURS24positive integerMin hours between consecutive graduation asks for the SAME preference. After "later" or no-reply, the row stays proposed but is hidden from the ask queue until this window passes.
PREFERENCE_ASK_MIN_GAP_TURNS5positive integerMin turns between consecutive graduation asks across DIFFERENT preferences inside the same REPL session. Prevents back-to-back card asks.
PREFERENCE_SKIP_IN_CHAT_ASK00/1Disable the in-chat graduation card entirely. The proposer still runs and writes proposals; operators review via /preferences pending + approve (REPL/CLI/REST).
PREFERENCE_HARD_AUTO_ACTIVATE10/1M3. When on, a user message hitting a strong-signal keyword (never, 绝不, kill switch, …) auto-activates a hard_constraint WITHOUT a card. User gets a 24h undo window via /preferences undo <id>.
PREFERENCE_STYLE_AUTO_ACTIVATE10/1M3. Silent auto-activation of personal_style preferences after N observations of the same dedup_key. Cards reserved for higher-impact preferences.
PREFERENCE_STYLE_MIN_OBSERVATIONS2positive integerM3. Same-statement observations needed before style auto-activates. Higher = more chances for the user to contradict themselves.
PREFERENCE_HARD_UNDO_WINDOW_HOURS24positive integerM3. How long after a strong-signal auto-activation can the user still /preferences undo <id>. Rows outside this window must use /preferences deprecate.
MINARA_SKIP_FUND_CONFIRMoff1/true/yes/onBypass confirm gate on every fund-moving tool. Non-interactive only.
DISABLE_SCRIPT_RISK_GATEoff1/true/yes/on⚠ Off switch for the static-analysis script risk gate that fires before execute_code / terminal / write_file / patch. When unset, RED findings (mass rm *, outside-workspace deletion, IMDS / SSRF, container escape, credential / wallet-store reads, indirect obfuscation + sink) hard-reject and YELLOW findings (fund-moving CLI shell-out, on-chain dangerous calls, env-poisoning, specific-path rm, risky package install) prompt via AskUserQuestion. Setting to truthy bypasses BOTH tiers — for incident response or fully offline CI only. Day-to-day workflow exemptions belong on the workflow definition's script_risk_policy field (body_sha256 + category), not this global env. Audited via bypassed_by="env_global" in script_risk_decisions.
MINARA_TOOL_RESULT_RETAIN_HOURS24integer in [1, 720]How many hours an oversized tool result persisted under <dataDir>/sandbox/files/.tool-results/ survives before the periodic sweep deletes it. Set to 168 (7 days) for compliance / quarterly review windows; set to 1 for ephemeral CI runs. Malformed / out-of-range values fall back to 24h with a warn log.

Hybrid memory retrieval

When EMBEDDING_PROVIDER=disabled (default) the hybrid search code path is inert: every memory row stays at embedding_state='pending' with a NULL embedding column and searchMemoriesHybrid falls through to the existing FTS5 BM25 path with byte-identical results. Operators only enable this when they want the cross-vocabulary recall lift (e.g. a Chinese query 山寨币最近怎么样 retrieving an English altcoin drawdown lesson). The write path stays sync — embeddings happen in a queueMicrotask after the row is committed, so user-facing latency is unchanged.

VariableDefaultFormatEffect
EMBEDDING_PROVIDERdisableddisabled|openai|voyageSelects the embedder. disabled = factory returns null, hybrid path inert. Other values require EMBEDDING_API_KEY.
EMBEDDING_API_KEYopaque stringBearer token. OpenAI: sk-.... Voyage: pa-.... Unset with a non-disabled provider → factory still returns null with a logged warn.
EMBEDDING_MODELprovider-native (text-embedding-3-small / voyage-3)model idModel identifier. Override only when you've validated the dimensionality matches EMBEDDING_DIM.
EMBEDDING_DIM1536positive integerVector dimensionality. Used to declare the vec0 virtual tables at boot. Mismatched models → row stays embedding_state='failed'. Changing on existing DB requires manual vec0 table drop + recreate.
EMBEDDING_BASE_URLprovider-nativeURL ending in /embeddingsOptional override for self-hosted gateways or proxies.
SQLITE_VEC_EXTENSION_PATHbundled sqlite-vec npm binaryabsolute filesystem pathExplicit path to the sqlite-vec loadable extension. Operators only set this when self-managing the binary (custom build, system path, Docker layer that strips node_modules). Failure to load is non-fatal — hybrid path silently degrades to BM25.

When the provider is enabled but transient API failures happen, rows land at embedding_state='failed'. minara doctor (Phase E1) exposes the per-state counts so operators can spot heaps; minara doctor --fix --apply (Phase E2) backfills via MemoryStore.backfillEmbeddings(). Both failed and pending are eligible for backfill — the same row may cycle until a successful embed lands it at embedded. Rows shorter than 10 chars or that trip the methodology injection scanner land at skipped and never embed.

Backtesting feedback loop (Sprint 6 — online outcome filler)

Periodic job that closes the learning loop for trades executed in-session. Computes a post-trade outcome ("+5.20% in 24h"), feeds it to EvaluationLoop.evaluatePacked, and (when LEARNING_RECORD_USAGE=true) calls MethodologyStore.recordUsage(id, was_correct) for every methodology consulted during the originating turn.

Does not replay historical trades. Only evaluates rows already in trade_history. Entirely dark by default (BACKTEST_ENABLED=false).

VariableDefaultFormatEffect
BACKTEST_ENABLEDfalsetrue|falseMaster switch. false = runner + scheduler never registered (zero runtime cost). true = scheduler fires every BACKTEST_CRON_HOURS hours.
BACKTEST_DRY_RUNfalsetrue|falseDry-run: compute outcomes, emit to shadow_runs(facet='backtest_outcome'), but skip updateTradeOutcome + recordUsage. First-week rollout protocol.
BACKTEST_MIN_TRADE_AGE_MS86400000 (24h)positive integer (ms)Minimum age before a trade is eligible. Threaded into ReviewEngine.minTradeAgeForEvalMs.
BACKTEST_OUTCOME_HORIZON_HOURS24positive integer (h)Hours after created_at the outcome price is sampled at. Rendered in the outcome string so the evaluator sees window length.
BACKTEST_BATCH_LIMIT20positive integerMax pending rows pulled per fire. Threaded into ReviewEngine.maxEvalsPerBatch.
BACKTEST_CRON_HOURS24positive numberScheduler interval. Uses setInterval().unref(). Values below 1 minute are clamped up.
BACKTEST_PRICE_PROVIDERautoauto|hyperliquid|yahooForce a single historical price source. auto routes crypto → Hyperliquid → Yahoo -USD; stock/unknown → Yahoo; stablecoin → 1.0.
BACKTEST_MAX_COST_USD_PER_RUN2.00non-negative floatPer-run cost cap. Runner snapshots BudgetTracker.getDailySpend("learning") before/after; stops with status=stopped_budget if exceeded. 0 disables. Stacks with BudgetTracker's daily/monthly caps.
LEARNING_RECORD_USAGEfalsetrue|falseWhen true, EvaluationLoop calls recordUsage(id, ok) for every id in trade_history.methodology_ids. This is the actual Wilson-LB feedback loop — closes the learning cycle. Flip last after shadow rows look clean.

Rollout protocol (plan dapper-coalescing-shell §Sprint 6):

  1. Set BACKTEST_ENABLED=true + BACKTEST_DRY_RUN=true for one cron cycle. Inspect shadow_runs WHERE facet='backtest_outcome' — check that outcome strings match ±N.NN% in Xh and skip reasons are sensible.
  2. Flip BACKTEST_DRY_RUN=false. The runner now writes back trade_history.outcome + state, and new methodologies may be created by evaluatePacked. recordUsage is still a no-op.
  3. Flip LEARNING_RECORD_USAGE=true. The Wilson counters begin to update on every evaluation. Monitor methodologies.times_used + times_correct delta per day.

Personalization rebuild (M3.2 event-driven thresholds)

Minara learns your trading preferences from every conversation via the PersonalizationRebuilder. Pre-M3.2 the rebuilder ran every 10 minutes on a cooldown. M3.2 makes it event-driven + threshold-gated: data writes (trades, memories, chat turns) emit events that wake the rebuilder, and each rebuild must clear two independent gates — a minimum new-input count and a minimum elapsed time since the last rebuild. Neither gate alone triggers a rebuild; both must be true.

The 60-minute safety-net scheduler is still there to cover lost events (e.g. process restarts between a write and its subscriber), but on a quiet system it produces zero LLM calls and zero info logs.

Trading summary thresholds

VariableDefaultFormatEffect
FIN_PROFILE_TRADING_SUMMARY_MIN_NEW_TRADES3positive intMin new trades since last rebuild before gate 2 allows a trading-summary rebuild.
FIN_PROFILE_TRADING_SUMMARY_MIN_INTERVAL_MIN30positive int, minutesMin elapsed time since last rebuild.
FIN_PROFILE_TRADING_SUMMARY_MAX_TRADES100positive intUpper bound on trades fed to the LLM on cold-start / force-regen.
FIN_PROFILE_TRADING_SUMMARY_INCREMENTAL_MAX_TRADES50positive intUpper bound on NEW trades fed to the LLM when an existing summary is merged into.

Raise the thresholds if your trading frequency causes the summary to churn too often; lower them if you want the summary to converge as fast as possible during backtests or onboarding.

Memory extraction thresholds

VariableDefaultFormatEffect
FIN_PROFILE_MEMORIES_MIN_NEW_TURNS5positive intMin new chat turns recorded since last extraction before rebuildMemories runs.
FIN_PROFILE_MEMORIES_MIN_INTERVAL_MIN10positive int, minutesMin elapsed time since last memory extraction.
MEMORY_CONSOLIDATION_ENABLEDfalse1/true/yes/on enablesOpt into contradiction resolution for chat-extracted facts. Off by default (each fact is appended, ADD-only). When on, one extra background LLM call per rebuild decides whether each fact duplicates, refines, or supersedes an existing fact; superseded facts are soft-deleted (recoverable) and every decision is logged to the memory_consolidation_events audit table.
MEMORY_CONSOLIDATION_GUIDANCEemptyfree text, max 500 charsOptional non-authoritative steer for how consolidation merges or retires facts (for example "prefer my most recent statement"). It never overrides the hard safety rules: a user-set hard constraint is never dropped except by an explicit newer user statement, and a user-stated fact always outranks an assistant-inferred one. No effect when consolidation is off.

rebuildMemories scans chat_turns rows written since the cursor last_indexed_chat_id and extracts discrete facts. By default it uses an ADD-only prompt (no UPDATE/DELETE), so contradictions are appended alongside prior facts and resolved at retrieve time. When MEMORY_CONSOLIDATION_ENABLED is on, a follow-up resolver pass instead decides ADD / UPDATE / SUPERSEDE per fact, so stale and contradictory rows are tidied at write time (see Minara Memory). Each extracted fact is written as a row in the shared memories table with fact_type, attributed_to, entities_json, and linked_memory_ids metadata.

Shared knobs

VariableDefaultFormatEffect
FIN_PROFILE_EVENT_DEBOUNCE_SEC30positive int, secondsDebounce window for scheduleCheck(dim). Collapses rapid bursts into one gate-checked rebuild.
CHAT_TURN_RECORDING1 (on)0/false/no/off disablesPersists (user_message, final_response, tool_calls) per turn into chat_turns. Required for rebuildMemories to have input.

Operational tip: on a brand-new deployment, set FIN_PROFILE_TRADING_SUMMARY_MIN_NEW_TRADES=1 and FIN_PROFILE_MEMORIES_MIN_NEW_TURNS=2 for the first day so the rebuilder warms up quickly. Restore defaults after the profile stabilizes.

Off-agent history mirror

The personalization rebuilder now consumes three sources: local trade_history (agent in-session), perps_fills (cross-sub mirror of Minara's /v1/perp-wallets/fills), and external_spot_activities (Minara's /v1/tx/cross-chain/activities). The knobs below shape how MinaraHistorySync keeps itself up to date and how much of the mirror the LLM rebuild reads.

VariableDefaultFormatEffect
FIN_PROFILE_HISTORY_SYNC_WINDOW_DAYS90positive int, daysRolling sync window. Anything older is never pulled.
FIN_PROFILE_HISTORY_SYNC_MIN_INTERVAL_MIN5positive int, minutesThrottle floor between successive sync triggers. Multiple scheduleSync() calls inside the window collapse.
FIN_PROFILE_HISTORY_SYNC_TIMEOUT_SEC8positive int, secondsHard timeout per syncAll() call. Wired through AbortController so in-flight HTTP requests are cancelled.
FIN_PROFILE_HISTORY_SYNC_PAGE_HINT500positive int, rowsUpstream "page might be full" heuristic for the perps-fills endpoint. When getPerpSubAccountFills returns ≥ this, the syncer slides startTime forward and asks again.
FIN_PROFILE_HISTORY_SYNC_OVERLAP_SEC60positive int, secondsOverlap when sliding startTime forward on a probably-truncated page. fill_uid dedup makes overlap harmless.
FIN_PROFILE_HISTORY_SYNC_MAX_ROUNDS_PER_SUB10positive intHard upper bound on the truncation-rolling loop per sub-account.
FIN_PROFILE_HISTORY_SYNC_MAX_FAILURES5positive intPer-(source, sub_account_id) consecutive failure threshold. At/above, that key is skipped during normal scheduling.
FIN_PROFILE_HISTORY_SYNC_FAILURE_COOLDOWN_MIN30positive int, minutesAfter a key hits MAX_FAILURES, the next probe attempt is gated on this cooldown. Probe success resets the counter; failure increments it. Prevents permanent freeze.
FIN_PROFILE_HISTORY_SYNC_SPOT_MAX_PAGES20positive intHard cap on the spot pagination loop.
FIN_PROFILE_HISTORY_SYNC_SPOT_PAGE_SIZE100positive intSpot pagination batch size. Forwarded to Minara as limit.
FIN_PROFILE_TRADING_SUMMARY_PERPS_RECENT_FILLS30positive intHow many of the newest perps fills the LLM rebuild reads. Aggregate is always sent in full.
FIN_PROFILE_TRADING_SUMMARY_SPOT_RECENT_ACTIVITIES20positive intSame for spot.
FIN_PROFILE_TRADING_SUMMARY_AGGREGATE_WINDOW_DAYS90positive int, daysAggregation window for per-symbol / per-pair rollups fed to the LLM.
FIN_PROFILE_MEMORY_SOFT_DELETE_RETENTION_DAYS30positive int, daysHow long soft-deleted memories stay recoverable. Beyond this, the 30-min purge cron physically removes them.

Sync trigger model. Three paths cause MinaraHistorySync.scheduleSync() to fire: (1) any trade:recorded event (the agent just executed a trade — the user likely also did something on web/mobile), (2) the PersonalizationRefreshTask's hourly safety-net tick (catch missed events), and (3) the explicit /refresh-personalization --force / POST /v1/profile/refresh path. The 5-minute throttle dedupes these calls so the API is never hit more than once per window.

Watermark granularity. minara_history_sync_state is keyed by (source, sub_account_id). A single sub-account's transient failure neither pollutes its peers' watermarks nor the global spot cursor.

Hyperliquid perps

MINARA_HL_DEX_DISCOVERY controls how widely Minara discovers Hyperliquid named perp DEXes when building perps snapshots and syncing trade history.

VariableDefaultFormatEffect
MINARA_HL_DEX_DISCOVERYoff1/true/yes/on to enableOpts the perps snapshot and history sync in to live Hyperliquid perpDexs discovery. Default behavior queries only the two dexes the current user cohort holds positions on ("" default + "xyz" stocks/commodities), keeping the per-sweep fan-out at 4 subs × 2 dexes × 2 calls = 16 HL requests, which fits inside HL's per-IP rate limit. When enabled, the snapshot also calls perpDexs (cached for 10 min) and fans out across every named dex HL exposes today (xyz, flx, vntl, hyna, km, abcd, cash, para — ~9 total). For the typical 4-sub user that pushes the budget to ~72 requests per sweep and reliably 429s the public /info endpoint. Set this only if you actually hold positions on a dex outside the default pair.

Strategy Studio (BETA)

Two operator knobs control the local strategy-codegen subagent and the default authoring path picked by the minara.strategy-studio skill plus the /strategy-new REPL command. Both default to sensible values, so leaving them unset is fine.

VariableDefaultFormatEffect
MINARA_SS_CODEGEN_MAX_ITER3positive integer, clamped to [1, 10]Step budget for the local codegen subagent's "generate code → ephemeral backtest → refine" loop per single minara_ss_local_generate tool call. The subagent runs to this many steps; the model decides when to backtest and when to stop within that budget. On return, success is recomputed from the final backtest (status COMPLETED + non-zero trades + drawdown < 0.95). Operators raise this when running a fast / cheap llmClient (e.g. Haiku) and want better convergence; lower (1–2) when using a slow / expensive model. Consumed by runStrategyCodeSubagent in apps/agent/src/core/strategy-code-subagent.ts.

Institution Mode (multi-agent firm simulation)

The minara_institution_analyze tool convenes a 6-phase pipeline (4 analysts in parallel → bull/bear research debate → research manager → trader → 3-way risk debate → portfolio manager) for high-stakes single-asset analysis. Modeled after TradingAgents; default values for round counts are anchored verbatim to that project's default_config.py, while timeouts / token caps follow Minara's deep_research and agent-loop conventions.

All variables below are agent-loop infrastructure — no MINARA_ prefix per the project convention. Leaving them all unset gives the TA-anchored defaults.

VariableDefaultFormatEffect
INSTITUTION_MAX_DEBATE_ROUNDS1integer, clamped to [1, 5]Bull vs Bear alternating rounds in Phase 2. 1 round = 2 turns (one bull + one bear). Matches TradingAgents max_debate_rounds: 1. Bump to 2 when the asset warrants extra adversarial scrutiny; leave at 1 for routine analyses. Consumed by runInstitution in apps/agent/src/tools/institution/orchestrator.ts.
INSTITUTION_MAX_RISK_ROUNDS1integer, clamped to [1, 5]Aggressive → Conservative → Neutral rotation rounds in Phase 5. 1 round = 3 turns. Matches TradingAgents max_risk_discuss_rounds: 1.
INSTITUTION_WALL_CLOCK_TIMEOUT_MS1200000integer milliseconds, clamped to [60000, 1800000]Hard upper bound on a single minara_institution_analyze call. When exceeded, the orchestrator short-circuits and returns whatever phases completed plus meta.truncated: true. The per-LLM-call timeout is governed by INSTITUTION_PER_CALL_TIMEOUT_MS and clamped to never exceed this wall clock. Bumped from 600 s when the per-call cap moved from 60 s to 300 s — operators wanting the old fast-fail behaviour set both knobs back.
INSTITUTION_PER_CALL_TIMEOUT_MS300000integer milliseconds, clamped to [5000, INSTITUTION_WALL_CLOCK_TIMEOUT_MS]Per-LLM-call timeout for every sub-agent call (analysts, debaters, managers, structured retries). Analysts that need to dispatch 2-3 data tools and write a structured AnalystReport in a single sub-agent loop routinely take longer than a 60 s cap; the previous wallClock / 10 derivation aborted them mid-write. Lower (e.g. 60000) to fail-fast on a quick-model deployment; raise (e.g. 600000) for a slow deep model alongside a matching wall-clock bump.
INSTITUTION_MAX_OUTPUT_TOKENS_PER_TURN4096integer, clamped to [1024, 16384]max_tokens ceiling applied to every LLM call in the pipeline (analysts, debaters, managers, structured retries). Cost ceiling, not behavioral. Matches the agent loop's default max_tokens. Raise when the Portfolio Manager keeps truncating its executive_summary.

Single-model policy. Every role in the pipeline (analysts, debate participants, Research Manager, Trader, risk personas, Portfolio Manager, Allocator) runs on the operator's selected agent model — same provider/model the rest of the agent uses. There is no per-role override. To run synthesis on a stronger model, switch the agent's default model for the whole session.

Cost note: a single institution run is roughly 25 LLM calls × 4 K output tokens ≈ 10⁵ output tokens — about $1–3 on Sonnet-class models at default settings. Document this prominently if exposing the tool to end users.

Hardcoded constants (NOT operator-tunable env vars in v1):

  • Phase-1 analyst concurrency = 4 (the analyst count; Promise.all over the four slots, no semaphore library).
  • Subagent inner-loop turn cap = 5 turns. Matches Minara's strategy-code-subagent discipline; analysts only need 1–3 tool calls before producing their report.
  • Per-LLM-call timeout = wallClock / 10, with a 5-second floor.
  • Output language = inherits from existing user-message language detection (the same pattern deep_research uses); internal debate turns stay in English regardless, only the final report localizes.

If real operations need to tune any of the hardcoded constants, lift them to env vars in a follow-up PR.

Self-learning + Phase B reflection (v2 PR 1 / PR 2)

The v2 self-learning track persists every institution run into three SQLite tables (institution_runs, institution_role_outputs, institution_reflections), runs a multi-window Phase B reflection ladder (1d/7d/30d/90d/180d/365d) on still-open positions, and writes a "lazy" reflection on stale prior runs before each new institution call (Phase 0 retrospective refresh). All variables below are agent-loop infrastructure; no MINARA_ prefix per project convention.

VariableDefaultFormatEffect
INSTITUTION_LEARNING_ENABLEDonon | offMaster switch for v2 persistence. When off, the capture hook no-ops and the institution_* tables stay empty. Production leaves on; the persisted rows feed Phase B reflection + methodology graduation downstream. Consumed by captureInstitutionRun in apps/agent/src/learning/institution/capture-hook.ts.
INSTITUTION_RETROSPECT_ENABLEDonon | offPhase 0 lazy-refresh master switch. When on, every new institution call walks recent runs on the same (ticker, asset_class) and writes a lazy_refresh reflection on any whose latest reflection is stale. The standard daily cron continues regardless.
INSTITUTION_RETROSPECT_LIMIT10integer, clamped to [1, 50]Phase 0 history depth. Maximum prior runs queried per (ticker, asset_class). Lower (3-5) for chatty multi-ticker users to bound per-call latency; raise (20+) when reflective context is more valuable than freshness.
INSTITUTION_RETROSPECT_TIMEOUT_MS120000positive integer msPhase 0 wall-clock cap. Effective timeout is min(this var, INSTITUTION_WALL_CLOCK_TIMEOUT_MS / 2) with a 5s floor — guarantees the orchestrator (Phase 1-6) still has at least 50% of its declared wall-clock budget when Phase 1 starts.
INSTITUTION_LAZY_REFRESH_STALE_HOURS24integer, clamped to [1, 168]A prior run's most-recent reflection is treated as stale (lazy-refresh-eligible) when its evaluated_at is older than this many hours.
INSTITUTION_LAZY_REFRESH_DEDUPE_HOURS6integer, clamped to [1, 48]Minimum hours between consecutive lazy_refresh writes on the same run. Prevents /institution BTC called repeatedly in 10 minutes from generating redundant reflections.
INSTITUTION_AUTO_STALE_DAYS90integer, clamped to [30, 365]An open run is promoted to auto_stale when its age exceeds this many days without being finalized. Auto-stale runs continue to receive Phase B reflections but get downweighted in PM past_context injection.
INSTITUTION_BENCHMARK_CRYPTOBTCtickerAlpha benchmark for crypto asset classes. Phase B reflections compute alpha = raw_return - benchmark_return. Set to whatever the configured price source can resolve.
INSTITUTION_BENCHMARK_STOCKSPYtickerAlpha benchmark for stocks / indices.
INSTITUTION_BENCHMARK_FOREXDXYtickerAlpha benchmark for forex pairs. Commodity / stablecoin / unknown have no benchmark — alpha is recorded as null for those classes.

Analyst recovery + canonical-asset preflight

Each Phase-1 analyst slot runs the model in a free tool-calling loop, then runs a single synthesis turn that asks for a structured prose summary in HEADLINE / KEY FINDINGS / CONFIDENCE format. The orchestrator parses the prose directly into an AnalystReport — no forced-submit toolChoice step, no retry harness. When the synthesis is empty or unparseable, the orchestrator falls back to buildSubagentSummaryReport, which always emits a usable report (either citing the captured tool data when tools succeeded, or falling back to the analyst's role-domain default reasoning when they didn't). Downstream phases always receive a usable summary; the legacy data_gap flag has been retired, and there are no operator-tunable retry budgets — the contract is "always produce something usable" and there is no budget to tune.

Before Phase 1 dispatches, an unknown ticker triggers a server-side canonical-identity preflight: parallel CoinGecko / CMC / DexScreener lookups normalised to a chain+contract (or chain+native) identity, persisted in a SQLite cache with per-outcome TTL.

VariableDefaultTypeDescription
INSTITUTION_FORCE_RESOLVER_PREFLIGHTfalsetrue | falseRun the canonical preflight for every ticker, not just classifyAsset === "unknown". Useful for ops validation; small latency cost (~50ms cache hit, ~200-500ms cache miss).
CANONICAL_ASSET_CACHE_TTL_RESOLVED_DAYS30positive integer daysTTL for outcome: "resolved" (single canonical chain+contract or native+chain).
CANONICAL_ASSET_CACHE_TTL_MULTI_DAYS14positive integer daysTTL for outcome: "multi" (multi-chain deployment, e.g. USDC on 20 chains). Shorter than resolved because user disambiguation may pin to a specific chain.
CANONICAL_ASSET_CACHE_TTL_AMBIGUOUS_DAYS7positive integer daysTTL for outcome: "ambiguous" (provider results disagree). Short so we re-resolve after provider data converges.
CANONICAL_ASSET_CACHE_TTL_NONE_DAYS1positive integer daysTTL for outcome: "none". Very short — providers update daily, so we want a fresh attempt rather than caching a negative.
CANONICAL_ASSET_CACHE_TTL_USER_DAYS365positive integer daysTTL for user-supplied entries (operator override via minara assets pin or banner CTA). Pin entries supersede provider resolution.
CANONICAL_ASSET_CACHE_FALLBACK_TO_EXPIREDtruetrue | falseWhen the live aggregator fails (providers down or rate-limited), return the expired cache entry tagged from_expired_fallback: true rather than reporting outcome: "none".

Direction-aware scoring. Phase B records raw_return flipped for short positions and zeroed for flat decisions (a profitable short on a falling underlying records a positive return, not a negative one). Long / unspecified directions pass through unchanged. See reflect.ts applyDirection().

Retryable data outages. A standard window evaluation that fails to fetch prices writes a data_unavailable placeholder; on the next cron pass the placeholder is UPSERT'd to ok once prices return. nextDueStandardWindow filters by status='ok' so the window is re-evaluated until it succeeds.

Offline learning-config tuning (Phase B gate)

The LEARNING_CONFIG hyperparameters in apps/agent/src/learning/methodology-store.ts are currently hand-picked. A future Bayesian-optimization harness will tune them against Sprint 6's accumulated P&L attribution data; the code lives in apps/agent/src/learning/replay/ and tools/tuning/ once built.

The harness is offline-only — it runs via CLI / Python subprocess, never as part of the request path. Until data accumulates to the readiness gate (≥100 evaluated trades, ≥20 unique methodology hits — see minara learning stats), the whole path stays disabled.

VariableDefaultFormatEffect
LEARNING_TUNING_ENABLEDfalsetrue|falseMaster gate for the tuning harness. minara learning replay short-circuits to { skipped: "tuning_disabled" } unless this is true. minara learning stats ignores this gate (pure readonly SQL). Leave false in prod until a human operator explicitly starts a tuning session.

Full roadmap + objective-function design in apps/agent/docs-src/bo-learning-config-tuning.md.

Methodology self-optimization loop (Phases 1-7)

End-to-end pipeline for capturing investment-advice decisions, backtesting them at multiple horizons, and learning per-asset-class methodology thresholds via Bayesian optimization. All sub-systems are dark-landed — every env var defaults OFF and activating any one is a deliberate operator action.

Phase 1 — Decision capture

Turn-end hook runs an independent Haiku-class summarizer that extracts {asset, decision, confidence, quoted_price} from advice-flavored turns and persists to decision_history. Fire-and-forget by default; zero user-visible latency change.

VariableDefaultFormatEffect
DECISION_CAPTURE_ENABLEDfalsetrue|falseMaster switch. false = hook returns immediately without pre-filter or LLM call.
DECISION_SUMMARIZER_MODELclaude-haiku-4-5-20251001model idWhich model extracts the decision JSON. Bump to Sonnet if coverage rate < 70%.
DECISION_SUMMARIZER_TIMEOUT_MS15000positive integerPer-call timeout. Timeout drops the row with a warn; no retry.
DECISION_CAPTURE_SYNC_MODEfalsetrue|falseAwait summarizer before returning from the hook. Only for deterministic tests — adds turn latency.
DECISION_CAPTURE_HEURISTIC_ENABLEDtruetrue|falseTier 2: capture turns whose response contains BUY/SELL keywords + asset ticker even without an advice scenario active.
DECISION_CAPTURE_UNIVERSAL_SCANfalsetrue|falseTier 3: invoke summarizer on every turn. Diagnostic use only — ~4× summarizer spend.

Phase 2 — Multi-horizon backtest + hallucination check

Cron runner fills decision_outcomes with 1d/3d/1w/1m returns. Before filling outcomes, it compares agent_quoted_price against the real historical price; if the deviation exceeds HALLUCINATION_MAX_PRICE_DELTA_PCT the decision is flagged and excluded from downstream learning.

VariableDefaultFormatEffect
DECISION_BACKTEST_ENABLEDfalsetrue|falseMaster switch. Off = runner never constructed, no cron timer.
DECISION_BACKTEST_DRY_RUNfalsetrue|falseCompute outcomes but route writes to shadow_runs(facet='decision_outcome') instead of decision_outcomes/decision_history. Week-1 rollout.
DECISION_BACKTEST_HORIZONS1d,3d,1w,1mCSVHorizon list. Each maps to one row per decision. Max horizon determines when pending decisions become eligible.
DECISION_BACKTEST_CRON_HOURS24positive numberRunner invocation interval.
DECISION_BACKTEST_MAX_AGE_DAYS60positive integerPending decisions older than this are skipped (backlog defense).
HALLUCINATION_MAX_PRICE_DELTA_PCT0.05decimal|quoted − real| / real threshold beyond which the decision is flagged.

Phase 3 — Reward

Reward function collapses the 4-horizon return vector into a single scalar per decision. BUY rewards upward moves, SELL rewards downward moves, HOLD rewards neutrality (|return| ≤ threshold).

VariableDefaultFormatEffect
DECISION_HORIZON_WEIGHTS_JSON{"1d":0.15,"3d":0.25,"1w":0.35,"1m":0.25}JSON objectPer-horizon weights in the weighted mean. Missing labels get 0 weight.
DECISION_HOLD_NEUTRALITY_THRESHOLD0.02decimal|return| below this counts as a HOLD win. Beyond this, HOLD gets negative reward (opportunity cost).

Phase 4 / 7 — Methodology instance dispatch

Per-(template, asset_class) threshold overrides. Phase 4 ships the scaffolding; Phase 7 flips the prompt-builder to prefer instance overrides. off keeps pre-Phase-4 behavior exactly.

Instance dispatch is default-on: the prompt-builder merges BO-tuned instance overrides onto template defaults when available. Revert to template-only resolution via the DISABLE_METHODOLOGY_INSTANCE_DISPATCH=1 off switch (documented in the agent-loop section above).

Phase 6 — BO tuning cycle

Python harness (in tools/tuning/) runs bayesian-optimization per eligible bucket, driven by a TS cycle orchestrator that evaluates 6 eligibility gates + asset-class profiles.

VariableDefaultFormatEffect
METHODOLOGY_INSTANCE_TUNING_ENABLEDfalsetrue|falseMaster switch for the BO cycle.
METHODOLOGY_TUNING_CRON_DAYS7positive integerDays between cycle invocations.
METHODOLOGY_TUNING_MAX_BUCKETS_PER_CYCLE10positive integerTop-N buckets (by tunability_score) processed per cycle.
METHODOLOGY_TUNING_PROFILES_PATH$MINARA_DATA_DIR/methodology-tuning-profiles.jsonfilesystem pathOverride JSON for asset-class profiles. Per-class shallow merge; null excludes.
METHODOLOGY_TUNING_MIN_DECISIONS_GLOBALpositive integerEmergency floor applied to every profile's min_decisions (takes max).
METHODOLOGY_TUNING_MIN_IMPROVEMENT_REL0.05decimalPost-BO check #1 — test-split mean reward must exceed baseline by this ratio.
METHODOLOGY_TUNING_MAX_SENSITIVITY_DROP_10PCT0.5decimal (0, 1]Post-BO check #2 — reject if ±10% neighbor score drops by more than this.
METHODOLOGY_TUNING_PARAM_BOUND_REL0.5decimalBO pbounds half-width, as a fraction of the template default.
METHODOLOGY_TUNING_MIN_CAPTURE_CONFIDENCE0.3decimal [0, 1]BO replay only considers decisions with capture_confidence ≥ this.

Rollout order:

  1. DECISION_CAPTURE_ENABLED=true — start recording advice turns.
  2. Wait ~30 days for data.
  3. DECISION_BACKTEST_ENABLED=true + DECISION_BACKTEST_DRY_RUN=true — shadow mode 1 week.
  4. DECISION_BACKTEST_DRY_RUN=false — live backtest writes.
  5. Check minara learning stats reports READY_FOR_BO=true.
  6. METHODOLOGY_INSTANCE_TUNING_ENABLED=true — activate BO cycle.
  7. Instance dispatch is on by default; keep DISABLE_METHODOLOGY_INSTANCE_DISPATCH unset.

Builtin tools

Every row is optional. Missing var disables the feature silently.

VariableEffect
TAVILY_API_KEYGates web_search + web_extract.
FIRECRAWL_API_KEYGates web_search + web_extract; takes priority over Tavily in both chains.
FAL_KEYEnables Fal.ai image / video models.
MESSAGING_DEFAULT_PROVIDERProvider id used when send_message omits provider (e.g. telegram, slack). Optional — first configured provider wins when unset.
MESSAGING_MAX_ATTACHMENT_BYTESPer-attachment size cap for send_message({attachments}) calls. Default: 52 428 800 (50 MB). Provider APIs enforce their own maxima independently.
TELEGRAM_BOT_TOKENTelegram outbound messaging. Pair with TELEGRAM_CHAT_ID. Supports streaming edits.
TELEGRAM_CHAT_IDNumeric chat id for Telegram.
TELEGRAM_RICH_TEXTRender Telegram replies as rich text (HTML, with a MarkdownV2 then plain-text fallback). Default on; set false for plain text.
SLACK_WEBHOOK_URLSlack Incoming Webhook URL. Simplest Slack path; no streaming.
SLACK_BOT_TOKENSlack bot token (xoxb-...). Enables streaming edits via chat.update. Pair with SLACK_CHANNEL_ID.
SLACK_CHANNEL_IDDefault Slack channel id (e.g. C0123ABC). Required with SLACK_BOT_TOKEN.
SLACK_APP_TOKENApp-level token (xapp-…) enabling Socket Mode inbound. The agent connects out to Slack over a WebSocket and receives Events API messages with no public Request URL. Pair with SLACK_BOT_TOKEN.
DISCORD_BOT_TOKENDiscord bot token. Streaming edits via PATCH /channels/{}/messages/{}. Pair with DISCORD_CHANNEL_ID.
DISCORD_CHANNEL_IDNumeric Discord channel id.
HASS_URLHome Assistant base URL (e.g. https://hass.local:8123).
HASS_TOKENHome Assistant long-lived access token.
HASS_NOTIFY_SERVICEHA notify service id (e.g. mobile_app_you, with or without notify. prefix). One-shot, no streaming.
SMTP_HOSTOutbound SMTP host (e.g. smtp.gmail.com). Required for email provider.
SMTP_PORTSMTP port (587 STARTTLS, 465 SSL).
SMTP_USERSMTP auth username (optional for relays without auth).
SMTP_PASSWORDSMTP auth password / app password (masked).
EMAIL_FROM"From" address used in outbound mail.
EMAIL_TODefault recipient when send_message omits channel.
GOOGLE_OAUTH_CLIENT_IDGoogle OAuth client ID for the one-click "Email (Gmail)" connector (email-gmail provider). Can also be entered in Settings → Messaging.
GOOGLE_OAUTH_CLIENT_SECRETGoogle OAuth client secret for the Gmail connector (masked).
GMAIL_REFRESH_TOKENSet by the Connect-Gmail flow. Long-lived token used to send via the Gmail API (gmail.send scope only; never reads mail).
GMAIL_SENDER_EMAILSet by the Connect-Gmail flow. The authorized mailbox messages are sent from.
GMAIL_TOOptional recipient for the email-gmail provider. Defaults to the connected inbox (push-to-self).
WHATSAPP_ACCESS_TOKENMeta Cloud API Bearer token. Enables whatsapp provider.
WHATSAPP_PHONE_NUMBER_IDNumeric phone number id from the Meta Developer app.
WHATSAPP_RECIPIENTDefault E.164 recipient (+12025551234).
SIGNAL_CLI_NUMBERYour registered Signal sender number (E.164). Requires signal-cli on PATH.
SIGNAL_RECIPIENTDefault E.164 recipient.
SIGNAL_CLI_BINARYOverride path to the signal-cli binary. Default: PATH lookup for signal-cli.
TELEGRAM_WEBHOOK_SECRETShared secret for Telegram inbound webhooks. /webhooks/telegram returns 404 when unset.
SLACK_SIGNING_SECRETSlack app signing secret for inbound webhook HMAC verification. /webhooks/slack returns 404 when unset.
DISCORD_APPLICATION_PUBLIC_KEYDiscord Ed25519 application public key (hex). /webhooks/discord returns 404 when unset.
WHATSAPP_APP_SECRETMeta app secret for WhatsApp Cloud API inbound HMAC-SHA256 over X-Hub-Signature-256. POST /webhooks/whatsapp returns 404 when unset.
WHATSAPP_VERIFY_TOKENWhatsApp Cloud API hub.verify_token echoed on the one-time GET handshake. GET /webhooks/whatsapp returns 404 when unset.
MESSAGING_INBOUND_TRANSCRIBEEnable voice transcription (via OpenAI Whisper) on inbound voice attachments. Requires OPENAI_API_KEY.
MESSAGING_VOICE_REPLYAnswer inbound voice notes with a spoken reply attachment in addition to the streamed text. Requires a voice provider (ELEVENLABS_API_KEY or OPENAI_API_KEY).
ELEVENLABS_API_KEYElevenLabs key. When set, ElevenLabs is the preferred provider for speech synthesis and transcription (lower latency); OpenAI stays as fallback.
VOICE_TTS_PROVIDERPin the speech-synthesis vendor: auto (default) / elevenlabs / openai.
VOICE_TTS_VOICEVoice id for read-aloud replies, native to the primary TTS provider. Empty = provider default.
VOICE_TTS_MODELTTS model. ElevenLabs: eleven_v3 (default, most human), eleven_multilingual_v2, eleven_turbo_v2_5, eleven_flash_v2_5. OpenAI default gpt-4o-mini-tts.
VOICE_TTS_STABILITY / VOICE_TTS_SIMILARITY_BOOST / VOICE_TTS_STYLE / VOICE_TTS_SPEAKER_BOOST / VOICE_TTS_SPEEDElevenLabs delivery defaults (0-1 except speed 0.7-1.2, speaker boost 1/0). The Settings -> Voice models sliders override these per user.
VOICE_TTS_FAST_FIRST1 (default) / 0. Reads the first sentence of each reply on the fastest model so speech starts sooner; later sentences keep the chosen model.
VOICE_STT_PROVIDERPin the speech-to-text vendor: auto (default) / elevenlabs / openai.
VOICE_STT_MODELSTT model override for the primary provider. Empty = scribe_v1 / gpt-4o-mini-transcribe.
VOICE_FFMPEG_PATHOptional ffmpeg path for voice transcodes (AMR inbound voice on WeCom/WeChat OA; AMR voice replies on WeCom). Default: PATH lookup; absent ffmpeg degrades gracefully.
TWITTERAPI_API_KEYThird-party Twitter scraper. Gates research.social.twitter.
X_API_BEARER_TOKENOfficial X API v2. Gates x.api skill.
GLASSNODE_API_KEYGlassnode on-chain metrics. Gates research.onchain.glassnode.
QDRANT_URLVector KB endpoint. Gates the research.knowledge_base skill AND the institution-mode kb_search tool (which also requires EMBEDDING_PROVIDER configured). When set, news / fundamentals / sentiment analysts query Qdrant BEFORE walking out to web_search — significantly cleaner inputs for the synthesis turn.
QDRANT_API_KEYOptional Qdrant Cloud auth. Sent as the api-key header on every Qdrant request when set.
KB_EMBEDDING_PROVIDERKB-only embedder override for kb_search. Use ONLY when the embedder that populated your Qdrant collections differs from EMBEDDING_PROVIDER (typical case: v1 Qdrant populated with OpenAI text-embedding-3-small while you run voyage-3 for memory). When unset, kb_search reuses the shared EMBEDDING_* family. A dimension mismatch yields a 400 from Qdrant on every call — kb_search surfaces a hint pointing at this override in that error envelope.
KB_EMBEDDING_API_KEYAuth for KB_EMBEDDING_PROVIDER. Defaults to EMBEDDING_API_KEY when unset.
KB_EMBEDDING_MODELModel id for KB_EMBEDDING_PROVIDER. Set to match the model used to populate Qdrant.
KB_EMBEDDING_DIMVector dimension. Auto-detected from common model ids; set explicitly when using a custom model.
E2B_API_KEYGates workspace.e2b cloud sandbox.
FORCE_E2BOps override: disable local sandbox, route all to E2B.

Messaging platforms (extended set)

Eleven additional IM and consumer-social platforms ship on top of the original seven (telegram, discord, slack, email, whatsapp, signal, home_assistant). Each platform has its own env-var block; missing values disable both outbound and inbound for that provider silently (the route 404s and the gateway drops out of the live map).

Detailed setup steps, inbound webhook URLs, and signature recipes live in the Messaging per-platform pages. The fastest path to wiring credentials is minara gateway add (interactive picker, lists all 18 platforms with their current configured / not-configured status).

Lark / Feishu

VariableEffect
LARK_APP_IDLark app id, cli_xxxxxxxxxxxxxxxx. Required for outbound.
LARK_APP_SECRETLark app secret. Used to mint tenant access tokens (2 h cache).
LARK_DEFAULT_CHAT_IDDefault oc_xxxxxxxxxxxxxxxx chat for outbound sends.
LARK_VERIFICATION_TOKENInbound webhook verification token. Required for inbound.
LARK_ENCRYPT_KEYInbound encryption key (optional). When set, inbound POST bodies arrive as {encrypt: ...} and are decrypted with AES-256-CBC, key = SHA256(encrypt_key), IV = first 16 bytes.
LARK_DOMAINopen.feishu.cn (mainland, default) or open.larksuite.com (international).

WeCom (企业微信)

VariableEffect
WECOM_CORP_IDCorp id from "我的企业" page.
WECOM_AGENT_IDApplication agent id (numeric).
WECOM_SECRETApplication secret. Mints access_token (2 h cache).
WECOM_DEFAULT_TOUSERDefault touser, pipe-separated user ids or @all.
WECOM_CALLBACK_TOKENInbound callback token. SHA1-signed with [token, ts, nonce, encrypt].
WECOM_CALLBACK_AES_KEY43-char EncodingAESKey for inbound AES-256-CBC decryption.

DingTalk (钉钉)

VariableEffect
DINGTALK_WEBHOOK_URLCustom robot webhook URL (https://oapi.dingtalk.com/robot/send?access_token=...).
DINGTALK_WEBHOOK_SECRETRobot signing secret (SECxxxx). Signs outbound with HMAC-SHA256 over timestamp\nsecret; same recipe verifies inbound outgoing-webhook signatures.
DINGTALK_STREAM_APP_KEYStream Mode app key (AppKey / ClientID). Enables the client-outbound Stream Mode inbound daemon, which receives bot messages over a gateway WebSocket with no public callback URL. Pair with DINGTALK_STREAM_APP_SECRET.
DINGTALK_STREAM_APP_SECRETStream Mode app secret (AppSecret / ClientSecret). Required with DINGTALK_STREAM_APP_KEY.

WeChat OA (公众号)

VariableEffect
WECHAT_OA_APP_IDOA AppID, wxxxxxxxxxxxxxxxxx.
WECHAT_OA_APP_SECRETOA AppSecret. Mints access_token (2 h cache).
WECHAT_OA_TOKENServer-config Token. SHA1-signed inbound (3-tuple on GET handshake, 4-tuple on POST).
WECHAT_OA_AES_KEY43-char EncodingAESKey for inbound AES-256-CBC decryption.
WECHAT_OA_DEFAULT_OPENIDDefault openid recipient. Customer-service messages must be within the 48 h interaction window.

QQ Bot

VariableEffect
QQ_BOT_APP_IDBot AppID (numeric).
QQ_BOT_APP_SECRETBot Secret. Doubles as the Ed25519 seed source for inbound signature verification (repeated to 32 bytes, then derived to a keypair).
QQ_BOT_TOKENBot token (legacy field, retained for compatibility).
QQ_BOT_DEFAULT_CHANNEL_IDDefault target, <kind>:<id> where kind ∈ channel / group / c2c / dm. Bare ids default to channel:. Active messages capped at 4/month.

LINE

VariableEffect
LINE_CHANNEL_ACCESS_TOKENLong-lived bearer for push API.
LINE_CHANNEL_SECRETChannel secret. Verifies X-Line-Signature HMAC-SHA256 (base64) over raw body. Optional for outbound-only setups.
LINE_DEFAULT_USER_IDDefault userId / groupId / roomId.

Mattermost

VariableEffect
MATTERMOST_URLServer URL (no trailing slash).
MATTERMOST_BOT_TOKENBot user personal access token.
MATTERMOST_DEFAULT_CHANNEL_IDDefault channel id for outbound.
MATTERMOST_OUTGOING_WEBHOOK_TOKENOutgoing-webhook token, constant-time compared against the token field on inbound bodies. Optional for outbound-only setups. Outgoing webhooks only fire in public channels with a trigger word.

Microsoft Teams

VariableEffect
TEAMS_BOT_APP_IDBot Microsoft App ID GUID. Audience for inbound JWT (multi-tenant) or audience plus tenant GUID (single-tenant).
TEAMS_BOT_APP_PASSWORDBot Microsoft App Password. Mints access tokens for outbound (1 h cache).
TEAMS_BOT_TENANT_IDcommon for multi-tenant, GUID for single-tenant. Affects inbound JWT audience check.
TEAMS_DEFAULT_CONVERSATION_IDBootstrap fallback conversation id. Production code should learn conversation ids from inbound activities.
TEAMS_DEFAULT_SERVICE_URLBootstrap fallback service URL. Defaults to https://smba.trafficmanager.net/teams. Production code learns from inbound activity.serviceUrl.

Google Chat

VariableEffect
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON_PATHPath to a service-account JSON key. Per CLAUDE.md §4, the file must live inside the data / sandbox tree.
GOOGLE_CHAT_DEFAULT_SPACE_IDDefault space resource name, spaces/AAAA1234567.
GOOGLE_CHAT_AUDIENCEMust match the Workspace console's "Authentication Audience" exactly (either GCP project number string OR endpoint URL). Wrong audience returns 401 on every inbound.

BlueBubbles (iMessage)

VariableEffect
BLUEBUBBLES_SERVER_URLPublic URL of a BlueBubbles server running on macOS (typically a tunnel).
BLUEBUBBLES_PASSWORDShared server password. Pure constant-time compare on inbound, no HMAC.
BLUEBUBBLES_DEFAULT_CHAT_GUIDDefault chat GUID, e.g. iMessage;-;+15551234567.

Matrix

VariableEffect
MATRIX_HOMESERVERHomeserver URL (e.g. https://matrix.org).
MATRIX_ACCESS_TOKENLong-lived bearer. Use the Authorization: Bearer header; the ?access_token= query form is deprecated.
MATRIX_USER_IDBot user (e.g. @bot:example.org). Used to filter self-loops in /sync.
MATRIX_DEFAULT_ROOM_IDDefault room id (!abc:example.org). The inbound daemon restricts emits to this room.
MESSAGING_MATRIX_INBOUNDWhen set to 1, app boot starts the /sync long-poll daemon. The daemon drops historical events on first run; cursor lives at <dataDir>/matrix-sync.json. No E2EE support.

Client-outbound inbound daemons

Telegram, Discord, Slack, Mattermost, QQ, DingTalk, and Lark each support a client-outbound inbound daemon: the agent connects out and holds a long connection (long-poll or WebSocket) instead of running a public webhook server. This is what makes two-way chat work on a personal machine with no public IP, no tunnel, and no third party. Each daemon auto-starts when the platform's outbound credentials are configured and no public webhook is wired for it; the switches below are an explicit tri-state override (unset = auto, 1 = force on, 0 = force off).

VariableEffect
MESSAGING_TELEGRAM_POLLINGTelegram getUpdates long-poll. Calls deleteWebhook on start; cursor at <dataDir>/telegram-updates.json. Webhook signal: TELEGRAM_WEBHOOK_SECRET.
MESSAGING_DISCORD_GATEWAYDiscord Gateway WebSocket. Also delivers normal channel / DM messages the Interactions webhook never sees. Requires the privileged Message Content intent in the Discord portal. Webhook signal: DISCORD_APPLICATION_PUBLIC_KEY.
MESSAGING_SLACK_SOCKETSlack Socket Mode. Requires SLACK_APP_TOKEN. Webhook signal: SLACK_SIGNING_SECRET.
MESSAGING_MATTERMOST_WSMattermost v4 WebSocket bot. Reaches DMs and private channels the outgoing-webhook path cannot. Webhook signal: MATTERMOST_OUTGOING_WEBHOOK_TOKEN.
MESSAGING_QQ_WSQQ v2 gateway WebSocket. No webhook-exclusive secret (the webhook reuses QQ_BOT_APP_SECRET), so the daemon is preferred; set to 0 to use the webhook.
MESSAGING_DINGTALK_STREAMDingTalk Stream Mode. Requires DINGTALK_STREAM_APP_KEY / DINGTALK_STREAM_APP_SECRET. DINGTALK_WEBHOOK_SECRET signs outbound, not inbound, so it does not suppress the daemon; set to 0 to use the webhook.
MESSAGING_LARK_WSLark / Feishu long connection via the official SDK. Webhook signal: LARK_VERIFICATION_TOKEN.

MCP integrations

VariableFormatEffect
MCP_SERVERSJSON arrayOverride default MCP server list.
DEFILLAMA_API_KEYopaque stringDefiLlama Pro API key. Gates the 10 defillama.* builtin skills (see apps/agent/src/skills/builtin/defillama/). Injected as URL path prefix on Pro-host calls by DefillamaClient at apps/agent/src/defillama/client.ts. Replaces the retired DEFILLAMA_MCP_TOKEN / https://mcp.defillama.com/mcp path. Unset: all 10 defillama.* skills hide from the catalog.
MCP_EVM_RPC_URLURLRaw EVM JSON-RPC sub-agent.
MCP_ETHERSCAN_URLURLEtherscan-style explorer (60+ EVM chains).
MCP_SOLSCAN_URLURLSolscan Solana explorer.
MCP_GOPLUS_URLURLGoPlus Web3 security analytics.

Native provider keys (PR-A → PR-G migration)

The provider-routing rollout retired the vendored coinglass / coinank / query-token-audit / meme-rush / polymarket / okx-dex-token / okx-wallet-portfolio / okx-security / okx-dex-trenches SKILL.md packages. Their capabilities now live in the native priority chains under apps/agent/src/tools/_shared/ + the analysis.derivatives / market.flows / security.token / discovery.dex / prediction.markets skills. Env vars listed below gate the native providers, not legacy SKILL.md packages.

VariableNative skill(s) it activatesHeader / usage
CMC_API_KEYpriority chain cmc provider (price / trending fallback in minara.core.crypto_kline, get_price, get_trending)X-CMC_PRO_API_KEY
CMC_PRO_API_KEYretained for the surviving vendored cmc-api-* SKILL.md packagesX-CMC_PRO_API_KEY
COINGECKO_API_KEYupgrades the coingecko-pro provider (price / kline / on-chain holders) and unlocks pro-tier endpoints in the surviving coingecko external SKILL.mdx-cg-pro-api-key or x-cg-demo-api-key
COINANK_API_KEYpriority chain coinank provider — fallback after CoinGlass for funding / OI / liq-history + the only path to derivatives_long_short_ratio (no other provider declares that capability) + flows_exchange_token alongside CoinGlassapikey
COINGLASS_API_KEYpriority chain coinglass provider — primary for funding / OI / liq-history; CoinGlass-only for derivatives_liquidation_heatmap and flows_etf_net (no other provider declares those capabilities). Does NOT serve derivatives_long_short_ratio.CG-API-KEY
BINANCE_WEB3_API_KEY + BINANCE_WEB3_SECRET_KEYpriority chain binance-web3-dex provider — preferred on-chain token data (discovery.dex meta / holders / pools) on Ethereum / BSC / Base / Solana, ranked above the coingecko on-chain tiers and okx-dex on those chains; serves wallet aggregation for queries scoped to those chains (or when the OKX quad is unset). Also gates the binance-web3 external skill.X-OC-* HMAC pair
OKX_API_KEY + OKX_SECRET_KEY + OKX_PASSPHRASE + OKX_PROJECT_IDpriority chain okx-dex provider — discovery.dex (wallet aggregation and dex tokens fallback behind binance-web3-dex), security.token on non-Binance-Web3 chains, meme.* on chains binance-web3 doesn't coverOKX HMAC quad
FMP_API_KEY11 fmp.* builtin skills (fmp.fundamentals, fmp.quotes, fmp.technicals, fmp.news, fmp.macro, fmp.analysts, fmp.calendar, fmp.etf, fmp.sectors, fmp.screener, fmp.sec)?apikey= query param (injected by apps/agent/src/fmp/client.ts)

The native prediction.markets skill is read-only (Polymarket's public Gamma + CLOB read endpoints) and needs no env. The retired external.polymarket SKILL.md used PRIVATE_KEY / RPC_URL / POLY_BUILDER_* for authenticated order placement; that write path is not yet ported and those env vars no longer have a consumer in the agent.

Surviving vendored external skills

The PR-G deletion left these vendored SKILL.md packages in place (they cover capabilities the native priority chain doesn't yet absorb):

PackagePurposeEnv
external.binanceSpot / margin / private endpoints beyond the public binance-public providernone for public; account keys for private
external.coingeckoOn-chain GeckoTerminal OHLCV + the deeper coingecko surface beyond the priority chain's free-tier coverageCOINGECKO_API_KEY
external.hyperliquidUser-account endpoints not covered by the native minara.perps_analytics skillper-user signer
external.cmc-api-* (4 packages)CoinMarketCap endpoints beyond price / trendingCMC_PRO_API_KEY
external.okx-defi-portfolio, external.okx-dex-market, external.okx-dex-signal, external.okx-dex-ws, external.okx-audit-logOKX Web3 / DeFi surfaces not in the native okx-dex providerOKX 4-var quad
external.diagram-design, external.strategy-studioUnrelated to provider routing — preserved unchangedn/a

Methodology learning loop — case attribution + synthesis (Phase 1-4)

Tuning knobs for the methodology learning loop introduced in the Phase 1-4 series (case-recorder, attribution cron, synthesis). All are optional — defaults are tuned for production. Most operators only ever touch the off switches and the synthesis thresholds.

Off switches (incident response)

VariableWhat it doesWhen to flip
DISABLE_METHODOLOGY_INJECTIONCloses the READ path — placeholder / fusion hint / methodology_lookup tool. Methodologies stop appearing in prompts and tool outputs.LLM hallucinating from a poisoned corpus; need a clean prompt to debug.
DISABLE_METHODOLOGY_CASE_RECORDINGCloses the case WRITE path only. recordHint / finalizeTurn no-op; reads continue.Case-table schema migration in flight; reads should still work.
DISABLE_METHODOLOGY_MUTATIONSCloses ALL methodology mutations: recordUsage / recordOutcome / requantize / synthesis / case-attribution / setMethodologyQuarantine. Reads stay alive.Wilson counters look corrupted; freeze everything while investigating. Broader than the other two.

Set to 1 / true / yes / on to enable. Unset / any other value leaves the path active. Hot-reads — flip without restart.

Synthesis tuning (Phase 4 D)

VariableDefaultEffect
METHODOLOGY_SYNTHESIS_FLAG_MEDIAN_BPS50Weighted-window median ≤ −0.5% (= −50 bps) triggers regime_shift_flagged. Lower = more sensitive.
METHODOLOGY_SYNTHESIS_FLAG_HIT_RATE0.45Weighted hit_rate below this triggers flag. Higher = more sensitive.
METHODOLOGY_SYNTHESIS_DEMOTE_MEDIAN_BPS200Auto-demote requires median ≤ −2% AND 30d sample size ≥ 10 AND lifetime Wilson > 0.55.
METHODOLOGY_SYNTHESIS_HALFLIFE_DAYS14Recency half-life across the 7d/30d/90d/180d windows. Lower = more reactive to recent data.
METHODOLOGY_STRESS_THRESHOLD_PCT25Black-swan circuit breaker. When > 25% of same-side methodologies trip auto-demote in one pass, suppress the cascade and apply factor=0.8 instead.

Price quality (Phase 3 F4.3)

Per-asset-class daily absolute return caps. A 7d window scales by sqrt(7). Rejects single-day FX prints, split-adjusted spikes, delisted-ticker zeros from polluting Wilson.

VariableDefaultNotes
METHODOLOGY_PRICE_CAP_MAJOR_CRYPTO0.550% daily
METHODOLOGY_PRICE_CAP_LAYER_10.5
METHODOLOGY_PRICE_CAP_LAYER_20.6L2s more volatile
METHODOLOGY_PRICE_CAP_DEFI_BLUE_CHIP0.6
METHODOLOGY_PRICE_CAP_MEME_COIN1.0Meme coins legitimately move 100%/day
METHODOLOGY_PRICE_CAP_STABLECOIN0.02Aggressive — stables shouldn't move > 2%
METHODOLOGY_PRICE_CAP_STOCK0.15
METHODOLOGY_PRICE_CAP_INDEX0.1
METHODOLOGY_PRICE_CAP_COMMODITY0.2
METHODOLOGY_PRICE_CAP_FOREX0.05

Case-recorder housekeeping (Phase 2)

VariableDefaultEffect
METHODOLOGY_HINT_ORPHAN_MS1800000 (30 min)TTL for pending hint rows before the orphan sweeper marks them orphaned. Should exceed the agent's max reasonable turn duration.

Operating the learning cron

The agent does NOT auto-schedule the learning cron. Wire one of the minara learning subcommands to a system cron / launchctl / systemd timer. Recommended daily cadence:

# Daily at 03:00 — sweep, attribute, synthesize. Idempotent so any
# missed run is replayed on the next tick.
0 3 * * *  /usr/local/bin/minara learning cron --json >> /var/log/minara-learning.log

CLI surface (Phase 4 E):

minara learning methodology explain <id>          # current state + audit
minara learning methodology lifecycle [--kind k]  # tail audit log
minara learning methodology cases [--asset c]     # tail case table
minara learning attribute                         # one 7d attribution pass
minara learning synthesize [--asset c]            # one synthesis pass
minara learning sweep-orphans                     # one orphan sweep
minara learning cron                              # all of the above

Methodology audit subsystem (passive observer)

A read-only audit subsystem layers over the learning loop and emits a composite health score. Six dimensions feed the composite: synthesis decision stability, graduation false-positive rate, attribution integrity, asset-class coverage, quarantine churn, and cron health. The subsystem never mutates learning state — its only write is one row per pass to methodology_audit_reports. An end-to-end test hashes the four learning tables before and after every audit pass to catch any regression that introduces a write.

The cron is cooperative with the agent loop. Every tick checks the shared BusyTracker. If a user turn is in flight, or the agent has been idle less than the configured threshold, the tick is deferred. A starvation guard runs the pass after N consecutive deferred ticks so a permanently busy installation doesn't lose audit coverage. Mid-pass, the orchestrator yields to the event loop between each SQL stage and pauses on the busy tracker if a turn arrives.

Default cadence is once per day. cron_health reads the methodology_cron_runs heartbeat table that src/learning/methodology-cron.ts writes at the tail of every learning tick, so a quiet attribution lane (no eligible cases) is not misread as a dead loop.

VariableDefaultEffect
METHODOLOGY_AUDIT_CRON_ENABLED0Opt-in switch for the in-process audit scheduler. Multi-worker deployments also gate on IS_PRIMARY_WORKER.
METHODOLOGY_AUDIT_CRON_INTERVAL_MS86400000 (24h)Tick interval. Clamped at runtime to [5min, 30d].
METHODOLOGY_AUDIT_WINDOW_DAYS30Lookback window for the window-bounded dimensions. Clamped to [1, 365].
METHODOLOGY_AUDIT_RETENTION_DAYS90Rows in methodology_audit_reports older than this are pruned once per local day. Clamped to [7, 3650].
METHODOLOGY_AUDIT_SKIP_BUSY_THRESHOLD_MS180000 (3min)Tick is deferred when the BusyTracker reports busy OR idle window shorter than this. Clamped to [0, 1h]. Set to 0 to disable the preflight gate.
METHODOLOGY_AUDIT_MAX_DEFERRED_TICKS4Starvation guard. After this many deferred ticks the next tick runs regardless of busy state. Clamped to [0, 100].
METHODOLOGY_AUDIT_YIELD_TIMEOUT_MS60000 (60s)Max wait at each mid-pass yield point before resuming despite busy state. Clamped to [0, 10min].
DISABLE_METHODOLOGY_AUDITunset1/true short-circuits the write path (cron + CLI audit run). Returns a band=disabled placeholder, persists nothing. Read paths (audit show/trend/findings) still work.

CLI surface:

minara learning audit run [--window-days N]  # inline single pass
minara learning audit show [--latest|--pass <id>]
minara learning audit trend [--days N]       # composite history + sparkline
minara learning audit findings [--severity high|medium|low]

Fund-confirm bypass (advanced — production caveat)

MINARA_SKIP_FUND_CONFIRM is documented in CLAUDE.md §3b. When set, fund-moving handlers skip the confirm: true requirement and execute on the first call. The case recorder mirrors this — turns executed under the bypass count as fund_moving_executed for Wilson attribution. Do NOT set in interactive or prod.

Proactive Wealth Agent

The background supervisor loop that runs each activated mandate (marks positions to market, take-profit / cut-loss / rebalance, snapshots PnL, and writes the work log), plus the slower re-discovery that hunts for fresh opportunities. All three are also editable live at Settings → Proactive (and inside the Proactive module's own settings tab); the env vars below are the boot fallback. Multi-worker deployments only run the loop on the worker with IS_PRIMARY_WORKER=1.

VariableDefaultEffect
PROACTIVE_SUPERVISOR_ENABLED1 (on)Master switch for the background supervisor. 0 pauses every activated mandate's autonomous activity; the mandates stay activated, they just stop acting until you turn it back on.
PROACTIVE_SUPERVISOR_INTERVAL_SEC900 (15min)How often each mandate checks its positions, in seconds. Clamped at runtime to [10s, 1h].
PROACTIVE_REDISCOVER_INTERVAL_SEC1800 (30min)How often a mandate hunts for fresh opportunities (the paid re-plan step), in seconds. Clamped to [1min, 24h]. Read live each cycle, so a change applies without a restart. Skipped gracefully when no model is configured.

x402 paywall pre-authorization (PR-X)

These variables bound the "pay-once-then-auto-pay" feature for HTTP 402 / x402 paywalls. The grant call itself ALWAYS goes through the §3b two-step confirm gate; subsequent payments matching an active session's scope skip the per-call user prompt and execute directly via transfer_token. Every auto-spend writes audit metadata (session id + remaining budget) to the response so the user can trace each charge.

Wired into:

  • apps/agent/src/minara/x402-preauth-store.ts — SQLite store + cap math
  • apps/agent/src/minara/x402-preauth-config.ts — env loader + cap helpers
  • apps/agent/src/tools/_shared/confirm.tsshouldAutoExecuteX402 helper
  • apps/agent/src/tools/trade.tstransfer_token consults auto-execute first
  • apps/agent/src/tools/x402-preauth.tsx402_preauth_grant / _list / _revoke
  • apps/agent/src/skills/builtin/x402-payment.ts — 4-option preview prose
  • apps/agent/src/gateway/repl-commands.ts/x402 preauth CLI surface
VariableDefaultFormatEffect
X402_PREAUTH_DEFAULT_TTL_HOURS24positive integer (hours)Default TTL applied when x402_preauth_grant is called without ttl_hours, and the label of the x402-payment skill's "preauth_default" preview option.
X402_PREAUTH_MAX_TTL_HOURS168 (7 days)positive integer (hours)Hard upper bound for non-forever TTL. Grants requesting more are rejected with field: "ttlMs".
X402_PREAUTH_ALLOW_FOREVER11/true/yes/on to allowWhen 0, ttl_hours: "forever" grants are rejected with field: "forever". The forever-budget cap is still read for documentation.
X402_PREAUTH_MAX_PER_CALL_USDC0.5positive number (USDC)Hard upper bound for a single auto-paid x402 payment. Single payments above this fall back to the regular two-step confirm even inside an active session — the safety bound that caps per-call blast radius.
X402_PREAUTH_MAX_BUDGET_USDC5positive number (USDC)Total-budget cap for non-forever sessions.
X402_PREAUTH_MAX_FOREVER_BUDGET_USDC20positive number (USDC)Total-budget cap for forever sessions. Independent ceiling because removing the time bound demands tighter $ control.
X402_PREAUTH_DISABLE01/true/yes/on to disableGlobal off switch. When set, every preauth code path short-circuits and x402 payments fall back to the regular two-step confirm flow. Use during incident response or compliance audit windows.

scope=any + ttl=forever is the most permissive grant possible. The REPL /x402 preauth grant handler requires double-explicit confirmation for that combination; the LLM tool x402_preauth_grant never proposes it automatically (the x402-payment skill prompt spells this out explicitly).

OpenClaw workspace integration

The agent reads + writes a workspace dir (default ~/.minara/workspace/, override via MINARA_WORKSPACE_DIR or the --workspace flag) containing markdown files modeled on OpenClaw's AGENTS.default.md schema: SOUL.md (identity), AGENTS.md (rules), IDENTITY.md, USER.md, MEMORY.md (curated long-term), HEARTBEAT.md (between-session memo), BOOTSTRAP.md (first-run only), TOOLS.md (env-specific tool notes), and memory/YYYY-MM-DD-*.md (daily logs).

Templates ship under apps/agent/src/workspace/templates/. The boot path calls seedWorkspaceIfMissing() automatically inside createApp() so a fresh install gets the full file set without forcing the operator to run minara setup. The seed is idempotent — files the user has edited are never overwritten. HEARTBEAT.md is intentionally NOT seeded (the agent writes a real one at the end of its first turn; seeding stale state would lie).

The Web UI's Settings → Workspace panel edits the runtime files at ~/.minara/workspace/ through the gateway's /v1/workspace/files endpoints with sha256 optimistic concurrency.

Wired into:

  • apps/agent/src/workspace/seed.tsseedWorkspaceIfMissing + atomicWriteFile
  • apps/agent/src/workspace/heartbeat-writer.ts — per-turn ## State writer
  • apps/agent/src/workspace/daily-log-writer.ts — per-session daily journal
  • apps/agent/src/workspace/dreaming-task.ts — periodic MEMORY.md consolidation
  • apps/agent/src/workspace/bootstrap-handler.ts — archive on BOOTSTRAP_DONE
  • apps/agent/src/workspace/soul-change-detector.ts — disclose SOUL.md edits
VariableDefaultFormatEffect
WORKSPACE_HEARTBEAT_ENABLED1 (on)0/false/no/off to disablePer-turn HEARTBEAT.md state writer. When on, after every turn the agent writes last_seen, session_id, surface, turn_count, last_user_query, plus heuristic open_loops extracted from the reply. The user-edited ## Schedule section is preserved verbatim across writes (schedule_raw round-trip). Disable on read-only workspace mounts, CI runs, or privacy-sensitive contexts.
WORKSPACE_DAILY_LOG_ENABLED0 (off)1/true/yes/on to enableWhen on, the agent appends a structured slice (user message, reply, tool calls) to <workspace>/memory/YYYY-MM-DD-<session>.md every WORKSPACE_DAILY_LOG_INTERVAL turns. Per-session files avoid multi-process append races between the REPL and HTTP gateway; the dreaming task merges across sessions. Off by default to keep the workspace dir small until explicitly opted in.
WORKSPACE_DAILY_LOG_INTERVAL5positive integer (turns)Cadence for the daily-log writer above. Smaller means more granular journals + more disk writes; larger means the model has to remember more turn-to-turn before capture. No effect when WORKSPACE_DAILY_LOG_ENABLED is off.
WORKSPACE_DREAM_ENABLED0 (off)1/true/yes/on to enablePeriodic MEMORY.md consolidation. When on, a setInterval task wakes every WORKSPACE_DREAM_INTERVAL_HOURS, reads recent daily logs from memory/, and asks the active LLM to extract durable facts / preferences / decisions. The result lands as a ## Dreamed YYYY-MM-DD section appended to MEMORY.md (append-only — your edits are never overwritten). A .dreamed-state.json watermark prevents duplicate promotion across runs, and the existing MEMORY.md is fed to the prompt for in-LLM dedupe. Costs LLM calls — keep off in cost-sensitive contexts.
WORKSPACE_DREAM_INTERVAL_HOURS24positive number (hours, can be fractional)Hours between dreaming runs. The scheduler skips a tick when the previous run is still in flight, so a slow LLM call can't pile up. No effect when WORKSPACE_DREAM_ENABLED is off.
WORKSPACE_DREAM_TOTAL_INPUT_BYTES262144 (256 KB)positive integer (bytes)Global byte budget for the dream prompt input (all daily logs combined). Daily logs are kept as a contiguous newest-first suffix under the budget, so the model always sees a chronologically continuous slice with the most recent activity included. Per-file content is still tail-truncated to 64 KB, and the most recent log is always kept even if it alone exceeds the budget. Defends against a long window of dense logs blowing past the model's context window. No effect when WORKSPACE_DREAM_ENABLED is off.
WORKSPACE_DREAM_LOCK_TTL_MS1800000 (30 min)positive integer (ms)TTL for the <workspace>/.dreaming.lock file. Older locks are considered stale and reclaimable by another process. Reflects the longest reasonable duration for a single dream pass; should not equal the interval between passes. Set higher if your LLM call routinely takes more than 30 min. NFS-mounted workspaces are not supported (the underlying O_EXCL semantics are not guaranteed atomic across all NFS clients) — keep dreaming on a single host. No effect when WORKSPACE_DREAM_ENABLED is off.

MINARA_WORKSPACE_DIR and the --workspace CLI flag override the default ~/.minara/workspace/ path for both reads and writes, including the auto-seed and the gateway editor. Useful when pointing Minara at an existing OpenClaw workspace (--workspace ~/.openclaw/workspace).

Deep Research report rendering

VariableDefaultAccepted valuesPurpose
REPORT_BUNDLE_CHART_PNGSunset (off)1 / true to enableFalls back the deep-research HTML / PDF export to PNG-bundled charts instead of the v7 default of interactive ECharts. Default (off): the gateway passes ["html", "pdf"] to renderDeepResearchReport; every chart://<id> link expands to the canonical ECharts embed (<div class="echart-host"> + inline <script> that hydrates from the echarts CDN) so the user gets zoom + tooltip + a saveAsImage PNG download toolbox button. On: the gateway passes ["html", "pdf", "charts"]; bundleChartPngs renders each chart server-side via headless playwright to <dir>/charts/<id>.png and the HTML embeds <img> tags. Use when exporting for offline distribution or when the echarts CDN is unreachable; trade-off is that charts become static (no interactivity). Consumed by the deep-research branch of handleChatStream in apps/agent/src/gateway/api.ts.

Shortcut questions (Chat landing page)

The /v1/shortcut-questions endpoint feeds the Chat landing page's chip-row + paginated question list. Each category has a hand-authored fixed list (always returned) and an LLM-generated dynamic list that refreshes on a cadence.

VarDefaultFormatPurpose
SHORTCUT_QUESTIONS_TTL_MS21600000 (6 hours)positive int millisecondsHow often the background generator refreshes per-(category, locale) dynamic shortcut questions. Each refresh runs ~11 Haiku 4.5 calls per locale (EN + ZH = ~22 calls). Lower for faster turnover on news / trending topics, higher to cut LLM cost. Consumed by src/shortcut-questions/refresh-loop.ts.
SHORTCUT_QUESTIONS_DISABLEDunset (off)1 to disableSkip dynamic LLM generation entirely. The endpoint still serves the hand-authored fixed lists; dynamic arrays stay empty. Use in tests, on cheap local dev without ANTHROPIC_API_KEY, or in environments where dynamic content isn't worth the spend.

Spot deposit watcher (Portfolio Deposit modal)

The /v1/wallet/deposits/watch SSE endpoint detects inbound stable transfers in real time so the Deposit modal can flip from "Waiting for your deposit…" to a green "credited" pill without the user having to refresh. Each chain has its own JSON-RPC transport (Arbitrum + 6 other EVM chains via eth_getLogs, Solana via getSignaturesForAddress on the user's USDC token accounts). The agent ships with foundation-grade public RPC fallbacks per chain; operators can swap each one for a paid endpoint via the variables below.

VarDefaultFormatPurpose
SOLANA_RPC_URLhttps://api.mainnet-beta.solana.comHTTPS URLSolana JSON-RPC endpoint used by the spot deposit watcher. The transport polls getTokenAccountsByOwner once per session (refreshed every 30s) to discover the user's USDC token accounts, then getSignaturesForAddress every 5s and getTransaction to decode the SPL Transfer instruction. Flips to credited at the confirmed commitment (slot finality, ~400ms). Swap for Helius / QuickNode / Triton / Alchemy Solana if the public RPC's rate limit becomes a bottleneck. Consumed by apps/agent/src/wallet-watch/solana-client.ts.

System resource metrics (Dashboard System status row)

The /v1/system-metrics endpoint feeds the Dashboard's System status panel with CPU, memory, disk-free + sandbox-folder size, gateway network throughput, and recency-weighted LLM token throughput. CPU / memory / disk sample every 5s; network is counted per request; tokens are recorded per turn from agent-loop.

VarDefaultFormatPurpose
MINARA_SYSTEM_METRICS_DISABLEDunset (off)1 to disableDisable the 5s sampling tick + sandbox folder walk. The endpoint stays registered and returns { disabled: true } so the web UI hides the row. Use on resource-constrained operator hosts, in test runs, or wherever the background sampling shouldn't run. Consumed by src/metrics/index.ts.
SYSTEM_METRICS_TOKEN_SESSION_WINDOW5positive integerHow many of the most recently active chat sessions feed the "Tokens/sec" headline. Per-session aggregates of input/output tokens + wall-clock duration are kept; on each snapshot the top-N most-recently active sessions are averaged so the number reflects current agent performance, not a lifetime average diluted by old idle sessions. Lower for a tighter "right now" reading, higher for a smoother number. Consumed by src/metrics/token-collector.ts.

Checklist for adding a new env var

Before merging:

  • Var is read via process.env.<NAME> with a sensible fallback (or explicit feature-gate if absent)
  • Entry added to .env.example in the correct section, with a detailed comment covering purpose, consumer, when to set, default-when-unset, and value format (no one-line stubs)
  • Entry added to the inventory above, matching the .env.example prose
  • For domain skills: requires_env: ["<NAME>"] is set so the skill self-hides when the var is missing
  • For tools: factory returns [] when the var is missing (never throws at boot)
  • Var name follows the convention <PROVIDER>_API_KEY / <PROVIDER>_TOKEN / <PROVIDER>_<FIELD> — UPPER_SNAKE, provider prefix first
  • No real secret value is committed anywhere

On this page

TL;DR ConventionHow .env loading worksInventoryLLM providersExtended thinkingweb_search provider chainMinara coreHTTP gatewaySafety and fund-movingHybrid memory retrievalBacktesting feedback loop (Sprint 6 — online outcome filler)Personalization rebuild (M3.2 event-driven thresholds)Trading summary thresholdsMemory extraction thresholdsShared knobsOff-agent history mirrorHyperliquid perpsStrategy Studio (BETA)Institution Mode (multi-agent firm simulation)Self-learning + Phase B reflection (v2 PR 1 / PR 2)Analyst recovery + canonical-asset preflightOffline learning-config tuning (Phase B gate)Methodology self-optimization loop (Phases 1-7)Phase 1 — Decision capturePhase 2 — Multi-horizon backtest + hallucination checkPhase 3 — RewardPhase 4 / 7 — Methodology instance dispatchPhase 6 — BO tuning cycleBuiltin toolsMessaging platforms (extended set)Lark / FeishuWeCom (企业微信)DingTalk (钉钉)WeChat OA (公众号)QQ BotLINEMattermostMicrosoft TeamsGoogle ChatBlueBubbles (iMessage)MatrixClient-outbound inbound daemonsMCP integrationsNative provider keys (PR-A → PR-G migration)Surviving vendored external skillsMethodology learning loop — case attribution + synthesis (Phase 1-4)Off switches (incident response)Synthesis tuning (Phase 4 D)Price quality (Phase 3 F4.3)Case-recorder housekeeping (Phase 2)Operating the learning cronMethodology audit subsystem (passive observer)Fund-confirm bypass (advanced — production caveat)Proactive Wealth Agentx402 paywall pre-authorization (PR-X)OpenClaw workspace integrationDeep Research report renderingShortcut questions (Chat landing page)Spot deposit watcher (Portfolio Deposit modal)System resource metrics (Dashboard System status row)Checklist for adding a new env var