How I Built an Autonomous AI Agent That Earns USDC While I Sleep
An honest walk‑through of the architecture, trade‑offs, and code that actually runs.
1. Why “earn while you sleep” is a misnomer
When you hear “autonomous AI agent that makes money”, the mental picture is often a black‑box that prints cash with zero oversight. In reality, an autonomous agent is just a deterministic (or mildly stochastic) program that:
- Receives a request – usually via an HTTP endpoint or a message queue.
- Performs a computation – LLM inference, data lookup, or a simple rule‑based transformation.
- Returns a result – and, if the caller has prepaid, releases the result only after payment verification.
The “earning” part comes from charging for each successful invocation in a stablecoin (USDC) on a low‑fee L2 like Base. The agent itself does not create value; it merely exposes a service that others are willing to pay for. Understanding this keeps expectations realistic and helps you focus on the engineering concerns that actually matter: latency, reliability, cost, and security.
2. High‑level architecture
+----------------+ +----------------+ +----------------+
| Client (Web) | ---> | API Gateway | ---> | Agent Worker |
+----------------+ +----------------+ +----------------+
| |
v v
+--------------+ +-----------------+
| Payment Verifier (x402) |<--| USDC Escrow (Base) |
+--------------+ +-----------------+
|
v
+-----------------+
| Result Store |
+-----------------+
- API Gateway – a thin Cloudflare Worker that does routing, rate‑limiting, and forwards the request to the worker pool.
- Agent Worker – another Cloudflare Worker (or a Durable Object) that loads the model, runs inference, and returns the answer.
- Payment Verifier – a tiny contract‑like service that checks the x402 payment header, confirms the USDC transfer on Base, and unlocks the worker.
- Result Store – optional KV or D1 cache for idempotent retries; not required for stateless agents.
All components run on Cloudflare’s edge network, which gives sub‑50 ms latency for most users and eliminates the need to manage VMs.
3. Choosing the model size & inference backend
The biggest cost driver is GPU time. I experimented with three options:
| Option | Approx. latency (95th) | Cost per 1k tokens (USDC) | Trade‑off |
|---|---|---|---|
| tiny‑llama‑1.1B (CPU‑only, ONNX) | 120 ms | $0.0004 | Good for trivial classification; struggles with nuanced prompts. |
| mistral‑7b‑instruct (GPU via Workers AI) | 350 ms | $0.0012 | Balanced quality & cost; still occasional hallucinations. |
| llama‑3‑8b‑instruct (GPU via Workers AI) | 620 ms | $0.0025 | Highest quality; latency may breach SLA for interactive apps. |
I settled on mistral‑7b‑instruct because the agent’s primary use case is short‑form summarization (< 200 tokens). The extra 230 ms latency over the tiny model is acceptable given the quality jump, and the per‑invocation cost stays well under the $0.01‑$0.10 range we target for paid calls.
Honest note: If you need sub‑100 ms latency, you must either shrink the model further or accept a higher error rate. There is no free lunch.
4. Payment flow with x402
x402 is a simple HTTP‑based payment protocol: the client includes an X402-Payment header that contains a signed claim (ECDSA over Secp256k1) referencing a USDC transfer on Base. The verifier checks:
- The claim’s signature matches the payer’s public key.
- The transferred amount meets the price set in the agent’s metadata.
- The nonce hasn’t been reused (prevents replay).
Below is the verifier I deployed as a Cloudflare Worker. It uses the @x402/verify helper (a tiny wrapper around viem for Base RPC calls).
// payment-verifier.ts
import { verifyPayment } from '@x402/verify';
import { env } from 'cloudflare:workers';
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Base USDC
const PRICE_USDC = 0.02; // $0.02 per call
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const paymentHeader = request.headers.get('X402-Payment');
if (!paymentHeader) {
return new Response('Missing X402-Payment header', { status: 402 });
}
try {
const { valid, payer, amount } = await verifyPayment({
payment: paymentHeader,
token: USDC_ADDRESS,
chainId: 8453, // Base
expectedAmount: PRICE_USDC,
});
if (!valid || amount < PRICE_USDC) {
return new Response('Insufficient or invalid payment', { status: 402 });
}
// Attach payer info for downstream logging
request.headers.set('X-Payer', payer);
// Forward to the actual agent worker
return env.AGENT.fetch(request);
} catch (e) {
console.error('Payment verification failed:', e);
return new Response('Payment error', { status: 500 });
}
},
};
Trade‑offs:
- Pros: No on‑chain contract deployment; verification is cheap (~0.0002 USDC) and can be done at the edge.
- Cons: Relies on a public RPC endpoint (I used Alchemy’s Base tier). If the RPC is down, payments fail even though the agent is healthy. Mitigation: keep a fallback RPC or cache recent successful verification nonces for a short window.
5. The agent worker – inference & idempotency
The worker loads the model once (using Cloudflare Workers AI’s built‑in model hub) and reuses the instance across requests. Cold starts add ~200 ms; after that, latency is dominated by GPU compute.
// agent-worker.ts
import { env } from 'cloudflare:workers';
const MODEL = '@hf/mistral/mistral-7b-instruct-v0.2';
// Simple in‑flight dedup cache to avoid double‑charging on retries
const dedupCache = new Map<string, { ts: number; result: string }>();
const DEDUP_TTL_MS = 60_000; // 1 minute
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const uid = request.headers.get('X-Request-ID') || crypto.randomUUID();
const now = Date.now();
// Idempotency check
const cached = dedupCache.get(uid);
if (cached && now - cached.ts < DEDUP_TTL_MS) {
return new Response(cached.result, { headers: { 'X-Cache': 'HIT' } });
}
const { prompt } = await request.json();
if (!prompt || typeof prompt !== 'string') {
return new Response('Invalid payload: expecting {prompt:string}', { status: 400 });
}
try {
const completion = await env.AI.run(MODEL, {
prompt,
max_tokens: 150,
temperature: 0.7,
});
const output = completion.response as string;
dedupCache.set(uid, { ts: now, result: output });
// Optional: prune old entries (simple sweep every 100 reqs)
if (dedupCache.size > 1000) {
for (const [k, v] of dedupCache) {
if (now - v.ts > DEDUP_TTL_MS) dedupCache.delete(k);
}
}
return new Response(output, {
headers: {
'Content-Type': 'text/plain',
'X-Request-ID': uid,
},
});
} catch (err) {
console.error('Inference error:', err);
return new Response('Inference failed', { status: 500 });
}
},
};
Key trade‑offs:
- Model caching: Keeping the model in memory reduces per‑call compute but raises the worker’s memory footprint (~2 GB for Mistral‑7B). Cloudflare Workers AI abstracts this away, but if you self‑host on a VM you’d need to size the instance accordingly.
- Idempotency: The dedup cache protects against retries caused by flaky networks, but it introduces a small stateful component. In a pure‑stateless design you’d rely on the client to send a unique idempotency key and store it in a durable KV store—more reliable but adds latency and cost.
- Safety filtering: I omitted a moderation layer for brevity. In production you’d run a lightweight classifier (e.g., Perspective API) on the output before returning it, which adds ~30 ms and a modest fee.
6. Observability & cost tracking
Because revenue is tied to each successful call, you need granular metrics:
| Metric | Source | Why it matters |
|---|---|---|
| Request count | Cloudflare Workers analytics | Directly maps to revenue. |
| Average latency | Worker timings (event.waitUntil) |
Helps spot model loading bottlenecks or GPU throttling. |
Top comments (0)