Fakes API to give infinite gems with a GUI (you might get banned)
// ==UserScript==
// @name Duolingo Unlimited Gems
// @icon https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg
// @namespace https://tampermonkey.net/
// @version 2.0
// @description Fakes API to give infinite gems with a GUI (you might get banned)
// @author apersongithub & merhametsize
// @match *://*.duolingo.com/*
// @match *://*.duolingo.cn/*
// @grant none
// @license MPL-2.0
// @source https://github.com/merhametsize/duo-gemsmith
// ==/UserScript==
(async function () {
'use strict';
const PAYLOAD = {
consumed: true,
fromLanguage: "en",
learningLanguage: "fr"
};
let isRunning = false;
let shouldStop = false;
let lastKnownGems = null; // track previous gem count for animation
const SETTINGS_COOKIE = 'duo_gemsmith_settings_v2';
function getParentCookieDomain() {
const host = location.hostname; // e.g. www.duolingo.com or preview.duolingo.cn
const parts = host.split('.');
if (parts.length < 2) return host;
const base = parts.slice(-2).join('.');
return '.' + base;
}
function readSettingsFromCookie() {
const cookies = document.cookie.split('; ');
for (const c of cookies) {
const [name, value] = c.split('=');
if (name === SETTINGS_COOKIE) {
try {
const decoded = decodeURIComponent(value);
const parsed = JSON.parse(decoded);
return parsed && typeof parsed === 'object' ? parsed : null;
} catch (e) {
console.warn('Failed to parse settings cookie:', e);
return null;
}
}
}
return null;
}
function writeSettingsToCookie(settings) {
try {
const domain = getParentCookieDomain();
const days = 365;
const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
const value = encodeURIComponent(JSON.stringify(settings));
document.cookie =
`${SETTINGS_COOKIE}=${value};` +
`expires=${expires};` +
`path=/;domain=${domain};SameSite=Lax`;
} catch (e) {
console.warn('Failed to write settings cookie:', e);
}
}
function defaultSettings() {
return {
iterations: 0,
speedMode: 'medium', // 'extreme', 'fast', 'medium', 'slow', 'custom'
customInterval: 1.5,
position: null, // { top, left }
totalGemsEver: 0
};
}
function loadSettings() {
const defaults = defaultSettings();
const fromCookie = readSettingsFromCookie();
if (!fromCookie) return defaults;
return {
iterations: typeof fromCookie.iterations === 'number' ? fromCookie.iterations : defaults.iterations,
speedMode: typeof fromCookie.speedMode === 'string' ? fromCookie.speedMode : defaults.speedMode,
customInterval: typeof fromCookie.customInterval === 'number' ? fromCookie.customInterval : defaults.customInterval,
position: fromCookie.position &&
typeof fromCookie.position.top === 'number' &&
typeof fromCookie.position.left === 'number'
? { top: fromCookie.position.top, left: fromCookie.position.left }
: null,
totalGemsEver: typeof fromCookie.totalGemsEver === 'number' ? fromCookie.totalGemsEver : defaults.totalGemsEver
};
}
const settings = loadSettings();
function saveSettings() {
writeSettingsToCookie(settings);
}
function getCurrentTime() {
return new Date().toLocaleTimeString('en-US', { hour12: false });
}
function getApiEnv() {
const host = window.location.host || '';
const isPreview = host.includes('preview.duolingo.com') || host.includes('preview.duolingo.cn');
if (isPreview) {
return {
env: 'preview',
usersBases: [
'https://preview.duolingo.com/2023-05-23/users', // primary
'https://www.duolingo.com/2017-06-30/users', // fallback 1
'https://www.duolingo.com/2023-05-23/users' // fallback 2
]
};
} else {
return {
env: 'normal',
usersBases: [
'https://www.duolingo.com/2017-06-30/users', // primary
'https://preview.duolingo.com/2023-05-23/users', // fallback 1
'https://www.duolingo.com/2023-05-23/users' // fallback 2
]
};
}
}
function addLog(logElement, message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${getCurrentTime()}] ${message}`;
logElement.appendChild(entry);
logElement.scrollTop = logElement.scrollHeight;
}
async function sendPatchRequest(url, data) {
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
return { status: response.status, ok: response.ok };
}
function getJWTFromCookies() {
const cookies = document.cookie.split('; ');
for (let cookie of cookies) {
const [name, value] = cookie.split('=');
if (name === 'jwt_token') {
return value;
}
}
return null;
}
function decodeToken(token) {
try {
if (!token || !token.includes('.')) {
throw new Error('Invalid JWT token');
}
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Malformed JWT. Expected 3 parts.');
}
const base64Userid = parts[1];
const padded = base64Userid + '='.repeat((4 - (base64Userid.length % 4)) % 4);
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
const payload = JSON.parse(decoded);
return { userid: payload.sub, base64Userid };
} catch (err) {
console.error(`Error decoding token: ${err.message}`);
throw err;
}
}
// Fetch user data with fallback chain; only log "using api" if primary fails
async function fetchUserDataWithFallback(userid, logEl) {
const { usersBases, env } = getApiEnv();
let lastError = null;
for (let i = 0; i < usersBases.length; i++) {
const base = usersBases[i];
const label =
i === 0 ? 'primary' :
i === 1 ? 'fallback 1' :
'fallback 2';
try {
const url = `${base}/${userid}?fields=trackingProperties,gems`;
const response = await fetch(url);
if (!response.ok) {
lastError = new Error(`Status ${response.status}`);
// Only log fallbacks (not primary success)
if (i === 0) {
addLog(logEl, `Primary user API failed (status ${response.status}), trying fallbacks...`, 'error');
} else {
addLog(logEl, `User API ${label} failed (status ${response.status})`, 'error');
}
continue;
}
const data = await response.json();
const username = data.trackingProperties?.username || 'Unknown';
const gems = data.gems || 0;
if (i === 0) {
// Primary worked silently (no "using api" log per request)
} else {
addLog(logEl, `Using user API ${label} endpoint: ${base}/users`, 'info');
}
return { username, gems, base };
} catch (err) {
lastError = err;
if (i === 0) {
addLog(logEl, `Primary user API error (${err.message}), trying fallbacks...`, 'error');
} else {
addLog(logEl, `User API ${label} error: ${err.message}`, 'error');
}
}
}
throw lastError || new Error('All user API endpoints failed');
}
// Build reward endpoint from a chosen base /users
function buildRewardUrlFromUsersBase(usersBase, userid, reward) {
// usersBase ends with /users, we want /users/{userid}/rewards/{reward}
return `${usersBase}/${userid}/rewards/${reward}`;
}
// Interruptible sleep that checks shouldStop every 100ms
async function interruptibleSleep(totalSeconds, shouldStopRef) {
const stepMs = 100;
const totalMs = totalSeconds * 1000;
let elapsed = 0;
while (elapsed < totalMs) {
if (shouldStopRef()) return; // exit early if stop requested
const remaining = totalMs - elapsed;
const wait = Math.min(stepMs, remaining);
await new Promise(r => setTimeout(r, wait));
elapsed += wait;
}
}
function makeDraggable(panel, handle, onStopDragging) {
let isDragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
function onMouseDown(e) {
if (e.button !== 0) return;
const tag = e.target.tagName.toLowerCase();
if (tag === 'button' || tag === 'input' || tag === 'select' || tag === 'textarea' || e.target.closest('button')) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.left = `${startLeft}px`;
panel.style.top = `${startTop}px`;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
}
function onMouseMove(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
const vw = window.innerWidth;
const vh = window.innerHeight;
const rect = panel.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const margin = 8;
newLeft = Math.max(margin - width + rect.width, Math.min(vw - margin - width + rect.width, newLeft));
newTop = Math.max(margin, Math.min(vh - margin - height, newTop));
panel.style.left = `${newLeft}px`;
panel.style.top = `${newTop}px`;
}
function onMouseUp() {
if (!isDragging) return;
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (typeof onStopDragging === 'function') {
const rect = panel.getBoundingClientRect();
onStopDragging({
top: rect.top,
left: rect.left
});
}
}
handle.addEventListener('mousedown', onMouseDown);
}
// animate lingot-stat elements when gem count increases
function animateLingotStats(oldValue, newValue) {
if (oldValue == null) return;
const oldNum = Number(oldValue);
const newNum = Number(newValue);
if (!Number.isFinite(oldNum) || !Number.isFinite(newNum)) return;
if (newNum <= oldNum) return;
const nodes = document.querySelectorAll('span[data-test="lingot-stat"]');
nodes.forEach(node => {
node.classList.remove('gs-lingot-animate');
void node.offsetWidth;
node.classList.add('gs-lingot-animate');
const onEnd = () => {
node.classList.remove('gs-lingot-animate');
node.removeEventListener('animationend', onEnd);
};
node.addEventListener('animationend', onEnd);
});
}
// Update both our UI and page lingot-stat elements, with animation on increase
function updateGemDisplays(newGemCount, elements) {
const prev = lastKnownGems;
lastKnownGems = newGemCount;
if (elements?.currentGemsValue) {
elements.currentGemsValue.textContent = newGemCount;
}
try {
const lingotNodes = document.querySelectorAll('span[data-test="lingot-stat"]');
lingotNodes.forEach(node => {
node.textContent = newGemCount;
});
} catch (e) {
console.warn('Failed to update lingot-stat elements:', e);
}
animateLingotStats(prev, newGemCount);
}
function createGUI() {
const existing = document.getElementById('duo-gemsmith-gui');
if (existing) existing.remove();
const gui = document.createElement('div');
gui.id = 'duo-gemsmith-gui';
gui.innerHTML = `
<style>
span#gs-gems-gained{
font-weight: 600;
}
#duo-gemsmith-gui {
position: fixed;
top: 24px;
right: 24px;
background: #050816;
border-radius: 18px;
padding: 18px 18px 16px;
box-shadow:
0 22px 45px rgba(15, 23, 42, 0.85),
0 0 0 1px rgba(148, 163, 184, 0.15);
z-index: 999999;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #e5e7eb;
min-width: 360px;
max-width: 440px;
backdrop-filter: blur(18px);
box-sizing: border-box;
cursor: default;
}
#duo-gemsmith-gui * {
box-sizing: border-box;
}
#duo-gemsmith-gui h2 {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.03em;
display: flex;
align-items: center;
justify-content: space-between;
color: #f9fafb;
cursor: move;
user-select: none;
}
#duo-gemsmith-gui h2 span.title-left {
display: inline-flex;
align-items: center;
gap: 8px;
}
#duo-gemsmith-gui h2 span.title-pill {
font-size: 11px;
padding: 3px 8px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(56,189,248,0.24));
color: #e0f2fe;
text-transform: uppercase;
letter-spacing: 0.08em;
}
#duo-gemsmith-gui .sub {
font-size: 11px;
color: #9ca3af;
margin-bottom: 10px;
}
#duo-gemsmith-gui .grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin-bottom: 10px;
}
#duo-gemsmith-gui .stat-card {
background: radial-gradient(circle at top left, rgba(56,189,248,0.22), transparent 55%);
border-radius: 12px;
padding: 8px 9px;
border: 1px solid rgba(148, 163, 184, 0.22);
display: flex;
flex-direction: column;
gap: 3px;
}
#duo-gemsmith-gui .stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.09em;
color: #9ca3af;
}
#duo-gemsmith-gui .stat-value {
font-size: 12px;
font-weight: 500;
color: #e5e7eb;
word-break: break-all;
}
#duo-gemsmith-gui .stat-value.gems {
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 4px;
}
#duo-gemsmith-gui .stat-value.gems img {
width: 14px;
height: 14px;
vertical-align: middle;
}
#duo-gemsmith-gui .stat-value.accent {
color: #a5b4fc;
font-weight: 600;
}
#duo-gemsmith-gui .divider {
height: 1px;
background: radial-gradient(circle, rgba(148,163,184,0.6), transparent);
margin: 10px 0;
opacity: 0.5;
}
#duo-gemsmith-gui label.field-label {
display: block;
font-size: 11px;
color: #9ca3af;
margin-bottom: 3px;
}
#duo-gemsmith-gui input[type="number"] {
width: 100%;
padding: 8px 9px;
border-radius: 10px;
border: 1px solid rgba(55, 65, 81, 0.9);
background: rgba(15,23,42,0.9);
color: #e5e7eb;
font-size: 13px;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
#duo-gemsmith-gui input[type="number"]::placeholder {
color: #6b7280;
}
#duo-gemsmith-gui input[type="number"]:focus {
border-color: #38bdf8;
box-shadow: 0 0 0 1px rgba(56,189,248,0.7);
background: rgba(15,23,42,1);
}
#duo-gemsmith-gui .section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9ca3af;
margin: 8px 0 6px;
}
#duo-gemsmith-gui .speed-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#duo-gemsmith-gui .speed-pill {
flex: 1 1 45%;
min-width: 0;
border-radius: 999px;
border: 1px solid rgba(55,65,81,0.9);
background: rgba(15,23,42,0.85);
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 1px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, transform 0.07s;
font-size: 10px;
}
#duo-gemsmith-gui .speed-pill .speed-name {
font-weight: 500;
color: #e5e7eb;
}
#duo-gemsmith-gui .speed-pill .speed-desc {
font-size: 10px;
color: #9ca3af;
}
#duo-gemsmith-gui .speed-pill[data-selected="true"] {
border-color: #38bdf8;
background: radial-gradient(circle at top left, rgba(56,189,248,0.26), rgba(15,23,42,0.95));
box-shadow: 0 0 0 1px rgba(56,189,248,0.4);
}
#duo-gemsmith-gui .speed-pill[data-selected="true"] .speed-name {
color: #e0f2fe;
}
#duo-gemsmith-gui .speed-pill[data-selected="true"] .speed-desc {
color: #bae6fd;
}
#duo-gemsmith-gui .speed-pill:active {
transform: scale(0.97);
}
#duo-gemsmith-gui .speed-row {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 6px;
align-items: center;
}
#duo-gemsmith-gui .custom-speed-box {
background: radial-gradient(circle at top right, rgba(52,211,153,0.2), rgba(15,23,42,0.96));
border-radius: 12px;
padding: 6px 8px 8px;
border: 1px solid rgba(34,197,94,0.35);
}
#duo-gemsmith-gui .custom-speed-box small {
display: block;
margin-top: 2px;
font-size: 10px;
color: #6ee7b7;
}
#duo-gemsmith-gui .buttons-row {
display: grid;
grid-template-columns: 1.1fr 0.9fr 0.7fr 0.7fr;
gap: 6px;
margin-top: 10px;
}
#duo-gemsmith-gui button {
border: none;
border-radius: 999px;
padding: 8px 10px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, transform 0.07s, box-shadow 0.15s, opacity 0.15s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
}
#duo-gemsmith-gui button:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
#duo-gemsmith-gui .start-btn {
background: linear-gradient(135deg, #22c55e, #16a34a);
color: #ecfdf5;
box-shadow: 0 10px 25px rgba(34,197,94,0.35);
}
#duo-gemsmith-gui .start-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #4ade80, #22c55e);
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(34,197,94,0.5);
}
#duo-gemsmith-gui .stop-btn {
background: linear-gradient(135deg, #f97316, #ea580c);
color: #fff7ed;
box-shadow: 0 10px 25px rgba(249,115,22,0.35);
}
#duo-gemsmith-gui .stop-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #fb923c, #f97316);
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(249,115,22,0.5);
}
#duo-gemsmith-gui .reset-btn {
background: rgba(31,41,55,0.95);
color: #e5e7eb;
border: 1px solid rgba(55,65,81,0.9);
}
#duo-gemsmith-gui .reset-btn:hover:not(:disabled) {
background: rgba(17,24,39,1);
color: #f9fafb;
transform: translateY(-1px);
}
#duo-gemsmith-gui .close-btn {
background: rgba(15,23,42,0.85);
color: #9ca3af;
border: 1px solid rgba(55,65,81,0.9);
}
#duo-gemsmith-gui .close-btn:hover {
background: rgba(17,24,39,1);
color: #e5e7eb;
transform: translateY(-1px);
}
#duo-gemsmith-gui .log {
background: radial-gradient(circle at top left, rgba(79,70,229,0.25), rgba(15,23,42,0.96));
padding: 8px 9px;
border-radius: 12px;
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
font-size: 11px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
border: 1px solid rgba(55,65,81,0.9);
}
#duo-gemsmith-gui .log-entry {
margin: 3px 0;
padding: 3px 4px;
border-left: 2px solid #22c55e;
padding-left: 6px;
border-radius: 2px;
background: rgba(15,23,42,0.75);
}
#duo-gemsmith-gui .log-entry.error {
border-left-color: #f97316;
color: #fed7aa;
}
#duo-gemsmith-gui .log-entry.success {
border-left-color: #22c55e;
color: #bbf7d0;
}
#duo-gemsmith-gui .log-entry.info {
border-left-color: #38bdf8;
color: #bae6fd;
}
#duo-gemsmith-gui .credits {
margin-top: 6px;
font-size: 10px;
text-align: right;
color: #6b7280;
}
#duo-gemsmith-gui .credits a {
color: #93c5fd;
text-decoration: none;
}
#duo-gemsmith-gui .credits a:hover {
text-decoration: underline;
}
#duo-gemsmith-gui .inline-gem {
width: 12px;
height: 12px;
vertical-align: -1px;
}
/* lingot blur animation */
@keyframes gsLingotPulse {
0% {
filter: blur(0px);
transform: scale(1);
opacity: 1;
}
25% {
filter: blur(1.5px);
transform: scale(1.06);
opacity: 0.9;
}
50% {
filter: blur(0.5px);
transform: scale(1.03);
opacity: 1;
}
100% {
filter: blur(0px);
transform: scale(1);
opacity: 1;
}
}
span[data-test="lingot-stat"].gs-lingot-animate {
animation: gsLingotPulse 0.45s ease-out;
}
/* ---------- RESPONSIVE LAYOUTS ---------- */
@media (max-width: 1024px) {
#duo-gemsmith-gui {
top: 16px;
right: 16px;
padding: 14px 14px 12px;
min-width: 320px;
max-width: 380px;
}
#duo-gemsmith-gui h2 {
font-size: 16px;
margin-bottom: 8px;
}
#duo-gemsmith-gui .sub {
font-size: 10px;
margin-bottom: 8px;
}
#duo-gemsmith-gui .stat-card {
padding: 7px 8px;
}
#duo-gemsmith-gui .stat-label {
font-size: 9px;
}
#duo-gemsmith-gui .stat-value {
font-size: 11px;
}
#duo-gemsmith-gui input[type="number"] {
font-size: 12px;
padding: 7px 8px;
}
#duo-gemsmith-gui button {
padding: 7px 8px;
font-size: 11px;
}
}
@media (max-width: 600px) {
#duo-gemsmith-gui {
top: 10px;
right: 8px;
left: 8px;
max-width: none;
width: auto;
padding: 12px 12px 10px;
border-radius: 14px;
}
#duo-gemsmith-gui h2 {
font-size: 15px;
margin-bottom: 6px;
}
#duo-gemsmith-gui .title-pill {
display: none;
}
#duo-gemsmith-gui .sub {
font-size: 10px;
margin-bottom: 6px;
}
#duo-gemsmith-gui .grid {
grid-template-columns: 1fr;
}
#duo-gemsmith-gui .buttons-row {
grid-template-columns: 1fr 1fr 0.7fr 0.7fr;
gap: 4px;
}
#duo-gemsmith-gui input[type="number"] {
font-size: 11px;
padding: 6px 7px;
}
#duo-gemsmith-gui button {
padding: 6px 7px;
font-size: 10px;
}
#duo-gemsmith-gui .section-title {
font-size: 10px;
}
#duo-gemsmith-gui .log {
max-height: 160px;
font-size: 10px;
}
#duo-gemsmith-gui .credits {
font-size: 9px;
}
}
@media (max-width: 400px) {
#duo-gemsmith-gui {
top: 6px;
left: 4px;
right: 4px;
padding: 10px 10px 8px;
}
#duo-gemsmith-gui h2 {
font-size: 14px;
}
#duo-gemsmith-gui .buttons-row {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
}
</style>
<h2 id="gs-header">
<span class="title-left">
<span>Duo-Gemsmith</span>
</span>
<span class="title-pill">Auto Claimer</span>
</h2>
<div class="sub">We are not responsible for misuse. Run reward claims at a responsible speed.</div>
<div class="grid">
<div class="stat-card">
<div class="stat-label">User ID</div>
<div class="stat-value" id="gs-userid">Loading...</div>
</div>
<div class="stat-card">
<div class="stat-label">Username</div>
<div class="stat-value accent" id="gs-username">Loading...</div>
</div>
<div class="stat-card">
<div class="stat-label">Current Gems</div>
<div class="stat-value gems" id="gs-current-gems">
<span class="gs-current-gems-value">0</span>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" alt="gems">
</div>
</div>
<div class="stat-card">
<div class="stat-label">Requests / Gained</div>
<div class="stat-value">
<span id="gs-requests">0</span> req ·
<span id="gs-gems-gained">0</span>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" class="inline-gem" alt="gems">
</div>
</div>
<div class="stat-card" style="grid-column: span 2;">
<div class="stat-label">Total Gems Smithed Ever</div>
<div class="stat-value gems">
<span id="gs-total-gems-ever">0</span>
<img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg" alt="gems">
</div>
</div>
</div>
<div class="divider"></div>
<label class="field-label" for="gs-iterations">Iterations</label>
<input
type="number"
id="gs-iterations"
placeholder="0 or Empty = run until stopped"
min="0"
value="0"
/>
<div class="section-title">Speed</div>
<div class="speed-row">
<div class="speed-group">
<button class="speed-pill" data-speed-mode="extreme">
<span class="speed-name">Extremely fast</span>
<span class="speed-desc">0.1s interval</span>
</button>
<button class="speed-pill" data-speed-mode="fast">
<span class="speed-name">Fast</span>
<span class="speed-desc">1s interval</span>
</button>
<button class="speed-pill" data-speed-mode="medium">
<span class="speed-name">Medium</span>
<span class="speed-desc">2s interval</span>
</button>
<button class="speed-pill" data-speed-mode="slow">
<span class="speed-name">Slow</span>
<span class="speed-desc">3s interval</span>
</button>
</div>
<div class="custom-speed-box">
<label class="field-label" for="gs-custom-interval" style="margin-bottom:1px;">Custom</label>
<input
type="number"
id="gs-custom-interval"
placeholder="Seconds"
min="0.05"
step="0.05"
/>
<small>Choose the custom pill to use this.</small>
</div>
</div>
<div class="speed-group" style="margin-top:6px;">
<button class="speed-pill" data-speed-mode="custom" style="flex:1 1 100%;">
<span class="speed-name">Custom speed</span>
<span class="speed-desc">Use your custom seconds value</span>
</button>
</div>
<div class="buttons-row">
<button class="start-btn" id="gs-start">▶ Start</button>
<button class="stop-btn" id="gs-stop" disabled>⏹ Stop</button>
<button class="reset-btn" id="gs-reset">Reset Settings</button>
<button class="close-btn" id="gs-close">✕</button>
</div>
<div class="log" id="gs-log"></div>
<div class="credits">
Created by
<a href="https://github.com/merhametsize" target="_blank" rel="noopener noreferrer">merhametsize</a>
&
<a href="https://github.com/apersongithub" target="_blank" rel="noopener noreferrer">apersongithub</a>
</div>
`;
document.body.appendChild(gui);
// Restore last position if available
if (settings.position && typeof settings.position.top === 'number' && typeof settings.position.left === 'number') {
gui.style.top = `${settings.position.top}px`;
gui.style.left = `${settings.position.left}px`;
gui.style.right = 'auto';
}
const els = {
panel: gui,
header: document.getElementById('gs-header'),
userid: document.getElementById('gs-userid'),
username: document.getElementById('gs-username'),
currentGemsValue: gui.querySelector('.gs-current-gems-value'),
requests: document.getElementById('gs-requests'),
gemsGained: document.getElementById('gs-gems-gained'),
totalGemsEver: document.getElementById('gs-total-gems-ever'),
iterations: document.getElementById('gs-iterations'),
speedPills: Array.from(gui.querySelectorAll('.speed-pill')),
customInterval: document.getElementById('gs-custom-interval'),
startBtn: document.getElementById('gs-start'),
stopBtn: document.getElementById('gs-stop'),
resetBtn: document.getElementById('gs-reset'),
closeBtn: document.getElementById('gs-close'),
log: document.getElementById('gs-log')
};
// Initialize total gems ever from cookie-backed settings
els.totalGemsEver.textContent = settings.totalGemsEver ?? 0;
// Make panel draggable and persist position
makeDraggable(els.panel, els.header, (pos) => {
settings.position = { top: pos.top, left: pos.left };
saveSettings();
});
// Apply saved settings to UI
els.iterations.value = settings.iterations ?? 0;
els.customInterval.value = settings.customInterval ?? 1.5;
function updateSpeedPillsSelection() {
els.speedPills.forEach(btn => {
const mode = btn.getAttribute('data-speed-mode');
btn.dataset.selected = mode === settings.speedMode ? 'true' : 'false';
});
}
updateSpeedPillsSelection();
// Reset everything back to defaults except totalGemsEver
function resetSettingsToDefaults() {
const defs = defaultSettings();
const preservedTotal = settings.totalGemsEver; // keep total gems made ever
settings.iterations = defs.iterations;
settings.speedMode = defs.speedMode;
settings.customInterval = defs.customInterval;
settings.position = defs.position;
settings.totalGemsEver = preservedTotal;
// Persist
saveSettings();
// Update UI
els.iterations.value = settings.iterations;
els.customInterval.value = settings.customInterval;
els.totalGemsEver.textContent = settings.totalGemsEver;
updateSpeedPillsSelection();
// Clear run-time counters (visual only)
els.requests.textContent = '0';
els.gemsGained.textContent = '0';
addLog(els.log, 'Settings reset to defaults (total gems made ever preserved).', 'info');
}
// Events to persist settings
els.iterations.addEventListener('change', () => {
settings.iterations = parseInt(els.iterations.value) || 0;
saveSettings();
});
// Auto-select custom when user edits the custom interval.
els.customInterval.addEventListener('input', () => {
const v = parseFloat(els.customInterval.value);
if (!isNaN(v) && v > 0) {
settings.customInterval = v;
settings.speedMode = 'custom';
saveSettings();
updateSpeedPillsSelection();
}
});
els.speedPills.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.getAttribute('data-speed-mode');
settings.speedMode = mode;
saveSettings();
updateSpeedPillsSelection();
});
});
els.resetBtn.addEventListener('click', () => {
if (isRunning) {
addLog(els.log, 'Cannot reset while running. Please stop first.', 'error');
return;
}
resetSettingsToDefaults();
});
return els;
}
function getIntervalSecondsFromSettings(settings) {
switch (settings.speedMode) {
case 'extreme': return 0.1;
case 'fast': return 1;
case 'medium': return 2;
case 'slow': return 3;
case 'custom':
default:
return settings.customInterval > 0 ? settings.customInterval : 1.5;
}
}
async function main() {
const elements = createGUI();
// Get JWT token
const token = getJWTFromCookies();
if (!token) {
addLog(elements.log, '❌ JWT token not found in cookies!', 'error');
addLog(elements.log, 'Please make sure you\'re logged in to Duolingo', 'error');
elements.startBtn.disabled = true;
return;
}
// Decode token and get user info
let userid;
try {
const decoded = decodeToken(token);
userid = decoded.userid;
elements.userid.textContent = userid;
addLog(elements.log, '✓ JWT token retrieved successfully', 'success');
} catch (err) {
addLog(elements.log, `❌ Failed to decode JWT: ${err.message}`, 'error');
elements.startBtn.disabled = true;
return;
}
// Fetch username and initial gem count with fallback logging
let username;
let initialGems = 0;
let userApiBase; // which base worked for user (with /users)
try {
const userData = await fetchUserDataWithFallback(userid, elements.log);
username = userData.username;
initialGems = userData.gems;
userApiBase = userData.base; // e.g., https://www.duolingo.com/2017-06-30/users
elements.username.textContent = username;
updateGemDisplays(initialGems, elements);
addLog(elements.log, `✓ Username: ${username}`, 'success');
addLog(elements.log, `✓ Current gems: ${initialGems}`, 'success');
} catch (err) {
addLog(elements.log, `❌ All user API endpoints failed: ${err.message}`, 'error');
elements.startBtn.disabled = true;
return;
}
const reward = `SKILL_COMPLETION_BALANCED-${username}DoesntLikeEnergy-2-GEMS`;
const apiUrl = buildRewardUrlFromUsersBase(userApiBase, userid, reward);
let requestCount = 0;
let totalGemsGained = 0;
// Auto-update gem count every 5 seconds using same fallback chain
const gemUpdateInterval = setInterval(async () => {
if (!document.getElementById('duo-gemsmith-gui')) {
clearInterval(gemUpdateInterval);
return;
}
try {
const userData = await fetchUserDataWithFallback(userid, elements.log);
updateGemDisplays(userData.gems, elements);
} catch {
// ignore periodic failures
}
}, 5000);
elements.startBtn.addEventListener('click', async () => {
if (isRunning) return;
const maxIterations = parseInt(elements.iterations.value) || 0;
settings.iterations = maxIterations;
saveSettings();
const customVal = parseFloat(elements.customInterval.value);
if (!isNaN(customVal) && customVal > 0) {
settings.customInterval = customVal;
saveSettings();
}
// Validate custom if selected
if (settings.speedMode === 'custom') {
if (!settings.customInterval || settings.customInterval <= 0) {
addLog(elements.log, '❌ Custom speed must be greater than 0 seconds', 'error');
return;
}
}
const intervalSeconds = getIntervalSecondsFromSettings(settings);
isRunning = true;
shouldStop = false;
elements.startBtn.disabled = true;
elements.stopBtn.disabled = false;
elements.iterations.disabled = true;
elements.speedPills.forEach(b => b.disabled = true);
elements.customInterval.disabled = false;
addLog(
elements.log,
`🚀 Starting... (${maxIterations === 0 ? 'infinite' : maxIterations} iterations, interval = ${intervalSeconds.toFixed(2)}s, mode = ${settings.speedMode})`,
'success'
);
while (!shouldStop) {
if (maxIterations > 0 && requestCount >= maxIterations) {
addLog(elements.log, `✓ Completed ${maxIterations} iterations`, 'success');
break;
}
requestCount++;
elements.requests.textContent = requestCount;
try {
const result = await sendPatchRequest(apiUrl, PAYLOAD);
if (result.ok) {
totalGemsGained += 30;
elements.gemsGained.textContent = totalGemsGained.toString();
// Update lifetime total in cookie-backed settings
settings.totalGemsEver = (settings.totalGemsEver || 0) + 30;
elements.totalGemsEver.textContent = settings.totalGemsEver;
saveSettings();
addLog(elements.log, `#${requestCount}: +30 gems (Status: ${result.status})`, 'success');
try {
const userData = await fetchUserDataWithFallback(userid, elements.log);
updateGemDisplays(userData.gems, elements);
} catch {
// ignore failure in refresh
}
} else {
addLog(elements.log, `#${requestCount}: Failed (Status: ${result.status})`, 'error');
}
} catch (err) {
addLog(elements.log, `#${requestCount}: Error - ${err.message}`, 'error');
}
if (!shouldStop && (maxIterations === 0 || requestCount < maxIterations)) {
await interruptibleSleep(intervalSeconds, () => shouldStop);
}
}
isRunning = false;
elements.startBtn.disabled = false;
elements.stopBtn.disabled = true;
elements.iterations.disabled = false;
elements.speedPills.forEach(b => b.disabled = false);
elements.customInterval.disabled = false;
if (shouldStop) {
addLog(elements.log, '⏹ Stopped by user', 'info');
}
});
elements.stopBtn.addEventListener('click', () => {
shouldStop = true;
elements.stopBtn.disabled = true;
});
elements.closeBtn.addEventListener('click', () => {
shouldStop = true;
clearInterval(gemUpdateInterval);
const panel = document.getElementById('duo-gemsmith-gui');
if (panel) panel.remove();
});
addLog(elements.log, '✓ Ready! Drag by the header, set iterations and choose a speed, then click Start', 'success');
}
try {
await main();
} catch (err) {
console.error('Fatal error:', err);
}
})();