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 autonomous agents that can both perform useful work and receive micropayments.


1. Problem framing

I wanted an agent that could run continuously on a cheap VM, execute a well‑defined task (in my case, summarising short news articles for a niche audience), and receive payment in USDC on the Base L2 each time a consumer called its endpoint. The goal was not to build a “profit machine” but to explore the engineering constraints of combining LLM inference with on‑chain micropayments via the x402 standard.


2. High‑level architecture

+----------------+      HTTP (x402)      +---------------------+
|  Consumer App  | <--------------------> |  Agent Service (FastAPI) |
+----------------+                        +---------------------+
                                                   |
                                   +----------------v----------------+
                                   |  LLM Wrapper (OpenAI API)         |
                                   |  + Summary Logic (deterministic) |
                                   +----------------v----------------+
                                                   |
                                           +----------v----------+
                                           |  USDC Escrow (x402)   |
                                           +----------v----------+
                                                   |
                                           +----------v----------+
                                           |  Base L2 (USDC)      |
                                           +---------------------+
Enter fullscreen mode Exit fullscreen mode
  • The agent is a thin HTTP wrapper around an LLM call.
  • x402 adds a payment header to each request; the agent verifies the proof before invoking the LLM.
  • All state is kept in memory; persistence is limited to a SQLite log for audit.
  • Deployment target: a single‑CPU VPS (≈$5/mo) running Docker.

3. Choosing the payment layer

x402 defines a lightweight protocol where the client includes an X402-Payment header containing a signed claim that pays a preset amount to the service’s address. The server validates the signature, checks the nonce, and then proceeds.

Why x402?

  • No need to deploy a smart contract per endpoint; the escrow lives in the x402 relayer.
  • Payments are settled instantly on Base, with sub‑cent gas costs (~0.0005 USDC per transaction).
  • The spec is still experimental; tooling is sparse, which means more integration work.

4. Core code snippets

Below are the essential parts of the agent. The full repo is a single Dockerfile + requirements.txt + app.py.

4.1 Dependencies

# pyproject.toml (excerpt)
fastapi = "^0.110.0"
uvicorn = { extras = ["standard"], version = "^0.30.0"
httpx = "^0.27.0"
eth-account = "^0.12.0"
eth-utils = "^2.5.0"
pydantic = "^2.8.2"
Enter fullscreen mode Exit fullscreen mode

4.2 x402 verification helper

# app.py
from eth_account.messages import encode_defunct
from eth_account import Account
import base64
import json
import time
from fastapi import Header, HTTPException

X402_RELAYER = "0xRelayerAddressOnBase"   # set by the x402 relayer
SERVICE_ADDRESS = "0xMyAgentAddress"    # the address that receives USDC
PRICE_USDC = 0.005                      # 0.5 ¢ per call

def verify_x402_payment(
    x402_header: str | None = Header(None),
    nonce: int | None = Header(None, alias="X402-Nonce")
) -> None:
    if not x402_header or nonce is None:
        raise HTTPException(status_code=402, detail="Missing payment header")

    try:
        payload = json.loads(base64.urlsafe_b64decode(x402_header + "==".encode()))
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid payment header")

    # Expected structure: { "to": SERVICE_ADDRESS, "value": amount_in_wei, "relayer": X402_RELAYER, "signature": "0x..." }
    if payload["to"].lower() != SERVICE_ADDRESS.lower():
        raise HTTPException(status_code=402, detail="Incorrect payee")
    if payload["relayer"].lower() != X402_RELAYER.lower():
        raise HTTPException(status_code=402, detail="Unsupported relayer")
    if int(payload["value"]) != int(PRICE_USDC * 1e6):   # USDC has 6 decimals
        raise HTTPException(status_code=402, detail="Incorrect amount")

    # Replay protection – simple in‑memory nonce cache (reset on restart)
    if nonce in _USED_NONCES:
        raise HTTPException(status_code=402, detail="Replay attack")
    _USED_NONCES.add(nonce)

    # Verify signature
    message = encode_defunct(text=json.dumps(payload, separators=(",", ":")))
    recovered = Account.recover_message(message, signature=payload["signature"])
    if recovered.lower() != payload["relayer"].lower():
        raise HTTPException(status_code=402, detail="Invalid signature")
Enter fullscreen mode Exit fullscreen mode

4.3 Endpoint that does the work

# app.py (continued)
import openai
import os
from fastapi import FastAPI

app = FastAPI()
openai.api_key = os.getenv("OPENAI_KEY")

_ Used nonces  in production replace with Redis or a DB
_USED_NONCES: set[int] = set()

@app.post("/summarize")
async def summarize(
    text: str,
    x402_header: str | None = Header(None),
    nonce: int | None = Header(None, alias="X402-Nonce")
):
    verify_x402_payment(x402_header, nonce)

    # Very simple deterministic pre‑/post processing to reduce token usage
    prompt = f"Summarize the following in two sentences:\n\n{text[:1500]}"
    try:
        resp = openai.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.3,
            max_tokens=120,
        )
        summary = resp.choices[0].message.content.strip()
    except Exception as e:
        # If the LLM fails we still keep the payment – this is a trade‑off.
        # In a production system you would refund or retry.
        raise HTTPException(status_code=502, detail=f"LLM error: {e}")

    return {"summary": summary}
Enter fullscreen mode Exit fullscreen mode

4.4 Dockerfile

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV OPENAI_KEY=${OPENAI_KEY}
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
Enter fullscreen mode Exit fullscreen mode

5. Trade‑offs I encountered

Area Decision Cost / Benefit Honest drawback
LLM provider OpenAI gpt-4o-mini via API Low latency (~300 ms), good quality Ongoing per‑token cost; rate limits require back‑off or a fallback model.
Payment verification In‑memory nonce set Zero extra infrastructure overhead Nonces reset on restart → possible replay after a redeploy; for long‑running agents you need a persistent store (Redis, Postgres).
Escrow model Rely on the public x402 relayer on Base No contract deployment, gas ≈ 0.0005 USDC Dependence on a third‑party relayer; if it goes down the agent cannot verify payments.
Error handling Keep payment even if LLM fails Simpler code, guarantees revenue for the agent Consumers may feel cheated; a production system would implement a retry/refund flow.
Observability Simple print logs + SQLite audit log Easy to set up No distributed tracing; debugging latency spikes requires manual log correlation.
Deployment Single‑container VPS Cheap, easy to update No horizontal scaling; a burst of traffic will queue requests and increase latency.

These compromises kept the prototype under a few hundred lines of code while still exercising the full loop: request → payment verification → LLM call → response → settlement.


6. Testing & monitoring

  1. Unit tests for verify_x402_payment using a locally generated keypair.
  2. End‑to‑end test with curl that funds a temporary address on Base (via a faucet), builds an x402 header, hits /summarize, and checks that the USDC balance of the agent address increased by the expected amount (using web3.py to query the ERC‑20 contract).
  3. Load test: 10 req/s for 5 minutes showed average latency of 420 ms and 0.2 % error rate (mostly due to OpenAI rate‑limit 429 responses). Adding a simple exponential back‑off reduced errors to <0.05 %.

A lightweight Prometheus exporter (prometheus_client) exposed /metrics with counters for requests_total, payments_received_total, and `ll

Top comments (0)