How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with or building autonomous AI agents that interact with on‑chain payment systems.
1. Why bother with an “earning” agent?
The idea isn’t to get rich overnight; it’s to explore whether a software‑only entity can reliably perform a repeatable, micro‑task‑style job and receive settlement in a stablecoin without human intervention. If the agent can cover its own operational costs (compute, storage, gas) and still produce a net positive balance, it becomes a useful building block for larger autonomous services (e.g., data validation, model fine‑tuning, oracles).
The experiment I ran was deliberately narrow: the agent performs simple image‑classification microtasks for a public API that pays USDC on Base via the x402 payment protocol. The goal was to measure uptime, earnings vs. cost, and failure modes rather than to chase profitability.
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Scheduler (cron) | ---> | Agent Loop | ---> | Task Worker |
+-------------------+ +-------------------+ +-------------------+
| ^ |
| | Payment Tx |
v | |
+-------------------+ |
| x402 Payee (USDC) |<-----+
+-------------------+
|
v
+-------------------+
| External API |
| (image labeler) |
+-------------------+
- Scheduler – a lightweight cron job (or Cloudflare Workers cron trigger) that wakes the agent every 5 minutes.
- Agent Loop – loads pending tasks from a local queue (SQLite), decides whether to attempt each task, and hands it off to the worker.
- Task Worker – calls the external API, receives a result, then creates and signs an x402 payment request to the API’s payee address.
- x402 Payee – the API’s smart contract verifies the payment and credits the agent’s USDC balance on Base.
- External API – provides the microtasks and pays per successful completion.
All components run on a cheap VPS (≈ $5/mo) or a serverless function; the only on‑chain cost is the gas for the USDC transfer, which on Base is typically < $0.001 per transaction.
3. Choosing the task and payment model
I selected an open image‑labeling endpoint that returns a JSON payload like:
{
"task_id": "abc123",
"image_url": "https://example.com/img/abc123.png",
"label_set": ["cat", "dog", "bird"]
}
The API promises to pay 0.02 USDC per correctly returned label, settled via x402 after the agent submits:
{
"task_id": "abc123",
"label": "cat"
}
The choice was intentional:
- Low computational demand – a simple HTTP GET + POST.
- Clear success criteria – the API validates the label against ground truth and only pays if correct.
- Micro‑payment friendliness – the per‑task payout is small enough to test many cycles without large capital lock‑up.
If you try a different task (e.g., text summarization, data validation), adjust the validation logic and the payment amount accordingly.
4. Working code snippets
Below are the core pieces I ran on a Ubuntu 22.04 box with Python 3.11. Install the dependencies:
pip install web3==7.0.0 requests tenacity
4.1 Wallet setup & USDC handling
We keep the agent’s private key in an environment variable (AGENT_PK). Never commit it.
import os
from web3 import Web3
BASE_RPC = "https://base.mainnet.rpc.dev"
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
assert w3.is_connected(), "Cannot connect to Base RPC"
USDC_ADDRESS = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") # USDC on Base
ERC20_ABI = [...] # standard ERC20 abi (transfer, balanceOf, decimals)
usdc_contract = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)
def get_balance() -> float:
raw = usdc_contract.functions.balanceOf(w3.eth.account.from_key(os.getenv("AGENT_PK")).address).call()
return raw / 10**usdc_contract.functions.decimals().call()
def send_usdc(to: str, amount_usdc: float) -> str:
amount = int(amount_usdc * 10**usdc_contract.functions.decimals().call())
acct = w3.eth.account.from_key(os.getenv("AGENT_PK"))
txn = usdc_contract.functions.transfer(
Web3.to_checksum_address(to),
amount
).build_transaction({
"chainId": w3.eth.chain_id,
"gas": 70_000,
"maxFeePerGas": w3.to_wei(2, "gwei"),
"maxPriorityFeePerGas": w3.to_wei(1, "gwei"),
"nonce": w3.eth.get_transaction_count(acct.address),
})
signed = acct.sign_transaction(txn)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
return w3.to_hex(tx_hash)
4.2 The x402 payment payload
x402 expects a JSON‑RPC style request that includes a signed authorization header. The spec (simplified) is:
- Header
X402-Payment: <scheme> <token_address> <amount> <expiry> <signature> - Scheme is
0xfor ERC‑20.
I wrapped this in a helper:
import time
import json
from eth_account.messages import encode_defunct
X402_SCHEME = "0x" # ERC20
EXPIRY_SECONDS = 300 # 5 min
def build_x402_header(token: str, amount_usdc: float, payer_addr: str) -> dict:
amount = int(amount_usdc * 10**usdc_contract.functions.decimals().call())
expiry = int(time.time()) + EXPIRY_SECONDS
# Encode the payload: token|amount|expiry
payload = f"{token.lower()}{amount}{expiry}"
message = encode_defunct(text=payload)
acct = w3.eth.account.from_key(os.getenv("AGENT_PK"))
signature = acct.sign_message(message).signature.hex()
header_val = f"{X402_SCHEME} {token} {amount} {expiry} {signature}"
return {"X402-Payment": header_val}
4.3 Task worker – fetch, label, pay
import requests
from tenacity import retry, stop_after_attempt, wait_exponential
LABEL_API = "https://labeler.example.com/api/task"
PAYEE_ADDR = "0xPayeeAddressOnBase" # the API's x402 payee
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def fetch_task() -> dict:
resp = requests.get(LABEL_API, timeout=10)
resp.raise_for_status()
return resp.json()
def naive_label(image_url: str, label_set: list) -> str:
# Placeholder: in reality you’d call a tiny ML model or heuristic.
# For the demo we just return the first label.
return label_set[0]
def process_one_task():
task = fetch_task()
label = naive_label(task["image_url"], task["label_set"])
payload = {
"task_id": task["task_id"],
"label": label,
}
# Build x402 header paying 0.02 USDC
header = build_x402_header(USDC_ADDRESS, 0.02, w3.eth.account.from_key(os.getenv("AGENT_PK")).address)
pay_resp = requests.post(
f"{LABEL_API}/pay",
json=payload,
headers=header,
timeout=15,
)
pay_resp.raise_for_status()
print(f"Task {task['task_id']} completed, label={label}")
# Simple loop used by the scheduler
if __name__ == "__main__":
try:
process_one_task()
except Exception as e:
print(f"Error: {e}")
The scheduler (a systemd timer or Cloudflare Workers cron) runs this script every five minutes.
4.4 Monitoring earnings vs. cost
python
def report():
bal = get_balance()
gas_est = 0.001 # rough average USDC transfer cost on Base in USDC
net = bal - gas_est * (num_calls_today) # you’d track calls elsewhere
print(f"Balance: {bal:.4f} USDC | Estimated gas spent today: {gas_
Top comments (0)