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

# Express

> Mount Key0 as Express middleware. Serves agent discovery, payment endpoints, and optional MCP routes — then protect your own routes with a single middleware.

## Installation

```bash theme={null}
npm install @key0ai/key0 express ioredis
```

```typescript theme={null}
import { key0Router, validateAccessToken } from "@key0ai/key0/express";
```

## Quick start

<Steps>
  <Step title="Create the Express app and dependencies">
    ```typescript theme={null}
    import {
      X402Adapter,
      RedisChallengeStore,
      RedisSeenTxStore,
      AccessTokenIssuer,
    } from "@key0ai/key0";
    import { key0Router, validateAccessToken } from "@key0ai/key0/express";
    import express from "express";
    import Redis from "ioredis";

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

    const redis = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
    ```
  </Step>

  <Step title="Mount the Key0 router">
    ```typescript theme={null}
    const SECRET = process.env.KEY0_ACCESS_TOKEN_SECRET!;
    const tokenIssuer = new AccessTokenIssuer(SECRET);

    app.use(
      key0Router({
        config: {
          agentName: "Photo Gallery Agent",
          agentDescription: "Premium photos via USDC payments on Base",
          agentUrl: "https://api.example.com",
          providerName: "Example Corp",
          providerUrl: "https://example.com",
          walletAddress: "0xYourWalletAddress" as `0x${string}`,
          network: "mainnet",
          plans: [
            { planId: "single-photo", unitAmount: "$0.10", description: "One photo." },
            { planId: "full-album", unitAmount: "$1.00", description: "Full album (24h)." },
          ],
          fetchResourceCredentials: async (params) => {
            return tokenIssuer.sign(
              {
                sub: params.requestId,
                jti: params.challengeId,
                resourceId: params.resourceId,
                planId: params.planId,
                txHash: params.txHash,
              },
              3600,
            );
          },
        },
        adapter: new X402Adapter({ network: "mainnet" }),
        store: new RedisChallengeStore({ redis }),
        seenTxStore: new RedisSeenTxStore({ redis }),
      }),
    );
    ```
  </Step>

  <Step title="Protect your routes">
    ```typescript theme={null}
    app.use("/api", validateAccessToken({ secret: SECRET }));

    app.get("/api/photos/:id", (req, res) => {
      res.json({ id: req.params.id, url: `https://cdn.example.com/${req.params.id}.jpg` });
    });

    app.listen(3000);
    ```
  </Step>
</Steps>

## `key0Router(opts)`

Creates an Express `Router` that serves agent discovery and the unified x402 payment endpoint.

```typescript theme={null}
import { key0Router } from "@key0ai/key0/express";

app.use(key0Router({ config, adapter, store, seenTxStore }));
```

### Options

| Parameter     | Type              | Description                                                                                     |
| ------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `config`      | `SellerConfig`    | Agent identity, wallet, plans, and callbacks. See [SellerConfig](/sdk-reference/seller-config). |
| `adapter`     | `X402Adapter`     | On-chain payment verification adapter.                                                          |
| `store`       | `IChallengeStore` | Challenge state storage (e.g. `RedisChallengeStore`).                                           |
| `seenTxStore` | `ISeenTxStore`    | Double-spend prevention store (e.g. `RedisSeenTxStore`).                                        |

### Routes served

The router mounts the following routes on your Express app:

| Method | Path                      | Description                                                                                                                              |
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`  | `/.well-known/agent.json` | A2A agent card for discovery.                                                                                                            |
| `GET`  | `/discover`               | Plan catalog (all plans and pricing).                                                                                                    |
| `POST` | `/x402/access`            | Unified x402 HTTP endpoint. Handles x402 HTTP flow by default; routes to A2A JSON-RPC handler when `X-A2A-Extensions` header is present. |

When `mcp: true` is set in `config`, four additional routes are mounted:

| Method   | Path                    | Description                                                       |
| -------- | ----------------------- | ----------------------------------------------------------------- |
| `GET`    | `/.well-known/mcp.json` | MCP discovery document.                                           |
| `POST`   | `/mcp`                  | MCP Streamable HTTP endpoint.                                     |
| `GET`    | `/mcp`                  | Returns 405 (SSE not supported in stateless mode).                |
| `DELETE` | `/mcp`                  | Returns 405 (session management not supported in stateless mode). |

## The `/x402/access` endpoint

This endpoint handles the full x402 payment flow over plain HTTP. It responds differently depending on the request body and headers:

| Case           | Request                                         | Response                                             |
| -------------- | ----------------------------------------------- | ---------------------------------------------------- |
| **No planId**  | Empty body (no `planId`)                        | `400` with pointer to `GET /discover`.               |
| **Challenge**  | Body with `planId`                              | `402` with payment requirements and a `challengeId`. |
| **Settlement** | Body with `planId` + `payment-signature` header | `200` with an `AccessGrant` (JWT + metadata).        |

<Note>
  The `requestId` field is optional. If you omit it, the server generates one automatically.
  Include it when you need client-side idempotency.
</Note>

## Pay-Per-Request Routes

`key0Router` returns the router **and** a `payPerRequest(planId, opts?)` factory. Use it to gate individual routes behind micro-payments — no JWT issued, payment settles inline.

### Embedded mode (route inside your app)

```typescript theme={null}
const key0 = key0Router({
  config: {
    // ...
    routes: [
      {
        routeId: "weather-query",
        method: "GET",
        path: "/api/weather/:city",
        unitAmount: "$0.01",
      },
    ],
    fetchResourceCredentials: async (params) => tokenIssuer.sign(params, 3600),
  },
  adapter, store, seenTxStore,
});
app.use(key0);

// Gate the route — every call needs a PAYMENT-SIGNATURE header
app.get(
  "/api/weather/:city",
  key0.payPerRequest("weather-query", {
    onPayment: (info) => console.log(`Settled: ${info.txHash}`),
  }),
  (req, res) => {
    const payment = req.key0Payment; // PaymentInfo
    res.json({ city: req.params.city, temp: 72, txHash: payment?.txHash });
  },
);
```

Without a `PAYMENT-SIGNATURE` header the middleware returns `402` with x402 payment requirements. After settlement, `req.key0Payment` contains the `PaymentInfo` object.

### Standalone mode (proxy via `/x402/access`)

Add `proxyTo` to `SellerConfig`. No separate route registrations needed — Key0 auto-mounts the configured routes.

```typescript theme={null}
const key0 = key0Router({
  config: {
    // ...
    routes: [
      { routeId: "weather-query", method: "GET", path: "/api/weather/:city", unitAmount: "$0.01" },
    ],
    fetchResourceCredentials: async (params) => tokenIssuer.sign(params, 3600),
    proxyTo: {
      baseUrl: process.env.BACKEND_URL!,
      // optional: shared secret so your backend rejects requests that bypass the gateway
      headers: { "x-gateway-secret": process.env.GATEWAY_SECRET! },
    },
  },
  adapter, store, seenTxStore,
});
app.use(key0);
// No app.get() needed — Key0 auto-mounts /api/weather/:city
```

Client request (HTTP):

```http theme={null}
GET /api/weather/london
```

Server returns `ResourceResponse` (the backend's response body, not a JWT):

```json theme={null}
{
  "type": "ResourceResponse",
  "challengeId": "...",
  "routeId": "weather-query",
  "txHash": "0x...",
  "resource": { "status": 200, "body": { "city": "london", "tempF": 65 } }
}
```

### `payPerRequest` options

| Parameter       | Type                                                     | Description                                                                                      |
| --------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `onPayment`     | `(info: PaymentInfo) => void \| Promise<void>`           | Called after successful settlement, before the route handler. Fire-and-forget.                   |
| `route`         | `{ method: string; path: string; description?: string }` | Explicit route metadata for discovery — useful when you want to override the auto-captured path. |
| `fetchResource` | `FetchResourceParams => Promise<FetchResourceResult>`    | Override or provide a custom backend call for this specific route (standalone mode).             |
| `proxyTo`       | `ProxyToConfig`                                          | Shorthand proxy config for this specific route (standalone mode).                                |

## `validateAccessToken(config)`

Express middleware that verifies the `Bearer` JWT on incoming requests and attaches the decoded payload to `req.key0Token`.

```typescript theme={null}
import { validateAccessToken } from "@key0ai/key0/express";

app.use("/api", validateAccessToken({ secret: process.env.KEY0_ACCESS_TOKEN_SECRET! }));

app.get("/api/resource", (req, res) => {
  const token = (req as Request & { key0Token?: unknown }).key0Token;
  res.json({ token });
});
```

| Parameter | Type     | Description                                                                                             |
| --------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `secret`  | `string` | The same secret used by `AccessTokenIssuer` (or your `fetchResourceCredentials` callback) to sign JWTs. |

The middleware returns a `401` or `403` JSON error if the token is missing, expired, or invalid.

## Enabling MCP mode

Set `mcp: true` in your `SellerConfig` to expose MCP (Model Context Protocol) routes alongside the standard x402 endpoint:

```typescript theme={null}
app.use(
  key0Router({
    config: {
      // ...
      mcp: true,
    },
    adapter,
    store,
    seenTxStore,
  }),
);
```

This adds `/.well-known/mcp.json` for discovery and `POST /mcp` for Streamable HTTP transport, exposing `discover_plans` and `request_access` as MCP tools.

<Warning>
  MCP mode requires the `@modelcontextprotocol/sdk` peer dependency. Install it separately if you enable this option.
</Warning>

See the [MCP protocol guide](/protocol/mcp) for details on how agents interact with these endpoints.

## Full example

A complete working example is available at [`examples/express-seller`](https://github.com/key0ai/key0/tree/main/examples/express-seller). To run it locally:

```bash theme={null}
cd examples/express-seller
bun install
REDIS_URL=redis://localhost:6379 KEY0_WALLET_ADDRESS=0x... KEY0_ACCESS_TOKEN_SECRET=your-secret bun run start
```

## Next Steps

<CardGroup cols={2}>
  <Card title="SellerConfig Reference" icon="sliders" href="/sdk-reference/seller-config">
    Complete reference for every configuration option.
  </Card>

  <Card title="Paying for Access" icon="credit-card" href="/guides/paying-for-access">
    How clients discover, pay, and call your protected endpoints.
  </Card>

  <Card title="Hono Integration" icon="bolt" href="/integrations/hono">
    Key0 as a Hono sub-app.
  </Card>

  <Card title="MCP Protocol" icon="plug" href="/protocol/mcp">
    Expose Key0 plans as MCP tools for Claude Code.
  </Card>
</CardGroup>
