How I Built an Autonomous AI Agent That Earns USDC While I Sleep
A no‑hype, developer‑focused walkthrough of the architecture, code, and trade‑offs behind a paying‑per‑call AI service on Base.
Introduction
The idea of an “AI that makes money while you sleep” sounds like a marketing slogan, but the underlying mechanics are straightforward: expose a useful model endpoint, require a micropayment for each invocation, and settle that payment in a stablecoin that can be withdrawn automatically. I built exactly that using the x402 HTTP 402 Payment Required protocol, a simple agent wrapper around OpenAI’s GPT‑4o, and a Cloudflare Worker as the deployment target. The result is a service that charges $0.01–$0.10 per call in USDC on the Base L2, with the earnings accruing to a wallet I control.
Below I walk through the core components, show the actual code I used, and discuss the honest trade‑offs I encountered. If you’re building your own autonomous agent, this should give you a concrete starting point rather than a buzz‑filled overview.
Architecture Overview
+-------------------+ +----------------------+ +-------------------+
| Client (HTTP) | ---> | Cloudflare Worker | ---> | Agent Logic (TS) |
+-------------------+ +----------------------+ +-------------------+
| ^ |
| | x402 payment verification
v |
+-------------------+
| x402 Middleware |
+-------------------+
|
v
+-------------------+
| Wallet (Base) |
+-------------------+
-
Client sends a JSON POST (
{ "question": "..."}) to the Worker’s URL. -
x402 middleware inspects the
Paymentheader, validates the signed USDC payment on Base, and either returns402 Payment Requiredor forwards the request. - If payment is good, the Worker instantiates the Agent class, which calls the OpenAI API, formats the answer, and returns JSON.
- The Worker itself never holds funds; the x402 library forwards the USDC directly to the destination address encoded in the payment header.
All pieces are stateless, which makes scaling trivial and keeps the operational footprint low.
Choosing the Payment Layer (x402)
x402 is an open standard that turns HTTP 402 into a verifiable micropayment channel. Its key properties for this use case:
| Property | Why it matters |
|---|---|
| Low‑latency verification | The middleware checks a cryptographic signature off‑chain; no on‑chain transaction is needed for each call, so verification adds < 5 ms. |
| Asset agnostic | You can specify any ERC‑20 on any EVM‑compatible chain. I chose USDC on Base because gas is ~ $0.0001 and the bridge to Ethereum is trustless. |
| Fixed‑price granularity | Amounts are encoded as a string with 6‑decimal precision (USDC’s decimals), enabling $0.01 increments without rounding tricks. |
| Custodial‑free | The payment goes straight to the address you set; the Worker never touches the funds, reducing attack surface. |
The downside is that you must manage a wallet that can receive USDC on Base and occasionally sweep funds to an exchange or a cold storage address. If your wallet gets compromised, an attacker could drain the accrued USDC, so standard key‑management practices (hardware wallet or multi‑sig) apply.
Building the Agent Logic
I kept the agent deliberately simple: a class that wraps the OpenAI chat completion endpoint. The goal was to demonstrate that the payment layer is orthogonal to the model choice—swap in any other LLM or even a deterministic service and the payment flow stays identical.
// agent.ts
import { OpenAI } from 'openai';
export class Agent {
private openai: OpenAI;
constructor(apiKey: string) {
this.openai = new OpenAI({ apiKey });
}
/**
* Sends a user question to the model and returns the text answer.
* Throws if the API call fails.
*/
async answer(question: string): Promise<string> {
const completion = await this.openai.chat.completions.create({
model: 'gpt-4o-2024-08-06',
messages: [{ role: 'user', content: question }],
temperature: 0.2,
max_tokens: 256,
});
const choice = completion.choices[0];
if (!choice.message?.content) {
throw new Error('Empty response from model');
}
return choice.message.content.trim();
}
}
Why this design?
-
Statelessness – Each request creates a fresh
Agentinstance (or reuses a singleton) with no in‑memory state, making it safe to run in a serverless environment where instances may be recycled. - Explicit error handling – The method throws on any failure; the Worker catches it and returns a 500 with a JSON error payload, avoiding leaking internal details.
-
Deterministic pricing – By fixing
temperatureandmax_tokens, I keep token usage predictable, which helps estimate the cost per call and ensures the USDC price covers the OpenAI expense plus a margin.
Deploying to Serverless (Cloudflare Workers)
Cloudflare Workers give sub‑second cold starts, built‑in KV for any caching you might need, and a generous free tier. The Worker simply chains the x402 middleware with the agent logic.
ts
// worker.ts
import { x402Middleware } from '@x402/worker';
import { Agent } from './agent';
interface Env {
OPENAI_KEY: string; // secret bound to the worker
WALLET_ADDRESS: string; // Base address that receives USDC
}
// Factory to avoid recreating the Agent on every request
const agentCache = new Map<string, Agent>();
function getAgent(env: Env): Agent {
if (!agentCache.has('main')) {
agentCache.set('main', new Agent(env.OPENAI_KEY));
}
return agentCache.get('main')!;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// ----- 1️⃣ x402 payment verification -----
const paymentResult = await x402Middleware(request, {
network: 'base', // Base L2
asset: 'USDC',
// Amount in USDC with 6 decimals (e.g., 0.01 => "0.010000")
amount: '0.010000',
destination: env.WALLET_ADDRESS,
});
// If the caller didn't attach a valid payment, respond with 402
if (paymentResult.paymentRequired) {
return new Response(JSON.stringify({ error: 'Payment required' }), {
status: 402,
headers: { 'Content-Type': 'application/json' },
});
}
// ----- 2️⃣ Agent processing -----
let body: any;
try {
body = await request.json();
} catch {
return new Response(JSON.stringify({ error: 'Invalid JSON' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const question
Top comments (0)