Score a fantasy MotoGP league
Turn race results into fantasy points with your own scoring rules, and rank your league's teams after every round.
10 min read
Fantasy games are one of the best uses of race data: players pick riders, and every result moves the league table. This guide scores a league in JavaScript with rules you control.
The rules
Each fantasy team picks three riders. After every round, each rider earns:
| Event | Fantasy points |
|---|---|
| Championship points scored in the race | 1 per point |
| Win | +10 bonus |
| Podium (P1–P3) | +5 bonus |
| Did not finish | −5 |
Keep rules as data, not code, so you can tune them between seasons.
// scoring.js
export const RULES = Object.freeze({
perPoint: 1,
win: 10,
podium: 5,
dnf: -5,
});
/** Fantasy points for one rider's race result. Pure: same input, same output. */
export function scoreResult(result, rules = RULES) {
let score = result.points * rules.perPoint;
if (result.status === "FINISHED") {
if (result.position === 1) score += rules.win;
if (result.position <= 3) score += rules.podium;
} else if (result.status === "DNF") {
score += rules.dnf;
}
return score;
}
Score a round
GET /races/seasons/{year}/round/{round}/results returns one row per rider with position,
points, status, rider_name and a stable rider_id. Index the rows by rider and sum each
team’s picks:
// round.js
import { scoreResult } from "./scoring.js";
const HOST = "motogp-api-championship-data-statistics.p.rapidapi.com";
const headers = { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": HOST };
async function get(path) {
const res = await fetch(`https://${HOST}${path}`, { headers });
if (!res.ok) throw new Error(`${path}: ${res.status}`);
return (await res.json()).data;
}
export async function scoreRound(year, round, teams) {
const results = await get(`/races/seasons/${year}/round/${round}/results?category=MOTOGP`);
const byRider = new Map(results.map((r) => [r.rider_name, r]));
return teams
.map((team) => ({
name: team.name,
points: team.riders.reduce((sum, rider) => {
const result = byRider.get(rider);
return sum + (result ? scoreResult(result) : 0); // didn't start: 0
}, 0),
}))
.sort((a, b) => b.points - a.points);
}
const league = [
{ name: "Late Brakers", riders: ["Marco Bezzecchi", "Raúl Fernández", "Fabio Di Giannantonio"] },
{ name: "Apex Hunters", riders: ["Marc Márquez", "Jorge Martín", "Pedro Acosta"] },
];
console.table(await scoreRound(2026, 1, league));
This example matches picks by name for readability. In production, store each pick’s rider_id
from GET /riders/seasons/{year} when the player builds their team, and index results by
rider_id.
Season totals
Score every finished round and add the results up. A round that has not been run yet returns an empty list and scores 0 for everyone:
export async function seasonTable(year, teams, lastRound) {
const totals = new Map(teams.map((t) => [t.name, 0]));
for (let round = 1; round <= lastRound; round++) {
for (const { name, points } of await scoreRound(year, round, teams)) {
totals.set(name, totals.get(name) + points);
}
}
return [...totals].map(([name, points]) => ({ name, points })).sort((a, b) => b.points - a.points);
}
Store each round’s scores in your database once results are in. Scores never change after a round is final, so you only call the API for the newest round.
Pick lists and pricing
- Who can be picked:
GET /riders/seasons/{year}?category=MOTOGPlists the season’s grid. - Rider prices: derive them from the current championship —
GET /standings/seasons/{year}/riders— so leaders cost more than backmarkers.