@osrs-tracker/hiscores
v6.0.0
Published
Helpers for OSRS hiscores and the world list: a hiscore client, a player count from the world list, the mapping from Jagex's JSON and snapshot diffs
Readme
@osrs-tracker/hiscores ·

Helpers for Old School RuneScape hiscores in OSRS Tracker: a client for Jagex's
JSON hiscores, the mapping from Jagex's JSON to the domain model (HiscoreEntry in @osrs-tracker/models), and
comparing two hiscore snapshots, plus a client for Jagex's world list that counts the players online. Import the skill
and activity names (SkillEnum, ActivityEnum) and the XP helpers from @osrs-tracker/models.
A HiscoreEntry keys skills and activities by name. A value is { rank, level, xp } / { rank, score } with
rank: null when unranked (below Jagex's ranking cut-off) but with xp or a score, null when there's no xp or score,
and a missing key when the name wasn't on the hiscores. Overall is never null. Names Jagex adds come through before
SkillEnum or ActivityEnum know them: the enums are for looking up known names only.
Install
npm install @osrs-tracker/hiscores @osrs-tracker/modelsUsage
import { getOverallXpDiff, hiscoreDiff } from '@osrs-tracker/hiscores';
import { calculateXPForSkillLevel } from '@osrs-tracker/models';
// What changed between two snapshots (a `HiscoreDiff`): keyed over both entries' names, every value a number; `null` or
// missing counts as 0, except a skill's level, which counts as 1
const gains = hiscoreDiff(todayEntry, lastWeekEntry);
const overallXpGained = getOverallXpDiff(todayEntry, lastWeekEntry);
calculateXPForSkillLevel(99); // 13034431Fetching a hiscore (Node 18+ or a browser; pass fetch to use your own client). getHiscore never throws: every
outcome is a result.
import { getHiscore } from '@osrs-tracker/hiscores';
const result = await getHiscore({ baseUrl: 'https://secure.runescape.com', username: 'Lynx Titan' });
if (result.status === 'found') {
const entry = { date: new Date(), scrapingOffset: 0, ...result.hiscore }; // a HiscoreEntry
console.log(entry.skills.Overall, result.layout.skills); // layout: the names in Jagex's order
} else if (result.status === 'notFound')
console.log('Not on the hiscores'); // HTTP 404/400, don't retry
else console.log(`Failed: ${result.reason}`); // worth retryingOptions: table (default 'hiscore_oldschool'; 'hiscore_oldschool_ironman', 'hiscore_oldschool_ultimate' or
'hiscore_oldschool_hardcore_ironman' for the ironman tables), fetch (default the global fetch) and timeoutMs
(default 10 seconds, after which the result is failed).
Already have Jagex's index_lite.json (typed JagexHiscoreJson), e.g. from a proxy? Map it the same way:
import { fromJagex, JagexHiscoreJson } from '@osrs-tracker/hiscores';
const { skills, activities, layout } = fromJagex(json as JagexHiscoreJson);Counting the players online from Jagex's world list (Node 18+; www.runescape.com sends no CORS headers, so not from a
browser). fetchWorldList throws on a non-2xx response, a network error or the timeout, and parseWorldList throws
when the response doesn't match the expected format. Don't poll it faster than once a minute.
import { fetchWorldList, parseWorldList } from '@osrs-tracker/hiscores';
const counts = parseWorldList(await fetchWorldList({ userAgent: 'github:me/my-app' }));
const playerCount = { date: new Date(), ...counts }; // a PlayerCount: total, members, f2p, worlds, byLocationOptions: fetch (default the global fetch), timeoutMs (default 10 seconds) and userAgent (not sent by default).
Development
OSRS Tracker was originally built entirely without AI assistance. Since October 2026, I've started using Claude, Anthropic's AI coding assistant, to help improve development speed and reliability.
