How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Goal: Show a minimal, production‑ish pattern for an AI‑driven service that autonomously charges USDC via the x402 protocol. The focus is on the plumbing, not on the AI model itself.
1. Why x402?
x402 is a lightweight HTTP‑based payment scheme that lets a server respond with a 402 Payment Required status and a payment request in the WWW-Authenticate header. Clients that understand x402 can automatically fetch USDC, sign a transaction, and retry the request.
For an autonomous agent this means:
- Statelessness – the agent doesn’t need to keep a user‑side balance; payment is enforced at the API boundary.
- Compatibility – any HTTP client (curl, Postman, a custom SDK) can be upgraded to pay without changing business logic.
- Low overhead – the protocol adds only a few bytes to the response; the heavy lifting stays in the payment SDK.
The trade‑off is that you must accept the extra round‑trip for unauthenticated callers and you need to host a wallet that can sign USDC transfers on the target chain (here, Base).
2. High‑level Architecture
+-------------------+ HTTP/x402 +-------------------+
| Client (any) | <----------------> | Agent Service |
+-------------------+ (FastAPI) +-------------------+
^ |
| v
| +-------------------+
| | Wallet Manager |
| | (web3.py + private|
| | key, USDC ABI) |
| +-------------------+
| |
| v
| +-------------------+
+---------------------------------| USDC Ledger |
| (Base testnet/main) |
+-------------------+
- Agent Service – a FastAPI app that exposes one or more useful endpoints (e.g., text summarization, image tagging). Each endpoint checks for a valid x402 payment; if missing, it returns a 402 with payment details.
-
Wallet Manager – a singleton that loads an Ethereum private key, constructs USDC transfer transactions, and signs them using
web3.py. -
USDC Ledger – the Base network contract (
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913on Base mainnet).
3. Code Walk‑through
Below is a complete, runnable example (≈120 lines) that you can copy into a file agent.py and run with uvicorn agent:app --host 0.0.0.0 --port 8000.
3.1 Dependencies
# pyproject.toml
[project]
name = "x402-agent"
dependencies = [
"fastapi==0.110.0",
"uvicorn[standard]==0.30.0",
"web3==7.8.0",
"eth-account==0.10.0",
"pydantic==2.7.1",
]
3.2 Core implementation
# agent.py
import os
from typing import Literal
from fastapi import FastAPI, Header, HTTPException, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_defunct
app = FastAPI(title="Autonomous USDC‑earning Agent")
# ----------------------------------------------------------------------
# Configuration – replace with your own values or load from env/secrets
# ----------------------------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://mainnet.base.org") # public RPC, OK for read‑only
USDC_CONTRACT = Web3.to_checksum_address(
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
) # USDC on Base
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # must fund this address with USDC
if not PRIVATE_KEY:
raise RuntimeError("Set AGENT_PRIVATE_KEY env var")
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
account = Account.from_key(PRIVATE_KEY)
usdc_abi = [
{
"constant": False,
"inputs": [
{"name": "_to", "type": "address"},
{"name": "_value", "type": "uint256"},
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function",
},
{
"constant": True,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function",
},
]
usdc_contract = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)
# ----------------------------------------------------------------------
# x402 helpers
# ----------------------------------------------------------------------
X402_SCHEME = "Bearer"
PRICE_USDC = 0.01 # price per call, in USDC (6 decimals)
USDC_DECIMALS = 6
def price_in_wei() -> int:
"""Return price in the smallest USDC unit (wei)."""
return int(PRICE_USDC * 10**USDC_DECIMALS)
def build_402(challenge_id: str) -> Response:
"""Create a 402 response with x402 WWW‑Authenticate header."""
header = (
f'{X402_SCHEME} x402; '
f'price="{price_in_wei()}"; '
f'currency="USDC"; '
f'network="base"; '
f'payee="{account.address}"; '
f'payload="{challenge_id}"'
)
return Response(
status_code=402,
headers={"WWW-Authenticate": header},
media_type="application/json",
content='{"error":"payment required"}',
)
def verify_payment(auth_header: str | None, payload: str) -> bool:
"""
Very small subset of x402 verification: we expect the client to have
sent a signed transaction hash in the Authorization header.
In practice you would use the official x402-py SDK; here we keep it
illustrative.
"""
if not auth_header or not auth_header.startswith(f"{X402_SCHEME} "):
return False
tx_hash = auth_header.split(" ", 1)[1]
# Basic sanity: transaction must exist and transfer correct amount to us
try:
tx = w3.eth.get_transaction(tx_hash)
except Exception:
return False
if tx["to"].lower() != USDC_CONTRACT.lower():
return False
# decode input data (simple transfer)
func_obj, func_params = usdc_contract.decode_function_input(tx["input"])
if func_obj.fn_name != "transfer":
return False
if func_params["_to"].lower() != account.address.lower():
return False
if func_params["_value"] != price_in_wei():
return False
# optional: check receipt status = 1
receipt = w3.eth.get_transaction_receipt(tx_hash)
return receipt.status == 1
# ----------------------------------------------------------------------
# Example AI endpoint – replace with your own model inference
# ----------------------------------------------------------------------
class SummarizeRequest(BaseModel):
text: str
max_length: int = 130
@app.post("/summarize")
async def summarize(
req: SummarizeRequest,
authorization: str = Header(None),
x_payload: str = Header(None),
):
"""
Returns a naive extractive summary. Payment is enforced via x402.
"""
# 1️⃣ Verify payment (if any)
if not verify_payment(authorization, x_payload or ""):
# generate a random challenge to avoid replay attacks
import uuid
challenge = str(uuid.uuid4())
return build_402(challenge)
# 2️⃣ Actual work – placeholder logic
sentences = req.text.split(". ")
summary = ". ".join(sentences[: max(1, req.max_length // 20)]) + "."
return JSONResponse({"summary": summary})
# ----------------------------------------------------------------------
# Health check – no payment required
# ----------------------------------------------------------------------
@app.get("/healthz")
async def healthz():
return {"status": "ok", "usdc_balance": usdc_contract.functions.balanceOf(account.address).call() / 10**USDC_DECIMALS}
What the snippet does
-
Wallet setup – loads a private key from the environment, connects to Base via a public RPC, and prepares a minimal USDC ABI (only
transferandbalanceOf). -
x402 response –
build_402creates a402 Payment Requiredheader that tells the client how much USDC to send, to which address, and on which chain. -
Payment verification –
verify_paymentexpects the client to resend the request with anAuthorization: Bearer <txHash>header. It checks that the transaction actually transferred the correct amount to the agent’s address. (In a real deployment you’d use the official x402‑py library which also validates signatures and replays.)
Top comments (0)