A local-first news desk for AI agents. Agent News Bot scans 14 topic "desks" from public,
key-free sources, filters everything down to only the stories you have not seen before, generates
a ready-to-post tweet and hashtags for each item, and serves the fresh batch over a small
token-authenticated HTTP API bound to 127.0.0.1. An agent, script, or newsroom pipeline can ask
"what's actually new right now?" and get a clean JSON answer instead of re-scraping the open web.
It ships as two parallel implementations of the same design:
- macOS app (
Sources/) — a native SwiftUI desktop newsroom with a dark UI, menu-bar extra, JSON exports, and optional one-click handoff to local agent runtimes. - Linux service (
linux/agent_newsbot/) — a headless Python daemon (systemd-friendly) with a periodic fetch loop and alatest.jsonfile snapshot for file-based consumers.
Both speak the same HTTP bridge API and share the same freshness model, so an agent written against one works against the other.
- Who it's for
- Key features
- The 14 desks & their sources
- Architecture
- The freshness model
- The bridge API
- Privacy & data model
- Build & run — macOS
- Build & run — Linux
- Configuration
- Optional agent integrations
- Screenshots
- Project layout
- Contributing
- License
- Agent / automation authors who need a deduplicated, time-bounded feed of current headlines and want to call one localhost endpoint instead of maintaining scrapers.
- Newsroom / social pipelines that must avoid re-posting the same story: the built-in freshness ledger guarantees each item is served once.
- Anyone who wants a private, keyless news aggregator on their own machine — no accounts, no API keys, no third-party analytics.
- 14 desks spanning general news, finance, tech/AI, prediction markets, and weather.
- Key-free sources. Every source is a public RSS/Atom feed or an open JSON API — no source requires an API key or login.
- "Only what's new." A SQLite freshness ledger dedupes by canonical URL and enforces a lookback window (default 12h). Ask twice and you won't get the same story twice.
- Social-ready output. Each article carries a pre-composed
tweet(280-char aware, counts thet.co23-char URL cost) and ahashtagslist built from desk tags + keyword extraction. - Token-authenticated localhost bridge.
GET /v1/fresh,/v1/desks,/v1/status, andPOST /v1/fetch, bound to127.0.0.1only, behind a bearer token that is generated on first run. - Two runtimes, one contract. Native macOS app and headless Linux daemon expose the same API.
- Live-desk carry-over (Linux). If a desk produces nothing new this cycle, still-fresh items from the previous batch are retained so the desk never looks empty while it's simply current.
| Desk | Source(s) | API key? |
|---|---|---|
breaking |
NPR, BBC, NY Times, AP (RSS) | none |
politics |
NPR, BBC, NY Times, Politico, The Hill (RSS) | none |
world |
BBC, NPR, NY Times, Al Jazeera, Deutsche Welle, AP (RSS) | none |
business |
NPR, BBC, CNBC, Yahoo Finance, MarketWatch (RSS) | none |
tech |
Ars Technica, The Verge, TechCrunch, Wired, BBC, The Register (RSS) | none |
ai |
TechCrunch AI, Verge AI, MIT Tech Review, Simon Willison, Hugging Face, Google AI, NVIDIA, VentureBeat, arXiv cs.AI/cs.LG, Wired AI (RSS) | none |
science |
NPR, BBC, NY Times, ScienceDaily, NASA (RSS) | none |
health |
NPR, BBC, NY Times, Medical News Today (RSS) | none |
entertainment |
BBC, Variety, Deadline, Hollywood Reporter (RSS) | none |
sports |
NPR, BBC Sport, ESPN, Yahoo Sports (RSS) | none |
markets |
Yahoo Finance chart API (configurable symbol list) | none |
hackerNews |
Hacker News Firebase API (new + top stories) | none |
polymarket |
Polymarket Gamma API (events + markets, by 24h volume) | none |
weather |
Open-Meteo geocoding + forecast API | none |
The exact feed URLs live in
linux/agent_newsbot/config.py(Python) andSources/Feeds.swift(Swift). Add or remove feeds by editing those tables.
┌────────────────────────────────────────────┐
│ Fetch engine (per desk, run concurrently) │
│ RSS/Atom · HN Firebase · Polymarket Gamma │
│ Yahoo Finance · Open-Meteo │
└───────────────┬──────────────────────────────┘
│ normalize → dedupe → tag → tweet
▼
┌────────────────────────────────────────────┐
│ Freshness ledger (SQLite: seen + quotes) │
│ canonical-URL SHA-256 id · lookback window │
└───────────────┬──────────────────────────────┘
│ fresh batch (snapshot)
▼
┌────────────────────────┴────────────────────────┐
│ Bridge (HTTP on 127.0.0.1, bearer-token auth) │
│ GET /health /v1/fresh /v1/desks /v1/status │
│ POST /v1/fetch │
└───────┬─────────────────────────────┬─────────────┘
│ │
macOS: SwiftUI UI + JSON export Linux: latest.json snapshot file
│ │
▼ ▼
agents / scripts read the same JSON contract
macOS implementation (Sources/, Swift/SwiftUI):
App.swift/ContentView.swift/SettingsView.swift— the desktop UI, menu-bar extra, and keyboard commands (⌘R fetch, ⇧⌘H send to Hermes).Engine.swift— orchestrates concurrent per-desk fetches, merges/sorts, marks items seen, exports JSON, and creates/loads the bridge token.AgentBridge.swift— a tiny hand-rolledsockaddr_inHTTP server bound to127.0.0.1, serving the bridge API.RSSClient/HNClient/PolymarketClient/MarketsClient/WeatherClient— one fetcher per source family;Freshness.swiftis the SQLite ledger;Hashtagger.swiftbuilds tags + tweets.SettingsStore.swift—UserDefaultsfor config, Keychain for optional integration tokens.HermesClient.swift/OpenClawClient.swift— optional handoff to local agent runtimes.
Linux implementation (linux/agent_newsbot/, Python 3, stdlib + feedparser + requests):
server.py—ThreadingHTTPServeron127.0.0.1, bearer-token auth, background fetch loop, atomiclatest.jsonwrites.engine.py— the concurrent fetchers (ThreadPoolExecutor), one per desk family.store.py— the SQLite freshness ledger (seen+quotestables), URL canonicalization, date parsing, HTML stripping.config.py— feed tables, desk titles/tags, and all env-var-driven configuration.hashtags.py— the tag + 280-char tweet composer.__main__.py— CLI:serve|fetch|status.
An item's identity is the SHA-256 of its canonicalized URL (tracking params like utm_*,
fbclid, gclid, ocid stripped; host lowercased; trailing slash removed), truncated to 24 hex
chars. The seen table records every id ever served, so an item is emitted exactly once. A
configurable lookback window (default 12h) drops anything older. For markets and weather,
a quotes table stores the last value so unchanged (or sub-0.15% market) readings are suppressed as
noise. The result: repeated calls return only genuinely new, recent stories, and an empty result
means "the desks are current" — not an error.
Base URL: http://127.0.0.1:18765 (port configurable). All endpoints except /health require the
bearer token in either an Authorization: Bearer <token> header or an X-Agent-Token: <token>
header. The server binds to loopback only and returns 401 without a valid token.
| Method & path | Auth | Description |
|---|---|---|
GET /health |
no | Liveness probe; returns app name + bridge URL. |
GET /v1/fresh?desk=<desk>&limit=<n> |
yes | The current fresh batch. Optional desk filter and limit (default 100). |
GET /v1/desks |
yes | Per-desk counts: fresh, skipped-old, skipped-dup, and source errors. |
GET /v1/status |
yes | Snapshot summary: total fresh, fetched_at, whether a fetch is in flight. |
POST /v1/fetch |
yes | Force an immediate refresh; returns the new total. |
Example (Linux service, token read from its export file):
TOKEN=$(cat "$AGENT_NEWSBOT_HOME/export/bridge.token")
curl -sS -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1:18765/v1/fresh?desk=ai&limit=20'
curl -sS -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1:18765/v1/desks'
curl -sS -X POST -H "Authorization: Bearer $TOKEN" 'http://127.0.0.1:18765/v1/fetch'On macOS the token is shown in Settings → Bridge and is written to
~/Library/Application Support/AgentNewsBot/bridge.token.
Article JSON shape:
{
"id": "3f2a…", "desk": "ai", "category": "AI",
"title": "…", "url": "https://…", "source": "TechCrunch AI",
"published": "2026-08-15T13:04:00+00:00", "summary": "…",
"tweet": "… https://… #AI #LLM", "tweet_chars": 174,
"hashtags": ["#AI", "#LLM"]
}- Local-first. The service runs on your machine and binds to
127.0.0.1. Nothing is exposed to the network by default. - No accounts, no keys, no telemetry. No source requires credentials; the app phones no analytics home.
- The only persisted state is a SQLite ledger of story ids/urls/titles you've already seen (for dedup) and last market/weather values (for change detection), plus an optional JSON export/snapshot of the current batch. No personal data is collected.
- The bearer token is generated on first run (macOS: a UUID-derived 32-hex string written 0600;
Linux:
secrets.token_hex(16)written 0640) and never leaves the machine. It is not in source control. Optional integration tokens on macOS are stored in the Keychain, not in plists. - Content is fetched, not redistributed. Agent News Bot links to and summarizes third-party articles at request time; it stores headlines/links for dedup but is not a content archive. Each upstream source's own terms govern use of its feed.
Requirements: macOS 14+, the Swift toolchain (Xcode or Command Line Tools). No Xcode project and no
third-party Swift packages — it compiles straight from source with swiftc.
./build.sh
open "/Applications/Agent News Bot.app"build.sh renders the icon, compiles all Sources/*.swift into an app bundle, writes Info.plist,
ad-hoc code-signs, and installs to /Applications. Launch it, open Fetch Fresh (⌘R), and the
bridge starts automatically on 127.0.0.1:18765.
Requirements: Python 3.10+, pip install feedparser requests.
Run it directly from the package directory:
cd linux
pip install feedparser requests
python3 -m agent_newsbot serve # start the API + periodic fetch loop
python3 -m agent_newsbot fetch # run one fetch, print JSON
python3 -m agent_newsbot status # show last snapshot countsFor a long-running deployment, install it as a systemd service:
cd linux
sudo ./install.sh # creates the 'agentnewsbot' user, installs to /opt/agent-news-bot,
# state under /var/lib/agent-news-bot, and starts agent-news-bot.serviceinstall.sh is fully env-var driven — override PREFIX, STATE_DIR, and SVC_USER to install
elsewhere or under a different service account. It builds the unit from
agent-news-bot.service.example (a plain, path-agnostic
example you can also install by hand). Nothing about any host is baked into these files.
The Linux service is configured entirely through environment variables (all optional):
| Variable | Default | Meaning |
|---|---|---|
AGENT_NEWSBOT_HOME |
(set per install) | State dir (SQLite ledger + export/bridge.token). |
AGENT_NEWSBOT_SHARED |
(set per install) | Dir where latest.json snapshot is written. |
AGENT_NEWSBOT_PORT |
18765 |
Bridge port (always bound to 127.0.0.1). |
AGENT_NEWSBOT_LOOKBACK_HOURS |
12 |
Freshness window. |
AGENT_NEWSBOT_MAX_PER_DESK |
20 |
Max items kept per desk. |
AGENT_NEWSBOT_WEATHER_CITY |
New York |
City for the weather desk. |
AGENT_NEWSBOT_SYMBOLS |
S&P/Dow/Nasdaq/mega-caps/BTC/ETH | Comma-separated market symbols. |
AGENT_NEWSBOT_FETCH_INTERVAL |
900 |
Seconds between background fetches. |
On macOS the equivalent settings live in Settings (lookback, max per desk, weather city, market
symbols, bridge port, enabled desks) and persist in UserDefaults.
The macOS app can hand a selected batch to a local agent runtime if one is installed:
- Hermes — via a
hermesCLI onPATH, or an OpenAI-compatible chat endpoint (default probehttp://127.0.0.1:8642; optional bearer token stored in Keychain). - OpenClaw — via an
openclawCLI, or a local gateway (defaultws://127.0.0.1:18789).
Both are optional and default to localhost. If neither is present, agents can still simply
GET /v1/fresh from the bridge — the integrations are a convenience, not a requirement. On launch
the app also writes a small skill descriptor (endpoint + token) to ~/.hermes/skills/ and
~/.agents/skills/ so a co-located agent can discover the local desk.
No screenshots are committed to this repository. The only bundled image is the app icon source at
Resources/icon-1024.png (used by build.sh to generate the .icns). If you add screenshots,
place them under a top-level docs/ or screenshots/ directory and reference them here.
.
├── build.sh # macOS build → /Applications
├── Resources/ # app icon source + icon-drawing script
├── Sources/ # macOS SwiftUI app (one file per concern)
├── skills/agent-news-bot/ # generic skill descriptor (localhost API)
└── linux/
├── agent_newsbot/ # Python package: server, engine, store, config…
├── agent-newsbot # CLI wrapper
├── install.sh # generic systemd installer (env-var driven)
├── agent-news-bot.service.example # path-agnostic example unit
└── SKILL.md # localhost skill descriptor
- Adding a source is usually just a new row in the feed tables (
Sources/Feeds.swift/linux/agent_newsbot/config.py) — the RSS path needs no code. - New source families (a non-RSS API) get a small dedicated fetcher mirroring the existing
*Client/fetch_*pattern and must respect the freshness ledger. - Keep the Swift and Python implementations behaviorally in sync — they share one API contract.
- Never commit tokens, real hostnames, private paths, or personal data; configuration flows through env vars (Linux) and Settings/Keychain (macOS).
Recommended: MIT (see repository LICENSE). The project uses only system frameworks on macOS and
permissively-licensed Python dependencies (feedparser, requests), so there is no copyleft
obligation. Agent News Bot fetches and links to third-party news at runtime and redistributes no news
content itself; each upstream source's terms govern use of its feed.