How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put a language‑model‑driven agent behind a micropayment gateway and run it unattended.
1. Why a paid agent?
Running an LLM‑based service continuously incurs compute cost. If you can offset that cost—or even turn a profit—by charging users per invocation, the agent can stay online 24/7 without you having to feed it money. The experiment below shows a minimal, production‑ish setup that:
- Accepts paid requests via the x402 micropayment standard.
- Executes a deterministic workflow (prompt → LLM → optional tool).
- Returns the result only after payment verification.
- Runs on a cheap, stateless platform (Cloudflare Workers) so the marginal cost per call is near‑zero.
I’ll walk through each piece, point out where things get tricky, and share the exact snippets I used. No hype—just trade‑offs and lessons learned.
2. High‑level architecture
+----------------+ x402 payment +-------------------+
| Client (web) | ---------------------> | Payment Verifier |
+----------------+ +-------------------+
|
| (paid?)
v
+----------------+ +----------------+ +----------------+
| Request Router| ---> | Agent Core | ---> | Tool Executer |
+----------------+ +----------------+ +----------------+
|
v
+----------------+ +----------------+ +----------------+
| Response Formatter| <‑‑ | LLM Call | <‑‑ | Prompt Builder |
+----------------+ +----------------+ +----------------+
- Payment Verifier – checks the x402 header, validates the signature, and confirms that enough USDC has been escrowed on Base.
- Request Router – strips payment data, forwards the plain JSON payload to the agent.
- Agent Core – a thin orchestrator that decides whether to call a tool (e.g., a weather API) or just return a LLM completion.
-
LLM Call – a stateless request to a hosted model (I used OpenAI’s
gpt-4o-minivia their API). - Tool Executer – optional side‑effects; in my demo it’s a simple currency‑conversion API.
All components run inside a single Cloudflare Worker, which means:
- Cold starts are ~5 ms on average (V8 isolate).
- No persistent state – each invocation is independent, simplifying scaling.
- Limited execution time – Workers have a 30 s CPU limit; heavy workloads must be off‑loaded to external services.
3. Payment verification with x402
The x402 spec defines an X402-Payment header containing a base64‑encoded JSON Web Token (JWT) that escrows the amount. The verifier checks:
- JWT signature against the publisher’s public key (published on‑chain).
- That the
amountfield matches the price we advertise. - That the token hasn’t been replayed (we keep a short‑lived nonce cache).
Below is the verifier I added to the worker. It relies on the @x402/verifier npm package (≈2 KB) and a tiny LRU cache for nonces.
// payment-verifier.js
import { verifyPayment } from '@x402/verifier';
import { LRUCache } from 'lru-cache';
// 5‑minute window, max 1000 entries (adjust for traffic)
const nonceCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });
export async function verify(request, priceUSDC) {
const header = request.headers.get('X402-Payment');
if (!header) return { ok: false, reason: 'missing payment header' };
let payload;
try {
payload = await verifyPayment(header, {
// Publisher's public key – hard‑coded for demo; in prod fetch from contract
publicKey: PUBLISHER_PUBLIC_KEY,
// Expect payment in USDC on Base (chainId 8453)
network: 'base',
token: 'USDC',
});
} catch (e) {
return { ok: false, reason: `verification failed: ${e.message}` };
}
// Check amount
if (Number(payload.amount) < priceUSDC) {
return { ok: false, reason: `insufficient amount: ${payload.amount}` };
}
// Replay protection
const nonce = payload.nonce;
if (nonceCache.has(nonce)) {
return { ok: false, reason: 'replay attack' };
}
nonceCache.set(nonce, true);
return { ok: true, payload };
}
Trade‑offs
- Latency – verification adds ~30‑50 ms (mostly network round‑trip to the x402 validator’s endpoint).
- Complexity – you must maintain the public key and handle key rotations; I chose to hard‑code it for simplicity, which means redeploying on each key change.
- Security – the nonce cache lives only in the worker’s memory; a sudden spike could evict nonces and enable replays. For high‑value services you’d replace it with a durable store (e.g., Durable Objects or KV).
4. Agent core: prompt → LLM → optional tool
The core is deliberately simple: it receives a JSON body { "prompt": string, "useTool?: boolean }, builds a system message, calls the model, and—if useTool is true—calls a helper API before returning the final answer.
// agent-core.js
import { OpenAI } from 'openai';
import fetch from 'node-fetch';
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
export async function handleAgent(request) {
const { prompt, useTool = false } = await request.json();
// 1️⃣ Build messages
const messages = [
{ role: 'system', content: 'You are a helpful assistant that answers concisely.' },
{ role: 'user', content: prompt },
];
// 2️⃣ Call LLM
let completion;
try {
completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages,
temperature: 0.2,
max_tokens: 256,
});
} catch (e) {
return new Response(JSON.stringify({ error: 'llm_failure', detail: e.message }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
});
}
let answer = completion.choices[0].message.content.trim();
// 3️⃣ Optional tool call (e.g., currency conversion)
if (useTool) {
try {
const toolResp = await fetch('https://api.exchangerate.host/convert?from=USD&to=EUR&amount=1');
const data = await toolResp.json();
answer += `\n\nTool result: 1 USD = ${data.result.toFixed(4)} EUR`;
} catch (e) {
// Tool failure shouldn’t break the main answer; we just note it.
answer += `\n\n[Tool error: ${e.message}]`;
}
}
return new Response(JSON.stringify({ answer }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
Trade‑offs
-
Determinism – Setting
temperature: 0.2reduces variability but doesn’t eliminate it. For mission‑critical outputs you’d need output validation or a stricter model. -
Cost – Each call to
gpt-4o-miniis about $0.00015 per 1k tokens. At 256 tokens output + ~100 tokens input, that’s ≈$0.00005 per request. The x402 price I chose ($0.01) leaves a comfortable margin for worker compute and potential retries. - Tool coupling – The example tool is a public, rate‑limited API. In production you’d either host your own micro‑service or use a reliable paid API with its own SLA.
5. Wiring it all together in the worker
The final worker script checks payment, strips the header, and delegates to the agent core.
javascript
// index.js
import { verify } from './payment-verifier.js';
import { handleAgent } from './agent-core.js';
const PRICE_USDC = 0.01; // $0.01 per call
export default {
async fetch(request, env, ctx) {
// Only accept POST to /agent
const url = new URL(request.url);
if (request.method !== 'POST' || url.pathname !== '/agent') {
return new Response('Not Found', { status: 404 });
}
// 1️⃣ Verify payment
const paymentResult = await verify(request, PRICE_USDC);
if (!paymentResult.ok) {
return new Response(
JSON.stringify({ error: 'payment_required', reason: paymentResult.reason }),
{ status: 402, headers: { 'Content-Type': 'application/json' } }
);
}
// 2️⃣ Strip payment header so the agent doesn’t see it
const newHeaders = new Headers(request.headers);
newHeaders.delete('X402-Payment');
const agent
Top comments (0)