Duolingo Gem Helper

Auto-extract JWT token and farm Duolingo gems

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                Duolingo Gem Helper
// @name:vi             Duolingo Gem Helper
// @namespace           https://github.com/yourusername/GemHelper
// @version             1.0.0
// @description         Auto-extract JWT token and farm Duolingo gems
// @description:vi      Tự động trích xuất JWT token và farm gems Duolingo.
// @author              GemHelper Team & @2pixel
// @match               https://*.duolingo.com/*
// @match               https://*.duolingo.cn/*
// @icon                https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg
// @run-at              document-end
// @grant               GM_xmlhttpRequest
// @grant               GM_addStyle
// @connect             duolingo.com
// @compatible          chrome   Tested on Chrome 120+ with Tampermonkey
// @compatible          firefox  Tested on Firefox 120+ with Tampermonkey / Violentmonkey
// @compatible          edge     Tested on Edge 120+ with Tampermonkey
// @license             MIT
// ==/UserScript==

(function () {
'use strict';

const _fontLink = document.createElement('link');
_fontLink.rel = 'stylesheet';
_fontLink.href = 'https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap';
document.head.appendChild(_fontLink);

GM_addStyle(`
:root {
    --gh-green: 88, 204, 2;
    --gh-blue: 28, 176, 246;
    --gh-red: 255, 77, 77;
    --gh-orange: 255, 159, 10;
    --gh-gray: 175, 175, 175;
}

@media (prefers-color-scheme: dark) {
    :root {
        --gh-green: 88, 204, 2;
        --gh-blue: 28, 176, 246;
    }
}

#GH_Root * {
    box-sizing: border-box;
    font-family: 'Nunito', sans-serif;
}

#GH_Main {
    position: fixed;
    bottom: 16px;
    right: 16px;
    z-index: 999999;
    width: 380px;
}

#GH_Box {
    background: rgba(255,255,255,0.95);
    border-radius: 16px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.15);
    overflow: hidden;
    backdrop-filter: blur(10px);
}

@media (prefers-color-scheme: dark) {
    #GH_Box {
        background: rgba(40,40,40,0.95);
        box-shadow: 0 8px 32px rgba(0,0,0,0.5);
    }
}

#GH_Header {
    background: linear-gradient(135deg, rgb(var(--gh-green)), rgb(88, 180, 2));
    padding: 16px 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: move;
    user-select: none;
}

#GH_Title {
    color: white;
    font-size: 18px;
    font-weight: 800;
    display: flex;
    align-items: center;
    gap: 8px;
}

#GH_Icon {
    width: 24px;
    height: 24px;
}

#GH_Close {
    background: rgba(255,255,255,0.2);
    border: none;
    color: white;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 18px;
    font-weight: bold;
    transition: all 0.2s;
}

#GH_Close:hover {
    background: rgba(255,255,255,0.3);
    transform: scale(1.1);
}

#GH_Content {
    padding: 20px;
}

.gh-section {
    margin-bottom: 20px;
}

.gh-section-title {
    font-size: 14px;
    font-weight: 700;
    color: #777;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 12px;
}

@media (prefers-color-scheme: dark) {
    .gh-section-title {
        color: #aaa;
    }
}

.gh-info-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin-bottom: 16px;
}

.gh-info-card {
    background: rgba(var(--gh-blue), 0.08);
    border-radius: 12px;
    padding: 12px;
    border: 2px solid rgba(var(--gh-blue), 0.15);
}

.gh-info-label {
    font-size: 11px;
    font-weight: 700;
    color: rgb(var(--gh-blue));
    text-transform: uppercase;
    letter-spacing: 0.3px;
    margin-bottom: 4px;
}

.gh-info-value {
    font-size: 20px;
    font-weight: 800;
    color: #333;
}

@media (prefers-color-scheme: dark) {
    .gh-info-value {
        color: #fff;
    }
}

.gh-button {
    width: 100%;
    padding: 14px;
    border: none;
    border-radius: 12px;
    font-size: 15px;
    font-weight: 800;
    cursor: pointer;
    transition: all 0.2s;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.gh-button:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}

.gh-button:active {
    transform: translateY(0);
}

.gh-button-primary {
    background: linear-gradient(135deg, rgb(var(--gh-green)), rgb(88, 180, 2));
    color: white;
}

.gh-button-danger {
    background: linear-gradient(135deg, rgb(var(--gh-red)), rgb(220, 60, 60));
    color: white;
}

.gh-button-secondary {
    background: linear-gradient(135deg, rgb(var(--gh-blue)), rgb(20, 150, 220));
    color: white;
}

.gh-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none !important;
}

.gh-log {
    background: rgba(0,0,0,0.03);
    border-radius: 10px;
    padding: 12px;
    max-height: 200px;
    overflow-y: auto;
    font-size: 12px;
    font-family: 'Courier New', monospace;
    margin-top: 12px;
}

@media (prefers-color-scheme: dark) {
    .gh-log {
        background: rgba(0,0,0,0.3);
    }
}

.gh-log-entry {
    padding: 4px 0;
    color: #333;
    word-wrap: break-word;
}

@media (prefers-color-scheme: dark) {
    .gh-log-entry {
        color: #ddd;
    }
}

.gh-log-success {
    color: rgb(var(--gh-green));
    font-weight: 700;
}

.gh-log-error {
    color: rgb(var(--gh-red));
    font-weight: 700;
}

.gh-log-info {
    color: rgb(var(--gh-blue));
    font-weight: 700;
}

.gh-status {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    background: rgba(var(--gh-gray), 0.1);
    border-radius: 10px;
    margin-bottom: 12px;
}

.gh-status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: rgb(var(--gh-gray));
}

.gh-status-dot.active {
    background: rgb(var(--gh-green));
    animation: pulse 2s infinite;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

.gh-status-text {
    font-size: 13px;
    font-weight: 600;
    color: #666;
}

@media (prefers-color-scheme: dark) {
    .gh-status-text {
        color: #aaa;
    }
}

.gh-minimize {
    background: rgba(255,255,255,0.2);
    border: none;
    color: white;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 18px;
    font-weight: bold;
    transition: all 0.2s;
    margin-right: 8px;
}

.gh-minimize:hover {
    background: rgba(255,255,255,0.3);
}

#GH_Toggle {
    position: fixed;
    bottom: 16px;
    right: 16px;
    z-index: 999998;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: linear-gradient(135deg, rgb(var(--gh-green)), rgb(88, 180, 2));
    border: none;
    cursor: pointer;
    box-shadow: 0 4px 16px rgba(0,0,0,0.2);
    display: none;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
}

#GH_Toggle:hover {
    transform: scale(1.1);
    box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}

#GH_Toggle svg {
    width: 28px;
    height: 28px;
    fill: white;
}
`);

let _token = null;
let _userId = null;
let _fromLang = null;
let _learningLang = null;
let _isRunning = false;
let _gems = 0;

const html = `
<div id="GH_Root">
    <button id="GH_Toggle">
        <img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" style="width: 32px; height: 32px;">
    </button>
    <div id="GH_Main">
        <div id="GH_Box">
            <div id="GH_Header">
                <div id="GH_Title">
                    <img id="GH_Icon" src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" style="width: 28px; height: 28px;">
                    GEM HELPER
                </div>
                <div style="display: flex; gap: 8px;">
                    <button id="GH_Minimize" class="gh-minimize">−</button>
                    <button id="GH_Close">×</button>
                </div>
            </div>
            <div id="GH_Content">
                <div class="gh-section">
                    <div class="gh-section-title">Account Info</div>
                    <div class="gh-info-grid">
                        <div class="gh-info-card">
                            <div class="gh-info-label">Current Gems</div>
                            <div class="gh-info-value" id="GH_Gems">---</div>
                        </div>
                        <div class="gh-info-card">
                            <div class="gh-info-label">User ID</div>
                            <div class="gh-info-value" id="GH_UserId" style="font-size: 12px; word-break: break-all;">---</div>
                        </div>
                    </div>
                    <div class="gh-status">
                        <div class="gh-status-dot" id="GH_StatusDot"></div>
                        <div class="gh-status-text" id="GH_StatusText">Disconnected</div>
                    </div>
                </div>

                <div class="gh-section">
                    <button class="gh-button gh-button-primary" id="GH_StartBtn">
                        Start Farming
                    </button>
                </div>

                <div class="gh-section">
                    <div class="gh-section-title">Log</div>
                    <div class="gh-log" id="GH_Log">
                        <div class="gh-log-entry">Ready to farm gems...</div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
`;

document.body.insertAdjacentHTML('beforeend', html);

const _main = document.getElementById('GH_Main');
const _toggle = document.getElementById('GH_Toggle');
const _log = document.getElementById('GH_Log');

function log(message, type = 'info') {
    const entry = document.createElement('div');
    entry.className = `gh-log-entry gh-log-${type}`;
    entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
    _log.appendChild(entry);
    _log.scrollTop = _log.scrollHeight;
}

function updateStatus(connected) {
    const dot = document.getElementById('GH_StatusDot');
    const text = document.getElementById('GH_StatusText');
    if (connected) {
        dot.classList.add('active');
        text.textContent = 'Connected';
    } else {
        dot.classList.remove('active');
        text.textContent = 'Disconnected';
    }
}

function extractToken() {
    try {
        const cookies = document.cookie.split(';');
        for (let cookie of cookies) {
            const [name, value] = cookie.trim().split('=');
            if (name === 'jwt_token') {
                return value;
            }
        }

        const authHeader = localStorage.getItem('authorization');
        if (authHeader && authHeader.startsWith('Bearer ')) {
            return authHeader.substring(7);
        }

        return null;
    } catch (e) {
        console.error('Token extraction error:', e);
        return null;
    }
}

async function fetchUserData() {
    if (!_token) {
        _token = extractToken();
        if (!_token) {
            log('Failed to extract JWT token', 'error');
            return false;
        }
    }

    try {
        const decoded = JSON.parse(atob(_token.split('.')[1]));
        _userId = decoded.sub;
        document.getElementById('GH_UserId').textContent = _userId;

        const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${_userId}?fields=fromLanguage,learningLanguage,currentCourseId`, {
            headers: {
                'authorization': `Bearer ${_token}`,
                'content-type': 'application/json'
            }
        });

        if (!response.ok) throw new Error('Failed to fetch user data');

        const userData = await response.json();
        _fromLang = userData.fromLanguage;
        _learningLang = userData.learningLanguage;

        log(`Detected: Learning ${_learningLang} from ${_fromLang}`, 'success');

        await updateGems();
        updateStatus(true);
        return true;
    } catch (e) {
        log(`Connection error: ${e.message}`, 'error');
        updateStatus(false);
        return false;
    }
}

async function updateGems() {
    try {
        const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_userId}?fields=gemsConfig`, {
            headers: {
                'authorization': `Bearer ${_token}`,
                'content-type': 'application/json'
            }
        });

        if (!response.ok) throw new Error('Failed to fetch gems');

        const data = await response.json();
        _gems = data.gemsConfig.gems;
        document.getElementById('GH_Gems').textContent = _gems.toLocaleString();
    } catch (e) {
        log(`Failed to update gems: ${e.message}`, 'error');
    }
}

async function fetchRewards() {
    try {
        const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_userId}?fields=rewardBundles`, {
            headers: {
                'authorization': `Bearer ${_token}`,
                'content-type': 'application/json'
            }
        });

        if (!response.ok) throw new Error('Failed to fetch rewards');

        const data = await response.json();
        const bundles = data.rewardBundles || [];
        const gemRewards = [];

        for (let bundle of bundles) {
            for (let reward of bundle.rewards || []) {
                if (!reward.consumed && (reward.id.includes('GEMS') || reward.currency === 'GEMS')) {
                    gemRewards.push({
                        id: reward.id,
                        amount: reward.amount || 0
                    });
                }
            }
        }

        return gemRewards;
    } catch (e) {
        log(`Failed to fetch rewards: ${e.message}`, 'error');
        return [];
    }
}

async function exploitReward(rewardId) {
    const body = {
        consumed: true,
        fromLanguage: _fromLang,
        learningLanguage: _learningLang,
        pathLevelSpecifics: {
            anchorSkillId: 'f22fd38157eea63965dc39eeac3c40c1',
            indexSinceAnchorSkill: 0,
            treeId: '14b1a2672c1bb3b250ebaa31b86c343e',
            nodeState: 'active'
        }
    };

    try {
        const response = await fetch(`https://www.duolingo.com/2023-05-23/users/${_userId}/rewards/${rewardId}`, {
            method: 'PATCH',
            headers: {
                'authorization': `Bearer ${_token}`,
                'content-type': 'application/json',
                'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
                'x-amzn-trace-id': `User=${_userId}`,
                'x-requested-with': 'XMLHttpRequest',
                'referer': 'https://www.duolingo.com/learn',
                'origin': 'https://www.duolingo.com'
            },
            body: JSON.stringify(body)
        });

        return response.ok;
    } catch (e) {
        return false;
    }
}

async function startFarming() {
    if (_isRunning) {
        _isRunning = false;
        document.getElementById('GH_StartBtn').textContent = 'Start Farming';
        document.getElementById('GH_StartBtn').className = 'gh-button gh-button-primary';
        log('Farming stopped', 'info');
        return;
    }

    if (!_token || !_userId) {
        const connected = await fetchUserData();
        if (!connected) return;
    }

    _isRunning = true;
    document.getElementById('GH_StartBtn').textContent = 'Stop Farming';
    document.getElementById('GH_StartBtn').className = 'gh-button gh-button-danger';

    log('Fetching available rewards...', 'info');
    const rewards = await fetchRewards();

    if (rewards.length === 0) {
        log('No rewards available to farm', 'error');
        _isRunning = false;
        document.getElementById('GH_StartBtn').textContent = 'Start Farming';
        document.getElementById('GH_StartBtn').className = 'gh-button gh-button-primary';
        return;
    }

    log(`Found ${rewards.length} gem rewards`, 'success');

    const threadCount = 1;
    const gemsBefore = _gems;
    let totalGained = 0;

    for (let reward of rewards) {
        if (!_isRunning) break;

        log(`Exploiting ${reward.id} (${reward.amount} base gems)...`, 'info');

        const startTime = Date.now();
        const promises = [];

        for (let i = 0; i < threadCount; i++) {
            promises.push(exploitReward(reward.id));
        }

        await Promise.all(promises);

        const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);

        await new Promise(resolve => setTimeout(resolve, 500));
        await updateGems();

        const gained = _gems - gemsBefore - totalGained;
        totalGained += gained;

        log(`✓ Completed in ${elapsed}s → +${gained} gems`, 'success');
    }

    log(`Total farmed: ${totalGained} gems`, 'success');
    log(`Final gems: ${_gems.toLocaleString()}`, 'success');

    _isRunning = false;
    document.getElementById('GH_StartBtn').textContent = 'Start Farming';
    document.getElementById('GH_StartBtn').className = 'gh-button gh-button-primary';
}

document.getElementById('GH_StartBtn').addEventListener('click', startFarming);

document.getElementById('GH_Close').addEventListener('click', () => {
    _main.style.display = 'none';
});

document.getElementById('GH_Minimize').addEventListener('click', () => {
    _main.style.display = 'none';
    _toggle.style.display = 'flex';
});

document.getElementById('GH_Toggle').addEventListener('click', () => {
    _main.style.display = 'block';
    _toggle.style.display = 'none';
});

let isDragging = false;
let currentX, currentY, initialX, initialY;

const header = document.getElementById('GH_Header');

header.addEventListener('mousedown', (e) => {
    if (e.target.tagName === 'BUTTON') return;
    isDragging = true;
    initialX = e.clientX - _main.offsetLeft;
    initialY = e.clientY - _main.offsetTop;
});

document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    e.preventDefault();
    currentX = e.clientX - initialX;
    currentY = e.clientY - initialY;
    _main.style.left = currentX + 'px';
    _main.style.top = currentY + 'px';
    _main.style.bottom = 'auto';
    _main.style.right = 'auto';
});

document.addEventListener('mouseup', () => {
    isDragging = false;
});

setTimeout(() => {
    fetchUserData();
}, 1000);

})();