const { useMemo, useState, useEffect } = React;
const PINK = "#ff4fa3";
const BLUE = "#4f8bff";
const fav = (domain) => `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
const host = (url) => { try { return new URL(url).hostname; } catch { return url; } };
// ErrorBoundary to avoid blank screens
class ErrorBoundary extends React.Component{
constructor(props){ super(props); this.state={hasError:false, error:null}; }
static getDerivedStateFromError(error){ return {hasError:true, error}; }
componentDidCatch(error, info){ console.error("CamCompare Error:", error, info); }
render(){
if(this.state.hasError){
return (
setOpen(false)} />}
Filters — {mode==="model"?"For Models":"For Customers"}
setOpen(false)} className="px-2 py-1 text-sm rounded-md border border-slate-200 hover:bg-slate-50">Close
{mode==="model" ? (<>
setModelFilters(f=>({...f,minRate:v}))} />
setModelFilters(f=>({...f,minCustomersK:v}))} min={0} max={20000} step={100} />
setModelFilters(f=>({...f,maxCustomersK:v}))} min={0} max={20000} step={100} />
setModelFilters(f=>({...f,minModelsK:v}))} min={0} max={2000} step={10} />
setModelFilters(f=>({...f,maxModelsK:v}))} min={0} max={2000} step={10} />
Features
{Object.keys(modelFilters.feat).map(k=>
setModelFilters(f=>({...f, feat:{...f.feat, [k]:!f.feat[k]}}))} />)}
Payout timeframe
{PAYOUT_OPTIONS.map(opt=>
togglePayout(opt)} />)}
Sort by
setModelFilters(f=>({...f, sortBy:e.target.value}))}>
Recommended
Revenue share
Estimated customers
Estimated models
Name (A–Z)
>) : (<>
setCustomerFilters(f=>({...f, tokenMax:Number(v.toFixed(2))}))} />
setCustomerFilters(f=>({...f, privateMax:Number(v.toFixed(1))}))} />
setCustomerFilters(f=>({...f, groupMax:Number(v.toFixed(2))}))} />
setCustomerFilters(f=>({...f, minValue:Number(v.toFixed(1))}))} />
Payments
{["Cards","Crypto"].map(k=>
setCustomerFilters(f=>({...f, payments:{...f.payments, [k]:!f.payments[k]}}))} />)}
Interactive
{["Cam2Cam","Lovense","VR"].map(k=>
setCustomerFilters(f=>({...f, interactive:{...f.interactive, [k]:!f.interactive[k]}}))} />)}
Free content
{["Low","Medium","High"].map(k=>
setCustomerFilters(f=>({...f, freeContent:{...f.freeContent, [k]:!f.freeContent[k]}}))} />)}
Sort by
setCustomerFilters(f=>({...f, sortBy:e.target.value}))}>
Best value
Lowest token price
Lowest private $/min
Lowest group $/min
Most active models
Name (A–Z)
>)}
>);
}
// ---------------- Tables ----------------
function ModelTable({ rows, onToggleSave, onToggleCompare, saved, compare }){
return (
Platform
Rev %
Per $100
Payout
Cust (K)
Models (K)
Lovense
VOD
PPV
VR
App
2FA
Geo
Actions
Link
{rows.map((p,idx)=> (
{p.ratePct}%
${p.earningsPer100.toFixed(0)}
{p.payout}
{p.estCustomersK.toLocaleString()}K
{p.estModelsK.toLocaleString()}K
{p.features.lovense?"✓":"—"}
{p.features.vod?"✓":"—"}
{p.features.ppvClips?"✓":"—"}
{p.features.vr?"✓":"—"}
{p.features.mobileApp?"✓":"—"}
{p.features.twoFA?"✓":"—"}
{p.features.geoBlocking?"✓":"—"}
onToggleSave(p.name)} className={`px-2 py-1 rounded-full text-xs border mr-2 ${saved.includes(p.name)?"bg-pink-500 border-pink-600 text-white":"bg-pink-50 border-pink-200 text-pink-700 hover:bg-pink-100"}`}>Save onToggleCompare(p.name)} className={`px-2 py-1 rounded-full text-xs border ${compare.includes(p.name)?"bg-blue-600 border-blue-700 text-white":"bg-blue-50 border-blue-200 text-blue-700 hover:bg-blue-100"}`}>Compare
Visit
))}
);
}
function CustomerTable({ items }){
const cols=[
{ key:"name", label:"Site", align:"left" },
{ key:"pricePerTokenUSD", label:"Token $", align:"right", fmt:v=>"$"+v.toFixed(2) },
{ key:"privatePerMinUSD", label:"Private/min", align:"right", fmt:v=>"$"+v.toFixed(2) },
{ key:"groupPerMinUSD", label:"Group/min", align:"right", fmt:v=>"$"+v.toFixed(2) },
{ key:"freeContent", label:"Free Content", align:"center" },
{ key:"videoQuality", label:"Quality", align:"center" },
{ key:"interactive", label:"Interactive", align:"center", fmt:v=>Array.isArray(v)? v.slice(0,3).join(" · "):v },
{ key:"payments", label:"Payments", align:"center", fmt:v=>Array.isArray(v)? v.join(", "):v },
{ key:"activeModelsK", label:"Active (K)", align:"right", fmt:v=>v+"K" },
{ key:"valueForMoney", label:"Value/5", align:"right", fmt:v=>v.toFixed(1) },
{ key:"link", label:"Visit", align:"right", action:true },
];
const itemsSorted = items.slice();
return (
{cols.map(c=> {c.label} )}
{itemsSorted.map((p,i)=> ({
cols.map(c=>{
let content=null; const cls=`${c.align==="right"?"text-right":c.align==="center"?"text-center":"text-left"} px-4 py-3`;
if(c.key==="name") content={p.name} ;
else if(c.action) content=Visit ;
else { const v=p[c.key]; content=c.fmt? c.fmt(v) : (Array.isArray(v) ? v.join(", ") : v); }
return {content} ;
})
} ))}
All figures are accurate at the time of updating (07/09/2025).
);
}
// ---------------- Calculators ----------------
function ModelCalculator(){
const [platform, setPlatform] = useState(PLATFORMS[0].name);
const [viewers, setViewers] = useState(200);
const [tippersPct, setTippersPct] = useState(5);
const [avgTip, setAvgTip] = useState(10);
const [hoursPerDay, setHoursPerDay] = useState(4);
const [daysPerWeek, setDaysPerWeek] = useState(5);
const ratePct = PLATFORMS.find(p=>p.name===platform)?.ratePct ?? 60;
const tipsPerHour = viewers*(tippersPct/100)*avgTip;
const takeHomePerHour = tipsPerHour*(ratePct/100);
const daily = takeHomePerHour*hoursPerDay;
const weekly = daily*daysPerWeek;
return (
Earnings Calculator (Models)
Platform
setPlatform(e.target.value)}>{PLATFORMS.map(p=> {p.name} ({p.ratePct}% share) )}
You keep per $100 tips
${((ratePct/100)*100).toFixed(0)}
Take-home per hour
${takeHomePerHour.toFixed(2)}
Estimated daily (net)
${daily.toFixed(2)}
Estimated weekly (net)
${weekly.toFixed(2)}
);
}
function CustomerCalculator(){
const [platform, setPlatform] = useState(PLATFORMS[0].name);
const p = PLATFORMS.find(x=>x.name===platform) || {};
const c = p.customer || {};
const [budget, setBudget] = useState(50);
const [tipSize, setTipSize] = useState(5);
const tokenPrice = c.pricePerTokenUSD ?? 0.10;
const privatePerMin = c.privatePerMinUSD ?? 2.99;
const groupPerMin = c.groupPerMinUSD ?? 1.49;
const tokens = Math.floor(budget / tokenPrice);
const privateMins = privatePerMin? (budget/privatePerMin): 0;
const groupMins = groupPerMin? (budget/groupPerMin): 0;
const tipsCount = tipSize? Math.floor(budget/tipSize): 0;
return (
Viewer Budget Planner (Customers)
Site
setPlatform(e.target.value)}>{PLATFORMS.map(pp=> {pp.name} )}
Token price: ${tokenPrice.toFixed(2)}
Private/min: ${privatePerMin.toFixed(2)}
Group/min: ${groupPerMin.toFixed(2)}
Tokens you can buy
{tokens.toLocaleString()}
Approx. private minutes
{privateMins.toFixed(1)} min
Approx. group minutes
{groupMins.toFixed(1)} min
Number of tips (avg)
{tipsCount.toLocaleString()}
Estimates assume constant prices; sites vary by room/model.
);
}
// ---------------- Views ----------------
function PlatformsView({ mode, saved, compare, onToggleSave, onToggleCompare }){
const [modelFilters,setModelFilters] = useModelFilters();
const [customerFilters,setCustomerFilters] = useCustomerFilters();
const [open,setOpen] = useState(false);
const rowsModel = filterModelRows(modelFilters);
const rowsCustomer = filterCustomerRows(customerFilters);
return (
{(mode==="model"? rowsModel.length : rowsCustomer.length)} platform{(mode==="model"? rowsModel : rowsCustomer).length===1?"":"s"} match
{mode==="model" ? (
Sort by
setModelFilters(f=>({...f,sortBy:e.target.value}))}>
Recommended
Revenue share
Estimated customers
Estimated models
Name (A–Z)
) : (
Sort by
setCustomerFilters(f=>({...f,sortBy:e.target.value}))}>
Best value
Lowest token price
Lowest private $/min
Lowest group $/min
Most active models
Name (A–Z)
)}
{mode==="model"? (
) : (
)}
);
}
function PayoutsView(){
return (
Payouts & Methods
Platform
Timeframe
Min Payout
Methods
Fees
Link
{PLATFORMS.map((p,i)=> (
{p.name}
{p.payout}
{p.payoutMin||"—"}
{(p.payoutMethods||[]).join(", ")}
{p.payoutFees||"—"}
Visit
))}
All figures are accurate at the time of updating (07/09/2025).
);
}
// ---------------- Deals Page ----------------
function DealsView({ mode }){
const colsModel = [
{ key:"headline", label:"Offer" },
{ key:"perks", label:"Incentives" },
{ key:"link", label:"Sign‑up" },
];
const colsCustomer = [
{ key:"signupBonus", label:"Sign‑up bonus" },
{ key:"welcomeCredits", label:"Welcome credits" },
{ key:"creditBonusPct", label:"Credit bonus %" },
{ key:"bundles", label:"Popular bundles" },
{ key:"finePrint", label:"Fine print" },
{ key:"link", label:"Open" },
];
const rows = PLATFORMS.map(p=> ({
name:p.name, logo:p.logo,
model: p.deals?.model || {},
customer: p.deals?.customer || {}
}));
const cols = mode==="model"? colsModel : colsCustomer;
return (
Latest deals & sign‑up incentives — {mode==="model"?"For Models":"For Customers"}
Switch mode at the top (For Models / For Customers)
Platform
{cols.map(c=> {c.label} )}
{rows.map((r,i)=> {
const data = mode==="model"? r.model : r.customer;
return (
{r.name}
{cols.map(c=>{
let content=null;
const v = data[c.key];
if(c.key==="perks"){
content = (v||[]).length? {v.map((p,idx)=>{p} )}
: "—";
} else if(c.key==="bundles"){
content = (v||[]).length? v.join(", ") : "—";
} else if(c.key==="creditBonusPct"){
content = (v||0) ? `${v}%` : "—";
} else if(c.key==="welcomeCredits"){
content = (v||0) ? `${v} credits` : "—";
} else if(c.key==="link"){
const link = data.link || "#";
content = {mode==="model"?"Sign‑up":"Open"} ;
} else {
content = v || "—";
}
return {content} ;
})}
);
})}
Deals placeholders — update with your live promos. Last updated 07/09/2025.
);
}
// Saved & Compare
function SavedView({ saved = [], onToggleSave }) {
const items = PLATFORMS.filter((p) => saved.includes(p.name));
if (!items.length) return
No saved platforms yet. On the Platforms tab, tap Save to add some.
;
return (
{items.map((p) => (
{p.name}
{p.ratePct}% share · {p.payout}
Visit
onToggleSave(p.name)} className="px-3 py-1 rounded-full border border-pink-200 bg-pink-50 text-pink-700">Remove
))}
);
}
function CompareView({ compare = [], setCompare }) {
const items = PLATFORMS.filter((p) => compare.includes(p.name));
if (!items.length) return
No platforms selected to compare. Go to Platforms and press Compare on up to 3.
;
const featCols = ["lovense", "vod", "ppvClips", "vr", "mobileApp", "twoFA", "geoBlocking"];
const remove = (name)=> setCompare(prev=> prev.filter(n=>n!==name));
const clear = ()=> setCompare([]);
return (
Clear all
Metric
{items.map((p) => (
{p.name}
remove(p.name)} className="ml-2 px-2 py-0.5 rounded-full text-[11px] border border-pink-200 bg-pink-50 text-pink-700">Remove
))}
Revenue share
{items.map((p) => {p.ratePct}% )}
Earn per $100
{items.map((p) => ${((p.ratePct/100)*100).toFixed(0)} )}
Payout
{items.map((p) => {p.payout} )}
Customers (K)
{items.map((p) => {p.estCustomersK.toLocaleString()}K )}
Models (K)
{items.map((p) => {p.estModelsK.toLocaleString()}K )}
Features
{items.map((p) => (
{featCols.map((k) => p.features[k] && {k} )}
))}
);
}
// Webcams & Toys
function WebcamsView() {
return (
{WEBCAMS.map((w) => (
{w.name}
Great for streaming
Autofocus, low-light friendly
USB plug-and-play
1080p+
Autofocus
Streamer-ready
))}
);
}
function ToysView() {
return (
{TOYS.map((t) => (
{t.name}
App + interactive cam support
Good battery life
Model & audience control modes
Interactive
App
USB-charge
))}
);
}
// ---------------- App Shell ----------------
function AppShell(){
const [active,setActive] = useState("platforms");
const [mode,setMode] = useState("model");
const [saved,setSaved] = useState([]);
const [compare,setCompare] = useState([]);
useEffect(()=>{ try{ setSaved(JSON.parse(localStorage.getItem("cc_saved")||"[]")); setCompare(JSON.parse(localStorage.getItem("cc_compare")||"[]")); }catch(e){} },[]);
useEffect(()=>{ try{ localStorage.setItem("cc_saved", JSON.stringify(saved)); }catch(e){} },[saved]);
useEffect(()=>{ try{ localStorage.setItem("cc_compare", JSON.stringify(compare)); }catch(e){} },[compare]);
const toggle=(setList,name,max=99)=> setList(prev=> prev.includes(name)? prev.filter(n=>n!==name) : [...prev,name].slice(0,max));
return (
CC
Compare Webcam Sites
Find the right platform & kit
setActive("platforms")} className={`px-4 py-2 rounded-full text-sm border ${active==="platforms"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Platforms
setActive("payouts")} className={`px-4 py-2 rounded-full text-sm border ${active==="payouts"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Payouts
setActive("deals")} className={`px-4 py-2 rounded-full text-sm border ${active==="deals"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Deals
setActive("calculator")} className={`px-4 py-2 rounded-full text-sm border ${active==="calculator"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Calculator
setActive("compare")} className={`px-4 py-2 rounded-full text-sm border ${active==="compare"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Compare
setActive("saved")} className={`px-4 py-2 rounded-full text-sm border ${active==="saved"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Saved
setActive("webcams")} className={`px-4 py-2 rounded-full text-sm border ${active==="webcams"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Webcams
setActive("toys")} className={`px-4 py-2 rounded-full text-sm border ${active==="toys"?"bg-pink-200 border-pink-400 text-pink-900":"bg-white border-slate-200 text-slate-700 hover:bg-slate-50"}`}>Toys
setMode("model")} className={`px-3 py-1.5 text-sm ${mode==="model"?"bg-pink-100 text-pink-900":"text-slate-700 hover:bg-slate-50"}`}>For Models
setMode("customer")} className={`px-3 py-1.5 text-sm border-l border-slate-200 ${mode==="customer"?"bg-blue-100 text-blue-900":"text-slate-700 hover:bg-slate-50"}`}>For Customers
Compare cam platforms by earnings , audience & features
Switch between For Models and For Customers to see the table, filters, calculator and deals page change. The left filter tab hides while open and the drawer content changes with the mode.
VOD Lovense VR PPV App
Quick tips
Higher revenue share ≠ higher income — traffic matters. Lovense support boosts interactivity and tips. Test 2–4 platforms for 2–4 weeks.
{active==="platforms" && setSaved(s=> s.includes(n)? s.filter(x=>x!==n): [...s,n])} onToggleCompare={(n)=>setCompare(s=> s.includes(n)? s.filter(x=>x!==n): [...s,n].slice(0,3))} />}
{active==="payouts" && }
{active==="deals" && }
{active==="calculator" && (mode==="model"? : )}
{active==="compare" && }
{active==="saved" && setSaved(s=> s.includes(n)? s.filter(x=>x!==n): [...s,n])} />}
{active==="webcams" && }
{active==="toys" && }
);
}
// attach to window for the shortcode boot script
window.CamCompareApp = AppShell;