Installation
npm install @key0ai/key0 express ioredis
import { key0Router , validateAccessToken } from "@key0ai/key0/express" ;
Quick start
Create the Express app and dependencies
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" );
Mount the Key0 router
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 }),
}),
);
Protect your routes
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 );
key0Router(opts)
Creates an Express Router that serves agent discovery and the unified x402 payment endpoint.
import { key0Router } from "@key0ai/key0/express" ;
app . use ( key0Router ({ config , adapter , store , seenTxStore }));
Options
Parameter Type Description configSellerConfigAgent identity, wallet, plans, and callbacks. See SellerConfig . adapterX402AdapterOn-chain payment verification adapter. storeIChallengeStoreChallenge state storage (e.g. RedisChallengeStore). seenTxStoreISeenTxStoreDouble-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.jsonA2A agent card for discovery. GET/discoverPlan catalog (all plans and pricing). POST/x402/accessUnified 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.jsonMCP discovery document. POST/mcpMCP Streamable HTTP endpoint. GET/mcpReturns 405 (SSE not supported in stateless mode). DELETE/mcpReturns 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).
The requestId field is optional. If you omit it, the server generates one automatically.
Include it when you need client-side idempotency.
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)
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.
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):
Server returns ResourceResponse (the backend’s response body, not a JWT):
{
"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. fetchResourceFetchResourceParams => Promise<FetchResourceResult>Override or provide a custom backend call for this specific route (standalone mode). proxyToProxyToConfigShorthand 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.
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 secretstringThe 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:
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.
MCP mode requires the @modelcontextprotocol/sdk peer dependency. Install it separately if you enable this option.
See the MCP protocol guide for details on how agents interact with these endpoints.
Full example
A complete working example is available at examples/express-seller . To run it locally:
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
SellerConfig Reference Complete reference for every configuration option.
Paying for Access How clients discover, pay, and call your protected endpoints.
Hono Integration Key0 as a Hono sub-app.
MCP Protocol Expose Key0 plans as MCP tools for Claude Code.