Contact Forms
connecting…
Jargon
Refresh
Sign out
Run
Per-person messages
Project & proxies
Proxies & cost
All settings
Glossary
Tell it what you want
Plan changes
Apply
What runs where ?
Contact-finding happens in three stages. You can put
proxies on any of them,
independently, from the Project & proxies tab.
Stage What it does Proxies 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
One unique message per website
Paste a list where each line is a website and the message to send to it.
GSA's own importer turns each row into a separate submission using the
csv macro — this is a built-in feature, not a hack.
Preview
Save & wire to project
Cost calculator · how many IPs and what they cost
Per-IP rates read from the vendors' own pricing pages. Dedicated means
one IP per customer with unmetered bandwidth — the grade you want for sending.
How many proxies
× premium static residential IPs
What grade of IP to buy ?
Coverage = roughly what share of sites that grade gets through. Your real sending
success is set by the hostile-site number.
Cheapest test that still proves something ?
Do captchas replace our proxies? do parse proxies need to be good ?
Farming without getting flagged — do you need different IPs? ?
Would TinyFish be a better parser? ?
Your setup: imported list → parse & send ?
IP fallback: escalate on failure, shrink as price rises ?
1 · Where to buy "unlimited residential" ?
Providers, with the real billing unit
2 · Install them into GSA
3 · Which 2 captcha subscriptions to buy ?
How many proxies, by daily volume
Pick a volume. Numbers come from GSA's own shipped service prices and your
local proxy-source list; the rules of thumb are labelled.
Sends per day
Captcha: what it costs you
Settings
Fill blanks with defaults
Fill everything with defaults
Save staged edits
every field is editable
// 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 => `${esc(p.name)} `).join("");
$("projSel").innerHTML = opts || `no projects found `;
$("pplProject").innerHTML = opts || `no projects `;
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 = `${esc(f.key)} ${tags}
${esc(f.help)}
`;
let ctl;
if (f.type === "bool") {
ctl = ``;
} else if (f.type === "int") {
ctl = `
${f.unit?`${esc(f.unit)} `:""}
${f.allowed?`${f.allowed.map(a=>``).join("")} `:""}
`;
} 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 => `
${pretty[r]}use proxies for ${r.replace("proxies for ","")}
`).join("")}
Proxy type per role ${dot("proven vs inferred")}
${roles.map(r => `
${pretty[r]}
${Object.entries(p.proxy_type_options).map(([id,label]) =>
`${label} `).join("")}
`).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 `}
Add
Save project changes
`;
} 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 =>
`${esc(t.label)}${t.recommend ? " ★" : ""} `).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)}
Sends/day Threads
Pool entries needed
Live at once
Distinct IPs/day
${p.rows.map(r => `${r.daily_sends.toLocaleString()} ${r.threads}
${r.pool_entries_needed.toLocaleString()} ${r.live_at_once_needed}
${r.distinct_ips_per_day.toLocaleString()} `).join("")}
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)}
Enable free proxy sources
`;
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):
Layer Service Model Est. cost
${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)")}
${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
Service Rating Model $/mo at your volume Broad
${c.services.map(s=>`${esc(s.name)} ${s.rating ?? "—"} ${esc(s.model)}
$${s.est_monthly_usd} ${s.broad_coverage?"✓":""} `).join("")}
`;
} 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
Code Attributable to Note
${d.failure_taxonomy.map(t => `${esc(t.code)}${esc(t.attributable_to)} ${esc(t.note)} `).join("")}
${esc(d.taxonomy_note)}
Sample size (to detect a ${ss.detect_pp}pp gap, base ${ss.pooled_baseline_pct}%)
Stratum Share n per arm
${ss.per_stratum.map(s => `${esc(s["class"])} ${s.share_pct}% ${s.n_per_arm} `).join("")}
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)
${d.must_be_identical.map(x => `${esc(x)} `).join("")}
Deliberately varied: ${esc(d.deliberately_varied[0])}
Confounds to kill
Confound Why it ruins the test Fix
${d.confounds_to_kill.map(c => `${esc(c.confound)} ${esc(c.why_it_ruins_it)} ${esc(c.fix)} `).join("")}
Controls
Positive canary: ${esc(d.controls.positive_canary.what)} — ${esc(d.controls.positive_canary.why)}
A/A noise floor: ${esc(d.controls.negative_aa_noise_floor.what)} — ${esc(d.controls.negative_aa_noise_floor.why)}
Pre-register: ${esc(d.controls.pre_register)}
Run it in GSA
${d.gsa_implementation.map(x => `${esc(x)} `).join("")}
Decision rule
Adopt cheap if: ${esc(d.decision_rule.adopt_cheap_if)}
Keep dedicated if: ${esc(d.decision_rule.keep_dedicated_if)}
Cannot call it if: ${esc(d.decision_rule.cant_call_it_if)}
What this still does NOT prove
${d.what_this_does_not_prove.map(x => `${esc(x)} `).join("")}
`;
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)}
Tier $/mo Attempts IPs/arm Threads ~Runtime What it proves / risk
${d.tiers.map(t => `
${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)} `).join("")}
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
${d.what_is_NOT_in_this_budget.map(x => `${esc(x)} `).join("")}
How to cut it further
${d.how_to_cut_it_further.map(x => `${esc(x)} `).join("")}
`;
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)}
Bucket $/mo Buys Why here Trade-off Confidence
${d.lines.map(row).join("")}
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
Bucket Marginal value
${d.what_you_get_for_each_dollar.map(x => `${esc(x.bucket)} ${esc(x.marginal_value)} `).join("")}
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.
Hop 1 (yours): ${esc(d.the_two_hops_they_cannot_do.hop_1_read)}
Hop 2 (theirs): ${esc(d.the_two_hops_they_cannot_do.hop_2_solve)}
Hop 3 (yours): ${esc(d.the_two_hops_they_cannot_do.hop_3_submit)}
${esc(d.the_two_hops_they_cannot_do.why_it_matters)}
Stage by stage
Stage Needs our proxy Grade needed Do captchas help
${d.stage_by_stage.map(s => `
${esc(s.stage)} ${esc(s.why)} ${esc(s.keep_separate)}
${esc(s.needs_our_proxy)}
${esc(s.grade_needed)}
${esc(s.captcha_services_help)} `).join("")}
${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
Parse pool: ${esc(d.the_hybrid_that_maximises_value.parse_pool)}
Send pool: ${esc(d.the_hybrid_that_maximises_value.send_pool)}
Captcha: ${esc(d.the_hybrid_that_maximises_value.captcha)}
${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)}
Arm What Vendor IPs $/mo
${d.arms.map(a => `
${esc(a.arm)}
${esc(a.label)}
${esc(a.vendor)}${esc(a.why_this_vendor)}
${a.ips} ${a.monthly === 0 ? "$0" : "$" + a.monthly.toFixed(2)} `).join("")}
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
${d.arms.map(a => `${esc(a.arm)}: ${esc(a.how_to_get)} ${esc(a.notes)} `).join("")}
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 => `${esc(x)} `).join("")}
Watch these when buying
${d.purchase_caveats.map(x => `${esc(x)} `).join("")}
`;
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)}
Loud failure: ${esc(d.the_dominant_asymmetry.loud)}
Silent failure: ${esc(d.the_dominant_asymmetry.silent)}
${esc(d.the_dominant_asymmetry.consequence)}
Where the loss actually lands — ${esc(d.loss_model.assumption_warning)}
Site class Share Cheap loss Why
${d.loss_model.by_site_class.map(c => `
${esc(c["class"])} ${esc(c.what)}
${c.share_pct}%
${c.cheap_loss_pct === 0 ? "0%" : c.cheap_loss_pct + "%"}
${esc(c.why)} `).join("")}
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.axis_1)}
${esc(d.two_axes_do_not_collapse_them.axis_2)}
${esc(d.two_axes_do_not_collapse_them.the_trap)}
${esc(d.two_axes_do_not_collapse_them.note)}
Options, with the loss attached
Option $/mo Loss vs dedicated ISP Useless?
${d.options_ranked.map(o => `
${esc(o.option)} ${o.monthly === 0 ? "$0" : "$" + o.monthly.toFixed(2)}
${o.loss_pct}% ${esc(o.useless)} `).join("")}
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:
${d.cheapest_non_stupid_setup.map(x => `${esc(x)} `).join("")}
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)}
Tier on the page From $/IP What it claims For sending
${d.the_quality_selector.map(t => `
${esc(t.name)} ${t.name==="Premium"?" ← pre-selected":""}
$${t.from_usd_per_ip.toFixed(2)} ${esc(t.desc)}
${esc(t.verdict_for_sending)} `).join("")}
Pick: ${esc(d.which_tier_for_sending.pick)}
Unless: ${esc(d.which_tier_for_sending.unless)}
What this costs you at ${d.ips} IPs
Option Monthly
${c.comparison.map(r => `${esc(r.option)} $${r.monthly_usd.toFixed(2)} `).join("")}
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
${d.checkout_hygiene.map(x => `${esc(x)} `).join("")}
${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 = `
Stage Verdict
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
Measure Value
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
${d.when_they_are_right.map(x => `${esc(x)} `).join("")}
Use for Buy
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)}
Vendor $/IP ~Monthly at ${d.ips_to_actually_buy} Why
${d.vendors_ranked.map(v => `#${v.rank} ${esc(v.name)} $${v.usd_per_ip}
~$${v.monthly_total} ${esc(v.why)}Catch: ${esc(v.catch)} `).join("")}
What NOT to buy, and why
Option Verdict Why
${d.what_not_to_buy.map(w => `${esc(w.option)} ${esc(w.verdict)} ${esc(w.why)} `).join("")}
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 => `${esc(s)} `).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)}
Quantity Value
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
Accounts Days to clear Back-to-back fetch time
${Object.entries(d.spread_table).map(([k,v]) => `${esc(k.replace("_"," "))} ${v.days_to_clear} ${esc(v.wall_clock_if_back_to_back)} `).join("")}
The real limits:
${d.the_real_limits.map(x => `${esc(x)} `).join("")}
`;
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)
Trigger Why it matters Fix Risk
${d.what_actually_gets_you_blocked.map(t => `
${esc(t.trigger)}
${esc(t.why)}
${esc(t.fix)}
${esc(t.severity)}
`).join("")}
IP strategy, ranked — start at #1
${d.ip_strategy_ranked.map(r => `
${esc(r.approach)} — ${esc(r.verdict)} ${esc(r.why)}
${r.how ? `How: ${esc(r.how)} ` : ""}
`).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
Option Strength Note
${d.email_options.map(e => `${esc(e.option)} ${esc(e.tier)} ${esc(e.note)} `).join("")}
${esc(d.email_options[0].note)}
The safety moves
${d.the_safety_moves.map(x => `${esc(x)} `).join("")}
Signs you are drifting into abuse
${d.signs_you_are_drifting_into_abuse.map(x => `${esc(x)} `).join("")}
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)}
Resource 1 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 speed 2.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
${d.advantage_over_proxies.map(x => `${esc(x)} `).join("")}
The breakpoint: ${esc(d.the_real_breakpoint.advice)}
Honest caveats — read before you sign up 10 emails
${d.honest_caveats.map(x => `${esc(x)} `).join("")}
How to actually do it
${d.how_to_actually_do_it.map(x => `${esc(x)} `).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
${d.why_not_a_swap.map(x => `${esc(x)} `).join("")}
The math holds up though
Measure Value
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
${d.where_it_wins.map(x => `${esc(x)} `).join("")}
Where it loses
${d.where_it_loses.map(x => `${esc(x)} `).join("")}
${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
Setting Currently Why
${d.hard_warnings.map(w => `
${esc(w.flag)}
${esc(w.currently)}
${esc(w.why)} `).join("")}
Proxies now only matter for two stages
Stage Risk Grade to use
${d.where_proxies_still_matter.map(w => `
${esc(w.stage)} ${esc(w.risk)}
${esc(d.grade_advice[w.stage] || "")} `).join("")}
scraping skipped —
turn proxies for se OFF
Full checklist
${d.ready_made_checklist.map(x => `${esc(x)} `).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)
${d.native_limits.map(x => `${esc(x)} `).join("")}
What does work natively
${d.what_does_work_natively.map(x => `${esc(x)} `).join("")}
The ladder (approximation)
Rung Grade IPs you point at the hard job Keeps all IPs?
${d.ladder.map(r => `${r.rung} ${esc(r.grade)}
${r.ips} ${r.keeps_all ? "yes — pool is shared" : ""} `).join("")}
${esc(d.count_rule)}
Cost per success — the only efficiency number
Grade Reaches hostile sites Cost per success
${Object.entries(cps).map(([k,v]) => `${esc(k)}
${esc(String((d.single_grade_comparison.find(s=>s.key===k)||{}).reach_hostile||"—"))}%
$${v} `).join("")}
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 => `${esc(s.do)} ${esc(s.detail)}
`).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 ``;
};
$("gradeOut").innerHTML = `
${esc(d.advice.headline)}
${esc(d.advice.why)}
IP grade Ordinary sites Hostile sites Google Cost
${d.grades.map(g => `
${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)} `).join("")}
${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
Provider Per IP For ${count} Bandwidth rule
${d.dedicated.map(line).join("")}
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).
Budget shared provider Per IP For ${count} Rule
${d.shared_budget.map(line).join("")}
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)}
Route What 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)}
Provider True billing unit Entry Caution
${d.providers.map(p => `
${esc(p.name)} ${p.fits_unlimited_residential ? "" : ` not unlimited `}
${esc(p.billing)}
${esc(p.entry_price)}
${esc(p.caveat)} `).join("")}
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 => `${esc(s.do)} ${esc(s.detail)}
`).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 = `
Volume
Layer Vendor Billing At your volume
${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 `)}
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);