How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with self‑service AI agents and want to see a concrete, low‑hype implementation that can receive micropayments in USDC on the Base layer‑2.
1. Why an autonomous, paid agent?
When you expose a capability as a HTTP service you can let callers pay per‑use instead of relying on subscriptions or ad‑based revenue. The x402 proposal (HTTP 402 Payment Required) gives a lightweight way to gate an endpoint until a verifiable payment is made. By chaining this with an agent that can perform useful work (e.g., data enrichment, simple inference, or report generation) you create a loop:
- Idle – the agent waits for incoming requests.
- Request arrives – the middleware checks for a valid USDC payment on Base.
- If paid – the agent runs its workload and returns the result.
- If not paid – the client receives a 402 response with payment instructions.
The agent can therefore “earn while I sleep” because it only needs to be reachable; the heavy lifting happens on demand.
2. Architecture Overview
+-------------------+ +-------------------+ +-------------------+
| Client (HTTP) | ---> | x402 Middleware | ---> | Agent Worker |
+-------------------+ +-------------------+ +-------------------+
^ | |
| v v
| +-------------------+ +-------------------+
| | Payment Verifier| | Task Executor |
| +-------------------+ +-------------------+
| | |
+-------------------------+-------------------------+
|
v
+-------------------+
| USDC on Base |
+-------------------+
-
x402 Middleware – a thin wrapper around your web framework that inspects the
Paymentheader, forwards the transaction hash to the verifier, and either lets the request proceed or returns a 402 with the required amount and destination address. - Payment Verifier – calls a read‑only RPC endpoint (e.g., Base’s public RPC) to confirm that a USDC transfer of at least the requested amount has been mined and is addressed to the agent’s wallet.
- Task Executor – the actual AI workload. In the example below it’s a simple sentiment‑analysis model, but you can swap in any CPU‑ or GPU‑bound function.
-
USDC on Base – we use the ERC‑20 contract
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913(the canonical USDC on Base).
3. Setting Up the Environment
# Python 3.11+ recommended
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn web3==6.11.0 eth-account==0.9.0 transformers torch
-
fastapigives us an async‑friendly server with minimal boilerplate. -
web3handles RPC calls and transaction decoding. -
transformerssupplies a tiny sentiment model (distilbert-base-uncased-finetuned-sst-2-english).
4. Payment Verifier Implementation
The verifier does three things:
-
Fetch the transaction from the hash supplied in the
x402-paymentheader. -
Check that the
toaddress matches our USDC wallet. - Confirm that the transferred amount (in wei) ≥ required amount.
# payment_verifier.py
from web3 import Web3
from eth_account.messages import encode_defunct
import os
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.v2")
USDC_ADDRESS = Web3.to_checksum_address(
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
)
AGENT_WALLET = os.getenv("AGENT_WALLET") # must be funded with USDC
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
# ERC‑20 minimal ABI for transfer events
ERC20_ABI = [
{
"anonymous": False,
"inputs": [
{"indexed": True, "internalType": "address", "name": "from", "type": "address"},
{"indexed": True, "internalType": "address", "name": "to", "type": "address"},
{"indexed": False, "internalType": "uint256", "name": "value", "type": "uint256"},
],
"name": "Transfer",
"type": "event",
}
]
usdc_contract = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)
def verify_payment(tx_hash: str, required_usdc: float) -> bool:
"""
Returns True if tx_hash corresponds to a USDC transfer of at least
required_usdc to AGENT_WALLET.
"""
try:
tx = w3.eth.get_transaction(tx_hash)
receipt = w3.eth.get_transaction_receipt(tx_hash)
# The transaction must be to the USDC contract (a call to its transfer function)
if tx.to.lower() != USDC_ADDRESS.lower():
return False
# Decode logs to find the Transfer event
logs = usdc_contract.events.Transfer().process_receipt(receipt)
for log in logs:
args = log["args"]
if (
args["to"].lower() == AGENT_WALLET.lower()
and args["value"] >= w3.to_wei(required_usdc, "ether")
):
return True
return False
except Exception as e:
# In production you’d log and possibly retry; here we fail closed.
print(f"Verification error: {e}")
return False
Trade‑offs
- Reliance on a public RPC – free tiers are rate‑limited; for production you’d run your own node or use a paid service to avoid throttling.
- Confirmation latency – we accept the transaction as soon as it’s in a block (≈2 s on Base). If you need stronger finality you could wait for additional confirmations, which adds latency.
- Gas cost – the verifier only makes read calls; the payer pays the gas for the USDC transfer.
5. x402 Middleware (FastAPI)
FastAPI doesn’t have built‑in x402 support, so we add a dependency that checks the payment header before calling the endpoint.
# main.py
from fastapi import FastAPI, Header, HTTPException, Depends
from pydantic import BaseModel
from payment_verifier import verify_payment
import os
app = FastAPI()
# How much we charge per call (in USDC)
PRICE_USDC = float(os.getenv("PRICE_USDC", "0.02")) # $0.02 per request
class SentimentRequest(BaseModel):
text: str
class SentimentResponse(BaseModel):
label: str
score: float
def x402_payment_required(required: float = PRICE_USDC):
async def dependency(
x402_payment: str = Header(None, alias="x402-payment")
):
if not x402_payment:
raise HTTPException(
status_code=402,
headers={
"Accept-Payment": f"usdc:{AGENT_WALLET}:{required}"
},
detail="Payment required",
)
# The client should send: "<tx_hash>"
if not verify_payment(x402_payment, required):
raise HTTPException(
status_code=402,
headers={
"Accept-Payment": f"usdc:{AGENT_WALLET}:{required}"
},
detail="Invalid or insufficient payment",
)
return dependency
@app.post("/sentiment", response_model=SentimentResponse)
async def sentiment(
req: SentimentRequest,
_: None = Depends(x402_payment_required()),
):
# ---- Simple model inference (replace with your own workload) ----
from transformers import pipeline
classifier = pipeline(
"sentiment-analysis",
model="distilbert-base-uncased-finetuned-sst-2-english",
return_all_scores=False,
)
result = classifier(req.text)[0]
return SentimentResponse(label=result["label"], score=result["score"])
How it works
- The client includes a header
x402-payment: <tx_hash>where<tx_hash>is the hash of a USDC transfer they just sent to the agent’s wallet. - The middleware runs
verify_payment. If the check passes, the request proceeds to the sentiment endpoint; otherwise a 402 is returned with anAccept-Paymentheader that tells the client exactly what to pay and where.
Trade‑offs
- Header‑based payment is simple but puts the
Top comments (0)