How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with long‑running LLM‑driven services and want to see a concrete, production‑ish skeleton.
1. Why an “earning” agent?
The idea is simple: expose a set of deterministic, stateless functions (e.g., token‑level text summarisation, simple data enrichment, or a tiny classification model) behind a pay‑per‑call API that settles in USDC on the Base layer‑2. If the functions are useful enough that external callers are willing to pay a few cents, the agent can run continuously without a traditional SaaS billing backend.
The trade‑off is that you must handle:
- Wallet custody – the agent needs a private key to sign USDC transfers.
- Price discovery – you must decide a static price or implement a simple oracle.
- Reliability – the agent must stay up, retry failed payments, and survive restarts.
- Cost vs. revenue – compute (CPU, memory, network) must stay below the per‑call price, otherwise you lose money.
Below is a minimal, working implementation that satisfies those constraints while staying easy to audit.
2. High‑level architecture
+----------------+ +----------------+ +----------------+
| Invoker (HTTP) | --> | Cloudflare | --> | Agent Worker |
| (curl, postman) | | Workers (edge) | | (Node.js) |
+----------------+ +----------------+ +----------------+
^ ^ ^
| | |
USDC payment (x402) Verifies signature Executes LLM task
| | |
v v v
+----------------+ +----------------+ +----------------+
| Wallet (USDC) | <-- | x402 Verifier | <-- | Task Queue |
+----------------+ +----------------+ +----------------+
- Cloudflare Workers act as a cheap, globally distributed entry point that enforces the x402 payment header before forwarding the request to the actual logic.
- The Agent Worker (a long‑running Node.js process) receives the validated request, runs the LLM inference, and returns the result.
- A task queue (here we use a simple in‑memory BullMQ backed by Redis) decouples payment verification from heavy compute, allowing retries if the model loads slowly.
- The wallet holds a small USDC balance on Base; the agent never moves funds out of it—it only receives inbound payments.
3. Prerequisites
- Node.js ≥ 20
- A wallet with a small USDC balance on Base (you can fund via a faucet or a bridge).
- Redis instance (local Docker or managed).
- Access to an LLM inference endpoint (we’ll use a local Hugging Face Transformers model for demo; replace with your own API).
Install the core dependencies:
npm i @cloudflare/workers-types wrangler bullmq ioredis ethers dotenv
npm i -D typescript @types/node
4. Wallet & USDC handling
We keep the private key in an environment variable (AGENT_PRIVATE_KEY). Never commit it.
// src/wallet.ts
import { ethers } from "ethers";
export const getWallet = () => {
const pk = process.env.AGENT_PRIVATE_KEY;
if (!pk) throw new Error("AGENT_PRIVATE_KEY not set");
return new ethers.Wallet(pk);
};
// USDC contract on Base (address: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
export const USDC_ABI = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
];
The agent never initiates a transfer; it only reads its balance to display stats.
5. x402 payment verification in Cloudflare Workers
The worker checks for the X402-Payment header, validates the signature against the known USDC contract, and forwards the request if the amount meets the price we set.
// src/x402-verifier.ts
import { ethers } from "ethers";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const PRICE_USDC = ethers.parseUnits("0.05", 6); // $0.05 per call
export async function verifyPayment(request: Request): Promise<boolean> {
const auth = request.headers.get("X402-Payment");
if (!auth) return false;
// Expected format: "usdc:<amount>:<signature>"
const [, amountStr, signature] = auth.split(":");
if (!amountStr || !signature) return false;
const amount = ethers.parseUnits(amountStr, 6);
if (amount < PRICE_USDC) return false;
// Recover signer from signature over the request body
const body = await request.clone().text();
const messageHash = ethers.hashMessage(body);
const recovered = ethers.recoverAddress(messageHash, signature);
// The signer must match the agent's wallet address
const walletAddress = (await getWallet()).address;
return ethers.getAddress(recovered) === ethers.getAddress(walletAddress);
}
Attach this to your Worker:
// src/index.ts
import { verifyPayment } from "./x402-verifier";
export default {
async fetch(request, env, ctx): Promise<Response> {
if (!(await verifyPayment(request))) {
return new Response("Payment required or invalid", { status 402 });
}
// Forward to the agent service (could be another Worker or external URL)
return fetch("https://agent-service.example.com/run", request);
},
};
Trade‑off: The verification adds ~2‑3 ms latency (mostly signature recovery). If you need sub‑millisecond response, you could move verification to a dedicated edge KV store that caches recent signatures, but that introduces a small replay‑attack surface you must mitigate with nonces.
6. Agent logic (LLM task)
For illustration we run a small‑parameter summarisation model (sshleifer/distilbart-cnn-12-6) via @xenova/transformers. In production you would swap this for a GPU‑accelerated endpoint (e.g., Replicate, Together.ai) and keep the worker thin.
// src/agent.ts
import { pipeline } from "@xenova/transformers";
import { Queue, Worker } from "bullmq";
import { IORedis } from "ioredis";
const redis = new IORedis(process.env.REDIS_URL ?? "redis://127.0.0.1:6379");
const taskQueue = new Queue("llm-tasks", { connection: });
// Load model once (cold start ~1‑2 s on a modest CPU)
let summarizer = null;
async function getSummarizer() {
if (!summarizer) {
summarizer = await pipeline("summarization", "Xenova/distilbart-cnn-12-6");
}
return summarizer;
}
// Worker processes queued jobs
new Worker(
"llm-tasks",
async (job) => {
const { text } = job.data;
const model = await getSummarizer();
const result = await model(text, {
max_length: 130,
min_length: 30,
do_sample: false,
});
return result[0].summary_text;
},
{ connection: redis }
);
// HTTP endpoint that enqueues a job and waits for the result
export async function handleRun(request: Request): Promise<Response> {
const { text } = await request.json();
if (!typeof text === "string" || text.length === 0) {
return new Response("Missing 'text' field", { status: 400 });
}
const job = await taskQueue.add("summarize", { text }, { attempts: 3 });
const result = await job.waitUntilFinished(); // resolves when worker finishes
return new Response(JSON.stringify({ summary: result.returnvalue }), {
headers: { "Content-Type": "application/json" },
});
}
Trade‑offs:
- Cold start: Loading the model takes ~1‑2 s on a CPU‑only instance. If you need sub‑second latency, pre‑warm the instance (keep a minimal ping) or move inference to a GPU‑enabled service and keep the worker as a thin proxy.
- Cost: A small CPU instance (e.g., Cloudflare Workers Unbound or a cheap VPS) runs at ~$0.005/hr. At $0.05 per call you need ~10 calls/hr to break even; actual usage will vary.
- Reliability: BullMQ retries failed jobs; the worker can be restarted without losing in‑flight tasks because they stay in Redis.
7. Deployment checklist
| Step | What to do | Why |
|---|---|---|
| 1️⃣ | Store AGENT_PRIVATE_KEY and REDIS_URL in a secret manager (e.g., Cloudflare Workers Secrets, Docker env, or Vault). |
Prevent key leakage. |
| 2️⃣ | Deploy the x402 verifier Worker (wrangler publish). |
Edge entry point, cheap and globally distributed. |
| 3️⃣ | Spin up a small VM/Container (e.g |
Top comments (0)