Self-hosted news monitor that pings you only on genuinely new info.
Please leave a ⭐ star if Topic Watch is useful - it helps others find it :).
Adding a topic - Topic Watch fetches the latest news and builds a per-topic knowledge baseline.
An LLM tracks a per-topic knowledge state and stays silent until something actually changes. Bring your own key, or run free against a local model.
Topic Watch runs in Docker. Get it at get.docker.com, or install Docker Desktop on macOS/Windows. Make sure it's running before you continue.
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/0xzerolight/topic_watch/main/scripts/install.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/0xzerolight/topic_watch/main/scripts/install.ps1 | iexManual install (without the script)
Docker, prebuilt image - same image the script uses, you just supply the compose file:
mkdir -p topic-watch/data && cd topic-watch
curl -fsSL https://raw.githubusercontent.com/0xzerolight/topic_watch/main/docker-compose.prod.yml -o docker-compose.yml
(umask 077; printf 'PUID=%s\nPGID=%s\n' "$(id -u)" "$(id -g)" > .env)
docker compose up -dThe .env line matches the container's user to yours so it can write data/, and is
owner-only since secrets go there later.
Build from source - no prebuilt image, builds from the Dockerfile:
git clone https://github.com/0xzerolight/topic_watch.git
cd topic_watch
(umask 077; printf 'PUID=%s\nPGID=%s\n' "$(id -u)" "$(id -g)" > .env)
docker compose up -dWithout the .env line the container chowns your checkout's data/ to UID 1000 - a
problem if your UID differs.
Without Docker (Python 3.11+):
git clone https://github.com/0xzerolight/topic_watch.git
cd topic_watch
python -m venv .venv && source .venv/bin/activate
pip install -e .
uvicorn app.main:app --host 127.0.0.1 --port 8000Then open http://localhost:8000 and set your LLM key in the
setup wizard. The editable install (-e) keeps config and the SQLite database in the
project's data/. --host 127.0.0.1 keeps it local - widen only behind an
authenticating reverse proxy (no login screen).
Updating
If you used the install script or the prebuilt image:
cd ~/topic-watch # or your install directory
docker compose pull
docker compose up -dOr run the updater, which reports the version change and points at your backups:
curl -fsSL https://raw.githubusercontent.com/0xzerolight/topic_watch/main/scripts/update.sh | bashIf you built from source, there is no image to pull - rebuild instead:
cd topic_watch # your clone
git pull
docker compose up -d --buildThe database is automatically backed up before any schema migration.
- Novelty detection: per-topic knowledge state, not keyword matching or summarization - ignores the 10th article rehashing the same story
- Any LLM via LiteLLM - OpenAI, Anthropic, Gemini, Groq, and more. BYOK, or run free and local with Ollama
- Cheap: ~$0.0003/check on GPT-5.4 Nano (under $0.20/month for 5 topics checked 4×/day), or free with Ollama
- Private and self-hosted on SQLite - no database server, no JavaScript build step. Outbound traffic: RSS feeds and the pages they link to, your LLM provider, your notifier, and your topic names to Exa if you enable it
- Auto feeds (Bing News, falling back to Google News), manual RSS/Atom URLs, or optional Exa AI semantic search per topic
- Per-topic check intervals (10 min to 6 months:
6h,1w 3d,2h 30m) and a plain-English novelty instruction ("official announcements only, ignore rumors") - 100+ notification services via Apprise - Discord, Slack, Telegram, email, ntfy, etc.
More features
- Importance scoring: every finding is rated 1-5, with an optional per-topic threshold that mutes minor findings without dropping them from the knowledge state
- Knowledge history: recent updates to a topic's knowledge state are kept as revisions, with an inline diff timeline showing what the AI added or removed
- Silence Heartbeat: one "sources failing" alert per topic after consecutive empty checks, and a recovery notice when sources return
- Custom JSON webhooks and a notification retry queue
- Feed health dashboard
- Topic tags and bulk check/delete
- Data export (JSON, CSV) and OPML import/export
- 5 color themes (Nord, Dracula, Solarized Dark, High Contrast, Tokyo Night)
- In-app settings page
How It Works
- Define a topic with RSS feed URLs, let it auto-generate a news-search feed (Bing News first, Google News as fallback), or point it at Exa AI semantic search.
- On a schedule, articles are fetched and compared against a knowledge state - a rolling summary of what's already known.
- An LLM decides if anything is actually new.
- New info -> notification with summary + sources. Nothing new -> silence.
LLM providers
Uses LiteLLM. Anything LiteLLM supports works - the model string needs its provider prefix.
| Provider | Model String |
|---|---|
| OpenAI | openai/gpt-5.4-nano |
| Anthropic | anthropic/claude-haiku-4-5 |
| Ollama | ollama/llama3.3 |
| Google Gemini | gemini/gemini-2.5-flash |
| Groq | groq/llama-3.3-70b-versatile |
| DeepSeek | deepseek/deepseek-chat |
| Azure OpenAI | azure/your-deployment |
| Cohere | cohere_chat/command-a-03-2025 |
| Together AI | together_ai/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8 |
Get an API key: OpenAI · Anthropic · Gemini · Groq · DeepSeek. Or skip keys entirely and run free + local with Ollama.
Ollama and OpenAI-compatible gateways (LM Studio, a LiteLLM proxy, OpenCode Go) need a
base_url - see config.example.yml.
Reaching Ollama on the host from inside the container also needs an override file.
From your install directory (~/topic-watch if you used the script):
curl -fsSL https://raw.githubusercontent.com/0xzerolight/topic_watch/main/docker-compose.override.example.yml -o docker-compose.override.yml
docker compose up -dIn a git clone the file is already there: cp docker-compose.override.example.yml docker-compose.override.yml.
Notifications and configuration
Notifications are off by default - Topic Watch tracks topics silently until you add
at least one Apprise URL on the Settings
page (ntfy://your-topic, discord://webhook_id/webhook_token, ...). Multiple URLs are
supported; Test Notification verifies them.
Everything else lives in data/config.yml (auto-copied from
config.example.yml on first run) and most of it is editable on the
Settings page; anything the environment supplies shows there as read-only. Any key can be
overridden with the TOPIC_WATCH_ env prefix, using __ for nested
keys (e.g. TOPIC_WATCH_LLM__API_KEY). Full key reference:
config.example.yml and ARCHITECTURE.md.
No built-in authentication by design (single-user tool). Because of that, the port is published on 127.0.0.1 by default - a stock install is reachable only from the machine it runs on.
Reaching it from other devices
To reach it from other devices, set TOPIC_WATCH_BIND_ADDR=0.0.0.0 in .env (the installer offers this as a question) and put it behind a reverse proxy with auth (Authelia, Authentik, Caddy basicauth, Nginx basic auth). A host firewall is not a substitute: Docker publishes ports ahead of ufw and firewalld rules. If the proxy forwards a hostname other than localhost, *.local or an IP address, list it too: TOPIC_WATCH_ALLOWED_HOSTS=topic-watch.example.com. See SECURITY.md.
| Issue | Fix |
|---|---|
| LLM errors / checks failing | Check your API key and that the model string has its provider prefix (openai/gpt-5.4-nano, not gpt-5.4-nano). Logs: docker compose logs -f. |
| No notifications | Add an Apprise URL on the Settings page and press Test Notification. Verify the URL format. |
| 0 articles found | Open the RSS URL in a browser and check the Feed Health page. Some sites block bots. |
| "Sources failing" alert | Sent after silence_heartbeat_checks consecutive checks (default 3) with no usable source - a dead feed, an expired Exa key, or no network. Fix the source, or set silence_heartbeat_checks: 0 to turn the alert off. |
| Topic stuck in "Researching" | Auto-recovers after 15 minutes (set to Error). Retry from the topic page. Usually an LLM connectivity issue. |
| Docker container exits | docker compose logs for details. Check that data/ is writable. The installer sets PUID/PGID automatically; see SECURITY.md. |
| Can't reach it from another device | The port is published on 127.0.0.1 by default. Set TOPIC_WATCH_BIND_ADDR=0.0.0.0 in .env and run docker compose up -d. Add a reverse proxy with auth first - there is no login screen. |
| "Invalid host header" (HTTP 400) | You are reaching it by hostname. Set TOPIC_WATCH_ALLOWED_HOSTS to that hostname in .env (comma-separated for several) and restart. localhost and IP addresses need no setting. |
| High memory | Lower max_articles_per_check or content_fetch_concurrency. Increase check intervals. |
Contributions of any kind are welcome.
- New here? Start with CONTRIBUTING.md.
- Architecture overview: ARCHITECTURE.md.
- Security: SECURITY.md.
Bug reports and feature requests -> Issues. Questions and discussion -> Discussions.
GNU General Public License v3.0. See LICENSE.
