I Built a Browser SDK That Classifies Sessions Into Three Categories, Not Two
CAPTCHA and fingerprinting were built for clumsy bots. In 2026, LLM agents fill forms with realistic typing cadence, patch out webdriver flags, and solve CAPTCHA for $0.50. Nyasa adds a third actor to the model: AuthorizedAgent. One SDK, 24 signals, six detection rules.

Every bot detection system I've seen works with two actors: human or bot. Block the bot, let the human through.
That model is wrong in 2026.
There is a third actor: AI agents acting legitimately on behalf of real users. Shopping assistants. Automated onboarding flows. Fintech integrations. These agents look like bots by every traditional signal: headless browser characteristics, scripted input patterns, no idle pauses. But blocking them means turning away real business.
I built Nyasa to handle all three.
Why existing tools fail now
CAPTCHA was built for bots that couldn't read distorted text. Those bots are dead. CAPTCHA farms solve challenges at $0.50 each. LLM vision models solve them in milliseconds.
Device fingerprinting catches webdriver flags and automation markers. Modern headless browsers patch those out. Playwright, Puppeteer, and every major automation framework have community patches specifically for passing fingerprint checks.
Behavioral analytics (typing cadence, mouse movement) catch scripted bots. But LLM agents don't use scripted input anymore. They type at 60-80 WPM with realistic keystroke intervals, move the mouse in curved paths, and pause at form fields before filling them.
The deeper problem is architectural. Existing systems ask: "Is this a bot?" Nyasa asks: "Who is this session?" The answer is one of three things.
The three-actor model
Nyasa classifies every session as exactly one of:
- Human: no detection rules fired
- AuthorizedAgent: holds a valid cryptographic identity claim, automatically bypasses bot rules
- UnauthorizedBot: one or more detection rules fired
The AuthorizedAgent category is the real addition. An AI shopping assistant built on top of your product shouldn't have to pass a CAPTCHA. It should present a signed identity claim, and your system should recognize and respect it. Nyasa handles that handshake.

The signal stack
24 signals total, split across three layers.
Behavioral signals (13)
| Signal | What it measures |
|---|---|
| Keystroke dwell and flight time | How long keys are held, time between keystrokes |
| Mouse path curvature | Deviation from straight-line movement |
| Paste vs typed ratio | Whether text was typed character by character or bulk-pasted |
| Click precision (center offset) | Distance from click point to element center |
| Session burst-pause rhythm | Alternation between fast activity and idle gaps |
| Backspace corrections | Correction frequency during text input |
| Scroll depth | How far down the page a session goes |
| Touch mechanics | Multi-touch patterns and pressure distribution |
| Field-level timing | Time spent on each form field before moving on |
| Input origin | Typed vs pasted vs dropped vs programmatic fill |
| Tab visibility | Whether the session loses and regains focus |
| File upload mechanics | How files are attached (drag, click, or programmatic) |
| Session rhythm | Overall pace and structure of the session |
Fingerprint signals (8)
| Signal | What it catches |
|---|---|
| Webdriver and CDP markers | Automation framework artifacts in the browser object |
| Iframe vs parent plugin consistency | Mismatches between iframe and parent context |
| Canvas fingerprint hash | Rendering environment identifier |
| WebGL renderer string | SwiftShader and LLVMpipe detection (headless GPU emulation) |
| Audio fingerprint via OfflineAudioContext | Audio processing environment |
| Incognito detection via storage quota probe | Private browsing mode |
| Timezone vs navigator.language consistency | Region mismatch between system and browser config |
| Persistent device UUID with isNew flag | Tracks whether this device has been seen before |
Network signals (3)
| Signal | What it measures |
|---|---|
| Page reaction time | Time from page load to first interaction |
| Connection type | Network type reported by Navigator API |
| Page load timing | Performance timing breakdown |
Detection rules
Six rules run against the collected signals. Each rule fires independently. Any fired rule pushes the verdict toward UnauthorizedBot, except isAuthorizedAgent which short-circuits to AuthorizedAgent regardless of other rules.
isHeadless reads the fingerprint layer for automation markers: webdriver properties, CDP exposure, WebGL renderer strings like SwiftShader or LLVMpipe, iframe/parent inconsistencies. If the browser environment looks like a headless runner, this fires.
isScripted reads behavioral signals for bot-like input patterns. Scripted bots fill fields in milliseconds, never backspace, and move between fields in perfect sequence. This rule catches that pattern.
isLLMAgent is the hardest one. I'll cover it in the next section.
isAuthorizedAgent reads the agent identity claim from window.__nyasaAgentSignature or a meta tag. If a valid claim is present, the session is classified as AuthorizedAgent and no further rules are evaluated.
isUploadAutomation checks file upload mechanics. Human uploads involve a file picker dialog or drag interaction. Programmatic uploads bypass both. This rule catches the bypass pattern.
isMultimodalBot looks for cross-signal contradictions: a session that passes one rule but shows soft signals consistent with automation across several others. It reads sibling DetectionResults directly rather than re-sampling signals, which avoids divergence when two rules read the same underlying data at different times.
isLLMAgent deep dive
This is the rule I spent the most time on. LLM agents are genuinely hard to distinguish from fast, focused humans. Seven signals, evaluated together:
- Machine-speed keystroke bursts under 20ms. Human dwell times cluster around 80-200ms. Sub-20ms bursts don't happen in human typing.
- Mouse stillness above 70%. Humans move the mouse constantly, even when not clicking. LLM-driven sessions often keep the cursor parked.
- Uniform keystroke variance near zero. Human typing has natural rhythm variation. LLM agent keystrokes have suspiciously consistent intervals.
- Zero backspace rate. Humans make corrections. An agent filling a form it computed upfront doesn't backspace.
- Pixel-perfect click precision. Humans click near the center of an element but not exactly on it. Agents click at the computed center coordinate.
- Missing field exploration. Humans often click into a field, leave, return, re-read the label. LLM agents visit each field once in sequence and move on.
- No idle micro-pauses. Humans have sub-second pauses between thoughts. Agent sessions show continuous forward progress.
No single signal is definitive. A fast typist has low keystroke variance. A focused user might not backspace. isLLMAgent requires several of these signals to align before it fires.

Feature extraction architecture
Early versions of Nyasa had each detection rule computing its own derived metrics from raw signals. That caused two problems: duplicated math across rules, and rules diverging when they read the same underlying signal at slightly different times during a session.
The feature extraction layer solves this. It runs once per session and computes 8 shared derived metrics before any detection rule evaluates. Every rule reads from the same computed values.
The metrics include things like overall typing variance, click precision distribution, and mouse activity ratio. Computing them once means a detection rule that needs "mouse stillness percentage" and a sibling rule that also needs it will always agree on the number.
isMultimodalBot benefits from this the most. It reads the DetectionResults of its sibling rules rather than re-running signal evaluation. Near-miss composition (where a session nearly triggers multiple rules without fully triggering any) gets caught without any rule having to re-sample data that may have aged out.
The verdict system
Every session gets a verdict object with three fields:
interface NyasaVerdict {
type: 'Human' | 'AuthorizedAgent' | 'UnauthorizedBot';
confidence: number; // 0.0 to 1.0
badges: DetectionBadge[]; // which rules fired or nearly fired
}Confidence is a noisy-OR score across all active rules. If one rule fires with 0.8 confidence and a second fires with 0.6, the combined score is 1 - (1 - 0.8) * (1 - 0.6) = 0.92. Multiple weak signals compound.
Badge labels tell you which rules contributed. A session might come back as UnauthorizedBot with badges for isHeadless and isLLMAgent, which tells you this is a headless LLM agent. That gets different handling than a scripted form filler.
The verdict payload ships via navigator.sendBeacon. Non-blocking, fires after the page interaction completes, survives page unload. Your analytics pipeline or backend decision layer receives it without adding latency to the user-facing flow.

Architecture

The SDK runs entirely in the browser. Signals are collected passively as the session progresses. The feature extraction layer runs on a timer and on key events. Detection rules evaluate when a verdict is requested or automatically at session end.
Dual packaging
Nyasa ships as both ESM and IIFE from a single tsup build config.
ESM for bundlers:
import { createNyasa } from '@devanshhq/nyasa';
const nyasa = createNyasa({
endpoint: 'https://your-backend.com/nyasa',
agentBypass: true,
});
nyasa.start();IIFE for script tags, no bundler required:
<script src="https://cdn.jsdelivr.net/npm/@devanshhq/nyasa/dist/nyasa.iife.js"></script>
<script>
Nyasa.createNyasa({
endpoint: '/api/nyasa',
agentBypass: true,
}).start();
</script>Same source, two outputs. The tsup config handles the split. No separate build for CDN distribution.
Installation and quick start
npm install @devanshhq/nyasaMinimal setup:
import { createNyasa } from '@devanshhq/nyasa';
const nyasa = createNyasa({
endpoint: '/api/session-verdict',
});
nyasa.start();
// Get the verdict at any point
const verdict = await nyasa.getVerdict();
console.log(verdict.type); // 'Human' | 'AuthorizedAgent' | 'UnauthorizedBot'
console.log(verdict.confidence); // 0.0 - 1.0
console.log(verdict.badges); // ['isHeadless', 'isLLMAgent', ...]On the backend, you receive the verdict via the beacon endpoint and decide what to do: allow, challenge, block, or route differently based on the type.
The authorized agent bypass
If you're building an AI agent that needs to interact with Nyasa-protected pages, set the signature before the SDK initializes:
// In your agent code, before navigating to the page
window.__nyasaAgentSignature = {
token: 'signed-jwt-from-your-auth-server',
agentId: 'shopping-assistant-v2',
issuedAt: Date.now(),
};Or via meta tag for server-rendered flows:
<meta name="nyasa-agent-signature" content="signed-jwt-here" />The isAuthorizedAgent rule reads this claim, validates the signature, and short-circuits to AuthorizedAgent. No other rules run. The session is logged as a known agent, not blocked as a bot.
This is the part that most bot detection tools don't have a concept for. If you're running a fintech integration or an AI onboarding assistant, you shouldn't have to fight your own security layer.
What it catches that others miss
Traditional fingerprinting misses LLM agents because they run in real browsers with patched automation markers. Traditional behavioral analytics miss them because modern LLM agents have realistic typing cadence.
Nyasa catches them through the combination: machine-speed micro-bursts that no human produces, combined with zero backspace rate and pixel-perfect clicks. Any one signal has false positives. All three together don't.
The multimodal rule catches the edge cases: sessions that pass fingerprinting and look almost human behaviorally but have soft contradictions across signals that don't fit either profile cleanly.
Live: nyasa.devanshtiwari.com
GitHub: github.com/Devansh-365/nyasa
npm: npm install @devanshhq/nyasa
Interested in working together?