A production-ready template for building AI agents that accept on-chain payments. Combines Hono (web framework), x402 (HTTP 402 payment protocol), A2A (Agent-to-Agent protocol), and ERC-8004 (on-chain agent identity) into a serverless agent that deploys to AWS Lambda in one command.
Pre-configured for Base Sepolia (testnet) — switch to Base Mainnet by changing two env vars (see Network Switching). Designed to be consumed by AI agents: ships with AGENT.md so tools like OpenClaw can index and interact with this repo out of the box.
flowchart LR
Client -->|HTTPS| FnURL["Lambda Function URL"]
subgraph AWS["AWS (Terraform-managed)"]
FnURL --> Lambda["Lambda<br/>(arm64, Docker)"]
Lambda --> SM["Secrets Manager<br/>(private key)"]
Lambda -.-> CW["CloudWatch Logs"]
ECR["ECR"] -.->|image source| Lambda
end
subgraph Hono["Hono App"]
Free["/health<br/>/.well-known/agent-card.json<br/>/.well-known/agent-registration.json"]
Paid["/api/*   /a2a"]
end
Lambda --> Hono
Paid -->|verify payment| Facilitator["x402 Facilitator"]
Facilitator -->|settle| Blockchain["Base (USDC)"]
Terraform manages all AWS infrastructure: Lambda function, ECR image repository, Secrets Manager (private key), IAM roles, CloudWatch log group, and the Function URL endpoint. Endpoints split into free (/health, agent card, read-only A2A methods) and paid (/api/*, A2A message/send and message/stream) — see How Payments Work for the full x402 sequence.
| Library | Package | What it does |
|---|---|---|
| Hono | hono |
Ultrafast web framework. Runs on Node.js, AWS Lambda, Deno, Bun, Cloudflare Workers — same code everywhere. |
| x402 | @x402/hono @x402/core @x402/evm @coinbase/x402 |
HTTP 402 payment protocol. Uses Coinbase CDP facilitator for payment verification and settlement. |
| A2A | @a2a-js/sdk |
Google's Agent-to-Agent protocol. Standardized JSON-RPC interface so agents can discover and talk to each other. |
| ERC-8004 | agent0-sdk erc-8004-js |
On-chain agent identity. Mints an NFT with IPFS metadata pointing to your agent's live endpoint. Supports update-or-create registration flow. |
| Zod | zod |
TypeScript-first schema validation. Used to validate API route inputs. |
- Node.js 22+ and npm
- AWS CLI configured (
aws configure) - Terraform (for Lambda deployment)
- Docker (for building the Lambda container image)
- Pinata account (free tier works — needed only for ERC-8004 identity registration)
git clone https://github.com/wgopar/a2a-x402-agent-template.git
cd a2a-x402-agent-template
npm installnpm run create-wallet -- my-agentThis generates a new Ethereum wallet and writes the address + private key to both .env (local dev) and infra/terraform.tfvars (Lambda deploy). It also sets function_name in tfvars, which names all AWS resources (Lambda, ECR, IAM roles). Use a unique name per agent to prevent resource collisions. Both files are gitignored.
Next: Fund the wallet with testnet ETH from a Base Sepolia faucet.
The wallet script pre-fills most values. Review .env and adjust if needed:
# .env (auto-generated, gitignored)
WALLET_ADDRESS=0x... # from create-wallet
PRIVATE_KEY=0x... # from create-wallet
NETWORK=eip155:84532 # Base Sepolia (testnet)
RPC_URL=https://sepolia.base.org
BYPASS_PAYMENTS=true # skip x402 for local dev (auto-disabled in production)
AGENT_NAME=Hello Agent
AGENT_URL=http://localhost:3000
PORT=3000
# CDP Facilitator (required when BYPASS_PAYMENTS=false)
CDP_API_KEY_ID=your-cdp-key-id
CDP_API_KEY_SECRET=your-cdp-key-secretSee Configuration Reference for all available variables.
npm run dev# Health check (always free)
curl http://localhost:3000/health
# Agent card with structured entrypoints (always free)
curl http://localhost:3000/.well-known/agent-card.json
# Agent registration linkage (always free)
curl http://localhost:3000/.well-known/agent-registration.json
# API endpoint — returns 200 with BYPASS_PAYMENTS=true, or 402 with payment terms
curl http://localhost:3000/api/helloWith BYPASS_PAYMENTS=true (default for local dev), all endpoints return responses directly. When payments are enabled, unpaid requests return 402 Payment Required with payment terms in the x-payment-required header.
Make sure you've run npm run create-wallet -- <name> first — it sets function_name in terraform.tfvars, which is required and names all AWS resources.
cd infra && terraform init
terraform apply -target=aws_ecr_repository.agent # create ECR repo
cd ..npm run deployThis builds a Docker image, pushes to ECR, and runs terraform apply — your agent is live. Terraform stores your private key in AWS Secrets Manager (not as a Lambda environment variable), so it's encrypted at rest and accessed via IAM at runtime.
flowchart LR
Build["docker build<br/>(esbuild bundle)"] --> Push["docker push<br/>(ECR)"]
Push --> Apply["terraform apply<br/>(Lambda + IAM + Secrets)"]
Apply --> Live["Function URL<br/>(public HTTPS)"]
# Get your Function URL from terraform output
cd infra && terraform output function_url
# Test it
curl https://<your-function-url>/health
curl https://<your-function-url>/.well-known/agent-card.jsonERC-8004 registration mints an NFT that points to your agent's live endpoint via IPFS metadata. This enables on-chain agent discovery. If assets/icon.png exists, it's automatically uploaded to IPFS and included in the metadata.
The register script uses an update-or-create flow: it auto-detects if the wallet already owns an agent on the registry and updates its metadata instead of minting a new token. You can also set AGENT_ID in .env to target a specific token.
Sign up at pinata.cloud, create an API key, and add the JWT to .env:
PINATA_JWT=your-pinata-jwtPoint AGENT_URL in .env to your deployed Lambda Function URL (not localhost):
AGENT_URL=https://<your-function-url>Place a PNG image at assets/icon.png. It will be uploaded to IPFS and included in the on-chain metadata automatically. To use a different path, set AGENT_IMAGE_PATH in .env.
npm run registerflowchart LR
Icon["Upload icon<br/>(assets/icon.png)"] --> Meta["Build metadata<br/>(name, skills, endpoint, image)"]
Meta --> IPFS["Upload to IPFS<br/>(via Pinata)"]
IPFS --> Check{"Agent exists?"}
Check -->|No| Mint["Mint ERC-8004 NFT<br/>(on-chain)"]
Check -->|Yes| Update["Update token URI<br/>(on-chain)"]
Anyone can now discover your agent on-chain:
tokenURI(tokenId) → ipfs://... → metadata JSON → services[0].endpoint → your agent
The x402 protocol uses gasless payments. Clients never submit blockchain transactions — they sign an off-chain EIP-3009 transferWithAuthorization, and the facilitator settles on-chain on their behalf.
sequenceDiagram
participant Client
participant Agent
participant Facilitator
participant Blockchain
Client->>Agent: Request (no payment)
Agent-->>Client: 402 + payment terms
Note over Client: Sign EIP-3009<br/>transferWithAuthorization<br/>(gasless, off-chain)
Client->>Agent: Retry + signed authorization
Agent->>Facilitator: Verify + settle
Facilitator->>Blockchain: Submit transferWithAuthorization
Facilitator-->>Agent: Settlement proof
Agent-->>Client: Response + tx hash
| Endpoint | Payment |
|---|---|
GET /health |
Free |
GET /.well-known/agent-card.json |
Free |
GET /.well-known/agent-registration.json |
Free |
POST /a2a — tasks/get, tasks/cancel |
Free |
POST /a2a — message/send, message/stream |
$0.01 USDC |
GET /api/hello |
$0.01 USDC |
POST /api/hello |
$0.01 USDC |
Read-only A2A methods are always free. Only work-producing methods (message/send, message/stream) require payment.
Three files to edit:
export const skills: AgentSkill[] = [
{
id: "my-skill",
name: "My Skill",
description: "What this skill does",
tags: ["tag1", "tag2"],
examples: ["Do the thing", "Another example"],
},
];export class MyExecutor implements AgentExecutor {
async execute(
requestContext: RequestContext,
eventBus: ExecutionEventBus,
): Promise<void> {
// Your agent logic here
const task: Task = {
kind: "task",
id: requestContext.taskId,
contextId: requestContext.contextId,
status: {
state: "completed",
message: {
kind: "message",
messageId: uuidv4(),
role: "agent",
parts: [{ kind: "text", text: "Your response" }],
contextId: requestContext.contextId,
},
},
};
eventBus.publish(task);
eventBus.publish({
kind: "status-update",
taskId: requestContext.taskId,
contextId: requestContext.contextId,
status: task.status,
final: true,
});
eventBus.finished();
}
}api.get("/my-endpoint", (c) => {
return c.json({ data: "your response" });
});New routes under /api/* are automatically payment-gated. Update the price in src/app.ts if needed.
├── src/
│ ├── app.ts # Composition root — middleware chain + routes
│ ├── config.ts # Environment config (async for Secrets Manager)
│ ├── server.ts # Node.js entrypoint (local dev)
│ ├── lambda.ts # AWS Lambda entrypoint (lazy init)
│ ├── agent/
│ │ ├── card.ts # AgentCard builder
│ │ ├── entrypoints.ts # ★ Structured entrypoints (customize this)
│ │ ├── executor.ts # ★ Task execution logic (customize this)
│ │ └── skills.ts # ★ Skill definitions (customize this)
│ ├── a2a/
│ │ └── handler.ts # A2A JSON-RPC handler + agent card endpoint
│ ├── payments/
│ │ └── x402.ts # x402 payment middleware factory
│ ├── routes/
│ │ ├── api.ts # ★ Paid HTTP routes (customize this)
│ │ └── health.ts # Health check
│ └── identity/
│ └── erc8004.ts # ERC-8004 registration (agent0-sdk)
├── scripts/
│ ├── create-wallet.ts # Generate wallet + set function_name → .env + terraform.tfvars
│ ├── register-identity.ts# Register on-chain identity
│ └── deploy.sh # Build + push + terraform apply
├── infra/
│ ├── main.tf # Lambda, ECR, IAM, Secrets Manager
│ ├── variables.tf # Terraform variable definitions
│ ├── outputs.tf # Terraform outputs (function URL, etc.)
│ └── terraform.tfvars.example
├── assets/
│ └── icon.png # Agent icon (uploaded to IPFS during registration)
├── test/ # Vitest tests
├── .env.example # Environment template
├── Dockerfile # Multi-stage (lambda default, server for ECS)
└── tsconfig.json
Files marked with ★ are the ones you customize.
| Variable | Required | Default | Description |
|---|---|---|---|
WALLET_ADDRESS |
Yes | — | Ethereum wallet address |
PRIVATE_KEY |
Yes (local dev) | — | Private key for signing. Not needed on Lambda if PRIVATE_KEY_SECRET_ARN is set. |
PRIVATE_KEY_SECRET_ARN |
Yes (Lambda) | — | AWS Secrets Manager ARN. Lambda uses this instead of PRIVATE_KEY. |
NETWORK |
No | eip155:84532 |
Chain identifier (Base Sepolia) |
RPC_URL |
No | https://sepolia.base.org |
JSON-RPC endpoint |
CDP_API_KEY_ID |
When payments enabled | — | CDP API key ID for x402 facilitator authentication |
CDP_API_KEY_SECRET |
When payments enabled | — | CDP API key secret for x402 facilitator authentication |
BYPASS_PAYMENTS |
No | false |
Skip x402 payment enforcement. Throws if true in production (NODE_ENV=production). |
AGENT_NAME |
No | Hello Agent |
Agent display name (shown in agent card) |
AGENT_DESCRIPTION |
No | A simple Hello World agent |
Agent description (shown in agent card) |
AGENT_URL |
No | http://localhost:3000 |
Public URL. Set to Function URL when deploying to Lambda. |
PORT |
No | 3000 |
Local dev server port |
AGENT_ID |
No | — | ERC-8004 agent token ID. Set after first registration. Used by register script for updates. |
PINATA_JWT |
For registration | — | Pinata API JWT. Required only for npm run register (ERC-8004). |
AGENT_IMAGE_PATH |
No | assets/icon.png |
Path to agent icon. Uploaded to IPFS during registration if present. |
AGENT_PROVIDER_NAME |
No | — | Provider org name. Requires AGENT_PROVIDER_URL to also be set. |
AGENT_PROVIDER_URL |
No | — | Provider org URL |
AGENT_DOCS_URL |
No | — | Documentation URL (shown in agent card) |
AGENT_ICON_URL |
No | — | Icon URL (shown in agent card) |
The same wallet works on both testnet and mainnet. Switch by editing .env and infra/terraform.tfvars:
| Base Sepolia (testnet) | Base Mainnet | |
|---|---|---|
NETWORK |
eip155:84532 |
eip155:8453 |
RPC_URL |
https://sepolia.base.org |
https://mainnet.base.org |
The CDP facilitator (@coinbase/x402) handles both networks — no URL switching needed. Just set CDP_API_KEY_ID and CDP_API_KEY_SECRET.
After switching, redeploy (npm run deploy) and re-register identity (npm run register) on the new network.
npm test # Run all tests
npm run lint # TypeScript type check25 tests cover app composition (including JSON-RPC error handling and Zod validation), agent card generation, executor event publishing, config loading (including bypass-payments safety), and Lambda handler setup.