I had to guess at how to do this because it's slightly different than Chrome but here's what I found:
async function getNcfaCookie() {
try {
const cookie = await browser.cookies.get({
url: "https://www.geoguessr.com/",
name: "_ncfa",
});
return cookie ? cookie.value : null;
} catch (e) {
console.error("Failed to read _ncfa cookie:", e);
return null;
}
}
This grabs all the games that you've played (I think it's limited to the last 250 but don't remember). This will do pagination by default but you can update it to just get your latest games without it.
/**
* Fetch *all* pages of GeoGuessr private feed.
* Fully paginated, matching the Go implementation.
*/
async function fetchGeoFeed(ncfaToken) {
const baseUrl = "https://www.geoguessr.com/api/v4/feed/private";
let paginationToken = "";
let allEntries = [];
while (true) {
let url = baseUrl;
if (paginationToken) {
url += `?paginationToken=${encodeURIComponent(paginationToken)}`;
}
const res = await fetch(url, {
headers: {
"Cookie": `_ncfa=${ncfaToken}`
}
});
if (!res.ok) {
throw new Error("Failed to fetch feed page: " + res.status);
}
const data = await res.json();
// Combine entries
if (data.entries && Array.isArray(data.entries)) {
allEntries.push(...data.entries);
}
// Stop when no paginationToken returned
if (!data.paginationToken) break;
paginationToken = data.paginationToken;
}
return extractGamesFromFeed(allEntries);
}
/**
* Detect movement mode based on movementOptions
*/
function detectMovement(options) {
if (!options) return null;
const { forbidMoving, forbidZooming, forbidRotating } = options;
if (forbidMoving && forbidZooming && forbidRotating) return "NMPZ";
if (forbidMoving) return "NoMove";
return "Moving";
}
/**
* Turn all feed entries into structured lists
*/
function extractGamesFromFeed(entries) {
const singlePlayer = [];
const duels = [];
const teamDuels = [];
for (const entry of entries) {
if (!entry.payload) continue;
let payload;
try {
payload = JSON.parse(entry.payload);
} catch {
continue;
}
for (const p of payload) {
const t = p.type;
const game = p.payload || {};
// -------------------- Single-player games --------------------
if (t === 2 || t === 1) {
singlePlayer.push({
type: "singleplayer",
gameMode: game.gameMode,
map: game.mapSlug || game.mapName,
gameToken: game.gameToken || game.challengeToken || null,
score: game.points,
time: p.time,
movement: null
});
}
// -------------------- Duels / Team Duels --------------------
if (t === 6 || t === 11) {
const id = game.gameId;
const movement = detectMovement(
game.movementOptions ||
game.options?.movementOptions
);
const info = {
type: t === 6 ? "duel-or-team" : "battle",
gameId: id,
gameMode: game.gameMode,
competitiveMode: game.competitiveGameMode,
time: p.time,
movement
};
if (game.gameMode === "TeamDuels") {
teamDuels.push(info);
} else {
duels.push(info);
}
}
}
}
return { singlePlayer, duels, teamDuels };
}
// -------------------- Example usage --------------------
(async () => {
const ncfa = "YOUR_COOKIE_HERE";
const result = await fetchGeoFeed(ncfa);
console.log("Single Player Games:", result.singlePlayer.length);
console.log("Duels:", result.duels.length);
console.log("Team Duels:", result.teamDuels.length);
})();
Output:
{
"singlePlayer": [
{
"type": "singleplayer",
"gameMode": "Standard",
"map": "world",
"gameToken": "4VaXMxWSrIhGepEe",
"score": 18066,
"time": "2025-10-10T14:17:47.924Z",
"movement": null
}
],
"duels": [
{
"type": "duel-or-team",
"gameId": "95c9331f-36b0-4fbe-a604-b7640566ed7c",
"gameMode": "Duels",
"competitiveMode": "StandardDuels",
"time": "2025-10-10T14:25:42.508Z",
"movement": "NMPZ"
}
],
"teamDuels": [
{
"type": "duel-or-team",
"gameId": "690a009860022c0e4216016d",
"gameMode": "TeamDuels",
"competitiveMode": "StandardDuels",
"time": "2025-11-04T13:35:16.493Z",
"movement": "Moving"
}
]
}
Get detailed Duels information:
/**
* Fetches and processes a full GeoGuessr duel
* from the game-server API endpoint.
*
* Returns a fully normalized object:
* {
* gameId, status, competitiveMode, movement,
* map: { name, slug, bounds },
* teams: [...],
* rounds: [...],
* result: {...}
* }
*/
async function getDuel(gameId, ncfaToken) {
const url = `https://game-server.geoguessr.com/api/duels/${gameId}`;
// ---- Fetch duel data ----
const res = await fetch(url, {
method: "GET",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Cookie": `_ncfa=${ncfaToken}`,
"X-Client": "web"
}
});
if (!res.ok) {
throw new Error(`Failed to load duel ${gameId}: HTTP ${res.status}`);
}
const raw = await res.json();
// ---- Determine movement mode ----
let movement = "Moving";
if (raw.movementOptions) {
const m = raw.movementOptions;
if (m.forbidMoving && m.forbidZooming && m.forbidRotating) movement = "NMPZ";
else if (m.forbidMoving) movement = "NoMove";
}
// ---- Normalize/Process ----
return {
gameId: raw.gameId,
status: raw.status,
competitiveMode: raw.options?.competitiveGameMode || null,
isTeamDuels: raw.options?.isTeamDuels || false,
movement,
map: {
name: raw.options?.map?.name || null,
slug: raw.options?.map?.slug || null,
bounds: raw.mapBounds || null
},
teams: raw.teams.map(t => ({
id: t.id,
name: t.name,
health: t.health,
players: t.players.map(p => ({
playerId: p.playerId,
rating: p.rating,
countryCode: p.countryCode,
guessPin: p.pin || null
})),
roundResults: t.roundResults || []
})),
rounds: raw.rounds.map(r => ({
roundNumber: r.roundNumber,
panorama: r.panorama,
multiplier: r.multiplier,
damageMultiplier: r.damageMultiplier,
isHealingRound: r.isHealingRound || false,
hasProcessedRoundTimeout: r.hasProcessedRoundTimeout || false
})),
result: raw.result || null
};
}
Single Player games live on this endpoint: https://www.geoguessr.com/api/v3/results/${gameToken}
Also check this out: https://github.com/miraclewhips/geoguessr-event-framework?tab=readme-ov-file
Pull the framework into your javascript in FireFox and you can fire off events based on game stuff.