How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put an LLM‑driven agent to work earning micropayments on‑chain. No buzzwords, just the parts that actually moved the needle.
1. Why an Autonomous Earner?
When I started experimenting with LLM agents, the most common demo was a chatbot that answered questions. I wanted something that could produce a measurable economic output without constant supervision. The goal was simple: give the agent a task that people are willing to pay for in USDC, let it run 24/7 on a cheap VPS, and collect the revenue automatically.
The constraints I set for myself were:
| Constraint | Reason |
|---|---|
| Self‑contained – no external orchestration framework (e.g., LangChain, Auto‑GPT) | To keep the surface area small and understand every failure mode. |
| Micropayment‑ready – use a protocol that can settle sub‑cent fees | USDC on Base via the x402 standard lets us charge per‑API‑call without gas‑fee overhead. |
| Deterministic safety – the agent must not be able to arbitrage funds or execute unauthorized transactions | Isolation of the payment module and strict scoping of the LLM’s toolset. |
| Observable – logs, metrics, and a simple health‑check endpoint | So I could spot a runaway loop before it drained the wallet. |
2. High‑Level Architecture
+-------------------+ +---------------------+ +-------------------+
| Scheduler (cron) | ---> | Agent Core (Python) | ---> | Payment Adapter |
+-------------------+ +---------------------+ +-------------------+
^ | |
| v v
+-------------------+ +---------------------+ +-------------------+
| Monitoring (Prometheus) | <-> | LLM Provider (local/remote) | <-> | USDC Wallet (Base) |
+-------------------+ +---------------------+ +-------------------+
-
Scheduler – a simple
cronentry (*/5 * * * *) launches the agent every five minutes. If the previous run is still alive, the script exits early (lock‑file pattern). - Agent Core – the decision loop: fetch a task from a queue, ask the LLM to produce a solution, validate the output, and call the payment adapter to invoice the requester.
- Payment Adapter – implements the x402 spec: it returns a 402 response with a macaroon‑style payment request, then, upon receiving a valid USDC payment, unlocks the paid resource.
-
Monitoring – Prometheus scrapes
/metricsexposed by the agent (task latency, success/failure counts, wallet balance). Alerts fire if balance drops below a threshold or if error rate > 5 %.
3. The Payment Flow – x402 on Base
x402 is an HTTP status code extension that lets a server request payment before serving a resource. The flow is:
- Client GET
/service/task?input=… - Agent detects no valid payment token → returns 402 Payment Required with header
X-Payment-Request: <macaroon>. - Client (or a front‑end) uses the macaroon to call the x402 payer contract on Base, transferring the agreed USDC amount.
- Client repeats the request with the payment proof in the
X-Paymentheader. - Agent verifies the proof, executes the paid logic, and returns the result.
Because Base is an L2 with ~ $0.001 transaction cost, charging $0.01–$0.10 per call is economically sensible.
4. Core Code – Agent Loop
Below is the minimal, production‑ready agent I ran on a $5/month VPS (2 vCPU, 1 GB RAM). It uses web3.py for Base interactions, openai for the LLM (you can swap any compatible endpoint), and fastapi for the HTTP service.
# agent.py
import os
import time
import uuid
import hashlib
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from web3 import Web3
from eth_account.messages import encode_defunct
import openai
app = FastAPI()
# ------------------------------------------------------------------
# Configuration (filled from env or .env)
# ------------------------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.chainstack.com")
USDC_ADDRESS = Web3.to_checksum_address(os.getenv("USDC_ADDRESS", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"))
PAYER_CONTRACT = Web3.to_checksum_address(os.getenv("PAYER_CONTRACT", "0x...")) # x402 payer on Base
AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # holds USDC, must be funded
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
PRICE_USDC = int(os.getenv("PRICE_USDC", "10")) # in cents, e.g., 10 = $0.10
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
usdc_abi = [...] # ERC20 minimal ABI (balanceOf, transfer)
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=usdc_abi)
account = w3.eth.account.from_key(AGENT_PRIVATE_KEY)
openai.api_key = OPENAI_API_KEY
# ------------------------------------------------------------------
# Helper: verify x402 payment proof
# ------------------------------------------------------------------
def verify_payment(macaroon: str, payment_header: str) -> bool:
"""
macaroon: value from X-Payment-Request header (opaque string)
payment_header: value from X-Payment header (signature of macaroon+price)
"""
# The payer contract expects a signature over keccak256(macaroon || price)
message = encode_defunct(text=macaoon + str(PRICE_USDC))
try:
recovered = w3.eth.account.recover_message(message, signature=payment_header)
return recovered.lower() == account.address.lower()
except Exception:
return False
# ------------------------------------------------------------------
# Endpoint: expose a paid LLM completion service
# ------------------------------------------------------------------
@app.post("/complete")
async def complete(
request: Request,
x_payment: str | None = Header(None),
x_payment_request: str | None = Header(None),
):
# 1️⃣ If no payment proof, ask for it (402)
if not x_payment:
# generate a fresh macaroon per request to avoid replay
macaroon = hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()
return Response(
status_code=402,
headers={"X-Payment-Request": macaroon},
content="Payment required",
)
# 2️⃣ Verify payment
if not x_payment_request or not verify_payment(x_payment_request, x_payment):
raise HTTPException(status_code=402, detail="Invalid payment")
# 3️⃣ Pull input from body
data = await request.json()
prompt = data.get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="Missing prompt")
# 4️⃣ Call LLM (simple completion)
try:
completion = openai.ChatCompletion.create(
model="gpt-4o-mini", # cheap, good enough for demos
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
max_tokens=256,
)
answer = completion.choices[0].message["content"]
except Exception as e:
# Log and return 502 – payment already consumed, but we refund manually off‑chain
raise HTTPException(status_code=502, detail=str(e))
# 5️⃣ Return result
return JSONResponse({"response": answer})
# ------------------------------------------------------------------
# Health & metrics endpoint (scraped by Prometheus)
# ------------------------------------------------------------------
@app.get("/metrics")
def metrics():
balance = usdc.functions.balanceOf(account.address).call()
return Response(
f"""
agent_balance_usdc {balance / 1e6}
agent_uptime_seconds {time.time() - START_TIME}
""",
media_type="text/plain",
)
# ------------------------------------------------------------------
# Entrypoint for cron launcher
# ------------------------------------------------------------------
if __name__ == "__main__":
import uvicorn
START_TIME = time.time()
uvicorn.run(app, host="0.0.0.0", port=8000)
What the script does:
- Starts a FastAPI server on port 8000.
- On each request to
/complete, it either asks for payment (402) or validates the supplied proof. - After validation, it forwards the prompt to an LLM, returns the generated text, and leaves the USDC in the payer contract (the contract automatically forwards funds to the agent’s address).
- Exposes
/metricsfor scraping.
The cron launcher (run_agent.sh) simply checks for a lock file, launches uvicorn agent.py:app, and tails the log to a file for debugging:
bash
#!/usr/bin/env bash
LOCKFILE=/tmp/agent.lock
if [ -e "$LOCKFILE" ]; then
echo "Agent already running"
exit 1
fi
touch "$LOCKFILE"
uvicorn agent.py:app --host 0.0.0.0 --port 8000 >> /var/log/
Top comments (0)