Skip to content

Get notified when someone stars your GitHub repo

GitHub doesn't email you when someone stars your repo, and the marketplace apps that do want an OAuth grant. This template is one cron-triggered val that polls the GitHub API for your repo's stargazers every hour, remembers who it has already seen in blob storage, and emails you whenever someone new shows up. It uses GitHub's public keyless API, so there is nothing to sign up for — no tokens, no env vars.

View and run this example on Val Town

import { blob } from "https://esm.town/v/std/blob/main.ts";
import { email } from "https://esm.town/v/std/email";
import { REPO } from "./config.ts";
const SEEN_KEY = "seen-stargazers";
async function github(path: string) {
const res = await fetch(`https://api.github.com${path}`, {
headers: { Accept: "application/vnd.github+json" },
});
if (!res.ok) throw new Error(`GitHub ${path}: ${res.status} ${await res.text()}`);
return res.json();
}
export default async function () {
const repo = await github(`/repos/${REPO}`);
const count: number = repo.stargazers_count;
// Stargazers come back oldest-first, so the newest are on the last pages.
// Fetching the last two pages covers up to ~100 new stars between runs.
const lastPage = Math.max(1, Math.ceil(count / 100));
const pages = lastPage > 1 ? [lastPage - 1, lastPage] : [1];
const recent: string[] = [];
for (const page of pages) {
const stargazers = await github(`/repos/${REPO}/stargazers?per_page=100&page=${page}`);
recent.push(...stargazers.map((s: { login: string }) => s.login));
}
const seen: string[] | undefined = await blob.getJSON(SEEN_KEY);
if (seen === undefined) {
// First run: remember current stargazers without emailing about them.
await blob.setJSON(SEEN_KEY, recent);
console.log(`Seeded ${recent.length} existing stargazers of ${REPO} (${count} total)`);
return;
}
const seenSet = new Set(seen);
const newStars = recent.filter((login) => !seenSet.has(login));
if (newStars.length === 0) {
console.log(`No new stars on ${REPO} (${count} total)`);
return;
}
await email({
subject: `${newStars.length} new star${newStars.length === 1 ? "" : "s"} on ${REPO} (${count} total)`,
text: newStars.map((login) => `${login} — https://github.com/${login}`).join("\n"),
});
await blob.setJSON(SEEN_KEY, [...seen, ...newStars]);
console.log(`Emailed about new stargazers: ${newStars.join(", ")}`);
}

Stargazers come back from the API oldest-first, so the val checks the last two pages — enough to cover about 100 new stars between runs.

  1. Remix this template — click the Remix button in the top right corner.
  2. In config.ts, set REPO to your repo, e.g. "yourname/yourrepo".

The first run seeds the list of current stargazers without emailing; after that, each new star triggers an email to your Val Town account address.

  • Notifications go to your Val Town account email — that's where std/email delivers by default.
  • The keyless GitHub API allows 60 requests per hour per IP, and this val uses up to 3 per run — the hourly schedule keeps usage low. Don't crank the schedule much faster without adding a token.
  • The repo must be public; the keyless API can't see private repos.