How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Autonomous agents are a hot topic, but the gap between a demo that talks to a language model and a service that reliably receives payment for useful work is wide. Below is a step‑by‑step walkthrough of the system I put together to let an LLM‑powered agent perform tiny, repeatable tasks on request and collect USDC on Base via the x402 micro‑payment spec. The goal is to show what actually works, where the friction lives, and what you’ll need to watch if you try to replicate it.
1. High‑level architecture
+-------------------+ HTTP (GET/POST) +-------------------+
| Client / UI | <----------------------> | Agent Service |
+-------------------+ (FastAPI) |
^ |
| v
+-------------------+ x402 payment +-------------------+
| Wallet (Base) | <----------------------> | x402 Mediator |
+-------------------+ (USDC escrow) +-------------------+
^ |
| v
+-------------------+ LLM call +-------------------+
| OpenAI / Local | <----------------------> | LLM Wrapper |
+-------------------+ (prompt + tools) +-------------------+
- Client/UI – Any front‑end that can make an HTTP request and attach an x402 payment header (e.g., a simple HTML page or a curl script).
- Agent Service – A thin FastAPI wrapper that validates the x402 payload, forwards the request to an LLM, and returns the result.
- x402 Mediator – The open‑source reference implementation that checks the payment, holds USDC in escrow, and releases it to the service’s wallet once the HTTP call succeeds.
- LLM Wrapper – A function that builds a prompt, optionally calls external tools (e.g., a calculator or a web search), and returns plain text.
The only stateful component is the mediator; the agent service itself is stateless and can be scaled horizontally.
2. Setting up the x402 mediator
I used the reference mediator from the x402-python repo because it already implements Base‑specific USDC handling. Deploying it to Vercel (or any Node‑compatible platform) takes a few minutes:
git clone https://github.com/skynetlabs/x402-python.git
cd x402-python/mediator
npm install
# .env example
BASE_RPC_URL="https://base.mainnet.rpc.dev"
USDC_CONTRACT="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
SERVICE_WALLET_PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
PORT=3000
npm start
The mediator exposes a /pay endpoint that expects:
POST /pay
Content-Type: application/json
{
"maxAmount": "0.01", // USDC
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"receiver": "0xServiceWallet…",
"payload": "<base64‑encoded request body>"
}
If the transaction confirms, the mediator forwards the original request to the URL you set in SERVICE_URL (your agent service) and returns the response to the caller.
Trade‑off: The mediator adds ~200‑300 ms of latency (mostly the time to wait for one Base block confirmation) and requires you to hold a small USDC balance in the mediator’s escrow account. If you need sub‑second response times, you’d have to run a trusted relayer off‑chain, which introduces custodial risk.
3. Agent service implementation (FastAPI)
Below is the core of the agent. It validates the x402 header, extracts the original JSON payload, calls the LLM, and returns a plain‑text answer.
# agent.py
import os
import json
import base64
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import PlainTextResponse
from web3 import Web3
from eth_account.messages import encode_defunct
app = FastAPI()
# Configuration – keep these in environment variables
BASE_RPC = os.getenv("BASE_RPC_URL", "https://base.mainnet.rpc.dev")
USDC_ADDR = Web3.to_checksum_address(os.getenv("USDC_CONTRACT"))
SERVICE_WALLET = Web3.to_checksum_address(os.getenv("SERVICE_WALLET_ADDRESS"))
SERVICE_KEY = os.getenv("SERVICE_WALLET_PRIVATE_KEY") # never commit this
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
def verify_x402_signature(payload_b64: str, sig_hex: str, nonce: str) -> bool:
"""
The mediator signs the concatenation:
keccak256( payload_b64 || nonce )
with the service wallet's private key.
"""
message = f"{payload_b64}{nonce}"
encoded = encode_defunct(text=message)
recovered = w3.eth.account.recover_message(encoded, signature=sig_hex)
return Web3.to_checksum_address(recovered) == SERVICE_WALLET
@app.post("/agent", response_class=PlainTextResponse)
async def agent_endpoint(request: Request):
# 1️⃣ Extract x402 headers
x402_sig = request.headers.get("x402-signature")
x402_nonce = request.headers.get("x402-nonce")
if not (x402_sig and x402_nonce):
raise HTTPException(status_code=400, detail="Missing x402 headers")
# 2️⃣ Read raw body (the mediator already base64‑encoded it)
raw_body = await request.body()
payload_b64 = base64.b64encode(raw_body).decode()
# 3️⃣ Verify signature – ensures the request really came via the mediator
if not verify_x402_signature(payload_b64, x402_sig, x402_nonce):
raise HTTPException(status_code=401, detail="Invalid x402 signature")
# 4️⃣ Decode the original JSON payload
try:
payload = json.loads(base64.b64decode(payload_b64))
except Exception:
raise HTTPException(status_code=400, detail="Invalid payload")
# 5️⃣ Simple LLM call – replace with your preferred provider
prompt = payload.get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="Missing 'prompt' field")
answer = call_llm(prompt) # implementation omitted for brevity
return PlainTextResponse(answer)
def call_llm(prompt: str) -> str:
"""
Placeholder – you can plug in OpenAI, Anthropic, or a local GGUF model.
The function must return a string; any exception bubbles up as 500.
"""
# Example using OpenAI's chat completions (requires openai>=1.0)
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
)
return resp.choices[0].message.content.strip()
Key points
- The service never touches USDC directly; it trusts the mediator’s signature verification.
- All sensitive keys (wallet private key, OpenAI key) live in environment variables; the container image should be built without them.
- The endpoint returns plain text because the mediator expects a 200 OK with a body; you can switch to JSON if you prefer structured responses.
Trade‑off: Verifying the x402 signature adds a small CPU cost (~0.5 ms per request) but eliminates the need for the service to hold funds or implement its own escrow logic. If you anticipate thousands of requests per second, you may want to cache the public key or move verification to an edge worker.
4. Deploying the agent
I containerized the FastAPI app and pushed it to Fly.io (any platform that can expose a TCP port works):
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent.py .
ENV PORT=8080
EXPOSE 8080
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "8080"]
# requirements.txt
fastapi==0.110.0
uvicorn[standard]==0.30.0
web3==7.2.0
eth-account==0.10.0
openai==1.40.0
Deploy:
bash
fly launch --name autonomous-agent --dockerfile ./Dockerfile
fly secrets set BASE_RPC_URL="https://base.mainnet.rpc.dev"
fly secrets set USDC_CONTRACT="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
fly secrets set SERVICE_WALLET_ADDRESS="0xYourServiceWallet"
fly secrets set SERVICE_WALLET_PRIVATE_KEY="0x…"
fly secrets set OPENAI_API
Top comments (0)