How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers interested in creating self‑sustaining AI services that receive micropayments in USDC.
Overview
The goal was to run an always‑on agent that performs a narrow, well‑defined task (text summarization) and gets paid per invocation in USDC on the Base blockchain. The agent does not “learn” while it sleeps; it simply waits for requests, validates a payment, runs a model, and returns the result. The architecture is deliberately minimal to keep operational complexity low and to make trade‑offs explicit.
Core components
| Component | Responsibility | Chosen tech | Why |
|---|---|---|---|
| Request handling & payment verification | Accept HTTP POST, check x402 payment header, reject if insufficient | Cloudflare Workers (JavaScript) | Serverless, global edge, free tier sufficient for low traffic |
| AI inference | Run a summarization model on the input text | HuggingFace transformers.js (distilbart‑cnn‑12‑6) |
Runs entirely in the Worker sandbox, no external API keys, modest size (~50 MB) |
| Payment settlement | Record earned USDC for later withdrawal | Simple server‑side counter (KV store) + manual claim via a wallet | Avoids integrating a full smart contract; x402 already guarantees payment off‑chain |
| Observability | Log requests, errors, and earnings | Built‑in Worker logs + optional external logging service | Minimal overhead, sufficient for debugging |
The flow for each request is:
- Client sends
POST /summarizewith JSON{ "text": "…" }and anX-Paymentheader containing a signed x402 proof. - Worker verifies the proof against the agent’s public key and the configured price (e.g., $0.02 USDC).
- If verification passes, the Worker runs the model, returns the summary, and increments an earnings counter.
- If verification fails, the Worker returns
402 Payment Requiredwith a helpful error message.
Payment verification with x402
x402 defines a HTTP‑based scheme for attaching a cryptographic proof of payment to a request. The proof consists of:
-
payload: the request method, path, and body hash. -
signature: an ECDSA signature over the payload using the payer’s private key. -
paywall: the price in USDC (encoded as a 64‑bit integer with 6 decimals).
The worker needs the agent’s public key to verify the signature. Below is a stripped‑down verification function using the ethers library (available via CDN in Workers).
// x402Verify.js
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.7.0/dist/ethers.min.js";
const AGENT_PUBLIC_KEY = "0xA1b2C3d4E5f67890..."; // replace with your agent's address
export async function verifyX402(request, expectedPriceMicroUSDC) {
const payloadHeader = request.headers.get("X-Payload");
const sigHeader = request.headers.get("X-Signature");
const paywallHeader = request.headers.get("X-Paywall");
if (!payloadHeader || !sigHeader || !paywallHeader) {
throw new Error("Missing x402 headers");
}
// Reconstruct the payload that was signed
const payload = JSON.parse(atob(payloadHeader));
// Expected shape: { method, path, bodyHash }
const { method, path, bodyHash } = payload;
// Verify paywall amount (USDC with 6 decimals)
const price = BigInt(paywallHeader);
if (price < BigInt(expectedPriceMicroUSDC)) {
throw new Error(`Insufficient payment: ${Number(price)/1e6} USDC`);
}
// Re‑create the signed message
const message = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["string", "string", "string"],
[method, path, bodyHash]
)
);
// Recover signer address from signature
const signer = ethers.utils.recoverAddress(message, sigHeader);
return signer.toLowerCase() === AGENT_PUBLIC_KEY.toLowerCase();
}
Trade‑off: Verifying signatures in a Worker adds ~2‑3 ms of CPU time. For low‑volume services this is negligible; for high‑throughput endpoints you might move verification to a dedicated validator or batch multiple requests.
Model inference inside the Worker
Running a transformer model directly in a Worker is possible with transformers.js, which compiles the model to WebAssembly. The model we chose (Xenova/distilbart-cnn-12-6) is ~50 MB and produces acceptable summaries for short paragraphs (< 500 tokens).
// summarizer.js
import { pipeline } from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.8.0/dist/index.min.js";
let summarizer = null;
async function getSummarizer() {
if (!summarizer) {
summarizer = await pipeline("summarization", "Xenova/distilbart-cnn-12-6");
}
return summarizer;
}
export async function summarize(text) {
const pipe = await getSummarizer();
// Model expects <1024 tokens; truncate if needed
const maxLength = 130; // summary length
const minLength = 30;
const result = await pipe(text, { max_length: maxLength, min_length: minLength });
return result[0].summary_text;
}
Trade‑off: The initial load of the model incurs a cold‑start penalty (~400 ms on the first request). Subsequent requests reuse the cached instance, dropping latency to ~120 ms for a 200‑token input. If sub‑100 ms latency is required, you would need to pre‑warm the Worker or move inference to a GPU‑enabled service (e.g., RunPod) – at the cost of higher operational complexity and expense.
Putting it together in a Cloudflare Worker
// index.js
import { verifyX402 } from "./x402Verify.js";
import { summarize } from "./summarizer.js";
// Price per call: 0.02 USDC = 20,000 micro‑USDC
const PRICE_MICRO_USDC = 20000;
// Simple KV namespace bound as "EARNINGS"
export default {
async fetch(request, env, ctx) {
if (request.method !== "POST" || new URL(request.url).pathname !== "/summarize") {
return new Response("Not Found", { status: 404 });
}
// 1️⃣ Verify payment
try {
await verifyX402(request, PRICE_MICRO_USDC);
} catch (e) {
return new Response(e.message, { status: 402, headers: { "Content-Type": "text/plain" } });
}
// 2️⃣ Parse body
let body;
try {
const json = await request.json();
if (!json.text || typeof json.text !== "string") throw new Error();
body = json.text;
} catch (_) {
return new Response('Invalid JSON: expected { "text": "string" }', { status: 400 });
}
// 3️⃣ Run summarization
let summary;
try {
summary = await summarize(body);
} catch (e) {
console.error("Summarization failed:", e);
return new Response("Internal error", { status: 500 });
}
// 4️⃣ Record earnings (optional)
ctx.waitUntil(
env.EARNINGS.add("total", 1).then(() => env.EARNINGS.add("usdc", PRICE_MICRO_USDC))
);
// 5️⃣ Return result
return new Response(JSON.stringify({ summary }), {
headers: { "Content-Type": "application/json" },
});
},
};
Explanation of the snippet
- The Worker checks the HTTP method and path early to avoid unnecessary work.
- Payment verification is performed before any model inference, guaranteeing that we never spend compute on unpaid requests.
- Earnings are aggregated in a KV store (
EARNINGS) usingctx.waitUntilso the response isn’t delayed by the write. - All heavy lifting (model load, inference) stays inside the Worker; no external API keys are required, reducing attack surface.
Operational considerations & honest trade‑offs
| Aspect | What we chose | Pros | Cons / Limitations |
|---|---|---|---|
| Compute | Serverless Worker + WASM model | No server management, automatic scaling, free tier covers low traffic | Cold start latency, limited CPU (no GPU), model size constrained by Worker memory (~128 MB) |
| Payment | x402 off |
Top comments (0)