Auto-extract JWT token and farm Duolingo gems
// ==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);
})();