A product endpoint can serve your frontend, a customer integration, and a scraper through the same URL. Your page analytics may only show the first of those.
Let's add request detection to a small Fastify API, watch a decoy request produce a verdict, and connect the results to a dashboard. We'll start in monitor mode so detection does not change the API's responses.
We build WebDecoy. This example uses its Fastify plugin and was prepared with AI assistance. It is request monitoring, separate from our FCaptcha project.
Use Node.js 22.12 or later. Create a folder and install the versions used here:
mkdir fastify-crawler-monitor
cd fastify-crawler-monitor
npm init -y
npm install --save-exact fastify@5.12.5 @webdecoy/fastify@0.18.3 @webdecoy/node@0.18.3
Create server.mjs:
import Fastify from 'fastify';
import webdecoy from '@webdecoy/fastify';
import { tripwire } from '@webdecoy/node';
const app = Fastify({ logger: false, trustProxy: false });
const apiKey = process.env.WEBDECOY_API_KEY || undefined;
await app.register(webdecoy, {
apiKey,
mode: 'monitor',
honeytoken: false,
skipPaths: ['/health'],
rules: [tripwire({ paths: ['/.env'] })],
});
app.addHook('preHandler', async (request) => {
const decision = request.webdecoyDecision;
if (!decision) return;
console.log({
route: request.routeOptions.url,
conclusion: decision.conclusion,
tripwire: decision.deniedBy('tripwire'),
dashboardConfigured: Boolean(apiKey),
});
});
app.get('/health', async () => ({ ok: true }));
app.get('/api/products', async () => ({
products: [{ id: 1, name: 'Field notebook' }],
}));
app.get('/.env', async (_request, reply) =>
reply.code(404).send({ error: 'Not found' })
);
await app.listen({ host: '127.0.0.1', port: 4310 });
Register WebDecoy before your routes and the hook that reads its decision. Fastify's hook documentation explains the lifecycle and scope.
The /.env handler returns a fixed 404. It never opens an environment file. We use a path that should not contain public application content to demonstrate a concrete rule.
Automatic HTML trap injection is disabled here with honeytoken: false. The sample is a JSON API, and we only need request observation for this walkthrough.
Start the server:
node server.mjs
From another terminal:
curl -i http://127.0.0.1:4310/api/products
curl -i http://127.0.0.1:4310/.env
The product request returns HTTP 200 and a small catalog. The decoy path returns HTTP 404, while the terminal records:
route: '/.env'
conclusion: 'DENY'
tripwire: true
dashboardConfigured: false
DENY is the rule verdict. Monitor mode lets the application continue, so the response is still the 404 defined by the handler. Seeing a refusal in the log does not mean the request was blocked.
The example only logs a route template and a few decision fields. It does not print request headers, query strings, bodies, IP addresses, or API keys.
The local tripwire works without an account. To collect cloud detections, create a WebDecoy account and create an API key for the site you want to observe.
Put the key in a local .env file:
WEBDECOY_API_KEY=your_api_key
Add .env to .gitignore. Keep the key on the server, including when you deploy. It does not belong in the frontend that calls this API.
Stop and restart the server with the file loaded:
node --env-file=.env server.mjs
Then send the reserved test request:
curl -i -A 'WebDecoy-Test/1.0' http://127.0.0.1:4310/api/products
Open Detections in WebDecoy and look for the labeled Test record. The endpoint should still return its catalog. The record verifies reporting; it does not represent an actual AI agent visiting your API.
dashboardConfigured: true only means the process received a key. It does not prove the key is valid or the report arrived. If the record is missing, check the selected site, the key, outbound connectivity, and reporting errors.
Keep your existing authentication, validation, authorization, and handlers. Add the plugin before the routes you want covered rather than replacing your server with this demo.
The demo binds to loopback and trusts no proxy headers. For deployment, configure trusted proxies for your actual network using the SDK's proxy settings. Do not trust arbitrary forwarded IP headers. Otherwise the traffic you are reviewing can be attributed to the wrong client.
This plugin uses preHandler. Requests rejected earlier in Fastify's lifecycle may never reach it. Likewise, requests served entirely by an upstream CDN do not reach this Node process. Use edge collection or access logs for those parts of the traffic.
After deploying, look at the paths, repeated requests, classification, and supporting signals. A client using a familiar AI crawler name is not automatically that company's crawler. A legitimate customer integration is automated too, which is one reason to observe before enforcing a policy.
Detections are not a complete request counter. Some requests can be allowed locally without cloud reporting, and caching can affect reporting frequency. Keep access logs for overall volume.
Start with one useful question: which automated clients are repeatedly requesting the content this API exposes? Once you can answer that, you have a better basis for deciding what to allow, limit, or investigate.
The example was tested locally on Node.js 26.5.0 with Fastify 5.12.5 and WebDecoy packages 0.18.3. Checks covered the catalog response, the tripwire verdict with its unchanged 404, the skipped health route, and the reserved test request in monitor mode.
No API key was used in those checks. Cloud delivery and real-crawler identification are not claimed as local test results.