A LIVE DEMONSTRATION FOR THE PEOPLE WHO ALREADY KNOW.
Every second wasted on a scammer is a potential victim saved.
Refund scammers all reach the same step. They open the developer tools, edit the bank dashboard’s HTML to show an inflated number, and tell the panicking victim they over-refunded. That step is preventable.
Open DevTools (⌘⌥I). Try to change a single character on this page.
Below is a page that looks like the kind of bank dashboard a scammer would doctor. The numbers are wrong on purpose. Try to fix them.
Welcome back.
PERSONAL CHECKING · ACCOUNT •••• 4127 · STATEMENT PERIOD APR 1 — APR 30
CURRENT BALANCE
$11,348.52
Last updated moments ago.
Recent activity
- Today · 09:47 AMREFUND TECH SUPPORT INC.+$10,247.83
- Today · 09:42 AMACH DEBIT SUPPORT VENDOR REFUND−$100.00
- May 1 · 08:15 AMDEPOSIT HENDERSON & CO. PAYROLL+$1,200.69
- Apr 30UTILITY PG&E ELECTRIC SERVICE−$84.00
- Apr 28CARD STARBUCKS #2814−$6.75
Right-click $10,247.83. Choose Edit as HTML. Type any number you like.
How is this possible? Can’t a scammer just turn it off?
The library installs a MutationObserver the moment the page loads. The observer is captured inside a function’s closure — meaning nothing in the browser’s developer tools can reach it to switch it off. There is no global handle, no exposed reference, no toggle in the DOM. Removing the script tag after the page has loaded changes nothing: the observer already exists in memory.
The two ways an attacker could neutralize it both fail in practice:
- Disable JavaScript entirely. But the page they’re trying to spoof depends on JavaScript to render — it would collapse in front of the victim, defeating the scam.
- Intercept before page load. They’d need to monkey-patch
MutationObserverbefore any page code runs. Doable on their own machine; not doable inside a Zoom screen-share session with a panicking elder on the line.
The relevant code is roughly this:
export function freeze(targetNode) {
const original = new Map();
const originalHTML = targetNode.innerHTML;
const observer = new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'characterData') {
m.target.textContent = original.get(m.target);
} else if (m.type === 'childList') {
targetNode.innerHTML = originalHTML;
}
}
});
observer.observe(targetNode, {
characterData: true, childList: true, subtree: true,
});
}Once freeze() returns, observer is unreachable from outside. That is the whole trick.
What this isn’t.
A determined attacker on their own machine — with a custom browser extension, a userscript, or a pre-launch flag — can defeat almost any client-side defense, this one included. That is not the threat model.
The threat model is the standard refund-scam playbook: the scammer is on a Zoom or AnyDesk screen-share inside a victim’s browser, in real time, with a panicked target on the line. They reach for DevTools, type into the HTML, and need the change to stay visible long enough for the lie to land. That window is the one this library closes.
Adopt the stall.
textfreezer is roughly sixty lines of JavaScript. It works in any modern browser. Install it on every page where a number must not be doctored.
Reach the author.
For integration help, questions, or to flag something this demo missed. The form below is the only editable region on this page — go ahead, type.
Direct email: [email available with JavaScript on]