Beacon
Client-side signal collection for AI agent detection: an npm SDK that sends browser, performance, interaction, and fingerprint signals to Checkpoint on evidence, not a timer
The Checkpoint Beacon (@kya-os/checkpoint-beacon) is a client-side SDK. It collects browser, performance, interaction, and fingerprint signals from a page and sends them to Checkpoint for AI agent detection.
It sends a page view at construction, then sends again only when the evidence changes enough to matter: the first interaction, evidence doubling, the first untrusted input, a stale posture token, or the page being hidden or unloaded. It retries failed sends, queues events while offline, and sends from a Web Worker when it can.
The Beacon collects signals. Classification happens server-side, so the client never gets a detection class. View results in the dashboard. To classify inside a request, use server-side Middleware or the Gateway.
Prerequisites
- A Checkpoint Project ID, the same one your pixel uses (its
data-project-id). Use the Project ID exactly as the dashboard shows it, and use that same value in every beacon on the page. Beacons share a running beacon only when their ids match exactly: see More than one beacon on a page. The constructor throwsProject ID is required for Checkpoint Beaconwithout one. Credentials shows where to find it. - Browser-only code. The constructor reads
navigatorandwindow, so it isn't safe during server-side rendering. Construct it in a client-side effect or a plain browser script. The Beacon has no server-side component.
Installation
npm install @kya-os/checkpoint-beaconFull TypeScript definitions ship with the package.
Script tag
The package also ships a classic script build, dist/beacon.min.js. It exposes the CheckpointBeacon class on window (the class itself, not a namespace object) and is built for exactly the browser support floor. The constructor sends the first pageview:
<script src="https://cdn.jsdelivr.net/npm/@kya-os/checkpoint-beacon/dist/beacon.min.js"></script>
<script>
const beacon = new CheckpointBeacon({ projectId: '<project UUID>' });
</script>That URL has no version, so it tracks the latest release. Pin a version to control when you upgrade. If your policy requires Subresource Integrity, also add an integrity attribute for that exact file:
<script
src="https://cdn.jsdelivr.net/npm/@kya-os/checkpoint-beacon@1.4.3/dist/beacon.min.js"
integrity="sha384-<hash of that exact file>"
crossorigin="anonymous"
></script>1.4.3 is the current release. The hash is specific to the file you pin, so compute it from that exact file rather than copying one from a doc:
curl -s https://cdn.jsdelivr.net/npm/@kya-os/checkpoint-beacon@1.4.3/dist/beacon.min.js | openssl dgst -sha384 -binary | openssl base64 -AThe lazy signals chunk loads from the same CDN directory with no extra configuration, checked against a hash built into beacon.min.js (Integrity check on the lazy chunk). The script build strips console output, so debug and logLevel only log in the npm build.
Quick start
Construct the beacon
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
});Construction sends one pageview, the arrival event, and wires everything else (How it works). There is no start() method.
Record events
beacon.trackEvent('checkout_started', { cartValue: 99.99, itemCount: 3 });trackEvent() sends your own events, with JSON-serializable metadata. A client-side route change needs beacon.collect('pageview') only before 1.4.3. From 1.4.3 the beacon sends those itself (see triggers.navigation).
Clean up
Call beacon.destroy() when your app tears the beacon down, for example in a React effect cleanup.
Unload needs no code: the beacon already listens for visibilitychange and pagehide. Wiring trackPageUnload() yourself is safe too. Both routes share the same "nothing changed since the last send" check, so you get one send, not two.
window.addEventListener('pagehide', () => beacon.trackPageUnload());Verify
Load a page and open the browser's Network tab. Look for a POST to https://kya.vouched.id/api/v1/beacon. A 202 means ingest accepted the batch. It doesn't mean the session is classified yet. If nothing shows in the dashboard, see No detections in the dashboard.
Configuration
projectId is the only required option. Every other option has a default. A typical setup adds a few:
const beacon = new CheckpointBeacon({
projectId: '<project UUID>',
environment: 'production',
tags: { version: '2.0.0', region: 'us-east' },
anonymizeIp: true,
scrubQueryParams: ['token', 'email'],
});Configuration options
To send batches through your own CDN instead of straight to Checkpoint, point endpoint at a path your edge proxies. See Forward edge evidence.
For dispatch triggers, worker settings, and how several beacons on one page combine their settings, see Tuning dispatch, Web Worker mode, and More than one beacon on a page.
Read once at construction: endpoint, sessionTimeout, sessionCookieName, and
lazySignalsUrl. updateConfig() doesn't change them, so construct a new beacon instead.
anonymizeIp and the URL redaction options apply from the next send when changed through
updateConfig(), and changing anonymizeIp restarts the worker. useWorker defaults to true
when the browser supports Web Workers. enableWebWorker is an accepted alias, and useWorker
wins when both are set.
Content security policy
If your site restricts outbound requests, allow the ingest host in connect-src. If you set a custom endpoint, allow that host instead:
Content-Security-Policy: connect-src 'self' https://kya.vouched.id;The script build also needs script-src for the host it loads from, and worker mode needs worker-src for your own origin. The Network & CSP reference has the full directive table, self-hosting the companion files, Trusted Types, and cross-site iframes.
Framework integration
React
import { useEffect } from 'react';
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';
function App() {
useEffect(() => {
const beacon = new CheckpointBeacon({
projectId: process.env.NEXT_PUBLIC_CHECKPOINT_PROJECT_ID!,
});
const onPageHide = () => beacon.trackPageUnload();
window.addEventListener('pagehide', onPageHide);
return () => {
window.removeEventListener('pagehide', onPageHide);
beacon.destroy();
};
}, []);
return <div>Your app</div>;
}Construction sends the first pageview, so don't also call collect('pageview') here. That sends a second one. Under React Strict Mode in development the effect runs twice, so the beacon is constructed, destroyed, and constructed again. That is harmless: both instances read the same session cookie.
Next.js (App Router)
'use client';
import { useEffect } from 'react';
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';
export function BeaconProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const beacon = new CheckpointBeacon({
projectId: process.env.NEXT_PUBLIC_CHECKPOINT_PROJECT_ID!,
});
return () => beacon.destroy();
}, []);
return <>{children}</>;
}Render it inside app/layout.tsx. The 'use client' directive matters: the constructor must not run during server rendering.
Vue
import { onMounted, onUnmounted } from 'vue';
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';
let beacon: CheckpointBeacon | undefined;
onMounted(() => {
beacon = new CheckpointBeacon({
projectId: import.meta.env.VITE_CHECKPOINT_PROJECT_ID,
});
});
onUnmounted(() => {
beacon?.destroy();
});Troubleshooting
"Project ID is required for Checkpoint Beacon" thrown at construction
The constructor throws synchronously if config.projectId is missing or empty. Pass the same Project ID used elsewhere in your stack. See Credentials for where to find it.
No detections in the dashboard
- Check the Network tab for a
POSTtohttps://kya.vouched.id/api/v1/beacon. If it is absent, the beacon is not sending. Otherwise the status tells you why nothing lands:202accepted,404Project ID unknown,400batch rejected (the response body says why),429rate limited. - If the visitor's browser sends Do Not Track, the beacon collects and sends nothing (
respectDoNotTrackdefaults totrue). This is silent by design, not an error. - Confirm the Project ID matches the project you are viewing in the dashboard.
- A restrictive CSP, corporate proxy, or tracker blocker can block the request to
kya.vouched.id. See Content Security Policy. - Classification runs server-side after ingest, so allow a few seconds before the dashboard reflects a new session.
Worker failures don't show up anywhere
Worker initialization and worker-send failures are caught internally, and the beacon falls back to the main thread. This is expected behavior, not a bug. To see what happened, use the npm build with debug: true and logLevel: 'debug', then look for [Checkpoint Beacon]-prefixed console messages such as Worker unavailable, using main thread or Worker failed, using fallback. With a bundler, the usual cause is a missing same-origin worker script: host beacon.worker.js and pass workerUrl, or leave useWorker off.
Nothing logs even with debug: true
- The log level defaults to
'error'. SetlogLevel: 'debug'(or'info') to see initialization and send logs. - The script tag build (
beacon.min.js) strips console output entirely. Only the npm build logs.
"window is not defined" or "navigator is not defined" during server rendering
The constructor touches browser globals. Construct the beacon inside a client-side effect (a 'use client' component in Next.js) or a plain browser script, never at module top level in code that also runs on the server.
The lazy signals or the worker never load with a bundler
Both files are located relative to a <script> tag that loaded beacon.min.js, which a bundled app does not have. Host dist/beacon-lazy-signals.min.js and dist/beacon.worker.js yourself, both from the same package version, and pass their URLs as lazySignalsUrl and workerUrl (Self-host the companion files).
Beacon vs Pixel
For a lightweight, no-code alternative, see the Marketing Pixel.
Next steps
- Beacon reference: API methods, sessions, data collection, delivery, and CSP detail
- Beacon Cookbook: step-by-step beacon setup guide
- Browser posture: let your server read a session's verdict.
- Marketing Pixel: lightweight alternative for marketing sites
- Dashboard Analytics: view detection data
- Enforce: add server-side enforcement
