nyasa organizes collected data into three pillars. Each one covers a different class of evidence about who or what is operating the browser.
Behavioral
Behavioral signals come from event listeners attached to the container element and document. They accumulate in real time as the user interacts. These are the richest signals for distinguishing humans from automation because they reflect physical and cognitive constraints that are hard to replicate programmatically without leaving traces.
Keystroke
interface KeystrokeSignals {
dwells: number[] // how long each key was held, in ms
flights: number[] // time between keyup and next keydown, in ms
}Keystroke timing is the most reliable behavioral signal. Human typing has high variance in both dwell and flight times because motor control is inherently imprecise. Scripts that dispatch keyboard events synthetically produce near-zero variance. Even scripts that deliberately inject random noise tend to produce variance an order of magnitude below the human baseline.
Human baseline: dwell variance above 50ms squared, flight variance above 200ms squared.
Mouse
interface MouseSignals {
pathLength: number // total number of recorded mouse positions
curvature: number[] // angular delta at each point along the path, in radians
stillnessRatio: number // fraction of samples where movement was under 2px
}Human mouse paths have natural curvature variance from neuromuscular tremor and unconscious drift. Scripted mouse paths are either perfectly straight, follow a smooth programmatic curve, or absent entirely. The curvature array captures angular change at each recorded point; a human session shows high variance across that array.
Human baseline: curvature variance above 0.1 rad squared.
Touch
interface TouchSignals {
touchCount: number // touchstart events (finger-down count)
taps: number // touchend events
pathLength: number // touchmove events
}Used alongside mouse signals. On a mobile device, zero mouse activity alongside zero touch activity is a strong pointer that no real user interaction occurred. The SDK accounts for this: the scripted bot detection will not flag a mobile session for "no mouse" alone.
Correction
interface CorrectionSignals {
backspaceCount: number // Backspace key presses
deleteCount: number // Delete key presses
correctionRatio: number // (backspaces + deletes) / typed chars
}Humans make typos and fix them. Long input with a correction ratio of zero is a reliable signal that the interaction was driven by automation. A bot that deliberately inserts random backspaces to mimic this behavior will produce a different statistical pattern than organic correction.
Paste
interface PasteSignals {
pasteRatio: number // pastedChars / totalChars (0 = all typed, 1 = all pasted)
pasteCount: number // number of paste events
charCount: number // total characters entered, typed and pasted combined
}LLM agents typically compose their response in the model's context then paste it into inputs in one or a few large chunks. A high paste ratio across all inputs, combined with no scroll events and fast completion, is a strong LLM indicator.
Scroll
interface ScrollSignals {
depths: number[] // scrollY values at each scroll event, in px
timestamps: number[] // performance.now() at each scroll event
}Humans almost always scroll while reading or navigating a longer page or flow. Agents that batch-fill inputs programmatically rarely scroll at all. Zero scroll events alongside substantial character input is one of the signals used in LLM detection.
Input origin
interface InputTypeSignals {
typed: number // insertText and insertReplacementText events
pasted: number // insertFromPaste events
dropped: number // insertFromDrop events
deleted: number // deleteContent events
programmatic: number // InputEvent with no inputType (dispatched by a script)
}Every InputEvent fired by a real keyboard or clipboard interaction carries an inputType string. When a script dispatches a bare InputEvent without an inputType, it appears as programmatic here. A high programmatic count alongside zero typed, pasted, or dropped events means the page was driven by code that bypassed the browser's input APIs entirely.
Upload
interface UploadSignals {
pickerCount: number // change events on file inputs (user opened the picker)
dragDropCount: number // drop events with files
programmaticCount: number // file count grew without a picker or drop event
filesAttached: number // total files currently attached across all file inputs
exifResults: ExifSignals[] // metadata analysis for each file from the picker
}
interface ExifSignals {
fileType: 'jpeg' | 'pdf' | 'png' | 'unknown'
hasExif: boolean // JPEG only: a missing EXIF block is suspicious for a claimed photo
software: string | null // EXIF Software tag, or PDF Producer value
aiGenerated: boolean // software string matched a known AI image generator
metadataEmpty: boolean // no readable metadata found in the file
}Files attached without a picker change event or a drop event were attached programmatically via DataTransfer. The EXIF and PDF metadata analysis is a secondary layer: it detects AI-generated images and metadata-scrubbed documents that might be submitted as identity documents.
Visibility
interface VisibilitySignals {
hiddenCount: number // times the tab transitioned to hidden visibilityState
blurCount: number // window blur events
totalHiddenMs: number // cumulative ms the page was not visible
}Tab switching and window focus loss during an interaction are normal human behaviors. Agents typically produce neither.
Click precision
interface ClickSignals {
count: number
centerOffsets: Array<[number, number]> // [dx, dy] px from element bounding box center
targeted: number // clicks on inputs, buttons, selects, or links
}Human clicks land with natural scatter of roughly 5 to 20px from the center of the target element. Automation frameworks like Playwright click at the exact bounding box center by default, producing mean offsets under 3px across multiple clicks.
Session rhythm
interface SessionRhythmSignals {
eventGaps: number[] // ms between consecutive events
maxGapMs: number // largest inactive period observed in the session
burstCount: number // distinct activity bursts (separated by gaps over 800ms)
meanBurstGapMs: number // mean silence between bursts
gapVariance: number // variance of the inter-burst gaps
}LLM agents operate in a cycle: a burst of input during the Act phase, then silence during the Decide phase, then another burst. This produces a sawtooth pattern of regular gaps that rarely appears in human sessions. The gap variance is low because inference time is consistent. In a human session, pauses are irregular.
Field timing
interface FieldTimingSignals {
fieldDwells: Record<string, number[]> // field name or id mapped to ms spent focused per visit
instantFills: number // field visits where content appeared in under 100ms
totalFields: number // distinct fields visited
}An agent that batch-fills inputs by setting values programmatically will produce multiple instant fill events across distinct fields. Two or more instant fills across at least two fields is a reliable LLM-act pattern.
Fingerprint
Fingerprint signals are stateless environment reads. They do not depend on any user interaction. They are collected once and cached for the session lifetime, with a retry on the next buildPayload() call if the first attempt was incomplete.
Webdriver markers
interface WebdriverSignals {
webdriver: boolean // navigator.webdriver is true
cdpPresent: boolean // Chrome DevTools Protocol objects found in window
playwrightPresent: boolean // Playwright injection markers found in window
}Headless browsers leave detectable artifacts. navigator.webdriver is the most obvious. Automation frameworks like Playwright also inject their own runtime objects into the browser context.
Iframe consistency
interface IframeSignals {
consistent: boolean
parentPluginCount: number
iframePluginCount: number
}In a real browser, navigator.plugins.length is identical in the parent frame and in an iframe. Headless browsers often return inconsistent counts because their plugin simulation is incomplete.
Canvas fingerprint
interface CanvasSignals {
hash: string // last 20 chars of a canvas data URL (stable per device and GPU)
supported: boolean
}Canvas rendering is deterministic per GPU and font stack. The hash characterizes the rendering environment and can identify known headless rendering profiles.
WebGL renderer
interface WebGLSignals {
vendor: string
renderer: string
supported: boolean
}Headless Chrome running on a server without a GPU falls back to SwiftShader, a software renderer. Containerized browsers often use LLVMpipe. Real browsers report the actual GPU vendor and renderer string. These are visible in the renderer field.
Audio fingerprint
interface AudioSignals {
hash: string // hash of an OfflineAudioContext render output
supported: boolean
}Audio rendering is deterministic per environment. The hash is prewarmed asynchronously when attach() is called so it is ready by the time buildPayload() runs. It can identify known headless audio profiles.
Incognito detection
interface IncognitoSignals {
isIncognito: boolean | null // null if the method was inconclusive
method: string | null // 'quota' or 'indexeddb'
}Incognito mode caps StorageManager.estimate() quota significantly and disables IndexedDB writes. Both differences are detectable without requesting any browser permission. isIncognito: null means neither method produced a conclusive result.
Timezone and locale consistency
interface TimezoneSignals {
timezone: string // IANA timezone name (e.g. "America/New_York")
timezoneOffset: number // minutes west of UTC
language: string // navigator.language
languages: string[] // navigator.languages
consistent: boolean // false if the timezone region contradicts the language country
}A VPN or datacenter proxy changes the IP geolocation but cannot change the browser's timezone or locale settings. A mismatch between the two is a signal of location spoofing. consistent: false is set when the timezone region and language country are geographically incompatible.
Device persistence
interface DevicePersistenceSignals {
deviceId: string // stable UUID written to localStorage on first visit
isNew: boolean // true if no prior UUID was found (first visit on this browser profile)
}The SDK writes a UUID to localStorage on first visit and reads it back on subsequent visits. This provides a persistent device identifier that survives page reloads and browser restarts. If localStorage is blocked or unavailable, the SDK falls back to an ephemeral UUID for the session only.
isNew: true on a suspicious session means the actor has either never visited before or cleared their browser storage.
Network
Network signals combine one stateful collector (reaction time) with two one-shot reads taken at page load (connection info and navigation timing).
Reaction time
interface ReactionSignals {
firstInputDelay: number | null // ms from first focus event to first input; null if no input yet
minInputDelay: number | null // smallest focus-to-input delay observed across all fields
engagementDelayMs: number | null // ms from attach() to first focus event
}Human reaction time between focusing a field and starting to type is physiologically bounded. It cannot be below roughly 80ms for a human. Sub-50ms reaction time is essentially impossible for a person and is reliably produced by scripts that set input.value and fire a synthetic event immediately after focus.
Connection
interface ConnectionSignals {
effectiveType: string | null // "4g" | "3g" | "2g" | "slow-2g"
rtt: number | null // browser-reported round-trip time in ms
downlink: number | null // estimated bandwidth in Mbps
saveData: boolean | null // true if the user has data-saver mode enabled
supported: boolean // false on Firefox and Safari
}Read from navigator.connection (Network Information API). Not supported in Firefox or Safari; supported will be false on those browsers and all other fields will be null. Datacenter connections tend to report low RTT and high downlink.
Page load timing
interface TimingSignals {
dnsMs: number | null // DNS lookup duration in ms
tcpMs: number | null // TCP connection duration in ms
tlsMs: number | null // TLS handshake duration in ms; null on plain HTTP
ttfbMs: number | null // time to first byte in ms
domLoadMs: number | null // DOMContentLoaded event duration in ms
supported: boolean
}Read from the Navigation Timing API. These values are fixed at page load time and do not change during the session. Datacenter and bot infrastructure typically shows near-zero DNS and TCP times because it resolves from within the same network as the target server.