/* ===== Sotero — real triage flow ===== Q1 funnel → Q2 amount → Q3 timing → Q4 funnel-specific → recoverability score (R0–R5) → tier route (T0–T3) → contact capture → send. Faithful to the operations plan (Parts 2 & 3). */ const { Icon } = window.SoteroSite; const DST = window.SoteroDesignSystem_d49ec1; /* ---- Q1 funnels (A–F + estate) ---- */ const FUNNELS = [ { id: 'A', icon: 'ShieldAlert', label: 'Theft, scam or fraud', sub: 'Drainer, phishing, investment scam, rug' }, { id: 'B', icon: 'Building2', label: 'Exchange won\'t release funds', sub: 'Frozen account, KYC, withdrawal lock' }, { id: 'C', icon: 'ArrowLeftRight', label: 'A transaction went wrong', sub: 'Wrong chain/address, stuck, failed swap' }, { id: 'D', icon: 'KeyRound', label: 'Lost wallet access', sub: 'Lost seed, password, or dead device' }, { id: 'E', icon: 'LifeBuoy', label: 'General help & setup', sub: 'Networks, gas, dApp use, a quick question' }, { id: 'G', icon: 'Users', label: 'Inheritance / estate access', sub: 'Access a loved one\'s crypto' }, ]; const AMOUNTS = ['< $1k', '$1k–10k', '$10k–50k', '$50k–250k', '> $250k']; const TIMINGS = ['In the last 24h', 'This week', 'This month', 'More than a month', 'More than a year']; /* ---- Q4 branches. Each option carries a recoverability signal r (R-score). ---- */ const Q4 = { A: [ { q: 'Do you have the transaction hash of the theft?', opts: [{ t: 'Yes, I have the hash', s: 'hash' }, { t: 'No / not sure', s: 'nohash' }] }, { q: 'Where did the funds end up?', opts: [ { t: 'A known exchange (Binance, Coinbase…)', s: 'cex', r: 4 }, { t: 'An unknown private wallet', s: 'wallet', r: 3 }, { t: 'Through a mixer / Tornado Cash', s: 'mixer', r: 0 }, { t: 'I don\'t know', s: 'unknown', r: 3 }, ] }, { q: 'How did it happen?', opts: [ { t: 'I approved / signed something malicious', s: 'approval' }, { t: 'I entered my seed on a fake site', s: 'phish' }, { t: 'Investment / "pig butchering" scam', s: 'invest' }, { t: 'Other', s: 'other' }, ] }, ], B: [ { q: 'Which exchange is holding your funds?', free: true, ph: 'e.g. Coinbase, Binance, Kraken' }, { q: 'What is your account status?', opts: [ { t: 'Frozen / locked', s: 'frozen', r: 3 }, { t: 'Stuck in KYC / verification', s: 'kyc', r: 3 }, { t: 'Withdrawal on hold', s: 'hold', r: 3 }, { t: 'Locked out (password / 2FA / email)', s: 'lockout', r: 2 }, ] }, { q: 'Is your identity verification (KYC) complete?', opts: [{ t: 'Yes, KYC is complete', s: 'kycdone', r: 3 }, { t: 'No / partially', s: 'kycno', r: 2 }] }, ], C: [ { q: 'What went wrong?', opts: [ { t: 'Sent to the wrong address (self-custody)', s: 'wrongaddr', r: 0 }, { t: 'Sent on the wrong chain to an exchange', s: 'wrongchain', r: 3 }, { t: 'Transaction stuck / pending', s: 'stuck', r: 2 }, { t: 'Swap or bridge failed mid-route', s: 'bridge', r: 2 }, { t: 'Missing memo / tag on a deposit', s: 'memo', r: 3 }, ] }, { q: 'Do you have the transaction hash?', opts: [{ t: 'Yes', s: 'hash' }, { t: 'No', s: 'nohash' }] }, ], D: [ { q: 'What do you still have access to?', opts: [ { t: 'Nothing — no seed, no password, no device', s: 'none', r: 0 }, { t: 'A partial seed phrase', s: 'partial', r: 3 }, { t: 'The device, but forgot the password', s: 'device', r: 3 }, { t: 'Wallet opens but shows zero balance', s: 'zero', r: 2 }, { t: 'A broken / bricked hardware wallet', s: 'hardware', r: 3 }, ] }, { q: 'Which wallet were you using?', free: true, ph: 'e.g. MetaMask, Ledger, Trust Wallet' }, ], E: [ { q: 'Briefly, what do you need help with?', free: true, ph: 'Describe your question', r: 1 }, ], G: [ { q: 'What do you need help with?', opts: [ { t: 'Discovery — do they even hold crypto?', s: 'discovery', r: 2 }, { t: 'Accessing an exchange account', s: 'estcex', r: 3 }, { t: 'Recovering a self-custody wallet', s: 'estwallet', r: 3 }, { t: 'Probate-ready documentation', s: 'estdocs', r: 3 }, ] }, { q: 'Do you have executor / probate documents?', opts: [{ t: 'Yes', s: 'docs', r: 3 }, { t: 'Not yet', s: 'nodocs', r: 2 }] }, ], }; /* ---- Recoverability + tier routing (first match wins, plan Part 2/3) ---- */ function classify(state) { const { funnel, amount, answers } = state; const amtIdx = AMOUNTS.indexOf(amount); // 0..4 ; >=3 is $50k+ const big = amtIdx >= 3; // base recoverability = min r-signal seen across Q4 answers (worst honest read), // but take the routing-relevant one: the last option with an explicit r wins, // else fall back per funnel. let r = null; Object.values(answers || {}).forEach((a) => { if (a && typeof a.r === 'number') r = (r === null ? a.r : Math.min(r, a.r)); }); if (r === null) r = funnel === 'E' ? 1 : 3; // forensics / legal escalation for large theft with a trail if (funnel === 'A' && big && r >= 3) r = 5; // route let tier; if (r === 0) tier = 'T0'; else if (r === 1) tier = 'T0'; else if (r === 5) tier = 'T3'; else if (r === 4) tier = big ? 'T3' : 'T2'; else if (r === 3) tier = big ? 'T2' : (amtIdx >= 1 ? 'T2' : 'T1'); else tier = amtIdx >= 1 ? 'T1' : 'T0'; // r === 2 return { r, tier }; } const R_COPY = { 0: { badge: 'Likely unrecoverable', tone: 'critical', head: 'We\'ll be honest with you.', body: 'Based on what you\'ve told us, this is very likely unrecoverable — and we won\'t charge you to chase something that can\'t be caught. Below is exactly what to check yourself, for free. If anything changes (you find a partial seed, a device, or a traceable destination), come back and we\'ll re-open it.' }, 1: { badge: 'Educational', tone: 'info', head: 'This one you can likely solve yourself.', body: 'This is a common question with a clear answer. We\'ll send you the exact walkthrough — no charge, no account needed.' }, 2: { badge: 'Good — solvable', tone: 'teal', head: 'There\'s a clear path here.', body: 'This is a low-complexity case that usually resolves with the right steps. Start with our guided fix; if it needs a human, a written review is a small flat fee.' }, 3: { badge: 'Recoverable with work', tone: 'teal', head: 'This is worth pursuing.', body: 'There\'s a real administrative or technical path here that needs cooperation and process work. A specialist can own this end to end.' }, 4: { badge: 'Time-critical', tone: 'warning', head: 'This is urgent — let\'s move now.', body: 'Funds routed to a traceable destination can sometimes be frozen, but the window is short. We\'ll prioritize your case.' }, 5: { badge: 'Legal / forensic', tone: 'critical', head: 'This needs forensic and legal coordination.', body: 'A loss of this size with an on-chain trail warrants a principal-led engagement — forensic tracing plus attorney and law-enforcement-ready documentation.' }, }; const TIER_META = { T0: { name: 'Self-serve', next: 'Here\'s your instant guide below — free, no account.' }, T1: { name: 'Written review', next: 'A specialist reviews your case and replies within 24 hours. Flat fee $99–149.' }, T2: { name: 'Case support', next: 'A specialist is assigned within 24h and books a call. $500–1,500 + success fee where funds are returned.' }, T3: { name: 'Priority case', next: 'A principal calls you back within 4 business hours. Retainer $2,500–10,000 + success fee.' }, }; /* ---- T0 self-serve answers. Keyed by the Q4 signal (falls back per funnel). ---- */ function t0Content(funnel, answers) { const sigs = Object.values(answers || {}).map((a) => a && a.s).filter(Boolean); const has = (s) => sigs.includes(s); if (has('zero')) return { title: 'Your wallet is almost certainly on the wrong network', steps: [ 'Wallets only display the one blockchain they\'re connected to — the funds are usually still there.', 'Open your wallet\'s network selector (top of the app) and switch to the chain you used (Ethereum, Polygon, BSC, etc.). Your balance should reappear.', 'If the network isn\'t listed, add it manually via its official RPC settings (never a link someone DMs you).', 'Still zero on every network? Reply and we\'ll help you check the block explorer for your address.', ] }; if (has('stuck')) return { title: 'How to clear a stuck / pending transaction', steps: [ 'A transaction is usually stuck because the gas fee was too low for current network conditions.', 'In your wallet, find the pending transaction and choose "Speed up" (resubmits with higher gas) or "Cancel".', 'If there\'s no button, send a $0 transaction to yourself using the SAME nonce to replace it (advanced — ask us if unsure).', 'Once the nonce clears, later transactions will go through.', ] }; if (has('bridge')) return { title: 'Failed swap or stuck bridge — first steps', steps: [ 'Bridges are often slow, not broken — funds can take minutes to hours to arrive on the destination chain.', 'Find your transaction on the bridge\'s own status/explorer page using your tx hash.', 'Check the destination wallet on the correct network (see the wrong-network guide).', 'If it\'s been over a few hours with no arrival, reply with your tx hash and we\'ll trace it.', ] }; if (has('memo')) return { title: 'Missing memo / tag on an exchange deposit', steps: [ 'Deposits to exchanges on chains like XRP, XLM, ATOM, and BNB require a memo/tag. Without it, the exchange holds the funds — they\'re usually recoverable.', 'Do NOT send anything else. Open a support ticket with the exchange and give them your tx hash, amount, and timestamp.', 'They\'ll ask you to verify ownership; most credit the deposit within days.', 'We can prepare the exchange escalation packet if their support stalls.', ] }; if (has('none')) return { title: 'A fully lost seed with no backup or device', honesty: true, steps: [ 'We\'ll be straight with you: a 12- or 24-word seed has astronomically many combinations. It cannot be brute-forced. If there is truly no seed, no partial, no password, and no device, the funds are unrecoverable.', 'Before you accept that, search hard: notebooks, safe deposit boxes, old phones, USB drives, password managers, and email for anything you saved.', 'Remember any fragment — even a few known words or a device that still opens opens a real recovery path. If you find anything, come back and we\'ll open a case.', 'Please ignore anyone who promises to "recover" a fully lost seed for a fee. That is always a scam.', ] }; if (has('wrongaddr')) return { title: 'Funds sent to a wrong self-custody address', honesty: true, steps: [ 'Honest answer: if the funds went to a valid address you don\'t control (a typo, a stranger\'s wallet), there is no technical way to reverse it. The blockchain is final.', 'The only path is if the address happens to belong to someone reachable, or to an exchange (then it may be recoverable — tell us).', 'If it went to an exchange deposit address by mistake, contact that exchange with your tx hash right away.', 'Do not pay anyone claiming they can "reverse" an on-chain transaction. They cannot.', ] }; if (has('mixer')) return { title: 'Funds traced into a mixer', honesty: true, steps: [ 'When stolen funds are pushed through a mixer like Tornado Cash with no identifiable exit, the trail is broken and recovery is not realistically possible.', 'If any portion touched a known exchange before or after mixing, that part may still be traceable — tell us the tx hash and we\'ll check.', 'File a report with law enforcement (IC3 in the US) for the record.', 'Beware follow-up "recovery" scammers who target victims after a theft.', ] }; if (funnel === 'E') return { title: 'Your quick question', steps: [ 'Based on what you described, this is something you can most likely handle yourself.', 'We\'ll email you the exact steps and the relevant guide.', 'If it turns out to be more involved, we\'ll point you to the right paid tier — no pressure.', ] }; return { title: 'Your guided next steps', steps: [ 'This looks like a low-complexity case with a clear self-serve path.', 'We\'ll email you the specific walkthrough for your situation.', 'If a human needs to step in, a written review is a small flat fee — never charged for hopeless cases.', ] }; } /* ---- UI ---- */ function OptionButton({ children, active, onClick, sub }) { return ( ); } const inpT = { width: '100%', height: 44, padding: '0 14px', background: 'var(--surface-sunken)', border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-md)', color: 'var(--text-primary)', fontFamily: 'var(--font-body)', fontSize: 14, outline: 'none', boxSizing: 'border-box' }; function IntakeModal({ open, onClose }) { const { Button } = DST; const [phase, setPhase] = React.useState('q1'); // q1,q2,q3,q4,result,contact,done const [funnel, setFunnel] = React.useState(null); const [amount, setAmount] = React.useState(null); const [timing, setTiming] = React.useState(null); const [q4i, setQ4i] = React.useState(0); const [answers, setAnswers] = React.useState({}); const [free, setFree] = React.useState({}); const [contact, setContact] = React.useState({ name: '', email: '', phone: '', telegram: '' }); const [sending, setSending] = React.useState(false); const [err, setErr] = React.useState(''); const [result, setResult] = React.useState(null); const scrollRef = React.useRef(); const reset = () => { setPhase('q1'); setFunnel(null); setAmount(null); setTiming(null); setQ4i(0); setAnswers({}); setFree({}); setContact({ name: '', email: '', phone: '', telegram: '' }); setErr(''); setSending(false); setResult(null); }; React.useEffect(() => { if (open) reset(); }, [open]); React.useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [phase, q4i]); if (!open) return null; const q4list = funnel ? Q4[funnel] : []; const stepNum = { q1: 1, q2: 2, q3: 3, q4: 4, result: 5, contact: 5, done: 5 }[phase]; const finishQ4 = () => { const res = classify({ funnel, amount, answers }); setResult(res); setPhase('result'); }; const advanceQ4 = () => { if (q4i < q4list.length - 1) setQ4i(q4i + 1); else finishQ4(); }; const send = async () => { setErr(''); if (!contact.name.trim() || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(contact.email)) { setErr('Please enter your name and a valid email.'); return; } setSending(true); try { const caseId = 'STR-' + Date.now().toString(36).toUpperCase().slice(-6); const q4summary = q4list.map((q, i) => { const a = answers[i]; const f = free[i]; return q.q + ' — ' + (a ? a.t : '') + (f ? (' ' + f) : ''); }).join('\n'); const body = new FormData(); body.append('case_id', caseId); body.append('case_type', FUNNELS.find((x) => x.id === funnel)?.label || funnel); body.append('amount', amount || ''); body.append('timing', timing || ''); body.append('recoverability', 'R' + result.r + ' (' + R_COPY[result.r].badge + ')'); body.append('recommended_tier', result.tier + ' ' + TIER_META[result.tier].name); body.append('details', q4summary); body.append('name', contact.name); body.append('email', contact.email); body.append('phone', contact.phone); body.append('telegram', contact.telegram); body.append('source', location.href); const res = await fetch('submit.php', { method: 'POST', body }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) throw new Error(data.error || 'send failed'); setPhase('done'); } catch (e) { setErr('Something went wrong sending your request. Please email support@sotero.net directly.'); } finally { setSending(false); } }; const rc = result ? R_COPY[result.r] : null; const tm = result ? TIER_META[result.tier] : null; const toneColor = (t) => t === 'critical' ? 'var(--status-critical)' : t === 'warning' ? 'var(--status-warning)' : t === 'info' ? 'var(--status-info)' : 'var(--accent)'; return (
{rc.body}
We've logged your case as {result ? result.tier : ''} · {result ? R_COPY[result.r].badge : ''} and sent it to our team. {result && (result.tier === 'T3' ? 'A principal will call you back within 4 business hours.' : result.tier === 'T2' ? 'A specialist will be assigned within 24 hours.' : result.tier === 'T1' ? 'You\'ll get a reviewed reply within 24 hours.' : 'Check your email for your free guide.')}
Reminder: we will never ask for your seed phrase, keys, or password.