How I Built an Autonomous AI Agent That Earns USDC While I Sleep
An honest walkthrough of the pieces that actually work, the compromises I made, and what you’d need to replicate it.
Introduction
The idea of an “AI agent that makes money while you sleep” pops up in every hackathon pitch. In practice, the agent is just a piece of software that repeatedly performs a useful, billable task and collects micropayments via a blockchain‑native payment rail. I built one that offers on‑demand text summarization, charges USDC on the Base layer‑2, and uses the x402 protocol for payment negotiation. It runs 24/7 on a cheap VM, and over a month it netted a few dollars—enough to cover its own hosting cost, but far from a get‑rich‑quick scheme.
Below I walk through the architecture, show the core loops, and call out the trade‑offs that kept the system from collapsing under its own weight.
System Overview
+-------------------+ +---------------------+ +------------------+
| HTTP API (FastAPI| ---> | x402 Payment Mediator| ---> | USDC Escrow (Base)|
| + Summarizer) | | (ERC‑4337 wallet) | | (ERC‑20) |
+-------------------+ +---------------------+ +------------------+
^ ^ ^
| | |
Client Request Payment Funds released to Owner’s
(summarize text) Verification agent’s address wallet
-
HTTP API – a tiny FastAPI service that receives JSON
{ "text": "…" }, runs a local Hugging‑Face summarization model, and returns the summary. - x402 Payment Mediator – wraps each request in an x402 handshake: the client presents a signed payment request, the mediator checks that the USDC escrow holds enough funds, signs a receipt, and forwards the request to the API.
- USDC Escrow – a simple ERC‑4337 smart contract wallet on Base that holds the agent’s USDC balance. When a payment succeeds, the mediator transfers the agreed amount from the escrow to the agent’s earnings address.
The loop is: client → x402 mediator → API → summary → client; the mediator handles money, the API handles work.
Core Logic – The Agent Loop
The agent itself is just a long‑running process that:
- Funds the escrow (once, off‑chain).
- Listens for incoming x402 payment requests via a JSON‑RPC endpoint exposed by the mediator.
- When a request is valid, it calls the summarizer, returns the result, and lets the mediator finalize the payment.
Below is the minimal Python loop that runs inside the mediator (the same code could live in a separate worker if you prefer separation of concerns).
# mediator.py
import os
import json
import asyncio
from eth_account import Account
from web3 import Web3
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
w3 = Web3(Web3.HTTPProvider(os.getenv("BASE_RPC"))) # e.g. https://base-mainnet.g.alchemy.com/v2/…
ACCOUNT = Account.from_key(os.getenv("PRIVATE_KEY")) # funds the escrow
ESCROW_ADDR = Web3.to_checksum_address(os.getenv("ESCROW_ADDRESS"))
USDC_ADDR = Web3.to_checksum_address(os.getenv("USDC_ADDRESS")) # Base USDC
# ERC‑20 minimal ABI for balanceOf / transfer
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)
@app.post("/x402/pay")
async def handle_x402(request: Request):
"""
Expects JSON:
{
"payload": "<base64‑encoded request text>",
"signature": "<0x-prefixed ECDSA signature over keccak256(payload)>",
"max_amount": "<uint256 in USDC wei (6 decimals)>"
}
"""
data = await request.json()
payload_b64 = data["payload"]
signature = data["signature"]
max_amount = int(data["max_amount"])
# Recover signer address from signature
message = Web3.keccak(text=payload_b64)
signer = Account.recover_message(message, signature=signature)
# Check escrow balance (must cover max_amount)
bal = usdc.functions.balanceOf(ESCROW_ADDR).call()
if bal < max_amount:
raise HTTPException(status_code=402, detail="Insufficient escrow funds")
# Optional: enforce a per‑call price ceiling (e.g. 0.05 USDC)
PRICE_PER_CALL = 50_000 # 0.05 USDC * 1e6
if max_amount > PRICE_PER_CALL:
raise HTTPException(status_code=402, detail="Amount exceeds allowed price")
# Transfer USDC from escrow to signer (the agent)
tx = usdc.functions.transfer(signer, max_amount).build_transaction({
"chainId": w3.eth.chain_id,
"nonce": w3.eth.get_transaction_count(ACCOUNT.address),
"gas": 100_000,
"gasPrice": w3.to_wei("0.1", "gwei"),
})
signed_tx = ACCOUNT.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
# Now actually do the work – call the summarizer microservice
summary = await call_summarizer(payload_b64) # defined elsewhere
return {
"receipt": tx_hash.hex(),
"result": summary,
}
async def call_summarizer(text_b64: str) -> str:
# Placeholder: in reality you’d hit your FastAPI summarizer endpoint.
# For demo, we just decode and return the first 120 chars.
import base64
text = base64.b64decode(text_b64).decode("utf-8")
return text[:120] + ("…" if len(text) > 120 else "")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
What this does
- Validates that the caller signed a payment intent.
- Checks the escrow holds enough USDC to cover the requested amount.
- Transfers the exact amount from escrow to the caller’s address (the agent’s earnings).
- Calls the summarizer and returns the result alongside the transaction receipt.
The summarizer itself is a separate FastAPI service:
# summarizer.py
from fastapi import FastAPI, Body
from transformers import pipeline
app = FastAPI()
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
@app.post("/summarize")
async def summarize(payload: dict = Body(...)):
text = payload.get("text", "")
if not text:
return {"error": "no text"}
summary = summarizer(text, max_length=130, min_length=30, do_sample=False)[0]["summary_text"]
return {"summary": summary}
You can run both services on the same VM (different ports) or split them across containers; the only hard coupling is the USDC transfer step.
Payment Mechanics – Why x402?
x402 (ERC‑4337‑based) lets a service request payment before doing work, avoiding the classic “do work then hope they pay” problem. The flow is:
- Client creates a payment request:
payload = base64_encode(text_to_summarize), signs it with their EOA. - Mediator validates the signature, checks escrow balance, and pre‑authorizes a transfer of up to
max_amount. - If the work succeeds, the mediator executes the transfer; if it fails, no money moves.
This eliminates the need for a separate invoicing system and gives the client a cryptographic proof of payment (the transaction hash). The downside is extra latency: each request now incurs an on‑chain transaction (≈1‑2 s on Base) plus the gas cost (~0.0005 USDC). For low‑value, high‑volume workloads you’d batch or use a roll‑up with cheaper gas, but for a demo of ~10 req/min it’s acceptable.
USDC on Base – Practicalities
-
Contract address:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913(official USDC on Base). - Decimals: 6, so 1 USDC = 1,000,000 wei.
- **Funding the escrow
Top comments (0)