How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are comfortable with Python, asyncio, and basic Ethereum tooling. The goal is to show a realistic, minimal‑viable architecture for an agent that offers paid micro‑services on‑chain and receives USDC settlements automatically.
1. Why bother with an “earning while you sleep” agent?
The idea isn’t to get rich overnight; it’s to experiment with a self‑sustaining loop where an agent:
- Lists a set of deterministic, verifiable services (e.g., data enrichment, simple ML inference, or API wrapping).
- Waits for incoming requests via a lightweight gateway.
- Executes the service, signs a receipt, and claims payment in USDC on the Base L2 using the x402 payment protocol.
If the services are cheap to run and the demand is steady, the agent can accumulate a small USDC balance without manual intervention. The trade‑off is that you must accept latency, occasional failed payments, and the operational overhead of keeping the agent online and funded for gas.
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Scheduler / | ---> | Service Registry| ---> | Payment Handler |
| Event Loop | | (off‑chain JSON) | | (x402 + USDC) |
+-------------------+ +-------------------+ +-------------------+
^ ^ ^
| | |
Incoming HTTP <---> Service Workers (stateless functions) <---> Base RPC (via web3.py)
request (x402) (gas funded by agent wallet)
-
Scheduler / Event Loop – an
asynciotask that polls a local JSON file (or a lightweight DB) for pending jobs, dispatches them to workers, and tracks completion. - Service Registry – a static manifest that maps a service ID to a Python callable, its price in USDC, and any input/output schema.
-
Payment Handler – wraps the x402 spec: validates the
x402header, builds a payment request, signs it with the agent’s EOA, and after successful execution callsx402.payto collect USDC. - Service Workers – pure functions; they receive validated input, perform work, and return a result. Keeping them stateless simplifies scaling and fault‑tolerance.
3. Code walk‑through
Below is a trimmed but runnable example. You’ll need Python 3.11+, web3.py>=6.0, fastapi, and uvicorn.
3.1 Dependencies (requirements.txt)
fastapi==0.110.0
uvicorn[standard]==0.29.0
web3==6.15.0
pydantic==2.7.1
python-dotenv==1.0.0
3.2 Environment variables
Create a .env file (never commit it):
BASE_RPC_URL=https://base.mainnet.rpc.chainstack.com
AGENT_PRIVATE_KEY=0x... # funds must be loaded with a small USDC balance for gas
AGENT_ADDRESS=0x... # derived from the private key
USDC_CONTRACT=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 # USDC on Base
X402_CONTRACT=0xYourX402Proxy # optional, if you use a custom proxy
3.3 Core loop (agent.py)
import asyncio
import json
import os
from datetime import datetime, timezone
from typing import Callable, Dict, Any
from dotenv import load_dotenv
from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
from web3 import Web3
from web3.middleware import geth_poa_middleware
load_dotenv()
w3 = Web3(Web3.HTTPProvider(os.getenv("BASE_RPC_URL")))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
AGENT_ADDR = Web3.to_checksum_address(os.getenv("AGENT_ADDRESS"))
AGENT_KEY = os.getenv("AGENT_PRIVATE_KEY")
USDC_ADDR = Web3.to_checksum_address(os.getenv("USDC_CONTRACT"))
# ----- ERC20 minimal ABI for USDC -----
ERC20_ABI = [
{"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_ADDR, abi=ERC20_ABI)
# ----- Service definitions -----
class ServiceInput(BaseModel):
payload: Dict[str, Any]
async def echo_service(inp: ServiceInput) -> Dict[str, Any]:
"""Example service: just returns the payload with a timestamp."""
return {"echo": inp.payload, "ts": datetime.now(timezone.utc).isoformat()}
SERVICES: Dict[str, Dict] = {
"echo": {
"callable": echo_service,
"price_usdc": 1_000_000, # 0.001 USDC (6 decimals)
"input_model": ServiceInput,
}
}
# ----- x402 helper (very simplified) -----
async def verify_x402_header(request: Request) -> str:
"""
Expects header: X402-Payment: <signature>;<service_id>;<amount>
In a real implementation you would recover the signer and check the nonce.
Here we just parse and trust the caller for demo purposes.
"""
raw = request.headers.get("x402-payment")
if not raw:
raise HTTPException(status_code=402, detail="Missing x402 header")
parts = raw.split(";")
if len(parts) != 3:
raise HTTPException(status_code=400, detail="Malformed x402 header")
sig, service_id, amount_str = parts
if service_id not in SERVICES:
raise HTTPException(status_code=404, detail="Unknown service")
return service_id # placeholder: actual verification omitted
# ----- FastAPI app -----
app = FastAPI()
@app.post("/{service_id}")
async def service_endpoint(service_id: str, request: Request, body: dict):
# 1. Verify payment header (simplified)
await verify_x402_header(request)
# 2. Validate input against service model
svc = SERVICES[service_id]
try:
inp = svc["input_model"](**body)
except Exception as e:
raise HTTPException(status_code=422, detail=f"Invalid input: {e}")
# 3. Execute service
result = await svc["callable"](inp)
# 4. (Optional) Escrow payment – in a real agent you would hold USDC in a contract
# and release after successful execution. For brevity we skip escrow.
return {"status": "ok", "result": result}
# ----- Background loop to monitor balance (for demo) -----
async def balance_watcher():
while True:
bal = usdc.functions.balanceOf(AGENT_ADDR).call()
print(f"[{datetime.now().isoformat()}] USDC balance: {bal/1e6:.6f}")
await asyncio.sleep(300) # every 5 min
@app.on_event("startup")
async def startup():
asyncio.create_task(balance_watcher())
# To run: uvicorn agent:app --host 0.0.0.0 --port 8000
What the snippet does
- Exposes a REST endpoint per service (
/echo). - Expects an
x402-paymentheader that, in a production system, would contain a signature proving the caller paid the exact amount. - Executes the service, returns JSON, and leaves payment settlement to the x402 contract (the caller’s side).
- Runs a background task that prints the agent’s USDC balance every five minutes – useful for observing earnings.
4. Trade‑offs and honest assessment
| Aspect | What we chose | Why it matters | Downside / Mitigation |
|---|---|---|---|
| Language / runtime | Python + FastAPI | Familiar ecosystem, easy to prototype ML or data‑wrapper services. | Higher memory footprint than a bare‑bones Go binary; consider container limits if you run many agents. |
| Payment verification | Minimal header parsing (demo) | Avoids pulling in a full x402 SDK for the example. | Insecure – a real agent must verify the signature, nonce, and that the payer actually sent the USDC via the |
Top comments (0)