Fortio — Ghostfolio Finance AI Agent
A production-ready AI agent built on LangGraph that provides intelligent portfolio analysis by integrating with Ghostfolio, the open-source wealth management platform.
Production URLs
| Service | URL |
|---|---|
| Ghostfolio App | ghostfolio-production.up.railway.app |
| **Ghostfolio Demo Account to test Fortio Agent ** | fortio-agent-production.up.railway.app |
| Fortio Agent Docs | fortio-agent-production.up.railway.app/docs |
Stack
- Agent: LangGraph (state machine) + Claude Haiku (primary LLM) + GPT-4o-mini (fallback)
- Tools: 16 domain tools — 5 core (Ghostfolio REST API) + 6 advanced multi-step + 5 FIRE Goal Tracker (opt-in, feature-flagged)
- Verification: 5-stage pipeline (disclaimer, hallucination guard, freshness, concentration, confidence) +
FIRE_PROJECTION_SPECULATIVEflag - Interfaces: FastAPI REST API · Typer CLI (
fortio) · MCP server (fortio mcp) - Observability: LangSmith tracing + structured eval suite (370+ tests)
- Deployment: Railway (CI/CD via GitHub Actions)
Quick Start (30 minutes to first API call)
1. Clone and set up Python environment
```bash
From the monorepo root
cd apps/agent python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt pip install -r requirements-dev.txt ```
2. Get your API keys
| Key | Where to get it | Cost |
|---|---|---|
ANTHROPIC_API_KEY |
console.anthropic.com → API Keys | ~$3/1M tokens |
OPENAI_API_KEY |
platform.openai.com → API Keys | ~$5/1M tokens (fallback) |
LANGCHAIN_API_KEY |
smith.langchain.com → Settings | Free tier available |
3. Configure environment
```bash cp .env.example .env
Edit .env and fill in your API keys
```
4. Get a Ghostfolio bearer token (for authenticated endpoints)
```bash
curl -X POST https://ghostfol.io/api/v1/auth/anonymous
-H "Content-Type: application/json"
-d '{"accessToken": "DEMO_ACCESS_TOKEN"}'
Copy the authToken from the response into GHOSTFOLIO_ACCESS_TOKEN in .env
```
5. Install the CLI and run tests
```bash
Install as editable so the fortio command is available
pip install -e .
Unit tests (mocked, no network)
pytest tests/unit/ -v
Eval suite (correctness, tool selection, edge cases, multi-step)
pytest tests/evals/ -v ```
CLI — Command Line Interface
Fortio ships a Typer-powered CLI (fortio) for quick terminal access to the agent without
starting a server. It supports single-shot queries, an interactive REPL, server management,
and an MCP server mode.
Install the CLI
Option A — Editable install (recommended for development)
```bash cd apps/agent pip install -e . fortio --help ```
Option B — Run as a Python module (no install required)
```bash pip install -r requirements.txt python -m agent.cli --help ```
Option C — Install from PyPI (once published)
```bash pip install fortio-agent fortio --help ```
CLI Commands
fortio ask — Single question, print answer, exit
```bash fortio ask "What does my portfolio look like?" fortio ask "How concentrated am I in tech?" --verbose fortio ask "What are my top performers?" --user-id alice ```
fortio chat — Interactive multi-turn REPL
```bash fortio chat fortio chat --conversation-id 550e8400-e29b-41d4-a716-446655440000 fortio chat --verbose ```
Inside the REPL:
| Command | Action |
|---|---|
/help |
Show topic suggestions grid |
/tools |
List all 11 available agent tools |
/clear |
Print a visual separator |
exit / q |
End the session |
| Ctrl+C | Interrupt at any time |
Example session:
``` Fortio Chat ───────────────────────────────────────── Welcome to Fortio, your Ghostfolio Finance Assistant! ... Session: 4f3c2a1b-...
You: What's my overall portfolio value? ╭─ Fortio ──────────────────────────────────────────╮ │ Your total portfolio value is $124,580 across 12 │ │ holdings ... │ │ 🟢 Confidence: HIGH │ ╰───────────────────────────────────────────────────╯ 💡 Follow-up suggestions
- How has my portfolio performed YTD?
- Am I too concentrated in any sector?
- Give me a portfolio health scorecard
You: quit Goodbye! 👋 ```
fortio serve — Start the FastAPI server
```bash fortio serve # default: 0.0.0.0:8001 fortio serve --port 9000 # custom port fortio serve --reload # dev mode with hot-reload fortio serve --workers 4 # production multi-worker ```
API docs: http://localhost:8001/docs
fortio mcp — Start the MCP server
```bash fortio mcp ```
Starts the Fortio MCP (Model Context Protocol) server on stdio, exposing all 11 portfolio tools + 3 resources + 1 prompt template to Claude Desktop, Cursor, or any MCP-compatible AI host.
See the MCP section below for full setup instructions.
fortio demo — Run all 11 tools in sequence
```bash fortio demo fortio demo --no-verbose # suppress per-tool detail ```
Runs one targeted question per tool so all 11 tools are exercised end-to-end in a single command.
Each question is specifically designed to trigger exactly one tool. Prints a summary at the end:
Demo complete: 11/11 tools succeeded. Ideal for verifying a fresh install or demonstrating
the agent in a recorded session.
fortio version — Show active configuration
```bash fortio version ```
``` ╭─ Version Info ──────────────────────────────────╮ │ Fortio – Ghostfolio Finance AI Agent │ │ │ │ Version: 0.1.0 │ │ Primary model: claude-haiku-4-5 │ │ Fallback model: gpt-4o-mini │ │ Environment: development │ │ Checkpoint: postgres │ │ Log level: INFO │ ╰─────────────────────────────────────────────────╯ ```
CLI vs Other Interfaces
| Interface | Best for | History | Persistence |
|---|---|---|---|
fortio ask |
Quick one-off queries, scripting | None | None |
fortio chat |
Interactive exploration in the terminal | In-memory (session only) | None |
fortio demo |
Verify all 11 tools work end-to-end | None | None |
fortio serve + /api/chat |
Angular/Ghostfolio frontend | Postgres (across restarts) | Full |
fortio mcp |
Claude Desktop / Cursor integration | Host-managed | Host |
Note:
fortio chat,fortio ask, andfortio demouseMemorySaver— conversation history is kept only for the lifetime of the process and is not persisted to Postgres.
MCP — Model Context Protocol
Fortio can run as an MCP server, letting Claude Desktop, Cursor, and other MCP-compatible hosts query your Ghostfolio portfolio using natural language — with no browser and no API keys pasted into a chat box.
What the MCP server exposes
| Type | Count | Details |
|---|---|---|
| Tools | 16 | All portfolio, performance, diversification, market, and FIRE tools |
| Resources | 3 | portfolio://summary, portfolio://performance, portfolio://health |
| Prompts | 1 | portfolio-analysis (pre-loads live data; focus: risk/perf/fees/all) |
Claude Desktop setup
Edit ~/Library/Application Support/Claude/claude_desktop_config.json
(Windows: %APPDATA%\Claude\claude_desktop_config.json):
```json { "mcpServers": { "fortio": { "command": "fortio", "args": ["mcp"], "env": { "GHOSTFOLIO_BASE_URL": "https://your-ghostfolio.railway.app", "GHOSTFOLIO_ACCESS_TOKEN": "your-security-token" } } } } ```
Restart Claude Desktop — the Fortio tools appear in the tool list automatically.
Local Ghostfolio? Use
"GHOSTFOLIO_BASE_URL": "http://localhost:3333"instead.
MCP server not connecting? Confirm
fortiois onPATHwithwhich fortio. If not found, use the full path:"command": "/path/to/.venv/bin/fortio".
Cursor setup
- Open Cursor → Settings → MCP
- Click Add server
- Paste the same JSON block as above
- Restart Cursor — Fortio tools appear in the MCP tool list
Using the portfolio-analysis prompt template
In Claude Desktop, open the Prompt Library and select portfolio-analysis. You can optionally specify a focus area:
| Focus value | What Claude analyses |
|---|---|
risk |
Concentration risk, sector exposure, rebalancing needs |
performance |
Returns, period comparisons, best/worst performers |
fees |
Fee drag, total fees paid, cost reduction opportunities |
all |
Comprehensive: diversification, performance, fees, and risk |
The prompt fetches live portfolio data (holdings + YTD performance + health score) and embeds it into context before Claude responds — no explicit tool calls required.
MCP resources (passive context loading)
Resources let Claude load portfolio data as background context without an explicit tool call:
``` portfolio://summary — current holdings, allocations, total value portfolio://performance — YTD return %, absolute gain/loss, current value portfolio://health — health score (0-100), letter grade, action items ```
Eval Suite
The agent ships with 370+ evaluation tests across 9 files (zero real network calls — all mocked
with respx), plus a LangSmith experiment suite.
Eval files
| File | Focus | Tests |
|---|---|---|
tests/evals/test_correctness.py |
Arithmetic accuracy, percentage conversions, sort order, fee sums, sector rollup | 11 |
tests/evals/test_tool_selection.py |
Tool docstring trigger keywords, domain boundary isolation, parameter mapping | 28 |
tests/evals/test_llm_tool_selection.py |
LLM-driven tool selection routing and keyword coverage | 16 |
tests/evals/test_tool_execution.py |
Advanced tool execution: happy path, error cases, edge inputs for all 6 advanced tools | 12 |
tests/evals/test_multi_step.py |
Cross-tool consistency, referential integrity, multi-session proactive monitor | 19 |
tests/evals/test_edge_cases.py |
Dict/list format switching, zero-value holdings, unicode, large portfolios, invalid inputs | 28 |
tests/evals/test_adversarial.py |
Prompt injection, jailbreaks, off-topic deflection, fabricated number detection | 29 |
tests/evals/test_safety.py |
Verification pipeline stages: disclaimer, hallucination guard, confidence scoring | 19 |
tests/evals/test_retirement_eval.py |
FIRE Goal Tracker: projection math, multi-turn, FRED fallback (10 categories) | 35+ |
tests/evals/ls_evals.py |
LangSmith tracked experiments: correctness, safety, latency, consistency, tool-keywords | 23 |
Running the eval suite
```bash
All evals (fast, ~5-10 s, no network required)
pytest tests/evals/ -v
Specific eval files
pytest tests/evals/test_correctness.py -v pytest tests/evals/test_tool_selection.py -v pytest tests/evals/test_llm_tool_selection.py -v pytest tests/evals/test_tool_execution.py -v pytest tests/evals/test_multi_step.py -v pytest tests/evals/test_edge_cases.py -v pytest tests/evals/test_adversarial.py -v pytest tests/evals/test_safety.py -v
Unit tests
pytest tests/unit/ -v
Adversarial / safety tests (standalone suite)
pytest tests/adversarial/ -v
With coverage report
pytest tests/unit/ tests/evals/ --cov=agent --cov-report=term-missing ```
LangSmith experiments (ls_evals.py)
ls_evals.py runs 5 scored eval types tracked in the LangSmith UI:
| Eval type | What it tests | Examples |
|---|---|---|
correctness |
Tool output matches numeric ground truth (totals, %, fee sums, sectors) | 6 |
safety |
Verification pipeline catches disclaimer, hallucination, concentration | 5 |
latency |
Tool calls complete within 2 s on mocked I/O | 4 |
consistency |
Same inputs → identical outputs across two runs | 3 |
tool-keywords |
Tool docstrings contain all LLM trigger keywords | 5 |
```bash
Prerequisites: LANGCHAIN_API_KEY must be set in .env
python tests/evals/ls_evals.py # all 5 eval types python tests/evals/ls_evals.py --only safety # one type python tests/evals/ls_evals.py --only latency python tests/evals/ls_evals.py --prefix feat/my-branch # tag experiment run
View results at: https://smith.langchain.com → Projects → fortio-evals
```
Evals in CI
Evals run automatically on every PR and push to main as part of the test-agent CI job.
They are advisory (continue-on-error: true) — results are visible in CI without blocking
deployment. Unit tests are the hard gate.
FIRE Goal Tracker
Fortio ships an opt-in FIRE (Financial Independence, Retire Early) planning module backed by live Federal Reserve data from the FRED API.
Enable it
# In .env
FIRE_TRACKER_ENABLED=true
FRED_API_KEY=your_key_here # free — see SETUP.md Step 2b
What users can ask
| Question | Tools invoked |
|---|---|
| "Retire at 50, spend $80K/year" | set_retirement_goal |
| "What's my FIRE number?" | get_retirement_goal |
| "Am I on track to retire?" | get_fire_progress + get_portfolio_summary |
| "When can I retire at my current savings rate?" | calculate_retirement_projection + FRED |
| "What if I save $500 more per month?" | calculate_retirement_projection (override) |
| "What's the current 10-year Treasury yield?" | get_macro_data |
How it works
Retirement projections fetch live CPI inflation and 10-year Treasury yields from FRED:
Real return = (1 + nominal_return) / (1 + CPI_inflation) − 1
Goals persist in the retirement_goals Postgres table. CRUD API routes:
| Operation | Route |
|---|---|
| Create / Update | POST /api/goals/retirement |
| Read | GET /api/goals/retirement/{user_id} |
| Delete | DELETE /api/goals/retirement/{user_id} |
Every projection response gets a FIRE_PROJECTION_SPECULATIVE flag (LOW severity) from the
verification pipeline — surfaced as a ⚠️ Flags detected note in the chat UI.
Zero impact when disabled:
FIRE_TRACKER_ENABLED=false(default) — no tools registered, no DB tables created, no FRED calls made. Existing deployments are completely unaffected.
See BOUNTY.md for the full feature write-up.
Verification Pipeline
Every final LLM response passes through a 5-stage verification pipeline:
| Stage | Check | What it does |
|---|---|---|
| 1 | check_disclaimer |
Detects investment-advice language; appends "not financial advice" |
| 2 | check_hallucination |
Extracts numbers from response; flags any not present in tool results |
| 3 | check_freshness |
Rejects tool results older than DATA_MAX_AGE_MINUTES (default: 5 min) |
| 4 | check_concentration |
Flags any position > PORTFOLIO_CONCENTRATION_THRESHOLD (default: 20%) |
| 5 | check_confidence |
Scores response HIGH/MEDIUM/LOW; escalates on HIGH severity + hallucination |
Flags are returned in the API response and surfaced as ⚠️ Flags detected in the chat UI.
Project Structure
``` apps/agent/ ├── agent/ │ ├── config.py # All settings via pydantic-settings │ ├── prompts.py # System prompt + persona guardrails │ ├── cli.py # Typer CLI: ask / chat / serve / mcp / version │ ├── tools/ # 11 LangGraph tools │ │ ├── init.py # ALL_TOOLS export │ │ │ — Core tools (single Ghostfolio API call) — │ │ ├── portfolio.py # get_portfolio_summary │ │ ├── performance.py # get_performance │ │ ├── transactions.py # get_transactions │ │ ├── diversification.py # analyze_diversification │ │ ├── market.py # get_market_data (Yahoo Finance via yfinance) │ │ │ — Advanced multi-step tools — │ │ ├── fee_drag.py # get_fee_drag_analysis │ │ ├── health_scorecard.py# get_portfolio_health_scorecard │ │ ├── rebalancing.py # get_rebalancing_plan │ │ ├── market_context.py # get_market_context_overlay │ │ ├── transaction_patterns.py # get_transaction_pattern_intelligence │ │ └── proactive_monitor.py # get_proactive_risk_monitor │ ├── verification/ │ │ └── init.py # 5-stage pipeline │ ├── mcp/ │ │ └── server.py # MCP server: 11 tools + 3 resources + 1 prompt │ ├── graph/ │ │ ├── state.py # AgentState TypedDict │ │ └── graph.py # LangGraph assembly + routing │ ├── clients/ │ │ ├── ghostfolio.py # Typed async Ghostfolio API client │ │ └── market.py # yfinance async client (source: Yahoo Finance) │ └── api/ │ ├── main.py # FastAPI app + /health + /api/chat │ └── schemas.py # Request/response Pydantic models │ ├── tests/ │ ├── conftest.py │ ├── unit/ # Fast mocked unit tests (hard gate in CI) │ │ ├── tools/ │ │ ├── clients/ │ │ ├── graph/ │ │ ├── api/ │ │ └── verification/ │ ├── evals/ # Eval suite (advisory in CI) │ │ ├── test_correctness.py │ │ ├── test_tool_selection.py │ │ ├── test_llm_tool_selection.py │ │ ├── test_tool_execution.py │ │ ├── test_multi_step.py │ │ ├── test_edge_cases.py │ │ ├── test_adversarial.py │ │ ├── test_safety.py │ │ └── ls_evals.py # LangSmith experiment runner │ ├── integration/ │ └── adversarial/ │ ├── docker/ │ └── docker-compose.yml # Ghostfolio + PostgreSQL + Redis ├── Dockerfile ├── railway.toml ├── pyproject.toml # Package metadata (fortio-agent on PyPI) ├── .env.example ├── requirements.txt ├── requirements-dev.txt └── pytest.ini ```
LangSmith Observability Setup
- Create free account at smith.langchain.com
- Create a project called
fortio-agent - Copy your API key to
.envasLANGCHAIN_API_KEY - Set
LANGCHAIN_TRACING_V2=true - Run the agent — traces appear automatically in your LangSmith dashboard
Running the Full Docker Stack
```bash
From monorepo root
docker compose -f docker/docker-compose.yml up -d
Ghostfolio UI → http://localhost:3333
Agent FastAPI docs → http://localhost:8001/docs
```
Why Claude Over GPT-4o for This Project
| Capability | Claude Haiku 4.5 | GPT-4o-mini |
|---|---|---|
| Tool use accuracy | ✅ Better multi-tool chaining | ✅ Good |
| Financial reasoning | ✅ Less hallucination on numbers | ⚠️ Occasionally invents figures |
| Safety instruction following | ✅ Excellent at "never do X" rules | ✅ Good |
| Context window | 200k tokens | 128k tokens |
| Cost | ~$0.80/1M tokens (input) | ~$0.15/1M tokens (input) |
| Structured output | ✅ Native | ✅ Native |
The larger context window matters when passing full portfolio data + conversation history + tool results in the same prompt.
Open Source
This agent is published as fortio-agent on PyPI:
```bash pip install fortio-agent ```
Metadata
Release files for fortio-agent 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fortio_agent-0.1.0.tar.gz | 109.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fortio_agent-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 227.4 kB
Release files / fortio_agent-0.1.0.tar.gz
| Download URL | fortio_agent-0.1.0.tar.gz |
|---|---|
| Size | 109.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
688667b203ba88e82325ba4d80a6d4bd3f49d4a84189dad719b245edea73fa67
|
|
BLAKE2b-256 checksum How to use checksums |
094dae1a8ab8ef97b45da55b8cd3f3d94f5905cebb8ff76f91cb315e885fe9c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|
Release files / fortio_agent-0.1.0-py3-none-any.whl
| Download URL | fortio_agent-0.1.0-py3-none-any.whl |
|---|---|
| Size | 117.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
989e707b0e7e3003370e3c260407f9776b7af71a6b464149425879662454154e
|
|
BLAKE2b-256 checksum How to use checksums |
667b493833c37acb9b9a3253305de3d5cedc24760a419a7826190370c9265f72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|