Skip to main content

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.

A complete Express server that charges 0.01perweatherqueryand0.01 per weather query and 0.005 per joke — settled on-chain every time a route is called. No long-lived token is issued; the route handler runs normally after payment. Source: examples/ppr-embedded

What it demonstrates

  • Defining top-level routes for pay-per-call pricing
  • Using key0.payPerRequest(routeId, opts?) middleware to gate individual routes
  • Accessing req.key0Payment (containing txHash, planId, amount, challengeId) in route handlers
  • Discovery via GET /discover and agent card showing per-request skills with route metadata
  • Automatic PENDING → PAID → DELIVERED state transitions with refund safety (pass a store to enable)

Architecture

Client                    Your Application (Express)
  |                              |
  |-- GET /.well-known/agent.json|  Discover per-request skills + pricing
  |<-- agent card + routes       |
  |                              |
  |-- GET /api/weather/london    |  No PAYMENT-SIGNATURE header
  |<-- HTTP 402 + requirements  |  (payment-required header set)
  |                              |
  |  ... signs EIP-3009 off-chain ...
  |                              |
  |-- GET /api/weather/london    |  PAYMENT-SIGNATURE header present
  |   + PAYMENT-SIGNATURE       |
  |                              |-- settle on-chain
  |                              |-- req.key0Payment set (txHash, etc.)
  |                              |-- route handler runs
  |<-- 200 { city, tempF, ... } |  Direct API response (no JWT)

Key differences from subscription mode

SubscriptionPer-Request (Embedded)
ResponseAccessGrant (JWT)Your route handler’s response
Token issuedYesNo
Follow-up authBearer JWT needed for each callEach call includes PAYMENT-SIGNATURE
Route protectionvalidateAccessToken middlewarekey0.payPerRequest() middleware
Payment metadataIn IssueTokenParams callbackIn req.key0Payment

Running locally

Prerequisites

  • Bun v1.3+ or Node 18+
  • Redis running locally
  • A wallet address on Base Sepolia
  • A gas wallet private key (ETH-funded) for self-contained settlement

Setup

cd examples/ppr-embedded
cp .env.example .env
# Edit .env: set KEY0_WALLET_ADDRESS and GAS_WALLET_PRIVATE_KEY
bun run start
Server starts on http://localhost:3000.

Environment variables

VariableDescription
KEY0_WALLET_ADDRESSWallet that receives USDC payments
GAS_WALLET_PRIVATE_KEYETH-funded wallet for settlement
KEY0_NETWORK"testnet" (default) or "mainnet"
REDIS_URLRedis connection URL (default: redis://localhost:6379)
PORTServer port (default: 3000)
PUBLIC_URLPublic-facing server URL (used in agent card)

Code walkthrough

1. Define top-level pay-per-call routes

routes: [
  {
    routeId: "weather-query",
    method: "GET",
    path: "/api/weather/:city",
    unitAmount: "$0.01",
    description: "Current weather conditions for a given city",
  },
  {
    routeId: "joke-of-the-day",
    method: "GET",
    path: "/api/joke",
    unitAmount: "$0.005",
    description: "A random programming joke",
  },
],

2. Mount key0Router and gate routes

const key0 = key0Router({ config, 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: tx=${info.txHash}`),
  }),
  (req, res) => {
    const payment = req.key0Payment; // PaymentInfo: { txHash, planId, amount, challengeId, ... }
    res.json({
      city: req.params.city,
      tempF: Math.round(55 + Math.random() * 35),
      txHash: payment?.txHash,
    });
  },
);

3. Discovery response

Agents call GET /discover and see pay-per-call routes directly:
{
  "routes": [
    {
      "routeId": "weather-query",
      "method": "GET",
      "path": "/api/weather/:city",
      "unitAmount": "$0.01"
    }
  ]
}
The agent card at /.well-known/agent.json includes these as route skills.

Testing with curl

# Step 1: Check what routes are available
curl http://localhost:3000/discover

# Step 2: Call the route without payment (expect 402)
curl -v http://localhost:3000/api/weather/london
# HTTP/1.1 402 Payment Required
# payment-required: eyJ4...

# Step 3: Sign and submit payment (use examples/client-agent or simple-x402-client.ts)
# After payment, the response is your route handler's output:
# { "city": "london", "tempF": 64, "txHash": "0x..." }

Extending the example

  • Add more routes with different plans
  • Use req.key0Payment.challengeId for your own audit records
  • Pass store to key0.payPerRequest() to enable refund safety on per-request routes (when settlement succeeds but the handler crashes)
  • Combine with subscription plans on the same server — agents can use both flows

Next Steps

PPR Standalone Example

Key0 as a gateway that proxies to a separate backend service.

Express Integration

Full reference for key0Router and payPerRequest options.

SellerConfig Reference

Routes, plans, and ProxyToConfig type definitions.

Building a Seller

End-to-end guide from install to production.