Skip to content

Instantly share code, notes, and snippets.

@teamcoltra
Last active November 23, 2025 02:23
Show Gist options
  • Select an option

  • Save teamcoltra/26f0008690c929038b992684c16501ef to your computer and use it in GitHub Desktop.

Select an option

Save teamcoltra/26f0008690c929038b992684c16501ef to your computer and use it in GitHub Desktop.
Get GeoGuessr Data - Firefox Extension

Get NCFA Token in FireFox

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;
  }
}

Get Data From Private Feed

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}

Further Reading:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment