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

Introduction

I wanted a minimal, self‑contained agent that could perform a useful task, receive payment in USDC on the Base L2, and run unattended on a cheap VPS. The goal was not to create a “general‑purpose” AI but to demonstrate the mechanics of looping perception, action, and settlement with realistic constraints. This article walks through the architecture, the key code pieces, and the trade‑offs I encountered.

Why USDC on Base?

Base offers low transaction fees (≈ $0.0001 per tx) and fast finality, making micro‑payments feasible. USDC is a widely accepted stablecoin, so the agent can earn value that is easy to convert or reinvest. Using an L2 avoids the noise of Ethereum mainnet gas spikes while still benefiting from EVM compatibility.

Architecture Overview

+----------------+      +----------------+      +----------------+
|   Scheduler    | ---> |   Agent Loop   | ---> |   Tool Exec    |
| (cron/APScheduler) |   (perceive → think → act)   | (paid via x402) |
+----------------+      +----------------+      +----------------+
          ^                         |                         |
          |                         v                         v
      +----------------+      +----------------+      +----------------+
      |   State Store  |      |   LLM Wrapper  |      |   x402 Client  |
      | (SQLite/Redis) |      | (prompt + CFG) |      | (sign & verify)|
      +----------------+      +----------------+      +----------------+
Enter fullscreen mode Exit fullscreen mode
  • Scheduler triggers the agent loop every N minutes (I used 5 min).
  • Agent Loop follows the classic ReAct pattern: observe environment, reason with an LLM, decide on a tool, execute, observe result, repeat until a termination condition.
  • Tool Exec isolates each capability; each tool is wrapped with an x402 payment middleware that requires a valid payment header before proceeding.
  • State Store holds the agent’s memory (short‑term context) and ledger of earned USDC. SQLite is sufficient for a single‑node deployment; swap to Redis if you need horizontal scaling.
  • LLM Wrapper abstracts the model call (I used OpenAI’s gpt‑3.5‑turbo via the openai Python package).
  • x402 Client handles the generation and verification of the payment receipt according to the x402 spec (EIP‑xxxx).

Agent Core – Perceive, Think, Act

Below is a stripped‑down version of the loop. It assumes a simple observation source: a public RSS feed of job postings. The agent’s goal is to summarize new postings and earn USDC for each summary it produces.

# agent_loop.py
import asyncio
import json
import time
from typing import List, Dict

import httpx
import openai
from x402 import verify_payment, PaymentRequired  # hypothetical helper

OPENAI_API_KEY = "sk-..."
LLM_MODEL = "gpt-3.5-turbo"
PAYMENT_AMOUNT = 0.02  # USDC per summary
CHAIN_ID = 8453        # Base

client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY)
http = httpx.AsyncClient(timeout=15.0)

async def perceive() -> List[Dict]:
    """Fetch new items from an RSS feed."""
    resp = await http.get("https://example.com/jobs.rss")
    # parse XML → list of {title, link, pubDate}
    return parse_rss(resp.text)   # implementation omitted for brevity

async def think(observation: List[Dict]) -> str:
    """Ask the LLM to produce a one‑sentence summary."""
    prompt = (
        "You are a concise summarizer. Given the following job posting, "
        "produce a single sentence that captures the role and key requirement.\n\n"
        f"{json.dumps(observation, indent=2)}"
    )
    resp = await client.chat.completions.create(
        model=LLM_MODEL,
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3,
        max_tokens=60,
    )
    return resp.choices[0].message.content.strip()

async def act(summary: str) -> None:
    """Expose the summary via an x402‑protected HTTP endpoint."""
    # In practice the endpoint lives in a separate service; here we simulate a call.
    url = "https://my-agent.example.com/summary"
    headers = {"Content-Type": "application/json"}
    payload = {"summary": summary}
    # The x402 client attaches a payment receipt; the server validates it.
    try:
        resp = await http.post(url, json=payload, headers=headers)
        resp.raise_for_status()
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 402:
            raise PaymentRequired("Missing or invalid payment")
        raise

async def run_cycle():
    obs = await perceive()
    if not obs:
        return  # nothing new
    summary = await think(obs)
    await act(summary)
    # record earning (simplified)
    await record_earning(PAYMENT_AMOUNT)

async def record_earning(amount: float):
    # SQLite INSERT into ledger table (ts, amount, tx_hash)
    pass

def main():
    while True:
        asyncio.run(run_cycle())
        time.sleep(300)  # 5 min

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Key points

  • The loop is deliberately synchronous on the outside (while True:) to keep the process simple; the inner calls are async to avoid blocking on network I/O.
  • Payment validation is delegated to the endpoint (/summary). The agent only proceeds after receiving a successful HTTP 2xx; a 402 triggers a retry or alert.
  • The record_earning function is a placeholder; in a real deployment you would sign a transaction on Base and store the hash for auditability.

Payment Integration – x402 Middleware

Below is a minimal FastAPI endpoint that enforces an x402 payment before returning the summary. The code uses the x402-py library (a community implementation of the spec).

# payment_endpoint.py
from fastapi import FastAPI, Request, HTTPException
from x402 import verify_payment, PaymentRequired
from decimal import Decimal

app = FastAPI()
PRICE_USDC = Decimal("0.02")   # price per call
USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  # USDC on Base
CHAIN_ID = 8453

@app.post("/summary")
async def summary_endpoint(request: Request):
    # Extract the payment receipt from the x402 header
    receipt = request.headers.get("x-x402-payment")
    if not receipt:
        raise PaymentRequired(
            payload={"scheme": "exact", "network": str(CHAIN_ID),
                     "token": USDC_CONTRACT, "price": str(PRICE_USDC)}
        )
    try:
        verify_payment(
            receipt,
            payer_address=request.client.host,  # simplistic; replace with actual signer
            expected_token=USDC_CONTRACT,
            expected_amount=PRICE_USDC,
            chain_id=CHAIN_ID,
        )
    except ValueError as e:
        raise HTTPException(status_code=402, detail=str(e))

    # Business logic – in this example we just echo back the body
    data = await request.json()
    return {"received": data["summary"], "status": "ok"}
Enter fullscreen mode Exit fullscreen mode

The verifier checks the signature, the chain ID, the token contract, and the exact amount. If any check fails, it returns a 402 with a payload that tells the caller how to pay.

State Persistence & Scheduler

I used APScheduler to drive the loop, but a simple cron entry works just as well. The state store is a SQLite file with two tables:

CREATE TABLE IF NOT EXISTS memory (
    id INTEGER PRIMARY KEY,
    ts   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    payload TEXT
);

CREATE TABLE IF NOT EXISTS ledger (
    id INTEGER PRIMARY KEY,
    ts   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    amount_usdc REAL,
    tx_hash TEXT
);
Enter fullscreen mode Exit fullscreen mode
  • memory holds the last N observations so the agent can avoid re‑summarizing the same item.
  • ledger logs each successful payment; I periodically query it to compute net earnings.

Honestly, this is enough for a low‑volume agent. If you expect >10 calls/min, consider switching to Redis or a lightweight Postgres instance to avoid SQLite’s write lock bottleneck.

Honest Trade‑offs

Aspect What I Chose Why Drawback
**LL

Top comments (0)