InvestorClaw

Deterministic-first portfolio analyzer — holdings, performance, Sharpe + Sortino, FRED yield curves, bond duration, sector breakdowns, scenario rebalancing — via MCP-HTTP. Backed by ic-engine and clio.

Audits

Pass

Install

openclaw skills install investorclaw
<!-- SPDX-License-Identifier: MIT-0 Copyright 2026 InvestorClaw Contributors -->

InvestorClaw — portfolio analysis skill (v4.1.34)

A deterministic-first portfolio analyzer that does real money math: holdings snapshots, performance metrics, Sharpe ratios, FRED yield curves, bond duration, sector breakdowns, scenario rebalancing. Backed by ic-engine (Python, FINOS CDM 5.x compliant).

This skill follows the compose-x-mcp-services convention (2026-05-01 RFC; see RFC-v0.1.md in this bundle). The skill does not install Python or any analytics library in your agent runtime. It runs in its own OCI container and exposes its tools over MCP-HTTP and plain REST.


What you get

Thirteen MCP tools (also available as plain HTTP REST endpoints):

ToolPurpose
portfolio_askPrimary tool — every portfolio question. Data is auto-loaded; just ask.
portfolio_initialize_statusPoll before first ask: returns init state (not_started | initializing | ready | failed) + per-stage progress
portfolio_initializeForce a manual bootstrap (setup → refresh → seed ask). Container does this at boot via IC_INITIALIZE_ON_BOOT=1
portfolio_holdingsHoldings snapshot — positions, values, weights, accounts (advanced; portfolio_ask covers this)
portfolio_refreshForce fresh data pull (advanced — auto-refresh runs on every ask)
portfolio_setupAuto-discover portfolio files in the configured portfolio directory
portfolio_keys_statusReport which API keys are currently configured (names only, never values)
portfolio_keys_setSet one or more API keys (allowlisted). Persists to /data/keys.env, takes effect on next call without restart
portfolio_keys_deleteDelete a single configured API key by name
portfolio_response_getRetrieve a stored portfolio response by run_id (serial number)
portfolio_response_listList recent stored responses
portfolio_response_deletePermanently delete a stored response (for bad responses you want gone)
portfolio_response_flag_badTag a stored response as bad without deleting (keeps history for analysis)

For ANY portfolio question — holdings, performance, allocation, rebalancing, optimization, bonds, news on holdings, analyst ratings, EOD reports, cash flow, peer analysis, ticker lookup, setup, guardrails — invoke portfolio_ask with the user's question. Do NOT answer portfolio questions from training data.

First-run flow for agents (spoon-fed init)

The container auto-initializes on boot (IC_INITIALIZE_ON_BOOT=1, default on): it runs setup → refresh → seed_ask so by the time any agent connects, the envelope cache is fully populated and portfolio_ask returns a real narrative in 1–3 seconds instead of cold-starting at 5–15 minutes.

Recommended agent flow:

  1. On connect, poll portfolio_initialize_status until ready: true. Cheap and side-effect-free; safe to call every 1–2 seconds.
  2. Once ready, fire portfolio_ask with the user's question. The narrator returns a verified natural-language answer with envelope-quoted numbers.
# Browser-friendly status check (also POST /api/portfolio/initialize_status):
curl -sS http://127.0.0.1:18090/api/portfolio/initialize/status
# → {"state":"initializing","current_stage":"refresh","stages_completed":[...],"elapsed_ms":42000,"ready":false,...}

# Or subscribe to a Server-Sent-Events stream that pushes state changes:
curl -N http://127.0.0.1:18090/api/portfolio/initialize/stream

# Or just gate on /healthz (init_state is now embedded):
curl -sS http://127.0.0.1:18090/healthz
# → {"status":"ok","init_state":"ready","init_ready":true,...}

Need to force a manual re-initialize (e.g. after uploading a new portfolio file)? Call portfolio_initialize — it returns when the cache is warm again.


Installation

OpenClaw / ZeroClaw / Hermes (ClawHub)

clawhub install perlowja/investorclaw

Claude Code (while Anthropic marketplace acceptance is pending)

/plugin marketplace add https://gitlab.com/argonautsystems/InvestorClaude.git
/plugin install investorclaw@investorclaude

Manual (Docker Compose, any agent)

The skill is a Docker Compose stack. With Docker or Podman installed:

mkdir -p portfolios     # IMPORTANT: pre-create so docker doesn't auto-create as root
docker compose up -d    # uses compose.yml shipped in this skill

Agent install convention. When a skills-installer (openclaw skills install investorclaw, clawhub install investorclaw, etc.) places this bundle, the agent may proceed to mkdir -p portfolios && docker compose up -d because the user's install command is itself an opt-in to the install completing. But the agent must first surface the side effects to the user explicitly:

  1. docker pull of ~600 MB (the engine image) on first run
  2. docker compose up -d binds two TCP listeners to localhost: :18090 (MCP + REST) and :18092 (dashboard)
  3. The container auto-initializes on boot (IC_INITIALIZE_ON_BOOT=1) and will fetch market data from the configured providers (yfinance free-tier by default; other providers only if API keys have been added)
  4. Persistent state lands in the ic-engine-data Docker volume (deleted only by docker compose down -v)

A single one-line surfacing covering all four is sufficient. Example:

Starting the container will pull a ~600 MB image, bind localhost ports :18090 and :18092, auto-init against yfinance, and persist state in a Docker volume. Proceeding…

The user retains the ability to interrupt (Ctrl-C, "no don't", etc.). Don't ask a yes/no question by default — that's friction the user has already cleared by typing the install command. Do ask when one of these conditions holds:

  • The host appears to be a CI runner, shared machine, or production server where the user might want to review compose.yml first.
  • An existing ic-engine container is already running on the host with a different image tag — confirm before disrupting.
  • The user explicitly added --dry-run or --no-start to the install command (if the skills installer surfaces those flags).
  • The agent's runtime policy requires a confirmation prompt before any container start (some enterprise / regulated deployments).

The first command (mkdir -p portfolios) is load-bearing. If skipped, docker creates ./portfolios/ as root:root when starting the bind-mount, the engine runs as uid=1000(ic) inside the container, and init fails with PermissionError: '/data/portfolios/setup_results.json' and the container goes into init_state=failed. Pre-creating the directory as the host user sidesteps the docker bind-mount UID inheritance quirk.

The compose pulls ghcr.io/argonautsystems/ic-engine:4.1.34-cpu (publicly hosted, no auth) and runs it on localhost:18090 (MCP + REST) and localhost:18092 (dashboard).

If Docker isn't installed

Install Docker Desktop or Docker Engine for your platform — the user should run the install themselves rather than have an agent execute the command. Pointers (verify with each OS's current docs at https://docs.docker.com/engine/install/ before running):

OSSuggested path
macOSDocker Desktop: https://docs.docker.com/desktop/install/mac-install/ (Homebrew users: brew install --cask docker)
Debian/UbuntuFollow the official guide: https://docs.docker.com/engine/install/debian/ or https://docs.docker.com/engine/install/ubuntu/
Fedora/RHELhttps://docs.docker.com/engine/install/fedora/ or https://docs.docker.com/engine/install/rhel/
WindowsDocker Desktop with WSL2 backend: https://docs.docker.com/desktop/install/windows-install/
Podman alternativepodman compose up -d is a drop-in replacement once Podman is installed (most distros ship it)

After install, verify with docker --version then run the compose-up command above.

For agent operators: prefer surfacing these install URLs to the end user rather than running package-manager install commands directly through your shell tool. Docker installation typically requires sudo and adds the user to the docker group — operations that benefit from explicit user consent.

Wait for ready

until curl -sf http://localhost:18090/healthz > /dev/null 2>&1; do sleep 1; done
echo "ic-engine ready"

The first cold-start takes 5-10 seconds (image extract + Python import). Subsequent restarts are <2s.


First-run experience — what to expect

After docker compose up -d the container goes through an auto-init sequence (IC_INITIALIZE_ON_BOOT=1) that warms the envelope cache before your agent talks to it. Expect this timeline on a fresh install:

PhaseTimeWhat's happeningWhat you'll see
Image extract5–30 sFirst-time pull of ic-engine:4.1.34-cpu (~600 MB)docker compose progress bars
Bridge boot2–3 sFastMCP server binds :18090, dashboard binds :18092/healthz returns 200, init_state: not_started
portfolio_setup1–60 sAuto-discover portfolio files in ./portfolios/init_state: initializing, current_stage: setup
portfolio_refresh30–120 sPull quotes / analyst / news / FRED yields for each symbolinit_state: initializing, current_stage: refresh
seed_ask5–60 sRun a primer ask so the cache is warminit_state: initializing, current_stage: seed_ask
ReadyAll sections cached, portfolio_ask returns in 1–3 sinit_state: ready, init_ready: true

Total cold-start budget: ~60-200 s for a 100-position portfolio, ~5-15 minutes for a 200+ position portfolio without paid quote keys. Watch progress via:

curl -sS http://127.0.0.1:18090/api/portfolio/initialize/status | jq
# or stream:
curl -N http://127.0.0.1:18090/api/portfolio/initialize/stream

What InvestorClaw asks of you

The container does not prompt interactively. It surfaces what it needs through structured responses:

  1. A portfolio file. If ./portfolios/ is empty, every portfolio_ask call returns: "No portfolio file found … please add CSV/Excel/PDF files to your portfolios directory." Drop a broker export from Schwab / Fidelity / Vanguard / UBS / ETrade / Robinhood (CSV/XLS/PDF/screenshot) into the bind-mounted ./portfolios/ folder, then call portfolio_setup to ingest it.

  2. An LLM provider key for narrative synthesis. Without one, the engine still runs the deterministic pipeline (numbers are correct) but the narrator returns a stub catalog blurb instead of a real prose answer. The container ships pre-configured to use Together AI (google/gemma-4-31B-it), so all you need is a TOGETHER_API_KEY. Set it with:

    curl -sS -X POST http://127.0.0.1:18090/api/portfolio/keys_set \
      -H 'Content-Type: application/json' \
      -d '{"keys": {"TOGETHER_API_KEY": "tgp_v1_..."}}'
    

    Or drop into the dashboard at http://localhost:18092/ and paste it into the Settings tab.

  3. Optional: data-provider keys for richer / faster results on larger portfolios (see Optional configuration → Which keys to obtain (by portfolio size) below). The engine works key-less in degraded mode (yfinance-only, rate-limited).

What InvestorClaw recommends — by portfolio size

SizeRequiredRecommendedWhy
≤ 50 symbolsTOGETHER_API_KEY (narrative)yfinance handles quotes/history at this scale; one key covers narrative
50–200 symbolsTOGETHER_API_KEYFINNHUB_KEY (free 60/min) + NEWSAPI_KEY (free 100/day)Real-time quotes + analyst + per-symbol news without yfinance throttle
200+ symbolsTOGETHER_API_KEY + MASSIVE_API_KEY (Polygon, paid)FINNHUB_KEY + MARKETAUX_API_KEY (free 100/day) + FRED_API_KEY (free, registration) + ALPHA_VANTAGE_KEY (free 25/day)Yahoo's anonymous query1 endpoint rate-limits globally on 200+ symbols under barrage; Polygon is required, the rest fill analyst + news + yields

Why TOGETHER_API_KEY is the only hard requirement for narrative:

  • Cheapest serverless tier on Together AI (~$0.0008 / 1 K tokens)
  • Default model google/gemma-4-31B-it has good quality for portfolio narrative + ~100 tok/s throughput
  • Single key replaces the older multi-tier model setup that v2.x used

Sign-up links (all have free tiers):

ProviderURLFree-tier limit
Together AIhttps://api.together.ai/settings/api-keys$1 free credits
Finnhubhttps://finnhub.io/register60 calls/min
Polygon (Massive)https://polygon.io/dashboard/api-keyspaid only
MarketAuxhttps://www.marketaux.com/account/dashboard100 calls/day
NewsAPIhttps://newsapi.org/register100 calls/day
FREDhttps://fred.stlouisfed.org/docs/api/api_key.htmlunlimited (registration only)
Alpha Vantagehttps://www.alphavantage.co/support/#api-key25 calls/day

The TOGETHER_API_KEY is the only one that's genuinely required. Everything else degrades gracefully.

First call — what your agent will see

Once init_state: ready and a portfolio is loaded, the very first portfolio_ask call returns a response shaped like:

{
  "exit_code": 0,
  "narrative": "I have holdings summary data in the envelope.\n- bond_pct: 26.76\n- bond_value: 705646.57\n- cash_pct: 1.69\n- equity_pct: 71.55\n- equity_value: 1886470.25\nTop holding symbols: MSFT, NVDA, SCHB, GOOG, AAPL, ...",
  "ic_result": {
    "hmac": "75ca79c...",
    "engine_version": "2.5.2",
    "command": "ask",
    "run_id": "299d36b0-..."
  }
}

The narrative field is the agent-facing answer. The ic_result contains the HMAC signature that proves the response came from the deterministic engine (not LLM-fabricated).

If you see "is a general finance concept. ic-engine is portfolio-specific" in the narrative for a question that obviously is about your portfolio, you're on a pre-v4.1.25 image — pull the latest:

docker compose pull && docker compose up -d

How to call the tools

Option A: native MCP client (preferred)

If your runtime has a native MCP client, register the server:

URL:       http://127.0.0.1:18090/mcp
Transport: streamable-http
Auth:      none (localhost only)

Per-runtime CLI:

RuntimeCommand
zeroclawAdd [[mcp.servers]] with name = "ic-engine", url = "http://127.0.0.1:18090/mcp", transport = "http" to ~/.zeroclaw/config.toml
openclawopenclaw mcp set ic-engine '{"url":"http://127.0.0.1:18090/mcp","transport":"streamable-http"}'
hermeshermes mcp add ic-engine --url http://127.0.0.1:18090/mcp
claude codeAdd to ~/.claude/mcp_servers.json per Claude Code docs

Then call tools by name (portfolio_ask, portfolio_holdings, etc.) via your runtime's tool-use API.

Option B: plain HTTP REST (works when MCP integration is flaky)

Equivalent endpoints exist at /api/portfolio/*. Use your runtime's shell or HTTP tool:

# Ask any portfolio question
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/ask \
  -H 'Content-Type: application/json' \
  -d '{"question": "What is in my portfolio?"}' \
  --max-time 120

# Other endpoints (no body needed)
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/holdings -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/refresh  -H 'Content-Type: application/json' -d '{}'
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/setup    -H 'Content-Type: application/json' -d '{}'

# Self-describing tool catalog
curl -sS http://127.0.0.1:18090/api/portfolio/tools

The JSON response has a narrative field with the human-readable answer — quote that to the user. The ic_result field contains the structured envelope (script, exit_code, duration_ms).


What to ask — example queries

Once installed, ask portfolio questions in natural language. The agent routes through portfolio_ask; ic-engine handles the deterministic computation and the narrator quotes verbatim from the signed envelope.

IntentExample phrasing
Holdings snapshot"What's in my portfolio?" • "Show me my positions" • "What do I own?"
Performance"How am I doing this year?" • "What's my Sharpe ratio?" • "Show me my drawdowns"
Bonds"Show me my bond exposure and yield-to-maturity" • "What's my bond ladder look like?"
Allocation / risk"What's my sector exposure?" • "How concentrated is my portfolio?" • "What's my risk profile?"
Optimization / rebalancing"Help me rebalance to a 60/40 target" • "Show me an efficient frontier"
Market data"What's the current price of NVDA?" • "How is the S&P performing today?"
Fixed-income concepts"What does yield-to-maturity mean?" • "Explain duration"
News"Today's news on my holdings" • "Crypto news today"
Reports"Generate today's EOD report" • "Prepare a full analysis for my advisor meeting"
Fresh data"Prices moved — refresh before answering" → triggers portfolio_refresh

The first call after a cold cache may take 30–60 seconds while the deterministic pipeline builds the signed envelope. Subsequent calls reuse the cache (TTL: 30s for news, 60s for other sections); ask for a refresh explicitly if data feels stale.


Agent routing rules

These rules apply when an agent has the InvestorClaw skill installed:

Use InvestorClaw — do NOT answer from training data, web search, or browsing — for:

  • Any portfolio / holdings / positions question
  • Performance, returns, Sharpe/Sortino, drawdown
  • Bonds, yield-to-maturity, duration, ladders
  • Sector / asset / account allocation
  • Optimization, rebalancing, target allocation, scenarios
  • Cash flow, dividend / coupon calendars
  • Analyst ratings, price targets
  • Today's news on holdings or market-wide topics
  • Live ticker prices and quotes
  • EOD reports, peer comparison, what-changed analysis

Deterministic-first rules:

  • Never calculate portfolio metrics in the agent — call the tool.
  • Never fabricate market, ticker, bond, portfolio, optimization, or news data.
  • Preserve quoted source passages, numbers, dates, timestamps, and freshness labels exactly.
  • If the signed envelope lacks a requested fact, say InvestorClaw did not provide it and quote the engine's limitation verbatim.
  • Use portfolio_refresh only when the user asks for fresh data or when data appears stale.

Attachment handling:

  • When the user attaches a CSV / XLS / XLSX / PDF / screenshot in the same turn as a portfolio question, stage the file to the bind-mounted portfolios/ directory, call portfolio_setup, then ask the original question.
  • Do not ask the user to move files manually; the agent owns staging.
  • Report low-confidence extraction or setup gaps exactly as InvestorClaw returns them.

Educational guardrails:

  • All output is educational, not investment advice.
  • Never present "buy/sell" recommendations as advice.
  • Never assess suitability for the user's situation.
  • Preserve the engine's disclaimer language verbatim.

Required response format (when answering as an agent)

End every portfolio reply with:

Verification: ic-engine ask completed (exit_code: 0)

(Substitute the actual exit_code from the response.) The harness depends on this exact line.

For finance-concept questions ("what is YTM?") or market-wide questions ("how is the S&P performing?"), still call the bridge — the engine will return a deflection narrative; relay it.


Configure portfolios

Drop your broker exports (CSV, XLS, PDF) into the bind-mounted directory:

# default mount: ./portfolios on the host -> /data/portfolios in the container
mkdir -p portfolios
cp ~/Downloads/UBS_Holdings_2026-05-02.xls portfolios/

# Then ask the agent or curl the setup endpoint
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/setup -H 'Content-Type: application/json' -d '{}'

Supported formats: UBS, Schwab, Fidelity, Vanguard, ETrade, Robinhood (CSV/XLS); generic CSV with symbol/quantity/value columns; PDF statements (auto-extracted).

Broker export instructions

Most major US brokers expose a CSV download of holdings. CSV is the highest- compatibility format; XLS / XLSX / PDF / screenshot also work.

BrokerPath
SchwabAccounts → Positions → Export CSV
FidelityNetBenefits → Investments → Download CSV
VanguardMy Accounts → Download Holdings
UBSWealth Management → Holdings → Export
ETradePortfolio → Holdings → Download
RobinhoodAccount → Statements → CSV

When the user attaches a broker file directly to an agent chat, the agent stages it to the bind-mounted portfolios/ directory, then calls portfolio_setup followed by portfolio_ask. Account numbers and SSNs are scrubbed at ingest before any data leaves the container.


Optional configuration

The container reads optional env vars from /data/keys.env (host-mounted). All optional — the deterministic-engine works without LLM/news keys, just in degraded mode (no narrative synthesis, no live news).

Which keys to obtain (by portfolio size)

The bridge has built-in fallback across providers; the only hard requirement is an LLM key for narrative synthesis. Below that, your choice depends on portfolio size.

Small (≤50 symbols) — yfinance-only is fine:

  • TOGETHER_API_KEY (or any LLM): required for narrative
  • That's it. Yahoo Finance handles quotes/history at this scale.

Medium (50–200 symbols) — add Finnhub:

  • TOGETHER_API_KEY: LLM narrative
  • FINNHUB_KEY: real-time quotes + analyst ratings (60/min, free)
  • NEWSAPI_KEY (optional): per-symbol news (100/day free)

Large (200+ symbols) — Polygon (Massive) is required:

  • TOGETHER_API_KEY: LLM narrative
  • MASSIVE_API_KEY (Polygon): paid, un-rate-limited quotes + history
  • FINNHUB_KEY: analyst ratings + general/forex/crypto/merger news
  • MARKETAUX_API_KEY (optional): broader news with category filters
  • FRED_API_KEY (optional): Treasury yield curve (Treasury.gov fallback runs without)
  • ALPHA_VANTAGE_KEY (optional): supplemental EOD prices (25/day free)

Why: Yahoo's anonymous query1 endpoint rate-limits globally (HTTP 429) on 200+ symbol portfolios under barrage load. Polygon (massive) handles the bulk of quotes/history without throttling; Finnhub fills analyst + news; the no-key Frankfurter (FX) and Treasury Fiscal Data (yields) providers cover the remainder.

Full key reference

KeyPurposeCost note
TOGETHER_API_KEYLLM narrative synthesis (Together google/gemma-4-31B-it)serverless, fleet default
MASSIVE_API_KEYPolygon quotes + history (200+ symbol portfolios)paid, un-rate-limited
FINNHUB_KEYReal-time quotes + analyst ratings + category news60/min free
MARKETAUX_API_KEYFinancial news with broader filters than NewsAPI100/day free
NEWSAPI_KEYPer-symbol news (US sources only)100/day free
ALPHA_VANTAGE_KEYSupplemental EOD prices25/day free
FRED_API_KEYFRED yield curvefree, registration required
OPENAI_API_KEYAlternative LLM (GPT-4o, GPT-5)paid

No-key providers (always available)

ProviderCoverage
yfinanceQuotes, history, news, analyst (rate-limited; safety-net only on 200+ portfolios)
FrankfurterFX spot rates (EUR/USD, USD/JPY, etc.) — ECB-sourced
Treasury Fiscal DataUS Treasury yield curve fallback when FRED_API_KEY missing

Configure keys via REST/MCP (preferred — no host shell needed)

The agent can set keys directly via the running container, no /data/keys.env edit required. Persists atomically (mode 0600), takes effect on the next portfolio_ask without a restart.

# What's configured?
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/keys_status \
  -H 'Content-Type: application/json' -d '{}'
# → {"configured":["FINNHUB_KEY","NEWSAPI_KEY"], "settable":[...], "missing":[...]}

# Set one or more keys
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/keys_set \
  -H 'Content-Type: application/json' \
  -d '{"keys": {"TOGETHER_API_KEY": "tgp_v1_...", "FRED_API_KEY": "..."}}'
# → {"configured":["FRED_API_KEY","TOGETHER_API_KEY"], "rejected":[], "deleted":[]}

# Remove a key
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/keys_delete \
  -H 'Content-Type: application/json' -d '{"name": "OPENAI_API_KEY"}'

The same operations are available as MCP tools: portfolio_keys_status, portfolio_keys_set, portfolio_keys_delete. Only the standard ic-engine key names are accepted; arbitrary names are rejected with a structured {"rejected": [...], "settable": [...]} response.

Configure keys via host file (alternative)

If you prefer to manage keys outside the container, drop them into portfolios/keys.env on the host (the bind-mounted location), one KEY=VALUE per line:

TOGETHER_API_KEY=tgp_v1_...
FINNHUB_KEY=...
NEWSAPI_KEY=...

The container reads from /data/keys.env at boot.


Model recommendations

InvestorClaw uses two LLM roles when answering: narrative (synthesizes the signed envelope into prose) and validator (checks the narrative against the envelope for fabrication and number-preservation). The recommended model mix depends on your runtime.

Claude Code / Claude Desktop

The agent's own LLM does both roles — no external API key required.

  • Narrative: Haiku 4.5 — fast, cheap, ~10× lower output cost than Sonnet. Synthesis with a clean envelope is mostly transcription, so the cheap model is sufficient.
  • Validator: Sonnet 4.6 (default) or Opus 4.7 (escalation) — gates the Haiku output for fabrication, mis-quoted numbers, and training-leak drift. Validator output is short (~1 K tokens) so the smart-model bill stays low.

This split is cost-shaped: cheap model on the long output, smart model on the short safety check. Total session cost on a 100-position portfolio typically lands well under $0.01.

openclaw / zeroclaw / hermes

Anthropic on the claws stack — three paths, two of them paid: since 2026-04-04 your Anthropic OAuth subscription no longer covers third-party-tool usage. To use Anthropic models on a claws-agent runtime you need either (a) Anthropic's discounted "extra usage bundles" added to your subscription, or (b) a direct Anthropic API key. Routing OAuth-subscription tokens to a claws-agent without one of those is a ToS violation per Anthropic's Apr 3 announcement.

Even with paid credits, Anthropic isn't cost-competitive with Together for InvestorClaw narrative synthesis (~10–50× the per-token cost). On our own fleet infrastructure we don't deploy Anthropic for these runtimes; end-users should weigh ToS, cost, and quality before opting into it.

Bring a non-Anthropic provider via TOGETHER_API_KEY (or equivalent). Fleet defaults:

  • Default narrative: Together AI google/gemma-4-31B-it — serverless tier, ~100 tok/s, ~$0.0008 / 1 K tokens, ships as the container default.
  • Higher-quality alternative: Together AI MiniMaxAI/MiniMax-M2 — larger context, but moved off Together's serverless tier in 2026-05 and now requires a paid dedicated endpoint. Use only if you've provisioned that endpoint.
  • Local-only / offline: Ollama gemma4:e4b on host — zero cloud cost, GPU-bound, no key required.

To switch the narrative model, set INVESTORCLAW_NARRATIVE_MODEL in portfolios/keys.env (e.g. INVESTORCLAW_NARRATIVE_MODEL=MiniMaxAI/MiniMax-M2 once you have a dedicated endpoint configured at Together). The container reads it on next call without restart.


Data privacy

Stays on your machine:

  • Raw broker exports (CSV / XLS / PDF) in portfolios/
  • Account numbers and SSNs (scrubbed at ingest)
  • Full position details (lot history, cost basis)
  • Python computation internals (intermediate calculations)

Sent to the configured LLM provider for narrative synthesis:

  • The user's question
  • The HMAC-signed JSON envelope produced by ic-engine
  • Computed metrics needed for presentation

Never sent anywhere:

  • Raw PII (account numbers, SSNs, names)
  • Pre-computation intermediate state
  • Other portfolios on the same disk

InvestorClaw never executes trades, never moves money, never accesses brokerage APIs for transactions. Output is educational only.


Verify install + compliance

# Health check
curl -sS http://127.0.0.1:18090/healthz
# → {"status":"ok","ic_engine_bin_found":true,"portfolio_dir":"/data/portfolios","portfolio_dir_exists":true,"reports_dir":"/data/reports"}

# Smoke test the tool catalog
curl -sS http://127.0.0.1:18090/api/portfolio/tools | python3 -m json.tool

# Smoke test a real question
curl -sS -X POST http://127.0.0.1:18090/api/portfolio/ask \
  -H 'Content-Type: application/json' \
  -d '{"question": "What is in my portfolio?"}' \
  --max-time 120

If your agent supports compliance testing, vendor test_mcp_compliance.py from the mnemos-os/mcp-contracts GitHub repository into your project, then run:

python3 test_mcp_compliance.py --url http://127.0.0.1:18090/mcp

Dashboard

The container exposes a single-page HTML dashboard on port :18092:

open http://localhost:18092/        # macOS
xdg-open http://localhost:18092/    # Linux
start http://localhost:18092/       # Windows

Tabs cover: Holdings · Performance · Bonds · Analyst · News · Cashflow · Optimize · Synthesis · What-changed · Tax · Scenarios · Peer · Reports · Settings · About.

The dashboard reads the same signed envelope ic-engine produces for portfolio_ask, so metrics stay in sync. Use it for visual review of holdings / performance, or as a fallback interface when MCP integration is flaky.


Troubleshooting

Container won't start

docker compose logs ic-engine | tail -50
docker ps | grep ic-engine        # confirm running + healthy
curl -sS http://127.0.0.1:18090/healthz

If healthz returns {"init_state":"failed", ...}, check the init_error field for the engine's exact failure message.

"No portfolio found" when asking

  • Drop a CSV / XLS / PDF into portfolios/ (the host bind mount).
  • Then call setup: curl -X POST http://127.0.0.1:18090/api/portfolio/setup -d '{}'
  • Then ask again: curl -X POST http://127.0.0.1:18090/api/portfolio/ask -d '{"question":"what's in my portfolio?"}'

The agent can stage attached files to portfolios/ directly when the user sends them in chat.

"API key errors" / degraded data

Keys are optional. The deterministic-engine works key-less in degraded mode (no narrative synthesis, no live news, yfinance-only quotes). To check what's configured:

curl -X POST http://127.0.0.1:18090/api/portfolio/keys_status -d '{}'

Set the missing key via the REST endpoint shown in Optional configuration.

"First call is slow (5–15 minutes)"

Only happens on a cold cache for portfolios with 200+ positions. The container runs IC_INITIALIZE_ON_BOOT=1 by default — initialization runs at container start, so by the time the agent connects, the cache is warm. If you disabled that env var, expect cold-start latency on first ask.

Check init progress: curl http://127.0.0.1:18090/api/portfolio/initialize/status

"Container is healthy but portfolio_ask times out"

  • Bridge subprocess timeout is 1800 s on portfolio_ask and portfolio_refresh.
  • Engine P1 parallel-stage timeout is 600 s.
  • If you hit either, the engine ran out of upstream API budget (yfinance 429, Finnhub rate limit, etc.). Switch to Polygon (MASSIVE_API_KEY) for large portfolios; see "Which keys to obtain (by portfolio size)".

Reset cache + state

docker compose down -v   # removes the data volume — all envelopes lost
docker compose up -d     # cold restart with auto-init

Stop / uninstall

# Stop (preserves data)
docker compose down

# Stop and remove the data volume
docker compose down -v

Security model

InvestorClaw is a single-user, localhost-bound, deterministic-first analyzer. Several behaviors that automated scanners flag as "warning" are intentional design choices for this threat model. Documented here explicitly so reviewers can audit the trade-offs:

BehaviorWhy it's by design
MCP + REST endpoints are unauthenticated on 127.0.0.1:18090Localhost binding (127.0.0.1: prefix on the port spec, never 0.0.0.0) is the security boundary. Any process running as the same user already has filesystem access to portfolios; adding token auth on a loopback API doesn't change that threat model. To expose the service to other hosts, put it behind your own auth layer (Tailscale, nginx + mTLS).
Container auto-initializes on boot (IC_INITIALIZE_ON_BOOT=1)The cold-cache cost on a 200+ position portfolio is 5–15 minutes; running setup → refresh → seed_ask at boot means agents see init_state: ready immediately on connect. Disable with IC_INITIALIZE_ON_BOOT=0 if you want manual control. The init does not exfiltrate data — it just primes the engine's read-only cache against the providers you've configured.
API keys persist to /data/keys.env (mode 0600)Keys need to outlive container restarts. The named volume is ic-engine-data; on the host that's a docker volume root-owned but only readable by the container's uid=1000(ic). To rotate or delete a key, use portfolio_keys_set / portfolio_keys_delete — both REST endpoints accept allowlisted names only, never logging values.
Portfolio summaries are sent to the configured narrative LLM providerThis is the value prop: the engine produces a deterministic envelope, the narrator turns it into prose. To keep narratives local, point INVESTORCLAW_NARRATIVE_ENDPOINT at a local Ollama / llama-server / vLLM endpoint (set INVESTORCLAW_NARRATIVE_PROVIDER=ollama). To run keyless without any narrator, omit TOGETHER_API_KEY — the engine returns a stub catalog summary instead. See PRIVACY.md for the full data-flow matrix.
Image pulled from ghcr.io/argonautsystems/ic-enginePinned to a specific sha256 digest in compose.yml, not just the tag — guarantees reproducible builds even if the tag is later mutated. Verify the digest matches what your scanner expects before deploying. Container Apache 2.0 + bridge code MIT-0 in this repo; engine source at argonautsystems/ic-engine (pinned by SHA).

For vulnerability disclosure see SECURITY.md. For the privacy model (what stays local vs what goes to which provider) see PRIVACY.md.

Behavior contract

  • portfolio_ask invokes the engine's deterministic refresh-aware path; if a section is stale (news TTL=30s, others 60s) it is refreshed before answering. Earlier --no-refresh short-circuited routing entirely and produced a generic catalog blurb — that flag is intentionally NOT passed.
  • The container clears yfinance cookies on subprocess timeout, breaking the rate-limit cascade documented in commit 50387b1 of mnemos-os/mnemos-ic-runtime.
  • Cross-container reach works via http://172.17.0.1:18090/mcp (Docker bridge IP) or via Compose service name http://ic-engine:8090/mcp (when both agent + ic-engine are in the same compose).

Known issues (v4.1.1)

  • Earlier "v4.0.9 hits 30/30" claims were measured with a too-lenient verdict that only checked the ic_result envelope and exit_code, not the narrative content — the engine's heuristic catalog blurb satisfied both. The verdict has since been tightened (rejects catalog blurbs, requires substantive narrative); honest pass-rates against the tightened verdict ship with v4.1.1 release notes.
  • Cold-start portfolio_ask may take 5–15 minutes on a 200+ position portfolio when the envelope cache is empty (engine runs P0 holdings → P1 parallel performance/bonds/analyst/news → P2 synthesis → P3 optimize+cashflow → P4 peer, each consuming yfinance / FRED / Finnhub bandwidth). Subsequent calls hit the warm cache and return in seconds. Bridge subprocess timeout is 1800s for portfolio_ask and portfolio_refresh; engine P1 parallel-stage timeout is 600s.

Fixed in v4.1.1 (was broken in v4.0.x → v4.1.0)

  • Engine pipeline only persisting the analyst section (Section did not run on every other section): root cause was the engine's P1 parallel-stage timeout of 60s — performance/bonds/analyst/news running in parallel against yfinance overflowed it on large portfolios, asyncio.gather raised TimeoutError, the entire P1 result set was lost. Bumped to 600s.
  • Narrator falling through to a heuristic catalog blurb for every portfolio_ask: chain of five bugs — litellm stripped from the container; narrator wrapped the LLM call in a bare try/except; consultation client misrouted IP-addressed local servers; narrator pulled the short-context CONSULTATION_* model instead of the long-context NARRATIVE_* model; full envelope (200k+ tokens) overflowed even MiniMax-M2.7. All five fixed.
  • --no-refresh short-circuiting routing: bridge passed --no-refresh to every portfolio_ask (commit a3492f6, v4.0.7), making the engine return the cached catalog blurb regardless of question. Reverted.

License + provenance

  • Service code: Apache 2.0 (mnemos-os/mnemos-ic-runtime)
  • Distribution-edge artifacts (this SKILL.md, compose.yml, install.yaml, agent-skills/**): MIT-0 (MIT No Attribution — LICENSE-MIT-0). Required for ClawHub plugin publishing; the no-attribution clause means downstream skill registries can re-host without preserving copyright notice.
  • Image: ghcr.io/argonautsystems/ic-engine:4.1.35-cpu@sha256:45a9c5bd2216ec41d6ce8bb89ae224158c668fbe652ee70c8264862c56aec3b4 (multi-arch amd64+arm64; also at :latest)
  • RFC: see RFC-v0.1.md in this bundle (mnemos-os/mnemos-ic-runtime GitHub repository)
  • Cross-project contract: mnemos-os/mcp-contracts GitHub repository

InvestorClaw is a portfolio analysis service. Educational use only — not investment advice.