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 a language‑model‑powered agent to work for micropayments using the x402 protocol on Base.


1. Why an autonomous, paid agent?

When I first experimented with LLM agents I noticed two recurring pain points:

  1. Idle compute – the model spends most of its time waiting for a prompt.
  2. No direct revenue stream – most demos are free‑to‑use, which makes long‑running agents a cost center rather than a profit center.

The x402 specification solves the second problem by letting any HTTP endpoint charge a fixed amount of ERC‑20 tokens (in my case USDC) per request. If the agent can expose a useful service via HTTP and autonomously decide when to invoke it, the agent can earn while it “sleeps” (i.e., while it is waiting for the next scheduled task).

Below is a walkthrough of the minimal, production‑ish system I ran for a few weeks on a cheap VPS. The code is deliberately simple; you can swap components (e.g., replace Flask with FastAPI, or the LLM wrapper with your own) without changing the core payment flow.


2. High‑level architecture

+-------------------+      +-------------------+      +-------------------+
|   Scheduler (APS)| ---> |   Task Queue      | ---> |   Worker Process  |
+-------------------+      +-------------------+      +-------------------+
                                          |
                                          v
                                   +--------------+
                                   |  Agent Core  |
                                   | (LLM + tools)|
                                   +--------------+
                                          |
                                          v
                                   +--------------+
                                   |  x402 HTTP   |
                                   |  endpoint    |
                                   +--------------+
                                          |
                                          v
                                   +--------------+
                                   |  Wallet (USDC|
                                   |  on Base)    |
                                   +--------------+
Enter fullscreen mode Exit fullscreen mode
  1. Scheduler – APScheduler runs every 5 minutes and pushes a new task identifier onto a Redis list.
  2. Worker – A long‑lived Python process pops a task, instantiates the agent core, runs it, and returns the result.
  3. Agent Core – A thin LangChain wrapper that can call a few deterministic tools (e.g., fetch a URL, run a simple calculation). The LLM decides how to use those tools to satisfy the task.
  4. x402 HTTP endpoint – Exposes the agent’s run_task function as a POST /run. The x402 middleware attaches a payment request; the caller must pay the configured USDC amount before the request is forwarded to the agent.
  5. Wallet – A private key stored encrypted on the host (via eth-account + cryptography). The wallet holds enough USDC to cover the gas for refunds (if any) and to receive incoming payments.

The loop is fully autonomous: once the scheduler is started, the agent will keep pulling tasks, serving paid requests, and accumulating USDC in its wallet until you stop it.


3. Setting up the wallet and USDC on Base

Trade‑off: Using a hot wallet on a VPS is convenient but exposes the private key to anyone with shell access. For production you should move to a hardware signer or a managed KMS.

# wallet_setup.py
from eth_account import Account
from cryptography.fernet import Fernet
import os, json

# Generate a new key (do this once and store the encrypted version)
def create_wallet():
    acct = Account.create()
    # Encrypt the private key with a passphrase‑derived key
    fernet_key = Fernet.generate_key()
    f = Fernet(fernet_key)
    encrypted = f.encrypt(acct.key.hex().encode())
    # Store both the encrypted key and the fernet key (the latter should be
    # kept separate in a secret manager in real deployments)
    data = {
        "address": acct.address,
        "encrypted_key": encrypted.decode(),
        "fernet_key": fernet_key.decode(),
    }
    with open("wallet.json", "w") as f:
        json.dump(data, f, indent=2)
    print(f"Wallet address: {acct.address}")
    print("⚠️  Keep wallet.json and the fernet key safe!")

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

After you have a wallet, bridge some USDC to Base (via the official Base bridge or a DEX). The agent only needs enough to cover the incoming payment gas (≈ 0.0001 ETH per transaction on Base) and a small buffer for refunds.


4. x402 payment middleware

The official x402-python package provides a WSGI middleware that injects the payment headers and validates the payer’s signature. Below is a minimal Flask app that protects a single endpoint.

# app.py
from flask import Flask, request, jsonify
from x402.middleware import x402_middleware
from eth_account import Account
import json, os

app = Flask(__name__)

# Load the wallet created in the previous step
with open("wallet.json") as f:
    wallet_data = json.load(f)
wallet_address = wallet_data["address"]
encrypted_key = bytes.fromhex(wallet_data["encrypted_key"])
fernet_key = wallet_data["fernet_key"].encode()
private_key = Fernet(fernet_key).decrypt(encrypted_key).decode()
account = Account.from_key(private_key)

# x402 configuration: price in USDC (6 decimals) and the token contract on Base
USDC_ON_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  # USDC on Base (mainnet)
PRICE_USDC = 5_000_000   # $0.005 = 5 000 000 USDC (6 decimals)

# Wrap the Flask app with x402 middleware
app.wsgi_app = x402_middleware(
    app.wsgi_app,
    wallet_address=account.address,
    private_key=account.key,
    token_address=USDC_ON_BASE,
    price=PRICE_USDC,
    # Optional: set a timeout for the payment receipt (seconds)
    timeout=120,
)

@app.route("/run", methods=["POST"])
def run_task():
    """
    Expected JSON payload:
    {
        "task_id": "<string>",
        "prompt": "<string>"
    }
    Returns the agent's output as plain text.
    """
    data = request.get_json(force=True)
    task_id = data.get("task_id")
    prompt = data.get("prompt")
    if not task_id or not prompt:
        return jsonify({"error": "task_id and prompt required"}), 400

    # Delegate to the agent core (see next section)
    from agent_core import run_agent
    result = run_agent(prompt)
    return jsonify({"task_id": task_id, "output": result})

if __name__ == "__main__":
    # In production put this behind a reverse proxy (NGINX, Caddy, etc.)
    app.run(host="0.0.0.0", port=8080)
Enter fullscreen mode Exit fullscreen mode

What the middleware does

  1. Adds an x402 header to the response that contains a payment request (amount, token, payee address, and a nonce).
  2. On the next request, it verifies that the caller included a valid x402-payment header (a signed authorization) that matches the nonce and covers the price.
  3. If the payment is valid, the request is forwarded to the Flask view; otherwise a 402 Payment Required is returned.

Trade‑off: The middleware adds ~ 30‑50 ms of latency per request (signature verification). On Base this is negligible compared to the LLM inference time, but it becomes noticeable if you chain many x402‑protected calls.


5. The agent core

I used LangChain with OpenAI’s GPT‑4o mini (you can swap any LLM that supports the invoke interface). The agent has two tools:

  • fetch_url – performs a GET request and returns the first 2 000 characters of the response.
  • calculator – evaluates a simple arithmetic expression safely via Python’s ast.

python
# agent_core.py
import os
from langchain.agents import initialize_agent, Tool
from langchain.llms import OpenAI
import requests
import ast
import operator as op

# ---- Tools -------------------------------------------------
def fetch_url(url: str) -> str:
    try:
        r = requests.get(url, timeout=10)
        r.raise_for_status()
        return r.text[:2000]  # truncate to keep token usage low
    except Exception as e:
        return f"Error fetching {url}: {e}"

def calculator(expr: str) -> str:
    # Allow only literals and basic operators
    allowed_nodes = {ast.Num, ast.BinOp, ast.Add, ast
Enter fullscreen mode Exit fullscreen mode

Top comments (0)