1import { Actor } from 'apify';
2import { connect as connectRealBrowser } from 'puppeteer-real-browser';
3
4const SITE_BASE = 'https://instantusername.com';
5
6const SCRAPER_CONFIG = {
7 turnstile: true,
8 headless: false,
9 scrollStepPx: 600,
10 scrollDelayMinMs: 350,
11 scrollDelayMaxMs: 550,
12 scrollSettleDelayMinMs: 250,
13 scrollSettleDelayMaxMs: 400,
14 loadingWaitTimeoutMs: 60000,
15 loadingStableRounds: 1,
16 loadingStableDelayMinMs: 400,
17 loadingStableDelayMaxMs: 600,
18};
19
20class UsernameAvailabilityScraper {
21 async getPageStats(page) {
22 try {
23 return await page.evaluate(() => {
24 const body = document.body;
25 return {
26 title: document.title || '',
27 resultCount: document.querySelectorAll('ul li div[class*="_container_"]').length,
28 showMoreCount: document.querySelectorAll('button[aria-label="Show more"]').length,
29 loadingCount: [...document.querySelectorAll('li')].filter((li) =>
30 /Loading/i.test(li.textContent || ''),
31 ).length,
32 scrollHeight: body?.scrollHeight ?? document.documentElement?.scrollHeight ?? 0,
33 ready: Boolean(body),
34 };
35 });
36 } catch (error) {
37 return {
38 title: '',
39 resultCount: 0,
40 showMoreCount: 0,
41 loadingCount: 0,
42 scrollHeight: 0,
43 ready: false,
44 error: error.message || String(error),
45 };
46 }
47 }
48
49 async waitForPageReady(page, timeoutMs = 30000) {
50 const start = Date.now();
51
52 while (Date.now() - start < timeoutMs) {
53 const stats = await this.getPageStats(page);
54 if (stats.ready && !stats.error) {
55 return stats;
56 }
57
58 await this.randomDelay(500, 1000);
59 }
60
61 return this.getPageStats(page);
62 }
63
64 isCloudflareChallenge(title = '') {
65 return (
66 title.includes('Just a moment') ||
67 title.includes('Attention Required') ||
68 title.includes('Cloudflare')
69 );
70 }
71
72 buildProxyOptions(proxyUrl) {
73 if (!proxyUrl) return undefined;
74
75 try {
76 const parsed = new URL(proxyUrl);
77 if (!parsed.hostname || !parsed.port) return undefined;
78
79 return {
80 host: parsed.hostname,
81 port: Number(parsed.port),
82 username: parsed.username || undefined,
83 password: parsed.password || undefined,
84 };
85 } catch (error) {
86 console.warn(`Invalid proxy URL detected: ${proxyUrl}`, error);
87 return undefined;
88 }
89 }
90
91 buildSearchUrl(username) {
92 const params = new URLSearchParams({ q: username });
93 return `${SITE_BASE}/?${params.toString()}`;
94 }
95
96 async run(input) {
97 const { usernames, proxyConfiguration } = input;
98
99 if (!Array.isArray(usernames) || usernames.length === 0) {
100 throw new Error('Input must include a non-empty usernames array');
101 }
102
103 const proxyConfig = proxyConfiguration
104 ? await Actor.createProxyConfiguration(proxyConfiguration)
105 : undefined;
106
107 for (const rawUsername of usernames) {
108 const username = String(rawUsername || '').trim();
109 if (!username) continue;
110
111 const proxyUrl = proxyConfig ? await proxyConfig.newUrl() : undefined;
112 const proxyOptions = this.buildProxyOptions(proxyUrl);
113
114 const realBrowserOption = {
115 args: ['--start-maximized', '--no-sandbox'],
116 turnstile: SCRAPER_CONFIG.turnstile,
117 headless: SCRAPER_CONFIG.headless,
118 customConfig: {},
119 connectOption: {
120 defaultViewport: { width: 1400, height: 900 },
121 },
122 ...(proxyOptions ? { proxy: proxyOptions } : {}),
123 plugins: [],
124 };
125
126 let browser;
127 try {
128 const { page, browser: connectedBrowser } = await connectRealBrowser(realBrowserOption);
129 browser = connectedBrowser;
130
131 await page.setDefaultNavigationTimeout(300000);
132 await page.setDefaultTimeout(300000);
133
134 await this.scrapeUsername(page, username);
135 } finally {
136 if (browser) {
137 await browser.close();
138 }
139 }
140
141 await this.randomDelay(1000, 2000);
142 }
143 }
144
145 async scrapeUsername(page, username) {
146 const searchUrl = this.buildSearchUrl(username);
147 console.log(`Checking username availability for "${username}"...`);
148
149 try {
150 await page.goto(searchUrl, { waitUntil: 'domcontentloaded', timeout: 180000 });
151
152 await this.waitForResults(page, username);
153 await this.expandAllSections(page);
154
155 const postExpandStats = await this.getPageStats(page);
156 if (postExpandStats.loadingCount > 0) {
157 await this.scrollThroughPage(page);
158 }
159
160 await this.waitForLoadingComplete(page, username);
161
162 const results = await this.extractResults(page, username);
163
164 if (results.length === 0) {
165 console.log(`No platform results found for "${username}"`);
166 await Actor.pushData([
167 {
168 username,
169 error: 'No platform results found',
170 scrapedAt: new Date().toISOString(),
171 },
172 ]);
173 return;
174 }
175
176 console.log(`Saved ${results.length} platform checks for "${username}"`);
177 await Actor.pushData(results);
178 } catch (error) {
179 const message = error.message || String(error);
180 console.log(`Scrape failed for "${username}": ${message}`);
181 await Actor.pushData([
182 {
183 username,
184 error: message,
185 scrapedAt: new Date().toISOString(),
186 },
187 ]);
188 }
189 }
190
191 async waitForCloudflare(page) {
192 const start = Date.now();
193 const timeoutMs = 120000;
194
195 while (Date.now() - start < timeoutMs) {
196 let stats;
197 try {
198 stats = await this.getPageStats(page);
199 } catch {
200 await this.randomDelay(1000, 2000);
201 continue;
202 }
203
204 if (stats.error) {
205 await this.randomDelay(1000, 2000);
206 continue;
207 }
208
209 if (!this.isCloudflareChallenge(stats.title)) {
210 await this.waitForPageReady(page);
211 return true;
212 }
213
214 await this.randomDelay(2000, 3000);
215 }
216
217 return false;
218 }
219
220 async waitForResults(page, username) {
221 const passed = await this.waitForCloudflare(page);
222 if (!passed) {
223 throw new Error('Cloudflare verification timed out');
224 }
225
226 const selector = 'ul li div[class*="_container_"]';
227 const start = Date.now();
228 const timeoutMs = 120000;
229 let found = false;
230
231 while (Date.now() - start < timeoutMs) {
232 const stats = await this.getPageStats(page);
233
234 if (stats.resultCount > 0) {
235 found = true;
236 break;
237 }
238
239 const selectorFound = await page.$(selector).then(Boolean).catch(() => false);
240 if (selectorFound) {
241 found = true;
242 break;
243 }
244
245 await this.randomDelay(2000, 3000);
246 }
247
248 if (!found) {
249 const stats = await this.getPageStats(page);
250 throw new Error(
251 `Result rows not found after ${Math.round((Date.now() - start) / 1000)}s. Page title: "${stats.title}"`,
252 );
253 }
254
255 await this.randomDelay(800, 1200);
256 }
257
258 async scrollGradually(page, options = {}) {
259 const stepPx = options.stepPx ?? SCRAPER_CONFIG.scrollStepPx;
260 const delayMin = options.delayMinMs ?? SCRAPER_CONFIG.scrollDelayMinMs;
261 const delayMax = options.delayMaxMs ?? SCRAPER_CONFIG.scrollDelayMaxMs;
262 const maxSteps = options.maxSteps ?? 80;
263 const stableLimit = options.stableLimit ?? 1;
264
265 let previousHeight = 0;
266 let stablePasses = 0;
267
268 for (let i = 0; i < maxSteps; i++) {
269 const scrollState = await page
270 .evaluate((step) => {
271 const body = document.body;
272 const docEl = document.documentElement;
273 const maxHeight = body?.scrollHeight ?? docEl?.scrollHeight ?? 0;
274 const viewportHeight = window.innerHeight || docEl?.clientHeight || 900;
275 const currentTop = window.scrollY || docEl?.scrollTop || 0;
276 const nextTop = Math.min(currentTop + step, Math.max(0, maxHeight - viewportHeight));
277
278 window.scrollTo(0, nextTop);
279
280 return {
281 maxHeight,
282 atBottom: nextTop + viewportHeight >= maxHeight - 4,
283 };
284 }, stepPx)
285 .catch(() => ({ maxHeight: 0, atBottom: true }));
286
287 await this.randomDelay(delayMin, delayMax);
288
289 if (scrollState.maxHeight === previousHeight) {
290 stablePasses++;
291 } else {
292 stablePasses = 0;
293 previousHeight = scrollState.maxHeight;
294 }
295
296 if (scrollState.atBottom && stablePasses >= stableLimit) {
297 break;
298 }
299 }
300
301 await this.randomDelay(
302 SCRAPER_CONFIG.scrollSettleDelayMinMs,
303 SCRAPER_CONFIG.scrollSettleDelayMaxMs,
304 );
305 }
306
307 async scrollToTop(page) {
308 await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {});
309 await this.randomDelay(150, 250);
310 }
311
312 async scrollThroughPage(page, options = {}) {
313 await this.scrollToTop(page);
314 await this.scrollGradually(page, options);
315 }
316
317 async expandAllSections(page) {
318 for (let round = 0; round < 20; round++) {
319 const clicked = await page.evaluate(() => {
320 const buttons = [
321 ...document.querySelectorAll('button[aria-label="Show more"]'),
322 ].filter((button) => button.offsetParent !== null);
323
324 buttons.forEach((button) => {
325 button.scrollIntoView({ block: 'center' });
326 button.click();
327 });
328 return buttons.length;
329 });
330
331 if (!clicked) {
332 break;
333 }
334
335 await this.randomDelay(500, 800);
336
337 const afterClickStats = await this.getPageStats(page);
338 if (afterClickStats.loadingCount > 0) {
339 await this.scrollThroughPage(page);
340 }
341 }
342 }
343
344 async waitForLoadingComplete(page, username) {
345 const timeoutMs = SCRAPER_CONFIG.loadingWaitTimeoutMs;
346 const start = Date.now();
347 let stableRounds = 0;
348
349 while (Date.now() - start < timeoutMs) {
350 const stats = await this.getPageStats(page);
351
352 if (stats.loadingCount === 0) {
353 stableRounds++;
354 if (stableRounds >= SCRAPER_CONFIG.loadingStableRounds) {
355 break;
356 }
357
358 await this.randomDelay(
359 SCRAPER_CONFIG.loadingStableDelayMinMs,
360 SCRAPER_CONFIG.loadingStableDelayMaxMs,
361 );
362 continue;
363 }
364
365 stableRounds = 0;
366
367 if (Date.now() - start >= timeoutMs) {
368 break;
369 }
370
371 await this.scrollThroughPage(page);
372 }
373
374 const finalStats = await this.getPageStats(page);
375 if (finalStats.loadingCount > 0) {
376 console.log(
377 `Warning: ${finalStats.loadingCount} platforms still loading for "${username}"`,
378 );
379 }
380
381 await this.randomDelay(400, 600);
382 }
383
384 async extractResults(page, username) {
385 const items = await page.evaluate(() => {
386 const results = [];
387
388 const getStatus = (container, text) => {
389 const className = container.className || '';
390 if (className.includes('_taken_') || /\bTaken\b/.test(text)) return 'Taken';
391 if (className.includes('_available_') || /\bAvailable\b/.test(text)) return 'Available';
392 if (className.includes('loading') || /\bLoading\b/i.test(text)) return 'Loading';
393 if (className.includes('unknown') || /\bUnknown\b/.test(text)) return 'Unknown';
394 return 'Unknown';
395 };
396
397 const getPlatformName = (li) => {
398 const platformText = li.querySelector('[class*="_platformText_"]');
399 if (platformText?.textContent?.trim()) {
400 return platformText.textContent.trim();
401 }
402
403 const linkText = li.querySelector('a')?.textContent || '';
404 return linkText
405 .replace(/Taken|Available|Unknown|Loading/gi, '')
406 .split('·')[0]
407 .trim();
408 };
409
410 const getDetail = (li) => {
411 const detailNode = li.querySelector(
412 '[class*="_twoLineText_"], [class*="_subtitle_"], [class*="_detail_"]',
413 );
414 const detail = detailNode?.textContent?.replace(/\s+/g, ' ').trim();
415 return detail || null;
416 };
417
418 document.querySelectorAll('ul').forEach((ul) => {
419 const section = ul.parentElement;
420 if (!section) return;
421
422 const category = section.firstElementChild?.textContent?.replace(/\s+/g, ' ').trim();
423 if (!category) return;
424
425 ul.querySelectorAll('li').forEach((li) => {
426 const container = li.querySelector('div[class*="_container_"]');
427 if (!container) return;
428
429 const platform = getPlatformName(li);
430 if (!platform) return;
431
432 const text = li.textContent?.replace(/\s+/g, ' ').trim() || '';
433 const link = li.querySelector('a[href]');
434 let profileUrl = link?.href || null;
435
436 if (profileUrl && profileUrl.includes('instantusername.com')) {
437 profileUrl = null;
438 }
439
440 results.push({
441 category,
442 platform,
443 status: getStatus(container, text),
444 detail: getDetail(li),
445 profileUrl,
446 });
447 });
448 });
449
450 return results;
451 });
452
453 const seen = new Set();
454
455 return items
456 .filter((item) => {
457 const key = `${item.category}::${item.platform}`;
458 if (seen.has(key)) return false;
459 seen.add(key);
460 return true;
461 })
462 .map((item) => ({
463 username,
464 ...item,
465 scrapedAt: new Date().toISOString(),
466 }));
467 }
468
469 async randomDelay(min = 500, max = 1500) {
470 const delay = Math.floor(Math.random() * (max - min + 1) + min);
471 await new Promise((resolve) => setTimeout(resolve, delay));
472 }
473}
474
475await Actor.init();
476
477Actor.main(async () => {
478 const input = await Actor.getInput();
479
480
481
482
483
484
485
486
487 const scraper = new UsernameAvailabilityScraper();
488 await scraper.run(input);
489});