How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are already comfortable with async JavaScript/TypeScript, REST APIs, and basic blockchain concepts.
1. Why “earn while I sleep” is a engineering problem, not a magic trick
An autonomous agent that receives micropayments for useful work must solve three intertwined concerns:
- Task generation & execution – the agent must decide what to do, run the work, and produce a verifiable output.
- Micro‑payment settlement – each unit of work must be tied to a cryptographic payment request that the caller can satisfy instantly, without custodial risk.
- Operational safety – the agent runs unattended; failures must not drain funds or expose keys.
The x402 protocol (HTTP 402 Payment Required) gives us a lightweight way to attach a payment request to any HTTP endpoint. By making the agent’s “skill” endpoints return a 402 when unpaid, callers can pay in USDC on Base and receive the result immediately. The agent itself never holds funds; it merely signs the payment request with a wallet that the caller validates.
Below is a walk‑through of the pieces I assembled, the code that makes them work, and the honest trade‑offs I encountered along the way.
2. High‑level architecture
+-------------------+ +---------------------+ +-------------------+
| Scheduler (cron) | ---> | Agent Loop (Worker)| ---> | x402‑Enabled API |
+-------------------+ +---------------------+ +-------------------+
^ | |
| v v
+----------------+ +----------------+ +----------------+
| State Store | | Skills (FSM) | | Wallet/Signer |
+----------------+ +----------------+ +----------------+
- Scheduler – a lightweight Cloudflare Cron Trigger that wakes the agent every 5‑15 minutes to check for pending work (e.g., “scan new GitHub issues”, “summarize RSS feeds”).
- Agent Loop – a Durable Object (or a simple Worker with KV state) that runs a deterministic finite‑state machine: idle → fetch task → execute skill → request payment → return result.
-
x402‑Enabled API – each skill is exposed as an HTTP handler that, if the request lacks a valid
Paymentheader, responds with402 Payment Requiredand a JSON‑encoded payment payload. - State Store – Cloudflare KV (or Durable Object storage) holds the agent’s nonce, last‑run timestamps, and a pointer to the wallet address used for signing.
- Wallet/Signer – an ethers.js signer loaded from a secret stored in Cloudflare Secrets (the private key never touches logs).
The agent never moves USDC; it only signs a payment request that the caller must fulfill. The caller’s payment settles on Base via the x402 relayer (or a custom relayer you run). This keeps the agent’s attack surface limited to signing, not custody.
3. Core code snippets
3.1 Agent loop (Durable Object)
import { DurableObject } from "cloudflare:workers";
import { ethers } from "ethers";
import { x402PaymentRequest } from "./x402";
interface Env {
// Secrets set via wrangler
PRIVATE_KEY: string;
WALLET_ADDRESS: string;
// KV binding for lightweight persistence
AGENT_STATE: KVNamespace;
}
export class AgentDO extends DurableObject<Env> {
private signer: ethers.Wallet;
private state: Record<string, any>;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.signer = new ethers.Wallet(env.PRIVATE_KEY);
this.state = {}; // will be hydrated from KV
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/tick") {
return this.handleTick();
}
// Fallback to skill endpoints (see §3.2)
return new Response("Not found", { status: 404 });
}
/** Called by the Cron Trigger every N minutes */
private async handleTick(): Promise<Response> {
// 1️⃣ Load persisted state (nonce, last run, etc.)
await this.loadState();
// 2️⃣ Choose a task – here we just poll a public RSS feed
const task = await this.fetchNextTask();
if (!task) {
return new Response("No work available", { status: 204 });
}
// 3️⃣ Execute the skill (example: summarize article)
const result = await this.executeSkill("summarize", task.payload);
// 4️⃣ Build a payment request for the result
const payment = await x402PaymentRequest(
this.signer,
this.env.WALLET_ADDRESS,
// price in USDC (6 decimals on Base)
ethers.parseUnits("0.05", 6), // $0.05 per summary
// a unique identifier to prevent replay
ethers.keccak256(ethers.toUtf8Bytes(`${this.state.nonce}:summarize`)),
// optional metadata the buyer can inspect
{ taskId: task.id, resultHash: ethers.keccak256(ethers.toUtf8Bytes(result)) }
);
// 5️⃣ Increment nonce and persist
this.state.nonce = (this.state.nonce ?? 0) + 1;
await this.saveState();
// 6️⃣ Return the raw result; the caller must attach the payment header
// (the x402 middleware will enforce it)
return new Response(result, {
headers: { "Content-Type": "text/plain" },
});
}
// -----------------------------------------------------------------
// Boiler‑plate KV helpers – replace with Durable Object storage if
// you need stronger consistency.
// -----------------------------------------------------------------
private async loadState() {
const raw = await this.env.AGENT_STATE.get("state") ?? "{}";
this.state = JSON.parse(raw);
}
private async saveState() {
await this.env.AGENT_STATE.put("state", JSON.stringify(this.state));
}
// -----------------------------------------------------------------
// Stub implementations – plug in your own sources / models.
// -----------------------------------------------------------------
private async fetchNextTask() {
// Example: pull latest item from a public RSS feed
const feed = await fetch("https://example.com/rss.xml").then(r => r.text());
// …parse and return {id, payload}
return { id: "1", payload: "Sample article text" };
}
private async executeSkill(skill: string, payload: string): Promise<string> {
if (skill === "summarize") {
// Very naive summarizer – replace with an LLM call or ML model.
return payload.split(". ").slice(0, 2).join(". ") + ".";
}
throw new Error(`Unknown skill: ${skill}`);
}
}
What this does
- The Cron Trigger (
/tick) drives the agent’s autonomy. - Each cycle fetches a unit of work, runs a deterministic skill, then builds an x402 payment request that encodes price, nonce, and a hash of the result.
- The agent returns the raw result only after the caller supplies a valid
Paymentheader; otherwise the x402 middleware (see below) will reply with402.
3.2 x402 middleware (Worker)
import { verifyPayment } from "x402-verifier"; // tiny helper you can copy from the spec
export async function onRequest(context) {
const { request, env, next } = context;
// Skip verification for the internal /tick endpoint
if (request.url.endsWith("/tick")) {
return await next();
}
const auth = request.headers.get("Payment");
if (!auth) {
// Build a 402 response that includes the payment payload the agent expects
const paymentReq = await x402PaymentRequest(
env.SIGNER, // same signer used by the agent
env.WALLET_ADDRESS,
ethers.parseUnits("0.05", 6),
ethers.keccak256(ethers.toUtf8Bytes("placeholder-nonce:summarize"))
);
return new Response(JSON.stringify({ error: "payment required", payment: paymentReq }), {
status: 402,
headers: { "Content-Type": "application/json" },
});
}
// Verify the payment matches what the agent would have asked for
const valid = await verifyPayment(auth, env.WALLET_ADDRESS, /* expected amount */);
if (!valid) {
return new Response("Invalid payment", { status: 402 });
}
// Payment good – let the request hit the skill handler
return await next();
}
The verifier checks the signature, the nonce, and that the amount corresponds to the skill’s price.
3.3 Wrangler configuration (excerpt)
toml
name = "nexusai-x402"
main = "src/index.ts"
compatibility_date = "2024-09-01"
[vars]
ENVIRONMENT = "production"
[[kv_namespaces]]
binding = "AGENT_STATE"
id = "your_kv_namespace_id"
[[triggers]]
crons = ["*/10 * * * *
Top comments (0)