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

# Quickstart: Embedded

> Add Key0 middleware to an existing Express, Hono, or Fastify server in under 5 minutes. Agents discover your pricing, pay in USDC, and access your protected endpoints.

By the end of this guide you will have a running server that:

* Exposes a plan catalog at `GET /discover`
* Issues a USDC payment challenge at `POST /x402/access`
* Settles the on-chain payment and returns a signed JWT (`AccessGrant`)
* Protects your own routes with `validateAccessToken` middleware

<Note>
  New to Key0? Read [Core Concepts](/introduction/core-concepts) first to understand terms like Plan, Challenge, AccessGrant, and EIP-3009.
</Note>

## Prerequisites

Before you begin, make sure you have:

* **Bun v1.3+** (or Node.js 18+)
* A **wallet address on Base Sepolia** (testnet) -- get testnet USDC from [faucet.circle.com](https://faucet.circle.com) (select Base Sepolia)
* **Redis** running locally or remotely (Postgres is also supported -- see [Storage](/sdk-reference/storage))

<Warning>
  Never use mainnet for development or testing. Base Sepolia testnet USDC has no real value and is free from the faucet.
</Warning>

## Setup

<Steps>
  <Step title="Install dependencies">
    ```bash theme={null}
    bun add @key0ai/key0
    bun add ioredis  # for Redis storage
    ```
  </Step>

  <Step title="Configure and mount">
    Create a `server.ts` file. Key0 mounts as middleware on your existing server -- it adds the agent card endpoint (`/.well-known/agent.json`), the plan discovery endpoint (`/discover`), the payment endpoint (`/x402/access`), and leaves your existing routes untouched.

    <CodeGroup>
      ```typescript Express theme={null}
      import express from "express";
      import { key0Router, validateAccessToken } from "@key0ai/key0/express";
      import {
        X402Adapter,
        AccessTokenIssuer,
        RedisChallengeStore,
        RedisSeenTxStore,
      } from "@key0ai/key0";
      import Redis from "ioredis";

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

      const adapter = new X402Adapter({ network: "testnet" });
      const tokenIssuer = new AccessTokenIssuer(process.env.ACCESS_TOKEN_SECRET!);

      const redis = new Redis(process.env.REDIS_URL!);
      const store = new RedisChallengeStore({ redis });
      const seenTxStore = new RedisSeenTxStore({ redis });

      app.use(
        key0Router({
          config: {
            agentName: "My Agent",
            agentDescription: "A payment-gated API",
            agentUrl: "https://my-agent.example.com",
            providerName: "My Company",
            providerUrl: "https://example.com",
            walletAddress: "0xYourWalletAddress" as `0x${string}`,
            network: "testnet",
            plans: [
              {
                planId: "basic",
                unitAmount: "$0.10",
                description: "Basic API access.",
              },
            ],
            fetchResourceCredentials: async (params) => {
              return tokenIssuer.sign(
                {
                  sub: params.requestId,
                  jti: params.challengeId,
                  resourceId: params.resourceId,
                },
                3600,
              );
            },
          },
          adapter,
          store,
          seenTxStore,
        }),
      );

      // Protect your existing routes with the access token middleware
      app.use(
        "/api",
        validateAccessToken({ secret: process.env.ACCESS_TOKEN_SECRET! }),
      );

      app.get("/api/data/:id", (req, res) => {
        res.json({ data: "premium content" });
      });

      app.listen(3000);
      ```

      ```typescript Hono theme={null}
      import { Hono } from "hono";
      import { key0App, honoValidateAccessToken } from "@key0ai/key0/hono";
      import {
        X402Adapter,
        AccessTokenIssuer,
        RedisChallengeStore,
        RedisSeenTxStore,
      } from "@key0ai/key0";
      import Redis from "ioredis";

      const adapter = new X402Adapter({ network: "testnet" });
      const redis = new Redis(process.env.REDIS_URL!);

      const gate = key0App({
        config: {
          agentName: "My Agent",
          agentDescription: "A payment-gated API",
          agentUrl: "https://my-agent.example.com",
          providerName: "My Company",
          providerUrl: "https://example.com",
          walletAddress: "0xYourWalletAddress" as `0x${string}`,
          network: "testnet",
          plans: [
            {
              planId: "basic",
              unitAmount: "$0.10",
              description: "Basic API access.",
            },
          ],
          fetchResourceCredentials: async (params) => {
            const tokenIssuer = new AccessTokenIssuer(process.env.ACCESS_TOKEN_SECRET!);
            return tokenIssuer.sign(
              { sub: params.requestId, jti: params.challengeId, resourceId: params.resourceId },
              3600,
            );
          },
        },
        adapter,
        store: new RedisChallengeStore({ redis }),
        seenTxStore: new RedisSeenTxStore({ redis }),
      });

      const app = new Hono();
      app.route("/", gate);

      // Protected routes
      const api = new Hono();
      api.use(
        "/*",
        honoValidateAccessToken({ secret: process.env.ACCESS_TOKEN_SECRET! }),
      );
      api.get("/data/:id", (c) => c.json({ data: "premium content" }));
      app.route("/api", api);

      export default { port: 3000, fetch: app.fetch };
      ```

      ```typescript Fastify theme={null}
      import Fastify from "fastify";
      import {
        key0Plugin,
        fastifyValidateAccessToken,
      } from "@key0ai/key0/fastify";
      import {
        X402Adapter,
        AccessTokenIssuer,
        RedisChallengeStore,
        RedisSeenTxStore,
      } from "@key0ai/key0";
      import Redis from "ioredis";

      const fastify = Fastify();
      const adapter = new X402Adapter({ network: "testnet" });
      const redis = new Redis(process.env.REDIS_URL!);

      await fastify.register(key0Plugin, {
        config: {
          agentName: "My Agent",
          agentDescription: "A payment-gated API",
          agentUrl: "https://my-agent.example.com",
          providerName: "My Company",
          providerUrl: "https://example.com",
          walletAddress: "0xYourWalletAddress" as `0x${string}`,
          network: "testnet",
          plans: [
            {
              planId: "basic",
              unitAmount: "$0.10",
              description: "Basic API access.",
            },
          ],
          fetchResourceCredentials: async (params) => {
            const tokenIssuer = new AccessTokenIssuer(process.env.ACCESS_TOKEN_SECRET!);
            return tokenIssuer.sign(
              { sub: params.requestId, jti: params.challengeId, resourceId: params.resourceId },
              3600,
            );
          },
        },
        adapter,
        store: new RedisChallengeStore({ redis }),
        seenTxStore: new RedisSeenTxStore({ redis }),
      });

      // Protect routes
      fastify.addHook(
        "onRequest",
        fastifyValidateAccessToken({
          secret: process.env.ACCESS_TOKEN_SECRET!,
        }),
      );

      fastify.listen({ port: 3000 });
      ```
    </CodeGroup>
  </Step>

  <Step title="Set environment variables">
    Create a `.env` file in your project root:

    ```bash .env theme={null}
    KEY0_NETWORK=testnet
    KEY0_WALLET_ADDRESS=0xYourWalletAddress
    ACCESS_TOKEN_SECRET=your-secret-min-32-chars
    REDIS_URL=redis://localhost:6379
    PORT=3000
    ```

    <Note>
      `ACCESS_TOKEN_SECRET` must be at least 32 characters. This secret is used to sign JWTs issued to agents after payment. Use a cryptographically random string.
    </Note>
  </Step>

  <Step title="Run and test">
    Start your server:

    ```bash theme={null}
    bun run server.ts
    # Server starts on http://localhost:3000
    # Agent card at http://localhost:3000/.well-known/agent.json
    ```

    Test the agent card, plan discovery, and payment endpoints with curl:

    ```bash Agent card theme={null}
    curl http://localhost:3000/.well-known/agent.json
    ```

    ```json Expected output theme={null}
    {
      "name": "My Agent",
      "description": "A payment-gated API",
      "url": "https://my-agent.example.com",
      "skills": [
        { "id": "basic", "name": "Basic API access.", "tags": ["x402", "usdc"] }
      ]
    }
    ```

    ```bash Browse plans (returns 200 with plan catalog) theme={null}
    curl http://localhost:3000/discover
    ```

    ```json Expected output theme={null}
    {
      "agentName": "My Agent",
      "description": "A payment-gated API",
      "plans": [
        { "planId": "basic", "unitAmount": "$0.10", "description": "Basic API access." }
      ],
      "routes": []
    }
    ```

    ```bash Request access (returns 402 challenge) theme={null}
    curl -X POST http://localhost:3000/x402/access \
      -H "Content-Type: application/json" \
      -d '{"planId": "basic"}'
    ```

    ```json Expected output (HTTP 402) theme={null}
    {
      "x402Version": 2,
      "accepts": [ { "amount": "100000", "payTo": "0xYourWallet...", "network": "eip155:84532" } ],
      "challengeId": "http-a1b2c3d4-...",
      "error": "Payment required"
    }
    ```

    The agent card describes your service. The discovery endpoint lists all plans — use the `planId` values when requesting access. The access endpoint creates a PENDING challenge containing the payment amount, destination wallet, and chain ID. A client uses the challenge to sign an EIP-3009 authorization and submit payment. See [Paying for Access](/guides/paying-for-access) for the full client walkthrough.
  </Step>
</Steps>

## Optional: Pay-Per-Request Routes

The quickstart above uses a **subscription plan** — the agent pays once and receives a signed JWT that grants access for the token's lifetime. Key0 also supports **pay-per-call routes**, where each API call is paid for individually and no JWT is issued.

The difference in configuration is adding a paid route to top-level `routes`:

```typescript theme={null}
// Subscription plan (default) — agent pays once, receives a JWT
{ planId: "basic", unitAmount: "$0.10", description: "API access token." }

// Pay-per-call route — agent pays per call, receives the route response directly
{
  routeId: "single-call",
  method: "GET",
  path: "/api/data/:id",
  unitAmount: "$0.01",
  description: "One API call — $0.01.",
}
```

On the server side, instead of protecting routes with `validateAccessToken`, you gate them with `key0.payPerRequest()` middleware. The middleware handles the entire payment cycle inline: it returns a 402 if no payment header is present, settles on-chain if it is, then calls `next()` so your route handler runs normally:

```typescript theme={null}
// Subscription plans and pay-per-call routes can coexist in the same config
plans: [
  { planId: "basic", unitAmount: "$0.10" },             // subscription
],
routes: [
  { routeId: "single-call", method: "GET", path: "/api/data/:id", unitAmount: "$0.01" },
],

// Subscription route — protected with JWT middleware (as shown above)
app.use("/api", validateAccessToken({ secret: process.env.ACCESS_TOKEN_SECRET! }));

// Per-request route — protected with payPerRequest middleware
// No JWT needed; payment settles inline. req.key0Payment contains txHash, planId, etc.
app.get("/api/data/:id", key0.payPerRequest("single-call"), (req, res) => {
  res.json({ data: "paid content", txHash: req.key0Payment?.txHash });
});
```

See the [Express integration guide](/integrations/express#pay-per-request-routes) for a complete per-request example with all three frameworks, and [PPR Embedded](/examples/ppr-embedded) for a runnable demo.

## What's next

<CardGroup cols={2}>
  <Card title="Pay-Per-Request (Embedded)" icon="coins" href="/examples/ppr-embedded">
    Full example: weather and joke routes with inline payment settlement.
  </Card>

  <Card title="Integrations" icon="puzzle-piece" href="/integrations/express">
    Detailed integration guides for Express, Hono, Fastify, and MCP.
  </Card>

  <Card title="Storage Setup" icon="database" href="/sdk-reference/storage">
    Configure Redis or Postgres for challenge and transaction storage.
  </Card>

  <Card title="Building a Seller" icon="rocket" href="/guides/building-a-seller">
    End-to-end guide covering plans, storage, mainnet, and production deployment.
  </Card>
</CardGroup>
