Created
March 1, 2026 04:33
-
-
Save m4chinations/f6d58711a94077d96cf4157665b0bab3 to your computer and use it in GitHub Desktop.
hackernews account age and karma userscript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name HN User Info (Age & Karma) | |
| // @namespace https://news.ycombinator.com/ | |
| // @version 1.0 | |
| // @description Shows account age (in days) and karma next to every username on Hacker News | |
| // @match https://news.ycombinator.com/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM.xmlHttpRequest | |
| // @connect hacker-news.firebaseio.com | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ── CONFIG ───────────────────────────────────────────────────────── | |
| // Toggle which fields to show | |
| const SHOW_AGE = true; | |
| const SHOW_KARMA = true; | |
| // Suffix strings (e.g. "d" → "5,055d", " days" → "5,055 days") | |
| const AGE_SUFFIX = 'd'; | |
| const KARMA_SUFFIX = 'k'; | |
| // ────────────────────────────────────────────────────────────────── | |
| // Cache fetched users so we don't re-fetch duplicates on the same page | |
| const userCache = {}; | |
| // Calculate age in days from a Unix timestamp | |
| function ageDays(createdUnix) { | |
| const now = Date.now() / 1000; | |
| return Math.floor((now - createdUnix) / 86400); | |
| } | |
| // Create the badge span that will be inserted after the username | |
| function makeBadge(username) { | |
| const span = document.createElement('span'); | |
| span.className = 'hn-userinfo-badge'; | |
| span.style.cssText = 'color:#828282; font-size:inherit; opacity:0.5;'; | |
| span.textContent = ' …'; | |
| span.dataset.user = username; | |
| return span; | |
| } | |
| // Format a number with commas (e.g. 13394 → "13,394") | |
| function fmt(n) { | |
| return n.toLocaleString('en-US'); | |
| } | |
| // Update every badge for a given username with the fetched data | |
| function fillBadges(username, data) { | |
| const days = ageDays(data.created); | |
| const karma = data.karma; | |
| const createdDate = new Date(data.created * 1000); | |
| const dateStr = createdDate.toLocaleDateString('en-US', { | |
| year: 'numeric', month: 'long', day: 'numeric' | |
| }); | |
| document.querySelectorAll(`.hn-userinfo-badge[data-user="${CSS.escape(username)}"]`).forEach(badge => { | |
| badge.textContent = ''; | |
| badge.style.opacity = '1'; | |
| badge.style.transition = 'opacity 0.3s'; | |
| if (SHOW_AGE) { | |
| const ageLabel = `${fmt(days)}${AGE_SUFFIX}`; | |
| const ageSpan = document.createElement('span'); | |
| ageSpan.textContent = ` | ${ageLabel}`; | |
| ageSpan.title = dateStr; | |
| ageSpan.style.cssText = 'cursor:help; border-bottom:1px dotted #828282;'; | |
| badge.appendChild(ageSpan); | |
| } | |
| if (SHOW_KARMA) { | |
| const karmaLabel = `${fmt(karma)}${KARMA_SUFFIX}`; | |
| badge.appendChild(document.createTextNode(` | ${karmaLabel}`)); | |
| } | |
| if (SHOW_AGE || SHOW_KARMA) { | |
| badge.appendChild(document.createTextNode(' |')); | |
| } | |
| }); | |
| } | |
| // Fetch user info from the public HN Firebase API | |
| function fetchUser(username) { | |
| if (userCache[username]) return; // already fetched or in-flight | |
| userCache[username] = true; // mark in-flight | |
| const url = `https://hacker-news.firebaseio.com/v0/user/${encodeURIComponent(username)}.json`; | |
| // Use GM_xmlhttpRequest if available (Greasemonkey / Tampermonkey), | |
| // otherwise fall back to plain fetch (Violentmonkey, etc.) | |
| const gmXHR = typeof GM_xmlhttpRequest === 'function' | |
| ? GM_xmlhttpRequest | |
| : (typeof GM !== 'undefined' && GM.xmlHttpRequest) | |
| ? GM.xmlHttpRequest | |
| : null; | |
| if (gmXHR) { | |
| gmXHR({ | |
| method: 'GET', | |
| url: url, | |
| onload: function (resp) { | |
| try { | |
| const data = JSON.parse(resp.responseText); | |
| if (data && data.created != null) { | |
| userCache[username] = data; | |
| fillBadges(username, data); | |
| } | |
| } catch (_) { /* silently ignore parse errors */ } | |
| } | |
| }); | |
| } else { | |
| fetch(url) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (data && data.created != null) { | |
| userCache[username] = data; | |
| fillBadges(username, data); | |
| } | |
| }) | |
| .catch(() => {}); | |
| } | |
| } | |
| // Main: find all .hnuser links, inject badges, kick off fetches | |
| function run() { | |
| const userLinks = document.querySelectorAll('a.hnuser'); | |
| const seen = new Set(); | |
| userLinks.forEach(link => { | |
| // Skip if we already processed this exact element | |
| if (link.dataset.hnInfoDone) return; | |
| link.dataset.hnInfoDone = '1'; | |
| // Extract username from href like "user?id=fulafel" | |
| const match = link.getAttribute('href')?.match(/user\?id=([^&]+)/); | |
| if (!match) return; | |
| const username = match[1]; | |
| // Insert a badge span right after the username link | |
| const badge = makeBadge(username); | |
| link.after(badge); | |
| // Queue a fetch (deduped) | |
| if (!seen.has(username)) { | |
| seen.add(username); | |
| fetchUser(username); | |
| } | |
| }); | |
| } | |
| // Run on page load | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', run); | |
| } else { | |
| run(); | |
| } | |
| })(); |
Author
m4chinations
commented
Mar 1, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment