Self-hosted dashboard for crypto, stocks, forex and options β enriched Twitter/X streams, sentiment analysis, and market signals.
fintwit-web aggregates financial markets data from Twitter/X, Reddit, Binance, Yahoo Finance,
TradingView, and more β enriched with ML-based sentiment analysis and chart recognition β and
surfaces it through a self-hosted React dashboard.
It's a full-stack migration of the fintwit-bot
Discord bot: same data pipelines and ML models, but without Discord's rate limits, low data
density, and lack of a custom UI. The dashboard covers crypto, stocks, forex, options, NFTs,
enriched tweets, and live portfolio tracking. The migration is ongoing and incremental β see
Documentation for current coverage.
- Enriched tweet timeline β streamed live from X/Twitter, classified by ticker, scored for sentiment (FinTwitBERT), and flagged for chart images (chart-recognizer)
- Crypto markets β price index, trending coins, gainers/losers, funding rates, liquidations, exchange listings, TradingView ideas
- Stock markets β price index, earnings calendar, halts, StockTwits, TradingView ideas, gainers/losers
- Options, forex & NFTs β options overview/volume/SPACs/short interest, forex index with economic events and yield curve, NFT top/trending/upcoming/P2E collections
- Reddit β WallStreetBets scraping for cross-subreddit ticker trends
- Portfolio tracking β live trade tracking, with optional Interactive Brokers sync
- Self-hosted β SQLite or PostgreSQL storage, runs via Docker Compose, no Discord required
Multiple custom-trained models power the enrichment pipeline. All are lightweight and load automatically when the backend starts:
- FinTwitBERT-sentiment β classifies the sentiment of financial tweets
- FinTwitBERT-wsb-sentiment β classifies the sentiment of WallStreetBets posts
- chart-recognizer β recognizes if an image is a financial chart
- chart-info-detector β extracts the ticker and price from a chart image if the post text doesn't contain it
- stock-recognizer-model β recognizes the mentioned stocks in reddit posts
The enriched tweet timeline (above) is the flagship view β each tweet is ticker-classified, sentiment-scored, and matched with a live price card, alongside cross-market overview panels (mention heat, sentiment shift, sector rotation) built from Twitter, Reddit, and StockTwits data.
- Key Features
- Screenshots
- Quick Start
- Connect your X account
- Configuration
- Optional add-ons
- Updating
- Local development
- Documentation
- Contributing
- License
Runs on a Raspberry Pi, a home server, or any Linux/macOS/Windows host with
Docker (on Linux: curl -fsSL https://get.docker.com | sh).
git clone https://github.com/StephanAkkerman/fintwit-web.git
cd fintwit-web
cp .env.example .env # then set X_AUTH_TOKEN and X_CT0, see below
docker compose up -dOpen http://127.0.0.1:3000. docker compose up pulls prebuilt images (amd64 and arm64)
and falls back to building from source if it can't.
Only the X cookies are needed for the tweet timeline (how to get them).
Skip them and everything else still works; the timeline explains how to connect. The stack is
localhost-only by default: the frontend is on 127.0.0.1:3000 and the backend API on
127.0.0.1:7999, and nothing is exposed beyond your machine until you opt into the
Cloudflare Tunnel.
The tweet timeline streams your X Following feed as your own logged-in session, not through an official API key. It needs two cookies from that session:
- Log into x.com in your browser.
- Open DevTools (F12) β Application tab (Firefox: Storage) β Cookies β
https://x.com. - Copy the values of
auth_tokenandct0into.env, then restart the backend (docker compose up -d):
X_AUTH_TOKEN=...
X_CT0=...These cookies grant full access to your X account: .env is gitignored, never commit it, and
treat the values like a password. They stay valid until that browser session is logged out.
When X starts rejecting them, the timeline shows an "X session expired" notice; recapture both
values and restart.
Already have a curl.txt?
A captured HomeLatestTimeline request (DevTools β Network β right-click β Copy as cURL) still
works when X_AUTH_TOKEN/X_CT0 are unset: put it at the repo root for local runs, or at
state/curl.txt for Docker. X_CURL_PATH overrides the location.
Everything lives in .env. .env.example lists what a typical install touches:
the X cookies, an optional API key, and commented-out blocks for each optional integration.
docs/configuration.md documents every variable, including tuning knobs.
In Docker, every .env value is passed to the backend. Keep the KEY=value format, with no
spaces around =.
The backend logs which integrations are active when it starts:
[startup] integrations: X timeline: cookies | Reddit: unauthenticated fallback | IBKR: off | ...
Setting API_KEY protects the backend's /api with an X-API-Key header. The dashboard keeps
working because the frontend's proxy (nginx in Docker, Vite in dev) attaches the key
server-side, so it never reaches the browser.
Each of these is opt-in; skip the ones you don't need.
Adds the ibgateway container and a backend sync worker. In .env, set:
IBKR_ENABLED=true
TRADING_MODE=live
TWS_USERID=your-ibkr-username
TWS_PASSWORD=your-ibkr-password
VNC_SERVER_PASSWORD=change-me-to-a-strong-password
COMPOSE_PROFILES=ibkrTRADING_MODE is live or paper; the backend picks the matching gateway port. Then run
docker compose up -d.
IB Gateway runs headless most days, but still asks for an occasional interactive login/2FA
approval. Connect a VNC client (e.g. Remmina: sudo apt install -y remmina remmina-plugin-vnc)
to 127.0.0.1:5901 with VNC_SERVER_PASSWORD, complete the login and any 2FA prompt, and leave
it running.
Check sync health:
docker logs -f fintwit-ibgateway
docker logs -f fintwit-backend
curl -H "X-API-Key: YOUR_API_KEY" http://127.0.0.1:7999/api/ibkr/status # connected: trueNotes:
- In Docker the backend reaches the gateway at
ibgateway:4003(live) or:4004(paper).connection refusedthere means IB Gateway is up but the login/2FA isn't complete yet. - Running the backend outside Docker? Set
IBKR_HOST=127.0.0.1; it then uses 4001/4002. - Gateway settings persist in
./ibgateway-settings.
Adds the cloudflared container and an Access login gate, so only people on your allowlist can
reach the dashboard. One-time Cloudflare setup:
- Create a free Cloudflare account and add your domain.
- At your domain registrar, switch its nameservers to the ones Cloudflare assigns.
- Create an API token at dash.cloudflare.com/profile/api-tokens with: Account β Access: Apps and Policies β Edit; Account β Access: Service Tokens β Edit; Account β Cloudflare Tunnel β Edit; Zone β DNS β Edit; Zone β Zone β Read.
Then provision the tunnel, DNS record and Access policy with Terraform, from infra/:
cp terraform.tfvars.example terraform.tfvars
# set your Cloudflare token/account values and access_allowed_emails
terraform init
terraform apply
terraform output -raw tunnel_tokenBack in the repo root, add to .env and run docker compose up -d:
CLOUDFLARE_TUNNEL_TOKEN=<terraform tunnel_token output>
COMPOSE_PROFILES=tunnel(COMPOSE_PROFILES=ibkr,tunnel if you also use IBKR.) The dashboard is then served at
https://<subdomain>.<zone_name> from terraform.tfvars. Visitors first see Cloudflare's login
page and verify with a one-time code emailed to them; anyone not on the allowlist is blocked at
Cloudflare's edge. access_allowed_emails only seeds the list; after that, manage it from the
dashboard's Admin page, which needs a few more .env values (see
infra/README.md).
app/mcp_server.py is a read-only MCP server over the
dashboard's API, so Claude (Code, Desktop, or a cloud session) can pull your tweets, market
data, Reddit trends and portfolio and reason over them. It reads the backend's OpenAPI schema,
so everything the dashboard shows is available without a tool per endpoint (list_endpoints +
get). Three bundled tools answer the common questions in one call each: portfolio (positions,
P&L, diversification, IBKR account), holdings_check (per holding: tweet chatter and
sentiment, Reddit buzz, recent news, upcoming earnings) and ticker (everything about one
symbol, including your position in it). market_snapshot and candidates cover the market and
buy ideas for the scheduled market check below. It can't change anything: edits,
the SSE stream and /api/admin/* are not reachable.
Claude can't complete Cloudflare's email-code login, so Terraform also creates an Access
service token (access_service_tokens in terraform.tfvars, ["claude"] by default): a
client id + secret that gets past Access without a login, and that you can revoke on its own.
After terraform apply:
terraform output -json access_service_tokens # {"claude": {"client_id": ..., "client_secret": ...}}Then point the MCP server at the deployment with these environment variables:
FINTWIT_URL=https://<subdomain>.<zone_name>
CF_ACCESS_CLIENT_ID=<client_id>
CF_ACCESS_CLIENT_SECRET=<client_secret>- Claude Code in this repo:
.mcp.jsonregisters the server asfintwit; export the variables before startingclaude(andpip install -e ".[test]"orpip install -r requirements.txtfor themcppackage). - Claude Code on the web: add the three variables to the cloud environment's environment
variables, and allow your dashboard's hostname under its network access. Sessions on that
environment get the
fintwittools automatically. - Claude Desktop / any MCP client: run
python -m app.mcp_serverfrom the repo root over stdio, with the same variables.
Without the variables it talks to a local backend on http://127.0.0.1:7999. To rotate the
token, terraform apply -replace='cloudflare_zero_trust_access_service_token.fintwit["claude"]'.
With the MCP server connected, Claude can check the market on a schedule and suggest buys and sells for you to act on. It is advice only: Claude can't place trades or change your rules.
- Open the dashboard's Advisor page (
/advisor) and write your rules (position limits, take-profit and stop levels, what to avoid, risk appetite), a watchlist, and optionally the cash you have available, so Claude can size its buy ideas. - Create a routine in Claude Code on the web, on the cloud environment from the section above,
that runs every hour on weekdays with the prompt "Run the market-check skill." The skill
(
.claude/skills/market-check/SKILL.md) skips the run when US markets are closed, reviews your holdings and buy candidates against your rules, and replies "Nothing new" when nothing changed. - Each suggestion is logged to the Advisor page's decision journal, priced when it was made, and graded after 7 and 30 days, so the scorecard shows whether the calls are worth following.
Before suggesting a symbol, Claude checks its valuation and analyst targets, short interest,
TradingView technicals and where the price sits against its 52-week range and 200-day average
(/api/research/*, part of the ticker and holdings_check tools).
Signa. .mcp.json also registers Signa's MCP server
(https://app.getsigna.ai/api/mcp), authenticated with the same SIGNA_KEY the backend uses
(a cmts_... key). Set SIGNA_KEY in the environment Claude runs in, and on a cloud
environment allow app.getsigna.ai in its network access. The market check then weighs Signa's
verdict for each symbol it suggests, and skips Signa when the key isn't set.
Without credentials, the r/wallstreetbets service uses an unauthenticated HTTP fallback, which
Reddit blocks more often. For more reliable access, create a "script" app at
reddit.com/prefs/apps and set REDDIT_CLIENT_ID and
REDDIT_CLIENT_SECRET in .env.
If the backend crashes with a PyTorch Illegal instruction error in chart recognition (common
on ARM and older CPUs), disable chart inference and/or chart OCR in .env:
CHART_ENABLED=false
CHART_EXTRACTION_ENABLED=falseTo find the failing stage (PyTorch conv vs timm vs chart model), run
docker exec -it fintwit-backend python -m app.runtime.probe_chart_stack. To use a different
PyTorch version, set TORCH_VERSION in .env and build locally with
docker compose up -d --build backend. The image uses CPU-only PyTorch wheels, so no CUDA
runtime is downloaded.
make updateThis runs git pull, pulls the latest prebuilt images, and restarts the stack, picking up any
COMPOSE_PROFILES (e.g. ibkr, tunnel) set in .env. If the images can't be pulled, it
builds them from source instead (docker compose up -d --build). No make on your host? Run
the same steps directly:
git pull
docker compose pull
docker compose down
docker compose up -dpip install -r requirements.txt
cd frontend && npm install && cd ..
cp .env.example .env
npm run devnpm run dev starts uvicorn on 127.0.0.1:7999 and the Vite dev server on
http://127.0.0.1:5173, which proxies /api/* to the backend. To run them separately:
uvicorn app.api.main:app --port 7999 --reload
cd frontend && npm run devmake check runs the same tests, linting and build checks as CI.
- Configuration Reference
- Documentation Index
- Migration Status
- API and Frontend Coverage Matrix
- Frontend Integration Map
- Cloudflare Terraform Notes
Contributions are welcome! If you have a feature request, bug report, or proposal for code refactoring, please feel free to open an issue on GitHub. See CONTRIBUTING.md for code style and PR guidelines. We appreciate your help in improving this project.
This project is licensed under the MIT License. See the LICENSE file for details.





