openportfolio

openportfolio

Enables AI agents to read and write a self-hosted portfolio tracker through 25 MCP tools, covering net worth, flows, forecasts, decisions, and audit.

Category
访问服务器

README

<div align="center">

<img src="assets/icon.svg" width="76" alt="openportfolio" />

openportfolio

Every account as one book. Every call on the record.

Open-source, self-hosted portfolio tracker. It pulls every brokerage, pension, wallet and bank account into a single net worth, stores the investor flows behind the price, and Brier-scores the forecasts you registered before the fact. No provider API key, anywhere in it.

License npm TypeScript Stars

Website · Docs · Live demo

</div>

<img src="assets/screenshots/net-worth.png" alt="The openportfolio net worth view: one total, then breakdowns by venue and by asset class, then the full position table" />

<sub>Screenshots show the demo book. Every figure in them is invented.</sub>

Status: pre-release. It runs and the setup below works. Interfaces will still move.

It is not a trading bot. The backend has no function that places an order, the adapters that ship declare canPlaceOrders: false, and PlaceOrderRequest requires an OrderConfirmation that has no default. What it does is aggregate, watch, and keep score.

Why

Two problems that look unrelated and are the same problem.

A portfolio is scattered by construction. A broker here, a pension there, an ISA, an exchange account, a bank balance, a holding that no API will ever return. Every one of those apps shows you a number, and none of them shows you yours. So the figures that actually decide things, the total, the concentration in one name, the share of the book sitting in a currency you do not spend, are the figures nobody has. They get estimated, and the estimate is generous in the direction that avoids a decision.

Market commentary is unaccountable, and became more so the moment a model would produce a confident directional view on anything you asked it. The problem is not that the views are wrong. It is that being wrong costs nothing and leaves no trace, so a forecaster worth reading and a forecaster who is merely fluent are indistinguishable from the outside, and from the inside too.

Both are bookkeeping failures, so openportfolio treats them as bookkeeping.

One net worth Accounts pulled through venue adapters into a single base currency, with the rate stored on the row it converted, so a snapshot records what the book was worth then rather than what today's rates say. Positions held in three places are one exposure.
Flows, not just prices Price is the output of who was buying and who was made to sell. Net buying by investor type, turnover, and a calendar of dated forward events are stored as first-class series, not derived when somebody remembers to ask. A forced seller is on a schedule, and the schedule is public.
A scored track record A call is registered before the fact with a probability, a horizon and the condition that settles it. When the horizon passes, the machine-resolvable ones settle themselves and are Brier-scored. The reliability diagram is the product: it shows what you said, what happened, and the gap between them.

There is a fourth table that exists for one reason. A recommendation phrased as "wait for the print, then decide" evaporates the moment it is said out loud. decisions is a queue of those, each with a trigger condition and an outcome, and they stay on the board until one of them changes.

<table> <tr> <td width="50%"><img src="assets/screenshots/track-record.png" alt="Track record view: mean Brier score over scored calls, with a reliability table comparing what was said against what happened" /></td> <td width="50%"><img src="assets/screenshots/flows.png" alt="Flows view: net buying by investor type over thirty sessions, with turnover on every row" /></td> </tr> <tr> <td align="center"><sub><b>Track record.</b> Said versus happened, bucket by bucket.</sub></td> <td align="center"><sub><b>Flows.</b> Net buying by investor type, turnover on the same row.</sub></td> </tr> </table>

No provider API key

Watching a book is only useful if something is actually watching: reconciling after the close, settling a call the day its horizon passes, noticing that a deferred decision came due three weeks ago.

Metered inference is the wrong shape for that. When each run bills per token, every autonomous check becomes a purchase, and a product that spends the operator's money unprompted has to ask first, or batch, or ration. All three turn a portfolio that watches itself into a portfolio that asks permission to look.

So every model call is dispatched instead to an agent CLI you are already signed in to (codex, antigravity, claude), with a per-task fallback order. There is no provider key in this repo and no field to put one in. That does not make a run free: subscription plans have rate limits, and the fallback chain exists partly because one provider runs out before the others do. What changes is the kind of limit. Agent work is bounded by quota and wall clock rather than by spend, so it never has to be justified one invocation at a time.

The consequence is that openportfolio is self-hosted by design. Your deployment runs your syncs on your machine under your own logins, against your own accounts.

What it does

Surface
Net worth accounts, balances, per-venue and per-asset-class breakdown, snapshots in one base currency, keyless FX
Venues adapter contract with declared capabilities; a reference keyless quote adapter and a manual one
Flows net buying and turnover by investor type per session, per market or per symbol
Forecasts probability, horizon and resolution criterion; auto-resolution on horizon expiry; Brier score and reliability buckets
Decisions the deferred-decision queue, with trigger conditions and outcomes
Catalysts dated forward events and the assets they touch
Audit append-only record of every state-changing mutation, including what the cron did unattended
MCP 25 tools so codex / antigravity / claude can read and write the book directly
Multi-tenancy every table scoped to a tenant, every index leading with it, one service key per tenant

Quick start

Node 22+, pnpm, and a Convex account. The free tier is enough.

git clone https://github.com/seonglae/openportfolio.git
cd openportfolio
pnpm install

cp .env.example .env.local
npx convex dev --once          # creates the deployment

# create the first book
npx convex env set OPENPORTFOLIO_DEV_TENANT home
npx convex run tenants:create '{"slug":"home","name":"Home","baseCurrency":"GBP"}'

# the UI, then the sync loop
pnpm --filter openportfolio-browser dev   # http://localhost:6101
npx tsx sync-worker.mts --once

With nothing linked it registers the venues it can serve and records a net worth of zero, which is correct. Add a manual holdings file to get a real one:

[
  { "accountKey": "isa", "symbol": "VWRL", "assetClass": "etf", "qty": 40, "price": 118.2, "currency": "GBP" },
  { "accountKey": "wallet", "symbol": "BTC", "assetClass": "crypto", "qty": 0.15, "price": 0, "currency": "USD" }
]
export OPENPORTFOLIO_MANUAL_HOLDINGS=$PWD/holdings.json
npx convex run accounts:link '{"accountKey":"isa","venue":"manual","kind":"brokerage","label":"ISA","currency":"GBP"}'
npx convex run accounts:link '{"accountKey":"wallet","venue":"manual","kind":"wallet","label":"Wallet","currency":"USD"}'
npx tsx sync-worker.mts --once

The BTC row is priced at 0 in the file on purpose: the worker re-quotes crypto through the keyless CoinGecko adapter, converts both rows into GBP, and writes one total.

Full walkthrough: openportfolio.app/docs/quickstart

Before exposing it

Two things are open on localhost and must be closed before the deployment is reachable from the internet.

  1. The dev tenant. While OPENPORTFOLIO_DEV_TENANT is set, any unauthenticated caller is scoped to that tenant. Unset it and configure Clerk.
  2. Service keys. Workers and the MCP server have no browser session, so they present a key. Generate it locally and send only its hash.
npx convex env set CLERK_ISSUER_URL https://your-app.clerk.accounts.dev
npx convex env unset OPENPORTFOLIO_DEV_TENANT

KEY="$(openssl rand -hex 32)"
npx convex run tenants:issueServiceKey "{\"key\":\"$KEY\",\"label\":\"sync-worker\",\"role\":\"member\"}"
echo "OPENPORTFOLIO_SERVICE_KEY=$KEY" >> .env.local

Multi-tenancy

One deployment holds many books. The invariant is that a caller never says which tenant it is.

tenantId is derived from the caller's membership rows or from the service key's own row, so there is no argument a client can set to reach another book. The public API accepts tenantSlug, and only as a disambiguator for a caller who belongs to several tenants; membership is still what decides. A document id belonging to another tenant reads as missing rather than forbidden, because "forbidden" confirms the row exists, which is itself the cross-tenant read.

Every index leads with tenantId, so a query that forgets the scope cannot use an index at all. One exception is deliberate and marked: the resolver cron sweeps every book's due calls through a tenant-less index, and is an internalMutation for exactly that reason. It is unreachable from any client.

Details: openportfolio.app/docs/multi-tenancy

Venue adapters

An adapter declares what it can do and implements only that:

type VenueAdapter = {
  venue: string;
  kind: AccountKind;
  capabilities: { canReadBalances: boolean; canReadQuotes: boolean; canPlaceOrders: boolean };
  readBalances(request: ReadBalancesRequest): Promise<AdapterBalance[]>;
  readQuote(request: ReadQuoteRequest): Promise<AdapterQuote>;
  placeOrder?(request: PlaceOrderRequest): Promise<OrderReceipt>;
};

Two ship. coingecko reads quotes and refuses balances, because a price source does not know what you hold and returning an empty list would read as "you hold nothing". manual reads a JSON file you maintain, which is how a pension or an unlisted holding gets into the total instead of being left out of it.

No keyed broker adapter ships. Adding one means writing a module in packages/node/src/adapters/, taking its credential from the worker's environment, and registering it in defaultRegistry(). Keep the credential in the worker process: the backend never sees it, and neither does this repo.

Details: openportfolio.app/docs/adapters

Requirements

  • Node 22+, pnpm
  • A Convex account (free tier is enough)
  • At least one agent CLI signed in, if you want the agent worker: codex, antigravity (agy), or claude
  • Optional: Clerk for auth, needed once more than one person uses the deployment or it is reachable from the internet

Development

pnpm typecheck     # every workspace, src and test alike
pnpm test          # vitest across packages, convex handlers, browser helpers

# the demo build used for the screenshots and the hosted demo
pnpm --filter openportfolio-browser exec vite build --config vite.demo.config.ts

# the marketing site and docs are static; regenerate the docs pages after editing
python3 site/build-docs.py

Conventions, the tenant invariant in full, and notes for agent CLIs working in this repo are in AGENTS.md.

License

Apache-2.0. See LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选