benchmark-captcha-providers.js
1 #!/usr/bin/env node 2 /** 3 * Benchmark CAPTCHA solving providers by submitting a test reCAPTCHA job to each 4 * and measuring time to solution. Updates data/captcha-provider-benchmark.json. 5 * Uses the public reCAPTCHA v2 demo sitekey which all providers accept for testing. 6 */ 7 import { readFileSync, writeFileSync } from 'fs'; 8 import { join, dirname } from 'path'; 9 import { fileURLToPath } from 'url'; 10 import Database from 'better-sqlite3'; 11 import '../src/utils/load-env.js'; 12 13 const __dirname = dirname(fileURLToPath(import.meta.url)); 14 const projectRoot = join(__dirname, '..'); 15 const BENCHMARK_PATH = join(projectRoot, 'data', 'captcha-provider-benchmark.json'); 16 17 // Public demo sitekey accepted by all solving services for testing 18 const TEST_SITEKEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'; 19 const TEST_URL = 'https://www.google.com/recaptcha/api2/demo'; 20 21 function loadBenchmark() { 22 try { 23 return JSON.parse(readFileSync(BENCHMARK_PATH, 'utf8')); 24 } catch { 25 return {}; 26 } 27 } 28 29 function saveBenchmark(data) { 30 writeFileSync(BENCHMARK_PATH, JSON.stringify(data, null, 2)); 31 } 32 33 async function benchmarkNopeCHA(apiKey) { 34 if (!apiKey) return null; 35 const start = Date.now(); 36 const submitRes = await fetch('https://api.nopecha.com/token', { 37 method: 'POST', 38 headers: { 'Content-Type': 'application/json' }, 39 body: JSON.stringify({ key: apiKey, type: 'recaptcha2', sitekey: TEST_SITEKEY, url: TEST_URL }), 40 }); 41 const submitData = await submitRes.json(); 42 if (submitData.error) { 43 console.log(`NopeCHA submit error ${submitData.error}`); 44 return null; 45 } 46 const jobId = submitData.data; 47 for (let i = 0; i < 60; i++) { 48 await new Promise(r => setTimeout(r, 3000)); 49 const res = await fetch(`https://api.nopecha.com/token?key=${apiKey}&id=${jobId}`); 50 const data = await res.json(); 51 if (data.error === 14) continue; 52 if (data.error) { 53 console.log(`NopeCHA poll error ${data.error}`); 54 return null; 55 } 56 if (data.data && typeof data.data === 'string' && data.data.length > 20) 57 return Date.now() - start; 58 } 59 return null; 60 } 61 62 async function benchmarkCapMonster() { 63 const apiKey = process.env.CAPMONSTER_API_KEY; 64 if (!apiKey) return null; 65 const start = Date.now(); 66 const createRes = await fetch('https://api.capmonster.cloud/createTask', { 67 method: 'POST', 68 headers: { 'Content-Type': 'application/json' }, 69 body: JSON.stringify({ 70 clientKey: apiKey, 71 task: { type: 'RecaptchaV2TaskProxyless', websiteURL: TEST_URL, websiteKey: TEST_SITEKEY }, 72 }), 73 }); 74 const createData = await createRes.json(); 75 if (createData.errorId !== 0) { 76 console.log(`CapMonster error ${createData.errorId}: ${createData.errorDescription}`); 77 return null; 78 } 79 const { taskId } = createData; 80 for (let i = 0; i < 60; i++) { 81 await new Promise(r => setTimeout(r, 3000)); 82 const res = await fetch('https://api.capmonster.cloud/getTaskResult', { 83 method: 'POST', 84 headers: { 'Content-Type': 'application/json' }, 85 body: JSON.stringify({ clientKey: apiKey, taskId }), 86 }); 87 const data = await res.json(); 88 if (data.errorId !== 0) { 89 console.log(`CapMonster poll error ${data.errorId}`); 90 return null; 91 } 92 if (data.status === 'ready') return Date.now() - start; 93 } 94 return null; 95 } 96 97 function updateBenchmark(benchmark, name, ms, envKey) { 98 if (ms !== null) { 99 const prev = benchmark[name] || { avgMs: ms, count: 0 }; 100 const count = prev.count + 1; 101 benchmark[name] = { 102 avgMs: Math.round((prev.avgMs * prev.count + ms) / count), 103 count, 104 lastMs: ms, 105 updated_at: new Date().toISOString(), 106 }; 107 console.log( 108 `${name}: ${Math.round(ms / 1000)}s (avg: ${Math.round(benchmark[name].avgMs / 1000)}s)` 109 ); 110 } else if (process.env[envKey]) { 111 benchmark[name] = { 112 ...benchmark[name], 113 lastMs: Infinity, 114 updated_at: new Date().toISOString(), 115 }; 116 console.log(`${name}: failed/timeout`); 117 } 118 } 119 120 function recentFormSends() { 121 const dbPath = process.env.DATABASE_PATH || join(projectRoot, 'db/sites.db'); 122 try { 123 const db = new Database(dbPath, { readonly: true }); 124 const row = db.prepare( 125 "SELECT COUNT(*) AS n FROM messages WHERE contact_method='form' AND sent_at > datetime('now','-1 hour')" 126 ).get(); 127 db.close(); 128 return row?.n ?? 0; 129 } catch { 130 return 0; 131 } 132 } 133 134 async function main() { 135 const sends = recentFormSends(); 136 if (sends < 10) { 137 console.log(`Skipping benchmark — only ${sends} form sends in the last hour (need ≥10).`); 138 process.exit(0); 139 } 140 console.log(`Benchmarking CAPTCHA providers (${sends} form sends in last hour)...`); 141 const benchmark = loadBenchmark(); 142 143 const [nopechaMs, nopecha2Ms, capmonsterMs] = await Promise.all([ 144 benchmarkNopeCHA(process.env.NOPECHA_API_KEY).catch(() => null), 145 benchmarkNopeCHA(process.env.NOPECHA_API_KEY_2).catch(() => null), 146 benchmarkCapMonster().catch(() => null), 147 ]); 148 149 updateBenchmark(benchmark, 'nopecha', nopechaMs, 'NOPECHA_API_KEY'); 150 updateBenchmark(benchmark, 'nopecha2', nopecha2Ms, 'NOPECHA_API_KEY_2'); 151 updateBenchmark(benchmark, 'capmonster', capmonsterMs, 'CAPMONSTER_API_KEY'); 152 153 saveBenchmark(benchmark); 154 155 const winner = Object.entries(benchmark) 156 .filter(([, v]) => v.avgMs && v.avgMs < Infinity) 157 .sort(([, a], [, b]) => a.avgMs - b.avgMs)[0]; 158 if (winner) 159 console.log(`Fastest provider: ${winner[0]} (avg ${Math.round(winner[1].avgMs / 1000)}s)`); 160 } 161 162 main().catch(console.error);