A Photo Gallery Agent built with Express that sells photo access to AI agents via USDC payments.
A production-ready seller built with Express that monetizes a photo gallery API. AI agents discover pricing via the A2A agent card, pay USDC on Base, and receive a JWT to access protected endpoints — all without human intervention.
import type { NetworkName } from "@key0ai/key0";import { AccessTokenIssuer, RedisChallengeStore, RedisSeenTxStore, X402Adapter,} from "@key0ai/key0";import { key0Router, validateAccessToken } from "@key0ai/key0/express";import express from "express";import Redis from "ioredis";const PORT = Number(process.env["PORT"] ?? 3000);const PUBLIC_URL = process.env["PUBLIC_URL"] ?? `http://localhost:${PORT}`;const NETWORK = (process.env["KEY0_NETWORK"] ?? "testnet") as NetworkName;const WALLET = (process.env["KEY0_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000000") as `0x${string}`;const SECRET = process.env["KEY0_ACCESS_TOKEN_SECRET"] ?? "dev-secret-change-me-in-production-32chars!";const REDIS_URL = process.env["REDIS_URL"] ?? "redis://localhost:6379";
All configuration is driven by environment variables with sensible defaults for local development. The @key0ai/key0/express subpath export provides the Express-specific router and middleware.
const adapter = new X402Adapter({ network: NETWORK, rpcUrl: process.env["KEY0_RPC_URL"],});const redis = new Redis(REDIS_URL);const store = new RedisChallengeStore({ redis });const seenTxStore = new RedisSeenTxStore({ redis });const tokenIssuer = new AccessTokenIssuer(SECRET);
Three components wire together the payment infrastructure:
X402Adapter — verifies ERC-20 Transfer events on Base by reading on-chain transaction receipts via viem.
RedisChallengeStore — manages the challenge state machine (PENDING, PAID, DELIVERED, EXPIRED) with atomic Lua-script transitions.
RedisSeenTxStore — prevents double-spend by tracking used transaction hashes with atomic SET NX.
The plans array defines your pricing. Each plan has a unique planId, a unitAmount in USD, and a human-readable description. These are exposed in the auto-generated agent card so AI agents can discover what you sell and how much it costs.challengeTTLSeconds controls how long an agent has to complete payment before the challenge expires (15 minutes here).
This is the core monetization hook. After Key0 verifies the on-chain payment, it calls fetchResourceCredentials and returns whatever credential you issue. Here, the callback mints a JWT with plan-dependent expiry:
single-photo: 1-hour token
full-album: 24-hour token
The JWT payload includes the transaction hash and plan ID for downstream auditing.
fetchResourceCredentials can return any string — a JWT, an API key, an OAuth token, or a signed URL. Key0 passes it through to the agent as-is.
onPaymentReceived fires after payment verification and credential issuance succeed. Use it for logging, analytics, webhooks, or notifying downstream systems.resourceEndpointTemplate tells agents where to use their token. The {resourceId} placeholder is replaced with the actual resource identifier in the AccessGrant response.
A single middleware call gates all /api/* routes behind JWT validation. This is decoupled from the payment flow — it only verifies the token signature and expiry. Your route handlers stay clean and focused on business logic.
The agent card is auto-generated from your SellerConfig. It describes the seller’s capabilities, pricing plans, and A2A endpoint URL — everything an agent needs to initiate a purchase.
To run a full end-to-end payment, start this seller and point an agent at it. The agent will discover the seller, select a plan, pay USDC on Base Sepolia, and retrieve the protected photo data.