Contact Forms

connecting…
Run
Per-person messages
Project & proxies
Proxies & cost
All settings
Glossary

Tell it what you want

What runs where ?

Contact-finding happens in three stages. You can put proxies on any of them, independently, from the Project & proxies tab.

StageWhat it doesProxies matter
Scraping Asks search engines for candidate sites highest risk
Parsing Loads each site, finds its contact form medium
Sending Fills the form and submits your message medium
// for a subdomain split. Set it to "" only if the API is at the exact origin root. const API = (window.PANEL_API_BASE ?? "/api").replace(/\/$/, ""); const LOGIN_URL = API + "/login"; let SCHEMA = [], VALUES = {}, DEFAULTS = {}, MISSING = {}, dirty = {}, PROJECTS = [], CUR_PROJECT = null; let GLOSS = {}, GLOSS_ALIAS = {}, GLOSS_TERMS = [], GLOSS_RE = null; const $ = id => document.getElementById(id); const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); async function api(path, opts) { const r = await fetch(API + path, Object.assign({credentials:'include', headers:{'Content-Type':'application/json'}}, opts)); if (r.status === 401 && !location.pathname.startsWith("/login")) { location.href = LOGIN_URL; // expired or missing session throw new Error("session expired"); } const t = await r.text(); let j; try { j = t ? JSON.parse(t) : {}; } catch { j = {raw:t}; } if (!r.ok) throw new Error(j.detail || j.raw || r.statusText); return j; } function showTab(name){ for (const t of document.querySelectorAll(".tab")) t.classList.toggle("active", t.dataset.tab === name); for (const id of ["run","people","project","sizing","settings","glossary"]) $("tab-" + id).classList.toggle("hidden", id !== name); if (name === "sizing" && !SZ_TIERS.length) loadSizing(); if (name === "glossary") renderGlossary(); } /* ---------------------------------------------------------------- glossary */ async function loadGlossary(){ try { const g = await api("/glossary"); GLOSS_TERMS = g.terms; for (const t of g.terms) GLOSS[t.key.toLowerCase()] = t; GLOSS_ALIAS = g.index; // longest first so "usable thread" wins over "thread" const alts = Object.keys(GLOSS_ALIAS).filter(k => k.length > 2) .sort((a,b) => b.length - a.length) .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); GLOSS_RE = new RegExp("(?${extra || "?"}`; } /* turn known jargon inside prose into hoverable spans */ function linkify(root){ if (!GLOSS_RE) return; for (const el of root.querySelectorAll(".glossy")) { if (el.dataset.linked === "1") continue; const raw = el.textContent; el.innerHTML = esc(raw).replace(GLOSS_RE, (m) => { const e = entryFor(m); return e ? `${m}` : m; }); el.dataset.linked = "1"; } } function renderGlossary(){ const q = ($("glossFilter").value || "").toLowerCase(); const list = GLOSS_TERMS.filter(t => !q || t.key.includes(q) || t.short.toLowerCase().includes(q)); $("glossList").innerHTML = list.map(t => `
${esc(t.title || t.key)}${esc(t.short)}
`).join("") || `
no match
`; } /* floating bubble, driven entirely by the backend glossary */ let bubOn = false; function showBubble(term, anchor){ const e = entryFor(term); if (!e) return; const b = $("bubble"); b.innerHTML = `
${esc(e.title || e.key)}
` + `
${esc(e.short)}
` + (e.long ? `
${esc(e.long)}
` : "") + (e.tip ? `
${esc(e.tip)}
` : ""); b.classList.add("on"); bubOn = true; const r = anchor.getBoundingClientRect(); const bw = b.offsetWidth, bh = b.offsetHeight; let left = r.left; let top = r.bottom + 10; if (left + bw > innerWidth - 12) left = innerWidth - bw - 12; if (left < 12) left = 12; if (top + bh > innerHeight - 12) top = r.top - bh - 10; if (top < 12) top = 12; b.style.left = left + "px"; b.style.top = top + "px"; } function hideBubble(){ if (bubOn) { $("bubble").classList.remove("on"); bubOn = false; } } document.addEventListener("mouseover", ev => { const t = ev.target.closest("[data-gloss]"); if (t) showBubble(t.dataset.gloss, t); else hideBubble(); }); document.addEventListener("mouseout", ev => { if (ev.target.closest("[data-gloss]")) hideBubble(); }); /* ---------------------------------------------------------------- load */ async function loadAll(){ try { const h = await api("/health"); $("status").className = "pill " + (h.app_running ? "on" : "off"); $("status").textContent = h.app_running ? "app running" : "app stopped"; $("health").innerHTML = `
config: ${esc(h.config_path)}
exists: ${h.config_exists}
pids: ${(h.app_pids||[]).join(", ") || "—"}
`; } catch(e){ $("status").className="pill off"; $("status").textContent="api offline"; } try { const e = await api("/nl/engine"); $("engine").textContent = e.llm_configured ? ("llm: " + e.model) : "llm: rules fallback"; } catch {} try { const d = await api("/settings/defaults"); DEFAULTS = d.defaults; MISSING = d.missing; } catch {} try { SCHEMA = (await api("/schema")).fields; VALUES = (await api("/settings")).values; renderFields(); } catch(e){ $("fields").innerHTML = `
${esc(e.message)}
`; } try { PROJECTS = (await api("/projects")).projects; const opts = PROJECTS.map(p => ``).join(""); $("projSel").innerHTML = opts || ``; $("pplProject").innerHTML = opts || ``; if (PROJECTS.length) loadProject(); } catch(e){ $("projOut").innerHTML = `
${esc(e.message)}
`; } linkify(document); } /* ---------------------------------------------------------------- settings grid */ function renderFields(){ const groups = {}; for (const f of SCHEMA) (groups[f.group] ||= []).push(f); const order = ["performance","captcha","proxies","filtering","scheduler","notifications","interface","misc","columns"]; const keys = Object.keys(groups).sort((a,b)=>order.indexOf(a)-order.indexOf(b)); $("fldCount").textContent = `— ${SCHEMA.length} fields`; $("fields").innerHTML = keys.map(g => `
${esc(g)} ${groups[g].length} ${groups[g].map(renderOne).join("")}
`).join(""); linkify($("fields")); } function curValue(f){ const [sec,key] = f.ref.split("::"); const v = (VALUES[sec]||{})[key]; return (v === undefined || v === null) ? DEFAULTS[f.ref] : v; } function renderOne(f){ const val = curValue(f); const id = "f_" + btoa(f.ref).replace(/[^a-zA-Z0-9]/g,""); const tags = `${esc(f.confidence)}` + (f.essential ? ` key` : "") + (f.advanced ? ` advanced` : ""); const label = ``; let ctl; if (f.type === "bool") { ctl = `
`; } else if (f.type === "int") { ctl = `
${f.unit?`${esc(f.unit)}`:""} ${f.allowed?`${f.allowed.map(a=>``:""}
`; } else { ctl = `
`; } return `
${label}${ctl}
`; } function set(ref,val){ dirty[ref]=val; stage(); } function toggle(ref,el){ const on = !el.classList.contains("on"); el.classList.toggle("on", on); dirty[ref] = on; stage(); } function stage(){ const n = Object.keys(dirty).length; $("stageMsg").textContent = n ? `${n} edit(s) staged` : "every field is editable"; $("saveFields").disabled = !n; } function fillDefaults(all){ const src = all ? DEFAULTS : MISSING; let n = 0; for (const [ref,val] of Object.entries(src)) { dirty[ref]=val; n++; } for (const [ref,val] of Object.entries(src)) { const f = SCHEMA.find(x=>x.ref===ref); if(!f) continue; VALUES[f.section] ||= {}; VALUES[f.section][f.key] = val; } renderFields(); stage(); $("stageMsg").textContent = `${n} value(s) staged — nothing written yet`; } /* ---------------------------------------------------------------- nl */ function planNL(apply){ return plan(apply); } async function plan(apply){ const text = $("nl").value.trim(); if (!text && !Object.keys(dirty).length) return; $("nlMsg").textContent = "thinking…"; $("diff").innerHTML = ""; try { const res = await api("/nl/plan", {method:"POST", body: JSON.stringify({ text, prefer:"auto", apply: !!apply })}); let html = ""; if (res.explanation) html += `
${esc(res.explanation)}
`; html += `
` + (res.changes.length ? res.changes.map(c=>`
[${esc(c.section)}] ${esc(c.key)}: ${esc(c.old)} → ${esc(c.new)}
`).join("") : `
no changes
`) + `
`; if (res.unknown?.length) html += `
unknown: ${res.unknown.map(esc).join(", ")}
`; if (res.notes?.length) html += `
${res.notes.map(esc).join("
")}
`; if (res.applied) html += `
applied ✓ verified=${res.applied.verified} bounced=${res.applied.bounced.length}
`; $("diff").innerHTML = html; $("applyBtn").disabled = !res.updates || Object.keys(res.updates).length===0; $("applyBtn").dataset.updates = JSON.stringify(res.updates||"{}"); $("nlMsg").textContent = apply ? "done" : `${res.count} change(s) proposed`; if (apply) { dirty={}; stage(); loadAll(); } } catch(e){ $("nlMsg").textContent="error"; $("diff").innerHTML=`
${esc(e.message)}
`; } } async function applyStaged(){ const updates = JSON.parse($("applyBtn").dataset.updates || "{}"); const merged = Object.assign({}, updates, dirty); try { const res = await api("/settings/apply", {method:"POST", body: JSON.stringify({updates:merged, apply:true})}); $("stageMsg").textContent = res.verified ? `saved ✓ ${res.changed.length} change(s)` + (res.added?.length ? ` (${res.added.length} new)` : "") : `saved with ${res.bounced.length} bounce(s)`; dirty = {}; $("saveFields").disabled = true; loadAll(); } catch(e){ $("stageMsg").textContent = "error"; $("diff").innerHTML += `
${esc(e.message)}
`; } } /* ---------------------------------------------------------------- project */ async function loadProject(){ const name = $("projSel").value; if (!name) return; CUR_PROJECT = name; try { const p = await api("/project?name=" + encodeURIComponent(name)); const roles = ["proxies for se","proxies for parse","proxies for sending"]; const pretty = { "proxies for se": `Search engines ${dot("scraping")}`, "proxies for parse": `Visiting / parsing sites ${dot("parsing")}`, "proxies for sending": `Submitting forms ${dot("sending")}`}; $("projStat").textContent = `${p.search_engines_enabled}/${p.search_engines_total} search engines on · ${p.keywords.length} keyword(s)`; $("projOut").innerHTML = `
Proxies live here, not in config.ini. Turning these on lets you raise threads without Google banning your home IP. Right now all three are off, so the proxy pool you already have is unused.
${roles.map(r => `
`).join("")}
Proxy type per role ${dot("proven vs inferred")}
${roles.map(r => `
`).join("")}
type ids: only 3 is proven from your file; others are inferred — verify once by hand.
Keywords
${p.keywords.map(k=>`${esc(k.name)}`).join("") || `none`}
`; } catch(e){ $("projOut").innerHTML = `
${esc(e.message)}
`; } } let projDirty = {}, kwDirty = null; function markProj(){ $("projSave").disabled = false; $("projMsg").textContent = "unsaved"; } function toggleProj(role, el){ const on = !el.classList.contains("on"); el.classList.toggle("on", on); projDirty["misc::"+role] = on; markProj(); } function setProj(key, val){ projDirty["misc::"+key] = Number(val); markProj(); } function kwList(){ if (kwDirty === null) { const names = [...document.querySelectorAll("#kwBox .kw")].map(e=>e.textContent.replace("×","").trim()); kwDirty = names; } return kwDirty; } function addKw(){ const v = $("newKw").value.trim(); if(!v) return; kwDirty = kwList().concat([v]); $("newKw").value = ""; renderKw(); markProj(); } function rmKw(name){ kwDirty = kwList().filter(k=>k!==name); renderKw(); markProj(); } function renderKw(){ $("kwBox").innerHTML = kwDirty.map(k=>`${esc(k)}`).join("") || `none`; } async function saveProject(){ const body = {name: CUR_PROJECT, updates: projDirty, apply: true}; if (kwDirty !== null) body.keywords = kwDirty; try { const res = await api("/project/apply", {method:"POST", body: JSON.stringify(body)}); $("projMsg").innerHTML = `saved ✓ ${res.changed.length} change(s), verified=${res.verified}`; projDirty = {}; kwDirty = null; setTimeout(() => { $("projSave").disabled = true; loadProject(); }, 900); } catch(e){ $("projMsg").innerHTML = `${esc(e.message)}`; } } /* ---------------------------------------------------------------- per-person import */ let importPlan = null; async function previewImport(){ const text = $("ppl").value.trim(); if (!text) { $("pplOut").innerHTML = `
paste some rows first
`; return; } try { const p = await api("/import/plan", {method:"POST", body: JSON.stringify({text})}); importPlan = p; $("pplOut").innerHTML = `
${p.row_count} row(s) · URL column ${esc(p.url_header)} ${p.message_header ? `· message column ${esc(p.message_header)} → macro ${esc(p.message_macro)}` : ""}
${p.notes.map(n=>`
${esc(n)}
`).join("")}
${esc(p.headers.join(" | "))}
${p.preview.map(r=>`
${p.headers.map(h=>esc((r[h]||"").slice(0,42))).join(" | ")}
`).join("")}
${p.message_header ? "" : `
No message column — every site would get the same text.
`}`; $("pplApply").disabled = !p.message_header; } catch(e){ $("pplOut").innerHTML = `
${esc(e.message)}
`; } } async function applyImport(wire){ const text = $("ppl").value.trim(); try { const res = await api("/import/apply", {method:"POST", body: JSON.stringify({ text, save_name: $("pplName").value.trim() || "per person import", project: $("pplProject").value, apply_project: wire, delete_after: true })}); $("pplOut").innerHTML = `
saved ${res.row_count} row(s) → ${esc(res.csv_path)}
${res.project_apply ? `
project ${esc(res.project)} wired to this file (import on, delete-after on)
` : ""} ${res.message_instructions ? `
Last step: put this in the project's message body so each row's text is used:
${esc(res.message_instructions.body)}
` : ""}`; } catch(e){ $("pplOut").innerHTML = `
${esc(e.message)}
`; } } /* ---------------------------------------------------------------- sizing */ let SZ_TIERS = []; async function loadSizing(){ const vol = Number($("szVol").value || 1000); loadIspPrice(Number($("ispCount").value || 40)); loadTinyfish(); loadFarm(3); loadFarmSafe(); loadCapacity(); loadProxyBuy(); loadRotating(); loadIpnux(); loadCheap(); loadSplit(); loadStages(); loadSpend(); loadMinBudget(); loadImported(); loadGrade(); loadFallback(); loadProviders(); loadSteps(vol); loadLayer2(vol); try { const p = await api("/sizing/proxies?tier=" + encodeURIComponent($("szTier").value || "free")); if (!SZ_TIERS.length) { SZ_TIERS = p.tiers; $("szTier").innerHTML = p.tiers.map(t => ``).join(""); const rec = p.tiers.find(t => t.recommend); if (rec) $("szTier").value = rec.key; } $("szSrc").textContent = `${p.sources.enabled}/${p.sources.total} built-in proxy sources on`; $("szSrc").innerHTML = `${p.sources.enabled}/${p.sources.total} built-in proxy sources on` + ` ${dot("pool entry")}`; const t = p.tiers.find(x => x.key === p.tier); $("szProxies").innerHTML = `
${esc(t.label)} — ${esc(t.quality)}
${esc(t.cost_note)}
${p.rows.map(r => ``).join("")}
Sends/dayThreads Pool entries needed Live at once Distinct IPs/day
${r.daily_sends.toLocaleString()}${r.threads} ${r.pool_entries_needed.toLocaleString()}${r.live_at_once_needed} ${r.distinct_ips_per_day.toLocaleString()}
A usable thread is a thread that always has a live proxy behind it — that is why the pool is sized at ${Math.round(p.assumptions.proxy_live_fraction*100)}% usable (${p.assumptions.page_requests_per_site} requests/site). ${esc(p.assumptions.note)}
`; szSources(p.sources); } catch(e){ $("szProxies").innerHTML = `
${esc(e.message)}
`; } try { const c = await api("/sizing/captcha?daily_sends=" + vol); const L = c.stack.layers; const line = (k,label) => L[k] ? `${label}${esc(L[k].name)} ${L[k].model}$${L[k].est_monthly_usd}/mo` : ""; $("szCaptcha").innerHTML = `
~${c.solves_per_day} captcha solves/day · ~${c.solves_per_month}/mo
Recommended layered stack (coverage per dollar):
${line("primary","Primary solver")} ${line("backup","Backup (independent vendor)")} ${line("text_ocr_free","Free text/image solver")} ${line("text_ocr_cheap","Cheap text solver")} ${line("ai_optional","AI solver (optional)")}
LayerServiceModelEst. cost
${esc(c.stack.why)}
If nothing is gated by captcha, cost is $0 — set the rate to 0 and skip this entirely.
All ${c.services.length} shipped services with prices ${c.services.map(s=>``).join("")}
ServiceRatingModel$/mo at your volumeBroad
${esc(s.name)}${s.rating ?? "—"}${esc(s.model)} $${s.est_monthly_usd}${s.broad_coverage?"✓":""}
`; } catch(e){ $("szCaptcha").innerHTML = `
${esc(e.message)}
`; } } async function loadSplit(){ try { const sends = Math.max(100, Number($("stSends").value || 5000)); const pp = Math.min(40, Math.max(1, Number($("stDelta").value || 10))); const d = await api(`/sizing/split-test?daily_sends=${sends}&detect_pp=${pp}`); const ss = d.sample_size; $("stOut").innerHTML = `
${esc(d.headline)}
Why captcha cannot be held constant
${esc(d.the_core_insight.why_captcha_cannot_be_constant)}
${esc(d.the_core_insight.so)}
${esc(d.the_core_insight.and)}
The design
Type${esc(d.design.name)}
Unit${esc(d.design.unit_of_assignment)}
Balanced on${esc(d.design.stratification_variable)}
Randomisation${esc(d.design.randomisation)}
Optional pairing${esc(d.design.paired_variant)}
Why not two plain lists${esc(d.design.why_not_two_plain_lists)}
Primary metric: ${esc(d.primary_metric.name)} — ${esc(d.primary_metric.definition)}
Not the metric: ${esc(d.primary_metric.explicitly_not_the_metric)}
Attribute failures — do not collapse them
${d.failure_taxonomy.map(t => ``).join("")}
CodeAttributable toNote
${esc(t.code)}${esc(t.attributable_to)}${esc(t.note)}
${esc(d.taxonomy_note)}
Sample size (to detect a ${ss.detect_pp}pp gap, base ${ss.pooled_baseline_pct}%)
${ss.per_stratum.map(s => ``).join("")}
StratumSharen per arm
${esc(s["class"])}${s.share_pct}%${s.n_per_arm}
Minimum total (both arms)${ss.minimum_total_attempts}
With A/A floor + canary + dropout${ss.recommended_total_with_safety}
Days at this volume${ss.days_at_daily_volume}
${esc(ss.read)}
${esc(ss.warning)}
Hold identical (or you are testing the wrong thing)
Deliberately varied: ${esc(d.deliberately_varied[0])}
Confounds to kill
${d.confounds_to_kill.map(c => ``).join("")}
ConfoundWhy it ruins the testFix
${esc(c.confound)}${esc(c.why_it_ruins_it)}${esc(c.fix)}
Controls
Run it in GSA
    ${d.gsa_implementation.map(x => `
  1. ${esc(x)}
  2. `).join("")}
Decision rule
What this still does NOT prove
`; linkify($("stOut")); await loadProc(sends, pp); } catch(e){ $("stOut").innerHTML = `
${esc(e.message)}
`; } } async function loadMinBudget(){ try { const d = await api(`/sizing/minimum-test?daily_sends=5000`); const pick = d.cheapest_defensible_pick; $("mbOut").innerHTML = `
${esc(d.headline)}
Two knobs people conflate
Correctness knob${esc(d.the_two_knobs_people_conflate.correctness_knob)}
Budget knob${esc(d.the_two_knobs_people_conflate.budget_knob)}
${esc(d.the_two_knobs_people_conflate.the_insight)}
${esc(d.the_two_knobs_people_conflate.so)}
${esc(d.the_two_knobs_people_conflate.false_step)}
${d.tiers.map(t => ``).join("")}
Tier$/moAttemptsIPs/armThreads~RuntimeWhat it proves / risk
${esc(t.tier)}
${esc(t.arms)}
$${t.usd_month.toFixed(2)}
${esc(t.breakdown.clean_arm)}
${t.attempts_total_with_safety}${t.ips_per_arm}${t.threads} ~${t.approx_runtime_minutes} min ${esc(t.what_it_proves)}
Risk: ${esc(t.risk)}
Hard floor: ${esc(d.hard_floor_note)}
Pick: ${esc(pick.take)} ($ ${d.recommended.usd_month.toFixed(2)})
${esc(pick.why)}
${esc(pick.true_floor)}
What is NOT in this budget
How to cut it further
`; linkify($("mbOut")); } catch(e){ $("mbOut").innerHTML = `
${esc(e.message)}
`; } } async function loadSpend(){ try { const sends = Math.max(100, Number($("spSends").value || 5000)); const d = await api(`/sizing/spend?daily_sends=${sends}`); const t = d.totals; const row = l => ` ${esc(l.bucket)}
${esc(l.item)} ${l.usd_month === 0 ? "$0" : "$" + l.usd_month.toFixed(2)}
${esc(l.scale)} ${esc(l.buys)} ${esc(l.why_here)} If skipped: ${esc(l.without_it)}
Cheaper: ${esc(l.cheap_alternative)} ${esc(l.confidence)}`; $("spOut").innerHTML = `
${esc(d.the_one_sentence)}
${esc(d.headline)}
${d.lines.map(row).join("")}
Bucket$/moBuysWhy hereTrade-offConfidence
Recurring$${t.recurring_monthly.toFixed(2)}
One-off test$${t.one_off_test.toFixed(2)}
First month all-in$${t.all_in_first_month.toFixed(2)} $${t.per_day}/day · $${t.per_send} per send
How to read it: ${esc(d.how_to_read_this.cost_driver)}
${esc(d.how_to_read_this.where_the_test_pays)}
Marginal value per dollar
${d.what_you_get_for_each_dollar.map(x => ``).join("")}
BucketMarginal value
${esc(x.bucket)}${esc(x.marginal_value)}
Cheapest sane (~$${d.cheapest_sane_version.rough_monthly}/mo)
${esc(d.cheapest_sane_version.how)}
${esc(d.cheapest_sane_version.trade)}
Premium (~$${d.premium_version.rough_monthly}/mo)
${esc(d.premium_version.how)}
${esc(d.premium_version.trade)}
Assumption ledger — what is proven vs guessed
Proven volumes: ${d.assumption_ledger.proven_volumes.map(esc).join(", ")}
Labelled rates: ${d.assumption_ledger.labelled_rates.map(esc).join(", ")}
Published prices: ${d.assumption_ledger.published_prices.map(esc).join(", ")}
${esc(d.assumption_ledger.fix_rule)}
`; linkify($("spOut")); } catch(e){ $("spOut").innerHTML = `
${esc(e.message)}
`; } } async function loadStages(){ try { const d = await api(`/sizing/stages?daily_sends=5000`); const proof = d.the_setting_that_proves_it; $("sgOut").innerHTML = `
${esc(d.straight_answer)}
Three hops happen per submission. The service covers ONE.
${esc(d.the_two_hops_they_cannot_do.why_it_matters)}
Stage by stage
${d.stage_by_stage.map(s => ``).join("")}
StageNeeds our proxyGrade neededDo captchas help
${esc(s.stage)}
${esc(s.why)}
${esc(s.keep_separate)}
${esc(s.needs_our_proxy)} ${esc(s.grade_needed)} ${esc(s.captcha_services_help)}
${esc(d.the_real_risk_they_do_not_remove.insight)}
${esc(d.the_real_risk_they_do_not_remove.detail)}
${esc(d.the_real_risk_they_do_not_remove.consequence)}
The setting that makes this concrete: ${esc(proof.key)} (live value ${proof.live_value})
${esc(proof.what_it_decides)}
${esc(proof.the_trap)}
The hybrid that maximises value
${esc(d.the_hybrid_that_maximises_value.why)}
${esc(d.the_hybrid_that_maximises_value.saving)}
Do parse proxies need to be good? ${esc(d.does_the_parse_pool_need_to_be_good.answer)}
${esc(d.does_the_parse_pool_need_to_be_good.reason)}
${esc(d.does_the_parse_pool_need_to_be_good.one_caveat)}
Still needed at full scale: ${esc(d.what_is_still_needed_1_to_1.point)} ${esc(d.what_is_still_needed_1_to_1.why)}
`; linkify($("sgOut")); } catch(e){ $("sgOut").innerHTML = `
${esc(e.message)}
`; } } async function loadProc(sends, pp){ try { const d = await api(`/sizing/procurement?daily_sends=${sends}&detect_pp=${pp}`); $("prOut").innerHTML = `

What to buy — and how to send it to me

${esc(d.headline)}
${esc(d.key_point)}
${d.arms.map(a => ``).join("")}
ArmWhatVendorIPs$/mo
${esc(a.arm)} ${esc(a.label)} ${esc(a.vendor)}
${esc(a.why_this_vendor)}
${a.ips}${a.monthly === 0 ? "$0" : "$" + a.monthly.toFixed(2)}
Estimated test cost$${d.estimated_test_cost_per_month.toFixed(2)}
Why so small? ${esc(d.scale_check.why_small)}
Attempts needed: ${d.scale_check.test_attempts_needed} · threads: ${d.scale_check.threads_for_test} · IPs per arm: ${d.scale_check.ips_per_arm}
How to get each one
Where to send them: ${esc(d.handover.where)}
${esc(d.handover.format)}
${esc(d.handover.example_line)}
${esc(d.handover.then)}
Then I do this automatically
    ${d.what_i_do_when_you_hand_them_over.map(x => `
  1. ${esc(x)}
  2. `).join("")}
Watch these when buying
`; linkify($("prOut")); } catch(e){ $("prOut").innerHTML = `
${esc(e.message)}
`; } } async function loadCheap(){ try { const sends = Math.max(100, Number($("cheapSends").value || 5000)); const d = await api(`/sizing/cheap-proxies?daily_sends=${sends}`); const b = d.breakeven; $("cheapOut").innerHTML = `
${esc(d.straight_answer)}
${esc(d.the_dominant_asymmetry.insight)}
${esc(d.the_dominant_asymmetry.consequence)}
Where the loss actually lands — ${esc(d.loss_model.assumption_warning)}
${d.loss_model.by_site_class.map(c => ``).join("")}
Site classShareCheap lossWhy
${esc(c["class"])}
${esc(c.what)}
${c.share_pct}% ${c.cheap_loss_pct === 0 ? "0%" : c.cheap_loss_pct + "%"} ${esc(c.why)}
Blended: ${d.loss_model.blended_loss_pct}% from the datacenter penalty, plus ${d.loss_model.shared_extra_pct}% if the pool is ALSO shared. Both fixes stack.
The trap: two axes, not one
${esc(d.two_axes_do_not_collapse_them.the_trap)}
${esc(d.two_axes_do_not_collapse_them.note)}
Options, with the loss attached
${d.options_ranked.map(o => ``).join("")}
Option$/moLoss vs dedicated ISPUseless?
${esc(o.option)}${o.monthly === 0 ? "$0" : "$" + o.monthly.toFixed(2)} ${o.loss_pct}%${esc(o.useless)}
Breakeven: ${esc(b.read)}
${esc(b.conclusion)}
Fix this before buying anything. ${esc(d.cost_of_getting_it_wrong.not_recoverable)}
${esc(d.cost_of_getting_it_wrong.fix_first)}
The smart compromise — split by stage
Send pool${esc(d.the_smart_compromise.send_pool)}
Parse pool${esc(d.the_smart_compromise.parse_pool)}
${esc(d.the_smart_compromise.why)}
Cheapest non-stupid setup:
How to replace my guesses with your own number
${esc(d.what_would_actually_settle_it.test)}
${esc(d.what_would_actually_settle_it.cost)}
`; linkify($("cheapOut")); } catch(e){ $("cheapOut").innerHTML = `
${esc(e.message)}
`; } } async function loadIpnux(){ try { const n = Math.max(1, Number($("ipxN").value || 33)); const d = await api(`/sizing/ipnux?ips=${n}`); const h = d.headline_problem, c = d.cost_impact; $("ipxOut").innerHTML = `
Their page says $${h.marketing_says_usd_per_ip.toFixed(2)}/IP. The checkout quoted $${h.checkout_quoted_usd_per_ip.toFixed(2)}/IP — ${h.multiple}x.
${esc(h.read_it_as)}
Why: ${esc(d.which_tier_for_sending.why)}
${d.the_quality_selector.map(t => ` `).join("")}
Tier on the pageFrom $/IPWhat it claimsFor sending
${esc(t.name)}${t.name==="Premium"?" ← pre-selected":""} $${t.from_usd_per_ip.toFixed(2)}${esc(t.desc)} ${esc(t.verdict_for_sending)}
Pick: ${esc(d.which_tier_for_sending.pick)}
Unless: ${esc(d.which_tier_for_sending.unless)}
What this costs you at ${d.ips} IPs
${c.comparison.map(r => ``).join("")}
OptionMonthly
${esc(r.option)}$${r.monthly_usd.toFixed(2)}
Downgrading the tier saves $${c.saving_from_downgrading_tier.toFixed(2)}/month. Comparing vendors is worth another $${c.saving_from_switching_vendor.toFixed(2)}/month.
Checkout hygiene
${esc(d.bottom_line)}
${esc(d.honesty)}
`; linkify($("ipxOut")); } catch(e){ $("ipxOut").innerHTML = `
${esc(e.message)}
`; } } async function loadRotating(){ try { const sends = Math.max(100, Number($("rotSends").value || 5000)); const kb = Math.max(10, Number($("rotKb").value || 80)); const d = await api(`/sizing/rotating-residential?daily_sends=${sends}&kb_per_page=${kb}`); const c = d.cost_math, cmp = d.the_comparison; $("rotOut").innerHTML = `
StageVerdict
Parsing${esc(d.would_they_work.for_parsing)}
Sending${esc(d.would_they_work.for_sending)}
Why sending breaks: ${esc(d.the_sticky_problem.why)}
${esc(d.the_sticky_problem.and_gsa_cannot_fix_it)}
Their own wording: ${esc(d.the_sticky_problem.their_wording)}
The workaround
${esc(d.the_workaround.idea)}
${esc(d.the_workaround.why_it_works)}
Caveat: ${esc(d.the_workaround.caveat)}
Cost at ${d.daily_sends.toLocaleString()} forms/day — this is where it loses
MeasureValue
Traffic (@${c.assumed_kb_per_page} KB/page, ${c.assumed_pages_per_site} pages/site)${c.gb_per_month_total} GB/month
Rotating residential @ realistic rate$${c.monthly_cost_low}–$${c.monthly_cost_high}/mo
Dedicated static ISP instead$${cmp.dedicated_monthly}/mo for ${cmp.dedicated_ips} IPs
${esc(c.why_not_headline)}
Verdict: ${esc(cmp.verdict)}
${esc(cmp.why)}
When they ARE the right buy
Use forBuy
Sending${esc(d.recommendation.sending)}
Parsing${esc(d.recommendation.parsing)}
Assumptions: ${d.assumptions.map(esc).join(" · ")}
`; linkify($("rotOut")); } catch(e){ $("rotOut").innerHTML = `
${esc(e.message)}
`; } } async function loadProxyBuy(){ try { const sends = Math.max(100, Number($("pxSends").value || 5000)); const d = await api(`/sizing/proxies-for?daily_sends=${sends}`); $("pxOut").innerHTML = `
${esc(d.recommended_type.grade)} — not ${esc(d.recommended_type.not)}.
${esc(d.recommended_type.why)}
Threads at ${d.daily_sends.toLocaleString()}/day
${d.threads}
IPs to actually buy
${d.ips_to_actually_buy}
1:1 with threads + 10% spare
${esc(d.spare_reasoning)}
${d.vendors_ranked.map(v => ``).join("")}
Vendor$/IP~Monthly at ${d.ips_to_actually_buy}Why
#${v.rank} ${esc(v.name)}$${v.usd_per_ip} ~$${v.monthly_total}${esc(v.why)}
Catch: ${esc(v.catch)}
What NOT to buy, and why
${d.what_not_to_buy.map(w => ``).join("")}
OptionVerdictWhy
${esc(w.option)}${esc(w.verdict)}${esc(w.why)}
Reality check: ${esc(d.the_reality_check)}
Tiered option: ${esc(d.a_tiered_option.split)}
${esc(d.a_tiered_option.why)}
Next steps
    ${d.next_steps.map(s => `
  1. ${esc(s)}
  2. `).join("")}
Assumptions: ${d.assumptions.map(esc).join(" · ")}
`; linkify($("pxOut")); } catch(e){ $("pxOut").innerHTML = `
${esc(e.message)}
`; } } async function loadCapacity(){ try { const sends = Math.max(100, Number($("capSends").value || 10000)); const found = Math.max(5, Math.min(100, Number($("capFound").value || 60))); const d = await api(`/sizing/capacity?daily_sends=${sends}&found_pct=${found}`); const rec = d.the_actual_answer.if_recurring_daily; const one = d.the_actual_answer.if_one_off_list; $("capOut").innerHTML = `
${esc(d.framing)}
Recurring daily ${d.daily_sends.toLocaleString()} submits
${rec.accounts} accounts
the daily allowance binds — ${esc(rec.time_to_clear_each_day)} of fetching
One-off list of ${d.parse_urls_per_day.toLocaleString()} URLs
${one.accounts} account${one.accounts !== 1 ? "s" : ""}
only speed binds here
${esc(d.the_actual_answer.warning)}
${esc(d.the_actual_answer.if_recurring_daily.because)}
QuantityValue
Forms submitted / day${d.daily_sends.toLocaleString()}
Sites parsed / day @ ${d.found_pct}% find-rate${d.parse_urls_per_day.toLocaleString()}
Sending threads${d.threads_recommended}
Clean sending IPs (dedicated)${d.proxy_by_tier.dedicated_dc.pool_entries_needed}
Captcha solves / day${d.captcha.solves_per_day.toLocaleString()} (~$${d.captcha.cost_per_day_usd}/day)
If you spread it across days instead
${Object.entries(d.spread_table).map(([k,v]) => ``).join("")}
AccountsDays to clearBack-to-back fetch time
${esc(k.replace("_"," "))}${v.days_to_clear}${esc(v.wall_clock_if_back_to_back)}
The real limits:
`; linkify($("capOut")); } catch(e){ $("capOut").innerHTML = `
${esc(e.message)}
`; } } async function loadFarmSafe(){ try { const d = await api("/sizing/farm-safety?accounts=3"); const sev = s => s.startsWith("medium") ? "warn" : "info"; $("farmSafe").innerHTML = `
${esc(d.the_one_line)}
Is a different IP required? — ${esc(d.is_different_ip_required.answer)}
${esc(d.is_different_ip_required.detail)}
What actually gets you flagged (and the fix)
${d.what_actually_gets_you_blocked.map(t => ` `).join("")}
TriggerWhy it mattersFixRisk
${esc(t.trigger)} ${esc(t.why)} ${esc(t.fix)} ${esc(t.severity)}
IP strategy, ranked — start at #1
    ${d.ip_strategy_ranked.map(r => `
  1. ${esc(r.approach)} — ${esc(r.verdict)}
    ${esc(r.why)} ${r.how ? `
    How: ${esc(r.how)}` : ""}
  2. `).join("")}
What to actually separate
Must differ${d.what_to_separate.must_differ.map(esc).join("
")}
Nice to differ${d.what_to_separate.nice_to_differ.map(esc).join("
")}
Can be identical${d.what_to_separate.can_be_identical.map(esc).join("
")}
Email options, ranked by effort
${d.email_options.map(e => ``).join("")}
OptionStrengthNote
${esc(e.option)}${esc(e.tier)}${esc(e.note)}
${esc(d.email_options[0].note)}
The safety moves
Signs you are drifting into abuse
When to stop: ${esc(d.stop_rule)}
`; } catch(e){ $("farmSafe").innerHTML = `
${esc(e.message)}
`; } } async function loadFarm(n){ try { const d = await api("/sizing/farm?accounts=" + Math.max(1, n)); const w = d.what_scales; $("farmOut").innerHTML = `
${esc(d.legal_answer)}
Resource1 account${d.accounts} accounts
Fetch URLs / day${w.daily_urls.per_account.toLocaleString()} ${w.daily_urls.total.toLocaleString()}
Fetch URLs / min${w.per_minute.per_account} ${w.per_minute.total_per_min}
Implied speed2.5/sec ${w.per_minute.implied_per_second}/sec
Cost$0$0
Proxies and accounts solve different problems. ${esc(d.proxies_relationship)}
Why this beats every other lever here
The breakpoint: ${esc(d.the_real_breakpoint.advice)}
Honest caveats — read before you sign up 10 emails
How to actually do it
    ${d.how_to_actually_do_it.map(x => `
  1. ${esc(x)}
  2. `).join("")}
Rule of thumb: ${esc(d.rule_of_thumb)}
`; linkify($("farmOut")); } catch(e){ $("farmOut").innerHTML = `
${esc(e.message)}
`; } } async function loadTinyfish(){ try { const d = await api("/sizing/tinyfish"); $("tinyOut").innerHTML = `
${esc(d.short_answer)}
Why it cannot be a straight swap
The math holds up though
MeasureValue
Free URLs / day${d.the_math.free_urls_per_day.toLocaleString()} ($0)
Free URLs / min${d.the_math.free_urls_per_min}
Implied speed ceiling${d.concurrency_reality.implied_per_second}/sec
Their cited coverage${d.the_math.their_cited_coverage_pct}% (own eval)
TinyAgent per step$${d.the_math.agent_cost_per_step_usd}
${esc(d.concurrency_reality.note)}
Where it wins
Where it loses
${esc(d.the_vision_that_does_work)}
${esc(d.agent_vs_captcha_cost)}
${esc(d.recommendation)}
${esc(d.honesty)}
`; linkify($("tinyOut")); } catch(e){ $("tinyOut").innerHTML = `
${esc(e.message)}
`; } } async function loadImported(){ try { const d = await api("/sizing/imported-list"); $("importedOut").innerHTML = `
Scraping is off your plate.
${esc(d.why_big_win)}
Do this now — matched to your live project
${d.hard_warnings.map(w => ``).join("")}
SettingCurrentlyWhy
${esc(w.flag)} ${esc(w.currently)} ${esc(w.why)}
Proxies now only matter for two stages
${d.where_proxies_still_matter.map(w => ``).join("")}
StageRiskGrade to use
${esc(w.stage)}${esc(w.risk)} ${esc(d.grade_advice[w.stage] || "")}
scraping skipped— turn proxies for se OFF
Full checklist
    ${d.ready_made_checklist.map(x => `
  1. ${esc(x)}
  2. `).join("")}
${esc(d.per_person_risk)}
${esc(d.scheduler_note)}
${esc(d.what_you_cannot_see_from_files)}
${esc(d.grounded)}
`; linkify($("importedOut")); } catch(e){ $("importedOut").innerHTML = `
${esc(e.message)}
`; } } async function loadFallback(){ try { const d = await api("/sizing/fallback?pool_size=300"); const cps = d.cost_per_success; $("fallbackOut").innerHTML = `
${esc(d.verdict)}
Where it stops (native limits)
What does work natively
The ladder (approximation)
${d.ladder.map(r => ``).join("")}
RungGradeIPs you point at the hard jobKeeps all IPs?
${r.rung}${esc(r.grade)} ${r.ips}${r.keeps_all ? "yes — pool is shared" : ""}
${esc(d.count_rule)}
Cost per success — the only efficiency number
${Object.entries(cps).map(([k,v]) => ``).join("")}
GradeReaches hostile sitesCost per success
${esc(k)} ${esc(String((d.single_grade_comparison.find(s=>s.key===k)||{}).reach_hostile||"—"))}% $${v}
Note the free tier's $0.00 — until you value your time. Paid grades cost more per success only because they are priced higher.
Do these first — they are free
    ${d.deduction_steps.map(s => `
  1. ${esc(s.do)}
    ${esc(s.detail)}
  2. `).join("")}
${esc(d.deduction_order_note)}
${esc(d.cost_reality)}
${esc(d.recommendation)}
${esc(d.grounded)}
`; linkify($("fallbackOut")); } catch(e){ $("fallbackOut").innerHTML = `
${esc(e.message)}
`; } } async function loadGrade(){ try { const d = await api("/sizing/ip-grade"); const bar = v => { const cls = v >= 80 ? "var(--ok)" : v >= 60 ? "var(--warn)" : "var(--bad)"; return `
${v}%
`; }; $("gradeOut").innerHTML = `
${esc(d.advice.headline)}
${esc(d.advice.why)}
${d.grades.map(g => ``).join("")}
IP gradeOrdinary sitesHostile sitesGoogleCost
${esc(g.label)}${g.recommend ? ` recommended` : ""}
${esc(g.best_stage)}
${bar(g.reach_general)} ${bar(g.reach_hostile)} ${bar(g.reach_google)} ${esc(g.cost_shape)}
${esc(d.advice.general_vs_hostile)}
Cheapest setup that still works: ${esc(d.advice.layering)}
${esc(d.advice.honesty)}
`; linkify($("gradeOut")); } catch(e){ $("gradeOut").innerHTML = `
${esc(e.message)}
`; } } async function loadIspPrice(count){ if (!count || count < 1) return; try { const d = await api("/sizing/isp-price?count=" + count); const line = r => ` ${esc(r.name)}${r.is_dedicated ? ` unmetered` : ` capped/shared`} $${r.usd_per_ip}/IP $${r.monthly_total.toLocaleString()}/mo ${esc(r.bandwidth)}`; $("ispOut").innerHTML = `
${count} good-grade residential proxies ≈ $${d.cheapest_dedicated.toLocaleString()}–$${d.dearest_dedicated.toLocaleString()}/mo
Cheapest dedicated, unmetered: $${d.cheapest_dedicated.toLocaleString()}/mo · realistic middle: $${d.typical_dedicated.toLocaleString()}/mo
${d.dedicated.map(line).join("")}
ProviderPer IPFor ${count}Bandwidth rule
Shared-but-cheap alternative, if you do not need 1 IP per thread: $${d.cheapest_shared} for ${count} (${(d.cheapest_shared/count).toFixed(2)}/IP).
${d.shared_budget.map(line).join("")}
Budget shared providerPer IPFor ${count}Rule
Rates read ${esc(d.prices_read_on)}, exclude VAT, and fall with quantity. Confirm at checkout. These are static, mostly-unmetered IPs, so roughly 1 IP = 1 thread — set GSA's max threads to about ${count}.
`; linkify($("ispOut")); } catch(e){ $("ispOut").innerHTML = `
${esc(e.message)}
`; } } async function loadProviders(){ try { const d = await api("/sizing/providers"); const a = d.advice; $("szAdvice").innerHTML = `
${esc(a.headline)}
${esc(a.explain)}
RouteWhat to buy
Cheapest real unmetered ${esc(a.recommended_first)}
Best send quality${esc(a.recommended_best)}
Measure your real usage${esc(a.test_first)}
The only true "unlimited" ${esc(a.the_only_real_unlimited)}
${esc(a.kinds.static_isp)}
${esc(a.kinds.unlimited_rotating)}
${esc(a.kinds.metered_rotating)}
${d.providers.map(p => ``).join("")}
ProviderTrue billing unitEntryCaution
${esc(p.name)}${p.fits_unlimited_residential ? "" : ` not unlimited`} ${esc(p.billing)} ${esc(p.entry_price)} ${esc(p.caveat)}
Prices quoted from the providers' own pages, 11 Nov 2026. They move — confirm at checkout.
`; linkify($("szAdvice")); } catch(e){ $("szAdvice").innerHTML = `
${esc(e.message)}
`; } } async function loadSteps(vol){ try { const d = await api("/sizing/steps?daily_sends=" + vol); $("szSteps").innerHTML = `
Proxy lines paste in as ${esc(d.proxy_line_format)} (some providers give ${esc(d.alt_format)}).
    ${d.steps.map(s => `
  1. ${esc(s.do)}
    ${esc(s.detail)}
  2. `).join("")}
${d.warnings.map(w => esc(w)).join("
")}
At ${vol.toLocaleString()} sends/day this needs ~${d.plan.pool_entries_needed.toLocaleString()} pool entries to keep ${d.plan.threads} usable threads fed.
`; linkify($("szSteps")); } catch(e){ $("szSteps").innerHTML = `
${esc(e.message)}
`; } } async function loadLayer2(vol){ try { const d = await api("/sizing/captcha?daily_sends=" + vol); const b = d.layer2.buy; const row = (label, x, tag) => !x ? "" : ` ${label} ${tag||""} ${esc(x.name || "GSA text recognition")} ${x.model ? esc(x.model) : "free"} ${x.est_monthly_usd !== undefined ? "$" + x.est_monthly_usd + "/mo" : "$0"}`; $("szLayer2").innerHTML = `
${row("Layer 1 — built in", b.layer_1_builtin, `free`)} ${row("Layer 2 — buy this first", b.layer_2_primary, `primary`)} ${row("Layer 2 — buy this second", b.layer_2_backup, `backup`)} ${row("Layer 3 — optional", b.layer_3_optional_free, `free`)} ${row("Layer 3 — optional", b.layer_3_optional_ai, `ai`)}
LayerVendorBillingAt your volume
Two-vendor total at ${vol.toLocaleString()} sends/day: $${d.layer2.two_vendor_total_monthly_usd}/mo (≈ ${d.layer2.solves_per_day.toLocaleString()} solves/day)
${esc(d.layer2.why_two)}
${esc(d.layer2.independence_note)}
${esc(d.layer2.ordering_rule)}
`; } catch(e){ $("szLayer2").innerHTML = `
${esc(e.message)}
`; } } async function enableSources(){ $("szEnableMsg").textContent = "enabling…"; try { const r = await api("/sizing/enable-sources", {method:"POST"}); $("szEnableMsg").innerHTML = `${r.changed} sources enabled → ${r.enabled}/${r.total} now on`; szSources({enabled: r.enabled, total: r.total, disabled: r.total - r.enabled}); } catch(e){ $("szEnableMsg").innerHTML = `${esc(e.message)}`; } } function szSources(s){ const btn = $("szEnableBtn"); if (!btn) return; btn.disabled = s.disabled === 0; btn.textContent = s.disabled === 0 ? `All ${s.total} free sources already on` : `Enable all ${s.total} free sources (${s.disabled} off)`; } /* ---------------------------------------------------------------- misc */ async function explain(){ const ref = $("ref").value.trim(); if(!ref) return; try { const e = await api("/nl/explain?ref=" + encodeURIComponent(ref)); $("explainOut").innerHTML = `
${esc(e.ref)} ${esc(e.type)}
${esc(e.help)}
${esc(e.guidance)}
${e.range?`
range: ${e.range[0]}–${e.range[1]}
`:""} ${e.allowed?`
allowed: ${esc(e.allowed.join(", "))}
`:""}`; } catch(e){ $("explainOut").innerHTML = `
${esc(e.message)}
`; } } async function act(what){ try { await api("/app/"+what, {method:"POST"}); setTimeout(loadAll, 700); } catch(e){ alert(e.message); } } $("applyBtn").onclick = applyStaged; async function loadAuthBadge(){ $("signout").href = API + "/logout"; // reach the API origin, not the Vercel one try { const r = await fetch(API + "/auth/status", {credentials:'include'}); const j = await r.json(); $("signout").style.display = (j.auth === "off") ? "none" : "inline-block"; } catch {} } loadGlossary().then(loadAll).then(loadAuthBadge);