A local-first, multi-agent feed aggregator. Your sources. Your weights. Your device.
BYOF pulls content from multiple sources — news, research papers, newsletters — scores each item with a Claude Haiku agent, and surfaces your most relevant 20 items in a single ranked feed. No account. No tracking. No data leaves your device.
It's also a learning project for progressively complex multi-agent AI architectures. Each version deliberately increases agent orchestration complexity as a first-class goal.
| Feature | Description |
|---|---|
| Snap-scroll feed | Full-viewport cards — swipe or scroll through your ranked items |
| LLM scoring | Claude Haiku reads each article: generates summary, keywords, relevance score |
| Persona twins | Switch between Researcher / Generalist / Engineer lenses — reranks instantly |
| Preference learning | Like / save / skip signals drift ranking weights over time — entirely on device |
| Explore | Filter by format (Articles · Newsletters · Papers), topic, or source |
| Saved | Search and filter everything you've bookmarked |
| Local-first | All data stays on your machine — SQLite, no cloud, no telemetry |
| Layer | Tool |
|---|---|
| Language | Python 3.13+ |
| Package manager | uv |
| Database | SQLite |
| API | FastAPI + uvicorn |
| Frontend | React (Vite) — Glacier design system |
| LLM | Claude Haiku (per-item scoring via Anthropic SDK) |
| Sources | Google News · TechCrunch · ArXiv · MIT Tech Review · TLDR Tech |
Open app → Landing page (localStorage auth gate)
↓ first run
Preference setup → preferences.json
┌──────────────────── privacy boundary — local only ─────────────────────┐
│ │
│ Connectors Google News · TechCrunch · ArXiv · MIT TR · TLDR Tech │
│ → save_items() → SQLite │
│ → Image pipeline: slug fetch → Playwright fallback → logo fallback │
│ │
│ LLM Swarm Claude Haiku per-item (summary + keywords + score) │
│ │
│ Ranking pipeline │
│ → Weighing agent: category/subcategory preferences │
│ → Persona agent: researcher / generalist / engineer reweighting │
│ → Learning agent: like/save/skip decay boosts (7-day half-life) │
│ → Aggregation: ranked feed, top 20 │
│ │
│ FastAPI api.py :8000 ←→ React Vite frontend :5173 │
│ Feed (For You) · Explore · Saved · Profile │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Requires: Python 3.13+, Node.js 18+, uv
# 1. Clone and install
git clone https://github.com/rohitkuk/BYOF.git
cd BYOF
uv sync
uv run playwright install chromium # for JS redirect image resolution
# 2. Add your Anthropic API key
cp .env.example .env
# Edit .env → set ANTHROPIC_API_KEY=sk-ant-...
# 3. Initial data fetch (populates DB + resolves images)
uv run python app.py
# 4. Start the backend API
uv run python api.py # → http://localhost:8000
# 5. Start the frontend (new terminal)
cd frontend && npm install && npm run dev # → http://localhost:5173Prefer a single command? The start script auto-detects your LAN IP so mobile devices on the same WiFi can connect:
bash start.shOpen http://localhost:5173. The landing page appears — click "Sign in with Google" to enter the app. This uses a localStorage bypass; no real OAuth or account is required.
Your ranked feed. Full-viewport snap-scroll cards, one article at a time.
Each card shows:
- Article image, category pills, title, source, and relative timestamp
- A READ link to open the full article
Action rail (right side of each card):
| Button | Action | Learning signal |
|---|---|---|
| ❤️ Like | Signals interest — boosts that source + keywords in future rankings | +1.0 |
| 🔖 Save | Bookmarks to Saved page — stronger interest signal | +1.5 |
| ⏭ Skip | Permanently hides this URL — gently demotes source in future rankings | −1.0 |
Signals accumulate across sessions and are weighted by recency. The feed gets smarter the more you use it — all on device.
Desktop users see progress dots on the right edge showing your position in the feed.
Filter the feed by content type, topic, or source.
- Format bar (top): All / Articles / Newsletters / Papers
- Topic pills: multi-select to narrow by subject area
- Source list: checkbox per source to include or exclude
- Tap Apply → filtered feed · Reset → clear all filters
Everything you've bookmarked via the save action.
- Search bar at top — full-text search across saved titles
- Type filter pills (Articles / Newsletters / Papers)
- Empty state shown when nothing saved yet
- Stats row: articles read · items saved · articles liked
- Preference pills: your current category preferences
- App info: source count, last refresh time, version
Trigger a new fetch + LLM scoring cycle:
curl -X POST http://localhost:8000/refreshCheck status:
curl http://localhost:8000/refresh/statusAfter each refresh, learned weights recompute automatically from your signal history and are applied to the next /feed call.
Three taste lenses that reweight the same item pool — no re-fetching required.
Edit preferences.json and set the "persona" field:
{ "persona": "researcher" }| Persona | Boosts |
|---|---|
researcher |
ArXiv · MIT Tech Review · papers, benchmarks, methodology, neural networks |
engineer |
TechCrunch · TLDR Tech · APIs, infrastructure, open-source, tooling, cloud |
generalist |
Balanced across all sources — default |
Switch persona → the next /feed call re-ranks instantly from the same SQLite data.
Every like / save / skip fires POST /signals to the backend. On each refresh cycle:
- Signals are read from SQLite joined with item metadata (source, keywords)
- Exponential decay applied — recent signals count more (7-day half-life means a signal from 2 weeks ago is worth 4× less than one from today)
- Per-source and per-keyword boosts computed, clamped to
[0.4 → 2.5] - Skipped URLs permanently filtered from feed output
- Boosts written to
preferences.json["learned"]— applied on every/feedcall
boost = max(0.4, min(2.5, 1.0 + accumulated_signal × 0.15))
The feed improves silently in the background. No model, no server, no sync — just your own signal history on your own machine.
Two independent tracks — advance either without coupling to the other.
| Phase | Sources | Status |
|---|---|---|
| 1 | Google News · TechCrunch · ArXiv · MIT Tech Review · TLDR Tech | ✅ Complete |
| 2 | Instagram + LinkedIn (browser session) | ⏳ Planned |
| 3 | Instagram Reels | ⏳ Planned |
| 4 | YouTube | ⏳ Planned |
| Version | What changes | Agents | Status |
|---|---|---|---|
| V1 Foundation | Curated feed, weighted, ranked, filterable | ~6 fixed | ✅ Complete |
| V2 Item swarm | Claude Haiku per-item: summary + keywords + score | 20–200+/day | ✅ Complete |
| V3 Persona + learning | Taste lenses + like/save/skip decay boosts | V2 + 2 | ✅ Complete |
| V4 Consensus jury | Agents debate; judge resolves; "why this beat that" visible | V3 + 1–3 | ⏳ Planned |
| V5 Dynamic allocation | Specialist agents spin up per niche content, within budget | Variable | ⏳ Planned |
See docs/ROADMAP.md for full detail.
- Slice 1 — Google News RSS connector → terminal output
- Slice 2 — Persist items to SQLite with dedup (
db/store.py) - Slice 3 — Aggregation agent ranks by recency (placeholder weighing)
- Slice 4 — Streamlit feed page reads DB and shows ranked list
- Slice 5 — Weighing agent with real preference ranking (category + subcategory scoring)
- Slice 6 — Snap-scroll reel cards, article og:images (slug + Playwright fallback), publisher logo fallback, 20-item feed cap
- Slice 6b — UI redesign: FAB drawer, action rail, like/skip/save
- Slice 6c — Frontend migration: Streamlit → FastAPI + React (Glacier design system)
- Slice 7 — TechCrunch connector
- Slice 8 — ArXiv, MIT Tech Review, TLDR Tech connectors
- Slice 9 — Full navigation redesign: TopBar, BottomNav, Explore, Saved, Profile pages
- Slice 10 — Landing page with localStorage auth bypass
- Anthropic SDK added
- DB schema extended: body, llm_summary, llm_keywords, llm_categories, llm_score, scored_at, signals
-
agents/content_fetcher.py— full page text + og:image extraction -
agents/item_scorer.py— Claude Haiku per-item scorer -
agents/swarm.py— ThreadPoolExecutor orchestrator, failure isolation, token tracking -
agents/aggregation.py— LLM score path (recency × llm_score × 10) -
/refreshendpoint wired to swarm;/runsendpoint added - Performance: WAL mode, DB indexes, 30s feed cache, GZip middleware
-
agents/personas.py— researcher / generalist / engineer source + keyword reweighting -
agents/learning.py— exponential-decay signal boosts, skipped URL filtering -
db/store.py— signals table,save_signal(),get_signals_with_items() -
api.py—POST /signalsendpoint; learned weights recomputed on each refresh -
agents/aggregation.py— persona + learned weights applied inrank() -
frontend/ActionRail.jsx— firesPOST /signalson like / save / skip
- Google News (RSS)
- TechCrunch
- ArXiv
- MIT Technology Review
- TLDR Tech (newsletter)
- Instagram / LinkedIn (Phase 2)
- YouTube (Phase 4)
Every file in connectors/ exposes one function:
def fetch() -> list[dict]:
"""
Returns items shaped as:
{"title": str, "url": str, "source": str, "published_at": str, "raw": dict}
"""Agents consume this shape only — nothing reaches into connector internals.
To add a source: create connectors/<name>.py implementing fetch(), then wire it into _do_refresh() in api.py.
Contributions welcome — especially new connectors and improvements to agent logic.
Before opening a PR, read docs/ARCHITECTURE.md — specifically the Connector Contract and the vertical-slice principle (build one connector end-to-end before the next; don't add a connector without wiring it all the way through to the feed).
