Skip to main content
The central configuration object for Key0. Pass it wrapped in a Key0Config object to any framework integration to stand up payment-gated endpoints:
  • Expresskey0Router({ config, adapter, store, seenTxStore })
  • Honokey0App({ config, adapter, store, seenTxStore })
  • Fastifyfastify.register(key0Plugin, { config, adapter, store, seenTxStore })
  • FactorycreateKey0({ config, adapter, store, seenTxStore })
import type { SellerConfig } from "@key0ai/key0";

Identity

Fields that populate the A2A agent discovery card.
FieldTypeRequiredDefaultDescription
agentNamestringYesDisplay name shown in the agent card.
agentDescriptionstringYesShort description of the agent’s capabilities.
agentUrlstringYesPublic base URL of your server (used for endpoint discovery).
providerNamestringYesOrganization or developer name.
providerUrlstringYesOrganization URL.
versionstringNo"1.0.0"Semantic version exposed in the agent card.

Payment

FieldTypeRequiredDefaultDescription
walletAddress`0x${string}`YesAddress that receives USDC payments.
network"testnet" | "mainnet"Yes"testnet" targets Base Sepolia (chain 84532). "mainnet" targets Base (chain 8453).

Product Catalog

FieldTypeRequiredDefaultDescription
plansreadonly Plan[]NoSubscription-style pricing plans. See Plan below.
routesreadonly Route[]NoPay-per-call route catalog. Use this for per-request pricing. See Route below.

Challenge

FieldTypeRequiredDefaultDescription
challengeTTLSecondsnumberNo900How long (in seconds) a challenge remains valid before expiring.

Credential Issuance

FieldTypeRequiredDefaultDescription
fetchResourceCredentials(params: IssueTokenParams) => Promise<TokenIssuanceResult>YesCalled after on-chain payment is verified for subscription plans. Return a token (JWT, API key, etc.) that grants the buyer access. Pay-per-call routes use fetchResource or proxyTo instead. See IssueTokenParams and TokenIssuanceResult.
tokenIssueTimeoutMsnumberNo15000Maximum time (ms) to wait for fetchResourceCredentials before timing out.
tokenIssueRetriesnumberNo2Number of retries on transient failures within fetchResourceCredentials.

Per-Request Proxy (Standalone Gateway)

Enable standalone gateway mode for top-level routes. In standalone mode, clients call the route directly, Key0 returns a 402 challenge for paid routes, and then proxies the request to your backend after payment. Subscription plans still use POST /x402/access.
Only one of fetchResource or proxyTo is needed. Use proxyTo for simple URL forwarding; use fetchResource when you need full control over the request.
FieldTypeRequiredDefaultDescription
fetchResource(params: FetchResourceParams) => Promise<FetchResourceResult>NoCalled after settlement for pay-per-call routes. Fetch and return the backend response. See FetchResourceParams and FetchResourceResult.
proxyToProxyToConfigNoShorthand: automatically builds a fetchResource that forwards to baseUrl. Supports optional headers (e.g. a shared secret so your backend can reject requests that bypass the gateway) and pathRewrite. See ProxyToConfig.

Lifecycle Hooks

Optional callbacks fired during the challenge lifecycle. Both are fire-and-forget — failures are logged but do not affect the payment flow.
FieldTypeRequiredDefaultDescription
onPaymentReceived(grant: AccessGrant) => Promise<void>NoFired after a payment is verified and credentials are issued.
onChallengeExpired(challengeId: string) => Promise<void>NoFired when a challenge passes its TTL without payment.

MCP

FieldTypeRequiredDefaultDescription
mcpbooleanNofalseWhen true, mounts MCP discovery (/.well-known/mcp.json) and Streamable HTTP (POST /mcp) endpoints alongside A2A routes.

Customization

FieldTypeRequiredDefaultDescription
basePathstringNo"/a2a"Prefix for all A2A endpoints.
resourceEndpointTemplatestringNoauto-generatedURL template containing {resourceId} that appears in the access grant. Example: "https://api.example.com/photos/{resourceId}".

Settlement

Control how on-chain settlement is performed. By default, settlement is routed through the Coinbase Developer Platform facilitator.
FieldTypeRequiredDefaultDescription
gasWalletPrivateKey`0x${string}`NoProvide a private key to enable self-contained gas wallet settlement instead of the facilitator.
facilitatorUrlstringNoCDP defaultOverride the default facilitator URL from CHAIN_CONFIGS.
redisIRedisLockClientNoRedis client for distributed locking when using gas wallet settlement across multiple instances. Without this, an in-process serial queue is used (single-instance only).

Supporting Types

Plan

Defines a pricing tier that buyers can select.
type Plan = {
  readonly planId: string;
  readonly unitAmount: string; // e.g. "$0.10"
  readonly description?: string;
};
FieldTypeRequiredDefaultDescription
planIdstringYesUnique identifier for this plan.
unitAmountstringYesDollar-formatted price string (e.g. "$0.10", "$5.00").
descriptionstringNoHuman-readable label for the plan.

Route

type Route = {
  readonly routeId: string;
  readonly method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
  readonly path: string;
  readonly unitAmount?: string;
  readonly description?: string;
};
FieldTypeRequiredDescription
routeIdstringYesUnique identifier for this route.
method"GET" | "POST" | "PUT" | "DELETE" | "PATCH"YesHTTP method for the route.
pathstringYesRoute path including any Express-style parameters (for example "/api/weather/:city").
unitAmountstringNoDollar-formatted price string. Omit for free routes.
descriptionstringNoHuman-readable description of the route.

ProxyToConfig

type ProxyToConfig = {
  readonly baseUrl: string;
  readonly headers?: Record<string, string>;
  readonly pathRewrite?: (path: string) => string;
};
FieldTypeRequiredDescription
baseUrlstringYesBase URL of your backend service (e.g. "https://api.internal").
headersRecord<string, string>NoExtra headers injected on every proxied request. Use this to pass a shared secret so your backend can verify that requests came through the gateway.
pathRewrite(path: string) => stringNoOptional function to rewrite the path before forwarding.

FetchResourceParams

Passed to your fetchResource callback after on-chain settlement.
type FetchResourceParams = {
  readonly paymentInfo: PaymentInfo;
  readonly method: string;
  readonly path: string;
  readonly headers: Record<string, string>;
  readonly body?: unknown;
};
FieldTypeDescription
paymentInfoPaymentInfoPayment metadata — txHash, payer, planId, amount, challengeId.
methodstringHTTP method of the original request.
pathstringPath of the original request (e.g. "/api/weather/london").
headersRecord<string, string>Forwarded request headers.
bodyunknownRequest body, if any.

FetchResourceResult

Returned by your fetchResource callback.
type FetchResourceResult = {
  readonly status: number;
  readonly body: unknown;
  readonly headers?: Record<string, string>;
};
FieldTypeRequiredDescription
statusnumberYesHTTP status code from the backend.
bodyunknownYesResponse body to forward to the client.
headersRecord<string, string>NoResponse headers to forward.

PaymentInfo

Available in req.key0Payment (embedded mode) and FetchResourceParams.paymentInfo (standalone mode).
type PaymentInfo = {
  readonly txHash: `0x${string}`;
  readonly payer?: string;
  readonly planId: string;
  readonly amount: string;
  readonly method: string;
  readonly path: string;
  readonly challengeId: string;
};
FieldTypeDescription
txHash`0x${string}`On-chain transaction hash of the USDC transfer.
payerstringPaying wallet address, when available from the on-chain event.
planIdstringPlan that was charged.
amountstringAmount charged (e.g. "$0.01").
methodstringHTTP method of the gated request.
pathstringRoute path of the gated request.
challengeIdstringInternal challenge ID — use for audit trails and refund lookups.

IssueTokenParams

Passed to fetchResourceCredentials after payment verification.
type IssueTokenParams = {
  readonly requestId: string;
  readonly challengeId: string;
  readonly resourceId: string;
  readonly planId: string;
  readonly txHash: string;
};
FieldTypeDescription
requestIdstringOriginal access request ID.
challengeIdstringChallenge that was fulfilled.
resourceIdstringResource the buyer is purchasing access to.
planIdstringSelected plan from the catalog.
txHashstringOn-chain transaction hash of the USDC transfer.

TokenIssuanceResult

Returned from fetchResourceCredentials.
type TokenIssuanceResult = {
  readonly token: string;
  readonly tokenType?: string; // Default "Bearer"
};
FieldTypeRequiredDescription
tokenstringYesThe credential (JWT, API key, etc.) the buyer will use to access the resource.
tokenTypestringNoToken scheme. Defaults to "Bearer".

Minimal Example

A working configuration with only the required fields:
import { key0Router } from "@key0ai/key0/express";
import { X402Adapter, RedisChallengeStore, RedisSeenTxStore } from "@key0ai/key0";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

const config = {
  agentName: "Photo API",
  agentDescription: "High-resolution stock photos via A2A.",
  agentUrl: "https://photos.example.com",
  providerName: "Example Inc.",
  providerUrl: "https://example.com",

  walletAddress: "0x1234567890abcdef1234567890abcdef12345678" as `0x${string}`,
  network: "mainnet" as const,

  plans: [
    { planId: "single", unitAmount: "$0.10" },
  ],

  async fetchResourceCredentials({ resourceId, planId }) {
    const token = await generateJwt({ resourceId, planId });
    return { token };
  },
};

app.use(
  key0Router({
    config,
    adapter: new X402Adapter({ network: "mainnet" }),
    store: new RedisChallengeStore({ redis }),
    seenTxStore: new RedisSeenTxStore({ redis }),
  }),
);

Full Example

All optional fields included:
import { key0Router } from "@key0ai/key0/express";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

const config = {
  // Identity
  agentName: "Photo API",
  agentDescription: "High-resolution stock photos via A2A.",
  agentUrl: "https://photos.example.com",
  providerName: "Example Inc.",
  providerUrl: "https://example.com",
  version: "2.1.0",

  // Payment
  walletAddress: "0x1234567890abcdef1234567890abcdef12345678" as `0x${string}`,
  network: "mainnet" as const,

  // Product catalog — subscription plans and pay-per-call routes can coexist
  plans: [
    { planId: "single", unitAmount: "$0.10", description: "One photo download" },
    { planId: "bulk-50", unitAmount: "$3.50", description: "50 photo downloads" },
  ],
  routes: [
    {
      routeId: "weather-query",
      method: "GET",
      path: "/api/weather/:city",
      unitAmount: "$0.01",
    },
  ],

  // Challenge
  challengeTTLSeconds: 600,

  // Credential issuance
  async fetchResourceCredentials({ resourceId, planId, txHash }) {
    const token = await generateJwt({ resourceId, planId, txHash });
    return { token, tokenType: "Bearer" };
  },
  tokenIssueTimeoutMs: 10_000,
  tokenIssueRetries: 3,

  // Lifecycle hooks
  async onPaymentReceived(grant) {
    await analytics.track("payment", { challengeId: grant.challengeId });
  },
  async onChallengeExpired(challengeId) {
    await analytics.track("challenge_expired", { challengeId });
  },

  // MCP
  mcp: true,

  // Customization
  basePath: "/v1/a2a",
  resourceEndpointTemplate: "https://photos.example.com/api/photos/{resourceId}",

  // Settlement
  gasWalletPrivateKey: process.env.GAS_WALLET_KEY as `0x${string}`,
  facilitatorUrl: "https://custom-facilitator.example.com",
  redis,
};

app.use(
  key0Router({
    config,
    adapter: new X402Adapter({ network: "mainnet" }),
    store: new RedisChallengeStore({ redis }),
    seenTxStore: new RedisSeenTxStore({ redis }),
  }),
);

Embedded Quickstart

Get started with SellerConfig in your existing Express/Hono/Fastify app.

Factory

createKey0() — the factory that wires SellerConfig into the full SDK.