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

A practical walkthrough for developers who want to turn a language‑model‑powered service into a self‑sustaining micro‑business.


1. Why an “earning‑while‑you‑sleep” agent?

The idea isn’t magic; it’s simply a stateless HTTP service that charges a tiny fee for each invocation. If the service is useful enough to be called repeatedly—say, to summarize news articles, extract entities from logs, or translate short snippets—then the aggregate of many micro‑payments can cover the host cost and leave a small surplus in USDC.

The biggest win is predictability: you know the exact price per call, you can monitor usage, and you can stop the service when it’s no longer profitable. The downside is that you’re now responsible for payment handling, blockchain interaction, and the reliability of a distributed system.


2. High‑level architecture

+----------------+      HTTP (POST)      +-------------------+
|  Client App    | ---------------------> |  Autonomous Agent |
+----------------+                        (FastAPI + Uvicorn) 
                                            |  LLM Wrapper      |
                                            |  (OpenAI/Mistral) |
                                            +----------+--------+
                                                       |
                                               USDC Payment
                                               (x402 middleware)
                                                       |
                                               +--------v--------+
                                               |  Base Wallet    |
                                               |  (private key)  |
                                               +-----------------+
Enter fullscreen mode Exit fullscreen mode
  • Client – any program that can POST JSON and attach an x402 payment header.
  • Agent – a thin FastAPI service that validates the payment, forwards the request to an LLM, and returns the result.
  • Payment layer – the x402 spec (HTTP 402 Payment Required) implemented as a Starlette middleware. It checks for a valid USDC transfer on Base, credits the agent’s wallet, and only then lets the request proceed.
  • Wallet – a simple eth‑account (Ethereum-compatible) holding USDC on Base. The private key lives in an environment variable; never commit it.

3. The LLM wrapper – keep it interchangeable

I started with the OpenAI chat completion API because it’s well‑documented, but the wrapper is deliberately abstract so you can swap in a local model (e.g., Llama‑3 via vLLM) or another provider with minimal changes.

# llm.py
import os
from typing import Dict, Any
import httpx

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL   = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

async def complete(prompt: str, temperature: float = 0.2) -> str:
    """Call OpenAI chat completions and return the assistant's text."""
    url = "https://api.openai.com/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {OPENAI_API_KEY}",
        "Content-Type": "application/json",
    }
    payload: Dict[str, Any] = {
        "model": OPENAI_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": temperature,
        "max_tokens": 512,
    }

    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.post(url, json=payload, headers=headers)
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["message"]["content"].strip()
Enter fullscreen mode Exit fullscreen mode

Trade‑off: Using a hosted LLM removes the need for GPU management, but you incur per‑token cost and latency (≈200‑400 ms on average). If you run your own model you cut the API fee but add hardware cost, cold‑start latency, and maintenance overhead.


4. x402 payment middleware

The x402 spec defines a Payment-Required HTTP status (402) and a set of headers that describe how to pay. For USDC on Base we follow the reference implementation from the x402-py library, but I stripped it down to the essentials to avoid pulling in a heavy dependency chain.

# payment.py
import os
from eth_account import Account
from web3 import Web3
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from starlette.status import HTTP_402_PAYMENT_REQUIRED

BASE_RPC   = os.getenv("BASE_RPC", "https://mainnet.base.org")
USDC_ADDR  = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")  # USDC on Base
AGENT_WALLET = Account.from_key(os.getenv("AGENT_PRIVATE_KEY"))  # never hard‑code
W3 = Web3(Web3.HTTPProvider(BASE_RPC))

class X402Middleware(BaseHTTPMiddleware):
    """
    Expects the client to include:
      - X-Payment-Token: signed erc20 approval (see x402 spec)
      - X-Payment-Address: the payer's Ethereum address
    The middleware verifies the token, checks that enough USDC was transferred
    to the agent's wallet, and then allows the request to continue.
    """
    PRICE_USDC = int(os.getenv("PRICE_USDC_CENTS", "1")) * 1_000_000  # 1 USDC = 1e6 wei (6 decimals)

    async def dispatch(self, request: Request, call_next):
        # Skip middleware for health checks
        if request.url.path == "/healthz":
            return await call_next(request)

        payer = request.headers.get("X-Payment-Address")
        token = request.headers.get("X-Payment-Token")
        if not payer or not token:
            return JSONResponse(
                {"error": "Missing payment headers"},
                status_code=HTTP_402_PAYMENT_REQUIRED,
                headers={
                    "WWW-Authenticate": f'Bearer realm="x402", '
                    f'token_uri="https://example.com/x402/token", '
                    f'price="{self.PRICE_USDC}", '
                    f'currency="USDC", '
                    f'network="base"'
                },
            )

        # Verify the token (simplified: recover signer from signed message)
        try:
            # The token is a signature of the string f"{payer}:{request.method}:{request.url.path}"
            message = f"{payer}:{request.method}:{request.url.path}"
            recovered = Account.recover_message(text=message, signature=token)
            if recovered.lower() != payer.lower():
                raise ValueError("Signature mismatch")
        except Exception as exc:
            return JSONResponse(
                {"error": f"Invalid payment token: {exc}"},
                status_code=HTTP_402_PAYMENT_REQUIRED,
            )

        # Check that payer has sent at least PRICE_USDC to our wallet
        # In a production system you would watch an event or use a indexing service.
        # Here we just query the USDC balance difference since last check.
        usdc_contract = W3.eth.contract(address=USDC_ADDR, abi=ERC20_ABI)
        balance = usdc_contract.functions.balanceOf(AGENT_WALLET.address).call()
        if balance < self.PRICE_USDC:
            return JSONResponse(
                {"error": "Insufficient funds paid"},
                status_code=HTTP_402_PAYMENT_REQUIRED,
                headers={"Pay": f"{AGENT_WALLET.address}:{self.PRICE_USDC}"},
            )

        # Deduct the price from our internal ledger (optional)
        # For simplicity we just trust the on‑chain balance; in practice you’d
        # subtract the amount and update a local counter to avoid double‑charging.
        return await call_next(request)
Enter fullscreen mode Exit fullscreen mode

ERC20 ABI snippet (only balanceOf needed):

ERC20_ABI = [
    {"constant":True,"inputs":[{"name":"_owner","type":"address"}],
     "name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],
     "type":"function"},
]
Enter fullscreen mode Exit fullscreen mode

Trade‑offs:

  • Security – The middleware trusts the client’s signature; a compromised private key lets anyone spoof payments. Rotate the agent key frequently and consider using a hardware signer or a managed wallet service.
  • Cost – Each request triggers a read‑only contract call (balanceOf). On Base this costs a fraction of a cent, but if you scale to thousands of RPS you’ll want to cache the balance or use an indexing service (e.g., The Graph) to avoid RPC overload.
  • User experience – Clients must obtain USDC, sign the payload, and attach the headers. For a developer‑oriented API this is acceptable; for end‑users you’d need a SDK or a front‑end that abstracts the payment flow.

5. The agent endpoint

With the middleware in place, the core route is trivial: validate JSON, call the LLM, return the result

Top comments (0)