# This is an unofficial scraper for https://duck.ai/ // Duck.ai has no public API. This tool interacts with the // private chat endpoint by driving a real browser (puppeteer-real-browser).

> Source: <https://gist.github.com/Omegatech-01/78e4cd406c63684ef1c81ef30eb9d84d>
> Published: 2026-10-16 19:32:02+00:00

|  | // duckai_cli.js | 
|  | // Author : Omegatech | 
|  | // WhatsApp : https://whatsapp.com/channel/0029Vb785rSBlHpWSitPY61i | 
|  | // Name : Duck.ai Scraper (CLI) | 
|  | // Date : 15 October 2026 | 
|  | // | 
|  | // ────────────────────────────────────────────────────────────── | 
|  | // DISCLAIMER | 
|  | // ────────────────────────────────────────────────────────────── | 
|  | // This is an unofficial scraper for https://duck.ai/ | 
|  | // Duck.ai has no public API. This tool interacts with the | 
|  | // private chat endpoint by driving a real browser (puppeteer-real-browser). | 
|  | // | 
|  | // • Use at your own risk. | 
|  | // • Do not abuse rate limits or run high-volume automation. | 
|  | // • DuckDuckGo may change the frontend / challenge at any time. | 
|  | // • This project is for educational / personal use only. | 
|  | // • The author is not responsible for any account blocks, IP bans, | 
|  | // or legal consequences arising from misuse. | 
|  | // ────────────────────────────────────────────────────────────── | 
|  | // | 
|  | // SCRAPE INFO | 
|  | // ────────────────────────────────────────────────────────────── | 
|  | // Target : https://duck.ai/ | 
|  | // Method : Browser automation (puppeteer-real-browser) | 
|  | // Endpoint : POST /duckchat/v1/chat (SSE stream) | 
|  | // Challenge : Solved automatically by the real browser session | 
|  | // Features : | 
|  | // - Text chat (single or multi-turn) | 
|  | // - Model selection | 
|  | // - Reasoning / sources / tool-calls extraction | 
|  | // - Image generation capture (base64 → saved files) | 
|  | // - Debug mode | 
|  | // Dependencies: | 
|  | // npm i puppeteer-real-browser | 
|  | // ────────────────────────────────────────────────────────────── | 
|  | const { connect } = require("puppeteer-real-browser"); | 
|  | const fs = require("fs"); | 
|  | const path = require("path"); | 
|  | // ── Config ── | 
|  | const DUCK_URL = "https://duck.ai/"; | 
|  | const INPUT_SELECTOR = | 
|  | 'textarea, div[contenteditable="true"][role="textbox"], div[contenteditable="true"]'; | 
|  | const IMAGE_DIR = path.join(process.cwd(), "duckai-images"); | 
|  | const DEFAULT_TIMEOUT = 240000; | 
|  | // ── Fetch hook injected into the page ── | 
|  | const HOOK = () => { | 
|  | window.__duckaiCaptures = []; | 
|  | const origFetch = window.fetch.bind(window); | 
|  | window.fetch = async function (...args) { | 
|  | let url = ""; | 
|  | let reqBody = null; | 
|  | try { | 
|  | if (typeof args[0] === "string") url = args[0]; | 
|  | else if (args[0] && args[0].url) url = args[0].url; | 
|  | else if (args[0] && args[0].toString) url = args[0].toString(); | 
|  | if (args[1] && args[1].body) { | 
|  | reqBody = | 
|  | typeof args[1].body === "string" ? args[1].body : "[non-string]"; | 
|  | } | 
|  | } catch (_) {} | 
|  | const response = await origFetch(...args); | 
|  | if (url && url.indexOf("/duckchat/v1/chat") !== -1) { | 
|  | const cap = { | 
|  | url, | 
|  | reqBody, | 
|  | ts: Date.now(), | 
|  | body: "", | 
|  | done: false, | 
|  | error: null, | 
|  | }; | 
|  | window.__duckaiCaptures.push(cap); | 
|  | try { | 
|  | const clone = response.clone(); | 
|  | const reader = clone.body.getReader(); | 
|  | const decoder = new TextDecoder(); | 
|  | (async () => { | 
|  | try { | 
|  | while (true) { | 
|  | const { done, value } = await reader.read(); | 
|  | if (done) break; | 
|  | cap.body += decoder.decode(value, { stream: true }); | 
|  | } | 
|  | cap.done = true; | 
|  | } catch (err) { | 
|  | cap.error = err && err.message ? err.message : String(err); | 
|  | cap.done = true; | 
|  | } | 
|  | })(); | 
|  | } catch (err) { | 
|  | cap.error = err && err.message ? err.message : String(err); | 
|  | cap.done = true; | 
|  | } | 
|  | } | 
|  | return response; | 
|  | }; | 
|  | }; | 
|  | // ── Browser helpers ── | 
|  | async function openDuck(options = {}) { | 
|  | const connection = await connect({ | 
|  | headless: options.headless === true, | 
|  | turnstile: true, | 
|  | fingerprint: true, | 
|  | args: [ | 
|  | "--no-sandbox", | 
|  | "--disable-setuid-sandbox", | 
|  | "--disable-dev-shm-usage", | 
|  | "--disable-blink-features=AutomationControlled", | 
|  | ], | 
|  | customConfig: {}, | 
|  | }); | 
|  | const { browser, page } = connection; | 
|  | await page.evaluateOnNewDocument(HOOK); | 
|  | await page.setViewport({ width: 1366, height: 900 }); | 
|  | await page.setExtraHTTPHeaders({ | 
|  | "Accept-Language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", | 
|  | }); | 
|  | await page.goto(DUCK_URL, { waitUntil: "domcontentloaded", timeout: 90000 }); | 
|  | await page.waitForSelector(INPUT_SELECTOR, { timeout: 60000 }); | 
|  | await new Promise((r) => setTimeout(r, 1500)); | 
|  | return { browser, page }; | 
|  | } | 
|  | // ── SSE parsing ── | 
|  | function parseSSE(body) { | 
|  | const events = []; | 
|  | if (!body) return events; | 
|  | const lines = body.split("\n"); | 
|  | for (const raw of lines) { | 
|  | const line = raw.trim(); | 
|  | if (!line.startsWith("data:")) continue; | 
|  | const payload = line.slice(5).trim(); | 
|  | if (!payload \|\| payload === "[DONE]" \|\| payload === "[PING]") continue; | 
|  | try { | 
|  | events.push(JSON.parse(payload)); | 
|  | } catch (_) { | 
|  | events.push({ _raw: payload }); | 
|  | } | 
|  | } | 
|  | return events; | 
|  | } | 
|  | function assembleResponse(events) { | 
|  | const textParts = []; | 
|  | const reasoningParts = []; | 
|  | const sources = []; | 
|  | const toolCalls = []; | 
|  | const toolResults = []; | 
|  | const images = []; | 
|  | let model = null; | 
|  | let responseId = null; | 
|  | let summaryText = null; | 
|  | for (const e of events) { | 
|  | if (!model && e.model) model = e.model; | 
|  | if (!responseId && e.id) responseId = e.id; | 
|  | if (e.role === "assistant" && typeof e.message === "string") { | 
|  | textParts.push(e.message); | 
|  | } | 
|  | if (e.role === "reasoning" && e.state === "text-delta" && typeof e.text === "string") { | 
|  | reasoningParts.push(e.text); | 
|  | } | 
|  | if (e.role === "reasoning" && e.state === "done" && Array.isArray(e.summaryText)) { | 
|  | summaryText = e.summaryText.join("\n"); | 
|  | } | 
|  | if (e.role === "source" && e.source) { | 
|  | sources.push(e.source); | 
|  | } | 
|  | if (e.role === "tool-invocation" && e.state === "call") { | 
|  | let args = e.toolArguments; | 
|  | try { | 
|  | args = typeof args === "string" ? JSON.parse(args) : args; | 
|  | } catch (_) {} | 
|  | toolCalls.push({ name: e.toolName, args, callId: e.toolCallId }); | 
|  | } | 
|  | if (e.role === "tool-invocation" && e.state === "result") { | 
|  | toolResults.push({ | 
|  | callId: e.toolCallId, | 
|  | result: e.result, | 
|  | data: e.data, | 
|  | }); | 
|  | } | 
|  | if ( | 
|  | e.role === "ui-component" && | 
|  | e.name === "generate-image" && | 
|  | e.state === "data" && | 
|  | e.data && | 
|  | typeof e.data.b64Image === "string" && | 
|  | e.data.status === "success" | 
|  | ) { | 
|  | images.push({ | 
|  | id: e.id, | 
|  | toolCallId: e.toolCallId, | 
|  | format: e.data.format \|\| "jpeg", | 
|  | width: e.data.width \|\| null, | 
|  | height: e.data.height \|\| null, | 
|  | generationDurationMs: e.data.generationDurationMs \|\| null, | 
|  | moderationToken: e.data.moderationToken \|\| null, | 
|  | b64: e.data.b64Image, | 
|  | }); | 
|  | } | 
|  | } | 
|  | return { | 
|  | model, | 
|  | responseId, | 
|  | text: textParts.join(""), | 
|  | reasoning: reasoningParts.join(""), | 
|  | reasoningSummary: summaryText, | 
|  | sources, | 
|  | toolCalls, | 
|  | toolResults, | 
|  | images, | 
|  | }; | 
|  | } | 
|  | // ── Image saver ── | 
|  | function saveImages(result, dir) { | 
|  | if (!result.images \|\| !result.images.length) return []; | 
|  | const targetDir = dir \|\| IMAGE_DIR; | 
|  | if (!fs.existsSync(targetDir)) { | 
|  | fs.mkdirSync(targetDir, { recursive: true }); | 
|  | } | 
|  | const saved = []; | 
|  | const stamp = Date.now(); | 
|  | for (let i = 0; i < result.images.length; i++) { | 
|  | const img = result.images[i]; | 
|  | const ext = img.format === "png" ? "png" : "jpg"; | 
|  | const name = `duckai-${stamp}-${i}.${ext}`; | 
|  | const full = path.join(targetDir, name); | 
|  | fs.writeFileSync(full, Buffer.from(img.b64, "base64")); | 
|  | const toolMatch = result.toolCalls.find( | 
|  | (c) => c.name === "GenerateImage" && c.callId === img.toolCallId | 
|  | ); | 
|  | saved.push({ | 
|  | path: full, | 
|  | format: img.format, | 
|  | width: img.width, | 
|  | height: img.height, | 
|  | bytes: Buffer.byteLength(img.b64, "base64"), | 
|  | prompt: toolMatch && toolMatch.args ? toolMatch.args.imageGenPrompt : null, | 
|  | }); | 
|  | } | 
|  | return saved; | 
|  | } | 
|  | // ── Send single message ── | 
|  | async function sendChat(page, message, options = {}) { | 
|  | const prevCount = await page.evaluate(() => window.__duckaiCaptures.length); | 
|  | const input = await page.waitForSelector(INPUT_SELECTOR, { timeout: 30000 }); | 
|  | await input.click(); | 
|  | await new Promise((r) => setTimeout(r, 300)); | 
|  | // Clear existing content | 
|  | await page.evaluate((sel) => { | 
|  | const el = document.querySelector(sel); | 
|  | if (!el) return; | 
|  | el.focus(); | 
|  | if (el.tagName === "TEXTAREA") { | 
|  | el.value = ""; | 
|  | } else if (el.isContentEditable) { | 
|  | el.innerHTML = ""; | 
|  | } | 
|  | }, INPUT_SELECTOR); | 
|  | await new Promise((r) => setTimeout(r, 200)); | 
|  | // Type via CDP (more reliable) | 
|  | const client = await page.target().createCDPSession(); | 
|  | await client.send("Input.insertText", { text: message }); | 
|  | await client.detach().catch(() => {}); | 
|  | await new Promise((r) => setTimeout(r, 700)); | 
|  | const typed = await page.evaluate((sel) => { | 
|  | const el = document.querySelector(sel); | 
|  | if (!el) return ""; | 
|  | return el.tagName === "TEXTAREA" | 
|  | ? el.value \|\| "" | 
|  | : el.textContent \|\| ""; | 
|  | }, INPUT_SELECTOR); | 
|  | if (options.debug) { | 
|  | console.log("[debug] typed:", JSON.stringify(typed)); | 
|  | console.log("[debug] want :", JSON.stringify(message)); | 
|  | } | 
|  | if (typed.trim().length === 0) { | 
|  | throw new Error("composer empty after typing"); | 
|  | } | 
|  | // Click send button or press Enter | 
|  | const clicked = await page.evaluate(() => { | 
|  | const buttons = Array.from(document.querySelectorAll("button")); | 
|  | const match = buttons.find((b) => { | 
|  | const label = ( | 
|  | b.getAttribute("aria-label") \|\| | 
|  | b.getAttribute("title") \|\| | 
|  | b.textContent \|\| | 
|  | "" | 
|  | ).toLowerCase(); | 
|  | const disabled = | 
|  | b.disabled \|\| b.getAttribute("aria-disabled") === "true"; | 
|  | if (disabled) return false; | 
|  | return ( | 
|  | label.includes("send") \|\| | 
|  | label.includes("kirim") \|\| | 
|  | label.includes("submit") \|\| | 
|  | label.includes("ask") | 
|  | ); | 
|  | }); | 
|  | if (match) { | 
|  | match.click(); | 
|  | return true; | 
|  | } | 
|  | const submit = buttons.find( | 
|  | (b) => | 
|  | b.type === "submit" && | 
|  | !b.disabled && | 
|  | b.getAttribute("aria-disabled") !== "true" | 
|  | ); | 
|  | if (submit) { | 
|  | submit.click(); | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | }); | 
|  | if (!clicked) { | 
|  | await page.keyboard.press("Enter"); | 
|  | } | 
|  | // Wait for capture | 
|  | const deadline = Date.now() + (options.timeout \|\| DEFAULT_TIMEOUT); | 
|  | let capture = null; | 
|  | while (Date.now() < deadline) { | 
|  | const state = await page.evaluate((prev) => { | 
|  | const caps = window.__duckaiCaptures; | 
|  | if (caps.length <= prev) return null; | 
|  | const c = caps[caps.length - 1]; | 
|  | return { | 
|  | done: c.done, | 
|  | error: c.error, | 
|  | body: c.body, | 
|  | reqBody: c.reqBody, | 
|  | }; | 
|  | }, prevCount); | 
|  | if (state && state.done) { | 
|  | capture = state; | 
|  | break; | 
|  | } | 
|  | await new Promise((r) => setTimeout(r, 400)); | 
|  | } | 
|  | if (!capture) throw new Error("capture timeout"); | 
|  | if (options.debug && capture.reqBody) { | 
|  | const m = capture.reqBody.match(/"text":"((?:[^"\\]\|\\.){0,300})/); | 
|  | if (m) console.log("[debug] request text:", JSON.stringify(m[1])); | 
|  | } | 
|  | const events = parseSSE(capture.body); | 
|  | const result = { events, ...assembleResponse(events) }; | 
|  | if (result.images.length && options.saveImages !== false) { | 
|  | result.savedImages = saveImages(result, options.imageDir); | 
|  | } | 
|  | return result; | 
|  | } | 
|  | // ── Public API ── | 
|  | async function ask(message, options = {}) { | 
|  | const { browser, page } = await openDuck(options); | 
|  | try { | 
|  | return await sendChat(page, message, options); | 
|  | } finally { | 
|  | if (!options.keepOpen) { | 
|  | try { | 
|  | await browser.close(); | 
|  | } catch (_) {} | 
|  | } | 
|  | } | 
|  | } | 
|  | async function askMany(messages, options = {}) { | 
|  | const { browser, page } = await openDuck(options); | 
|  | const results = []; | 
|  | try { | 
|  | for (const msg of messages) { | 
|  | const r = await sendChat(page, msg, options); | 
|  | results.push({ message: msg, ...r }); | 
|  | await new Promise((res) => | 
|  | setTimeout(res, options.gapMs \|\| 1800) | 
|  | ); | 
|  | } | 
|  | return results; | 
|  | } finally { | 
|  | if (!options.keepOpen) { | 
|  | try { | 
|  | await browser.close(); | 
|  | } catch (_) {} | 
|  | } | 
|  | } | 
|  | } | 
|  | // ── CLI ── | 
|  | function printResult(r) { | 
|  | console.log("MODEL:", r.model \|\| "unknown"); | 
|  | console.log("─".repeat(50)); | 
|  | console.log(r.text \|\| "(empty)"); | 
|  | if (r.reasoning) { | 
|  | console.log("\n── reasoning ──"); | 
|  | console.log(r.reasoning); | 
|  | } | 
|  | if (r.sources && r.sources.length) { | 
|  | console.log("\n── sources ──"); | 
|  | for (const s of r.sources) { | 
|  | console.log(s.url \|\| s.title \|\| JSON.stringify(s)); | 
|  | } | 
|  | } | 
|  | if (r.toolCalls && r.toolCalls.length) { | 
|  | console.log("\n── tool calls ──"); | 
|  | for (const t of r.toolCalls) { | 
|  | console.log(t.name, JSON.stringify(t.args)); | 
|  | } | 
|  | } | 
|  | if (r.savedImages && r.savedImages.length) { | 
|  | console.log("\n── images ──"); | 
|  | for (const img of r.savedImages) { | 
|  | console.log("saved:", img.path); | 
|  | console.log( | 
|  | ` format=${img.format} size=${img.width}x${img.height} bytes=${img.bytes}` | 
|  | ); | 
|  | if (img.prompt) console.log(" prompt:", img.prompt); | 
|  | } | 
|  | } | 
|  | } | 
|  | async function main() { | 
|  | const args = process.argv.slice(2); | 
|  | const mode = args[0] \|\| "single"; | 
|  | const payload = args.slice(1).join(" ") \|\| "halo"; | 
|  | const headless = args.includes("--headless"); | 
|  | const debug = args.includes("--debug"); | 
|  | console.log("Duck.ai Scraper (Omegatech)"); | 
|  | console.log("─".repeat(50)); | 
|  | try { | 
|  | if (mode === "multi") { | 
|  | const list = payload | 
|  | .split("\|") | 
|  | .map((s) => s.trim()) | 
|  | .filter(Boolean); | 
|  | const results = await askMany(list, { headless, debug }); | 
|  | for (const r of results) { | 
|  | console.log("\n=== USER:", r.message); | 
|  | printResult(r); | 
|  | } | 
|  | return; | 
|  | } | 
|  | // default: single | 
|  | const message = mode === "single" ? payload : args.join(" ") \|\| "halo"; | 
|  | const r = await ask(message, { headless, debug }); | 
|  | printResult(r); | 
|  | } catch (err) { | 
|  | console.error("\n[error]", err && err.message ? err.message : err); | 
|  | process.exit(1); | 
|  | } | 
|  | } | 
|  | // Export for require() | 
|  | module.exports = { | 
|  | openDuck, | 
|  | sendChat, | 
|  | parseSSE, | 
|  | assembleResponse, | 
|  | saveImages, | 
|  | ask, | 
|  | askMany, | 
|  | }; | 
|  | // Run as CLI | 
|  | if (require.main === module) { | 
|  | main(); | 
|  | } |
