cd /news/ai-tools/this-is-an-unofficial-scraper-for-ht… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-151587] src=gist.github.com β†— pub= topic=ai-tools verified=true sentiment=Β· neutral

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).

A developer operating under the name Omegatech released duckai_cli.js, an unofficial Node.js command-line scraper for DuckDuckGo's Duck.ai chat service, which has no public API. The tool uses puppeteer-real-browser to drive a real browser session against the private POST /duckchat/v1/chat SSE endpoint, automatically clearing the site's bot challenge, and supports multi-turn text chat, model selection, reasoning/source/tool-call extraction, and base64 image-generation capture saved to disk. The author warns it is for educational and personal use only and that DuckDuckGo may change its frontend or challenge at any time.

by read15 min views13 publishedOct 16, 2026

| | // 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(); | 

| | } |

── more in #ai-tools 4 stories Β· sorted by recency
── more on @duck.ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/this-is-an-unofficia…] indexed:0 read:15min 2026-10-16 Β· β€”