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 want to put an LLM‑driven agent to work earning micropayments on‑chain. No buzzwords, just the parts that actually moved the needle.


1. Why an Autonomous Earner?

When I started experimenting with LLM agents, the most common demo was a chatbot that answered questions. I wanted something that could produce a measurable economic output without constant supervision. The goal was simple: give the agent a task that people are willing to pay for in USDC, let it run 24/7 on a cheap VPS, and collect the revenue automatically.

The constraints I set for myself were:

Constraint Reason
Self‑contained – no external orchestration framework (e.g., LangChain, Auto‑GPT) To keep the surface area small and understand every failure mode.
Micropayment‑ready – use a protocol that can settle sub‑cent fees USDC on Base via the x402 standard lets us charge per‑API‑call without gas‑fee overhead.
Deterministic safety – the agent must not be able to arbitrage funds or execute unauthorized transactions Isolation of the payment module and strict scoping of the LLM’s toolset.
Observable – logs, metrics, and a simple health‑check endpoint So I could spot a runaway loop before it drained the wallet.

2. High‑Level Architecture

+-------------------+        +---------------------+        +-------------------+
|  Scheduler (cron) | --->   |  Agent Core (Python) | --->   |  Payment Adapter  |
+-------------------+        +---------------------+        +-------------------+
        ^                          |                            |
        |                          v                            v
+-------------------+        +---------------------+        +-------------------+
|  Monitoring (Prometheus) | <-> |  LLM Provider (local/remote) | <-> |  USDC Wallet (Base) |
+-------------------+        +---------------------+        +-------------------+
Enter fullscreen mode Exit fullscreen mode
  • Scheduler – a simple cron entry (*/5 * * * *) launches the agent every five minutes. If the previous run is still alive, the script exits early (lock‑file pattern).
  • Agent Core – the decision loop: fetch a task from a queue, ask the LLM to produce a solution, validate the output, and call the payment adapter to invoice the requester.
  • Payment Adapter – implements the x402 spec: it returns a 402 response with a macaroon‑style payment request, then, upon receiving a valid USDC payment, unlocks the paid resource.
  • Monitoring – Prometheus scrapes /metrics exposed by the agent (task latency, success/failure counts, wallet balance). Alerts fire if balance drops below a threshold or if error rate > 5 %.

3. The Payment Flow – x402 on Base

x402 is an HTTP status code extension that lets a server request payment before serving a resource. The flow is:

  1. Client GET /service/task?input=…
  2. Agent detects no valid payment token → returns 402 Payment Required with header X-Payment-Request: <macaroon>.
  3. Client (or a front‑end) uses the macaroon to call the x402 payer contract on Base, transferring the agreed USDC amount.
  4. Client repeats the request with the payment proof in the X-Payment header.
  5. Agent verifies the proof, executes the paid logic, and returns the result.

Because Base is an L2 with ~ $0.001 transaction cost, charging $0.01–$0.10 per call is economically sensible.


4. Core Code – Agent Loop

Below is the minimal, production‑ready agent I ran on a $5/month VPS (2 vCPU, 1 GB RAM). It uses web3.py for Base interactions, openai for the LLM (you can swap any compatible endpoint), and fastapi for the HTTP service.

# agent.py
import os
import time
import uuid
import hashlib
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from web3 import Web3
from eth_account.messages import encode_defunct
import openai

app = FastAPI()

# ------------------------------------------------------------------
# Configuration (filled from env or .env)
# ------------------------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.chainstack.com")
USDC_ADDRESS = Web3.to_checksum_address(os.getenv("USDC_ADDRESS", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"))
PAYER_CONTRACT = Web3.to_checksum_address(os.getenv("PAYER_CONTRACT", "0x..."))  # x402 payer on Base
AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY")  # holds USDC, must be funded
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
PRICE_USDC = int(os.getenv("PRICE_USDC", "10"))  # in cents, e.g., 10 = $0.10

w3 = Web3(Web3.HTTPProvider(BASE_RPC))
usdc_abi = [...]  # ERC20 minimal ABI (balanceOf, transfer)
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=usdc_abi)
account = w3.eth.account.from_key(AGENT_PRIVATE_KEY)

openai.api_key = OPENAI_API_KEY

# ------------------------------------------------------------------
# Helper: verify x402 payment proof
# ------------------------------------------------------------------
def verify_payment(macaroon: str, payment_header: str) -> bool:
    """
    macaroon: value from X-Payment-Request header (opaque string)
    payment_header: value from X-Payment header (signature of macaroon+price)
    """
    # The payer contract expects a signature over keccak256(macaroon || price)
    message = encode_defunct(text=macaoon + str(PRICE_USDC))
    try:
        recovered = w3.eth.account.recover_message(message, signature=payment_header)
        return recovered.lower() == account.address.lower()
    except Exception:
        return False

# ------------------------------------------------------------------
# Endpoint: expose a paid LLM completion service
# ------------------------------------------------------------------
@app.post("/complete")
async def complete(
    request: Request,
    x_payment: str | None = Header(None),
    x_payment_request: str | None = Header(None),
):
    # 1️⃣ If no payment proof, ask for it (402)
    if not x_payment:
        # generate a fresh macaroon per request to avoid replay
        macaroon = hashlib.sha256(str(uuid.uuid4()).encode()).hexdigest()
        return Response(
            status_code=402,
            headers={"X-Payment-Request": macaroon},
            content="Payment required",
        )

    # 2️⃣ Verify payment
    if not x_payment_request or not verify_payment(x_payment_request, x_payment):
        raise HTTPException(status_code=402, detail="Invalid payment")

    # 3️⃣ Pull input from body
    data = await request.json()
    prompt = data.get("prompt", "")
    if not prompt:
        raise HTTPException(status_code=400, detail="Missing prompt")

    # 4️⃣ Call LLM (simple completion)
    try:
        completion = openai.ChatCompletion.create(
            model="gpt-4o-mini",  # cheap, good enough for demos
            messages=[{"role": "user", "content": prompt}],
            temperature=0.2,
            max_tokens=256,
        )
        answer = completion.choices[0].message["content"]
    except Exception as e:
        # Log and return 502 – payment already consumed, but we refund manually off‑chain
        raise HTTPException(status_code=502, detail=str(e))

    # 5️⃣ Return result
    return JSONResponse({"response": answer})

# ------------------------------------------------------------------
# Health & metrics endpoint (scraped by Prometheus)
# ------------------------------------------------------------------
@app.get("/metrics")
def metrics():
    balance = usdc.functions.balanceOf(account.address).call()
    return Response(
        f"""
        agent_balance_usdc {balance / 1e6}
        agent_uptime_seconds {time.time() - START_TIME}
        """,
        media_type="text/plain",
    )

# ------------------------------------------------------------------
# Entrypoint for cron launcher
# ------------------------------------------------------------------
if __name__ == "__main__":
    import uvicorn
    START_TIME = time.time()
    uvicorn.run(app, host="0.0.0.0", port=8000)
Enter fullscreen mode Exit fullscreen mode

What the script does:

  • Starts a FastAPI server on port 8000.
  • On each request to /complete, it either asks for payment (402) or validates the supplied proof.
  • After validation, it forwards the prompt to an LLM, returns the generated text, and leaves the USDC in the payer contract (the contract automatically forwards funds to the agent’s address).
  • Exposes /metrics for scraping.

The cron launcher (run_agent.sh) simply checks for a lock file, launches uvicorn agent.py:app, and tails the log to a file for debugging:


bash
#!/usr/bin/env bash
LOCKFILE=/tmp/agent.lock
if [ -e "$LOCKFILE" ]; then
    echo "Agent already running"
    exit 1
fi
touch "$LOCKFILE"
uvicorn agent.py:app --host 0.0.0.0 --port 8000 >> /var/log/
Enter fullscreen mode Exit fullscreen mode

Top comments (0)