Device intelligence is computed where the signals live: in the browser or the app. That is a problem, because the client is hostile territory. Any value your JavaScript produces can be inspected, altered, or fabricated before it reaches your server. An attacker who wants to look like a trusted returning visitor simply edits the result on the way out. If your backend trusts a raw client-side payload, you have built a lock whose key is printed on the door.
Sealed results solve this. Instead of shipping a plaintext result the client could rewrite, the platform encrypts and signs the result on the client so that only your server can read it and any tampering is detectable. This article explains why raw client results cannot be trusted, what sealing actually does cryptographically, and how to verify a sealed result on your server without introducing new weaknesses.
The problem with trusting the client
Everything the fingerprinting agent computes runs on the user’s machine, under the user’s control. That means:
- Values can be forged. A visitor ID, a confidence score, or a bot flag is just data in the page. A determined attacker can replace it with whatever benefits them.
- Signals can be suppressed. A session that should carry a
botorvpnflag can have that flag stripped before submission. - Payloads can be replayed. A known-good result captured once can be resubmitted for many fraudulent sessions.
The naive fix is to call a server-side API for the real answer, and that works, but it adds a round-trip and a runtime dependency at the exact moment you need a decision. Sealing offers a different trade: let the client carry the full result, but make it cryptographically impossible to tamper with and readable only by you. This is the same trust boundary discussed in server-side versus client-side bot detection, solved with cryptography instead of an extra call.
What sealing actually does
A sealed result is the full device-intelligence payload wrapped in two cryptographic guarantees: confidentiality and integrity.
| Property | Mechanism | What it prevents |
|---|---|---|
| Confidentiality | Encryption with your server-held key | The client reading or mining the raw signals |
| Integrity | Authenticated encryption / signature | Silent tampering with the result |
| Authenticity | Key held only by your server | Forged results the server did not issue keys for |
The client receives the sealed blob and forwards it to your server. Because the decryption key never leaves your infrastructure, the client cannot read the contents to learn which signals it should fake, and cannot alter them without the authentication check failing. When your server decrypts and the authentication tag verifies, you know the payload is exactly what the agent produced. If it fails, you discard the result. There is no partial trust: a sealed result is either intact and genuine, or rejected.
Verifying a sealed result
Verification happens entirely on your server, using a key you control. The flow is: receive the blob, decrypt with authenticated encryption, check freshness, then act on the signals.
import { unseal } from "@prynt/server";
app.post("/evaluate", async (req, res) => {
let result;
try {
// Decrypts and verifies the authentication tag in one step.
// Throws if the payload was tampered with or the key is wrong.
result = await unseal(req.body.sealed, process.env.PRYNT_SEAL_KEY);
} catch {
return res.status(400).json({ error: "invalid sealed result" });
}
// Replay protection: bind to time and a one-time nonce.
if (Date.now() - result.timestamp > 60_000) {
return res.status(400).json({ error: "stale result" });
}
if (await nonceSeen(result.nonce)) {
return res.status(400).json({ error: "replayed result" });
}
await consumeNonce(result.nonce);
// Now the signals are trustworthy.
if (result.signals.bot) return res.status(403).end();
res.json({ visitorId: result.visitorId, confidence: result.confidence });
});
Three rules make this safe. Use authenticated encryption so decryption and integrity checking are one atomic operation; never decrypt and then separately validate. Keep the key server-side and rotate it on a schedule. And enforce freshness, because sealing proves the result is genuine but not that it is recent. The confidence score and the suspect score inside the payload are only as trustworthy as the freshness and replay checks around them.
When sealed results are the right tool
Sealing is not always necessary. For low-stakes analytics, a raw client result is fine. Reach for sealed results when the decision matters and the client is adversarial:
- High-value actions. Payments, withdrawals, and account changes justify the strongest integrity guarantee. See payment fraud device signals.
- Latency-sensitive paths. When a server-side lookup at decision time is too slow, a sealed result lets the client carry a trusted answer with no extra round-trip.
- Offline or edge decisions. At the edge or in environments without a live path back to the intelligence API, a self-verifying payload is ideal.
- Defense in depth. Even with a server-side call available, sealing prevents the class of attacks that forge or strip client signals.
The mechanism composes well with the rest of the stack. A sealed result can feed webhook signals for asynchronous follow-up, and it pairs naturally with reason codes so that once you trust the payload, you can explain the decision it drives. For the broader engine, see device fingerprinting.
Frequently asked questions
Why not just trust the client-side result directly?
Anything computed in the browser can be altered before it reaches your server. A raw client result can be spoofed to claim a clean visitor ID or hide a bot flag. Sealing makes tampering detectable and the payload confidential.
Do sealed results remove the need for a server-side API call?
They can. A sealed result carries the full, verified signal set, so your server can decrypt and trust it locally without a separate lookup, which cuts latency and dependency on an online call at decision time.
What happens if someone replays an old sealed result?
You bind each result to a timestamp and a nonce and reject stale or reused ones. Sealing protects integrity and confidentiality; replay protection is a separate check you must enforce on top.
Sealed results move device intelligence across the hostile client boundary without losing trust. The client carries a payload it cannot read or forge, and your server decrypts a result it can rely on. Add freshness and replay checks, keep the key close, and you get server-grade trust with client-side convenience. Read the docs for the sealing API and key management, and explore the SDKs for server verification helpers in your language.
Run it yourself
Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.