How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are designing or already running autonomous agents and want to understand a pragmatic, production‑oriented approach to monetising those agents with crypto‑native micro‑payments.
1. Why bother with on‑chain payments?
When an agent can be called by anyone, the simplest way to recoup compute cost is to charge per invocation. Traditional APIs rely on API keys, rate‑limits, and invoicing pipelines that add operational overhead. The x402 protocol lets you attach a payment‑required HTTP header to any endpoint; the caller must settle a tiny amount of USDC before the request is processed. On Base (an Optimistic Rollup backed by Ethereum) a single USDC transfer costs a few fractions of a cent, making it feasible to price calls at $0.01‑$0.10 without worrying about chargebacks or fraud.
The trade‑off is added latency (the payment must confirm) and the need to handle wallet keys securely. If your agent’s workload is latency‑sensitive (sub‑second response), you may prefer off‑chain billing; otherwise, x402 gives you a trustless, programmable revenue stream.
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Caller (dApp) | ---> | x402 Gateway | ---> | Agent Worker |
| (wallet + USDC) | | (CF Worker) | | (Python + LLM) |
+-------------------+ +-------------------+ +-------------------+
^ | |
| v v
| +-------------------+ +-------------------+
| | Payment Verifier | | Tool Orchestrator|
| +-------------------+ +-------------------+
| | |
+-------------------------+-------------------------+
|
+-------------------+
| Result Cache (KV)|
+-------------------+
-
x402 Gateway – a thin Cloudflare Workers script that reads the
Paymentheader, validates the USDC transfer on Base, and either returns402 Payment Requiredor forwards the request to the worker. -
Agent Worker – a long‑running serverless function (Python) that loads a lightweight LLM (e.g., Llama‑3‑8B quantised via
llama.cpp), decides which tool to invoke, runs the tool, and returns the result. - Tool Orchestrator – a plug‑in system that wraps deterministic APIs (price feeds, web scrapers, calculators) as callable functions.
- Result Cache – optional KV store to avoid re‑computing identical queries; cache TTL is set to a few minutes to keep data fresh.
3. Payment verification in the gateway
The gateway needs to confirm that a USDC transfer of at least the required amount arrived from the caller’s address to the agent’s treasury address. On Base we can query the USDC contract’s Transfer events via an RPC endpoint (e.g., Alchemy or a public Base node).
// x402-gateway.js (Cloudflare Workers)
import { ethers } from "https://esm.sh/ethers@6";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const TREASURY = "0xYourAgentTreasuryAddress";
const RPC_URL = "https://base.mainnet.rpc.async.ag"; // replace with your node
export default {
async fetch(request, env, ctx) {
// 1️⃣ Read payment header: "Payment: <amount>USDC:<tokenAddr>"
const payment = request.headers.get("Payment");
if (!payment) {
return new Response("402 Payment Required", { status: 402, headers: { "Payment": `Price: 0.01USDC:${USDC_ADDRESS}` } });
}
const [amountStr, , tokenAddr] = payment.split(":");
const amount = ethers.parseUnits(amountStr, 6); // USDC has 6 decimals
if (tokenAddr.toLowerCase() !== USDC_ADDRESS.toLowerCase() || amount < ethers.parseUnits("0.01", 6)) {
return new Response("Insufficient payment", { status: 402 });
}
// 2️⃣ Verify that the sender actually sent this amount
const sender = request.headers.get("x402-payer"); // injected by the x402 client library
if (!sender) return new Response("Missing payer header", { status: 400 });
const provider = new ethers.JsonRpcProvider(RPC_URL);
const usdc = new ethers.Contract(USDC_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], provider);
// Look at the latest block (could be optimized with a filter)
const latest = await provider.getBlockNumber();
const fromBlock = Math.max(0, latest - 200); // check last ~200 blocks (~1 minute on Base)
const filter = usdc.filters.Transfer(null, TREASURY);
const logs = await usdc.queryFilter(filter, fromBlock, latest);
const paid = logs.some(l => {
return l.args.from.toLowerCase() === sender.toLowerCase() &&
l.args.value >= amount;
});
if (!paid) return new Response("Payment not verified", { status: 402 });
// 3️⃣ Forward to the actual agent worker (same service, different path)
return await env.AGENT.fetch(request);
}
};
Honest notes
- Scanning the last 200 blocks adds ~200 ms latency on Base; you can reduce it by running a dedicated indexer or using The Graph, but that adds operational complexity.
- The gateway stores no private keys; only the treasury address is needed to verify inbound transfers, which limits attack surface.
- If the payer uses a contract wallet that forwards USDC via a fallback, the simple
Transfercheck may fail; you’d need to support ERC‑20approve/transferFrompatterns.
4. The agent worker – core loop
The worker is a lightweight Python service that can be deployed as a Cloudflare Worker (via workers-python) or an AWS Lambda. Below is a minimal, functional version that uses a quantised Llama‑3 model hosted on Hugging Face’s Inference API (you could swap this for a local llama.cpp binary if you prefer GPU‑free execution).
python
# agent_worker.py
import json, os, time
from typing import Dict, Any
from openai import OpenAI # compatible with Hugging Face TGI endpoints
# ---- Configuration -------------------------------------------------
HF_API_URL = os.getenv("HF_API_URL", "https://api-inference.huggingface.co/models/meta-llama/Llama-3-8b-instruct")
HF_TOKEN = os.getenv("HF_TOKEN") # private, never exposed to clients
SYSTEM_PROMPT = ("You are a helpful agent that can answer questions, "
"summarise text, and call external tools when needed.")
# -------------------------------------------------------------------
client = OpenAI(base_url=HF_API_URL, api_key=HF_TOKEN)
# Example tool: a simple price fetcher for ETH/USD via CoinGecko
def get_eth_price() -> float:
import requests
r = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", timeout=5)
r.raise_for_status()
return r.json()["ethereum"]["usd"]
TOOLS = {
"get_eth_price": get_eth_price,
# add more tools: web_search, calculator, etc.
}
def call_tool(name: str, args: Dict[str, Any]) -> Any:
if name not in TOOLS:
raise ValueError(f"Unknown tool {name}")
return TOOLS[name](**args)
def agent_loop(user_input: str) -> str:
# 1️⃣ Ask the LLM to decide whether to use a tool
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input}
]
resp = client.chat.completions.create(
model="meta-llama/Llama-3-8b-instruct",
messages=messages,
temperature=0.2,
max_tokens=256,
tools=[{"type": "function", "function": {"name": "get_eth_price",
"description": "Return current ETH/USD price",
"parameters": {"type": "object", "properties": {}}}}],
tool_choice="auto"
)
msg = resp.choices[0].message
# 2️⃣ If the model wants a tool, execute it and feed the result back
if msg.tool_calls:
for call in msg.tool_calls:
if call.function.name == "get_eth_price":
result = call_tool("get_eth_price", {})
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
# second pass to produce final answer
final = client.chat
Top comments (0)