Node.js SDK
The WebDecoy Node SDK (@webdecoy/node) brings bot detection into your own backend: an in-process detection engine, deterministic tripwire rules, a self-hosted proof-of-work captcha, and JA3/JA4 fingerprint matching when your proxy supplies TLS details (the SDK does not read them from the socket; pass them as tls_info in the request metadata). Framework packages wrap it for Express, Fastify, and Next.js, and the Browser Client is its client-side counterpart.
Create a free account → · Compare all install methods
For the client-and-server architecture and package overview, see the Bot Detection SDK product page.
Installation
Section titled “Installation”npm install @webdecoy/nodeQuick Start
Section titled “Quick Start”The core method is protect(). Pass request metadata, get a decision:
import { WebDecoy } from '@webdecoy/node';
const webdecoy = new WebDecoy({ apiKey: process.env.WEBDECOY_API_KEY});
app.post('/api/submit', async (req, res) => { const { allowed, detection } = await webdecoy.protect({ method: req.method, path: req.path, ip: req.ip, // required user_agent: req.get('user-agent'), headers: req.headers, timestamp: Date.now() });
if (!allowed) { return res.status(403).json({ error: 'Bot detected' }); }
// Process legitimate request});The core protect() call has no monitor mode of its own: it returns a decision and your code chooses what to do with it. The framework packages (Express, Fastify, Next.js) default to monitor mode and only block when you set mode: 'enforce'.
Verify Your Install
Section titled “Verify Your Install”Send one request with the reserved test User-Agent. It always fires a detection through the real pipeline, works from localhost before you deploy, and shows up labeled Test in the dashboard (excluded from stats and billing):
curl -A "WebDecoy-Test/1.0" http://localhost:3000/Within a few seconds a detection with the Test category chip appears on the Detections page. If nothing appears, the request never reached the SDK: check that protect() runs for that route and that WEBDECOY_API_KEY is set (local-only mode has nowhere to report to).
Configuration
Section titled “Configuration”const webdecoy = new WebDecoy({ apiKey: 'sk_live_...', // optional, omit for local-only mode apiUrl: 'https://in.webdecoy.com', // default enableTLSFingerprinting: true, // default threatScoreThreshold: 70, // block above this score (default: 80) timeout: 5000, // request timeout (ms) debug: false});Per-request options on protect(metadata, options):
| Option | Description |
|---|---|
threshold |
Custom threat score threshold for this request |
skipLocalAnalysis |
Only use server-side detection |
metadata |
Extra data to include in the detection |
The Detection Response
Section titled “The Detection Response”protect() resolves to a decision object. Its main fields are conclusion (ALLOW, DENY, CHALLENGE, or ERROR), allowed, results (each rule’s outcome), detection, error?, ruleResult?, and agent?, plus the helpers isAllowed(), isDenied(), isChallenged(), isErrored() and deniedBy(rule). detection is:
| Field | Meaning |
|---|---|
decision |
allow, challenge, or block |
confidence |
Confidence score, 0–100 |
threat_level |
MINIMAL, LOW, MEDIUM, HIGH, or CRITICAL |
bot_detected |
Whether a bot was detected |
bot_type |
Type of bot, when detected |
detection_id |
ID for cross-referencing the Detections screen |
rule_enforced |
Whether a response rule fired |
The SDK fails open: if the detection call errors, the request is allowed and the error is surfaced on error.
Rules: Tripwires and Honeytokens
Section titled “Rules: Tripwires and Honeytokens”Rules evaluate locally, before any network call. A tripwire turns a hidden decoy link into a deterministic block:
import { WebDecoy, tripwire, honeytoken } from '@webdecoy/node';
// Generate a hidden decoy link + its secret pathconst trap = honeytoken(); // { path, linkHtml }
const webdecoy = new WebDecoy({ rules: [tripwire({ paths: [trap.path] })]});
// Inject trap.linkHtml into your pages. It's off-screen,// aria-hidden, and nofollow. Only scrapers ever request it.Rules Engine (no API key required)
Section titled “Rules Engine (no API key required)”The rules engine runs entirely inside the SDK. Rate limiting, filtering, and
tripwires work locally even with no apiKey configured:
import { WebDecoy, rateLimit, filter, tripwire, bots } from '@webdecoy/node';
const wd = new WebDecoy({ rules: [ // Fixed or sliding-window rate limiting, keyed however you like. // `window` is in seconds. rateLimit({ max: 60, window: 60, algorithm: 'sliding', action: 'THROTTLE' }),
// Filter expression language over request + enrichment data filter({ expression: 'ip.abuse_score > 80 or ip.tor', action: 'DENY' }),
// Honeypot paths that only scrapers ever request tripwire({ includeDefaults: true, action: 'DENY' }),
// Declared AI crawlers, by category bots({ categories: ['training_crawler'], action: 'DENY' }), ],});- Actions are
'DENY'or'THROTTLE'. - Every rule supports
dryRun: trueto evaluate and report without blocking. - Rule violations are batched to
POST /api/v1/sdk/violations/batch(when an API key is set) so they appear in your dashboard. - Filter expressions referencing IP intelligence use
GET /api/v1/sdk/ip/{address}/enrichment(cached in-SDK for an hour).
Blocking AI crawlers
Section titled “Blocking AI crawlers”bots() matches the agent registry that also powers
AI scraper scoring, so the category names
here are the same ones you see in your dashboard.
// Keep AI models out of your content, keep your search ranking.bots({ categories: ['training_crawler'] })
// Everything AI, minus the one you want referral traffic from.bots({ ai: true, allow: ['perplexitybot'] })
// A specific operator, slowed rather than blocked.bots({ agents: ['gptbot', 'ClaudeBot'], action: 'THROTTLE' })ai: true covers training_crawler, ai_search_crawler, ai_agent and
ai_assistant. It deliberately does not cover search_crawler, because blocking
Googlebot would deindex your site.
Filter expression fields
Section titled “Filter expression fields”| Namespace | Fields |
|---|---|
ip |
vpn, proxy, tor, relay, hosting, country, country_name, city, timezone, asn, asn_org, abuse_score, total_reports, is_high_risk |
req |
path, method, ip, user_agent, header("name") |
bot |
known, ai, category, name, id, organization, score, respects_robots |
edge |
present, class, clearance, verified, crawler, script, browser |
filter({ expression: 'bot.category == "training_crawler"' })filter({ expression: 'bot.ai and not bot.respects_robots' })filter({ expression: 'bot.category in ["generic_scraper", "headless_browser"]' })Comparisons against a missing value are always false, so a rule cannot accidentally fire on data that was never populated.
Verifying AI agents
Section titled “Verifying AI agents”detectBot() verifies Web Bot Auth signatures locally: no network call on the warm path, and no API key required. Pass a WHATWG Request or a plain { method, url, headers }:
const verdict = await webdecoy.detectBot(request);// verdict.status: 'verified' | 'impersonation' | 'claimed' | 'none'| Status | Meaning |
|---|---|
verified |
Signature checked out against the agent’s published keys |
impersonation |
It claimed to be a signing agent and the proof failed |
claimed |
Says who it is; nothing corroborates it |
none |
No agent claim at all |
To act on it, add the rule when you create the client. It denies impersonation by default:
import { WebDecoy, webBotAuth } from '@webdecoy/node';
const webdecoy = new WebDecoy({ apiKey: process.env.WEBDECOY_API_KEY, rules: [webBotAuth()],});
const result = await webdecoy.protect(metadata);// result.agent carries the verdict either wayTwo design points worth knowing:
- Key directories are curated, never fetched from the request. A signature naming an arbitrary
Signature-AgentURL will not send the SDK to that host, so a malicious agent cannot use verification as an SSRF primitive. - Directories are cached with stale-while-revalidate, so a warm verification never blocks on the network.
Requires @webdecoy/node 0.11.0 or later, and runs in edge runtimes as well as Node.
Self-Hosted Captcha
Section titled “Self-Hosted Captcha”The SDK ships a proof-of-work captcha you run on your own domain, with no third-party script:
import { Captcha } from '@webdecoy/node';
const captcha = new Captcha({ secret: process.env.WEBDECOY_SECRET });
// Verify a token submitted by the browser widgetapp.post('/login', async (req, res) => { const { valid } = await captcha.verifyToken(req.body.webdecoy_token, req.ip); if (!valid) { return res.status(403).json({ error: 'captcha failed' }); } // proceed});createCaptchaEndpoints() generates the challenge/verify HTTP handlers; the framework packages mount them for you, and the Browser Client provides the widget that solves them.
TypeScript
Section titled “TypeScript”Full type definitions ship with the package: WebDecoyConfig, RequestMetadata, ProtectResult, SDKDetectionResponse, and the rule and captcha types are all exported.
Next Steps
Section titled “Next Steps”- Express Middleware: drop-in protection for Express apps
- Fastify Plugin: the same for Fastify
- Next.js Middleware: edge middleware for Next.js
- Browser Client: the client-side half
- API Keys: get your
sk_live_key