DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

How I Built an Autonomous AI Agent That Earns USDC While I Sleep

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)  |
                         +-------------------+
Enter fullscreen mode Exit fullscreen mode
  1. Scheduler – a lightweight cron job (or Cloudflare Workers cron trigger) that wakes the agent every 5 minutes.
  2. Agent Loop – loads pending tasks from a local queue (SQLite), decides whether to attempt each task, and hands it off to the worker.
  3. Task Worker – calls the external API, receives a result, then creates and signs an x402 payment request to the API’s payee address.
  4. x402 Payee – the API’s smart contract verifies the payment and credits the agent’s USDC balance on Base.
  5. 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"]
}
Enter fullscreen mode Exit fullscreen mode

The API promises to pay 0.02 USDC per correctly returned label, settled via x402 after the agent submits:

{
  "task_id": "abc123",
  "label": "cat"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 0x for 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}
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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_
Enter fullscreen mode Exit fullscreen mode

Top comments (0)