Disclosure
This article contains referral links to RewardVid. If you sign up through our links, we may earn a small commission at no cost to you. We only recommend platforms we've personally tested. See our full disclaimer.
The RewardVid Auto Surfer is a free TamperMonkey userscript that automates the desktop browser version of rewardvid.com. It picks the shortest unwatched video on each pass, enforces a 24-hour cooldown per video ID so you never re-watch the same content before it resets, handles Cloudflare Turnstile pauses, and automatically cycles through pages when the current page is exhausted. Install takes under two minutes.
You'll need a free RewardVid account before the script is useful.
Sign Up for RewardVid (Free)Referral link — no cost to you
What Is RewardVid?
RewardVid is a Get-Paid-To (GPT) platform that pays you to watch videos. Advertisers pay for guaranteed views; the platform passes a fraction of that to you in the form of points redeemable for cryptocurrency or PayPal. The platform is available as a web interface and an Android app.
The honest earning rate from video watching alone lands between $0.05 and $0.15 per hour depending on your region and available ad inventory. That's not impressive in isolation — the entire case for using it rests on running it as a background process that requires minimal attention. Our full RewardVid review covers the platform in detail including the withdrawal process, offerwall mechanics, and spare-device strategy.
The desktop web version has one friction point that makes it semi-passive rather than fully passive: periodic "Are you still watching?" captcha interrupts. This script solves that friction for the video selection and navigation side of things, while also handling the broader automation logic.
What the Script Does
Here is a precise breakdown of every behaviour the Auto Surfer implements:
Core Surfing Logic
- Shortest-first selection: On each landing page pass, the script scans all visible video links, extracts their durations, filters out anything on cooldown or marked no-budget, and navigates to the shortest eligible video. This maximises the number of completions per unit of time.
- 24-hour cooldown per video ID: Every watched video gets its ID stored in localStorage with a timestamp. The script skips that ID for the next 24 hours. If the platform's own cooldown differs (e.g. the page text says "refreshes every 12 hours"), the script detects it and adjusts accordingly.
- Automatic page cycling: When the current page has no eligible videos, the script increments the page number and navigates to the next. When all pages up to the configured maximum are exhausted, it either waits and restarts from page 1 (with a countdown) or bumps the max video length filter by 1 minute and restarts immediately, depending on your settings.
- Duration-based countdown: Once on a video page, the script calculates the wait time from the known video duration plus a 15-second safety buffer. It counts down in real time so you can see exactly when navigation back to the landing page will occur.
- Auto-resume after navigation: The script stores its running state in localStorage. If it was running when the page reloaded (as happens between every video), it resumes automatically without requiring you to click Start again.
Cloudflare Turnstile Handling
When a Cloudflare Turnstile challenge is detected — either a visible iframe from challenges.cloudflare.com, a .cf-turnstile element, or a full-page challenge overlay — the script pauses and displays a warning in its status panel. It polls every second until the challenge is cleared. Once you solve it manually, the script resumes automatically after a 3-second stabilisation delay. You cannot automate Turnstile itself; you solve it, everything else resumes without intervention.
No-Budget Detection
Some videos have exhausted their advertiser budget. When the script detects phrases like "does not have enough budget" or "no views available" on a video page, it logs that video's ID to a persistent no-budget list, marks it as watched (skipping it for the cooldown period), and returns to the landing page. The no-budget count is visible in the panel.
Installation: Step by Step
Step 1: Install TamperMonkey
TamperMonkey is a free browser extension that runs userscripts. Install it from the official source for your browser:
- Chrome / Brave / Edge: tampermonkey.net → Chrome Web Store link
- Firefox: tampermonkey.net → Firefox Add-ons link
Once installed, the TamperMonkey icon appears in your browser toolbar (a circle with two overlapping squares).
Step 2: Create a New Script
- Click the TamperMonkey icon in your toolbar.
- Select Dashboard from the dropdown.
- In the Dashboard, click the + tab (or the "Create a new script" button).
- A default script template appears in the editor.
Step 3: Paste the Script
- Select all the text in the editor (Ctrl+A / Cmd+A).
- Delete it.
- Copy the complete script from the code block below.
- Paste it into the empty editor.
Step 4: Save
Press Ctrl+S (or Cmd+S on Mac), or click File → Save. TamperMonkey will confirm the script is saved and enabled.
Step 5: Navigate to RewardVid and Start
- Open rewardvid.com and log in.
- A floating panel labelled "RewardVid Auto Surfer" appears in the top-right area of the page. Click it to expand the control panel.
- Click Start. The script begins scanning for videos immediately.
Browser Recommendation
Run this in a secondary browser profile or a dedicated browser window, not your main browsing session. A spare desktop or an always-on laptop plugged into power is the ideal setup — same principle as the spare Android device strategy for the mobile app.
The Control Panel: Every Option Explained
The floating panel (click the icon to expand) contains everything you need to configure and monitor the script.
Start / Stop
The primary toggle. Green "Start" begins the surfing loop; red "Stop" halts it immediately. The script does not navigate away when stopped — it just freezes until you resume.
Status Line
A live text feed of what the script is currently doing: "Scanning page 3 for unwatched videos...", "Watching video — 1:42 remaining...", "Turnstile challenge detected — solve it to continue...", and so on. Also displayed as a persistent toast in the bottom-right corner of the page.
Filters
- Max Video Length (minutes): The script skips videos longer than this threshold. Set to 0 for no limit. Default is 10 minutes. Lower values mean faster cycling through short videos; higher values allow longer content if short inventory runs out.
- Max Pages to Scan: How many paginated pages of videos to cycle through before declaring the full library exhausted. Default is 25.
- Auto-increase max length by 1m after full scan: When checked, instead of sitting idle after exhausting all pages, the script bumps the Max Video Length filter by 1 minute and starts over from page 1. This allows it to gradually unlock longer videos when short inventory is depleted.
Videos Watched
Two counters: Today (resets at midnight based on your local time) and Total (lifetime count for this browser/device). These are stored in localStorage and persist across page reloads.
Cooldowns (24h)
- Active: Number of video IDs currently under a cooldown lock.
- Next expires: How long until the earliest cooldown lifts, formatted as "Xh Ym". Useful for knowing when new eligible videos will open up.
No Budget / Blocked
- Logged videos: Count of videos the script has identified as having no advertiser budget.
- Block This Video button: If you're on a video page and you want to manually flag it as blocked (e.g. it's broken or stuck), click this. The script marks it as no-budget and watched, then returns to the landing page.
Reset Controls
- Clear Stats: Resets the video count and earnings counters. Does not affect cooldowns or the no-budget list.
- Reset Cooldowns: Clears all 24-hour cooldown records. All previously watched videos become eligible again immediately.
- Clear No Budget Log: Removes all manually blocked and no-budget flagged video IDs. Use this if budget has been refilled and you want to retry those videos.
Key Behaviours to Know
The 24-Hour Cooldown System
Every video on RewardVid has a unique ID in its URL. The script records each ID when you start watching it and stores a timestamp. For the next 24 hours, that ID is skipped on every scan pass. This prevents wasteful re-navigation to videos that will reject the view. When the cooldown expires (24 hours by default, or whatever duration the page text indicates), the ID becomes eligible again automatically.
Turnstile Pause Behaviour
Cloudflare Turnstile challenges require human interaction — there is no way around them. When the script detects one, it pauses the countdown, displays a warning both in the panel and as a toast, and polls every second until the challenge iframe disappears. Once you click through the Turnstile, the script detects clearance and resumes after a 3-second stabilisation pause. You do not need to click Start again.
Duration-Based Wait with 15-Second Buffer
When the script navigates to a video page, it looks up the duration it scraped from the listing and waits for that duration plus 15 seconds. The 15-second buffer accounts for page load time and any server-side verification lag. If a YouTube-embedded video ends early (detected via the YouTube IFrame API), the remaining wait is shortened to the 15-second buffer only.
Auto-Resume After Page Navigation
Every video completion sends the browser back to the landing page. This is a full page navigation — the script's in-memory state is lost. To handle this, the script saves its running state to localStorage before every navigation. On page load, it checks this flag and resumes automatically if it was running. This is why the script keeps surfing after every video without you touching anything.
The Spare-Device Approach
This script is designed for a secondary, always-on computer: an old laptop, a desktop you leave running, anything that stays on and connected. Keep it on a secondary browser window, plug the machine into power, and let it run. Check in occasionally to solve Turnstile challenges. You don't need to watch it — the panel and status toast will surface anything that needs your attention.
Don't have a RewardVid account yet? Sign up free before installing the script.
Create Free RewardVid Account →Referral link — no cost to you
The Complete Script
Copy the entire block below. In TamperMonkey: Dashboard → + → select all → paste → Ctrl+S.
// ==UserScript==
// @name RewardVid Auto Surfer
// @namespace http://tampermonkey.net/
// @version 2026.04.30.3
// @description Auto-surfs RewardVid.com — picks shortest unwatched video per pass, enforces 24-hour cooldown per video ID, handles Cloudflare Turnstile, cycles through pages when current page is exhausted.
// @author You
// @match https://rewardvid.com/*
// @icon https://rewardvid.com/public/uploads/settings/e276bd175964104e5fe5a682f040c31b.png
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ── Constants ──────────────────────────────────────────────────────────
const SCRIPT_NAME = 'RewardVid Auto Surfer';
const PREFIX = 'rv';
const ACCENT = '#f59e0b';
const ACCENT_TEXT = '#0d0e1a';
const BORDER = '#3d2a06';
const LOGO_SRC = 'https://rewardvid.com/public/uploads/settings/e276bd175964104e5fe5a682f040c31b.png';
const DEFAULT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
const NAV_DELAY_MS = 2500;
const SETTLE_MS = 1800;
const EXHAUST_WAIT = 5 * 60 * 1000;
const DEFAULT_MAX_LENGTH_MINS = 10;
const DEFAULT_MAX_PAGES = 25;
const DURATION_BUFFER_SECS = 15;
const FALLBACK_WAIT_SECS = 600;
const YT_HEARTBEAT_MS = 6000;
const getMaxPages = () => S.get(`${PREFIX}_max_pages`, DEFAULT_MAX_PAGES);
const isVideoPage = () => /\/video\//.test(location.pathname);
const getVideoId = () => { const m = location.pathname.match(/\/video\/([^/]+)/); return m?.[1] ?? null; };
const getCurrentPage = () => { const p = new URLSearchParams(location.search).get('page'); return p ? parseInt(p, 10) : 1; };
const pageUrl = (n) => n <= 1 ? 'https://rewardvid.com/' : `https://rewardvid.com/?page=${n}`;
const S = {
get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : JSON.parse(v); } catch { return d; } },
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
};
const todayStr = () => new Date().toISOString().slice(0, 10);
function checkDailyReset() {
if (S.get(`${PREFIX}_last_reset`, '') !== todayStr()) {
S.set(`${PREFIX}_videos_today`, 0);
S.set(`${PREFIX}_earn_today`, 0);
S.set(`${PREFIX}_last_reset`, todayStr());
}
}
function readEntry(raw) {
if (typeof raw === 'number') return { ts: raw, cooldownMs: DEFAULT_COOLDOWN_MS };
return raw;
}
function isWatched(videoId) {
const watched = S.get(`${PREFIX}_watched`, {});
const raw = watched[videoId];
if (!raw) return false;
const { ts, cooldownMs } = readEntry(raw);
return Date.now() - ts < cooldownMs;
}
function markWatched(videoId, cooldownMs = DEFAULT_COOLDOWN_MS) {
const watched = S.get(`${PREFIX}_watched`, {});
watched[videoId] = { ts: Date.now(), cooldownMs };
for (const [id, raw] of Object.entries(watched)) {
const { ts, cooldownMs: cdMs } = readEntry(raw);
if (Date.now() - ts > 2 * cdMs) delete watched[id];
}
S.set(`${PREFIX}_watched`, watched);
}
function countActiveCooldowns() {
const watched = S.get(`${PREFIX}_watched`, {});
return Object.values(watched).filter(raw => {
const { ts, cooldownMs } = readEntry(raw);
return Date.now() - ts < cooldownMs;
}).length;
}
function nextCooldownExpiry() {
const watched = S.get(`${PREFIX}_watched`, {});
const expiries = Object.values(watched)
.map(raw => { const { ts, cooldownMs } = readEntry(raw); return ts + cooldownMs; })
.filter(exp => exp > Date.now());
return expiries.length ? Math.min(...expiries) : null;
}
function scrapeCooldownMs() {
const text = (document.body.innerText || '').toLowerCase();
const m = text.match(/(?:every|cooldown|refresh(?:es)?(?:\s+every)?)\s*(\d+)\s*h(?:ours?)?/);
if (m) {
const h = parseInt(m[1], 10);
if (h > 0 && h <= 48) return h * 60 * 60 * 1000;
}
return DEFAULT_COOLDOWN_MS;
}
const getMaxLengthSecs = () => S.get(`${PREFIX}_max_length`, DEFAULT_MAX_LENGTH_MINS) * 60;
function logNoBudget(videoId) {
const nb = S.get(`${PREFIX}_no_budget`, {});
nb[videoId] = { ts: Date.now(), url: location.href };
S.set(`${PREFIX}_no_budget`, nb);
}
function isNoBudget(videoId) {
return !!S.get(`${PREFIX}_no_budget`, {})[videoId];
}
function countNoBudget() {
return Object.keys(S.get(`${PREFIX}_no_budget`, {})).length;
}
function log(msg) {
console.log(`%c[${SCRIPT_NAME}] %c${msg}`, `color:${ACCENT};font-weight:bold`, 'color:#ccc');
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
function parseDuration(str) {
if (!str) return Infinity;
const s = str.trim();
const parts = s.split(':').map(Number);
if (parts.length === 2) return parts[0] * 60 + parts[1];
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
const n = parseInt(s, 10);
return isNaN(n) ? Infinity : n;
}
function fmtSeconds(s) {
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${String(r).padStart(2, '0')}`;
}
function fmtTimeUntil(ms) {
const s = Math.max(0, Math.ceil((ms - Date.now()) / 1000));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return `${h}h ${m}m`;
}
function getToast() {
let el = document.getElementById(`${PREFIX}-toast`);
if (!el) {
el = document.createElement('div');
el.id = `${PREFIX}-toast`;
el.style.cssText = [
'position:fixed;bottom:20px;right:20px;',
'background:linear-gradient(135deg,#78450a,#a05c0a);',
'color:#fff;padding:10px 18px;border-radius:8px;',
'z-index:2147483646;',
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;',
'font-size:13px;box-shadow:0 4px 20px rgba(0,0,0,.5);',
'pointer-events:none;max-width:320px;line-height:1.4;',
].join('');
document.body.appendChild(el);
}
return el;
}
function setStatus(msg) { getToast().textContent = msg; log(msg); updateStatusLine(msg); }
function clearStatus() { document.getElementById(`${PREFIX}-toast`)?.remove(); }
function updateStatusLine(msg) {
const el = document.getElementById(`${PREFIX}-status-line`);
if (el) el.textContent = msg;
}
const INP = `background:#0d0e1a;border:1px solid ${BORDER};color:#e8e8e8;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:12px;`;
const SEC = `color:${ACCENT};font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px;`;
const ROW = 'display:grid;grid-template-columns:1fr auto;gap:3px 14px;font-size:12px;margin-bottom:10px;align-items:center;';
const BTN_BASE = 'border:none;border-radius:6px;padding:5px 13px;cursor:pointer;font-family:inherit;font-size:12px;font-weight:600;white-space:nowrap;transition:opacity .15s;';
const BTN_GHOST = `${BTN_BASE}background:#23243a;color:${ACCENT};border:1px solid ${BORDER};font-weight:400;`;
const BTN_DANGER = `${BTN_BASE}background:#1a0d0d;color:#f87171;border:1px solid #7b2020;font-weight:400;`;
let isRunning = false;
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:10px;right:280px;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;user-select:none;';
function buildOverlay() {
overlay.innerHTML = `
<div id="${PREFIX}-icon-btn" title="${SCRIPT_NAME}" style="
height:44px;padding:0 16px;border-radius:10px;cursor:pointer;
background:#141624;border:2px solid ${ACCENT};
display:flex;align-items:center;justify-content:center;gap:10px;
box-shadow:0 2px 14px rgba(0,0,0,.6);
">
<img src="${LOGO_SRC}" style="width:28px;height:28px;object-fit:contain;display:block;" onerror="this.style.display='none'">
<span style="color:${ACCENT};font-size:13px;font-weight:700;letter-spacing:.4px;">${SCRIPT_NAME}</span>
</div>
<div id="${PREFIX}-panel" style="display:none;
position:absolute;top:52px;right:0;width:320px;
background:#141624;border:1px solid ${BORDER};border-radius:12px;
box-shadow:0 10px 36px rgba(0,0,0,.75);overflow:hidden;color:#e8e8e8;
">
<div style="padding:14px 15px;">
<div style="display:flex;gap:8px;margin-bottom:14px;align-items:center;">
<button id="${PREFIX}-toggle" style="${BTN_BASE}background:${ACCENT};color:${ACCENT_TEXT};">▶ Start</button>
</div>
<div style="${SEC}">Status</div>
<div id="${PREFIX}-status-line" style="font-size:11px;color:#888;margin-bottom:6px;min-height:14px;">Idle</div>
<div id="${PREFIX}-next-video" style="font-size:10px;color:#444;word-break:break-all;margin-bottom:12px;"></div>
<div style="border-top:1px solid ${BORDER};margin:0 -15px 12px;"></div>
<div style="${SEC}">Filters</div>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;">
<span style="color:#888;flex:1;">Max Video Length</span>
<input id="${PREFIX}-max-length" type="number" min="0" max="120" step="1"
value="${S.get(`${PREFIX}_max_length`, DEFAULT_MAX_LENGTH_MINS)}"
style="${INP}width:52px;text-align:right;" title="0 = no limit">
<span style="color:#888;">min</span>
</div>
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;">
<span style="color:#888;flex:1;">Max Pages to Scan</span>
<input id="${PREFIX}-max-pages" type="number" min="1" max="99" step="1"
value="${S.get(`${PREFIX}_max_pages`, DEFAULT_MAX_PAGES)}"
style="${INP}width:52px;text-align:right;">
</div>
<label style="display:flex;align-items:center;gap:8px;font-size:12px;color:#888;margin-bottom:12px;cursor:pointer;">
<input id="${PREFIX}-auto-inc" type="checkbox" ${S.get(`${PREFIX}_auto_inc`, false) ? 'checked' : ''}
style="accent-color:${ACCENT};width:14px;height:14px;cursor:pointer;">
<span>Auto-increase max length by 1m after full scan</span>
</label>
<div style="border-top:1px solid ${BORDER};margin:0 -15px 12px;"></div>
<div style="${SEC}">Videos Watched</div>
<div style="${ROW}">
<span style="color:#888;">Today</span>
<span id="${PREFIX}-vids-day" style="color:#4ade80;text-align:right;font-weight:600;">0</span>
<span style="color:#888;">Total</span>
<span id="${PREFIX}-vids-tot" style="color:#4ade80;text-align:right;font-weight:600;">0</span>
</div>
<div style="border-top:1px solid ${BORDER};margin:0 -15px 12px;"></div>
<div style="${SEC}">Cooldowns (24h)</div>
<div style="${ROW}">
<span style="color:#888;">Active</span>
<span id="${PREFIX}-cool-count" style="color:#60a5fa;text-align:right;font-weight:600;">0</span>
<span style="color:#888;">Next expires</span>
<span id="${PREFIX}-cool-next" style="color:#60a5fa;text-align:right;font-weight:600;">—</span>
</div>
<div style="border-top:1px solid ${BORDER};margin:0 -15px 12px;"></div>
<div style="${SEC}">No Budget / Blocked</div>
<div style="${ROW}">
<span style="color:#888;">Logged videos</span>
<span id="${PREFIX}-nb-count" style="color:#f87171;text-align:right;font-weight:600;">0</span>
</div>
<button id="${PREFIX}-block-video" style="${BTN_BASE}background:#23243a;color:#f87171;border:1px solid #7b2020;font-weight:600;width:100%;margin-bottom:12px;">⊘ Block This Video</button>
<div style="border-top:1px solid ${BORDER};margin:0 -15px 12px;"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button id="${PREFIX}-clear-stats" style="${BTN_DANGER}">Clear Stats</button>
<button id="${PREFIX}-clear-cooldowns" style="${BTN_DANGER}">Reset Cooldowns</button>
<button id="${PREFIX}-clear-nb" style="${BTN_DANGER}">Clear No Budget Log</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
wirePanelEvents();
}
function wirePanelEvents() {
const iconBtn = document.getElementById(`${PREFIX}-icon-btn`);
const panel = document.getElementById(`${PREFIX}-panel`);
let dragMoved = false;
iconBtn.addEventListener('mousedown', e => {
if (e.button !== 0) return;
dragMoved = false;
const startX = e.clientX, startY = e.clientY;
const rect = overlay.getBoundingClientRect();
overlay.style.right = 'auto';
overlay.style.left = rect.left + 'px';
overlay.style.top = rect.top + 'px';
const onMove = ev => {
const dx = ev.clientX - startX, dy = ev.clientY - startY;
if (!dragMoved && Math.abs(dx) + Math.abs(dy) > 4) dragMoved = true;
if (dragMoved) {
overlay.style.left = (rect.left + dx) + 'px';
overlay.style.top = (rect.top + dy) + 'px';
}
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
if (!dragMoved) panel.style.display = panel.style.display === 'none' ? '' : 'none';
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
document.getElementById(`${PREFIX}-toggle`).addEventListener('click', () => {
isRunning ? stopSurfer() : startSurfer();
});
document.getElementById(`${PREFIX}-max-length`).addEventListener('change', e => {
const v = Math.max(0, parseInt(e.target.value, 10) || 0);
e.target.value = v;
S.set(`${PREFIX}_max_length`, v);
});
document.getElementById(`${PREFIX}-max-pages`).addEventListener('change', e => {
const v = Math.max(1, parseInt(e.target.value, 10) || DEFAULT_MAX_PAGES);
e.target.value = v;
S.set(`${PREFIX}_max_pages`, v);
});
document.getElementById(`${PREFIX}-auto-inc`).addEventListener('change', e => {
S.set(`${PREFIX}_auto_inc`, e.target.checked);
});
document.getElementById(`${PREFIX}-clear-stats`).addEventListener('click', () => {
if (!confirm('Clear video counts? (cooldowns are not affected)')) return;
S.set(`${PREFIX}_videos_today`, 0);
S.set(`${PREFIX}_videos_total`, 0);
S.set(`${PREFIX}_earn_today`, 0);
S.set(`${PREFIX}_earn_total`, 0);
refreshUI();
});
document.getElementById(`${PREFIX}-clear-cooldowns`).addEventListener('click', () => {
if (!confirm('Reset all 24-hour cooldowns? All videos will be re-eligible.')) return;
S.set(`${PREFIX}_watched`, {});
refreshUI();
});
document.getElementById(`${PREFIX}-clear-nb`).addEventListener('click', () => {
if (!confirm('Clear the no-budget / blocked video log?')) return;
S.set(`${PREFIX}_no_budget`, {});
refreshUI();
});
document.getElementById(`${PREFIX}-block-video`).addEventListener('click', () => {
const videoId = getVideoId();
if (!videoId) { setStatus('Must be on a video page to block a video.'); return; }
logNoBudget(videoId);
markWatched(videoId);
refreshUI();
setStatus('Video blocked — returning to landing page…');
setTimeout(() => {
if (isRunning) location.href = pageUrl(S.get(`${PREFIX}_last_page`, 1));
}, 1000);
});
}
function setToggleVisual(running) {
const btn = document.getElementById(`${PREFIX}-toggle`);
if (!btn) return;
if (running) {
btn.textContent = '▮ Stop';
btn.style.background = '#e74c3c';
btn.style.color = '#fff';
} else {
btn.textContent = '▶ Start';
btn.style.background = ACCENT;
btn.style.color = ACCENT_TEXT;
}
}
function refreshUI() {
const set = (id, val) => { const e = document.getElementById(`${PREFIX}-${id}`); if (e) e.textContent = val; };
set('vids-day', S.get(`${PREFIX}_videos_today`, 0));
set('vids-tot', S.get(`${PREFIX}_videos_total`, 0));
set('cool-count', countActiveCooldowns());
const nxt = nextCooldownExpiry();
set('cool-next', nxt ? fmtTimeUntil(nxt) : '—');
set('nb-count', countNoBudget());
if (!isRunning) set('status-line', 'Idle');
const mlEl = document.getElementById(`${PREFIX}-max-length`);
if (mlEl) mlEl.value = S.get(`${PREFIX}_max_length`, DEFAULT_MAX_LENGTH_MINS);
const mpEl = document.getElementById(`${PREFIX}-max-pages`);
if (mpEl) mpEl.value = S.get(`${PREFIX}_max_pages`, DEFAULT_MAX_PAGES);
const aiEl = document.getElementById(`${PREFIX}-auto-inc`);
if (aiEl) aiEl.checked = S.get(`${PREFIX}_auto_inc`, false);
}
function scanVideos() {
const seen = new Set();
const candidates = [];
document.querySelectorAll('a[href*="/video/"]').forEach(a => {
const href = a.href;
const m = href.match(/\/video\/([^/?#]+)/);
if (!m) return;
const videoId = m[1];
if (seen.has(videoId)) return;
seen.add(videoId);
if (isWatched(videoId)) return;
if (isNoBudget(videoId)) return;
const container = a.closest('.card, .video-card, .video-item, [class*="video"], article, li, .col, .item') || a.parentElement || a;
let duration = Infinity;
const durEl = container.querySelector('[class*="duration"], [class*="time"], [class*="length"], .badge, [class*="sec"], [class*="min"]');
if (durEl) {
const t = durEl.textContent.trim();
const tm = t.match(/(\d{1,2}:\d{2}(?::\d{2})?)/);
if (tm) duration = parseDuration(tm[1]);
}
if (duration === Infinity) {
const text = container.textContent || '';
const tm = text.match(/\b(\d{1,2}:\d{2}(?::\d{2})?)\b/);
if (tm) duration = parseDuration(tm[1]);
}
const maxSecs = getMaxLengthSecs();
if (maxSecs > 0 && duration !== Infinity && duration > maxSecs) return;
if (/\$0\.0+\b/.test(container.textContent)) return;
candidates.push({ videoId, href, duration });
});
return candidates.sort((a, b) => a.duration - b.duration);
}
async function doLandingPage() {
await sleep(SETTLE_MS);
if (!isRunning) return;
const curPage = getCurrentPage();
setStatus(`Scanning page ${curPage} for unwatched videos…`);
const videos = scanVideos();
const maxMins = S.get(`${PREFIX}_max_length`, DEFAULT_MAX_LENGTH_MINS);
const filterNote = maxMins > 0 ? ` (≤${maxMins}m, not on cooldown)` : ' (not on cooldown)';
if (videos.length === 0) {
const maxPages = getMaxPages();
const nextPage = curPage + 1;
if (nextPage > maxPages) {
if (S.get(`${PREFIX}_auto_inc`, false)) {
const curMax = S.get(`${PREFIX}_max_length`, DEFAULT_MAX_LENGTH_MINS);
const newMax = curMax + 1;
S.set(`${PREFIX}_max_length`, newMax);
refreshUI();
setStatus(`All ${maxPages} pages exhausted — bumped max to ${newMax}m, returning to page 1…`);
await sleep(1500);
} else {
for (let s = EXHAUST_WAIT / 1000; s > 0; s--) {
if (!isRunning) return;
setStatus(`All ${maxPages} pages exhausted${filterNote} — restarting in ${fmtSeconds(s)}…`);
await sleep(1000);
}
}
if (isRunning) location.assign(pageUrl(1));
return;
}
setStatus(`Page ${curPage}: no eligible videos${filterNote} — trying page ${nextPage}…`);
await sleep(1200);
if (isRunning) location.href = pageUrl(nextPage);
return;
}
const pick = videos[0];
const durLabel = pick.duration === Infinity ? 'unknown duration' : fmtSeconds(pick.duration);
setStatus(`Found ${videos.length} unwatched — navigating to shortest (${durLabel})…`);
const nextVidEl = document.getElementById(`${PREFIX}-next-video`);
if (nextVidEl) nextVidEl.textContent = pick.href;
S.set(`${PREFIX}_last_page`, curPage);
S.set(`${PREFIX}_last_duration`, pick.duration === Infinity ? 0 : pick.duration);
await sleep(1200);
if (isRunning) location.href = pick.href;
}
function isTurnstilePresent() {
const needsUserAction = (el) => {
if (!el) return false;
const r = el.getBoundingClientRect();
return r.width > 100 && r.height > 80;
};
if (needsUserAction(document.querySelector('iframe[src*="challenges.cloudflare.com"]'))) return true;
if (needsUserAction(document.querySelector('.cf-turnstile'))) return true;
const cfOverlay = document.querySelector('#cf-challenge-running, #cf-error-details, .cf-challenge-running');
if (cfOverlay && cfOverlay.offsetParent !== null) return true;
return false;
}
async function waitForTurnstile() {
if (!isTurnstilePresent()) return;
setStatus('⚠ Turnstile challenge detected — solve it to continue…');
while (isTurnstilePresent() && isRunning) await sleep(1000);
if (!isRunning) return;
setStatus('Turnstile passed — resuming…');
await sleep(3000);
}
function kickPlayback() {
const vid = document.querySelector('video');
if (vid) vid.play().catch(() => {});
const ytFrame = document.querySelector('iframe[src*="youtube.com/embed"], iframe[src*="youtube-nocookie.com/embed"]');
if (ytFrame) {
try {
ytFrame.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*');
} catch {}
const r = ytFrame.getBoundingClientRect();
if (r.width > 0) {
ytFrame.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: r.left + r.width / 2, clientY: r.top + r.height / 2 }));
}
}
}
function wireYouTubeDetection(ytEndedFlag, ytPlayingFlag) {
const SEL = 'iframe[src*="youtube.com/embed"], iframe[src*="youtube-nocookie.com/embed"]';
let heartbeat = null;
let messageHandler = null;
const doWire = (frame) => {
if (!frame.src.includes('enablejsapi=1')) {
frame.addEventListener('load', () => setTimeout(() => doWire(frame), 1500), { once: true });
try {
const url = new URL(frame.src);
url.searchParams.set('enablejsapi', '1');
frame.src = url.toString();
} catch {}
return;
}
const announce = () => {
try { frame.contentWindow.postMessage(JSON.stringify({ event: 'listening', id: 1 }), '*'); } catch {}
try { frame.contentWindow.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: [] }), '*'); } catch {}
};
announce();
heartbeat = setInterval(announce, YT_HEARTBEAT_MS);
messageHandler = (e) => {
if (!e.data) return;
try {
const msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
if (!msg?.event) return;
if ((msg.event === 'onStateChange' && msg.info === 1) || (msg.event === 'infoDelivery' && msg.info?.playerState === 1)) ytPlayingFlag[0] = true;
if ((msg.event === 'onStateChange' && msg.info === 0) || (msg.event === 'infoDelivery' && msg.info?.playerState === 0)) ytEndedFlag[0] = true;
} catch {}
};
window.addEventListener('message', messageHandler);
};
let attempts = 0;
const poll = setInterval(() => {
const frame = document.querySelector(SEL);
if (frame) { clearInterval(poll); doWire(frame); return; }
if (++attempts >= 12) clearInterval(poll);
}, 1000);
return () => {
clearInterval(poll);
clearInterval(heartbeat);
if (messageHandler) window.removeEventListener('message', messageHandler);
};
}
async function doVideoPage() {
const videoId = getVideoId();
if (!videoId) return;
const pageArrivalMs = Date.now();
markWatched(videoId);
await sleep(SETTLE_MS);
if (!isRunning) return;
const detectedCooldownMs = scrapeCooldownMs();
if (detectedCooldownMs !== DEFAULT_COOLDOWN_MS) markWatched(videoId, detectedCooldownMs);
const ytEndedFlag = [false];
const ytPlayingFlag = [false];
const cleanupYT = wireYouTubeDetection(ytEndedFlag, ytPlayingFlag);
kickPlayback();
const hasNoBudget = () => {
const t = (document.body.innerText || '').toLowerCase();
return t.includes('does not have enough budget') || t.includes('no views available');
};
for (let i = 0; i < 6 && isRunning; i++) {
if (hasNoBudget()) {
cleanupYT();
logNoBudget(videoId);
setStatus('No budget for this video — going back…');
refreshUI();
await sleep(1200);
if (isRunning) location.href = pageUrl(S.get(`${PREFIX}_last_page`, 1));
return;
}
await sleep(1000);
}
if (!isRunning) { cleanupYT(); return; }
await waitForTurnstile();
if (!isRunning) { cleanupYT(); return; }
const WU_KEY = `${PREFIX}_last_wu`;
const lastWU = S.get(WU_KEY, null);
let remainingSecs;
if (lastWU && lastWU.id === videoId) {
const remainingMs = lastWU.until - Date.now();
remainingSecs = remainingMs <= 0 ? 0 : Math.ceil(remainingMs / 1000);
} else {
const knownSecs = S.get(`${PREFIX}_last_duration`, 0);
const rawWaitSecs = (knownSecs > 0) ? knownSecs + DURATION_BUFFER_SECS : FALLBACK_WAIT_SECS;
const elapsedSecs = Math.floor((Date.now() - pageArrivalMs) / 1000);
remainingSecs = Math.max(5, rawWaitSecs - elapsedSecs);
S.set(WU_KEY, { id: videoId, until: Date.now() + remainingSecs * 1000 });
}
const nativeVid = document.querySelector('video');
if (nativeVid) {
nativeVid.addEventListener('ended', () => { ytEndedFlag[0] = true; }, { once: true });
}
const YT_SEL = 'iframe[src*="youtube.com/embed"], iframe[src*="youtube-nocookie.com/embed"]';
let skipVideo = false;
const safetyTimeout = setTimeout(() => {
if (!isRunning || skipVideo || remainingSecs <= 0) return;
if (!document.querySelector(YT_SEL) && !document.querySelector('video')) {
setStatus('No video player found after 30s — returning to landing page…');
skipVideo = true;
}
}, 30000);
while (remainingSecs > 0 && isRunning && !skipVideo) {
if (ytEndedFlag[0] && remainingSecs > DURATION_BUFFER_SECS) remainingSecs = DURATION_BUFFER_SECS;
setStatus(`Watching video — ${fmtSeconds(remainingSecs)} remaining…`);
await sleep(1000);
remainingSecs--;
}
clearTimeout(safetyTimeout);
cleanupYT();
if (!isRunning) return;
if (skipVideo) {
await sleep(1200);
if (isRunning) location.href = pageUrl(S.get(`${PREFIX}_last_page`, 1));
return;
}
await waitForTurnstile();
if (!isRunning) return;
S.set(WU_KEY, null);
S.set(`${PREFIX}_videos_today`, S.get(`${PREFIX}_videos_today`, 0) + 1);
S.set(`${PREFIX}_videos_total`, S.get(`${PREFIX}_videos_total`, 0) + 1);
refreshUI();
setStatus('Video complete! Returning to landing page…');
await sleep(NAV_DELAY_MS);
if (isRunning) location.href = pageUrl(1);
}
function startSurfer() {
if (isRunning) return;
isRunning = true;
S.set(`${PREFIX}_running`, true);
setToggleVisual(true);
if (isVideoPage()) doVideoPage();
else doLandingPage();
}
function stopSurfer() {
isRunning = false;
S.set(`${PREFIX}_running`, false);
setToggleVisual(false);
setStatus('Stopped.');
refreshUI();
setTimeout(clearStatus, 3000);
}
function checkAutoResume() {
if (S.get(`${PREFIX}_running`, false)) setTimeout(startSurfer, 400);
}
function init() {
checkDailyReset();
buildOverlay();
refreshUI();
setInterval(refreshUI, 2000);
checkAutoResume();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
The script is only useful if you have a RewardVid account. Sign up free — no deposit required.
Create Your Free RewardVid Account →Referral link — no cost to you
Is the Script Worth Using?
What It Solves
- Eliminates manual video selection on every pass
- Prevents wasted time on no-budget or cooldown-locked videos
- Handles Turnstile gracefully — just solve and it resumes
- Persists state across page navigations automatically
- Configurable without touching the code
- Useful stats panel baked in
Limitations
- Cannot solve Turnstile challenges for you — still requires occasional manual interaction
- Desktop browser only — does not work in the Android app
- Hourly earning rate from video watching remains $0.05–$0.15 regardless of automation
- Requires TamperMonkey installed in your browser
Bottom line: the script removes the most tedious parts of using RewardVid on desktop — manual video selection and navigation — without changing the fundamental earning rate. If you were going to run RewardVid in a browser anyway, the script makes it significantly less annoying to maintain.
Related Reading
- RewardVid Full Review (2026) — The complete platform breakdown: earning rates, withdrawal mechanics, and spare-device strategy
- How To Automate LuckyWatch Earnings (2026 Guide) — Similar approach for LuckyWatch using MacroDroid on Android
- FaucetPay Review — The micro-wallet where your RewardVid withdrawals land
- Getting Started with Micro-Earning
- More Blog Posts
Was this article helpful?