Today we are opening AI Protection as a public beta.

AI Protection is for the AI features you build yourself: your chat route, your completion endpoint, the tools you expose over MCP. It is a set of server-side SDKs that run inside your route, after your own authentication and validation and immediately before the model or tool runs. If a request is denied, the model never starts, so you never pay for it.

This post is both the announcement and the setup guide. By the end you will have a Next.js chat route that:

  • keeps your existing authentication and validation in front,
  • enforces a plan-based input limit of your own,
  • reports every decision to the WebDecoy dashboard,
  • and lets WebDecoy observe automated traffic before you decide to enforce anything.

We build WebDecoy. Everything below uses our Node SDK, which is open source under Apache-2.0.

What you need

  • A Next.js App Router app using the Node.js runtime and Node.js 22.22.3 or newer. The example uses the Vercel AI SDK to call the model.
  • Your existing authentication. AI Protection does not replace it.
  • A WebDecoy account. Installing the SDK and observing traffic works on any plan.

Step 1: Get a property ID and a key

In the WebDecoy dashboard, open AI Protection and select the property for your application, then open the Setup tab. Copy the property ID.

Then go to API keys and create a key scoped to that property with Write Detections permission. This key is a server secret. Never put it in browser code.

Step 2: Install the SDK

npm install @webdecoy/ai-protection

The example also uses Vercel’s helper for the client IP:

npm install @vercel/functions

Step 3: Add environment variables

Add these to your server environment (for local work, .env.local). None of them may start with NEXT_PUBLIC_.

WEBDECOY_URL=https://ai-protection.webdecoy.com
WEBDECOY_KEY=your-property-scoped-key
WEBDECOY_PROPERTY_ID=your-property-id
WEBDECOY_SUBJECT_SECRET=a-random-string-of-at-least-32-characters

WEBDECOY_SUBJECT_SECRET is used to hash identifiers before they leave your server. Generate one with openssl rand -hex 32 and keep it the same across all your instances.

Step 4: Create the protection module

Create lib/ai-protection.ts. It builds the protection instance once, on first use, so next build does not need your secrets.

import { after } from 'next/server';
import { ipAddress } from '@vercel/functions';
import { createAIProtection, type AIProtection } from '@webdecoy/ai-protection';

// Server-side context your route passes in. Never read these from the request body.
type ChatContext = { plan: 'free' | 'paid'; inputLength: number };

let protection: AIProtection<ChatContext> | undefined;

// Created on first use, so `next build` does not need the secrets.
export function getProtection() {
  protection ??= createAIProtection<ChatContext>({
    webdecoyUrl: process.env.WEBDECOY_URL!,
    webdecoyKey: process.env.WEBDECOY_KEY!,
    propertyId: process.env.WEBDECOY_PROPERTY_ID!,
    subjectSecret: process.env.WEBDECOY_SUBJECT_SECRET!,
    scopeId: 'support-chat',
    route: '/api/chat',
    protectionMode: 'observe',
    // Keep reporting alive after the response is sent.
    waitUntil: (task) => after(() => task),
    // On Vercel, the platform sets the client IP. Elsewhere, read it from a
    // proxy you control; never trust a client-supplied X-Forwarded-For.
    resolveClientIP: (request) => ipAddress(request) ?? null,
    // Your own policy, enforced locally even while cloud detection observes.
    rules: [
      {
        id: 'free_plan_input_limit',
        mode: 'enforce',
        evaluate: (context) => ({
          allowed: context.plan === 'paid' || context.inputLength <= 4000,
          reason: 'free_plan_input_limit',
          status: 403,
        }),
      },
    ],
  });
  return protection;
}

A few things worth knowing about these options:

  • protectionMode: 'observe' records what WebDecoy’s detection would decide without blocking anyone. Start here.
  • rules are yours. This one stops free-plan users from sending prompts over 4,000 characters, and because it is in enforce mode it applies right away, independent of cloud detection.
  • resolveClientIP must return an address your hosting vouches for. ipAddress() reads the header Vercel sets. If you run behind your own proxy, read the header that proxy overwrites and block direct access to your origin. If no trusted IP is available, the SDK skips cloud scoring and reports degraded coverage rather than guessing.
  • route is a fixed template. Never pass a raw URL that could contain user IDs.

Step 5: Wrap the chat route

Here is app/api/chat/route.ts. Your authentication and validation stay first. The model call moves inside the callback you hand to the protection.

import { streamText, consumeStream } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getProtection } from '@/lib/ai-protection';
import { getSession } from '@/lib/auth'; // your existing auth

export const runtime = 'nodejs';

export async function POST(request: Request) {
  // 1. Your existing authentication and input validation stay first.
  const session = await getSession(request);
  if (!session) return Response.json({ error: 'unauthorized' }, { status: 401 });

  const body = await request.json().catch(() => null);
  const prompt = body?.prompt;
  if (typeof prompt !== 'string' || !prompt || prompt.length > 8000)
    return Response.json({ error: 'invalid_prompt' }, { status: 400 });

  // 2. WebDecoy checks the request. The callback, and the model call inside it,
  //    only runs if the request is allowed.
  return getProtection()(
    request,
    () =>
      streamText({
        model: openai('gpt-4o-mini'),
        prompt,
        abortSignal: request.signal,
      }).toUIMessageStreamResponse({ consumeSseStream: consumeStream }),
    { plan: session.plan, inputLength: prompt.length }
  );
}

getSession stands in for whatever authentication you already have. The third argument is the trusted server context your rule reads: it comes from your session, not from anything the client sent. The streamed response passes through unchanged, and request.signal means a client that disconnects also cancels the model call.

Step 6: Try it locally

Start your app and send a normal request with a valid session:

curl -N -X POST http://localhost:3000/api/chat \
  -H "Authorization: Bearer <your session token>" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Hello"}'

You get the model’s streamed response as before. Now send a free-plan prompt over 4,000 characters:

{"error":"free_plan_input_limit","request_id":"..."}

That comes back as a 403, and the model was never called. Locally there is no trusted client IP, so cloud scoring is skipped and recorded as degraded coverage. That is expected: you will see full detection once you deploy behind your hosting provider.

Step 7: Check every path without paying for a model

Before you deploy, you can check how your configuration behaves when a request is allowed, denied, streamed, cancelled, or when WebDecoy is unreachable, without calling a real model. The doctor lives in the SDK repository:

git clone https://github.com/WebDecoy/ai-protection && cd ai-protection && npm ci

Save this as check-chat.mjs in that folder, using the same options as your route:

import { checkModelProtection } from './scripts/doctor-model.mjs';

const report = await checkModelProtection({
  // The same options as your route. The doctor swaps in a stand-in runtime and
  // a stub model, so the key and secret can be placeholders.
  protection: {
    webdecoyUrl: 'https://ai-protection.webdecoy.com',
    webdecoyKey: 'placeholder',
    propertyId: process.env.WEBDECOY_PROPERTY_ID,
    subjectSecret: 'placeholder-secret-at-least-32-characters',
    scopeId: 'support-chat',
    route: '/api/chat',
    protectionMode: 'observe',
    resolveClientIP: () => null,
    rules: [
      {
        id: 'free_plan_input_limit',
        mode: 'enforce',
        evaluate: (context) => ({
          allowed: context.plan === 'paid' || context.inputLength <= 4000,
          reason: 'free_plan_input_limit',
          status: 403,
        }),
      },
    ],
  },
  allowedContext: { plan: 'free', inputLength: 120 },
  deniedContext: { plan: 'free', inputLength: 5000 },
});

for (const check of report.checks) console.log(check.status.padEnd(12), check.name);
console.log(report.passed ? 'All checks passed' : 'Some checks need attention');
WEBDECOY_PROPERTY_ID=your-property-id node check-chat.mjs
verified     allowed
verified     local_denial
verified     detector_block
verified     streaming
verified     cancellation
verified     outage
All checks passed

Each check counts calls to a stub model independently, so “the denied request never reached the model” is measured, not assumed. You may also see a few “report failed” warnings while it runs: the outage check makes the stand-in refuse everything on purpose. The report lists what it did not exercise too, including your own route handler, so it complements a real test rather than replacing one.

Step 8: Deploy and confirm in the dashboard

Deploy, send a few real requests through your app, then open AI Protection in WebDecoy. The Overview tab shows whether model requests are reporting, how many were allowed and denied, and anything that needs attention. The Model requests tab lists each decision, the checks behind it and whether your code ran.

If the Overview still says “Not set up yet”, check that the environment variables reached your server and that the key belongs to the same property.

Step 9: Turn on enforcement when you are ready

Leave observe mode on until the dashboard matches what you expect. Then switch protectionMode to 'enforce' in your code and set the detection mode to Enforce in the AI Protection Setup tab. Both are required. Enforcing cloud detection decisions requires a paid plan. Your own local rules, like the input limit above, already enforce on any plan.

Protecting MCP tools

If your AI feature exposes tools over MCP, the Node SDK includes an adapter that checks authentication, your per-tool authorization and optional work limits before each tool call. Tool listing is filtered by scope, every call checks again, and the tenant always comes from your verified caller, never from tool arguments.

The same SDK checkout has a doctor for MCP servers:

node scripts/doctor-mcp.mjs detect /path/to/your/app

detect reads your project without running it and prints the exact setup commands for your files. doctor-mcp.mjs check then runs your own allowed, forbidden, cross-tenant and outage calls against the running server. The MCP setup guide walks through it.

What the beta does not do

  • It does not replace your authentication or authorization. It runs after them.
  • It is not prompt-injection or output-safety protection. It decides whether a caller may do the work, not what the prompt says.
  • We have not published detection accuracy numbers yet. We will when there is real-world data behind them.
  • The MCP adapter is Node only for now. The Go and Python SDKs cover request admission.
  • It requires a code change in your backend. There is no drop-in proxy.

Next steps

Frequently Asked Questions

Does WebDecoy AI Protection read my prompts or model responses? +

No. The SDK sends request metadata such as the method, your route template, client IP, User-Agent and header names, plus decisions and outcomes. It does not send prompts, model responses, tool arguments, tool results, raw user identities or credentials.

What happens to my AI route if WebDecoy is unreachable? +

You choose per control. Cloud detection fails open by default, so requests keep working with degraded coverage. A limit you configure to fail closed refuses with a 503 instead. Your own local rules and authorization still apply either way.

Do I need a paid plan? +

You can install the SDK, run in observe mode and use the dashboard on any plan. Enforcing cloud detection decisions requires a paid plan. Local rules you write run in your own code either way.

Want to see WebDecoy in action?

Get a personalized demo from our team.

Request Demo