How I Built an Autonomous AI Agent That Earns USDC While I Sleep
A pragmatic walk‑through for developers who want to turn a simple LLM‑powered service into a self‑sustaining micro‑business on Base.
1. The Idea in One Sentence
Run a stateless HTTP endpoint that performs a useful AI task (e.g., short‑form text summarization) and charge callers a tiny USDC fee via the x402 protocol; the agent simply watches its balance grow.
2. Why x402?
- Micropayments native to HTTP – no separate invoicing or off‑chain settlement.
- USDC on Base – low‑cost, fast finality (~2 s) and negligible gas (< 0.0001 ETH).
- Statelessness – each request carries its own proof of payment, making horizontal scaling trivial.
The trade‑off is that you must handle payment verification on every request and design your service to be idempotent; a failed payment means no compute is wasted, but you also lose the chance to retry a failed call without recharging the user.
3. High‑Level Architecture
+----------------+ x402 proof +-------------------+
| Caller (any) | -----------------> | Agent Service |
+----------------+ (USDC on Base) +-------------------+
^ |
| v
| +-------------------+
| | Earnings Tracker |
| +-------------------+
| |
+--------------------------------------+
(periodic balance read)
- Agent Service – a FastAPI app that validates the x402 header, runs the AI model, and returns the result.
- Earnings Tracker – a lightweight script that polls the USDC contract (or uses a blockchain indexer like The Graph) to record incoming payments.
- Scheduler – not required for the service itself, but useful if you want to run background tasks (e.g., model warm‑up, cache priming).
4. Service Implementation
Below is a minimal, production‑ready example. Feel free to swap the summarization model for anything you prefer (local HuggingFace, OpenAI API, etc.).
4.1 Dependencies
# pyproject.toml
[project]
name = "usdc-summarizer"
version = "0.1.0"
dependencies = [
"fastapi==0.110.0",
"uvicorn[standard]==0.29.0",
"pydantic==2.7.1",
"web3==7.2.0",
"eth-account==0.10.0",
"transformers==4.41.2",
"torch==2.3.0",
]
4.2 Core Code
python
# main.py
import os
import json
from fastapi import FastAPI, Request, HTTPException, Header
from pydantic import BaseModel
from web3 import Web3
from eth_account.messages import encode_defunct
from transformers import pipeline
# ----------------------------------------------------------------------
# Configuration (keep these in env vars or a secrets manager)
# ----------------------------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://base.meowrpc.com")
USDC_ADDRESS = Web3.to_checksum_address(
os.getenv("USDC_ADDRESS", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") # USDC on Base
)
AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # <-- NEVER commit this
PRICE_IN_USDC = int(os.getenv("PRICE_USDC", "1")) # 1 USDC = 1e6 wei (6 decimals)
# ----------------------------------------------------------------------
# Web3 setup
# ----------------------------------------------------------------------
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
usdc_abi = json.loads("""[
{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},
{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"}
]""")
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=usdc_abi)
# ----------------------------------------------------------------------
# AI model (load once at startup)
# ----------------------------------------------------------------------
summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6", device=0 if w3.is_connected() else -1)
app = FastAPI(title="USDC‑Paid Summarizer")
class SummaryRequest(BaseModel):
text: str
max_length: int = 130
min_length: int = 30
def verify_x402(payload: dict, signature: str, address: str) -> bool:
"""
Very small subset of the x402 spec:
- payload: JSON‑serializable dict that the client signed.
- signature: hex string (0x prefixed) of an Ethereum signed message.
- address: Ethereum address that should have signed the payload.
Returns True if the signature is valid and the nonce/replay protection passes.
"""
# Re‑create the message that was signed
message = json.dumps(payload, separators=(",", ":"), sort_keys=True)
eth_message = encode_defunct(text=message)
recovered = w3.eth.account.recover_message(eth_message, signature=signature)
return recovered.lower() == address.lower()
@app.post("/summarize")
async def summarize(
request: Request,
body: SummaryRequest,
x402_payload: str = Header(None),
x402_signature: str = Header(None),
x402_address: str = Header(None),
):
"""
Endpoint protected by x402.
The client must send:
- x402_payload: JSON string containing at least `{ "price": <wei>, "nonce": <int> }`
- x402_signature: Ethereum signature of the payload
- x402_address: address that signed the payload
"""
if not all([x402_payload, x402_signature, x402_address]):
raise HTTPException(status_code=402, detail="Missing x402 headers")
try:
payload = json.loads(x402_payload)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid x402 payload JSON")
# Basic replay protection: require a monotonically increasing nonce stored per address.
# In production you would use a DB or Redis; here we keep it in memory for demo.
nonce = payload.get("nonce")
price = payload.get("price")
if nonce is None or price is None:
raise HTTPException(status_code=400, detail="Payload missing price or nonce")
if price != PRICE_IN_USDC * 10 ** 6: # USDC has 6 decimals
raise HTTPException(status_code=402, detail="Incorrect price")
# In‑memory nonce store (replace with persistent store!)
if not hasattr(app.state, "seen_nonces"):
app.state.seen_nonces = {}
seen = app.state.seen_nonces.setdefault(x402_address.lower(), set())
if nonce in seen:
raise HTTPException(status_code=402, detail="Replay attack detected")
seen.add(nonce)
# Verify signature
if not verify_x402(payload, x402_signature, x402_address):
raise HTTPException(status_code=401, detail="Invalid signature")
# ---- Payment successful, run the AI ----
try:
result = summarizer(
body.text,
max_length=body.max_length,
min_length=body.min_length,
do_sample=False,
)[0]["summary_text"]
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Model error: {exc}")
return {"summary": result}
# ---------------------------------------------------------------------
Top comments (0)