How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with autonomous agents that can both perform useful work and receive micropayments.
1. Problem framing
I wanted an agent that could run continuously on a cheap VM, execute a well‑defined task (in my case, summarising short news articles for a niche audience), and receive payment in USDC on the Base L2 each time a consumer called its endpoint. The goal was not to build a “profit machine” but to explore the engineering constraints of combining LLM inference with on‑chain micropayments via the x402 standard.
2. High‑level architecture
+----------------+ HTTP (x402) +---------------------+
| Consumer App | <--------------------> | Agent Service (FastAPI) |
+----------------+ +---------------------+
|
+----------------v----------------+
| LLM Wrapper (OpenAI API) |
| + Summary Logic (deterministic) |
+----------------v----------------+
|
+----------v----------+
| USDC Escrow (x402) |
+----------v----------+
|
+----------v----------+
| Base L2 (USDC) |
+---------------------+
- The agent is a thin HTTP wrapper around an LLM call.
- x402 adds a payment header to each request; the agent verifies the proof before invoking the LLM.
- All state is kept in memory; persistence is limited to a SQLite log for audit.
- Deployment target: a single‑CPU VPS (≈$5/mo) running Docker.
3. Choosing the payment layer
x402 defines a lightweight protocol where the client includes an X402-Payment header containing a signed claim that pays a preset amount to the service’s address. The server validates the signature, checks the nonce, and then proceeds.
Why x402?
- No need to deploy a smart contract per endpoint; the escrow lives in the x402 relayer.
- Payments are settled instantly on Base, with sub‑cent gas costs (~0.0005 USDC per transaction).
- The spec is still experimental; tooling is sparse, which means more integration work.
4. Core code snippets
Below are the essential parts of the agent. The full repo is a single Dockerfile + requirements.txt + app.py.
4.1 Dependencies
# pyproject.toml (excerpt)
fastapi = "^0.110.0"
uvicorn = { extras = ["standard"], version = "^0.30.0"
httpx = "^0.27.0"
eth-account = "^0.12.0"
eth-utils = "^2.5.0"
pydantic = "^2.8.2"
4.2 x402 verification helper
# app.py
from eth_account.messages import encode_defunct
from eth_account import Account
import base64
import json
import time
from fastapi import Header, HTTPException
X402_RELAYER = "0xRelayerAddressOnBase" # set by the x402 relayer
SERVICE_ADDRESS = "0xMyAgentAddress" # the address that receives USDC
PRICE_USDC = 0.005 # 0.5 ¢ per call
def verify_x402_payment(
x402_header: str | None = Header(None),
nonce: int | None = Header(None, alias="X402-Nonce")
) -> None:
if not x402_header or nonce is None:
raise HTTPException(status_code=402, detail="Missing payment header")
try:
payload = json.loads(base64.urlsafe_b64decode(x402_header + "==".encode()))
except Exception:
raise HTTPException(status_code=400, detail="Invalid payment header")
# Expected structure: { "to": SERVICE_ADDRESS, "value": amount_in_wei, "relayer": X402_RELAYER, "signature": "0x..." }
if payload["to"].lower() != SERVICE_ADDRESS.lower():
raise HTTPException(status_code=402, detail="Incorrect payee")
if payload["relayer"].lower() != X402_RELAYER.lower():
raise HTTPException(status_code=402, detail="Unsupported relayer")
if int(payload["value"]) != int(PRICE_USDC * 1e6): # USDC has 6 decimals
raise HTTPException(status_code=402, detail="Incorrect amount")
# Replay protection – simple in‑memory nonce cache (reset on restart)
if nonce in _USED_NONCES:
raise HTTPException(status_code=402, detail="Replay attack")
_USED_NONCES.add(nonce)
# Verify signature
message = encode_defunct(text=json.dumps(payload, separators=(",", ":")))
recovered = Account.recover_message(message, signature=payload["signature"])
if recovered.lower() != payload["relayer"].lower():
raise HTTPException(status_code=402, detail="Invalid signature")
4.3 Endpoint that does the work
# app.py (continued)
import openai
import os
from fastapi import FastAPI
app = FastAPI()
openai.api_key = os.getenv("OPENAI_KEY")
_ Used nonces – in production replace with Redis or a DB
_USED_NONCES: set[int] = set()
@app.post("/summarize")
async def summarize(
text: str,
x402_header: str | None = Header(None),
nonce: int | None = Header(None, alias="X402-Nonce")
):
verify_x402_payment(x402_header, nonce)
# Very simple deterministic pre‑/post processing to reduce token usage
prompt = f"Summarize the following in two sentences:\n\n{text[:1500]}"
try:
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=120,
)
summary = resp.choices[0].message.content.strip()
except Exception as e:
# If the LLM fails we still keep the payment – this is a trade‑off.
# In a production system you would refund or retry.
raise HTTPException(status_code=502, detail=f"LLM error: {e}")
return {"summary": summary}
4.4 Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV OPENAI_KEY=${OPENAI_KEY}
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
5. Trade‑offs I encountered
| Area | Decision | Cost / Benefit | Honest drawback |
|---|---|---|---|
| LLM provider | OpenAI gpt-4o-mini via API |
Low latency (~300 ms), good quality | Ongoing per‑token cost; rate limits require back‑off or a fallback model. |
| Payment verification | In‑memory nonce set | Zero extra infrastructure overhead | Nonces reset on restart → possible replay after a redeploy; for long‑running agents you need a persistent store (Redis, Postgres). |
| Escrow model | Rely on the public x402 relayer on Base | No contract deployment, gas ≈ 0.0005 USDC | Dependence on a third‑party relayer; if it goes down the agent cannot verify payments. |
| Error handling | Keep payment even if LLM fails | Simpler code, guarantees revenue for the agent | Consumers may feel cheated; a production system would implement a retry/refund flow. |
| Observability | Simple print logs + SQLite audit log |
Easy to set up | No distributed tracing; debugging latency spikes requires manual log correlation. |
| Deployment | Single‑container VPS | Cheap, easy to update | No horizontal scaling; a burst of traffic will queue requests and increase latency. |
These compromises kept the prototype under a few hundred lines of code while still exercising the full loop: request → payment verification → LLM call → response → settlement.
6. Testing & monitoring
-
Unit tests for
verify_x402_paymentusing a locally generated keypair. -
End‑to‑end test with
curlthat funds a temporary address on Base (via a faucet), builds an x402 header, hits/summarize, and checks that the USDC balance of the agent address increased by the expected amount (usingweb3.pyto query the ERC‑20 contract). - Load test: 10 req/s for 5 minutes showed average latency of 420 ms and 0.2 % error rate (mostly due to OpenAI rate‑limit 429 responses). Adding a simple exponential back‑off reduced errors to <0.05 %.
A lightweight Prometheus exporter (prometheus_client) exposed /metrics with counters for requests_total, payments_received_total, and `ll
Top comments (0)