Your year of code, as a full-screen, click-through recap — Spotify Wrapped, but for your commits.
It runs entirely on your machine. You give it a read-only GitHub token, it pulls your own activity, computes the stats locally, and opens an animated slide deck in your browser. The last slide is a 1200×675 card you can download or copy straight into a post.
Thirteen slides, auto-advancing like Instagram Stories, each one stat:
| Slide | What it shows |
|---|---|
| Intro | Your avatar, name, and how many days you showed up |
| Contributions | The big number, counting up |
| Heatmap | Your whole year as a glowing contribution grid |
| Streak | Longest consecutive run, with dates |
| Clock | Hour-by-hour histogram and your coding persona (Night Owl, Early Bird, …) |
| Weekday | Your favourite day to build |
| Languages | Top 3 by bytes across everything you touched |
| Pull requests | Opened vs merged, with an animated merge-rate ring |
| Repos | How many you committed to, and the most-starred one |
| Busiest day | Your single heaviest day of the year |
| Commit | The funniest commit message we could find in your history |
| Percentile | A playful, clearly-labelled estimate of where you land |
| Share | The exportable summary card |
Slides with nothing to say are skipped automatically — no empty states.
git clone <your-fork-url> github-wrapped
cd github-wrapped
npm installRequires Node 18 or newer.
Go to github.com/settings/personal-access-tokens/new and create a fine-grained token:
- Repository access: Public repositories (read-only)
- Expiration: whatever you like — a week is plenty
- No extra permissions needed
That's enough for every stat except private-repo detail.
Want your private contributions included too?
Fine-grained tokens can't read the aggregate private-contribution data the profile calendar uses. For that you need a classic token with the repo scope, and you need Settings → Profile → "Include private contributions on my profile" turned on.
Without it, private work still shows up in your totals (GitHub reports the count), it just can't be broken down by repo or language.
cp .env.example .envOpen .env and paste the token in:
GITHUB_TOKEN=github_pat_....env is gitignored. The token is read only by the local fetch script and is sent to api.github.com and nowhere else — it never reaches the browser, and it's never written into the generated data file.
npm run startThat fetches your data, writes public/data/wrapped.json, and opens the app at http://localhost:5959.
npm run start # fetch + open the app
npm run fetch # just refresh the data
npm run fetch -- --year 2024 # a different year
npm run fetch -- --user someone # someone else's public year
npm run dev # open the app with existing data
npm run demo # generate fake data to preview the UInpm run demo needs no token at all — handy for seeing the whole experience before you commit to setting one up.
| Key | Action |
|---|---|
→ ↓ j / click right |
Next slide |
← ↑ k / click left |
Previous slide |
Space |
Pause / resume autoplay |
Home / End |
First / last slide |
m |
Toggle slide sounds (off by default) |
Swipe works on touch devices, and holding your finger (or mouse button) down pauses autoplay, like Stories.
On the final slide:
- Download image saves a 2400×1350 PNG (2× of 1200×675, sized for X)
- Copy to clipboard puts the image straight on your clipboard
- Share on X opens a pre-filled post — attach the downloaded image to it
If you post yours, we'd love to see it. The card carries a small github-wrapped credit so other people can find their way here.
All optional, all in .env:
| Variable | Default | What it does |
|---|---|---|
GITHUB_TOKEN |
— | Required. Your personal access token |
GITHUB_USERNAME |
token owner | Whose year to wrap |
WRAPPED_YEAR |
current year | Which year to wrap |
WRAPPED_TIMEZONE |
your system zone | Used to bucket commits into hours and weekdays |
About the timezone: GitHub stamps each commit with the offset it was authored in, so "you're a night owl" is based on your local clock at the time, not UTC. WRAPPED_TIMEZONE is the fallback for commits stamped in UTC — CI jobs and edits made through the GitHub web UI.
scripts/fetch.ts CLI: reads .env, runs the pipeline, writes the JSON
src/api/ GraphQL + REST fetching (the only code that sees your token)
client.ts auth, retries, rate-limit backoff
graphql.ts contribution calendar, commit history, PR search
rest.ts token owner, rate limits, language fallback
errors.ts every failure, translated into something readable
src/stats/ Pure functions: raw API data in, finished stats out
streak.ts longest/current streaks, busiest day, monthly totals
rhythm.ts hour + weekday distributions, persona
languages.ts bytes by language across repos you touched
pullRequests.ts merge rate, merge times
commits.ts the funny-commit heuristic
percentile.ts the illustrative comparison stat
src/components/ React: one component per slide, plus shared primitives
src/app/ App shell, slide deck definition, navigation, autoplay
The fetch step and the UI are fully decoupled: the script writes a JSON file, the app reads it. Nothing in the browser knows your token exists.
Everything in src/stats/ is a pure function, so the numbers are reproducible and easy to check — npm run demo runs the exact same code over synthetic data.
The "you're ahead of X% of GitHub" number is an illustrative estimate, not a measurement. GitHub doesn't publish per-user contribution distributions, so it interpolates your total against a hand-set curve that reflects how long-tailed contribution counts are. The reference curve is right there in src/stats/percentile.ts if you want to argue with it.
It's labelled as an estimate on the slide itself. Please don't put it in a performance review.
"GitHub rejected your token (401)" — the token is wrong, truncated, or expired. Regenerate it and make sure the whole thing is on one line in .env with no quotes.
"You've hit GitHub's rate limit" — very active accounts need a lot of requests. Wait for the reset time the message prints and run it again. You can also lower the ceiling: npm run fetch -- --max-commits 800.
"A quiet year" — we found almost no public activity. Either your work lives in private repos (see the token note above) or you're looking at the wrong year.
Timing stats look off — check WRAPPED_TIMEZONE, and remember that commits made through the GitHub web UI or by CI are stamped UTC.
The fetch is slow — the commit-history walk is the expensive part, and it's paged per repo. A big year takes a minute or two. It stops at 2500 commits by default; totals stay complete either way, only the timing and commit-message stats are sampled.
| Intro | Heatmap | Share card |
|---|---|---|
docs/intro.png |
docs/heatmap.png |
docs/share.png |
To capture your own: run npm run demo, open the app, and screenshot the slides you like.
The app itself can't be usefully hosted — the fetch step is a local Node script, so a deployed build has no way to get anyone's data. What you can deploy is the demo, which gives you a link that shows the full experience without a token.
vercel.json is already set up for this:
{ "buildCommand": "npm run demo && npm run build", "outputDirectory": "dist" }The npm run demo step matters: public/data/wrapped.json is gitignored, so a build from
a fresh clone would otherwise ship a site permanently stuck on the "No data yet" screen.
Generating demo data at build time is what makes the deploy work.
On Netlify the equivalent is:
[build]
command = "npm run demo && npm run build"
publish = "dist"The deployed demo shows The Octocat's synthetic year — none of your own data is involved.
The Open Graph tags and public/og.png are already wired up, so the link unfurls with the
share card when you post it.
- Your token lives in
.env, which is gitignored, and is only ever read byscripts/fetch.ts. - The only network calls are to
api.github.com(data) and Google Fonts (typefaces). public/data/wrapped.jsonis gitignored — your stats stay on your disk.- Nothing is uploaded, logged, or phoned home. There is no server.
MIT. Fork it, restyle it, add your own slides.
