Deterministic-first portfolio analyzer — holdings, performance, Sharpe + Sortino, FRED yield curves, bond duration, sector breakdowns, scenario rebalancing —...
---
name: investorclaw
description: 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.
homepage: https://github.com/argonautsystems/InvestorClaw
user-invocable: true
metadata: {"license":"MIT-0","version":"4.1.36","image":"ghcr.io/argonautsystems/ic-engine:4.1.36-cpu","mcp-endpoint":"http://localhost:18090/mcp","transport":"streamable-http"}
---
<!--
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):
| Tool | Purpose |
|---|---|
| **`portfolio_ask`** | **Primary tool — every portfolio question. Data is auto-loaded; just ask.** |
| `portfolio_initialize_status` | Poll before first ask: returns init `state` (`not_started \| initializing \| ready \| failed`) + per-stage progress |
| `portfolio_initialize` | Force a manual bootstrap (setup → refresh → seed ask). Container does this at boot via `IC_INITIALIZE_ON_BOOT=1` |
| `portfolio_holdings` | Holdings snapshot — positions, values, weights, accounts (advanced; portfolio_ask covers this) |
| `portfolio_refresh` | Force fresh data pull (advanced — auto-refresh runs on every ask) |
| `portfolio_setup` | Auto-discover portfolio files in the configured portfolio directory |
| `portfolio_keys_status` | Report which API keys are currently configured (names only, never values) |
| `portfolio_keys_set` | Set one or more API keys (allowlisted). Persists to `/data/keys.env`, takes effect on next call without restart |
| `portfolio_keys_delete` | Delete a single configured API key by name |
| `portfolio_response_get` | Retrieve a stored portfolio response by run_id (serial number) |
| `portfolio_response_list` | List recent stored responses |
| `portfolio_response_delete` | Permanently delete a stored response (for bad responses you want gone) |
| `portfolio_response_flag_bad` | Tag 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.
```bash
# 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)
```bash
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:
```bash
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):
| OS | Suggested path |
|---|---|
| **macOS** | Docker Desktop: <https://docs.docker.com/desktop/install/mac-install/> (Homebrew users: `brew install --cask docker`) |
| **Debian/Ubuntu** | Follow the official guide: <https://docs.docker.com/engine/install/debian/> or <https://docs.docker.com/engine/install/ubuntu/> |
| **Fedora/RHEL** | <https://docs.docker.com/engine/install/fedora/> or <https://docs.docker.com/engine/install/rhel/> |
| **Windows** | Docker Desktop with WSL2 backend: <https://docs.docker.com/desktop/install/windows-install/> |
| **Podman alternative** | `podman 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
```bash
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:
| Phase | Time | What's happening | What you'll see |
|---|---|---|---|
| Image extract | 5–30 s | First-time pull of `ic-engine:4.1.34-cpu` (~600 MB) | docker compose progress bars |
| Bridge boot | 2–3 s | FastMCP server binds `:18090`, dashboard binds `:18092` | `/healthz` returns 200, `init_state: not_started` |
| `portfolio_setup` | 1–60 s | Auto-discover portfolio files in `./portfolios/` | `init_state: initializing`, `current_stage: setup` |
| `portfolio_refresh` | 30–120 s | Pull quotes / analyst / news / FRED yields for each symbol | `init_state: initializing`, `current_stage: refresh` |
| `seed_ask` | 5–60 s | Run a primer ask so the cache is warm | `init_state: initializing`, `current_stage: seed_ask` |
| **Ready** | — | All sections cached, `portfolio_ask` returns in 1–3 s | `init_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:
```bash
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:
```bash
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
| Size | Required | Recommended | Why |
|---|---|---|---|
| **≤ 50 symbols** | `TOGETHER_API_KEY` (narrative) | — | yfinance handles quotes/history at this scale; one key covers narrative |
| **50–200 symbols** | `TOGETHER_API_KEY` | `FINNHUB_KEY` (free 60/min) + `NEWSAPI_KEY` (free 100/day) | Real-time quotes + analyst + per-symbol news without yfinance throttle |
| **200+ symbols** | `TOGETHER_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):
| Provider | URL | Free-tier limit |
|---|---|---|
| Together AI | https://api.together.ai/settings/api-keys | $1 free credits |
| Finnhub | https://finnhub.io/register | 60 calls/min |
| Polygon (Massive) | https://polygon.io/dashboard/api-keys | paid only |
| MarketAux | https://www.marketaux.com/account/dashboard | 100 calls/day |
| NewsAPI | https://newsapi.org/register | 100 calls/day |
| FRED | https://fred.stlouisfed.org/docs/api/api_key.html | unlimited (registration only) |
| Alpha Vantage | https://www.alphavantage.co/support/#api-key | 25 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:
```json
{
"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:
```bash
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:
| Runtime | Command |
|---|---|
| zeroclaw | Add `[[mcp.servers]]` with `name = "ic-engine"`, `url = "http://127.0.0.1:18090/mcp"`, `transport = "http"` to `~/.zeroclaw/config.toml` |
| openclaw | `openclaw mcp set ic-engine '{"url":"http://127.0.0.1:18090/mcp","transport":"streamable-http"}'` |
| hermes | `hermes mcp add ic-engine --url http://127.0.0.1:18090/mcp` |
| claude code | Add 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:
```bash
# 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.
| Intent | Example 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:
```bash
# 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.
| Broker | Path |
|---|---|
| Schwab | Accounts → Positions → Export CSV |
| Fidelity | NetBenefits → Investments → Download CSV |
| Vanguard | My Accounts → Download Holdings |
| UBS | Wealth Management → Holdings → Export |
| ETrade | Portfolio → Holdings → Download |
| Robinhood | Account → 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
| Key | Purpose | Cost note |
|---|---|---|
| `TOGETHER_API_KEY` | LLM narrative synthesis (Together google/gemma-4-31B-it) | serverless, fleet default |
| `MASSIVE_API_KEY` | Polygon quotes + history (200+ symbol portfolios) | paid, un-rate-limited |
| `FINNHUB_KEY` | Real-time quotes + analyst ratings + category news | 60/min free |
| `MARKETAUX_API_KEY` | Financial news with broader filters than NewsAPI | 100/day free |
| `NEWSAPI_KEY` | Per-symbol news (US sources only) | 100/day free |
| `ALPHA_VANTAGE_KEY` | Supplemental EOD prices | 25/day free |
| `FRED_API_KEY` | FRED yield curve | free, registration required |
| `OPENAI_API_KEY` | Alternative LLM (GPT-4o, GPT-5) | paid |
### No-key providers (always available)
| Provider | Coverage |
|---|---|
| **yfinance** | Quotes, history, news, analyst (rate-limited; safety-net only on 200+ portfolios) |
| **Frankfurter** | FX spot rates (EUR/USD, USD/JPY, etc.) — ECB-sourced |
| **Treasury Fiscal Data** | US 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.
```bash
# 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:
```env
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
```bash
# 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:
```bash
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`:
```bash
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
```bash
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:
```bash
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](#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
```bash
docker compose down -v # removes the data volume — all envelopes lost
docker compose up -d # cold restart with auto-init
```
---
## Stop / uninstall
```bash
# 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:
| Behavior | Why it's by design |
|---|---|
| **MCP + REST endpoints are unauthenticated on `127.0.0.1:18090`** | Localhost 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 provider** | This 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`](PRIVACY.md) for the full data-flow matrix. |
| **Image pulled from `ghcr.io/argonautsystems/ic-engine`** | Pinned 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`](SECURITY.md). For the
privacy model (what stays local vs what goes to which provider) see
[`PRIVACY.md`](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.*
don't have the plugin yet? install it then click "run inline in claude" again.