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:
-
Read it via
process.env.<NAME>at factory/initializer time. Never hard-code secrets. Never accept them as CLI args you echo. -
Append a documented entry to
.env.examplein 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. -
For domain skills (
apps/agent/src/skills/builtin/*.tsorapps/agent/src/skills/external/<id>/), declare the var inrequires_envso 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"], }; -
For tools (
apps/agent/src/tools/*.ts), the factory should return an emptyToolEntry[]when the env var is missing. The tool registry silently excludes unregistered names, so downstream skilltool_namesreferences will gracefully degrade:export function createMyProviderTools(): ToolEntry[] { const apiKey = process.env.MY_PROVIDER_API_KEY; if (!apiKey) return []; // ... } -
Never commit a real secret.
.envis git-ignored;.env.exampleis 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 modeapps/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.
| Variable | Default | Format | Effect |
|---|---|---|---|
ANTHROPIC_API_KEY | — | sk-ant-... | Primary Claude credential. Unset: falls through to stored OAuth, then OpenRouter. |
OPENROUTER_API_KEY | — | sk-or-... | Alternative multi-model router. Unset: tried only if Anthropic paths fail. |
OPENAI_API_KEY | — | sk-... | 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_URL | https://api.openai.com/v1 | URL | Optional override for the OpenAI API base URL — set when targeting an Azure-compatible gateway or a corporate proxy. |
OPENAI_ORG_ID | — | org-... | Optional OpenAI-Organization header for billing routing. |
XAI_API_KEY | — | opaque | Native 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_URL | https://api.x.ai/v1 | URL | Optional override for the xAI API base URL. |
MINARA_XAI_OAUTH_CLIENT_ID | xAI Grok-CLI public id | UUID | Override 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
THINKING_ENABLED | true | true | false | Master 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_TOKENS | 4000 | positive integer | Max thinking tokens per LLM turn. Anthropic charges only for tokens the model actually used. |
INSTITUTION_THINKING_BUDGET_TOKENS | THINKING_BUDGET_TOKENS | positive integer | Override 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_TOKENS | THINKING_BUDGET_TOKENS | positive integer | Override 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
EXA_API_KEY | — | Exa API key | Highest-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_KEY | — | Firecrawl API key | Runs 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_ENABLED | true | true | false | Anthropic 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_KEY | — | Tavily API key | Strong external choice with LLM-generated answer summaries. Get one at https://tavily.com. |
GOOGLE_SEARCH_API_KEY | — | Google Cloud key | Google Custom Search via Programmable Search Engine. Free 100 queries/day, then $5/1000. Requires GOOGLE_SEARCH_CX set together. |
GOOGLE_SEARCH_CX | — | Programmable Search Engine ID | The CX (engine id) from https://programmablesearchengine.google.com/. |
BRAVE_SEARCH_API_KEY | — | Brave subscription token | Independent 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
| Variable | Default | Format | Effect |
|---|---|---|---|
MINARA_API_KEY | — | opaque string | Minara REST credential. Unset: falls back to a stored OAuth profile (web-UI sign-in). |
MINARA_BASE_URL | https://api.minara.ai | absolute URL | Backend API origin. Override only when targeting a staging environment or local mock. |
MINARA_FRONTEND_BASE_URL | https://minara.ai | absolute URL | Web 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_MODEL | claude-sonnet-4-6 | model id | Main agent-loop model. Also settable via /model. |
MINARA_DATA_DIR | ~/.minara | absolute path | Root for SQLite + sandbox + auth + logs. |
MINARA_TERMINAL_CWD | — | absolute path | Override 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/files | URL or path | Public file link prefix (behind reverse proxy). |
OFFLINE_MODE | off | 1/true/yes/on | Hard-disable outbound HTTP from sandbox tools. |
LOG_LEVEL | info | debug|info|warn|error | Logger threshold. debug is safe but ~4x volume. |
HTTP gateway
| Variable | Default | Format | Effect |
|---|---|---|---|
GATEWAY_PORT | 8080 | integer | TCP port the gateway binds. |
GATEWAY_AUTH_TOKEN | — | opaque string | Bearer token on every /v1/... route. Unset: auth disabled. |
WEB_UI_DIST_DIR | — | absolute path | Built web-ui dist/ served same-origin at / (desktop / single-port). Unset: no static UI (API only). |
WEBHOOK_PORT | — | integer | Optional dedicated inbound-webhook port. |
Safety and fund-moving
| Variable | Default | Format | Effect |
|---|---|---|---|
DISABLE_STRICT_PLAYBOOK | unset (strict on) | 1/true to disable | Reverts buildPlaybookBlock to the legacy soft-advisory header. Default behavior renders the imperative "AUTHORITATIVE specification for this turn" checklist tone. |
DISABLE_METHODOLOGY_INJECTION | unset (injection on) | 1/true to disable | Off 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_DISPATCH | unset (dispatch on) | 1/true to disable | Off switch for BO-tuned methodology instance overrides. Default behavior merges instance thresholds onto template defaults; disabled = template-only resolution. |
DISABLE_KNOWLEDGE_BUDGET | unset (budget on) | 1/true to disable | Off 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_INJECT | unset (inject on) | 1/true to disable | Off 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_CALLS | unset (parallel on) | 1/true to disable | Off 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_ENABLED | unset (off) | 1/true to enable | Intent-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_MODE | sampled | off|sampled|on | A/B observation recorder. Writes (current, proposed) variant pairs at classifier / memory-snapshot / role-hints decision points to the shadow_runs SQLite table. |
SHADOW_SAMPLE_RATE | 0.1 | [0, 1] | Sampling probability when SHADOW_MODE=sampled. |
SHADOW_RETENTION_DAYS | 30 | positive integer | Days of shadow_runs rows to keep. One-shot prune runs at boot. |
MEMORY_SNAPSHOT_PREF_LIMIT | 50 | non-negative integer | Quota for preference-category rows in the session memory snapshot. |
MEMORY_SNAPSHOT_STRAT_LIMIT | 30 | non-negative integer | Quota for strategy-category rows. |
MEMORY_SNAPSHOT_TRADE_LIMIT | 50 | non-negative integer | Quota for trade_note-category rows. |
MEMORY_SNAPSHOT_OBS_LIMIT | 30 | non-negative integer | Base quota for observation rows. Unused slots from the pref / strategy / trade buckets overflow into this bucket. |
MEMORY_REFRESH_WRITES | 3 | non-negative integer | Writes-since-rebuild threshold for soft snapshot refresh. |
MEMORY_REFRESH_TURNS | 10 | non-negative integer | Turns-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_LEN | 2000 | positive 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_TOKENS | 15000 | non-negative integer | Cap 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_TAGS | false | true|false | Prepend each dynamic knowledge block with an HTML-comment provenance tag. For human / log audit only. |
PREFERENCE_LEARNING | 0 | 0/1 | Master 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_INTERVAL | 30 | positive integer | Turns 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_QUOTA | 3 | positive integer | Max 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_THRESHOLD | 0.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_SIZE | 200 | positive integer | Max recent candidates pulled into a single proposer LLM call. |
PREFERENCE_MIN_CLUSTER_SIZE | 3 | positive 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_HOURS | 24 | positive integer | Min 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_TURNS | 5 | positive integer | Min turns between consecutive graduation asks across DIFFERENT preferences inside the same REPL session. Prevents back-to-back card asks. |
PREFERENCE_SKIP_IN_CHAT_ASK | 0 | 0/1 | Disable 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_ACTIVATE | 1 | 0/1 | M3. 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_ACTIVATE | 1 | 0/1 | M3. Silent auto-activation of personal_style preferences after N observations of the same dedup_key. Cards reserved for higher-impact preferences. |
PREFERENCE_STYLE_MIN_OBSERVATIONS | 2 | positive integer | M3. Same-statement observations needed before style auto-activates. Higher = more chances for the user to contradict themselves. |
PREFERENCE_HARD_UNDO_WINDOW_HOURS | 24 | positive integer | M3. 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_CONFIRM | off | 1/true/yes/on | Bypass confirm gate on every fund-moving tool. Non-interactive only. |
DISABLE_SCRIPT_RISK_GATE | off | 1/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_HOURS | 24 | integer 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
EMBEDDING_PROVIDER | disabled | disabled|openai|voyage | Selects the embedder. disabled = factory returns null, hybrid path inert. Other values require EMBEDDING_API_KEY. |
EMBEDDING_API_KEY | — | opaque string | Bearer token. OpenAI: sk-.... Voyage: pa-.... Unset with a non-disabled provider → factory still returns null with a logged warn. |
EMBEDDING_MODEL | provider-native (text-embedding-3-small / voyage-3) | model id | Model identifier. Override only when you've validated the dimensionality matches EMBEDDING_DIM. |
EMBEDDING_DIM | 1536 | positive integer | Vector 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_URL | provider-native | URL ending in /embeddings | Optional override for self-hosted gateways or proxies. |
SQLITE_VEC_EXTENSION_PATH | bundled sqlite-vec npm binary | absolute filesystem path | Explicit 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).
| Variable | Default | Format | Effect |
|---|---|---|---|
BACKTEST_ENABLED | false | true|false | Master switch. false = runner + scheduler never registered (zero runtime cost). true = scheduler fires every BACKTEST_CRON_HOURS hours. |
BACKTEST_DRY_RUN | false | true|false | Dry-run: compute outcomes, emit to shadow_runs(facet='backtest_outcome'), but skip updateTradeOutcome + recordUsage. First-week rollout protocol. |
BACKTEST_MIN_TRADE_AGE_MS | 86400000 (24h) | positive integer (ms) | Minimum age before a trade is eligible. Threaded into ReviewEngine.minTradeAgeForEvalMs. |
BACKTEST_OUTCOME_HORIZON_HOURS | 24 | positive 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_LIMIT | 20 | positive integer | Max pending rows pulled per fire. Threaded into ReviewEngine.maxEvalsPerBatch. |
BACKTEST_CRON_HOURS | 24 | positive number | Scheduler interval. Uses setInterval().unref(). Values below 1 minute are clamped up. |
BACKTEST_PRICE_PROVIDER | auto | auto|hyperliquid|yahoo | Force a single historical price source. auto routes crypto → Hyperliquid → Yahoo -USD; stock/unknown → Yahoo; stablecoin → 1.0. |
BACKTEST_MAX_COST_USD_PER_RUN | 2.00 | non-negative float | Per-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_USAGE | false | true|false | When 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):
- Set
BACKTEST_ENABLED=true+BACKTEST_DRY_RUN=truefor one cron cycle. Inspectshadow_runs WHERE facet='backtest_outcome'— check that outcome strings match±N.NN% in Xhand skip reasons are sensible. - Flip
BACKTEST_DRY_RUN=false. The runner now writes backtrade_history.outcome+ state, and new methodologies may be created byevaluatePacked.recordUsageis still a no-op. - Flip
LEARNING_RECORD_USAGE=true. The Wilson counters begin to update on every evaluation. Monitormethodologies.times_used+times_correctdelta 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
| Variable | Default | Format | Effect |
|---|---|---|---|
FIN_PROFILE_TRADING_SUMMARY_MIN_NEW_TRADES | 3 | positive int | Min new trades since last rebuild before gate 2 allows a trading-summary rebuild. |
FIN_PROFILE_TRADING_SUMMARY_MIN_INTERVAL_MIN | 30 | positive int, minutes | Min elapsed time since last rebuild. |
FIN_PROFILE_TRADING_SUMMARY_MAX_TRADES | 100 | positive int | Upper bound on trades fed to the LLM on cold-start / force-regen. |
FIN_PROFILE_TRADING_SUMMARY_INCREMENTAL_MAX_TRADES | 50 | positive int | Upper 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
| Variable | Default | Format | Effect |
|---|---|---|---|
FIN_PROFILE_MEMORIES_MIN_NEW_TURNS | 5 | positive int | Min new chat turns recorded since last extraction before rebuildMemories runs. |
FIN_PROFILE_MEMORIES_MIN_INTERVAL_MIN | 10 | positive int, minutes | Min elapsed time since last memory extraction. |
MEMORY_CONSOLIDATION_ENABLED | false | 1/true/yes/on enables | Opt 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_GUIDANCE | empty | free text, max 500 chars | Optional 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
| Variable | Default | Format | Effect |
|---|---|---|---|
FIN_PROFILE_EVENT_DEBOUNCE_SEC | 30 | positive int, seconds | Debounce window for scheduleCheck(dim). Collapses rapid bursts into one gate-checked rebuild. |
CHAT_TURN_RECORDING | 1 (on) | 0/false/no/off disables | Persists (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.
| Variable | Default | Format | Effect |
|---|---|---|---|
FIN_PROFILE_HISTORY_SYNC_WINDOW_DAYS | 90 | positive int, days | Rolling sync window. Anything older is never pulled. |
FIN_PROFILE_HISTORY_SYNC_MIN_INTERVAL_MIN | 5 | positive int, minutes | Throttle floor between successive sync triggers. Multiple scheduleSync() calls inside the window collapse. |
FIN_PROFILE_HISTORY_SYNC_TIMEOUT_SEC | 8 | positive int, seconds | Hard timeout per syncAll() call. Wired through AbortController so in-flight HTTP requests are cancelled. |
FIN_PROFILE_HISTORY_SYNC_PAGE_HINT | 500 | positive int, rows | Upstream "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_SEC | 60 | positive int, seconds | Overlap when sliding startTime forward on a probably-truncated page. fill_uid dedup makes overlap harmless. |
FIN_PROFILE_HISTORY_SYNC_MAX_ROUNDS_PER_SUB | 10 | positive int | Hard upper bound on the truncation-rolling loop per sub-account. |
FIN_PROFILE_HISTORY_SYNC_MAX_FAILURES | 5 | positive int | Per-(source, sub_account_id) consecutive failure threshold. At/above, that key is skipped during normal scheduling. |
FIN_PROFILE_HISTORY_SYNC_FAILURE_COOLDOWN_MIN | 30 | positive int, minutes | After 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_PAGES | 20 | positive int | Hard cap on the spot pagination loop. |
FIN_PROFILE_HISTORY_SYNC_SPOT_PAGE_SIZE | 100 | positive int | Spot pagination batch size. Forwarded to Minara as limit. |
FIN_PROFILE_TRADING_SUMMARY_PERPS_RECENT_FILLS | 30 | positive int | How many of the newest perps fills the LLM rebuild reads. Aggregate is always sent in full. |
FIN_PROFILE_TRADING_SUMMARY_SPOT_RECENT_ACTIVITIES | 20 | positive int | Same for spot. |
FIN_PROFILE_TRADING_SUMMARY_AGGREGATE_WINDOW_DAYS | 90 | positive int, days | Aggregation window for per-symbol / per-pair rollups fed to the LLM. |
FIN_PROFILE_MEMORY_SOFT_DELETE_RETENTION_DAYS | 30 | positive int, days | How 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
MINARA_HL_DEX_DISCOVERY | off | 1/true/yes/on to enable | Opts 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
MINARA_SS_CODEGEN_MAX_ITER | 3 | positive 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
INSTITUTION_MAX_DEBATE_ROUNDS | 1 | integer, 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_ROUNDS | 1 | integer, 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_MS | 1200000 | integer 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_MS | 300000 | integer 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_TURN | 4096 | integer, 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.allover the four slots, no semaphore library). - Subagent inner-loop turn cap = 5 turns. Matches Minara's
strategy-code-subagentdiscipline; 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_researchuses); 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
INSTITUTION_LEARNING_ENABLED | on | on | off | Master 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_ENABLED | on | on | off | Phase 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_LIMIT | 10 | integer, 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_MS | 120000 | positive integer ms | Phase 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_HOURS | 24 | integer, 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_HOURS | 6 | integer, 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_DAYS | 90 | integer, 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_CRYPTO | BTC | ticker | Alpha 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_STOCK | SPY | ticker | Alpha benchmark for stocks / indices. |
INSTITUTION_BENCHMARK_FOREX | DXY | ticker | Alpha 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.
| Variable | Default | Type | Description |
|---|---|---|---|
INSTITUTION_FORCE_RESOLVER_PREFLIGHT | false | true | false | Run 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_DAYS | 30 | positive integer days | TTL for outcome: "resolved" (single canonical chain+contract or native+chain). |
CANONICAL_ASSET_CACHE_TTL_MULTI_DAYS | 14 | positive integer days | TTL 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_DAYS | 7 | positive integer days | TTL for outcome: "ambiguous" (provider results disagree). Short so we re-resolve after provider data converges. |
CANONICAL_ASSET_CACHE_TTL_NONE_DAYS | 1 | positive integer days | TTL for outcome: "none". Very short — providers update daily, so we want a fresh attempt rather than caching a negative. |
CANONICAL_ASSET_CACHE_TTL_USER_DAYS | 365 | positive integer days | TTL for user-supplied entries (operator override via minara assets pin or banner CTA). Pin entries supersede provider resolution. |
CANONICAL_ASSET_CACHE_FALLBACK_TO_EXPIRED | true | true | false | When 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
LEARNING_TUNING_ENABLED | false | true|false | Master 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
DECISION_CAPTURE_ENABLED | false | true|false | Master switch. false = hook returns immediately without pre-filter or LLM call. |
DECISION_SUMMARIZER_MODEL | claude-haiku-4-5-20251001 | model id | Which model extracts the decision JSON. Bump to Sonnet if coverage rate < 70%. |
DECISION_SUMMARIZER_TIMEOUT_MS | 15000 | positive integer | Per-call timeout. Timeout drops the row with a warn; no retry. |
DECISION_CAPTURE_SYNC_MODE | false | true|false | Await summarizer before returning from the hook. Only for deterministic tests — adds turn latency. |
DECISION_CAPTURE_HEURISTIC_ENABLED | true | true|false | Tier 2: capture turns whose response contains BUY/SELL keywords + asset ticker even without an advice scenario active. |
DECISION_CAPTURE_UNIVERSAL_SCAN | false | true|false | Tier 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.
| Variable | Default | Format | Effect |
|---|---|---|---|
DECISION_BACKTEST_ENABLED | false | true|false | Master switch. Off = runner never constructed, no cron timer. |
DECISION_BACKTEST_DRY_RUN | false | true|false | Compute outcomes but route writes to shadow_runs(facet='decision_outcome') instead of decision_outcomes/decision_history. Week-1 rollout. |
DECISION_BACKTEST_HORIZONS | 1d,3d,1w,1m | CSV | Horizon list. Each maps to one row per decision. Max horizon determines when pending decisions become eligible. |
DECISION_BACKTEST_CRON_HOURS | 24 | positive number | Runner invocation interval. |
DECISION_BACKTEST_MAX_AGE_DAYS | 60 | positive integer | Pending decisions older than this are skipped (backlog defense). |
HALLUCINATION_MAX_PRICE_DELTA_PCT | 0.05 | decimal | |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).
| Variable | Default | Format | Effect |
|---|---|---|---|
DECISION_HORIZON_WEIGHTS_JSON | {"1d":0.15,"3d":0.25,"1w":0.35,"1m":0.25} | JSON object | Per-horizon weights in the weighted mean. Missing labels get 0 weight. |
DECISION_HOLD_NEUTRALITY_THRESHOLD | 0.02 | decimal | |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.
| Variable | Default | Format | Effect |
|---|---|---|---|
METHODOLOGY_INSTANCE_TUNING_ENABLED | false | true|false | Master switch for the BO cycle. |
METHODOLOGY_TUNING_CRON_DAYS | 7 | positive integer | Days between cycle invocations. |
METHODOLOGY_TUNING_MAX_BUCKETS_PER_CYCLE | 10 | positive integer | Top-N buckets (by tunability_score) processed per cycle. |
METHODOLOGY_TUNING_PROFILES_PATH | $MINARA_DATA_DIR/methodology-tuning-profiles.json | filesystem path | Override JSON for asset-class profiles. Per-class shallow merge; null excludes. |
METHODOLOGY_TUNING_MIN_DECISIONS_GLOBAL | — | positive integer | Emergency floor applied to every profile's min_decisions (takes max). |
METHODOLOGY_TUNING_MIN_IMPROVEMENT_REL | 0.05 | decimal | Post-BO check #1 — test-split mean reward must exceed baseline by this ratio. |
METHODOLOGY_TUNING_MAX_SENSITIVITY_DROP_10PCT | 0.5 | decimal (0, 1] | Post-BO check #2 — reject if ±10% neighbor score drops by more than this. |
METHODOLOGY_TUNING_PARAM_BOUND_REL | 0.5 | decimal | BO pbounds half-width, as a fraction of the template default. |
METHODOLOGY_TUNING_MIN_CAPTURE_CONFIDENCE | 0.3 | decimal [0, 1] | BO replay only considers decisions with capture_confidence ≥ this. |
Rollout order:
DECISION_CAPTURE_ENABLED=true— start recording advice turns.- Wait ~30 days for data.
DECISION_BACKTEST_ENABLED=true+DECISION_BACKTEST_DRY_RUN=true— shadow mode 1 week.DECISION_BACKTEST_DRY_RUN=false— live backtest writes.- Check
minara learning statsreportsREADY_FOR_BO=true. METHODOLOGY_INSTANCE_TUNING_ENABLED=true— activate BO cycle.- Instance dispatch is on by default; keep
DISABLE_METHODOLOGY_INSTANCE_DISPATCHunset.
Builtin tools
Every row is optional. Missing var disables the feature silently.
| Variable | Effect |
|---|---|
TAVILY_API_KEY | Gates web_search + web_extract. |
FIRECRAWL_API_KEY | Gates web_search + web_extract; takes priority over Tavily in both chains. |
FAL_KEY | Enables Fal.ai image / video models. |
MESSAGING_DEFAULT_PROVIDER | Provider id used when send_message omits provider (e.g. telegram, slack). Optional — first configured provider wins when unset. |
MESSAGING_MAX_ATTACHMENT_BYTES | Per-attachment size cap for send_message({attachments}) calls. Default: 52 428 800 (50 MB). Provider APIs enforce their own maxima independently. |
TELEGRAM_BOT_TOKEN | Telegram outbound messaging. Pair with TELEGRAM_CHAT_ID. Supports streaming edits. |
TELEGRAM_CHAT_ID | Numeric chat id for Telegram. |
TELEGRAM_RICH_TEXT | Render Telegram replies as rich text (HTML, with a MarkdownV2 then plain-text fallback). Default on; set false for plain text. |
SLACK_WEBHOOK_URL | Slack Incoming Webhook URL. Simplest Slack path; no streaming. |
SLACK_BOT_TOKEN | Slack bot token (xoxb-...). Enables streaming edits via chat.update. Pair with SLACK_CHANNEL_ID. |
SLACK_CHANNEL_ID | Default Slack channel id (e.g. C0123ABC). Required with SLACK_BOT_TOKEN. |
SLACK_APP_TOKEN | App-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_TOKEN | Discord bot token. Streaming edits via PATCH /channels/{}/messages/{}. Pair with DISCORD_CHANNEL_ID. |
DISCORD_CHANNEL_ID | Numeric Discord channel id. |
HASS_URL | Home Assistant base URL (e.g. https://hass.local:8123). |
HASS_TOKEN | Home Assistant long-lived access token. |
HASS_NOTIFY_SERVICE | HA notify service id (e.g. mobile_app_you, with or without notify. prefix). One-shot, no streaming. |
SMTP_HOST | Outbound SMTP host (e.g. smtp.gmail.com). Required for email provider. |
SMTP_PORT | SMTP port (587 STARTTLS, 465 SSL). |
SMTP_USER | SMTP auth username (optional for relays without auth). |
SMTP_PASSWORD | SMTP auth password / app password (masked). |
EMAIL_FROM | "From" address used in outbound mail. |
EMAIL_TO | Default recipient when send_message omits channel. |
GOOGLE_OAUTH_CLIENT_ID | Google OAuth client ID for the one-click "Email (Gmail)" connector (email-gmail provider). Can also be entered in Settings → Messaging. |
GOOGLE_OAUTH_CLIENT_SECRET | Google OAuth client secret for the Gmail connector (masked). |
GMAIL_REFRESH_TOKEN | Set by the Connect-Gmail flow. Long-lived token used to send via the Gmail API (gmail.send scope only; never reads mail). |
GMAIL_SENDER_EMAIL | Set by the Connect-Gmail flow. The authorized mailbox messages are sent from. |
GMAIL_TO | Optional recipient for the email-gmail provider. Defaults to the connected inbox (push-to-self). |
WHATSAPP_ACCESS_TOKEN | Meta Cloud API Bearer token. Enables whatsapp provider. |
WHATSAPP_PHONE_NUMBER_ID | Numeric phone number id from the Meta Developer app. |
WHATSAPP_RECIPIENT | Default E.164 recipient (+12025551234). |
SIGNAL_CLI_NUMBER | Your registered Signal sender number (E.164). Requires signal-cli on PATH. |
SIGNAL_RECIPIENT | Default E.164 recipient. |
SIGNAL_CLI_BINARY | Override path to the signal-cli binary. Default: PATH lookup for signal-cli. |
TELEGRAM_WEBHOOK_SECRET | Shared secret for Telegram inbound webhooks. /webhooks/telegram returns 404 when unset. |
SLACK_SIGNING_SECRET | Slack app signing secret for inbound webhook HMAC verification. /webhooks/slack returns 404 when unset. |
DISCORD_APPLICATION_PUBLIC_KEY | Discord Ed25519 application public key (hex). /webhooks/discord returns 404 when unset. |
WHATSAPP_APP_SECRET | Meta app secret for WhatsApp Cloud API inbound HMAC-SHA256 over X-Hub-Signature-256. POST /webhooks/whatsapp returns 404 when unset. |
WHATSAPP_VERIFY_TOKEN | WhatsApp Cloud API hub.verify_token echoed on the one-time GET handshake. GET /webhooks/whatsapp returns 404 when unset. |
MESSAGING_INBOUND_TRANSCRIBE | Enable voice transcription (via OpenAI Whisper) on inbound voice attachments. Requires OPENAI_API_KEY. |
MESSAGING_VOICE_REPLY | Answer 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_KEY | ElevenLabs key. When set, ElevenLabs is the preferred provider for speech synthesis and transcription (lower latency); OpenAI stays as fallback. |
VOICE_TTS_PROVIDER | Pin the speech-synthesis vendor: auto (default) / elevenlabs / openai. |
VOICE_TTS_VOICE | Voice id for read-aloud replies, native to the primary TTS provider. Empty = provider default. |
VOICE_TTS_MODEL | TTS 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_SPEED | ElevenLabs 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_FIRST | 1 (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_PROVIDER | Pin the speech-to-text vendor: auto (default) / elevenlabs / openai. |
VOICE_STT_MODEL | STT model override for the primary provider. Empty = scribe_v1 / gpt-4o-mini-transcribe. |
VOICE_FFMPEG_PATH | Optional 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_KEY | Third-party Twitter scraper. Gates research.social.twitter. |
X_API_BEARER_TOKEN | Official X API v2. Gates x.api skill. |
GLASSNODE_API_KEY | Glassnode on-chain metrics. Gates research.onchain.glassnode. |
QDRANT_URL | Vector 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_KEY | Optional Qdrant Cloud auth. Sent as the api-key header on every Qdrant request when set. |
KB_EMBEDDING_PROVIDER | KB-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_KEY | Auth for KB_EMBEDDING_PROVIDER. Defaults to EMBEDDING_API_KEY when unset. |
KB_EMBEDDING_MODEL | Model id for KB_EMBEDDING_PROVIDER. Set to match the model used to populate Qdrant. |
KB_EMBEDDING_DIM | Vector dimension. Auto-detected from common model ids; set explicitly when using a custom model. |
E2B_API_KEY | Gates workspace.e2b cloud sandbox. |
FORCE_E2B | Ops 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
| Variable | Effect |
|---|---|
LARK_APP_ID | Lark app id, cli_xxxxxxxxxxxxxxxx. Required for outbound. |
LARK_APP_SECRET | Lark app secret. Used to mint tenant access tokens (2 h cache). |
LARK_DEFAULT_CHAT_ID | Default oc_xxxxxxxxxxxxxxxx chat for outbound sends. |
LARK_VERIFICATION_TOKEN | Inbound webhook verification token. Required for inbound. |
LARK_ENCRYPT_KEY | Inbound 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_DOMAIN | open.feishu.cn (mainland, default) or open.larksuite.com (international). |
WeCom (企业微信)
| Variable | Effect |
|---|---|
WECOM_CORP_ID | Corp id from "我的企业" page. |
WECOM_AGENT_ID | Application agent id (numeric). |
WECOM_SECRET | Application secret. Mints access_token (2 h cache). |
WECOM_DEFAULT_TOUSER | Default touser, pipe-separated user ids or @all. |
WECOM_CALLBACK_TOKEN | Inbound callback token. SHA1-signed with [token, ts, nonce, encrypt]. |
WECOM_CALLBACK_AES_KEY | 43-char EncodingAESKey for inbound AES-256-CBC decryption. |
DingTalk (钉钉)
| Variable | Effect |
|---|---|
DINGTALK_WEBHOOK_URL | Custom robot webhook URL (https://oapi.dingtalk.com/robot/send?access_token=...). |
DINGTALK_WEBHOOK_SECRET | Robot signing secret (SECxxxx). Signs outbound with HMAC-SHA256 over timestamp\nsecret; same recipe verifies inbound outgoing-webhook signatures. |
DINGTALK_STREAM_APP_KEY | Stream 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_SECRET | Stream Mode app secret (AppSecret / ClientSecret). Required with DINGTALK_STREAM_APP_KEY. |
WeChat OA (公众号)
| Variable | Effect |
|---|---|
WECHAT_OA_APP_ID | OA AppID, wxxxxxxxxxxxxxxxxx. |
WECHAT_OA_APP_SECRET | OA AppSecret. Mints access_token (2 h cache). |
WECHAT_OA_TOKEN | Server-config Token. SHA1-signed inbound (3-tuple on GET handshake, 4-tuple on POST). |
WECHAT_OA_AES_KEY | 43-char EncodingAESKey for inbound AES-256-CBC decryption. |
WECHAT_OA_DEFAULT_OPENID | Default openid recipient. Customer-service messages must be within the 48 h interaction window. |
QQ Bot
| Variable | Effect |
|---|---|
QQ_BOT_APP_ID | Bot AppID (numeric). |
QQ_BOT_APP_SECRET | Bot Secret. Doubles as the Ed25519 seed source for inbound signature verification (repeated to 32 bytes, then derived to a keypair). |
QQ_BOT_TOKEN | Bot token (legacy field, retained for compatibility). |
QQ_BOT_DEFAULT_CHANNEL_ID | Default target, <kind>:<id> where kind ∈ channel / group / c2c / dm. Bare ids default to channel:. Active messages capped at 4/month. |
LINE
| Variable | Effect |
|---|---|
LINE_CHANNEL_ACCESS_TOKEN | Long-lived bearer for push API. |
LINE_CHANNEL_SECRET | Channel secret. Verifies X-Line-Signature HMAC-SHA256 (base64) over raw body. Optional for outbound-only setups. |
LINE_DEFAULT_USER_ID | Default userId / groupId / roomId. |
Mattermost
| Variable | Effect |
|---|---|
MATTERMOST_URL | Server URL (no trailing slash). |
MATTERMOST_BOT_TOKEN | Bot user personal access token. |
MATTERMOST_DEFAULT_CHANNEL_ID | Default channel id for outbound. |
MATTERMOST_OUTGOING_WEBHOOK_TOKEN | Outgoing-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
| Variable | Effect |
|---|---|
TEAMS_BOT_APP_ID | Bot Microsoft App ID GUID. Audience for inbound JWT (multi-tenant) or audience plus tenant GUID (single-tenant). |
TEAMS_BOT_APP_PASSWORD | Bot Microsoft App Password. Mints access tokens for outbound (1 h cache). |
TEAMS_BOT_TENANT_ID | common for multi-tenant, GUID for single-tenant. Affects inbound JWT audience check. |
TEAMS_DEFAULT_CONVERSATION_ID | Bootstrap fallback conversation id. Production code should learn conversation ids from inbound activities. |
TEAMS_DEFAULT_SERVICE_URL | Bootstrap fallback service URL. Defaults to https://smba.trafficmanager.net/teams. Production code learns from inbound activity.serviceUrl. |
Google Chat
| Variable | Effect |
|---|---|
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON_PATH | Path to a service-account JSON key. Per CLAUDE.md §4, the file must live inside the data / sandbox tree. |
GOOGLE_CHAT_DEFAULT_SPACE_ID | Default space resource name, spaces/AAAA1234567. |
GOOGLE_CHAT_AUDIENCE | Must 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)
| Variable | Effect |
|---|---|
BLUEBUBBLES_SERVER_URL | Public URL of a BlueBubbles server running on macOS (typically a tunnel). |
BLUEBUBBLES_PASSWORD | Shared server password. Pure constant-time compare on inbound, no HMAC. |
BLUEBUBBLES_DEFAULT_CHAT_GUID | Default chat GUID, e.g. iMessage;-;+15551234567. |
Matrix
| Variable | Effect |
|---|---|
MATRIX_HOMESERVER | Homeserver URL (e.g. https://matrix.org). |
MATRIX_ACCESS_TOKEN | Long-lived bearer. Use the Authorization: Bearer header; the ?access_token= query form is deprecated. |
MATRIX_USER_ID | Bot user (e.g. @bot:example.org). Used to filter self-loops in /sync. |
MATRIX_DEFAULT_ROOM_ID | Default room id (!abc:example.org). The inbound daemon restricts emits to this room. |
MESSAGING_MATRIX_INBOUND | When 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).
| Variable | Effect |
|---|---|
MESSAGING_TELEGRAM_POLLING | Telegram getUpdates long-poll. Calls deleteWebhook on start; cursor at <dataDir>/telegram-updates.json. Webhook signal: TELEGRAM_WEBHOOK_SECRET. |
MESSAGING_DISCORD_GATEWAY | Discord 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_SOCKET | Slack Socket Mode. Requires SLACK_APP_TOKEN. Webhook signal: SLACK_SIGNING_SECRET. |
MESSAGING_MATTERMOST_WS | Mattermost v4 WebSocket bot. Reaches DMs and private channels the outgoing-webhook path cannot. Webhook signal: MATTERMOST_OUTGOING_WEBHOOK_TOKEN. |
MESSAGING_QQ_WS | QQ 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_STREAM | DingTalk 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_WS | Lark / Feishu long connection via the official SDK. Webhook signal: LARK_VERIFICATION_TOKEN. |
MCP integrations
| Variable | Format | Effect |
|---|---|---|
MCP_SERVERS | JSON array | Override default MCP server list. |
DEFILLAMA_API_KEY | opaque string | DefiLlama 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_URL | URL | Raw EVM JSON-RPC sub-agent. |
MCP_ETHERSCAN_URL | URL | Etherscan-style explorer (60+ EVM chains). |
MCP_SOLSCAN_URL | URL | Solscan Solana explorer. |
MCP_GOPLUS_URL | URL | GoPlus 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.
| Variable | Native skill(s) it activates | Header / usage |
|---|---|---|
CMC_API_KEY | priority chain cmc provider (price / trending fallback in minara.core.crypto_kline, get_price, get_trending) | X-CMC_PRO_API_KEY |
CMC_PRO_API_KEY | retained for the surviving vendored cmc-api-* SKILL.md packages | X-CMC_PRO_API_KEY |
COINGECKO_API_KEY | upgrades the coingecko-pro provider (price / kline / on-chain holders) and unlocks pro-tier endpoints in the surviving coingecko external SKILL.md | x-cg-pro-api-key or x-cg-demo-api-key |
COINANK_API_KEY | priority 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 CoinGlass | apikey |
COINGLASS_API_KEY | priority 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_KEY | priority 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_ID | priority 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 cover | OKX HMAC quad |
FMP_API_KEY | 11 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.marketsskill is read-only (Polymarket's public Gamma + CLOB read endpoints) and needs no env. The retiredexternal.polymarketSKILL.md usedPRIVATE_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):
| Package | Purpose | Env |
|---|---|---|
external.binance | Spot / margin / private endpoints beyond the public binance-public provider | none for public; account keys for private |
external.coingecko | On-chain GeckoTerminal OHLCV + the deeper coingecko surface beyond the priority chain's free-tier coverage | COINGECKO_API_KEY |
external.hyperliquid | User-account endpoints not covered by the native minara.perps_analytics skill | per-user signer |
external.cmc-api-* (4 packages) | CoinMarketCap endpoints beyond price / trending | CMC_PRO_API_KEY |
external.okx-defi-portfolio, external.okx-dex-market, external.okx-dex-signal, external.okx-dex-ws, external.okx-audit-log | OKX Web3 / DeFi surfaces not in the native okx-dex provider | OKX 4-var quad |
external.diagram-design, external.strategy-studio | Unrelated to provider routing — preserved unchanged | n/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)
| Variable | What it does | When to flip |
|---|---|---|
DISABLE_METHODOLOGY_INJECTION | Closes 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_RECORDING | Closes the case WRITE path only. recordHint / finalizeTurn no-op; reads continue. | Case-table schema migration in flight; reads should still work. |
DISABLE_METHODOLOGY_MUTATIONS | Closes 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)
| Variable | Default | Effect |
|---|---|---|
METHODOLOGY_SYNTHESIS_FLAG_MEDIAN_BPS | 50 | Weighted-window median ≤ −0.5% (= −50 bps) triggers regime_shift_flagged. Lower = more sensitive. |
METHODOLOGY_SYNTHESIS_FLAG_HIT_RATE | 0.45 | Weighted hit_rate below this triggers flag. Higher = more sensitive. |
METHODOLOGY_SYNTHESIS_DEMOTE_MEDIAN_BPS | 200 | Auto-demote requires median ≤ −2% AND 30d sample size ≥ 10 AND lifetime Wilson > 0.55. |
METHODOLOGY_SYNTHESIS_HALFLIFE_DAYS | 14 | Recency half-life across the 7d/30d/90d/180d windows. Lower = more reactive to recent data. |
METHODOLOGY_STRESS_THRESHOLD_PCT | 25 | Black-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.
| Variable | Default | Notes |
|---|---|---|
METHODOLOGY_PRICE_CAP_MAJOR_CRYPTO | 0.5 | 50% daily |
METHODOLOGY_PRICE_CAP_LAYER_1 | 0.5 | |
METHODOLOGY_PRICE_CAP_LAYER_2 | 0.6 | L2s more volatile |
METHODOLOGY_PRICE_CAP_DEFI_BLUE_CHIP | 0.6 | |
METHODOLOGY_PRICE_CAP_MEME_COIN | 1.0 | Meme coins legitimately move 100%/day |
METHODOLOGY_PRICE_CAP_STABLECOIN | 0.02 | Aggressive — stables shouldn't move > 2% |
METHODOLOGY_PRICE_CAP_STOCK | 0.15 | |
METHODOLOGY_PRICE_CAP_INDEX | 0.1 | |
METHODOLOGY_PRICE_CAP_COMMODITY | 0.2 | |
METHODOLOGY_PRICE_CAP_FOREX | 0.05 |
Case-recorder housekeeping (Phase 2)
| Variable | Default | Effect |
|---|---|---|
METHODOLOGY_HINT_ORPHAN_MS | 1800000 (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.logCLI 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 aboveMethodology 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.
| Variable | Default | Effect |
|---|---|---|
METHODOLOGY_AUDIT_CRON_ENABLED | 0 | Opt-in switch for the in-process audit scheduler. Multi-worker deployments also gate on IS_PRIMARY_WORKER. |
METHODOLOGY_AUDIT_CRON_INTERVAL_MS | 86400000 (24h) | Tick interval. Clamped at runtime to [5min, 30d]. |
METHODOLOGY_AUDIT_WINDOW_DAYS | 30 | Lookback window for the window-bounded dimensions. Clamped to [1, 365]. |
METHODOLOGY_AUDIT_RETENTION_DAYS | 90 | Rows in methodology_audit_reports older than this are pruned once per local day. Clamped to [7, 3650]. |
METHODOLOGY_AUDIT_SKIP_BUSY_THRESHOLD_MS | 180000 (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_TICKS | 4 | Starvation guard. After this many deferred ticks the next tick runs regardless of busy state. Clamped to [0, 100]. |
METHODOLOGY_AUDIT_YIELD_TIMEOUT_MS | 60000 (60s) | Max wait at each mid-pass yield point before resuming despite busy state. Clamped to [0, 10min]. |
DISABLE_METHODOLOGY_AUDIT | unset | 1/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.
| Variable | Default | Effect |
|---|---|---|
PROACTIVE_SUPERVISOR_ENABLED | 1 (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_SEC | 900 (15min) | How often each mandate checks its positions, in seconds. Clamped at runtime to [10s, 1h]. |
PROACTIVE_REDISCOVER_INTERVAL_SEC | 1800 (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 mathapps/agent/src/minara/x402-preauth-config.ts— env loader + cap helpersapps/agent/src/tools/_shared/confirm.ts—shouldAutoExecuteX402helperapps/agent/src/tools/trade.ts—transfer_tokenconsults auto-execute firstapps/agent/src/tools/x402-preauth.ts—x402_preauth_grant / _list / _revokeapps/agent/src/skills/builtin/x402-payment.ts— 4-option preview proseapps/agent/src/gateway/repl-commands.ts—/x402 preauthCLI surface
| Variable | Default | Format | Effect |
|---|---|---|---|
X402_PREAUTH_DEFAULT_TTL_HOURS | 24 | positive 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_HOURS | 168 (7 days) | positive integer (hours) | Hard upper bound for non-forever TTL. Grants requesting more are rejected with field: "ttlMs". |
X402_PREAUTH_ALLOW_FOREVER | 1 | 1/true/yes/on to allow | When 0, ttl_hours: "forever" grants are rejected with field: "forever". The forever-budget cap is still read for documentation. |
X402_PREAUTH_MAX_PER_CALL_USDC | 0.5 | positive 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_USDC | 5 | positive number (USDC) | Total-budget cap for non-forever sessions. |
X402_PREAUTH_MAX_FOREVER_BUDGET_USDC | 20 | positive number (USDC) | Total-budget cap for forever sessions. Independent ceiling because removing the time bound demands tighter $ control. |
X402_PREAUTH_DISABLE | 0 | 1/true/yes/on to disable | Global 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=foreveris the most permissive grant possible. The REPL/x402 preauth granthandler requires double-explicit confirmation for that combination; the LLM toolx402_preauth_grantnever 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.ts—seedWorkspaceIfMissing+atomicWriteFileapps/agent/src/workspace/heartbeat-writer.ts— per-turn## Statewriterapps/agent/src/workspace/daily-log-writer.ts— per-session daily journalapps/agent/src/workspace/dreaming-task.ts— periodic MEMORY.md consolidationapps/agent/src/workspace/bootstrap-handler.ts— archive onBOOTSTRAP_DONEapps/agent/src/workspace/soul-change-detector.ts— disclose SOUL.md edits
| Variable | Default | Format | Effect |
|---|---|---|---|
WORKSPACE_HEARTBEAT_ENABLED | 1 (on) | 0/false/no/off to disable | Per-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_ENABLED | 0 (off) | 1/true/yes/on to enable | When 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_INTERVAL | 5 | positive 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_ENABLED | 0 (off) | 1/true/yes/on to enable | Periodic 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_HOURS | 24 | positive 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_BYTES | 262144 (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_MS | 1800000 (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_DIRand the--workspaceCLI 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
| Variable | Default | Accepted values | Purpose |
|---|---|---|---|
REPORT_BUNDLE_CHART_PNGS | unset (off) | 1 / true to enable | Falls 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.
| Var | Default | Format | Purpose |
|---|---|---|---|
SHORTCUT_QUESTIONS_TTL_MS | 21600000 (6 hours) | positive int milliseconds | How 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_DISABLED | unset (off) | 1 to disable | Skip 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.
| Var | Default | Format | Purpose |
|---|---|---|---|
SOLANA_RPC_URL | https://api.mainnet-beta.solana.com | HTTPS URL | Solana 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.
| Var | Default | Format | Purpose |
|---|---|---|---|
MINARA_SYSTEM_METRICS_DISABLED | unset (off) | 1 to disable | Disable 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_WINDOW | 5 | positive integer | How 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.examplein 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.exampleprose - 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