How I Built an Autonomous AI Agent That Earns USDC While I Sleep
By a developer who prefers shipping code to chasing hype.
Why an “earning‑while‑sleeping” agent?
Autonomous agents are usually discussed in the context of research prototypes or toy demos. In practice, the biggest blocker to putting an LLM‑driven process into production is the cost‑revenue loop: you spend on inference, storage, and networking, but you rarely see a direct financial return unless you wrap the agent in a paid API or a subscription model.
I wanted to test whether a minimal, self‑contained agent could receive micro‑payments for each useful action it performs, settle those payments in a stablecoin (USDC) on a low‑fee L2 (Base), and keep running without manual intervention. The goal wasn’t to build a “money‑making bot” but to explore the engineering trade‑offs of payment‑driven autonomy.
High‑level Architecture
+-------------------+ +-------------------+ +-------------------+
| Task Queue (HTTP| ---> | Agent Core | ---> | Payment Handler |
| / x402 endpoint)| | (LLM + Planner) | | (USDC on Base) |
+-------------------+ +-------------------+ +-------------------+
^ | |
| v v
+------------------+ +----------------+ +-------------------+
| External Caller | | State Store | | Observability |
+------------------+ +----------------+ +-------------------+
- Task Queue – A simple HTTP server that exposes x402-paid endpoints. Each endpoint declares a price in USDC (e.g., $0.01 per call). The caller must attach a valid x402 payment proof before the request is processed.
- Agent Core – An asyncio‑driven loop that pulls pending tasks from the queue, runs a lightweight LLM (via a local llama.cpp server or a hosted API), executes any needed side‑effects (e.g., data lookup, simple computation), and returns a result.
- Payment Handler – Verifies the x402 proof, mints a receipt, and, if the proof is valid, initiates a USDC transfer on Base to the agent’s wallet.
- State Store – SQLite (or PostgreSQL) for persisting task IDs, payment nonces, and agent memory.
- Observability – Structured logging (JSON) plus Prometheus metrics for latency, success/failure rates, and USDC balance.
The whole system runs as a single Docker container (≈150 MB) on a cheap VPS; the only external dependency is a Base RPC endpoint (e.g., Infura or Alchemy) and, optionally, a USDC‑issuer API for fiat‑on‑ramp if you need to top‑up the wallet.
Technical Details & Code Snippets
Below are the essential parts of the implementation. I kept them deliberately minimal so you can copy‑paste them into a repo and iterate.
1. x402 Payment Verifier
The x402 spec defines a header X-PAYMENT containing a JSON‑Web‑Token‑like payload: {addr, amount, token, chainId, nonce, signature}. We verify the signature against the caller’s Ethereum address and check that the amount matches the endpoint’s price.
# payment.py
import json
import base64
from eth_account.messages import encode_defunct
from eth_account import Account
from typing import Tuple
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # Base USDC
CHAIN_ID = 8453 # Base
def verify_x402(header: str, expected_price: int) -> Tuple[bool, str]:
"""
Returns (is_valid, error_message)
expected_price is in wei (1 USDC = 1e6 wei on Base)
"""
try:
payload = json.loads(base64.urlsafe_b64decode(header + "==").decode())
except Exception as e:
return False, f"bad header: {e}"
# Basic field checks
for field in ("addr", "amount", "token", "chainId", "nonce", "signature"):
if field not in payload:
return False, f"missing {field}"
if payload["token"].lower() != USDC_ADDRESS.lower():
return False, "wrong token"
if payload["chainId"] != CHAIN_ID:
return False, "wrong chain"
if int(payload["amount"]) != expected_price:
return False, "amount mismatch"
# Replay protection: check nonce against DB (omitted for brevity)
# if nonce_used(payload["nonce"]): return False, "replay"
# Verify signature
message = encode_defunct(text=json.dumps({
"addr": payload["addr"],
"amount": payload["amount"],
"token": payload["token"],
"chainId": payload["chainId"],
"nonce": payload["nonce"],
}))
try:
recovered = Account.recover_message(message, signature=payload["signature"])
except Exception:
return False, "invalid signature"
if recovered.lower() != payload["addr"].lower():
return False, "signature address mismatch"
return True, ""
2. Agent Core – Task Loop
The core pulls tasks from an in‑memory queue (you could swap this for Redis or a DB table). Each task includes the HTTP method, path, and any payload sent by the caller.
# agent.py
import asyncio
import logging
from typing import Callable, Dict, Any
from payment import verify_x402
logger = logging.getLogger("agent")
class Agent:
def __init__(self, llm_call: Callable[[str], str], price_map: Dict[str, int]):
"""
llm_call: async function that takes a prompt and returns a text response.
price_map: endpoint path -> price in wei (USDC).
"""
self.llm_call = llm_call
self.price_map = price_map
self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
async def worker(self):
while True:
task = await self.queue.get()
try:
await self._handle_task(task)
except Exception as exc:
logger.exception("Task failed: %s", exc)
finally:
self.queue.task_done()
async def _handle_task(self, task: Dict[str, Any]):
path = task["path"]
price = self.price_map.get(path)
if price is None:
logger.warning("No price defined for %s", path)
return
# Verify payment
header = task.get("headers", {}).get("X-PAYMENT")
ok, err = verify_x402(header, price)
if not ok:
logger.warning("Payment verification failed for %s: %s", path, err)
return
# Run the LLM (or any other logic)
prompt = task.get("body", "")
result = await self.llm_call(prompt)
# TODO: send result back to caller (e.g., via HTTP response)
logger.info("Task %s completed, paying %s wei", path, price)
# Trigger payment handler (see below)
await payment_handler.payout(task["caller_addr"], price)
def start(self, workers: int = 2):
for _ in range(workers):
asyncio.create_task(self.worker())
# Example LLM wrapper (local llama.cpp via HTTP)
async def dummy_llm(prompt: str) -> str:
# Replace with real call to your LLM endpoint
await asyncio.sleep(0.2) # simulate latency
return f"Echo: {prompt}"
3. Payment Handler – USDC Transfer on Base
We use web3.py to send a simple ERC‑20 transfer. The agent must hold enough USDC in its wallet; you can top‑up via a fiat‑on‑ramp or by receiving payments from other agents.
python
# payment_handler.py
from web3 import Web3
import os
RPC_URL = os.getenv("BASE_RPC", "https://base.mainnet.rpc.cloud")
USDC_ABI = [...] # standard ERC20 abi (transfer(address,uint256))
w3 = Web3(Web3.HTTPProvider(RPC_URL))
usdc = w3.eth.contract(address=Web3.to_checksum_address(USDC_ADDRESS), abi=USDC_ABi)
AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # keep secret!
agent_addr = w3.eth.account.from_key(AGENT_PRIVATE_KEY).address
async def payout(recipient: str, amount_wei: int):
"""
Sends USDC from agent to recipient.
amount_wei is in USDC wei (1 USDC = 1e6 wei).
"""
nonce = w3.eth.get_transaction_count(agent_addr)
tx = usdc.functions.transfer(
Web3.to_checksum_address(recipient),
amount_wei
).build_transaction({
"chainId": 8453
Top comments (0)