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 two-process setup: a Key0 payment gateway (port 3000) in front of a backend API (port 3001). Clients never talk to the backend directly — they call the gateway route they want to buy, Key0 returns a 402, then proxies the request after payment and returns the backend’s response as a ResourceResponse. Source: examples/ppr-standalone

What it demonstrates

  • proxyTo on SellerConfig to enable standalone per-request gateway mode
  • Direct route calls for pay-per-use plus /x402/access for subscription token purchases
  • Automatic payment header injection (x-key0-tx-hash, x-key0-plan-id, x-key0-amount) so the backend can log/audit payments
  • A shared secret (x-gateway-secret) that the backend uses to reject requests that bypass the gateway
  • Full HTTP, A2A, and MCP client support with no separate route registrations needed
  • PENDING → PAID → DELIVERED state transitions with refund safety when the backend returns non-2xx

Architecture

Client                    Key0 Gateway (port 3000)         Backend (port 3001)
  |                              |                                |
  |-- GET /discover             |                                |
  |<-- plan catalog + routes     |                                |
  |                              |                                |
  |-- GET /api/weather/london    |                                |
  |                              |-- create PENDING challenge      |
  |<-- HTTP 402 + requirements  |                                |
  |                              |                                |
  |  ... signs EIP-3009 off-chain ...
  |                              |                                |
  |-- GET /api/weather/london    |                                |
  |   + PAYMENT-SIGNATURE        |                                |
  |                              |-- settle on-chain               |
  |                              |-- PENDING → PAID                |
  |                              |                                |
  |                              |-- GET /api/weather/london ------>|
  |                              |   (x-key0-tx-hash: 0x...)       |
  |                              |   (x-gateway-secret: ***)       |
  |                              |<--- { city, tempF, ... } -------|
  |                              |-- PAID → DELIVERED              |
  |<-- ResourceResponse          |                                |
  |   { type: "ResourceResponse",|                                |
  |     resource: { status: 200, |                                |
  |     body: { city, tempF } } }|                                |

Key differences from embedded mode

EmbeddedStandalone
Where Key0 runsInside your applicationSeparate gateway process
Route registrationapp.get("/api/…", key0.payPerRequest())Key0 auto-mounts configured routes
Backend callHandler runs in-processKey0 proxies to proxyTo.baseUrl
Response to clientYour handler’s responseResourceResponse wrapper
A2A / MCP supportHTTP-only for per-requestHTTP direct routes plus A2A/MCP route discovery
Backend authN/AShared secret via proxyTo.headers

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 settlement

Setup

cd examples/ppr-standalone
cp .env.example .env
# Edit .env: set KEY0_WALLET_ADDRESS, GAS_WALLET_PRIVATE_KEY, GATEWAY_SECRET

# Terminal 1: Start the backend API
bun run start:backend    # Starts on port 3001

# Terminal 2: Start the Key0 gateway
bun run start:gateway    # Starts on port 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)
BACKEND_URLBackend URL for proxying (default: http://localhost:3001)
GATEWAY_SECRETShared secret injected as x-gateway-secret header
GATEWAY_PORTGateway port (default: 3000)
PUBLIC_URLPublic-facing gateway URL (used in agent card)

Code walkthrough

1. Enable proxy mode with proxyTo

const key0 = key0Router({
  config: {
    // ...
    routes: [
      {
        routeId: "weather-query",
        method: "GET",
        path: "/api/weather/:city",
        unitAmount: "$0.01",
        description: "Current weather — $0.01 per call",
      },
    ],

    // proxyTo enables standalone mode:
    // After payment, Key0 forwards to BACKEND_URL and returns the response.
    proxyTo: {
      baseUrl: process.env.BACKEND_URL!,
      // Shared secret so the backend rejects direct (non-gateway) requests
      headers: { "x-gateway-secret": process.env.GATEWAY_SECRET! },
    },
  },
  adapter, store, seenTxStore,
});

app.use(key0);
// No app.get() needed — Key0 auto-mounts the configured paid routes

2. Client request (HTTP)

# Step 1: Challenge
curl http://localhost:3000/api/weather/london
# → HTTP 402 + PaymentRequirements

# Step 2: Settle (add PAYMENT-SIGNATURE header)
curl http://localhost:3000/api/weather/london \
  -H "PAYMENT-SIGNATURE: eyJ4NDAy..." \
# → 200 ResourceResponse

3. ResourceResponse shape

{
  "type": "ResourceResponse",
  "challengeId": "http-a1b2c3d4-...",
  "requestId": "550e8400-...",
  "routeId": "weather-query",
  "txHash": "0x7f9fade1...",
  "explorerUrl": "https://sepolia.basescan.org/tx/0x7f9fade1...",
  "resource": {
    "status": 200,
    "body": { "city": "london", "tempF": 65, "condition": "Cloudy" }
  }
}

4. Backend receives payment headers

The backend receives these headers on every proxied request:
x-key0-tx-hash: 0x7f9fade1...
x-key0-plan-id: weather-query
x-key0-amount:  $0.01
x-key0-payer:   0xClientWallet...  (when available)
x-gateway-secret: ***  (reject if missing)

5. Non-2xx backend response

If the backend returns 4xx/5xx, Key0 wraps it in a ResourceResponse and the challenge stays in PAID state. The refund cron will process it:
{
  "type": "ResourceResponse",
  "resource": {
    "status": 404,
    "body": { "error": "City not found" }
  }
}

A2A client flow

A2A agents discover the same route catalog and request access with routeId plus the concrete backend path they want to proxy:
{
  "method": "message/send",
  "params": {
    "message": {
      "parts": [{
        "kind": "data",
        "data": {
          "type": "AccessRequest",
          "routeId": "weather-query",
          "resource": { "method": "GET", "path": "/api/weather/london" }
        }
      }]
    }
  }
}
After settlement, the executor returns a completed task with a ResourceResponse artifact.

MCP client flow

MCP clients (Claude Code, Cursor) call the request_access tool with routeId and a resource field. Set mcp: true in SellerConfig to enable:
{
  "name": "request_access",
  "arguments": {
    "routeId": "weather-query",
    "resource": { "method": "GET", "path": "/api/weather/london" }
  }
}

Next Steps

PPR Embedded Example

Per-request with Key0 inside your application.

SellerConfig: proxyTo

ProxyToConfig, FetchResourceParams, and FetchResourceResult types.

POST /x402/access

Full API reference for the unified payment endpoint.

Two Modes

When to use standalone vs. embedded.