How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with autonomous AI agents and want to understand the practical steps, trade‑offs, and code needed to make an agent that can receive micropayments for its work.
1. Why “earn while I sleep” is a matter of infrastructure, not magic
The phrase “earn while I sleep” is often used as a shorthand for a system that can perform useful work continuously, receive compensation automatically, and require little human oversight. In practice this means:
- A stateless service that can be invoked on demand (or via a queue) and returns a deterministic result.
- A payment rails integration that lets the caller pay the service provider in a programmable token (USDC) without manual invoicing.
- Observability and fault tolerance so the agent keeps running despite transient failures or network glitches.
If any of those pieces are missing, the agent will either stall, over‑charge, or require manual intervention—defeating the “sleep” promise.
2. Choosing the payment protocol: x402 on Base
Several blockchain‑based micropayment schemes exist (e.g., Lightning, Stellar, Polygon). For this experiment I selected x402 because:
- It is designed specifically for AI‑service monetisation: the client includes a payment proof in the HTTP header, the service verifies it, and then returns the result.
- The reference implementation works on Base, an Ethereum L2 with low gas fees (~$0.0005 per transaction) and fast finality (~2 seconds).
- The SDK provides a lightweight Python wrapper (
x402-py) that handles signature verification and USDC escrow logic.
The trade‑off is that you must accept the custodial model of the x402 relayer (the service that holds USDC in escrow until the proof is verified). If you need full self‑custody, you would have to build a custom escrow contract, which adds considerable complexity.
3. High‑level architecture
+----------------+ HTTP (POST) +----------------+ x402 proof +-------------------+
| Task Queue | -----------------> | Agent Service | <---------------- | Client (Caller) |
| (Redis/RMQ) | | (FastAPI) | verifies USDC | (pays via x402) |
+----------------+ +----------------+ +-------------------+
^ |
| v
| +-------------------+
| | Wallet (USDC) |
| | (agent's address)|
| +-------------------+
|
| Periodic poll (every 30 s) for new tasks
v
+----------------+ internal logic +-----------------+
| Worker Loop | ---------------------> | AI Model (e.g.,|
| (asyncio) | – run inference, | Ollama/Llama) |
| | – format output, +-----------------+
| | – store result |
+----------------+ +-----------------+
- The task queue holds JSON payloads describing the work (e.g., a text prompt for summarisation).
- The agent service exposes a single
POST /runendpoint. When a request arrives, it:- Extracts the x402 payment proof from the
X-PAYMENTheader. - Verifies the proof against the USDC contract on Base using the x402 SDK.
- If valid, pulls the next task from the queue, runs the AI model, and returns the result.
- If invalid, returns
402 Payment Requiredwith a helpful error message.
- Extracts the x402 payment proof from the
- The worker loop runs independently of the HTTP layer; it ensures the agent can keep earning even when no inbound requests are present (it simply idles, waiting for the next queued task).
4. Setting up the environment
# Python 3.11+ recommended
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn redis x402-py web3 transformers torch
-
redis– for the task queue (you can replace with RabbitMQ or any broker). -
x402-py– providesverify_paymenthelper. -
web3.py– to read USDC contract details on Base (address, decimals). -
transformers+torch– to run a small open‑source LLM locally (e.g.,TinyLlama/TinyLlama-1.1B-Chat-v1.0).
5. The x402 verification helper
# payment.py
from x402 import verify_payment
from web3 import Web3
import os
BASE_RPC = os.getenv("BASE_RPC", "https://mainnet.base.org")
USDC_ADDRESS = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") # USDC on Base
CHAIN_ID = 8453
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
usdc_contract = w3.eth.contract(address=USDC_ADDRESS, 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"},
{"constant":True,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],
"type":"function"}
])
def check_x402_payment(header: str, agent_addr: str, amount_usdc: int) -> bool:
"""
header – raw value of the X-PAYMENT header (base64‑encoded JSON)
agent_addr – Ethereum address that should receive the USDC
amount_usdc – expected payment in smallest USDC units (6 decimals)
Returns True if proof is valid and funds are escrowed.
"""
try:
# verify_payment validates the signature, nonce, and that the relayer
# has locked the required amount.
return verify_payment(
header,
agent_addr,
amount_usdc,
usdc_contract,
w3,
chain_id=CHAIN_ID,
)
except Exception as e:
# In production you would log e with context.
return False
Trade‑off note: The helper trusts the x402 relayer to have correctly locked USDC. If the relayer misbehaves, the agent could receive a valid proof but never see the funds. Mitigation: monitor the relayer’s on‑chain escrow contract and fallback to a manual dispute process (outside the scope of this simple prototype).
6. The FastAPI service
python
# main.py
import os
import json
import base64
import asyncio
import redis
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from payment import check_x402_payment
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
app = FastAPI()
r = redis.Redis(host=os.getenv("REDIS_HOST", "localhost"), port=6379, db=0)
# ----- AI model loading (done once at startup) -----
MODEL_NAME = os.getenv("MODEL_NAME", "TinyLlama/TinyLlama-1.1B-Chat-v1.0")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)
generator = pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=150)
AGENT_ADDRESS = os.getenv("AGENT_ADDRESS") # must be set to your Base wallet
PRICE_PER_CALL_USDC = int(os.getenv("PRICE_PER_CALL_USDC", "10")) # $0.01 = 10 * 10^‑6 USDC
# USDC has 6 decimals, so 10 = 0.000010 USDC → adjust as needed
class RunRequest(BaseModel):
prompt: str
@app.post("/run")
async def run_agent(req: RunRequest, x_payment: str = Header(None)):
if not x_payment:
raise HTTPException(status_code=402, detail="Missing X-PAYMENT header")
# decode base64 header (client sends base64‑encoded JSON)
try:
payload = json.loads(base64.b64decode(x_payment))
except Exception:
raise HTTPException(status_code=400, detail="Invalid X-PAYMENT format")
if not
Top comments (0)