const axios = require('axios');
const { JSDOM } = require('jsdom');
const { VM } = require('vm2');
const UserAgent = require('user-agents');
const { webcrypto } = require('crypto');
const subtle = webcrypto.subtle;
class DuckAI {
static MODELS = ['gpt-5-mini', 'gpt-4o-mini', 'openai/gpt-oss-120b', 'meta-llama/Llama-4-Scout-17B-16E-Instruct', 'claude-haiku-4-5', 'mistralai/Mistral-Small-24B-Instruct-2501'];
static _ua = null;
static _rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
static _sleep = ms => new Promise(r => setTimeout(r, ms));
constructor({ maxRetries = 5, useTools = false } = {}) {
this._maxRetries = maxRetries;
this._useTools = useTools;
this._vqd = null;
this._vqdAt = 0;
if (!DuckAI._ua) {
let s = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', p = 'Win32', vd = 'Google Inc.';
try { const u = new UserAgent({ deviceCategory: 'desktop', vendor: vd, platform: p }); if (u.toString().includes('Chrome')) { s = u.toString(); p = u.data?.platform || p; vd = u.data?.vendor || vd; } } catch {}
const v = (s.match(/Chrome\/(\d+)/) || [])[1] || '135';
DuckAI._ua = {
str: s,
platform: p,
vendor: vd,
secChUa: `"Google Chrome";v="${v}", "Not-A.Brand";v="8", "Chromium";v="${v}"`,
};
}
}
_headers(extra = {}) {
const { str, platform, secChUa } = DuckAI._ua;
return {
'accept-language': 'en-US,en;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'referer': 'https://duck.ai/',
'origin': 'https://duck.ai',
'cache-control': 'no-store',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'sec-ch-ua': secChUa,
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': `"${platform}"`,
'user-agent': str,
...extra,
};
}
async _getVqd(force = false) {
if (!force && this._vqd && Date.now() - this._vqdAt < 200_000) return this._vqd;
const res = await axios.get('https://duck.ai/duckchat/v1/status', {
headers: this._headers({ 'x-vqd-accept': '1' }),
});
const raw = res.headers['x-vqd-hash-1'];
if (!raw) throw new Error('VQD header missing');
this._vqd = await DuckAI._solveChallenge(raw);
this._vqdAt = Date.now();
return this._vqd;
}
static async _solveChallenge(raw) {
const STACK = ['Error', ' at l (https://duck.ai/dist/duckai-dist/entry.duckai.508538477be99c7fc13b8.js:2:1180630)', ' at async https://duck.ai/dist/duckai-dist/entry.duckai.508538477be99c7fc13b8.js:2:1063659'].join('\n');
let src;
try { src = Buffer.from(decodeURIComponent(raw), 'base64').toString('utf-8'); } catch { return raw; }
let obj = null;
try {
obj = await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('timeout')), 10_000);
let dom;
try {
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
url: 'https://duck.ai',
runScripts: 'dangerously',
pretendToBeVisual: true,
resources: 'usable',
});
} catch (e) {
clearTimeout(t);
return reject(e);
}
const win = dom.window;
for (const [k, v] of [['webdriver', false], ['plugins', { length: 3 }], ['languages', ['en-US', 'en']], ['hardwareConcurrency', 8], ['deviceMemory', 8]])
try {
Object.defineProperty(win.navigator, k, { get: () => v });
} catch {}
win.__resolve = r => { clearTimeout(t); dom.window.close(); resolve(r); };
win.__reject = e => { clearTimeout(t); dom.window.close(); reject(new Error(e)); };
win.chrome = { runtime: { id: undefined } };
try {
const el = win.document.createElement('script');
el.textContent = `(async()=>{try{window.__resolve(await(${src}))}catch(e){window.__reject(String(e?.message??e))}})()`;
win.document.head.appendChild(el);
} catch (e) {
clearTimeout(t);
dom.window.close();
reject(e);
}
});
if (!Array.isArray(obj?.server_hashes) || !obj.server_hashes.length) obj = null;
} catch {}
if (!obj) {
try {
const r = DuckAI._rand, ua = DuckAI._ua;
const sb = {
navigator: {
userAgent: ua.str,
webdriver: false,
languages: ['en-US', 'en'],
platform: ua.platform,
hardwareConcurrency: 8,
deviceMemory: 8,
maxTouchPoints: 0,
vendor: ua.vendor,
plugins: { length: 3 },
cookieEnabled: true,
},
document: {
createElement: () => ({
innerHTML: '',
children: { length: 0 },
appendChild: () => {},
removeChild: () => {},
addEventListener: () => {},
removeEventListener: () => {},
querySelectorAll: () => [],
}),
body: { appendChild: () => {}, removeChild: () => {}, children: { length: r(5, 12) }, onerror: null },
querySelectorAll: () => Object.assign([], { length: r(20, 60) }),
},
performance: { now: () => Date.now() },
location: { href: 'https://duck.ai', origin: 'https://duck.ai', hostname: 'duck.ai' },
chrome: { runtime: { id: undefined } },
crypto: {
subtle,
getRandomValues: a => { for (let i = 0; i < a.length; i++) a[i] = r(0, 255); return a; },
randomUUID: () => webcrypto.randomUUID(),
},
setTimeout: (fn, ms) => { setTimeout(fn, Math.min(ms || 0, 50)); return 1; },
clearTimeout: () => {},
atob: s => Buffer.from(s, 'base64').toString('binary'),
btoa: s => Buffer.from(s, 'binary').toString('base64'),
decodeURIComponent, encodeURIComponent,
Array, Object, String, Number, Boolean, Math, JSON, Symbol, Proxy, Promise, Map, Set, WeakMap,
Uint8Array, Int32Array, ArrayBuffer, DataView, parseInt, parseFloat, isNaN, isFinite,
Error: class extends globalThis.Error {
constructor(m) { super(m); this.stack = STACK; }
static captureStackTrace() {}
},
};
sb.window = sb; sb.self = sb; sb.top = sb; sb.globalThis = sb;
const res = await new VM({ timeout: 8000, sandbox: sb, eval: false, wasm: false }).run(`(async()=>await(${src}))()`);
if (Array.isArray(res?.server_hashes) && res.server_hashes.length) obj = res;
} catch {}
}
if (!obj) {
const hashes = [], re = /['"]([A-Za-z0-9+/]{40,64}={0,2})['"]/g; let m;
while ((m = re.exec(src))) hashes.push(m[1]);
obj = {
server_hashes: hashes.slice(0, 3),
client_hashes: [],
signals: {},
meta: {
v: '4',
challenge_id: src.match(/challenge_id['"]\s*[,)]\s*['"]\s*([^'"]{10,})['"]/)?.[1] || '',
timestamp: src.match(/'(\d{13})'/)?.[1] || String(Date.now()),
},
};
}
const clientHashes = await Promise.all((obj.client_hashes || []).map(async h => {
const hash = await subtle.digest('SHA-256', new TextEncoder().encode(h));
return Buffer.from(hash).toString('base64');
}));
return Buffer.from(JSON.stringify({
server_hashes: obj.server_hashes || [],
client_hashes: clientHashes,
signals: obj.signals || {},
meta: {
...(obj.meta || {}),
v: '4',
origin: 'https://duck.ai',
stack: STACK,
duration: String(DuckAI._rand(820, 1380)),
},
})).toString('base64');
}
async chat({ messages, model = 'gpt-4o-mini' } = {}) {
let attempt = 0, lastErr = null;
while (attempt <= this._maxRetries) {
try {
const vqd = await this._getVqd(attempt > 0);
await DuckAI._sleep(DuckAI._rand(200, 700));
const r = DuckAI._rand;
const signals = Buffer.from(JSON.stringify({
start: Date.now() - r(1800, 4000),
events: [
{ name: 'onboarding_impression', delta: r(300, 500) },
{ name: 'onboarding_finish', delta: r(9000, 12000) },
{ name: 'startNewChat_free', delta: r(9500, 13000) },
{ name: 'initSwitchModel', delta: r(11000, 15000) },
],
end: r(20000, 28000),
})).toString('base64');
const { data } = await axios.post('https://duck.ai/duckchat/v1/chat', {
model,
messages,
metadata: {
toolChoice: {
NewsSearch: this._useTools,
VideosSearch: false,
LocalSearch: false,
WeatherForecast: false,
},
},
canUseTools: this._useTools,
canUseApproxLocation: null,
}, {
headers: this._headers({
'accept': 'text/event-stream',
'content-type': 'application/json',
'x-fe-version': 'serp_20260311_192230_ET-a6d03d3dee4ffac4d95a950f0bb5590dcf8b187a',
'x-fe-signals': signals,
'x-vqd-hash-1': vqd,
}),
responseType: 'text'
});
const parsed = data.split('\n').filter(l => l.startsWith('data: {')).map(l => JSON.parse(l.slice(6)));
const content = parsed.filter(p => p.action === 'success' && p.role === 'assistant').map(p => p.message).join('');
const detectedModel = parsed.findLast(p => p.model)?.model ?? model;
return {
model: detectedModel,
content: content
};
} catch (err) {
lastErr = err;
if (err.name === 'AbortError' || err.name === 'CanceledError') throw err;
if (attempt >= this._maxRetries) break;
if (err.response?.status === 418) {
await DuckAI._sleep(DuckAI._rand(4000, 8000));
await this._getVqd(true);
}
console.error(`[DuckAI] Attempt ${attempt + 1} failed:`, err.message);
await DuckAI._sleep(Math.min(2000 * Math.pow(2, attempt) + DuckAI._rand(200, 800), 30_000));
attempt++;
}
}
throw lastErr;
}
}
const duck = new DuckAI();
duck.chat({
messages: [{
role: 'user',
content: 'hi'
}],
model: 'claude-haiku-4-5'
}).then(console.log);