AI visibility ROI 1

Visibility ROI Tracker

Track time, effort, and spend across traditional search + AI answer engines, then model ROI from AI SEO and AI agents.

Business inputs







Visibility channels
Monthly spend + hours per channel (SEO, content, listings, ads mgmt, etc.).

Baseline performance before AI improvements.





Capture internal time cost — hours reduced with AI agents.





AI SEO (growth)
Model improved visibility on search and AI answer engines.



AI agents (savings)
Automate research, content drafts, reporting, review replies, etc.





Scenario settings
Change horizon + ramp time. Conservative mode reduces gains.



Note: This is a forecasting calculator, not a guarantee. Add LTV/repeat purchase later for higher accuracy.

ROI snapshot

Incremental leads / month
Incremental customers / month
Incremental revenue / month
Incremental profit / month
Hours saved / month
Value of time saved / month
Owner + team blended value
External spend saved / month
From tracked spend + agency cost
AI tools cost / month
Included in net benefit
Net benefit / month
= profit lift + time value + spend saved − AI costs
Net benefit over 12 months
ROI (over horizon)
(Net benefit − AI cost) ÷ AI cost
Payback period
AI cost ÷ net monthly benefit
Quick sales summary
Auto-generated from current inputs

let S = defaultState();

// --- Helpers --- const $ = (id) => document.getElementById(id); const n = (v) => isFinite(v) ? Number(v) : 0; const clamp = (v,min,max)=>Math.min(max,Math.max(min,v)); const fmtInt = (v)=> new Intl.NumberFormat(undefined,{maximumFractionDigits:0}).format(v); const fmtPct = (v)=> (v*100).toFixed(1) + \"%\"; const fmtMoney = (v,cur)=> new Intl.NumberFormat(undefined,{style:'currency',currency:cur,maximumFractionDigits:0}).format(v);

function avgRampFactor(){ const H = Math.max(1, Math.floor(n(S.scenario.horizonMonths))); const R = Math.max(0, Math.floor(n(S.scenario.rampMonths))); if(R===0) return 1; let total=0; for(let m=1;m<=H;m++){ total += (m<=R) ? (m/R) : 1; } return total/H; } function totals(){ const spend = S.channels.reduce((a,c)=>a+n(c.monthlySpend),0); const hrs = S.channels.reduce((a,c)=>a+n(c.hoursPerMonth),0); return { spend, hrs }; }

function compute(){ const cur = S.biz.currency; const ramp = avgRampFactor(); const conservative = !!S.scenario.conservativeMode;

const leadLift = S.ai.includeAiSeo ? n(S.ai.aiSeoLiftLeads) : 0; const convLiftRel = S.ai.includeAiSeo ? n(S.ai.aiLeadQualityLift) : 0;

const leadLiftAdj = conservative ? leadLift*0.7 : leadLift; const convLiftAdj = conservative ? convLiftRel*0.7 : convLiftRel;

const baseLeads = n(S.baseline.monthlyLeads); const baseConv = n(S.baseline.leadToCustomerRate); const baseCustomers = baseLeads * baseConv; const baseRevenue = baseCustomers * n(S.baseline.averageOrderValue); const baseProfit = baseRevenue * n(S.baseline.grossMargin);

const newLeads = baseLeads * (1 + leadLiftAdj * ramp); const newConv = baseConv * (1 + convLiftAdj * ramp); const newCustomers = newLeads * newConv; const newRevenue = newCustomers * n(S.baseline.averageOrderValue); const newProfit = newRevenue * n(S.baseline.grossMargin);

const incrLeads = Math.max(0, newLeads - baseLeads); const incrCustomers = Math.max(0, newCustomers - baseCustomers); const incrRevenue = Math.max(0, newRevenue - baseRevenue); const incrProfit = Math.max(0, newProfit - baseProfit);

const t = totals(); const effortHours = n(S.effort.ownerHoursPerMonth) + n(S.effort.teamHoursPerMonth) + t.hrs;

const timeSavedPct = S.ai.includeAiAgents ? n(S.ai.timeSavedPct) : 0; const timeSavedAdj = conservative ? timeSavedPct*0.8 : timeSavedPct; const hoursSaved = effortHours * timeSavedAdj;

const blended = n(S.effort.ownerHourlyValue)*0.4 + n(S.effort.teamHourlyCost)*0.6; const timeValueSaved = hoursSaved * blended;

const externalSpend = t.spend + n(S.ai.monthlyAgencyOrContractorCost); const moneySavedPct = S.ai.includeAiAgents ? n(S.ai.moneySavedPct) : 0; const moneySavedAdj = conservative ? moneySavedPct*0.8 : moneySavedPct; const moneySaved = externalSpend * moneySavedAdj;

const aiCost = (S.ai.includeAiSeo || S.ai.includeAiAgents) ? n(S.ai.monthlyAiToolsCost) : 0;

const netMonthlyBenefit = incrProfit + timeValueSaved + moneySaved - aiCost;

const H = Math.max(1, Math.floor(n(S.scenario.horizonMonths))); const netHorizon = netMonthlyBenefit * H; const totalAiCost = aiCost * H; const roi = totalAiCost > 0 ? (netHorizon - totalAiCost) / totalAiCost : null; const payback = (netMonthlyBenefit > 0 && aiCost > 0) ? (aiCost / netMonthlyBenefit) : null;

return {cur, ramp, baseLeads, incrLeads, newLeads, incrCustomers, incrRevenue, incrProfit, effortHours, hoursSaved, timeValueSaved, moneySaved, aiCost, netMonthlyBenefit, netHorizon, roi, payback, totalSpend:t.spend}; }

// --- UI Rendering --- const currencies = ["GBP","USD","EUR","AUD","CAD","INR"];

function renderCurrencyChips(){ const wrap = $("vrt-currencyChips"); wrap.innerHTML = ""; currencies.forEach(c=>{ const b = document.createElement("button"); b.type="button"; b.className = "vrt-chip" + (S.biz.currency===c ? " is-active" : ""); b.textContent = c; b.addEventListener("click", ()=>{ S.biz.currency=c; renderCurrencyChips(); syncInputsToCurrency(); render(); }); wrap.appendChild(b); }); }

function channelRow(c){ const div = document.createElement("div"); div.className = "vrt-channel"; div.innerHTML = `

`; return div; }

function renderChannels(){ const wrap = $("vrt-channels"); wrap.innerHTML = ""; S.channels.forEach(c=>wrap.appendChild(channelRow(c))); }

function renderTotals(){ const t = totals(); $("vrt-totals").textContent = `Totals: Spend ${fmtMoney(t.spend, S.biz.currency)} / month · ${fmtInt(t.hrs)} hrs / month`; }

function renderMetrics(){ const m = compute(); $("m-incrLeads").textContent = fmtInt(m.incrLeads); $("m-leadsSub").textContent = `New: ${fmtInt(m.newLeads)} vs Baseline: ${fmtInt(m.baseLeads)}`; $("m-incrCustomers").textContent = fmtInt(m.incrCustomers); $("m-customersSub").textContent = `Ramp avg: ${fmtPct(m.ramp)}`; $("m-incrRevenue").textContent = fmtMoney(m.incrRevenue, m.cur); $("m-revSub").textContent = `AOV: ${fmtMoney(n(S.baseline.averageOrderValue), m.cur)}`; $("m-incrProfit").textContent = fmtMoney(m.incrProfit, m.cur); $("m-profitSub").textContent = `Gross margin: ${fmtPct(n(S.baseline.grossMargin))}`;

$("m-hoursSaved").textContent = fmtInt(m.hoursSaved) + " hrs"; $("m-hoursSub").textContent = `Total effort tracked: ${fmtInt(m.effortHours)} hrs/month`; $("m-timeValue").textContent = fmtMoney(m.timeValueSaved, m.cur); $("m-moneySaved").textContent = fmtMoney(m.moneySaved, m.cur); $("m-aiCost").textContent = fmtMoney(m.aiCost, m.cur);

$("m-netMonthly").textContent = fmtMoney(m.netMonthlyBenefit, m.cur); $("m-horizonTitle").textContent = `Net benefit over ${Math.max(1,Math.floor(n(S.scenario.horizonMonths)))} months`; $("m-netHorizon").textContent = fmtMoney(m.netHorizon, m.cur); $("m-rampSub").textContent = `Average ramp factor: ${fmtPct(m.ramp)}`;

$("m-roi").textContent = (m.roi===null) ? "—" : fmtPct(m.roi); $("m-payback").textContent = (m.payback===null) ? "—" : (m.payback.toFixed(1) + " months");

const netCard = $("m-netCard"); netCard.classList.remove("vrt-good","vrt-bad","vrt-warn"); if (m.netMonthlyBenefit > 0) netCard.classList.add("vrt-good"); else if (m.netMonthlyBenefit < 0) netCard.classList.add("vrt-bad"); else netCard.classList.add("vrt-warn"); const name = (S.biz.businessName||"the business").trim() || "the business"; const H = Math.max(1,Math.floor(n(S.scenario.horizonMonths))); const summary = `Based on your tracked visibility investment (~${fmtMoney(m.totalSpend,m.cur)}/month and ${fmtInt(m.effortHours)} hours/month), implementing AI SEO and AI agents for ${name} is modelled to add about ${fmtInt(m.incrLeads)} extra leads and ${fmtInt(m.incrCustomers)} extra customers per month, contributing ~${fmtMoney(m.incrRevenue,m.cur)} revenue and ~${fmtMoney(m.incrProfit,m.cur)} profit monthly. In parallel, automation is estimated to save ~${fmtInt(m.hoursSaved)} hours and ~${fmtMoney(m.moneySaved,m.cur)} in external spend per month. Net benefit is ~${fmtMoney(m.netMonthlyBenefit,m.cur)}/month, or ~${fmtMoney(m.netHorizon,m.cur)} over ${H} months.`;\n $(\"m-summary\").textContent = summary;\n }\n\n function escapeHtml(s){\n return String(s||\"\")\n .replaceAll(\"&\",\"&\")\n .replaceAll(\"<\",\"<\")\n .replaceAll(\">\",\">\")\n .replaceAll('\"',\""\")\n .replaceAll(\"'\",\"'\");\n }\n\n function syncInputsToCurrency(){\n // purely cosmetic labels already use S.biz.currency in renders\n }\n\n function render(){\n renderCurrencyChips();\n renderChannels();\n renderTotals();\n renderMetrics();\n }\n\n // --- Wire up tab switching ---\n document.querySelectorAll(\".vrt-tab\").forEach(btn=>{\n btn.addEventListener(\"click\",()=>{\n document.querySelectorAll(\".vrt-tab\").forEach(b=>b.classList.remove(\"is-active\"));\n document.querySelectorAll(\".vrt-tabpane\").forEach(p=>p.classList.remove(\"is-active\"));\n btn.classList.add(\"is-active\");\n const tab = btn.getAttribute(\"data-tab\");\n $(\"vrt-tab-\"+tab).classList.add(\"is-active\");\n });\n });\n\n // --- Input bindings ---\n function bindBasics(){\n $(\"vrt-businessName\").addEventListener(\"input\", e=>{ S.biz.businessName=e.target.value; renderMetrics(); });\n $(\"vrt-industry\").addEventListener(\"input\", e=>{ S.biz.industry=e.target.value; });\n $(\"vrt-location\").addEventListener(\"input\", e=>{ S.biz.location=e.target.value; });\n\n $(\"vrt-monthlyLeads\").addEventListener(\"input\", e=>{ S.baseline.monthlyLeads=n(e.target.value); renderMetrics(); });\n $(\"vrt-convPct\").addEventListener(\"input\", e=>{ S.baseline.leadToCustomerRate=clamp(n(e.target.value)/100,0,1); renderMetrics(); });\n $(\"vrt-aov\").addEventListener(\"input\", e=>{ S.baseline.averageOrderValue=n(e.target.value); renderMetrics(); });\n $(\"vrt-gmPct\").addEventListener(\"input\", e=>{ S.baseline.grossMargin=clamp(n(e.target.value)/100,0,1); renderMetrics(); });\n\n $(\"vrt-ownerHourly\").addEventListener(\"input\", e=>{ S.effort.ownerHourlyValue=n(e.target.value); renderMetrics(); });\n $(\"vrt-teamHourly\").addEventListener(\"input\", e=>{ S.effort.teamHourlyCost=n(e.target.value); renderMetrics(); });\n $(\"vrt-ownerHours\").addEventListener(\"input\", e=>{ S.effort.ownerHoursPerMonth=n(e.target.value); renderMetrics(); });\n $(\"vrt-teamHours\").addEventListener(\"input\", e=>{ S.effort.teamHoursPerMonth=n(e.target.value); renderMetrics(); });\n\n $(\"vrt-aiSeoOn\").addEventListener(\"change\", e=>{ S.ai.includeAiSeo=!!e.target.checked; renderMetrics(); });\n $(\"vrt-leadLiftPct\").addEventListener(\"input\", e=>{ S.ai.aiSeoLiftLeads=clamp(n(e.target.value)/100,0,2); renderMetrics(); });\n $(\"vrt-qualityLiftPct\").addEventListener(\"input\", e=>{ S.ai.aiLeadQualityLift=clamp(n(e.target.value)/100,0,0.6); renderMetrics(); });\n\n $(\"vrt-aiAgentsOn\").addEventListener(\"change\", e=>{ S.ai.includeAiAgents=!!e.target.checked; renderMetrics(); });\n $(\"vrt-timeSavedPct\").addEventListener(\"input\", e=>{ S.ai.timeSavedPct=clamp(n(e.target.value)/100,0,0.8); renderMetrics(); });\n $(\"vrt-moneySavedPct\").addEventListener(\"input\", e=>{ S.ai.moneySavedPct=clamp(n(e.target.value)/100,0,0.5); renderMetrics(); });\n $(\"vrt-aiToolsCost\").addEventListener(\"input\", e=>{ S.ai.monthlyAiToolsCost=n(e.target.value); renderMetrics(); });\n $(\"vrt-agencyCost\").addEventListener(\"input\", e=>{ S.ai.monthlyAgencyOrContractorCost=n(e.target.value); renderMetrics(); });\n\n $(\"vrt-conservative\").addEventListener(\"change\", e=>{ S.scenario.conservativeMode=!!e.target.checked; renderMetrics(); });\n $(\"vrt-horizon\").addEventListener(\"input\", e=>{ S.scenario.horizonMonths=clamp(Math.round(n(e.target.value)),1,60); renderMetrics(); });\n $(\"vrt-ramp\").addEventListener(\"input\", e=>{ S.scenario.rampMonths=clamp(Math.round(n(e.target.value)),0,24); renderMetrics(); });\n\n $(\"vrt-addChannel\").addEventListener(\"click\", ()=>{\n S.channels.push({ id:\"custom-\"+Math.random().toString(16).slice(2), name:\"New Channel\", type:\"Traditional\", monthlySpend:0, hoursPerMonth:0 });\n render();\n });\n\n $(\"vrt-reset\").addEventListener(\"click\", ()=>{\n S = defaultState();\n syncAllInputsFromState();\n render();\n });\n }\n\n function bindChannelEvents(){\n // Delegate events from channels container\n $(\"vrt-channels\").addEventListener(\"input\", (e)=>{\n const el = e.target;\n const id = el.getAttribute(\"data-id\");\n const k = el.getAttribute(\"data-k\");\n if(!id || !k) return;\n const idx = S.channels.findIndex(c=>c.id===id);\n if(idx<0) return;\n if(k===\"name\") S.channels[idx].name = el.value;\n else S.channels[idx][k] = n(el.value);\n renderTotals();\n renderMetrics();\n });\n\n $(\"vrt-channels\").addEventListener(\"click\", (e)=>{\n const el = e.target;\n const rid = el.getAttribute(\"data-remove\");\n if(rid){\n S.channels = S.channels.filter(c=>c.id!==rid);\n render();\n return;\n }\n const type = el.getAttribute(\"data-type\");\n const id = el.getAttribute(\"data-id\");\n if(type && id){\n const c = S.channels.find(x=>x.id===id);\n if(c){ c.type = type; render(); }\n }\n });\n }\n\n function syncAllInputsFromState(){\n $(\"vrt-businessName\").value = S.biz.businessName;\n $(\"vrt-industry\").value = S.biz.industry;\n $(\"vrt-location\").value = S.biz.location;\n\n $(\"vrt-monthlyLeads\").value = S.baseline.monthlyLeads;\n $(\"vrt-convPct\").value = (S.baseline.leadToCustomerRate*100).toFixed(1);\n $(\"vrt-aov\").value = S.baseline.averageOrderValue;\n $(\"vrt-gmPct\").value = (S.baseline.grossMargin*100).toFixed(1);\n\n $(\"vrt-ownerHourly\").value = S.effort.ownerHourlyValue;\n $(\"vrt-teamHourly\").value = S.effort.teamHourlyCost;\n $(\"vrt-ownerHours\").value = S.effort.ownerHoursPerMonth;\n $(\"vrt-teamHours\").value = S.effort.teamHoursPerMonth;\n\n $(\"vrt-aiSeoOn\").checked = !!S.ai.includeAiSeo;\n $(\"vrt-leadLiftPct\").value = Math.round(S.ai.aiSeoLiftLeads*100);\n $(\"vrt-qualityLiftPct\").value = Math.round(S.ai.aiLeadQualityLift*100);\n\n $(\"vrt-aiAgentsOn\").checked = !!S.ai.includeAiAgents;\n $(\"vrt-timeSavedPct\").value = Math.round(S.ai.timeSavedPct*100);\n $(\"vrt-moneySavedPct\").value = Math.round(S.ai.moneySavedPct*100);\n $(\"vrt-aiToolsCost\").value = S.ai.monthlyAiToolsCost;\n $(\"vrt-agencyCost\").value = S.ai.monthlyAgencyOrContractorCost;\n\n $(\"vrt-conservative\").checked = !!S.scenario.conservativeMode;\n $(\"vrt-horizon\").value = S.scenario.horizonMonths;\n $(\"vrt-ramp\").value = S.scenario.rampMonths;\n }\n\n // --- Init ---\n syncAllInputsFromState();\n bindBasics();\n bindChannelEvents();\n render();\n\n })();\n \n

Scroll to Top