overlay-arena
A live directory and side-by-side comparison matrix for local-AI desktop overlays — Thuki-style hotkey assistants, MouseClaw-style pixel pets, MacGPT-class menu-bar AIs, Ollama-Pilots. Built for macOS/Windows power users who want a non-cloud Cursor/ChatGPT alternative but can't keep track of the weekly Show HN deluge.
The directory pulls real Show HN posts and Product Hunt launches at runtime, enriches each entry with live GitHub stats (stars, last commit, license), and — when an LLM key is configured — extracts a structured feature matrix from each repo's README. Two or three overlays can be compared in a permalinkable side-by-side view.
No mock data. Every overlay row points back to a real HN thread or Product Hunt launch with a timestamp.
What it does
- Directory (
#/) — every overlay we found this hour, with OS chips, feature chips, GitHub stars, HN points, and a click-to-compare checkbox. - Detail (
#/o/:slug) — full description, source links, repo stats, LLM-extracted feature matrix, share button. - Compare (
#/compare?slugs=a,b,c) — 2- or 3-column table with every feature axis. Differing cells are highlighted yellow. The URL is the share asset. - Weekly digest (
#/digest) — top 10 overlays surfaced in the last 7 days, ranked byhn_points × 0.5 + ph_votes × 0.5 + min(stars, 500) × 0.1. - Submit (
#/submit) — founders can drop their overlay. If the GitHub URL matches a repo we already track, it's auto-linked; otherwise it stays in the public submissions queue and surfaces when the next GitHub refresh detects ≥1 star and a recent commit. - Sources (
#/sources) — every cron tick logged: source, status, items added/updated, duration, error.
API endpoints
All mounted under /overlay-arena. JSON. CORS open. No auth.
| Method | Path | Purpose |
|---|---|---|
| GET | /health | {ok:true} HTTP 200 |
| GET | /api/overlays | List overlays. Query: os (macos/windows/linux), oss=1, vision=1, voice=1, search=1, q=<text>, limit, offset. Sorted by score desc. |
| GET | /api/overlays/:slug | Full overlay record incl. features, repo stats, source links. |
| GET | /api/compare?slugs=a,b,c | Returns {overlays, axes} for up to 3 slugs. |
| GET | /api/digest | Last-7-day top 10. |
| GET | /api/stats | Totals + last refresh timestamps. |
| GET | /api/sources | Latest cron-run row per source. |
| GET | /api/sources/history | Last 50 cron runs. |
| POST | /api/submit | Body: {name, url, repo_url, description, contact}. Returns {id, status, linked_overlay_id}. |
| GET | /api/submissions | Public submission queue. |
Data sources
| Source | URL | Refresh | On failure |
|---|---|---|---|
| HN Algolia Search | https://hn.algolia.com/api/v1/search?tags=story&query=<term>&hitsPerPage=50 (10 queries: "local ai overlay", "ollama overlay", "menu bar ai", "ai hotkey", "desktop ai assistant", "ai pet desktop", "local llm desktop", "macos ai assistant", "show hn ollama", "local llm overlay") | Every 30 min via node-cron + once at boot | Logged to fetch_log; previous rows retained |
| Product Hunt GraphQL | https://api.producthunt.com/v2/api/graphql (filter topic="artificial-intelligence", top 50 by votes; client-side keyword filter) | Daily 03:15 + once at boot | Skipped silently if PRODUCTHUNT_TOKEN unset; error logged otherwise |
| GitHub Repos API | https://api.github.com/repos/{owner}/{repo} + /commits?per_page=1 | Every 15 min — 10 oldest-updated tracked repos, 1/sec staggered | 404 → repo_dead=1, row kept; 403 → back off until next tick |
| GitHub raw README | https://raw.githubusercontent.com/{owner}/{repo}/HEAD/README.md (with .rst/no-ext fallbacks) | When repo's HEAD SHA changes | Leaves readme_text=NULL; row remains, feature extraction skipped |
| OpenRouter (LLM feature extractor) | https://openrouter.ai/api/v1/chat/completions model anthropic/claude-haiku-4-5 | Every 20 min, up to 5 overlays where README is fresh + features stale | Skipped silently if OPENROUTER_API_KEY unset; card shows unverified badge |
The product degrades gracefully without PRODUCTHUNT_TOKEN or OPENROUTER_API_KEY — HN + GitHub alone seed the directory and stats.
Run locally
npm install
PORT=4809 node server.js
# open http://localhost:4809/overlay-arena/
Optional env (.env.example shows the keys):
PRODUCTHUNT_TOKEN=... # enables PH scrape
OPENROUTER_API_KEY=... # enables LLM feature extraction
GITHUB_TOKEN=... # bumps GitHub rate limit 60/h → 5000/h
On boot, the server runs one HN scrape and starts a non-blocking GitHub refresh. The directory has real overlays within ~10 seconds.
Stack
- Node.js ≥22, ES modules
- Express 5
- better-sqlite3 (WAL mode)
- node-cron
- helmet + compression
- Vanilla JS frontend with hash router
File layout
overlay-arena/
├── server.js # Express boot, mounts /overlay-arena
├── db.js # SQLite schema + prepared statements
├── cron.js # node-cron registration + boot fetch
├── lib/
│ ├── http.js # fetch wrapper (UA, timeout, retry)
│ ├── slug.js # slugify + name derivation
│ ├── extract-repo.js # github.com/owner/repo regex
│ └── score.js # ranking + rel-time helper
├── routes/
│ ├── overlays.js # list / detail / digest / stats
│ ├── compare.js # /compare?slugs=
│ ├── submit.js # POST + list submissions
│ └── meta.js # health + sources
├── scrapers/
│ ├── hn.js # HN Algolia multi-query
│ ├── producthunt.js # PH GraphQL
│ ├── github.js # repo meta + last commit + README
│ └── features.js # OpenRouter feature extraction
├── public/
│ ├── index.html # SPA shell
│ ├── app.js # router + views
│ └── style.css # dark theme
└── .env.example
Out of scope
Auth, accounts, ratings, comments, screenshots, benchmarks, latency measurements, downloads, paid feature audits, RSS/email digests, Linux-only tools. The "moderation queue" is heuristic only — no human-review UI.