Every image below is served live from the deployed app — click through to the real surface.
| Surface | Live preview |
|---|---|
| Homepage (terminal + bubble map) | trendingrepo.com · OG card |
| Compare deep-dive | /compare · OG card |
| Repo detail | NawfalMotii79/PLFM_RADAR · OG card |
| Category page | /categories/ai-agents |
| Portal docs | /portal/docs |
| CLI page | /cli |
TrendingRepo ingests GitHub, Hacker News, ProductHunt, Bluesky, dev.to, package, funding, and model/ecosystem signals from the HOSTUP worker fleet, scores momentum + breakout velocity across the stack, and surfaces the movers through four parallel surfaces: a web terminal with a bubble map, a zero-dependency CLI, an MCP server for Claude / any agent, and a Portal v0.1 endpoint so any LLM can query trending repos with a single manifest fetch. Reddit is intentionally paused until a credentialed, non-empty producer is approved.
One data pipeline. Four consumers. No mocks — every number is anchored in a live source or a committed snapshot, so the numbers you see are the numbers your agent queries.
- 🫧 Bubble map — Coin360-style physics visualisation of the top 220 movers per window (24 h / 7 d / 30 d). Auto-stops when settled, so idle CPU is zero.
- 📈 Momentum score (0–100) — composite of 24 h / 7 d / 30 d star velocity + fork growth + contributor churn + commit / release freshness + anti-spam dampening.
- 🔥 Breakout + hot + quiet-killer classifier — tier-aware rules run against rolling baselines, not static cutoffs.
- 🎯 15 categories × 15 distinct hues — DevTools, AI Agents, MCP, Databases, Infra, Rust, Crypto, Web Frameworks, and more.
- ⚡ ISR-cached homepage —
revalidate = 1800. Static edge hit, fresh data from HOSTUP-worker-owned Redis payloads. - 🧩 Portal v0.1 + MCP server — same three tools (
top_gainers,search_repos,maintainer_profile) exposed over both protocols from a single registry. No drift between the CLI, the browser, and the agent. - 📊 Side-by-side compare — pick up to 4 repos, see star-history, contributor grids, commit heatmaps, language breakdown, and a winner scoreboard.
- 🔖 Watchlist + bookmarks — local-first, synced via Zustand +
localStorage. No auth required. - 💻 Zero-dep CLI (
bin/ss.mjs) — Node 18+, reads the same pipeline as the web. - 🔎 Live search preview — debounced autocomplete dropdown with keyboard nav, rendered through a Portal so it escapes the sticky-header stacking context.
Three ways to use TrendingRepo, ordered by effort:
1. Visit the site. trendingrepo.com
2. Query from a terminal.
# Via the spec-native Portal visitor
npx @visitportal/visit https://trendingrepo.com/portal top_gainers --limit=10
# Or via the native CLI (clones + runs from GitHub)
npx github:0motionguy/starscreener trending --window=24h --limit=53. Plug into Claude / any MCP agent.
# HTTP transport (Claude Code 2+)
claude mcp add trendingrepo \
--transport http \
--url https://trendingrepo.com/portalOr hit the REST endpoint directly:
curl -X POST https://trendingrepo.com/portal/call \
-H "Content-Type: application/json" \
-d '{"tool":"search_repos","params":{"query":"agent","limit":5}}'starscreener/
├─ src/
│ ├─ app/ Next.js App Router — pages, API routes, OG cards
│ │ ├─ page.tsx Homepage: bubble map + terminal (ISR 30 min)
│ │ ├─ search/ compare/ Search + side-by-side compare surfaces
│ │ ├─ repo/[owner]/[name]/ Repo detail page — growth chart, why-moving, mentions
│ │ ├─ collections/ categories/ Curated sets + category browsers
│ │ ├─ portal/ GET /portal + POST /portal/call
│ │ ├─ portal/docs/ Integration guide (REST + MCP tabs)
│ │ ├─ cli/ CLI landing page
│ │ └─ api/ REST + pipeline admin + SSE stream
│ ├─ components/
│ │ ├─ terminal/ Dense table, BubbleMap, FilterBar, FeaturedCards, column defs
│ │ ├─ compare/ Side-by-side chart, heatmap, contributor grid, winner chips
│ │ ├─ detail/ Repo detail: header, Recharts growth, why-moving, mentions
│ │ ├─ layout/ AppShell, Sidebar, MobileDrawer, Footer
│ │ └─ shared/ Primitives: Sparkline, SearchBar, badges
│ ├─ lib/
│ │ ├─ derived-repos.ts Assembles the full Repo[] from data/*.json (cached once)
│ │ ├─ trending.ts OSS Insight + git-history delta loader
│ │ ├─ bubble-pack.ts Circle-pack algorithm for the bubble map
│ │ ├─ pipeline/
│ │ │ ├─ scoring/ Momentum score: components + weights + modifiers
│ │ │ ├─ classification/ Category + tag inference from topics, name, desc
│ │ │ ├─ ingestion/ GitHub API pulls, stargazer backfill
│ │ │ └─ storage/ Snapshot + repo + score stores (in-memory)
│ │ └─ types.ts Single source of truth for Repo, Category, etc.
│ ├─ portal/ Portal v0.1 server — manifest, dispatcher, rate-limit, validate
│ └─ tools/ Tool handlers (callable via /portal/call + MCP + CLI)
│ ├─ top-gainers.ts
│ ├─ search-repos.ts
│ └─ maintainer-profile.ts
├─ mcp/ Published MCP server (trendingrepo-mcp) — stdio bridge
├─ bin/
│ └─ ss.mjs Zero-dependency CLI (Node 18+)
├─ data/ Committed JSON — ships with every deploy
│ ├─ trending.json OSS Insight buckets (24h/7d/30d × 5 langs)
│ ├─ deltas.json Star deltas computed from git history
│ ├─ repo-metadata.json GitHub REST snapshot per repo
│ ├─ recent-repos.json Newly discovered repos
│ ├─ hot-collections.json
│ ├─ collection-rankings.json
│ └─ collections/ Curated YAML sets (imported from OSS Insight, Apache 2.0)
├─ scripts/ Data-refresh mjs scripts (run via GHA)
│ ├─ scrape-trending.mjs
│ ├─ compute-deltas.mjs
│ ├─ fetch-repo-metadata.mjs
│ ├─ discover-recent-repos.mjs
│ └─ portal-conformance.mjs
├─ docs/ Architecture notes (ARCHITECTURE, API, DEPLOY, etc.)
├─ .github/workflows/
│ ├─ ci.yml Typecheck + lint + test + build on push/PR
│ ├─ scrape-trending.yml Refresh data/*.json every 20 min (cron 7/27/47 * * * *)
│ └─ refresh-collection-rankings.yml
└─ public/ Favicons, manifest, app icons
GitHub
│
┌────────────────┼────────────────┐
│ │ │
scrape-trending fetch-repo-metadata discover-recent
(every 20m) (daily) (daily)
│ │ │
└────────────────┼────────────────┘
▼
data/*.json ──── committed to main
│
│ (read once per Lambda cold start)
▼
src/lib/derived-repos.ts
┌───────────┬───────────┼───────────┬───────────┐
│ │ │ │ │
classify score synth-sparkline deltas metadata
└───────────┴─────────┬─┴───────────┴───────────┘
│
fully-scored Repo[]
│
┌─────────────┬──────┴──────┬──────────────┐
│ │ │ │
/ + /search /api/repos /portal/call mcp/server.js
(ISR 30m) (REST + SSE) (Portal v0.1) (stdio → LLM)
- Every 20 min —
scrape-trending.ymlrefreshesdata/trending.json(top movers across 24 h / 7 d / 30 d × 5 languages) and regeneratesdata/deltas.jsonfrom that file's git history so each repo gets windowed star deltas anchored in real commit timestamps. - Daily —
fetch-repo-metadata.mjspullsdata/repo-metadata.jsonfrom the GitHub REST API (stars, forks, contributors, topics, avatar). - Runtime —
src/lib/derived-repos.tsmerges trending + metadata + recent-repos, runsclassifyBatch()+scoreBatch(), and caches the result module-level (survives Lambda warm-starts). - Deltas warm up over time — for repos new to the tracking set,
delta_24h.basis === "cold-start"until 24 h of history accumulates. The UI displays a dash instead of a fake 0 %, and sparklines are synthesized from the available anchors so rows aren't blank.
Full OpenAPI 3.1 spec: docs/openapi.yaml (source of truth) or docs/openapi.json (served live at /api/openapi.json).
Explore interactively at /docs — rendered with Redoc from a CDN-loaded bundle (zero added app-bundle weight). Raw spec: docs/openapi.yaml or /api/openapi.json.
# Local
open http://localhost:3023/docs
# Or bring your own tool against the deployed spec
npx @redocly/cli preview-docs docs/openapi.yaml
open "https://petstore.swagger.io/?url=https://trendingrepo.com/api/openapi.json"Primary entry point for programmatic use: GET /api/repos/{owner}/{name}?v=2 — returns the full profile (score, reasons, mentions, freshness, twitter, npm, ProductHunt, revenue, funding, related, prediction, ideas) in one round-trip.
Auth surfaces summarised: public reads have no auth; write endpoints use Authorization: Bearer <CRON_SECRET | ADMIN_TOKEN | USER_TOKEN>, or the HMAC-signed ss_user cookie issued by POST /api/auth/session. See the spec for the per-endpoint matrix.
When editing the spec, regenerate the JSON sibling so /api/openapi.json stays in sync:
npx @redocly/cli bundle --ext json docs/openapi.yaml > docs/openapi.jsonTrendingRepo exposes RSS 2.0 feeds for the two highest-leverage streams so aggregators, newsletters, and LLM agents can subscribe without polling the HTML pages. Every feed is hand-rolled (no deps), cached for 30 min at the edge, and valid against W3C Feed Validator.
| Feed | Contents | Cadence |
|---|---|---|
/feeds/breakouts.xml |
Top 30 repos firing on 2+ signals (GitHub + Reddit + HN), sorted by cross-signal score. Matches the /breakouts page's default "multi" filter. |
s-maxage=1800, stale-while-revalidate=3600 |
/feeds/funding.xml |
30 most recent AI / startup funding signals from TechCrunch, VentureBeat, Sifted, YC, NewsAPI. Each <item> links to the source article. |
s-maxage=1800, stale-while-revalidate=3600 |
Each item includes an RFC-822 pubDate, an isPermaLink guid, category
tags, author attribution, and a CDATA-wrapped HTML description so inline
markup survives round-tripping through RSS readers. Discovery tags are
emitted via <atom:link rel="self"> in both feeds.
The sitemap (/sitemap.xml) includes
every static page, every category, every collection, and up to 5,000 tracked
repos ordered by momentum — each /repo/{owner}/{name} entry carries its
own lastModified timestamp derived from lastCommitAt, with priority
scaled 0.3–0.9 by momentumScore so hot repos get crawled more aggressively.
# Subscribe via any RSS client, or preview in a terminal:
curl -sS https://trendingrepo.com/feeds/breakouts.xml | head -c 500
curl -sS https://trendingrepo.com/feeds/funding.xml | head -c 500
curl -sS https://trendingrepo.com/sitemap.xml | head -c 500# Read the manifest
curl https://trendingrepo.com/portal | jq
# Call a tool
curl -X POST https://trendingrepo.com/portal/call \
-H "Content-Type: application/json" \
-d '{"tool":"top_gainers","params":{"limit":5}}'Three tools, one registry (src/tools/):
| Tool | Purpose |
|---|---|
top_gainers |
Repos sorted by star delta across a window (24 h / 7 d / 30 d) |
search_repos |
Full-text search over name / description / topics, ranked by momentum |
maintainer_profile |
Aggregates owned repos for a GitHub handle across the tracked set |
Envelope on every response: { ok: true, result } or { ok: false, error, code } with stable codes (NOT_FOUND, INVALID_PARAMS, RATE_LIMITED, INTERNAL). Rate limit returns HTTP 429 with Retry-After.
A stdio MCP bridge lives in mcp/. Build + register:
npm run mcp:build
claude mcp add trendingrepo node ./mcp/dist/server.jsOr go HTTP-native (no bundle required):
claude mcp add trendingrepo --transport http --url https://trendingrepo.com/portalV3 is the production skin: a Node/01 x Linear fusion. Dark canvas, sharp 2px
corners, hairline frames, mono uppercase labels, accent reserved for the
focused object. New work targets --v3-* tokens and .v3-* utility classes;
older --v2-* names are aliased to V3 in src/app/globals.css so partially
migrated components inherit the V3 palette automatically.
The full token vocabulary (surfaces, hairlines, ink, accents, motion) is
documented in docs/DESIGN_SYSTEM.md. Two CI
guards prevent regression: npm run lint:tokens rejects new --v2-* /
legacy hex references, and npm run lint:v3-budget snapshots --v2-*
alias counts in scripts/_v3-token-baseline.json and fails when any
pattern grows.
Three runners cover different layers; the CI workflow
runs all three on every PR and push to main.
npm run test:hooks # Vitest — 93 tests for hooks, components, lib
npm test # node:test + tsx --test — collectors, pipeline, tools, portal
npm run test:e2e # Playwright — 12 E2E smokes against a production buildIn CI the gate also runs npm run typecheck, the chained npm run lint:guards
(tokens, err-message, zod-routes, route-runtime, error-envelope), the
standalone lint:v3-budget guard, and a full next build before Playwright
boots next start on port 3023.
# Install + run
npm install
npm run dev # http://localhost:3023
# Quality
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run test # pipeline + tools + portal tests
npm run build # production build
# Data refresh (local parity with GHA)
npm run scrape
npm run scrape:ph # requires PRODUCTHUNT_TOKEN
npm run fetch:metadata
npm run compute-deltas
# Portal conformance against the live endpoint
npm run portal:conformanceProduction is HOSTUP behind Cloudflare Tunnel, not Vercel. Do not deploy,
promote, unpause, or reconnect the Vercel starscreener project unless Mirko
explicitly approves a reversal. See docs/DEPLOY-TOOLBOX.md
and docs/HANDOVER-2026-06-01-PRODUCTION-HARDENING.md.
Verify live production with:
npm run health:prod
curl -sI https://trendingrepo.com/ | grep -iE "^HTTP|^server:|^x-vercel"Expected: HTTP 200, Server: cloudflare, and no X-Vercel-* headers.
Required env: GITHUB_TOKEN (for scraping + compare API), CRON_SECRET (guards /api/pipeline/* admin routes). ProductHunt refreshes also require a GitHub Actions secret named PRODUCTHUNT_TOKEN for .github/workflows/scrape-producthunt.yml, or the same variable in .env.local for npm run scrape:ph. See .env.example.
Production data freshness is owned by the HOSTUP worker. GitHub Actions schedules that remain enabled should be live probes or explicit app-cron calls, not duplicate data producers for worker-owned sources.
The table below is historical and should be rechecked against docs/ENGINE.md,
workflow YAML, and npm run health:prod before use.
| Schedule (UTC) | Endpoint | Purpose |
|---|---|---|
15 */2 * * * |
/api/pipeline/ingest |
GitHub + social adapters batch ingest |
30 */6 * * * |
/api/pipeline/persist |
Flush in-memory stores to JSONL |
0 4 * * * |
/api/pipeline/cleanup |
Archive / delete stale repo rows |
0 5 * * 0 |
/api/pipeline/rebuild |
Weekly full rebuild (Sundays) |
0 6 * * * |
/api/cron/predictions |
Daily top-N momentum predictions |
0,30 * * * * |
/api/cron/aiso-drain |
Drain AISO rescan queue (every 30 min) |
5,35 * * * * |
/api/cron/webhooks/scan |
Enqueue breakout + funding webhook deliveries |
10,40 * * * * |
/api/cron/webhooks/flush |
Drain Slack / Discord webhook queue |
9 9 * * * |
/api/cron/webhooks/dead-letter-digest |
Summarize failed customer webhook deliveries |
*/15 * * * * |
/api/health |
Unauthed freshness / status probe |
.github/workflows/cron-*.yml — richer logs, manual fire via workflow_dispatch, and per-run concurrency groups. Requires the repo's Actions to be enabled and these secrets/vars:
secrets.CRON_SECRET— must match the server'sCRON_SECRETenvvars.TRENDINGREPO_URL(legacy aliasSTARSCREENER_URLstill accepted) — optional; defaults to the hard-coded prod URL in each workflow
Vercel Cron is not the production path. Vercel starscreener should remain
paused and Git-disconnected under the cost guard. Keep this section only as
historical fallback documentation.
vercel.json -> top-level crons field — native to Vercel Deployments, auto-adds Authorization: Bearer <CRON_SECRET> when you set CRON_SECRET as a project env var, no extra wiring required. Vercel Cron fires GET (not POST), so every cron endpoint either already exports GET or has a GET alias that delegates to POST. /api/pipeline/ingest keeps its existing GET usage-docs response and only switches to ingest behavior when called with ?cron=1 (as registered in vercel.json).
The Vercel/GitHub double-fire guidance below is historical. In current production, HOSTUP worker freshness is primary and duplicate GitHub data producers should stay disabled.
If you deploy on Vercel AND have GitHub Actions enabled, both will fire on roughly the same cadence. The in-process ingest cooldown, per-file locks (AISO queue, predictions JSONL), and idempotent persistence make duplicate runs safe — but you will pay for two runs per cycle.
- Keep GH Actions primary (recommended): delete or comment out the
cronsblock invercel.json. - Keep Vercel Cron primary: disable the
.github/workflows/cron-*.ymlworkflows (Actions -> select -> "Disable workflow"). Do not delete them — the files remain a usable fallback. - Keep both (belt-and-suspenders): leave both live. Expect roughly 2x cron runs with no correctness impact.
If you deploy outside Vercel (e.g. self-hosted), only the GitHub Actions path fires.
Do not run this checklist unless Mirko explicitly approves reversing the HOSTUP cost guard.
- Set
CRON_SECRETin the Vercel project's env (Production + Preview), distinct fromADMIN_TOKEN. - Redeploy so the new env reaches the cron handlers.
- First cron tick after deploy: verify in the Vercel -> Cron dashboard that all 7 jobs registered and their next-run times are correct.
- Manually trigger any cron from the Vercel dashboard to smoke the auth path end-to-end.
Deliver breakout, funding, and (phase-2) revenue events to Slack or Discord as structured messages. No UX — pure outbound infrastructure.
How it works:
data/webhook-targets.jsonholds a list of operator-configured targets. The scan cron (/api/cron/webhooks/scan, every 30 min at :05 / :35) reads the latest derived repos + funding feed and enqueues a row per matching target into.data/webhook-queue.jsonl. Enqueue is idempotent — the same (event, subject, target) tuple never duplicates.- The flush cron (
/api/cron/webhooks/flush, every 30 min at :10 / :40) drains the queue. 5s timeout per POST, 3s gap between POSTs to avoid rate limits. Non-2xx responses bumpattempts; rows that hit 5 attempts move to.data/webhook-dead-letter.jsonlso the queue can drain forward. - The dead-letter digest cron (
/api/cron/webhooks/dead-letter-digest, daily at 09:09 UTC) reads the dead-letter JSONL file, aggregates recent rows by target/event, and posts a customer-ops Slack summary without exposing webhook URLs.
Add a Slack target:
- Create an Incoming Webhook at
https://api.slack.com/apps-> your app -> "Incoming Webhooks". Copy thehttps://hooks.slack.com/services/…URL. - Append to
data/webhook-targets.json:[ { "id": "devrel-slack", "provider": "slack", "url": "https://hooks.slack.com/services/T000/B000/XXXX", "events": ["breakout", "funding"], "filters": { "minMomentum": 80 }, "enabled": true } ] - Commit, or just place locally. The loader is mtime-cached — the next scan/flush tick picks it up automatically.
Add a Discord target:
- In Discord: Server Settings -> Integrations -> Webhooks -> New Webhook. Copy the
https://discord.com/api/webhooks/…URL. - Append to
data/webhook-targets.json:{ "id": "community-discord", "provider": "discord", "url": "https://discord.com/api/webhooks/123/abc", "events": ["funding"], "filters": { "minAmountUsd": 10000000 }, "enabled": true }
Filters:
minMomentum— breakouts only fire whenmomentumScore >= N.minAmountUsd— funding events only fire when the extracted amount meets the floor.languages— breakouts only fire for repos whose primary language matches (case-insensitive).
Operator notes:
- The URL is treated as a secret: it's never logged, never in error responses, never echoed by any API. Only the target
idappears in logs. - Only
https://*.slack.comand Discord-family hostnames are accepted — a misconfigured URL is silently dropped at load time, so the queue cannot be redirected at an internal host. WEBHOOK_TARGETS_PATHcan override the default path if you want to keep the config file outside the repo.- Smoke test (no targets configured yet returns a clean
ok: true, delivered: 0):curl -sS -X POST "http://localhost:3008/api/cron/webhooks/flush" \ -H "Authorization: Bearer $CRON_SECRET" \ -H "Content-Type: application/json" -d '{}'
Built by @0motionguy. Curated collections imported from pingcap/ossinsight under Apache 2.0 (see data/collections/NOTICE.md). Portal spec from visitportal.dev. MCP from modelcontextprotocol.io.
MIT — see LICENSE.