Ga naar hoofdinhoud
GIDS

Hoe actorcontext toevoegen aan een Node.js-app

Een stap-voor-stapgids voor het bouwen van middleware die registreert wie of wat achter elk verzoek zit, zodat je echt verkeer kunt bestuderen voordat je iets afdwingt.

Toshendra Sharma

Door Toshendra SharmaOprichter & CEO, AgentCredia · Updated November 13, 2026

Beginner · 9 min lezen

Waarom beginnen met actorcontext in plaats van met regels?

De meeste teams die AI-agents in hun verkeer opmerken, grijpen eerst naar een regel. Blokkeer een user agent, voeg een challenge toe, trek een rate limit aan. Het probleem is dat een regel die je schrijft voordat je het verkeer begrijpt, vaak de verkeerde mensen raakt. Legitieme assistenten worden geblokkeerd, echte gebruikers worden uitgedaagd en de automatisering waar je je echt zorgen over maakte, blijft doorkomen omdat die zich in eerste instantie nooit heeft geïdentificeerd.

Actorcontext is de stap vóór regels. Voor elk verzoek leg je vast wat je applicatie kan waarnemen over wie of wat handelt: een interactief persoon, een persoon die met een AI-assistent werkt, een autonome agent of iets dat je nog niet uit elkaar kunt houden. Je legt het bewijs achter die inschatting vast en zet dat naast de uitkomst van het verzoek. Na een week of twee hiervan heb je iets wat geen enkele regelsjabloon je kan geven: een kaart van je eigen verkeer. Als je nog wilt bepalen of agents je site überhaupt bereiken, begin dan met hoe je vaststelt of websiteverkeer van AI-agents komt.

Deze gids bouwt die laag in een Node.js-applicatie met gewone middleware. Dit werkt met Express, en dezelfde ideeën gelden ook voor Fastify, Koa, Hono of een Next.js route handler.

Wat je gaat bouwen

Aan het einde heb je:

  1. Middleware die actorbewijsmateriaal uit elk verzoek haalt.
  2. Een kleine, expliciete classificatie met een vermelde basis voor elk resultaat.
  3. Gestructureerde observatielogs die nooit veranderen hoe het verzoek wordt afgehandeld.
  4. Een manier om het resultaat aan het verzoek te koppelen zodat latere handlers het kunnen lezen.

Niets hier blokkeert een verzoek. Dat is bewust.

Stap 1: Welk bewijs kan je server eigenlijk zien?

Een server ziet minder dan mensen aannemen. Zet vóór je code schrijft de signalen op die aan je edge beschikbaar zijn:

  • Verzoeksignaturen. Sommige geautomatiseerde clients ondertekenen verzoeken met HTTP Message Signatures (RFC 9421), en sturen Signature-Input- en Signature-headers, vaak samen met een Signature-Agent-header die verwijst naar de gepubliceerde sleutels van de operator, zoals beschreven in de IETF-concepttekst voor het Web Bot Auth-protocol.
  • Verklaarde identiteit. Clients kunnen zichzelf benoemen in de User-Agent-header, in een From-header of via protocolmetadata zoals een MCP client name. MCP-clients melden een zelfgekozen clientInfo-naam in de initialize request van de revisie 2025-11-25, en de revisie 2026-07-28 draagt clientidentiteit over als per-request metadata. Dit zijn verklaringen, en iedereen kan ze kopiëren.
  • Authenticatiecontext. Of het verzoek een session cookie, een bearer token of een API key bevat, en wat voor soort credential het is.
  • Protocolvorm. Accept-headers, of de client HTML of JSON aanvraagt, of die pagina-assets laadt, en hoe die redirects en cookies verwerkt.
  • Applicatieverklaringen. Je eigen product kan dingen weten die het netwerk niet kan zien, zoals een gebruiker die een optie "AI assisted" kiest, of een verzoek dat uit je ingebouwde assistentfunctie komt.

Elk signaal heeft een andere sterkte. Een gevalideerde handtekening is controleerbaar. Een user agent string is een bewering. Een ontbrekende cookie is gewoon ontbrekend. Die apart houden is precies het doel van deze oefening.

Stap 2: Modelleer het resultaat expliciet

Bepaal de vorm van een actorresultaat voordat je de detectielogica schrijft. Een klein, getypeerd model houdt iedereen eerlijk over wat een classificatie betekent. De vier klassen die hier worden gebruikt, worden uitgelegd in Human, AI-assisted or agent: een praktische taxonomie voor applicatieverkeer.

TS
export type ActorClass = "human" | "human_ai" | "agent" | "unknown";
export type EvidenceBasis = "verified" | "declared" | "inferred" | "unknown";

export interface ActorEvidence {
  signaturePresent: boolean;
  signatureAgent?: string;
  declaredClient?: string;
  credentialType: "session" | "api_key" | "bearer" | "none";
  acceptsHtml: boolean;
  appDeclaredAssist: boolean;
}

export interface ActorResult {
  actor: ActorClass;
  basis: EvidenceBasis;
  reasons: string[];
  evidence: ActorEvidence;
  observedAt: string;
}

Twee velden zijn het belangrijkst. actor is de klasse. basis zegt waarom je dat gelooft. Een agentresultaat dat is gebaseerd op een gevalideerde handtekening en een agentresultaat dat is gebaseerd op een user agent string zijn allebei "agent", maar later verdienen ze heel verschillende behandeling, dus je mag ze nooit samenvoegen.

Stap 3: Extraheer bewijs in middleware

De extractiestap leest headers en request state. Er wordt nog niets beslist.

TS
import type { Request, Response, NextFunction } from "express";

const KNOWN_AGENT_TOKENS = ["bot", "crawler", "spider", "agent", "gpt", "claude"];

export function extractEvidence(req: Request): ActorEvidence {
  const ua = (req.get("user-agent") || "").toLowerCase();
  const auth = req.get("authorization") || "";
  const hasSession = Boolean(req.cookies?.session);

  let credentialType: ActorEvidence["credentialType"] = "none";
  if (hasSession) credentialType = "session";
  else if (auth.toLowerCase().startsWith("bearer ")) credentialType = "bearer";
  else if (req.get("x-api-key")) credentialType = "api_key";

  const declaredToken = KNOWN_AGENT_TOKENS.find((t) => ua.includes(t));

  return {
    signaturePresent: Boolean(req.get("signature") && req.get("signature-input")),
    signatureAgent: req.get("signature-agent") || undefined,
    declaredClient: declaredToken ? ua.slice(0, 200) : undefined,
    credentialType,
    acceptsHtml: (req.get("accept") || "").includes("text/html"),
    appDeclaredAssist: req.get("x-app-assist") === "1",
  };
}

Een paar opmerkingen over deze code:

  • signaturePresent registreert alleen dat signature-headers bestaan. Validering is een aparte stap, behandeld in de gids over het verifiëren van ondertekende agentverzoeken.
  • De user agent-tokenlijst is bewust eenvoudig. Die levert een verklaring op, nooit een verificatie.
  • x-app-assist staat voor wat je eigen frontend ook verstuurt wanneer een gebruiker via je ingebouwde AI-functie werkt. Vervang dit door je echte mechanisme.

Stap 4: Classificeer met een vermelde basis

Zet bewijs nu om in een resultaat. Houd de regels kort, gerangschikt van sterkste bewijs naar zwakste, en val altijd terug op Unknown.

TS
export async function classify(
  evidence: ActorEvidence,
  verifySignature: () => Promise<boolean>,
): Promise<ActorResult> {
  const reasons: string[] = [];
  const observedAt = new Date().toISOString();

  if (evidence.signaturePresent && (await verifySignature())) {
    reasons.push("request signature validated");
    return { actor: "agent", basis: "verified", reasons, evidence, observedAt };
  }

  if (evidence.appDeclaredAssist) {
    reasons.push("application reported its built-in assistant");
    return { actor: "human_ai", basis: "declared", reasons, evidence, observedAt };
  }

  if (evidence.declaredClient) {
    reasons.push("client declared itself as automated");
    return { actor: "agent", basis: "declared", reasons, evidence, observedAt };
  }

  if (evidence.credentialType === "session" && evidence.acceptsHtml) {
    reasons.push("interactive browser session");
    return { actor: "human", basis: "inferred", reasons, evidence, observedAt };
  }

  reasons.push("insufficient evidence");
  return { actor: "unknown", basis: "unknown", reasons, evidence, observedAt };
}

Let op wat dit niet doet. Het verheft een verklaring nooit tot verificatie. Het kent nooit Human toe aan een verzoek zonder interactief signaal. Als het het niet kan bepalen, zegt het Unknown. Unknown is een geldig, bruikbaar antwoord, en het vertelt je precies waar je zicht ophoudt. De volledige uitleg geven we in waarom Unknown een geldig resultaat is voor AI-verkeer.

Stap 5: Koppel het in observatiemodus

Monteer de middleware vroeg, koppel het resultaat aan het verzoek en log het. Neem er geen beslissingen op basis van.

TS
import express from "express";
import cookieParser from "cookie-parser";

const app = express();
app.use(cookieParser());

app.use(async (req, res, next) => {
  try {
    const evidence = extractEvidence(req);
    const result = await classify(evidence, async () => false);
    res.locals.actor = result;

    res.on("finish", () => {
      logActor({
        route: req.route?.path ?? req.path,
        method: req.method,
        status: res.statusCode,
        actor: result.actor,
        basis: result.basis,
        reasons: result.reasons,
      });
    });
  } catch {
    res.locals.actor = undefined;
  }
  next();
});

Drie details maken dit veilig:

  1. Het try-blok garandeert dat een fout in detectie nooit invloed heeft op de response. Observatie moet fail open zijn.
  2. Logging gebeurt op finish, zodat je de echte statuscode vastlegt en later classificaties met uitkomsten kunt vergelijken.
  3. verifySignature is stubbed op false totdat je echte validatie implementeert. Daardoor blijven ondertekende verzoeken in de buckets declared of unknown in plaats van dat je ze te hoog inschat.

Stap 6: Log voor analyse, niet alleen voor debugging

Schrijf logs die je kunt aggregeren. Een gestructureerde regel per verzoek is genoeg.

TS
function logActor(entry: Record<string, unknown>) {
  process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}

Houd persoonlijke data uit deze logs. Je hebt de route, de klasse, de basis, de redenen en de uitkomst nodig. Je hebt geen volledige IP-adressen, ruwe cookies of request bodies nodig om je verkeersmix te begrijpen. Als je toch een identifier bewaart voor correlatie, hash die dan met een roterende salt en zet een korte bewaartermijn.

Stap 7: Lees de data voordat je iets doet

Draai lang genoeg in observatiemodus om normale wekelijkse patronen te zien. Beantwoord dan een paar vragen:

  • Welke routes trekken agents aan? Documentatie-, catalogus- en zoekendpoints doen dat meestal. Account- en checkoutflows zouden dat meestal niet moeten doen.
  • Welk aandeel van de agentresultaten is verified versus declared? Die verhouding laat zien hoeveel je veilig op identiteit kunt vertrouwen wanneer je beleid schrijft.
  • Waar is Unknown geconcentreerd? Een hoog Unknown-percentage op een route betekent vaak dat je daar een signaal mist, bijvoorbeeld een pagina die je frontendcode nooit draait.
  • Verschillen de uitkomsten per klasse? Vergelijk foutpercentages, conversiestappen of misbruikmeldingen tussen klassen. Hieruit ontstaan echte beleidsideeën.

De antwoorden zullen je vaak verrassen. Teams ontdekken regelmatig dat een groot deel van hun "botprobleem" eigenlijk legitieme automatisering is die prima een gestructureerd endpoint zou gebruiken als dat bestond.

Stap 8: Stel het resultaat beschikbaar voor de rest van je app

Zodra de data stroomt, laat handlers res.locals.actor lezen voor niet-afdwingende toepassingen: analytics-tags, een responseformaat kiezen of vastleggen dat een supportticket uit een AI-assisted sessie kwam. Zulke toepassingen verbeteren de ervaring zonder dat iemand buitengesloten raakt.

TS
app.get("/docs/:page", (req, res) => {
  const actor = res.locals.actor as ActorResult | undefined;
  if (actor?.actor === "agent" && req.accepts("text/markdown")) {
    return res.type("text/markdown").send(renderMarkdown(req.params.page));
  }
  return res.render("doc", { page: req.params.page });
});

Afdwinging, zoals challenges, rate limits of step-up checks, komt later, nadat je false positives hebt gemeten. De gids over het uitrollen van actorbewuste policies behandelt dat proces.

Welke fouten moet je vermijden?

  • De user agent als identiteit behandelen. Dit is de header die het gemakkelijkst te wijzigen is. Registreer hem als een verklaring. Ook AI-operators zeggen dat: Anthropic's guidance voor zijn gehoste browser-agent merkt op dat iedereen dezelfde User-Agent-header kan sturen en raadt aan de gepubliceerde IP-adressen te controleren voordat je het verkeer als echt behandelt.
  • Standaard op Human zetten. Als je code Human zegt zodra die geen bot-signalen vindt, worden je Human-cijfers opgeblazen en vertrouwen je policies verkeerd verkeer.
  • Klasse en basis door elkaar halen. Sla altijd beide op. Een dashboard dat alleen "Agent" laat zien, verbergt het verschil tussen een geverifieerde operator en een gekopieerde string.
  • Tijdens observatie blokkeren. Zelfs een tijdelijke blokkade maakt je metingen ongeldig, omdat geblokkeerde clients hun gedrag veranderen.

Waar past Proof of Actor in dit geheel?

De bovenstaande middleware is een solide startpunt, en laat het kernmodel zien: eerst bewijs, voor elk resultaat een expliciete basis, Unknown zichtbaar gehouden en beslissingen overgelaten aan je applicatie. Proof of Actor bouwt voort op hetzelfde model met diepere signaaldekking over server-, edge-, browser- en MCP-contexten, en analytics waarmee je met vertrouwen kunt Detect, Understand en Route.

Als je hulp wilt om dit verder te brengen in een productie-app, meld je aan voor vroege toegang. Lees How Proof of Actor works om de volledige beslisflow te zien.

Bronnen

Bouw het met ons.

Sluit u aan bij het Proof of Actor founding developer program.

Eerst liever even praten? Spreek met de oprichters