> ## 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.

## 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>
