How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with long‑running LLM‑driven agents and want to understand the practical plumbing—not the marketing fluff.
1. Why “earn while I sleep” is a mis‑nomer
The phrase suggests passive income, but an autonomous agent still consumes compute, network, and‑most importantly‑trust. In my prototype the agent:
- Runs on a cheap VPS (2 vCPU, 4 GB RAM) that incurs a fixed hourly cost.
- Pays for every external call it makes (LLM inference, blockchain reads/writes, third‑party APIs).
- Earns only when a user voluntarily pays for a service the agent provides (via the x402 protocol).
If the agent is idle, it burns money; if it’s over‑aggressive, it can drain its USDC balance faster than it earns. The goal, therefore, is to balance expected revenue against operational cost and to fail‑gracefully when the balance drops below a safety threshold.
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Scheduler (cron) | ---> | Agent Core (ASGI) | ---> | x402 Payment SDK |
+-------------------+ +-------------------+ +-------------------+
^ | |
| v v
+-------------------+ +-------------------+ +-------------------+
| Config & Secrets | | LLM Wrapper | | Base Wallet (USDC)|
+-------------------+ +-------------------+ +-------------------+
-
Scheduler – a simple
cronentry (*/5 * * * *) that hits the agent’s health endpoint every five minutes. If the agent is down, the scheduler restarts the Docker container. -
Agent Core – an ASGI app (FastAPI) that exposes a single
/runendpoint. The endpoint receives a JSON payload describing a task, runs the LLM, optionally calls external tools, and returns a result. -
x402 Payment SDK – wraps the outgoing HTTP response with a payment request header (
X-Payment-Required: ...). The client must attach a valid USDC payment (on Base) before the server processes the request. - LLM Wrapper – a thin abstraction over a hosted inference API (e.g., Together.ai or a self‑hosted Llama‑3‑8B via vLLM). It handles retries, token‑budget enforcement, and logging.
- Wallet – a deterministic HD wallet (derived from a mnemonic stored as a Docker secret) that holds USDC on Base. The agent only ever signs x402 payment responses; it never initiates outgoing transfers.
3. Payment flow with x402
The x402 spec turns HTTP 402 responses into a pay‑per‑call mechanism. When a client calls /run without a valid payment, the agent returns:
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Payment-Required: {"network":"base","currency":"usdc","amount":"0.05","dest":"0xAbc...","maxFee":"0.001"}
{
"error": "payment required",
"payment": {
"network": "base",
"currency": "usdc",
"amount": "0.05",
"dest": "0xAbc...",
"maxFee": "0.001"
}
}
The client must then:
- Submit a USDC transfer (via its own wallet) to
destfor the exactamount. - Include the transaction hash in the
X-Payment-Txheader on a retry.
The agent verifies the transaction on‑chain (using a lightweight JSON‑RPC call to a Base RPC endpoint) before proceeding. If verification fails, it returns another 402 with an updated maxFee to incentivize a higher fee.
4. Core code snippets
Below are the parts that actually make the loop work. I kept them deliberately minimal; production code would add more validation, metrics, and circuit‑breakers.
4.1. FastAPI entry point
# agent/main.py
import os
from fastapi import FastAPI, Request, Header, HTTPException
from x402 import verify_payment, PaymentRequired
from llm_wrapper import generate
from wallet import get_address
app = FastAPI()
AGENT_ADDRESS = get_address() # deterministic from mnemonic secret
@app.post("/run")
async def run_task(
request: Request,
x_payment_tx: str | None = Header(default=None),
):
try:
# Verify that a valid payment was attached
verify_payment(
request=request,
tx_hash=x_payment_tx,
agent_address=AGENT_ADDRESS,
expected_currency="usdc",
# The amount is dynamic; we infer it from the request body later.
)
except PaymentRequired as prec:
# We don't know the price yet—let the LLM wrapper tell us.
body = await request.json()
suggested_price = estimate_price(body) # ← see §4.2
raise PaymentRequired(
amount=suggested_price,
network="base",
currency="usdc",
dest=AGENT_ADDRESS,
maxFee="0.001",
) from prec
# Payment OK – process the task
body = await request.json()
result = await generate(body["prompt"])
return {"output": result}
4.2. Estimating price from the prompt
# agent/pricing.py
BASE_COST = 0.01 # USDC per 1k tokens (LLM inference)
TOKEN_ESTIMATE = 4 # rough chars‑to‑token ratio
def estimate_price(payload: dict) -> str:
prompt = payload.get("prompt", "")
tokens = max(1, len(prompt) // TOKEN_ESTIMATE)
cost = BASE_COST * (tokens / 1000)
# Round up to the nearest cent to avoid micropayment dust
return f"{max(0.01, round(cost, 2)):.2f}"
4.3. Payment verification (x402 SDK wrapper)
# agent/x402.py
import json
import requests
from eth_utils import is_checksum_address
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.trackit.io")
def verify_payment(
*,
request: Request,
tx_hash: str | None,
agent_address: str,
expected_currency: str,
amount: str | None = None,
) -> None:
if not tx_hash:
raise PaymentRequired(...) # will be raised by caller
# 1️⃣ Fetch transaction receipt
payload = {
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": [tx_hash],
"id": 1,
}
resp = requests.post(BASE_RPC, json=payload, timeout=5)
resp.raise_for_status()
data = resp.json()
receipt = data.get("result")
if not receipt or receipt["status"] != "0x1":
raise PaymentRequired(detail="tx not successful")
# 2️⃣ Check that it pays the agent the right amount & currency
if receipt["to"].lower() != agent_address.lower():
raise PaymentRequired(detail="wrong destination")
# For USDC on Base, the contract address is known; we skip token‑specific checks here
# (a production version would call the USDC contract's balanceOf and decimals).
# 3️⃣ If an explicit amount was expected, compare
if amount:
# value is in wei (18 decimals); USDC uses 6 decimals → convert
value_wei = int(receipt["effectiveGasPrice"], 16) * int(receipt["gasUsed"], 16)
# Simplistic: we just ensure the transaction sent *some* value; exact amount check omitted for brevity
pass
Note: The snippet above deliberately omits the full ERC‑20 verification logic to keep the example readable. In a real deployment you would call the USDC contract (
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913on Base) and confirm that the transferred amount matches the requestedamount(adjusted for 6 decimals).
4.4. LLM wrapper with budgeting
# agent/llm_wrapper.py
import openai # or any compatible client
from tenacity import retry, stop_after_attempt, wait_exponential
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MAX_TOKENS_PER_CALL = 1500 # safety net to avoid runaway costs
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def generate(prompt: str) -> str:
response = await openai.ChatCompletion.acreate(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=MAX_TOKENS_PER_CALL,
temperature=0.2,
)
return response.choices[0].message["content"].strip()
5. Operational trade‑offs
| Aspect | Decision | Reasoning | Downside |
|---|---|---|---|
| Host | $5/m |
Top comments (0)