How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put a language‑model‑driven agent to work for micropayments on‑chain.
1. Why bother with an “earning” agent?
Autonomous agents are useful when they can perform a repeatable, well‑scoped task faster or cheaper than a human. Adding a micropayment layer turns the agent into a service that can settle its own costs (e.g., API calls, compute) and collect revenue directly from users. The goal isn’t to get rich overnight; it’s to prove that an agent can be self‑sustaining while you’re offline.
2. High‑level architecture
+----------------+ x402 payment header +----------------+
| Client (web) | ------------------------------> | Agent API |
+----------------+ +----------------+
^ |
| v
| +----------------+
| | LLM + Tools |
| +----------------+
| |
| v
| +----------------+
+------------------------------------------| Result Store |
+----------------+
- Client – any HTTP caller (browser, curl, another agent).
- Agent API – a thin gateway that validates the x402 payment header, enqueues a job, and returns a job‑ID.
- LLM + Tools – the core reasoning loop (LangChain‑style) that can call external APIs, run scripts, or query a DB.
- Result Store – a durable key‑value store (e.g., Cloudflare KV or Redis) where the agent writes the output so the client can poll for it.
All components run on the same compute platform (Cloudflare Workers) to keep latency low and avoid cross‑region data transfer fees.
3. Micropayments with x402
The x402 spec lets you attach payment metadata to an HTTP 402 Payment Required response. The client then resends the request with a signed payment proof (USDC on Base) in the Pay header.
Verification flow (pseudo‑code):
# worker.js – simplified
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const path = url.pathname;
// 1️⃣ Reject non‑POST or missing payload early
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
// 2️⃣ Check for x402 payment header
const payHeader = request.headers.get("Pay");
if (!payHeader) {
// Ask the client to pay
return new Response(null, {
status: 402,
headers: {
"WWW-Authenticate": `Bearer token="${env.X402_PUBLIC_KEY}"`,
"Accept": "usdc",
"Price": "0.05", // USDC per call
"Network": "base",
"Currency": "usdc",
},
});
}
// 3️⃣ Verify the signed payment (using Circle's USDC SDK or viem)
const isValid = await verifyUsdcPayment(payHeader, {
amount: "0.05",
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02FF9", // USDC on Base
network: "base",
receiver: env.WALLET_ADDRESS,
});
if (!isValid) {
return new Response("Invalid payment", { status: 402 });
}
// 4️⃣ Payment ok → enqueue job
const jobId = await env.JOB_QUEUE.put(JSON.stringify({
payload: await request.json(),
caller: request.headers.get("CF-Connecting-IP"),
}));
return new Response(JSON.stringify({ jobId }), { status: 202 });
}
Trade‑off: Adding the payment round‑trip adds ~150‑200 ms latency (client must receive 402, sign, retry). For low‑value, high‑volume calls this is acceptable; for latency‑sensitive UI interactions you’d batch or pre‑fund a wallet.
4. Agent core – LLM + tool use
I chose LangChain with the OpenAI chat model (gpt‑4o‑mini) because it gives a clean AgentExecutor loop. The agent’s toolset is deliberately narrow:
- WebFetch – retrieves a URL and returns raw text.
- CSVQuery – runs a simple SQL‑like query on an uploaded CSV (useful for data‑clean‑up tasks).
- Erc20Balance – reads the agent’s own USDC balance on Base via an RPC call.
# agent.py
from langchain.agents import initialize_agent, Tool
from langchain.chat_models import ChatOpenAI
from langchain.utilities import WikipediaAPIWrapper
llm = ChatOpenAI(temperature=0, model_name="gpt-4o-mini")
tools = [
Tool(
name="WebFetch",
func=lambda url: requests.get(url, timeout=10).text,
description="Fetch a web page and return its HTML/text.",
),
Tool(
name="CSVQuery",
func=lambda q: pandasql.sqldf(q, {"df": uploaded_df}),
description="Run a SELECT query on the CSV stored in memory.",
),
Tool(
name="Erc20Balance",
func=lambda _: web3.eth.call({
"to": USDC_ADDRESS,
"data": encode_function_data("balanceOf(address)", [AGENT_WALLET]),
}),
description="Return the agent's USDC balance on Base.",
),
]
agent_executor = initialize_agent(
tools, llm, agent="zero-shot-react-description", verbose=False
)
def run_task(payload):
# payload is a dict like {"action":"fetch","url":"https://example.com"}
return agent_executor.run(payload)
Honest trade‑off: The agent is stateless per invocation; it relies on the external tool implementations for any persistence. If a tool fails (e.g., RPC timeout), the whole call fails and the client must retry. In production you’d wrap each tool with a retry‑and‑circuit‑breaker pattern.
5. Job queue & scheduling
Because the worker must return quickly (202 Accepted) after payment verification, I offload the actual LLM work to a durable queue. Cloudflare Workers provides a Queue binding that guarantees at‑least‑once delivery.
// worker.js (continued)
export async function scheduled() {
// This runs every minute via a Cron Trigger
while (true) {
const msg = await env.JOB_QUEUE.get();
if (!msg) break; // empty queue
const { payload, caller } = JSON.parse(msg.body);
try {
const result = await runTask(payload); // calls the Python agent via WASM or a sub‑worker
await env.RESULT_STORE.put(msg.id, JSON.stringify({ result, caller }), {
expirationTtl: 3600, // keep for 1 h
});
} catch (e) {
console.error("Job failed", e);
// Optionally move to a dead‑letter queue for inspection
}
// Acknowledge successful processing
await env.JOB_QUEUE.delete(msg);
}
}
Trade‑off: The queue adds another hop (worker → queue → worker) and introduces a processing delay of up to the queue poll interval (here, 1 min). For sub‑second latency you could invoke the agent synchronously, but you’d then hold the HTTP connection open while the LLM runs, increasing the chance of timeouts and making scaling harder.
6. Security, idempotency & error handling
-
Idempotency – each request includes a UUID (
Idempotency-Keyheader). The worker stores the UUID with the job result; duplicate keys return the cached outcome instead of re‑enqueuing. - Replay protection – the x402 library includes a nonce; the verifier rejects any payment with a previously seen nonce.
- Funds safety – the agent’s wallet is a restricted account that can only receive USDC; outgoing transfers require a multisig approval (I use a 2‑of‑3 Gnosis Safe where two signatures are held by me and a third by a hardware key).
- Logging – all inbound requests, payment verification outcomes, and job results are written to Cloudflare Logpush (sent to a Loki instance) for audit.
Top comments (0)