The standalone service example is the most comprehensive Key0 deployment pattern. It runs Key0 as its own microservice, decoupled from your backend, handling the full payment lifecycle: agent discovery, challenge issuance, on-chain verification, token delivery, payment notifications, and automatic refunds.
This is the recommended architecture for production deployments where you want to isolate payment infrastructure from your application logic.
Architecture
┌─────────────────────────────┐
│ AI Agent │
└──────┬───────────┬───────────┘
│ │
1. Discover │ │ 5. Use token
+ Pay (A2A) │ │ (Bearer JWT)
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Key0 Service │ │ Your Backend │
│ │──>│ │
│ - Challenge │ 3.│ - Protected APIs │
│ - Verify tx │ │ - /internal/* │
│ - Issue JWT │ │ │
│ - Refund │ │ │
└──────────────┘ └──────────────────┘
│ ▲ ▲
2. │ │ 4. │
▼ │ │
┌──────────────┐ │
│ Base (USDC) │ │
└──────────────┘ │
│
┌──────────────┐ │
│ Redis/Postgres├────────┘
└──────────────┘
Flow:
Agent discovers the Key0 service via /.well-known/agent.json and requests access via A2A
Key0 issues an x402 challenge; agent pays USDC on Base
Key0 verifies the on-chain transfer and notifies your backend
Key0 issues a JWT (locally or via your backend) and returns it to the agent
Agent uses the JWT to call your protected API directly
What This Example Demonstrates
Capability Options Storage backend Redis or Postgres, selected at startup Auth strategies noAuth, sharedSecretAuth, signedJwtAuth for backend communicationToken issuance Native (local JWT) or remote (delegates to backend) Gas wallet Optional self-contained settlement mode MCP support mcp: true enables MCP tool discovery alongside A2AAutomatic refunds BullMQ cron processes refunds for failed deliveries Multi-plan pricing Pay-as-you-go, monthly, and yearly plans
Configuration Reference
Core Settings
Variable Required Default Description KEY0_PORTNo 3001Port the service listens on KEY0_NETWORKNo testnettestnet (Base Sepolia) or mainnet (Base)KEY0_ACCESS_TOKEN_SECRETYes — Secret for JWT signing. Must be at least 32 characters. KEY0_WALLET_ADDRESSYes — Wallet address to receive USDC payments KEY0_PUBLIC_URLNo http://localhost:{port}Public URL for agent card and endpoint URLs CHALLENGE_TTL_SECONDSNo 900How long a payment challenge remains valid
Backend Communication
Variable Required Default Description BACKEND_API_URLYes — Base URL of your backend service BACKEND_AUTH_STRATEGYNo noneAuth strategy: none, shared-secret, or jwt INTERNAL_AUTH_SECRETConditional — Required when BACKEND_AUTH_STRATEGY=shared-secret TOKEN_MODENo nativenative (local JWT) or remote (call backend)
Storage
Variable Required Default Description STORAGE_BACKENDNo redisredis or postgresREDIS_URLConditional — Required when STORAGE_BACKEND=redis DATABASE_URLConditional — Required when STORAGE_BACKEND=postgres
Gas Wallet and Refunds
Variable Required Default Description USE_GAS_WALLETNo falseEnable gas wallet settlement mode GAS_WALLET_PRIVATE_KEYConditional — Required when USE_GAS_WALLET=true KEY0_WALLET_PRIVATE_KEYNo — Private key for sending refunds. Refund cron is disabled without this. REFUND_INTERVAL_MSNo 15000How often the refund cron runs (ms) REFUND_MIN_AGE_MSNo 30000Minimum age before a failed delivery is eligible for refund (ms)
Variable Required Default Description AGENT_NAMENo Key0 ServiceName shown in the A2A agent card AGENT_DESCRIPTIONNo Payment-gated API access for AI agentsDescription in the agent card PROVIDER_NAMENo Example CorpProvider name in the agent card PROVIDER_URLNo https://example.comProvider URL in the agent card
Code Walkthrough
Storage Backend Selection
The service selects its storage layer at startup based on a single environment variable. Both backends implement the same IChallengeStore and ISeenTxStore interfaces, so the rest of the code is storage-agnostic.
const redis = new Redis ( process . env . REDIS_URL ! );
store = new RedisChallengeStore ({ redis , challengeTTLSeconds: 900 });
seenTxStore = new RedisSeenTxStore ({ redis });
Redis uses atomic Lua scripts for state transitions and SET NX for double-spend prevention. Best for low-latency, single-region deployments. const postgres = ( await import ( "postgres" )). default ;
const sql = postgres ( process . env . DATABASE_URL ! );
store = new PostgresChallengeStore ({ sql });
seenTxStore = new PostgresSeenTxStore ({ sql });
Postgres uses row-level locking for atomicity. Best when you already run Postgres and want a single data store.
Regardless of which storage backend you choose, the BullMQ refund cron always requires a Redis connection. If you use Postgres for storage, set REDIS_URL or BULLMQ_REDIS_URL separately for the job queue.
Auth Strategy Configuration
When Key0 communicates with your backend (to notify payments or request token issuance), it authenticates using one of three strategies:
let authProvider : AuthHeaderProvider ;
if ( BACKEND_AUTH_STRATEGY === "jwt" ) {
authProvider = signedJwtAuth ( serviceTokenIssuer , "backend-service" );
} else if ( BACKEND_AUTH_STRATEGY === "shared-secret" ) {
authProvider = sharedSecretAuth ( "X-Internal-Auth" , INTERNAL_AUTH_SECRET );
} else {
authProvider = noAuth ();
}
Strategy When to use What it sends noAuth()Local development, trusted internal networks No auth headers sharedSecretAuth()Simple production setups X-Internal-Auth: <secret> headersignedJwtAuth()Zero-trust environments Authorization: Bearer <signed-jwt> header
Never use noAuth() in production. Use shared-secret as a minimum, or jwt for environments where services communicate across network boundaries.
Token Issuance Modes
After payment verification, Key0 needs to issue a credential. The standalone service supports two modes:
Key0 signs a JWT locally using AccessTokenIssuer. The token includes the challenge ID, resource ID, plan ID, and transaction hash: const localTokenIssuer = new AccessTokenIssuer ( SECRET );
fetchResourceCredentials = async ( params ) => {
const ttl = 3600 ;
return localTokenIssuer . sign (
{
sub: params . requestId ,
jti: params . challengeId ,
resourceId: params . resourceId ,
planId: params . planId ,
txHash: params . txHash ,
},
ttl ,
);
};
Your backend validates the token using the same KEY0_ACCESS_TOKEN_SECRET. Best when Key0 and your backend share a secret. Key0 calls your backend’s /internal/issue-token endpoint, letting your backend control the token format entirely: const remoteTokenIssuer = createRemoteTokenIssuer ({
url: ` ${ BACKEND_API_URL } /internal/issue-token` ,
auth: authProvider ,
timeoutMs: 10000 ,
});
fetchResourceCredentials = remoteTokenIssuer ;
Best when your backend already has its own token system (OAuth, API keys, session tokens) and you want Key0 to integrate with it.
Plan Catalog
The service defines a multi-tier pricing structure that agents discover via the A2A agent card:
const plans = [
{
planId: "basic" ,
unitAmount: "$0.015" ,
description: "Pay-as-you-go. 2 concurrent agents, 10 req/min." ,
},
{
planId: "starter-monthly" ,
unitAmount: "$15.00" ,
description: "1,650 req/month, 10 concurrent agents, 100 req/min." ,
},
{
planId: "starter-yearly" ,
unitAmount: "$168.00" ,
description: "Same as starter-monthly, billed yearly (save 7%)." ,
},
{
planId: "pro-monthly" ,
unitAmount: "$150.00" ,
description: "16,500 req/month, 50 concurrent agents, 1,000 req/min." ,
},
];
Agents select a plan when requesting access. Key0 issues a challenge for the corresponding unitAmount, and the entire payment/verification/delivery flow proceeds automatically.
Payment Notifications
After a payment is verified and a credential is issued, Key0 notifies your backend so you can activate the subscription, update billing records, or trigger downstream workflows:
onPaymentReceived : async ( grant ) => {
const headers = await authProvider ();
await fetch ( ` ${ BACKEND_API_URL } /internal/payment-received` , {
method: "POST" ,
headers: { "Content-Type" : "application/json" , ... headers },
body: JSON . stringify ( grant ),
});
},
The payment notification is fire-and-forget. If the backend is unreachable, the error is logged but the agent still receives their token. Design your backend to handle idempotent replays if needed.
Refund Cron Setup
The BullMQ-based refund cron automatically processes refunds for challenges that reached the PAID state but failed delivery. It runs on a configurable interval and only processes challenges older than a grace period:
const refundQueue = new Queue ( "refund-cron" , { connection: bullConnection });
await refundQueue . add ( "process-refunds" , {}, {
repeat: { every: REFUND_INTERVAL_MS },
});
const cronWorker = new Worker ( "refund-cron" , () => runRefundCron (), {
connection: bullConnection ,
});
The refund processor transitions challenges through PAID -> REFUND_PENDING -> REFUNDED (or REFUND_FAILED), sending USDC back to the payer on-chain.
The refund cron requires KEY0_WALLET_PRIVATE_KEY to sign refund transactions. Without it, the cron starts but skips processing. Never commit private keys to source control — use a secrets manager.
Running the Example
Start infrastructure
You need Redis running (required for BullMQ, and optionally for storage). If using Postgres for storage, start that as well. Redis only
Redis + Postgres
docker run -d --name redis -p 6379:6379 redis:7-alpine
Configure environment variables
Create a .env file in the examples/standalone-service directory: # Core
KEY0_PORT = 3001
KEY0_NETWORK = testnet
KEY0_ACCESS_TOKEN_SECRET = your-secret-at-least-32-characters-long
KEY0_WALLET_ADDRESS = 0xYourWalletAddress
# Backend
BACKEND_API_URL = http://localhost:3000
BACKEND_AUTH_STRATEGY = shared-secret
INTERNAL_AUTH_SECRET = your-internal-secret
# Storage
STORAGE_BACKEND = redis
REDIS_URL = redis://localhost:6379
# Token mode
TOKEN_MODE = native
Install dependencies
cd examples/standalone-service
bun install
Start the service
You should see: Key0 Standalone Service
Port: 3001
Network: testnet
Storage: REDIS
Token Mode: native
Facilitation Mode: Standard
Backend URL: http://localhost:3000
Agent Card: http://localhost:3001/.well-known/agent.json
A2A Endpoint: http://localhost:3001/agent
Refund cron:
Interval : 15s
Grace period : 30s
Status : DISABLED (set KEY0_WALLET_PRIVATE_KEY)
Endpoints
Once running, the service exposes:
Endpoint Description GET /.well-known/agent.jsonA2A agent card with capabilities and pricing POST /x402/accessUnified x402 HTTP payment endpoint (also handles A2A JSON-RPC with X-A2A-Extensions header) GET /.well-known/mcp.jsonMCP discovery (when mcp: true) POST /mcpMCP Streamable HTTP endpoint GET /healthHealth check with storage, token mode, and network info GET /.well-known/token-infoToken format metadata for backend integration
Testing
Verify the agent card
curl http://localhost:3001/.well-known/agent.json | jq .
Check health
curl http://localhost:3001/health | jq .
Expected response:
{
"status" : "ok" ,
"service" : "key0" ,
"storage" : "redis" ,
"tokenMode" : "native" ,
"network" : "testnet"
}
Run a full payment flow
Point an agent at this service to run a complete discovery-to-payment-to-access flow.
Test MCP discovery
curl http://localhost:3001/.well-known/mcp.json | jq .
Production Checklist
Before deploying to production:
Source code examples/standalone-service/server.ts