Skip to main content
A lightweight seller agent built with Hono that monetizes a photo gallery API. AI agents discover the seller via its A2A agent card, pay USDC on Base, and receive a JWT to access protected photo endpoints.

What It Demonstrates

  • Mounting the Key0 payment gateway on a Hono app with key0App
  • Defining multiple pricing plans with different prices and access durations
  • Issuing JWTs with plan-specific TTLs via AccessTokenIssuer
  • Protecting downstream routes with honoValidateAccessToken middleware

Architecture

Agent                          Hono Seller
  |                                |
  |-- GET /.well-known/agent.json -|  (discover capabilities)
  |-- GET /discover ------------->|  (browse plans and pricing)
  |<-------- plan catalog ---------|  (planIds, amounts, wallet)
  |-- POST /x402/access ---------->|  (request access, planId)
  |<-------- X402Challenge --------|  (amount, destination, chainId)
  |                                |
  |  ... pays USDC on-chain ...    |
  |                                |
  |-- POST /x402/access ---------->|  (submit payment proof)
  |<-------- AccessGrant ----------|  (JWT + resource endpoint)
  |                                |
  |-- GET /api/photos/:id -------->|  (Bearer token)
  |<-------- photo data -----------|

Prerequisites

  • Bun v1.0+
  • A wallet address to receive USDC payments
This example omits store and seenTxStore from the key0App options for brevity. In practice, createKey0() (used internally) throws if these are missing — RedisChallengeStore and RedisSeenTxStore (or their Postgres equivalents) are required for the server to start. See the Storage guide for setup instructions.

Key Files

The entire example is a single file:
examples/hono-seller/
├── server.ts          # Hono app with Key0 payment gateway
├── package.json
└── .env.example

Code Walkthrough

1. Imports and configuration

import type { NetworkName } from "@key0ai/key0";
import { AccessTokenIssuer, X402Adapter } from "@key0ai/key0";
import { honoValidateAccessToken, key0App } from "@key0ai/key0/hono";
import { Hono } from "hono";

const PORT = Number(process.env["PORT"] ?? 3001);
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!";
All configuration comes from environment variables with safe defaults for local development. The @key0ai/key0/hono subpath export provides Hono-specific helpers.

2. Payment adapter and token issuer

const adapter = new X402Adapter({
  network: NETWORK,
  rpcUrl: process.env["KEY0_RPC_URL"],
});

const tokenIssuer = new AccessTokenIssuer(SECRET);
X402Adapter handles on-chain verification of USDC transfers on Base. AccessTokenIssuer signs and verifies JWTs using the shared secret.

3. Payment gateway

const gate = key0App({
  config: {
    agentName: "Photo Gallery Agent",
    agentDescription: "Purchase access to premium photos via USDC payments on Base",
    agentUrl: `http://localhost:${PORT}`,
    providerName: "Example Corp",
    providerUrl: "https://example.com",
    walletAddress: WALLET,
    network: NETWORK,
    challengeTTLSeconds: 900,
    plans: [
      {
        planId: "single-photo",
        unitAmount: "$0.10",
        description: "Single photo — 1 hour access",
      },
      {
        planId: "full-album",
        unitAmount: "$1.00",
        description: "Full album — 24 hour access",
      },
    ],
    // ...fetchResourceCredentials shown below
  },
  adapter,
});
key0App returns a Hono sub-app that mounts the A2A agent card at /.well-known/agent.json and the x402 payment endpoint at POST /x402/access. The plans array defines two tiers — a single photo for $0.10 and a full album for $1.00.

4. Token issuance

fetchResourceCredentials: async (params) => {
  const ttl = params.planId === "single-photo" ? 3600 : 86400;
  return tokenIssuer.sign(
    {
      sub: params.requestId,
      jti: params.challengeId,
      resourceId: params.resourceId,
      planId: params.planId,
      txHash: params.txHash,
    },
    ttl,
  );
},
fetchResourceCredentials runs after payment verification succeeds, minting a JWT with a plan-appropriate lifetime — 1 hour for single photos, 24 hours for full albums.

5. Payment lifecycle hook

The optional onPaymentReceived callback fires after a payment is verified and a credential is issued. Use it for logging, analytics, or notifying downstream systems:
onPaymentReceived: async (grant) => {
  console.log(`[Payment] Received payment for ${grant.resourceId} (${grant.planId})`);
  console.log(`  TX: ${grant.explorerUrl}`);
},

6. Protecting routes with JWT middleware

const app = new Hono();
app.route("/", gate);

const api = new Hono();
api.use("/*", honoValidateAccessToken({ secret: SECRET }));

api.get("/photos/:id", (c) => {
  const photoId = c.req.param("id");
  return c.json({
    id: photoId,
    url: `https://cdn.example.com/photos/${photoId}.jpg`,
    title: `Premium Photo ${photoId}`,
    resolution: "4K",
  });
});

app.route("/api", api);
The /api sub-router is gated by honoValidateAccessToken. Any request without a valid Bearer token receives a 401. The middleware is independent of the payment flow — it only verifies JWTs signed by the same secret.

7. Server export

export default {
  port: PORT,
  fetch: app.fetch,
};
Bun picks up the default export automatically. No explicit serve() call needed.

Running the Example

1

Install dependencies

cd examples/hono-seller
bun install
2

Configure environment

cp .env.example .env
Edit .env and set your wallet address:
.env
KEY0_WALLET_ADDRESS=0xYourWalletAddress
KEY0_ACCESS_TOKEN_SECRET=your-secret-at-least-32-characters-long
KEY0_NETWORK=testnet
3

Start the server

bun run start

Expected Output

Photo Gallery Agent (Hono) running on http://localhost:3001
  Agent card: http://localhost:3001/.well-known/agent.json
  x402 endpoint: http://localhost:3001/x402/access
  Network: testnet
  Wallet: 0xYour...Address

Verify the agent card

curl http://localhost:3001/.well-known/agent.json | jq .
This returns the auto-generated A2A agent card describing the seller’s capabilities, pricing plans, and payment endpoint URL.

Complete a payment flow

Run an agent against this seller to execute a full discovery, payment, and resource access cycle. When a payment completes, you will see:
[Payment] Received payment for photo-1 (single-photo)
  TX: https://sepolia.basescan.org/tx/0x...

Hono vs Express

The Key0 integration surface is nearly identical across frameworks. The main differences:
HonoExpress
Gateway mountkey0App() returns a Hono sub-appkey0Router() returns an Express router
Token middlewarehonoValidateAccessToken()validateAccessToken()
Import path@key0ai/key0/hono@key0ai/key0/express
Serverexport default { fetch }app.listen(port)
For full integration documentation, see the Hono integration guide.

Source code

examples/hono-seller/server.ts