Skip to main content

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 (select Base Sepolia)
  • Redis running locally or remotely (Postgres is also supported — see Storage)
Never use mainnet for development or testing. Base Sepolia testnet USDC has no real value and is free from the faucet.

Setup

1

Install dependencies

bun add @key0ai/key0
bun add ioredis  # for Redis storage
2

Configure and mount

Create a server.ts file. Key0 mounts as middleware on your existing server — it adds the discovery endpoint (/.well-known/agent.json), the challenge/proof routes (/x402/access, /x402/proof), and leaves your existing routes untouched.
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);
3

Set environment variables

Create a .env file in your project root:
.env
KEY0_NETWORK=testnet
KEY0_WALLET_ADDRESS=0xYourWalletAddress
ACCESS_TOKEN_SECRET=your-secret-min-32-chars
REDIS_URL=redis://localhost:6379
PORT=3000
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.
4

Run and test

Start your server:
bun run server.ts
# Server starts on http://localhost:3000
# Agent card at http://localhost:3000/.well-known/agent.json
Test the discovery and access endpoints with curl:
Discovery
curl http://localhost:3000/.well-known/agent.json
Request access (returns 402 challenge)
curl -X POST http://localhost:3000/x402/access \
  -H "Content-Type: application/json" \
  -d '{"planId": "basic"}'
The first call returns the A2A agent card with your pricing. The second returns an x402 challenge containing the payment amount, destination wallet, and chain ID. A client agent uses this to submit payment on-chain and then prove it.

What’s next