Sealed in Plain Sight
A price on a page is a fact someone paid to produce. It took a supplier, a pricing model, maybe someone's whole afternoon, and now it sits in public where anything that can open the page can copy it for free. For most of the web's life that was fine, because the things opening pages were mostly people. That's the part that's changing.
The web is read by software now as much as by people: search crawlers, price bots, and more and more, autonomous agents browsing on someone's behalf. The line between a helpful agent and a scraper is thin, and it lives in code I'd only ever used from the outside, never built. So I started building my own anti-bot system, Datamuro, mostly to understand two things properly: how a browser actually works, and how modern bot defences decide what's human. This post is the first piece of that, the part that protects the data itself.
The data is the product#
Scraping doesn't feel like theft, because nothing goes missing. The original stays exactly where it was. But the value was never in the bytes, it was in the work of gathering them. A catalogue of prices lets a competitor undercut you the same morning. A list of contact details is a ready-made lead list, or a spam target. A whole product database, copied cleanly, is a rival service with none of the cost of building the first one. The data on the page is often the most expensive thing the business made, published in the one format that's trivial to copy at scale.
Guessing who's asking#
The usual move is to identify the visitor: rate limits, user-agent checks, and the deeper fingerprint-and-score machinery a modern anti-bot stack runs, all trying to answer one question, is this a human? It works until it doesn't. Every signal is a guess, and a guess is something a determined scraper studies and beats. Hiding the data behind a private API is worse: it just moves the value to a JSON endpoint a scraper finds once and then calls forever, with none of the page in the way.
Leave it in place, but seal it#
My bet is different: don't guess, and don't move the data. Leave the value exactly where it belongs on the page, but ship it sealed, and only let it open for a browser that's proven it did some work first.
To a bot it's dots. To a verified browser it decodes. Flip between the two:
- Price
- Phone
A bot only ever sees the dots.
Interactive mockup: the reveal runs entirely in your browser, and no real values are ever sent. In production the plaintext is fetched from the reveal endpoint only after the browser proves it solved the challenge.
Same URL, two readers#
Here's the trick that makes everything else work: the server sends one response, and it's already sealed. There's no bot version and human version of the page to tell apart. Everyone gets the cipher and the mask. What changes is whether the reader can turn that cipher back into a value.
Because there's no unprotected endpoint handing out the plaintext, there's nothing for a scraper to point at. The only way in is the door that checks your work.
The flow, end to end#
Underneath, it's six moves. The value gets sealed on the server, the page ships the sealed form, the browser quietly works through a challenge, the server hands back a signed token as a receipt, and only then will it trade a token for plaintext.
On the page, it's almost nothing to write. A value goes out sealed, and the real thing never shows up in the response:
// Sealed per request. Only the cipher and a shape-preserving mask ship.
const price = dm.data.seal("$1,299.00");
// The browser reveals it only after it has earned a token.
// A reader with no valid token gets back nothing at all.Step three does the heavy lifting, and it's the one I'm most careful about. A real browser runs it without the user ever noticing, the way an invisible, Turnstile-style challenge does. A cheap scraper, with no real browser and no interest in spending the work, never even gets to the point of asking for a token. Step four is a plain, well-worn idea: a short-lived signed token, the same kind of receipt systems like Privacy Pass hand out for a solved challenge. It can't be forged without the server's key, and it goes stale fast, so a stolen one is worthless within seconds.
Raise the cost, not the wall#
The seal doesn't try to be unbreakable. It just makes reading expensive. Solving the challenge costs a little compute, paid once, and a real browser gets through it so fast the user never sees it. A scraper that wants a million values pays that cost a million times.
That's the whole point of proof of work: cheap to do once, painful to do at scale, so the economics tip against the exact thing you're trying to stop. You're not building a wall, you're putting a meter on the door. It's the same make it expensive or make it invisible trade every bot defence lives on, and the seal takes a bit of both.
Where the line is#
The useful way to think about the design is as a boundary. Some things are public whether you like it or not: they're in the page, so assume the attacker has already read them. Everything that actually protects the data sits on the far side of one gate.
Notice what isn't protecting anything: the fact that the challenge code is hard to read. That code is public, it ships to every browser. A scheme that leaned on nobody understanding it would already be broken. This is just Kerckhoffs's principle, the old rule that a system should stay secure even when everything about it except the key is public knowledge. The security rests on the gate: the server holds the key, grades the work, and won't hand out a plaintext to any request that can't show a valid token. Making the client hard to reverse buys time and cost, not secrecy.
Where scale still wins#
None of this is a wall, and it's worth being straight about the limits. A determined scraper running a real, full browser can pay the compute, solve the challenge, and collect a token like anyone else. The seal raises the bill, it doesn't put the data out of reach. It protects the sealed values, not the shape of the page around them, and anything a logged-in human can see, an automated logged-in session can see too. That's the point, though, not a flaw: the goal was never a wall. It's a meter, and a meter that turns cheap, casual, at-scale copying into something slow and expensive is doing its job.
The core design holds up on its own. What it doesn't have, and what the big commercial players sell on top of the same idea, is the aggregate layer. DataDome, Kasada, HUMAN (formerly PerimeterX), and Castle all run a version of what I run at the edge, but behind it sits a network that sees the same attacker across thousands of sites, models retrained constantly on fresh attack traffic, and full-time teams reversing the newest bypass the day it ships. That's a real advantage, and you buy it with scale, not with a cleverer challenge: one site on its own can't recognise a bot it's never seen on someone else's property. A per-site seal is the wrong tool in exactly one place, when you're a top target and someone is funded to beat you specifically, and that shared telemetry is what you'd be paying for. Everywhere else, raising the cost is the whole job, and the seal does that on its own.
The trade#
You can't stop the web from being read. That's what it's for, and it's more true now that so much of the reading is done by software. What you can decide is what a single read costs. Seal the data, and that cost falls on the bot pulling it a million times, not the person who reads it once. The other half of the story, how a server works out you're a bot in the first place, is its own post: How a Website Decides You're a Bot.
References
- Proof of work, the concept and its history (Wikipedia)
- Hashcash, the original proof-of-work function
- Cloudflare Turnstile, an invisible, privacy-preserving challenge
- Privacy Pass, redeeming tokens for a solved challenge (IETF working group)
- RFC 2104, HMAC: keyed hashing for message authentication
- Kerckhoffs's principle, security must not depend on obscurity (Wikipedia)
- OWASP, Automated Threats to Web Applications
- JA4+, modern network fingerprinting (FoxIO)
- Web scraping, an overview (Wikipedia)
Related articles
whatsapp-web.js runs a whole headless Chromium to act like a phone. The other door skips the browser and speaks WhatsApp's native protocol over a plain WebSocket, then hands you a raw socket and a firehose of events. whatsweb is the thin layer I built to make that socket feel like a bot.
The same bot, a user's message answered by code I wrote, costs a handful of lines on Telegram, a gateway and a bag of intents on Discord, and a whole headless browser pretending to be a phone on WhatsApp. Four libraries in, the pattern was clear, and the hard part was never the bot.