> For an index of all Botscent documentation, see https://botscent.nibnalin.me/llms.txt.

# Server API (TypeScript)

Every function, type and adapter of the TypeScript server half, with its options.

The server half reads the headers of one request. It runs on Node.js 22.12 or later, Cloudflare Workers, Vercel, Deno and Bun. The functions come from `botscent/server`. Each adapter runs the server half inside one framework. For the Python server half, see [Server API (Python)](/docs/python-api).

## `inspect`

Returns the request's own verdict. Reads headers only, and makes no network request.

```ts title="Signature"
function inspect(request: RequestLike, options?: InspectOptions): Promise<Verdict>

type RequestLike =
  | Request
  | {
      headers: Headers | { get(name: string): string | null } | Record<string, string | string[] | undefined>
      method?: string
      url?: string
    }

type InspectOptions = {
  cf?: CloudflareHints | null
  now?: number
  debug?: boolean | ((line: string) => void)
}

type CloudflareHints = {
  verifiedBotCategory?: string | null
  botManagement?: { verifiedBot?: boolean | null } | null
}
```

```ts title="app/api/checkout/route.ts"
import { inspect } from 'botscent/server'

export async function POST(request: Request) {
  const verdict = await inspect(request)
  // => { type: 'agent', agent_name: 'curl', reasons: ['ua.declared-agent-token'] }
  return Response.json(verdict)
}
```

### Parameters

| Name            | Type                                  | Default      | Description                                                                        |
| --------------- | ------------------------------------- | ------------ | ---------------------------------------------------------------------------------- |
| `request`       | `RequestLike`                         | (required)   | A Fetch `Request`, or an object with `headers` and an optional `method` and `url`. |
| `options.cf`    | `CloudflareHints`                     | (none)       | Cloudflare's `request.cf`, read for its verified-bot field.                        |
| `options.now`   | `number`                              | `Date.now()` | The request time in milliseconds since the epoch, for tests and replays.           |
| `options.debug` | `boolean \| ((line: string) => void)` | (none)       | Logs each decision with `console.debug`, or sends each line to the function.       |

### Returns

`Promise<Verdict>`. See [The verdict](/docs/verdict). The function is asynchronous because WebCrypto verifies signatures asynchronously.

### Notes

* `inspect` reads the request headers, the method, the URL when given, and the platform hints. It reads no body and no page report.
* `inspect` never throws. On an internal failure, it returns the verdict of the evidence so far, at worst `{ type: 'human', reasons: [] }`.
* `inspect` verifies signatures against signer keys bundled in the release. It makes no network request.
* If `debug` is unset, `BOTSCENT_DEBUG=1` in `process.env` turns on debug output. Each line has the prefix `[botscent]` and shows tokens, signatures, hints and the verdict.
* In Nuxt, pass `event.node.req`. In SvelteKit, pass `event.request`.

## `readReport`

Parses a page report. Returns a verdict whose reasons carry the `page.` prefix, or `null`.

```ts title="Signature"
function readReport(value: string | Verdict | null | undefined): Verdict | null
```

```ts title="app/api/checkout/route.ts"
import { inspect, readReport } from 'botscent/server'

export async function POST(request: Request) {
  const own = await inspect(request)
  const report = readReport(request.headers.get('botscent-report'))
  // => { type: 'agent', reasons: ['page.browser.webdriver-flag'] }
  return Response.json({ request: own, report })
}
```

### Parameters

| Name    | Type      | Default    | Description                                                                                    |
| ------- | --------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `value` | `unknown` | (required) | The `Botscent-Report` header, the form's `botscent` field, or a verdict object from `verdict`. |

### Returns

`Verdict | null`. The value is `null` for anything malformed, longer than 256 bytes, or of an unknown major version.

### Notes

* Every reason of a parsed report gets the `page.` prefix. A report therefore never passes `isVerified`.
* A verdict object must follow the same grammar as the wire string.
* A person's page sends no report, so `readReport` returns `null` for the missing header.

> **Warning:** Do not grant access on a page report. Scripts on the page can forge it. Use [`isVerified`](#isverified) on the verdict from `inspect`.

## `combine`

Adds a page report's evidence to the request's own verdict.

```ts title="Signature"
function combine(request: Verdict, report: Verdict | null | undefined): Verdict
```

```ts title="app/api/checkout/route.ts"
import { combine, inspect, readReport } from 'botscent/server'

export async function POST(request: Request) {
  const own = await inspect(request)
  const report = readReport(request.headers.get('botscent-report'))
  return Response.json({ request: own, report, combined: combine(own, report) })
}
```

### Parameters

| Name      | Type                           | Default    | Description                               |
| --------- | ------------------------------ | ---------- | ----------------------------------------- |
| `request` | `Verdict`                      | (required) | The request's own verdict from `inspect`. |
| `report`  | `Verdict \| null \| undefined` | (required) | The page report from `readReport`.        |

### Returns

`Verdict`. The combined verdict is an agent when either verdict is an agent. Its reasons are the request's reasons, then the report's reasons.

### Notes

* The `agent_name` is the request's name when it has one. Otherwise it is the report's name.
* A missing or empty report returns the request verdict unchanged.
* A combined verdict is for measurement and for changes to the interface. It is never for access.

## `isVerified`

Returns `true` when the request itself was verified. With a name, returns `true` only when a verified signature names that agent.

```ts title="Signature"
function isVerified(verdict: Verdict | null | undefined, name?: AgentName): boolean
```

```ts title="app/api/checkout/route.ts"
import { inspect, isVerified } from 'botscent/server'

export async function POST(request: Request) {
  const verdict = await inspect(request)
  if (!isVerified(verdict, 'chatgpt')) return new Response('Forbidden', { status: 403 })
  return Response.json({ ok: true })
}
```

### Parameters

| Name      | Type                           | Default    | Description                                         |
| --------- | ------------------------------ | ---------- | --------------------------------------------------- |
| `verdict` | `Verdict \| null \| undefined` | (required) | The request's own verdict from `inspect`.           |
| `name`    | `AgentName`                    | (none)     | The agent name that a verified signature must give. |

### Returns

`boolean`. Without `name`, the value is `true` when `reasons` contains `signer.web-bot-auth.verified` or `signer.edge-verified-bot` without a prefix. With `name`, it is `true` when `reasons` contains `signer.web-bot-auth.verified`, `agent_name` is `name`, and no reason has the `page.` prefix.

### Notes

* Only `inspect` produces the two verified reasons.
* The platform's verified-bot field never satisfies a check with a name. The field says that some bot was verified, not which bot.
* To let one agent through, use `isVerified(verdict, 'chatgpt')`. The check `isVerified(verdict) && verdict.agent_name === 'chatgpt'` can pair a verified bot with a name that it only declared.
* Signer keys are pinned in each release. A key that a signer removes still verifies until the installation upgrades. See [Versions and stability](/docs/versions).
* Verification authenticates the operator's infrastructure, not the person. See [The trust model](/docs/trust-model).

> **Warning:** Do not use `agent_name` alone to allow access. Anyone can send the headers that produce a name. Use `isVerified(verdict, name)`.

## `VERSION`

Holds the version of the library.

```ts title="Signature"
const VERSION: string
```

```ts title="src/version.ts"
import { VERSION } from 'botscent/server'

console.log(VERSION)
```

## `botscent/names.json`

Gives every agent name as data, with its display name, vendor and kind.

```ts title="Shape"
Record<string, { display: string; vendor: string; kind: string }>
```

```ts title="src/names.ts"
import names from 'botscent/names.json'

console.log(names['chatgpt'])
// => { display: 'ChatGPT', vendor: 'OpenAI', kind: 'computer-use-agent' }
```

### Notes

* The keys are the `agent_name` values. The [Agents](/docs/agents) page lists them.
* `kind` is one of `computer-use-agent`, `automation`, `fetcher`, `previewer`, `crawler` or `client`.

## Types

`botscent/server` exports these types. `botscent` exports `AgentName`, `Reason`, `Verdict`, `Diagnostics` and `StartOptions`.

```ts title="Types"
type Verdict = {
  readonly type: 'agent' | 'human'
  readonly agent_name?: AgentName
  readonly reasons: readonly Reason[]
}
type Reason = KnownReason | (string & {})
type AgentName = KnownAgentName | (string & {})
type TransportMode = 'auto' | 'always' | 'never'
// also: CloudflareHints, InspectOptions, RequestLike (see inspect)
```

`KnownReason` and `KnownAgentName` list every id in this release. A reason or a name from a newer server still type-checks.

## What every adapter does

Every adapter below runs `inspect` for each request. Most adapters keep the request's own verdict where the framework keeps per-request state. An adapter also does these things:

* It keeps status codes, redirects, cookies, streaming responses, request bodies and the application's own exceptions unchanged.
* It never turns a statically rendered route into a dynamic route.
* It removes inherited `botscent` entries from `Server-Timing`, and keeps all other entries. The Next.js proxy and Vercel Routing Middleware cannot remove an entry that the response itself carries.
* If its transport is on, it adds the `botscent` entry and `Cache-Control: no-store` to an agent's document navigation.
* On its own failure, it gives no evidence and passes the request on.

The `transport` option controls the `Server-Timing` entry. `'always'` states that no shared cache stores the HTML. See [From the server to the page](/docs/server-to-page).

## `botscent/next`

Runs the server half as a Next.js proxy.

```ts title="Signature"
const proxy: NextProxy
function withBotscent(existing?: NextProxy, options?: BotscentNextOptions): NextProxy
function withBotscent(options: BotscentNextOptions): NextProxy

type Result = Response | NextResponse | null | undefined | void
type NextProxy = (request: NextRequest, event: NextFetchEvent) => Result | Promise<Result>
type BotscentNextOptions = { transport?: TransportMode; debug?: boolean | ((line: string) => void) }
```

```ts title="proxy.ts"
import { withBotscent } from 'botscent/next'

export const proxy = withBotscent({ transport: 'auto', debug: true })
```

### Parameters

| Name                | Type                                  | Default  | Description                                                                                     |
| ------------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `existing`          | `NextProxy`                           | (none)   | The app's own proxy, which `withBotscent` wraps and keeps.                                      |
| `options.transport` | `'auto' \| 'always' \| 'never'`       | `'auto'` | Sends the entry on Vercel only with `'auto'`, always with `'always'`, and never with `'never'`. |
| `options.debug`     | `boolean \| ((line: string) => void)` | (none)   | Logs each decision, as in `inspect`.                                                            |

### Notes

* With no proxy in the app, `proxy.ts` is one line: `export { proxy } from 'botscent/next'`.
* Before Next.js 16, the file is `middleware.ts`, with `export { proxy as middleware } from 'botscent/next'` or `export const middleware = withBotscent(existingMiddleware)`.
* The proxy does not pass on the verdict. Route handlers call `inspect(request)`.
* The wrapped proxy's response is kept. Only `Server-Timing` and, for agents, `Cache-Control` change.
* A proxy cannot see headers that the route sets later. On an agent navigation that it decorates, its `Server-Timing` replaces one that the route set.

### Examples

#### Wrap an existing proxy

Pass the app's own proxy to `withBotscent`:

```ts title="proxy.ts"
import { NextResponse } from 'next/server'
import { withBotscent } from 'botscent/next'

function existingProxy() {
  return NextResponse.next()
}

export const proxy = withBotscent(existingProxy)
```

## `botscent/vercel`

Runs the server half as Vercel Routing Middleware, for projects that are not Next.js.

```ts title="Signature"
declare const middleware: VercelMiddleware
export default middleware
function withBotscent(existing?: VercelMiddleware, options?: BotscentVercelOptions): VercelMiddleware
function withBotscent(options: BotscentVercelOptions): VercelMiddleware

type Result = Response | null | undefined | void
type VercelMiddleware = (request: Request, context?: unknown) => Result | Promise<Result>
type BotscentVercelOptions = { transport?: TransportMode; debug?: boolean | ((line: string) => void) }
```

```ts title="middleware.ts"
export { default } from 'botscent/vercel'
```

### Parameters

| Name                | Type                                  | Default  | Description                                                             |
| ------------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------- |
| `existing`          | `VercelMiddleware`                    | (none)   | The project's own middleware, which `withBotscent` wraps.               |
| `options.transport` | `'auto' \| 'always' \| 'never'`       | `'auto'` | Sends the entry with `'auto'` and `'always'`, and never with `'never'`. |
| `options.debug`     | `boolean \| ((line: string) => void)` | (none)   | Logs each decision, as in `inspect`.                                    |

### Notes

* The middleware does not pass on the verdict. It continues to the project.
* Routing Middleware runs before the response exists. It cannot remove an entry that the response itself carries.
* Vercel adds the middleware's `Server-Timing` to the response's own.

## `botscent/workers`

Runs the server half around a Cloudflare Worker's `fetch` handler or a Netlify Edge Function.

```ts title="Signature"
function withBotscent<Env = unknown>(
  handler: (request: Request, env: Env, ctx: Context, verdict: Verdict) => Response | Promise<Response>,
  options?: BotscentWorkersOptions,
): (request: Request, env: Env, ctx: Context) => Promise<Response>

type Context = { waitUntil(promise: Promise<unknown>): void; passThroughOnException?(): void }
type BotscentWorkersOptions = { transport?: TransportMode; debug?: boolean | ((line: string) => void) }
```

```ts title="src/index.ts"
import { withBotscent } from 'botscent/workers'

export default {
  fetch: withBotscent(async (request, env, ctx, verdict) => fetch(request)),
}
```

### Parameters

| Name                | Type                                  | Default    | Description                                                                    |
| ------------------- | ------------------------------------- | ---------- | ------------------------------------------------------------------------------ |
| `handler`           | `FetchWithVerdict<Env>`               | (required) | The Worker's handler, which gets the request's verdict as its fourth argument. |
| `options.transport` | `'auto' \| 'always' \| 'never'`       | `'auto'`   | Sends the entry with `'auto'` and `'always'`, and never with `'never'`.        |
| `options.debug`     | `boolean \| ((line: string) => void)` | (none)     | Logs each decision, as in `inspect`.                                           |

### Notes

* The adapter passes Cloudflare's `request.cf` to `inspect`.
* If the Worker stores HTML with the Cache API, set `transport: 'never'`.
* Behind Workers Cache, cached pages carry no entry.
* On Netlify, `withBotscent<Context>((request, context) => context.next())` continues to the site, with `Context` from `@netlify/edge-functions`.

## `botscent/hono`

Runs the server half as Hono middleware.

```ts title="Signature"
function botscent(options?: BotscentHonoOptions): MiddlewareHandler<{ Variables: { botscent: Verdict } }>

type BotscentHonoOptions = { transport?: TransportMode; debug?: boolean | ((line: string) => void) }
```

```ts title="src/index.ts"
import { Hono } from 'hono'
import { botscent } from 'botscent/hono'

const app = new Hono().use(botscent())

app.get('/verdict', (c) => c.json(c.get('botscent')))

export default app
```

### Parameters

| Name                | Type                                  | Default  | Description                                                                                                 |
| ------------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `options.transport` | `'auto' \| 'always' \| 'never'`       | `'auto'` | Sends the entry on Cloudflare Workers only with `'auto'`, always with `'always'`, and never with `'never'`. |
| `options.debug`     | `boolean \| ((line: string) => void)` | (none)   | Logs each decision, as in `inspect`.                                                                        |

### Notes

* The request's verdict is `c.get('botscent')`. Chain `.use(botscent())` on `new Hono()` to type it.
* On Cloudflare Workers, the middleware passes `request.cf` to `inspect`.

## `botscent/express`

Runs the server half as Express or Connect middleware.

```ts title="Signature"
function botscent(options?: BotscentExpressOptions): (req: IncomingMessage, res: ServerResponse, next: (error?: unknown) => void) => void

type BotscentExpressOptions = { transport?: 'always' | 'never'; debug?: boolean | ((line: string) => void) }
```

```js title="server.js"
const express = require('express')
const { botscent } = require('botscent/express')

const app = express()
app.use(botscent())

app.get('/verdict', (req, res) => res.json(req.botscent))

app.listen(3000)
```

### Parameters

| Name                | Type                                  | Default   | Description                                                |
| ------------------- | ------------------------------------- | --------- | ---------------------------------------------------------- |
| `options.transport` | `'always' \| 'never'`                 | `'never'` | Sends the entry with `'always'`, and never with `'never'`. |
| `options.debug`     | `boolean \| ((line: string) => void)` | (none)    | Logs each decision, as in `inspect`.                       |

### Notes

* The request's verdict is `req.botscent` in every handler after the middleware.
* Express runs at the origin, which cannot see a CDN in front of it. The transport is therefore off by default.

## `botscent/astro`

Adds both halves to an Astro site as one integration.

```ts title="Signature"
export default function botscent(options?: BotscentAstroOptions): AstroIntegration

type BotscentAstroOptions = { transport?: 'always' | 'never'; debug?: boolean }
```

```ts title="astro.config.mjs"
import { defineConfig } from 'astro/config'
import botscent from 'botscent/astro'

export default defineConfig({
  integrations: [botscent()],
})
```

### Parameters

| Name                | Type                  | Default   | Description                                                 |
| ------------------- | --------------------- | --------- | ----------------------------------------------------------- |
| `options.transport` | `'always' \| 'never'` | `'never'` | Sends the entry with `'always'`, and never with `'never'`.  |
| `options.debug`     | `boolean`             | `false`   | Logs each decision of the server half with `console.debug`. |

### Notes

* The integration adds `botscent/auto` to every page.
* On routes rendered on demand, the request's verdict is `Astro.locals.botscent`.
* Prerendered pages have no request to inspect, so the server half does not run for them.
