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 long‑running LLM‑driven agents and want to understand the practical plumbing—not the marketing fluff.


1. Why “earn while I sleep” is a mis‑nomer

The phrase suggests passive income, but an autonomous agent still consumes compute, network, and‑most importantly‑trust. In my prototype the agent:

  • Runs on a cheap VPS (2 vCPU, 4 GB RAM) that incurs a fixed hourly cost.
  • Pays for every external call it makes (LLM inference, blockchain reads/writes, third‑party APIs).
  • Earns only when a user voluntarily pays for a service the agent provides (via the x402 protocol).

If the agent is idle, it burns money; if it’s over‑aggressive, it can drain its USDC balance faster than it earns. The goal, therefore, is to balance expected revenue against operational cost and to fail‑gracefully when the balance drops below a safety threshold.


2. High‑level architecture

+-------------------+      +-------------------+      +-------------------+
|  Scheduler (cron) | ---> |  Agent Core (ASGI) | ---> |  x402 Payment SDK |
+-------------------+      +-------------------+      +-------------------+
        ^                         |                         |
        |                         v                         v
+-------------------+      +-------------------+      +-------------------+
|  Config & Secrets |      |  LLM Wrapper      |      |  Base Wallet (USDC)|
+-------------------+      +-------------------+      +-------------------+
Enter fullscreen mode Exit fullscreen mode
  • Scheduler – a simple cron entry (*/5 * * * *) that hits the agent’s health endpoint every five minutes. If the agent is down, the scheduler restarts the Docker container.
  • Agent Core – an ASGI app (FastAPI) that exposes a single /run endpoint. The endpoint receives a JSON payload describing a task, runs the LLM, optionally calls external tools, and returns a result.
  • x402 Payment SDK – wraps the outgoing HTTP response with a payment request header (X-Payment-Required: ...). The client must attach a valid USDC payment (on Base) before the server processes the request.
  • LLM Wrapper – a thin abstraction over a hosted inference API (e.g., Together.ai or a self‑hosted Llama‑3‑8B via vLLM). It handles retries, token‑budget enforcement, and logging.
  • Wallet – a deterministic HD wallet (derived from a mnemonic stored as a Docker secret) that holds USDC on Base. The agent only ever signs x402 payment responses; it never initiates outgoing transfers.

3. Payment flow with x402

The x402 spec turns HTTP 402 responses into a pay‑per‑call mechanism. When a client calls /run without a valid payment, the agent returns:

HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Payment-Required: {"network":"base","currency":"usdc","amount":"0.05","dest":"0xAbc...","maxFee":"0.001"}
Enter fullscreen mode Exit fullscreen mode
{
  "error": "payment required",
  "payment": {
    "network": "base",
    "currency": "usdc",
    "amount": "0.05",
    "dest": "0xAbc...",
    "maxFee": "0.001"
  }
}
Enter fullscreen mode Exit fullscreen mode

The client must then:

  1. Submit a USDC transfer (via its own wallet) to dest for the exact amount.
  2. Include the transaction hash in the X-Payment-Tx header on a retry.

The agent verifies the transaction on‑chain (using a lightweight JSON‑RPC call to a Base RPC endpoint) before proceeding. If verification fails, it returns another 402 with an updated maxFee to incentivize a higher fee.


4. Core code snippets

Below are the parts that actually make the loop work. I kept them deliberately minimal; production code would add more validation, metrics, and circuit‑breakers.

4.1. FastAPI entry point

# agent/main.py
import os
from fastapi import FastAPI, Request, Header, HTTPException
from x402 import verify_payment, PaymentRequired
from llm_wrapper import generate
from wallet import get_address

app = FastAPI()
AGENT_ADDRESS = get_address()  # deterministic from mnemonic secret

@app.post("/run")
async def run_task(
    request: Request,
    x_payment_tx: str | None = Header(default=None),
):
    try:
        # Verify that a valid payment was attached
        verify_payment(
            request=request,
            tx_hash=x_payment_tx,
            agent_address=AGENT_ADDRESS,
            expected_currency="usdc",
            # The amount is dynamic; we infer it from the request body later.
        )
    except PaymentRequired as prec:
        # We don't know the price yet—let the LLM wrapper tell us.
        body = await request.json()
        suggested_price = estimate_price(body)  # ← see §4.2
        raise PaymentRequired(
            amount=suggested_price,
            network="base",
            currency="usdc",
            dest=AGENT_ADDRESS,
            maxFee="0.001",
        ) from prec

    # Payment OK – process the task
    body = await request.json()
    result = await generate(body["prompt"])
    return {"output": result}
Enter fullscreen mode Exit fullscreen mode

4.2. Estimating price from the prompt

# agent/pricing.py
BASE_COST = 0.01  # USDC per 1k tokens (LLM inference)
TOKEN_ESTIMATE = 4  # rough chars‑to‑token ratio

def estimate_price(payload: dict) -> str:
    prompt = payload.get("prompt", "")
    tokens = max(1, len(prompt) // TOKEN_ESTIMATE)
    cost = BASE_COST * (tokens / 1000)
    # Round up to the nearest cent to avoid micropayment dust
    return f"{max(0.01, round(cost, 2)):.2f}"
Enter fullscreen mode Exit fullscreen mode

4.3. Payment verification (x402 SDK wrapper)

# agent/x402.py
import json
import requests
from eth_utils import is_checksum_address

BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.trackit.io")

def verify_payment(
    *,
    request: Request,
    tx_hash: str | None,
    agent_address: str,
    expected_currency: str,
    amount: str | None = None,
) -> None:
    if not tx_hash:
        raise PaymentRequired(...)  # will be raised by caller

    # 1️⃣ Fetch transaction receipt
    payload = {
        "jsonrpc": "2.0",
        "method": "eth_getTransactionReceipt",
        "params": [tx_hash],
        "id": 1,
    }
    resp = requests.post(BASE_RPC, json=payload, timeout=5)
    resp.raise_for_status()
    data = resp.json()
    receipt = data.get("result")
    if not receipt or receipt["status"] != "0x1":
        raise PaymentRequired(detail="tx not successful")

    # 2️⃣ Check that it pays the agent the right amount & currency
    if receipt["to"].lower() != agent_address.lower():
        raise PaymentRequired(detail="wrong destination")
    # For USDC on Base, the contract address is known; we skip token‑specific checks here
    # (a production version would call the USDC contract's balanceOf and decimals).

    # 3️⃣ If an explicit amount was expected, compare
    if amount:
        # value is in wei (18 decimals); USDC uses 6 decimals → convert
        value_wei = int(receipt["effectiveGasPrice"], 16) * int(receipt["gasUsed"], 16)
        # Simplistic: we just ensure the transaction sent *some* value; exact amount check omitted for brevity
        pass
Enter fullscreen mode Exit fullscreen mode

Note: The snippet above deliberately omits the full ERC‑20 verification logic to keep the example readable. In a real deployment you would call the USDC contract (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 on Base) and confirm that the transferred amount matches the requested amount (adjusted for 6 decimals).

4.4. LLM wrapper with budgeting

# agent/llm_wrapper.py
import openai  # or any compatible client
from tenacity import retry, stop_after_attempt, wait_exponential

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MAX_TOKENS_PER_CALL = 1500   # safety net to avoid runaway costs

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def generate(prompt: str) -> str:
    response = await openai.ChatCompletion.acreate(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=MAX_TOKENS_PER_CALL,
        temperature=0.2,
    )
    return response.choices[0].message["content"].strip()
Enter fullscreen mode Exit fullscreen mode

5. Operational trade‑offs

Aspect Decision Reasoning Downside
Host $5/m

Top comments (0)