> ## Documentation Index
> Fetch the complete documentation index at: https://docs.epode.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js SDK: Express, Fastify, and MCP Instrumentation

> Instrument your Express or Fastify app with Agent Feedback. One middleware call adds outcome feedback to every eligible JSON, HTML, or header response.

The `@agent-feedback/node` package provides drop-in middleware for Express and Fastify. Add it once and every eligible JSON object response gains an `_agentFeedback` envelope, HTML responses receive an injected `<script>` tag, and all other responses fall back to the `Agent-Feedback` header. No handler changes are required, and the feedback telemetry queue never blocks your product response.

## Requirements

Node.js 20 or later is required. Express 4–5 and Fastify 4–5 are each optional peer dependencies — install only the one you use.

## Installation

<Steps>
  <Step title="Install the package">
    ```bash theme={null}
    npm install @agent-feedback/node
    ```
  </Step>

  <Step title="Create a product key">
    Open the Agent Feedback dashboard, create a product, and copy the API key. Store it as `AGENT_FEEDBACK_KEY` in your environment.
  </Step>
</Steps>

***

## Express middleware

Import `agentFeedback` from the `@agent-feedback/node/express` entry point and register it as a global middleware before your routes.

<CodeGroup>
  ```ts Basic theme={null}
  import { agentFeedback } from "@agent-feedback/node/express";

  const middleware = agentFeedback({
    apiKey: process.env.AGENT_FEEDBACK_KEY,
    include: ["/search", "/docs/*"],
  });

  app.use(middleware);
  ```

  ```ts With customer context theme={null}
  import { agentFeedback } from "@agent-feedback/node/express";

  const middleware = agentFeedback({
    apiKey: process.env.AGENT_FEEDBACK_KEY,
    include: ["/search", "/docs/*"],
    customerRef: (req) => req.user?.accountId,
    sessionRef: (req) => req.headers["x-session-id"] as string | undefined,
  });

  app.use(middleware);
  ```
</CodeGroup>

### Named operation override

By default the middleware derives the operation name from the Express route path. Use `middleware.wrap` to assign an explicit name for any individual handler — useful when one route serves multiple logical operations.

```ts theme={null}
app.get(
  "/search",
  middleware.wrap("product.search", async (req, res) => {
    res.json({ results: [] });
  }),
);
```

### Graceful shutdown

Call `middleware.shutdown()` before your process exits so the telemetry queue drains cleanly.

```ts theme={null}
process.on("SIGTERM", async () => {
  await middleware.shutdown();
  process.exit(0);
});
```

***

## Fastify plugin

Import `agentFeedback` from `@agent-feedback/node/fastify` and register it with `fastify.register`. The plugin uses `fastify-plugin` so it shares the same Fastify scope as your routes.

<CodeGroup>
  ```ts Basic theme={null}
  import { agentFeedback } from "@agent-feedback/node/fastify";

  await fastify.register(agentFeedback({
    apiKey: process.env.AGENT_FEEDBACK_KEY,
    include: ["/search"],
  }));
  ```

  ```ts With customer context theme={null}
  import { agentFeedback } from "@agent-feedback/node/fastify";

  await fastify.register(agentFeedback({
    apiKey: process.env.AGENT_FEEDBACK_KEY,
    include: ["/search"],
    customerRef: (request) => request.user?.accountId,
    sessionRef: (request) => request.headers["x-session-id"] as string | undefined,
  }));
  ```
</CodeGroup>

### Graceful shutdown

The plugin automatically hooks into Fastify's `onClose` lifecycle. If you need to shut down the telemetry queue independently, call `plugin.shutdown()` directly.

```ts theme={null}
const plugin = agentFeedback({ apiKey: process.env.AGENT_FEEDBACK_KEY });
await fastify.register(plugin);

// Manual shutdown (the onClose hook also calls this automatically)
fastify.addHook("onClose", () => plugin.shutdown());
```

***

## Configuration options

Pass these options to either `agentFeedback` constructor. All fields except `apiKey` are optional.

<ParamField body="apiKey" type="string" required>
  Your Agent Feedback product key. Must match the pattern `af_live_<keyId>_<secret>`. Store this in an environment variable — never commit it to source control.
</ParamField>

<ParamField body="endpoint" type="string" default="https://agent-feedback-api-production.up.railway.app">
  Override the Agent Feedback API endpoint. Useful for testing against a staging environment. Falls back to the `AGENT_FEEDBACK_URL` environment variable if set.
</ParamField>

<ParamField body="include" type="string[]">
  Glob-style path patterns that opt routes in. When this array is empty, all routes are instrumented (subject to `exclude`). Supports `*` (any segment) and `**` (any path). Examples: `["/search", "/docs/*"]`.
</ParamField>

<ParamField body="exclude" type="string[]">
  Glob-style path patterns to skip. Health probes, metrics, and internal endpoints are excluded by default (`/health`, `/healthz`, `/metrics`, `/favicon.ico`, `/robots.txt`, `/_agent-feedback/*`, `/api/v2/outcomes`). Patterns you add here extend the default list.
</ParamField>

<ParamField body="feedbackMode" type="&#x22;auto&#x22; | &#x22;ask&#x22; | &#x22;off&#x22;" default="&#x22;auto&#x22;">
  Controls the feedback instruction sent to agents.

  * `"auto"` — instructs the agent to submit a review autonomously without asking the user.
  * `"ask"` — instructs the agent to submit only when the outcome is unambiguous.
  * `"off"` — disables instrumentation entirely.
</ParamField>

<ParamField body="customerRef" type="(request: Request) => string | undefined | null">
  Callback that extracts an opaque customer identifier from the request. Agent Feedback never uses this to identify individual end-users — it groups interactions by account for aggregate insights.
</ParamField>

<ParamField body="sessionRef" type="(request: Request) => string | undefined | null">
  Callback that extracts a session identifier from the request. Sessions allow Agent Feedback to link a sequence of interactions in the dashboard.
</ParamField>

<ParamField body="runtimeHint" type="(request: Request) => string | undefined | null">
  Callback that returns a hint about the agent runtime making the request (e.g. `"claude"`, `"gpt-4o"`). Used for runtime-level analytics.
</ParamField>

<ParamField body="logger" type="Pick<Console, 'debug' | 'warn'>" default="console">
  Provide a custom logger. The SDK calls `.warn()` for non-fatal delivery failures and `.debug()` for internal diagnostics.
</ParamField>

<ParamField body="flushIntervalMs" type="number" default="500">
  How often the telemetry queue flushes in milliseconds. The queue also flushes immediately when it reaches 50 events.
</ParamField>

<ParamField body="maxQueueSize" type="number" default="1000">
  Maximum number of telemetry events held in memory. When the queue is full, the oldest event is dropped to make room for the newest.
</ParamField>

***

## Agent helper functions

The `@agent-feedback/node/agent` entry point (also re-exported from the main entry) provides two functions for building feedback-aware agent runtimes that submit outcomes deterministically rather than relying on the agent to notice the response envelope.

### `feedbackFromResponse`

Reads the `FeedbackEnvelope` from a response without executing it. Checks the parsed body first (JSON `_agentFeedback` field or HTML `<script id="agent-feedback">` tag), then falls back to the `Agent-Feedback` response header.

```ts theme={null}
import { feedbackFromResponse } from "@agent-feedback/node/agent";

const response = await fetch("https://api.example.com/search?q=logs");
const body = await response.json();
const feedback = feedbackFromResponse(response, body);
// feedback is a FeedbackEnvelope or undefined
```

### `submitProductOutcome`

Submits a compact outcome review to the URL embedded in the envelope. The helper validates the envelope, enforces the allowed-origin allow-list, and throws on invalid input or HTTP errors.

```ts theme={null}
import {
  feedbackFromResponse,
  submitProductOutcome,
} from "@agent-feedback/node/agent";

const response = await fetch("https://api.example.com/search?q=logs");
const body = await response.json();
const feedback = feedbackFromResponse(response, body);

if (feedback) {
  await submitProductOutcome(feedback, {
    outcome: "success",
    note: "The search returned the log entries needed to resolve the incident.",
  });
}
```

<Note>
  `submitProductOutcome` refuses to POST to any origin not in `allowedSubmitOrigins`. The hosted Agent Feedback service is trusted by default. Pass additional origins only when running a self-hosted endpoint.
</Note>

<Tip>
  For MCP-based agent runtimes, use `instrumentMcp` from `@agent-feedback/node/mcp` instead. MCP interactions are automatically classified as `confirmed` with the highest reliability surface.
</Tip>
