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

# Agentify SDK middleware

> Serve agent traffic agentified markdown from your own site with drop-in middleware for TypeScript, Python, Go, and Ruby: detection, cache-header forwarding, and fail-open semantics.

Every [seller SDK](/sdks/overview) ships Agentify middleware for its language's dominant boundary: a fetch-handler wrapper in TypeScript, ASGI and WSGI adapters in Python, `net/http` middleware in Go, and Rack middleware in Ruby. Mount it in front of your marketing site, and agent traffic gets [agentified markdown](/agentify/overview) while everyone else gets your site untouched.

All four implement one contract:

* Only `GET` requests are considered; every other method passes through.
* [Detection](/agentify/overview#how-detection-works) is one predicate over the request's `Accept` and `User-Agent` headers: markdown-preferring or AI-agent traffic matches.
* A match short-circuits with `200 text/markdown`, forwarding the Agentify response's `cache-control` and `etag` so your CDN caches the markdown too, plus `Vary: accept, user-agent` so a shared cache never hands it to a browser.
* **Any Agentify failure falls through to your app.** A missing key, a network error, a non-200, a timeout — the middleware never breaks your site. The worst case is an agent seeing HTML.

## Configure the key

Agentify calls use a dedicated config field holding a `zc_` key with the [`agentify:convert` scope](/agentify/overview#the-agentify-key-scope); when unset, they fall back to the combined `apiKey`. The field has no construction-time requirement — a client that never agentifies needs no key — so a missing key surfaces as a typed error at call time, which the middleware absorbs by falling through.

<CodeGroup>
  ```ts TypeScript theme={null}
  const zeroClick = createSeller({
    signingSecrets: {
      [process.env.ZEROCLICK_SIGNING_SECRET_KID!]:
        process.env.ZEROCLICK_SIGNING_SECRET!,
    },
    apiKey: process.env.ZEROCLICK_API_KEY!,
    agentifyKey: process.env.ZEROCLICK_AGENTIFY_KEY!,
  });
  ```

  ```python Python theme={null}
  zeroclick = create_async_seller(
      signing_secrets={
          os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
              "ZEROCLICK_SIGNING_SECRET"
          ]
      },
      api_key=os.environ["ZEROCLICK_API_KEY"],
      agentify_key=os.environ["ZEROCLICK_AGENTIFY_KEY"],
  )
  ```

  ```go Go theme={null}
  seller, err := sellers.New(sellers.Config{
  	SigningSecrets: secrets,
  	APIKey:         os.Getenv("ZEROCLICK_API_KEY"),
  	AgentifyKey:    os.Getenv("ZEROCLICK_AGENTIFY_KEY"),
  	ServiceSlug:    "product-watch",
  })
  ```

  ```ruby Ruby theme={null}
  config.zeroclick.api_key = ENV.fetch("ZEROCLICK_API_KEY")
  config.zeroclick.agentify_key = ENV.fetch("ZEROCLICK_AGENTIFY_KEY")
  config.zeroclick.signing_secrets = ZeroClick::Sellers.secrets_from_env
  ```
</CodeGroup>

Conversion runs a full pipeline on the API side, so Agentify calls default to a 10-second timeout (`agentifyTimeoutMs` / `agentify_timeout_seconds` / `AgentifyTimeout`), independent of the allowance check's much tighter one.

## Mount the middleware

<CodeGroup>
  ```ts TypeScript theme={null}
  import { withAgentify } from "@zeroclickai/sellers";

  // Wrap the fetch handler that serves your site. Works anywhere the SDK
  // works: Node.js servers, Hono, Next.js route handlers, edge runtimes.
  const handler = withAgentify(zeroClick, (request) => renderPage(request));

  export default { fetch: handler };
  ```

  ```python Python theme={null}
  # ASGI (Starlette, FastAPI) — pass the async client:
  from zeroclick_sellers.adapters import AgentifyAsgiMiddleware

  app.add_middleware(AgentifyAsgiMiddleware, seller=zeroclick)

  # WSGI (Flask, Django) — pass the blocking client from create_seller:
  from zeroclick_sellers.adapters import AgentifyWsgiMiddleware

  app.wsgi_app = AgentifyWsgiMiddleware(app.wsgi_app, seller=zeroclick)
  ```

  ```go Go theme={null}
  // A plain func(http.Handler) http.Handler, like Meter and Identify: it
  // composes with net/http, chi, gorilla/mux, and gin's WrapH.
  mux.Handle("/", seller.Agentify(sellers.AgentifyOptions{})(site))
  ```

  ```ruby Ruby theme={null}
  # Rails: mount it in config/application.rb. Omit `seller:` and the
  # middleware resolves the process-wide client on the first request.
  config.middleware.use ZeroClick::Sellers::Middleware::Agentify

  # Outside Rails, pass the client explicitly:
  use ZeroClick::Sellers::Middleware::Agentify, seller: SELLER
  ```
</CodeGroup>

Mount it in front of the pages agents read — your marketing routes — not in front of your billed API, which already speaks to agents through your [storefront](/website/enable-agent-traffic). If your marketing site runs as its own process, configure a client there the same way; the middleware only ever uses the Agentify key.

## Options

Each middleware takes the same three options, named in its language's idiom:

| Option                                                                                     | TypeScript   | Python        | Go           | Ruby          |
| ------------------------------------------------------------------------------------------ | ------------ | ------------- | ------------ | ------------- |
| The seller whose storefront to inline (`sel_…`); needed only by multi-seller organizations | `seller`     | `seller_id`   | `Seller`     | `seller_id`   |
| Override the public URL to convert for a request                                           | `resolveUrl` | `resolve_url` | `ResolveURL` | `resolve_url` |
| Observe the fail-open path; never re-raised                                                | `onError`    | `on_error`    | `OnError`    | `on_error`    |

By default the middleware converts the URL the agent requested, as far as your process can see it. Behind a proxy or load balancer that rewrites the scheme or host, override it — the API fetches this URL from the outside, so it must be the public one:

```ts theme={null}
const handler = withAgentify(zeroClick, renderPage, {
  resolveUrl: (request) =>
    `https://www.acme.com${new URL(request.url).pathname}`,
});
```

## Failing open, observably

The middleware exists to add an agent-readable representation, never to add an outage: any error on the Agentify path serves your HTML instead. That silence is deliberate, and it also means a misconfigured key looks exactly like working HTML — so wire the error hook to your logger:

```ts theme={null}
const handler = withAgentify(zeroClick, renderPage, {
  onError: (error, request) =>
    logger.warn({ error, url: request.url }, "agentify fell through to HTML"),
});
```

In Go, a nil `OnError` falls back to the client's configured `Logger`.

## Verify

With the middleware mounted, the same page answers both audiences:

```sh theme={null}
# An agent gets markdown that references your storefront:
curl -s -H "Accept: text/markdown, text/html, */*" https://www.acme.com/pricing | head -20

# A browser Accept header gets your HTML, unchanged:
curl -s -H "Accept: text/html,application/xhtml+xml" https://www.acme.com/pricing | head -5
```

The first request of a page takes a few seconds while the conversion runs; repeats are served from [cache](/agentify/overview#caching). This also satisfies the markdown step of [enable agent traffic](/website/enable-agent-traffic): the served document inlines your storefront, which is exactly what verification checks for.

## Combining with Page views

[Page views](/page-views/overview) and Agentify are independent opt-ins: run either alone or both. When you run both, mount **Page views as the outer middleware, wrapping Agentify.** The order is not cosmetic: this middleware short-circuits agent traffic (on a match it returns markdown and never calls the inner handler), so a Page views beacon mounted *inside* Agentify would never fire for agent requests, silently dropping exactly the agent page views you most want to measure. Only the outer layer observes them.

Being outer also lets Page views stitch the journey: Agentify attaches a journey id to its markdown response, and the outer Page views middleware forwards it on the view, tying the marketing page view to the agentified read. The [Page views middleware](/page-views/sdk-middleware#combining-with-agentify) shows the wrap for each language.

## Next steps

<Columns cols={2}>
  <Card title="SPA setup" icon="zap" href="/agentify/spa-setup">
    No server to mount middleware in? Front the SPA with an edge function.
  </Card>

  <Card title="REST quickstart" icon="rocket" href="/agentify/quickstart-rest">
    The underlying endpoint: request, response anatomy, and errors.
  </Card>
</Columns>
