How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Introduction
I wanted a minimal, self‑contained agent that could perform a useful task, receive payment in USDC on the Base L2, and run unattended on a cheap VPS. The goal was not to create a “general‑purpose” AI but to demonstrate the mechanics of looping perception, action, and settlement with realistic constraints. This article walks through the architecture, the key code pieces, and the trade‑offs I encountered.
Why USDC on Base?
Base offers low transaction fees (≈ $0.0001 per tx) and fast finality, making micro‑payments feasible. USDC is a widely accepted stablecoin, so the agent can earn value that is easy to convert or reinvest. Using an L2 avoids the noise of Ethereum mainnet gas spikes while still benefiting from EVM compatibility.
Architecture Overview
+----------------+ +----------------+ +----------------+
| Scheduler | ---> | Agent Loop | ---> | Tool Exec |
| (cron/APScheduler) | (perceive → think → act) | (paid via x402) |
+----------------+ +----------------+ +----------------+
^ | |
| v v
+----------------+ +----------------+ +----------------+
| State Store | | LLM Wrapper | | x402 Client |
| (SQLite/Redis) | | (prompt + CFG) | | (sign & verify)|
+----------------+ +----------------+ +----------------+
- Scheduler triggers the agent loop every N minutes (I used 5 min).
- Agent Loop follows the classic ReAct pattern: observe environment, reason with an LLM, decide on a tool, execute, observe result, repeat until a termination condition.
- Tool Exec isolates each capability; each tool is wrapped with an x402 payment middleware that requires a valid payment header before proceeding.
- State Store holds the agent’s memory (short‑term context) and ledger of earned USDC. SQLite is sufficient for a single‑node deployment; swap to Redis if you need horizontal scaling.
-
LLM Wrapper abstracts the model call (I used OpenAI’s gpt‑3.5‑turbo via the
openaiPython package). - x402 Client handles the generation and verification of the payment receipt according to the x402 spec (EIP‑xxxx).
Agent Core – Perceive, Think, Act
Below is a stripped‑down version of the loop. It assumes a simple observation source: a public RSS feed of job postings. The agent’s goal is to summarize new postings and earn USDC for each summary it produces.
# agent_loop.py
import asyncio
import json
import time
from typing import List, Dict
import httpx
import openai
from x402 import verify_payment, PaymentRequired # hypothetical helper
OPENAI_API_KEY = "sk-..."
LLM_MODEL = "gpt-3.5-turbo"
PAYMENT_AMOUNT = 0.02 # USDC per summary
CHAIN_ID = 8453 # Base
client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY)
http = httpx.AsyncClient(timeout=15.0)
async def perceive() -> List[Dict]:
"""Fetch new items from an RSS feed."""
resp = await http.get("https://example.com/jobs.rss")
# parse XML → list of {title, link, pubDate}
return parse_rss(resp.text) # implementation omitted for brevity
async def think(observation: List[Dict]) -> str:
"""Ask the LLM to produce a one‑sentence summary."""
prompt = (
"You are a concise summarizer. Given the following job posting, "
"produce a single sentence that captures the role and key requirement.\n\n"
f"{json.dumps(observation, indent=2)}"
)
resp = await client.chat.completions.create(
model=LLM_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=60,
)
return resp.choices[0].message.content.strip()
async def act(summary: str) -> None:
"""Expose the summary via an x402‑protected HTTP endpoint."""
# In practice the endpoint lives in a separate service; here we simulate a call.
url = "https://my-agent.example.com/summary"
headers = {"Content-Type": "application/json"}
payload = {"summary": summary}
# The x402 client attaches a payment receipt; the server validates it.
try:
resp = await http.post(url, json=payload, headers=headers)
resp.raise_for_status()
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 402:
raise PaymentRequired("Missing or invalid payment")
raise
async def run_cycle():
obs = await perceive()
if not obs:
return # nothing new
summary = await think(obs)
await act(summary)
# record earning (simplified)
await record_earning(PAYMENT_AMOUNT)
async def record_earning(amount: float):
# SQLite INSERT into ledger table (ts, amount, tx_hash)
pass
def main():
while True:
asyncio.run(run_cycle())
time.sleep(300) # 5 min
if __name__ == "__main__":
main()
Key points
- The loop is deliberately synchronous on the outside (
while True:) to keep the process simple; the inner calls areasyncto avoid blocking on network I/O. - Payment validation is delegated to the endpoint (
/summary). The agent only proceeds after receiving a successful HTTP 2xx; a 402 triggers a retry or alert. - The
record_earningfunction is a placeholder; in a real deployment you would sign a transaction on Base and store the hash for auditability.
Payment Integration – x402 Middleware
Below is a minimal FastAPI endpoint that enforces an x402 payment before returning the summary. The code uses the x402-py library (a community implementation of the spec).
# payment_endpoint.py
from fastapi import FastAPI, Request, HTTPException
from x402 import verify_payment, PaymentRequired
from decimal import Decimal
app = FastAPI()
PRICE_USDC = Decimal("0.02") # price per call
USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
CHAIN_ID = 8453
@app.post("/summary")
async def summary_endpoint(request: Request):
# Extract the payment receipt from the x402 header
receipt = request.headers.get("x-x402-payment")
if not receipt:
raise PaymentRequired(
payload={"scheme": "exact", "network": str(CHAIN_ID),
"token": USDC_CONTRACT, "price": str(PRICE_USDC)}
)
try:
verify_payment(
receipt,
payer_address=request.client.host, # simplistic; replace with actual signer
expected_token=USDC_CONTRACT,
expected_amount=PRICE_USDC,
chain_id=CHAIN_ID,
)
except ValueError as e:
raise HTTPException(status_code=402, detail=str(e))
# Business logic – in this example we just echo back the body
data = await request.json()
return {"received": data["summary"], "status": "ok"}
The verifier checks the signature, the chain ID, the token contract, and the exact amount. If any check fails, it returns a 402 with a payload that tells the caller how to pay.
State Persistence & Scheduler
I used APScheduler to drive the loop, but a simple cron entry works just as well. The state store is a SQLite file with two tables:
CREATE TABLE IF NOT EXISTS memory (
id INTEGER PRIMARY KEY,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
payload TEXT
);
CREATE TABLE IF NOT EXISTS ledger (
id INTEGER PRIMARY KEY,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
amount_usdc REAL,
tx_hash TEXT
);
-
memoryholds the last N observations so the agent can avoid re‑summarizing the same item. -
ledgerlogs each successful payment; I periodically query it to compute net earnings.
Honestly, this is enough for a low‑volume agent. If you expect >10 calls/min, consider switching to Redis or a lightweight Postgres instance to avoid SQLite’s write lock bottleneck.
Honest Trade‑offs
| Aspect | What I Chose | Why | Drawback |
|---|---|---|---|
| **LL |
Top comments (0)