Checkpoint Docs
Cookbooks

Detect: Beacon Integration

Add client-side signal collection for AI agent detection to your JavaScript application

Goal

Integrate the Checkpoint JavaScript Beacon into your web application to collect client-side signals for AI agent detection. By the end of this cookbook, you'll have:

  • Client-side signal collection with Web Worker offloading
  • Custom event tracking
  • Automatic offline resilience with batched uploads
  • Full TypeScript support

The Beacon collects signals and sends them to Checkpoint; classification happens server-side. There is no client-side detection result — you view classifications in the dashboard. If you need a verdict inline in a request, use Middleware or the Gateway.

Best for: React, Vue, Angular, and other SPAs where you want rich client-side signals plus custom event tracking.

Prerequisites

  • A Checkpoint account with a project created
  • A JavaScript/TypeScript application
  • Node.js 16+ and npm/yarn/pnpm

Time Estimate

15 minutes


Steps

Install the Beacon Package

npm install @kya-os/checkpoint-beacon
yarn add @kya-os/checkpoint-beacon
pnpm add @kya-os/checkpoint-beacon

Get Your Project ID

Find your Project ID on Installations — see Credentials for the exact click-path and ID format.

Add it to your environment variables:

# .env.local (Next.js)
NEXT_PUBLIC_CHECKPOINT_PROJECT_ID=acme-corp

# .env (Vite)
VITE_CHECKPOINT_PROJECT_ID=acme-corp

# .env (Create React App)
REACT_APP_CHECKPOINT_PROJECT_ID=acme-corp

Use the same Project ID as your pixel — the Beacon and Pixel report into the same project.

Initialize the Beacon

Create a small hook that initializes the Beacon, records a page view, and exposes a track helper:

// hooks/useCheckpointBeacon.ts
import { useEffect, useRef } from 'react';
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';

export function useCheckpointBeacon() {
  const beaconRef = useRef<CheckpointBeacon | null>(null);

  useEffect(() => {
    const beacon = new CheckpointBeacon({
      projectId: process.env.NEXT_PUBLIC_CHECKPOINT_PROJECT_ID!,
      debug: process.env.NODE_ENV === 'development',
      logLevel: process.env.NODE_ENV === 'development' ? 'debug' : 'error',
    });
    beaconRef.current = beacon;

    // Record the initial page view
    beacon.collect('pageview');

    return () => {
      beacon.destroy(); // flushes pending events
    };
  }, []);

  const track = (name: string, metadata?: Record<string, unknown>) => {
    beaconRef.current?.trackEvent(name, metadata);
  };

  return { track };
}

Expose it through context so any component can send events:

// components/CheckpointProvider.tsx
'use client';

import { createContext, useContext, ReactNode } from 'react';
import { useCheckpointBeacon } from '@/hooks/useCheckpointBeacon';

const BeaconContext = createContext<ReturnType<typeof useCheckpointBeacon> | null>(null);

export function CheckpointProvider({ children }: { children: ReactNode }) {
  const beacon = useCheckpointBeacon();
  return <BeaconContext.Provider value={beacon}>{children}</BeaconContext.Provider>;
}

export function useBeacon() {
  const context = useContext(BeaconContext);
  if (!context) throw new Error('useBeacon must be used within CheckpointProvider');
  return context;
}
// composables/useCheckpointBeacon.ts
import { onMounted, onUnmounted, ref } from 'vue';
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';

export function useCheckpointBeacon() {
  const beacon = ref<CheckpointBeacon | null>(null);

  onMounted(() => {
    beacon.value = new CheckpointBeacon({
      projectId: import.meta.env.VITE_CHECKPOINT_PROJECT_ID,
    });
    beacon.value.collect('pageview');
  });

  onUnmounted(() => {
    beacon.value?.destroy();
  });

  const track = (name: string, metadata?: Record<string, unknown>) => {
    beacon.value?.trackEvent(name, metadata);
  };

  return { track };
}

Use it in a component:

<script setup lang="ts">
import { useCheckpointBeacon } from '@/composables/useCheckpointBeacon';

const { track } = useCheckpointBeacon();

function handleFormSubmit() {
  track('form_submit', { formId: 'contact' });
}
</script>

<template>
  <button @click="handleFormSubmit">Submit</button>
</template>
// detection.ts
import { CheckpointBeacon } from '@kya-os/checkpoint-beacon';

const beacon = new CheckpointBeacon({
  projectId: 'acme-corp',
  debug: true,
  logLevel: 'debug',
});

// Record the initial page view
beacon.collect('pageview');

// Track custom events
document.getElementById('checkout-btn')?.addEventListener('click', () => {
  beacon.trackEvent('checkout_started', {
    cartValue: 99.99,
    itemCount: 3,
  });
});

// Flush reliably on unload
window.addEventListener('pagehide', () => {
  beacon.trackPageUnload();
});

export { beacon };

Configure Advanced Options

Customize the Beacon for your use case (see the full options table):

const beacon = new CheckpointBeacon({
  // Required
  projectId: process.env.NEXT_PUBLIC_CHECKPOINT_PROJECT_ID!,

  // Segmentation
  environment: process.env.NODE_ENV,
  tags: { version: '2.0.0' },

  // Collection & batching
  flushInterval: 5000, // ms between automatic collections/flushes (default 5000)
  batchSize: 10, // events per batch (default 10)
  maxQueueSize: 100, // max queued events (default 100)

  // Reliability
  retryAttempts: 3, // default 3
  retryDelay: 2000, // base retry delay in ms (default 2000)

  // Privacy
  respectDoNotTrack: true, // honor the browser DNT signal (default true)

  // Performance — Web Worker is auto-disabled in local dev
  useWorker: true,

  // Debugging
  debug: process.env.NODE_ENV === 'development',
  logLevel: 'error', // set to 'debug' to see verbose logs
});

There is no endpoint, autoStart, batchInterval, compression, or timeout option. The send interval is flushInterval; the destination is fixed to https://kya.vouched.id/v1/beacon.

Add Event Tracking

Send custom events with trackEvent(name, metadata):

// Track a page view for a specific route (e.g. on SPA navigation)
beacon.collect('pageview');

// Track user actions
beacon.trackEvent('button_click', {
  buttonId: 'signup-cta',
  variant: 'hero',
});

// Track form submissions
beacon.trackEvent('form_submit', {
  formId: 'contact-form',
  fields: ['name', 'email', 'message'],
});

Verify Installation

  1. Start your development server
  2. Open the browser console — with logLevel: 'debug' you'll see Beacon logs
  3. Check the Network tab for a POST to https://kya.vouched.id/v1/beacon
  4. Visit your Checkpoint dashboard → Analytics

You should see detections flowing in within seconds.


Verify It's Working

Console Verification

With debug: true and logLevel: 'debug', you'll see logs prefixed [Checkpoint Beacon]:

[Checkpoint Beacon] Beacon initialized { sessionId: '…', useWorker: false }
[Checkpoint Beacon] Data collected and sent { type: 'pageview', … }

Setting debug: true alone isn't enough to see informational logs — the log level defaults to error. Set logLevel: 'debug' (or 'info') to see initialization and send logs.

Network Verification

Open DevTools → Network and confirm the Beacon is POSTing to https://kya.vouched.id/v1/beacon after you call collect()/trackEvent() (and on the periodic heartbeat). If you see no requests, check the troubleshooting table below.

Confirm Classification in the Dashboard

Classification happens server-side. After sending events, open the dashboard Analytics tab and find your session to see how Checkpoint classified the traffic (human, ai_agent, bot, or incomplete_data). Spoofing a User-Agent in the browser does not produce a client-side class — the Beacon has no client-side verdict.


Troubleshooting

Beacon Not Sending

SymptomCauseFix
Project ID is required throwMissing projectIdSet projectId; check the env var and restart the server
No network requestsDo Not Track is onThe Beacon honors DNT by default — it sends nothing when the browser signals DNT. Set respectDoNotTrack: false only if appropriate for your use case.
No network requestsNothing triggered a send yetCall collect('pageview') / trackEvent(...), or wait for the flushInterval heartbeat
Worker error in devBundler can't serve the workerWorkers auto-disable in dev; set useWorker: false to silence it

Events Not Appearing

  • Batching — events are batched and flushed on the flushInterval. Call beacon.destroy() on teardown to flush pending events (there is no stop() method).
  • Offline — events queue when offline (up to maxQueueSize) and send on reconnect.
  • Server-side delay — allow a few seconds for detections to appear in the dashboard.

TypeScript

Type definitions ship with the package — no extra @types install is needed. Just import CheckpointBeacon (and any exported types) from @kya-os/checkpoint-beacon.


What You Learned

  • How to install and initialize the Beacon SDK (it auto-starts collection on construction)
  • How to record page views with collect() and custom events with trackEvent()
  • How to flush on teardown with destroy()
  • That classification is server-side — viewed in the dashboard, not returned on the client

Next Steps

GoalNext Cookbook
Block detected agentsMiddleware Enforcement
DNS-level enforcementGateway Setup
Server-side detectionMiddleware Detection