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

By a developer who prefers concrete trade‑offs over buzzwords


TL;DR

I wired a simple LLM‑driven worker to a micro‑service marketplace that pays in USDC on Base via the x402 protocol. The agent continuously polls for available tasks, runs a deterministic inference loop, signs the result with a wallet key, and submits a payment request. The whole thing runs on a cheap VPS (≈ $5/mo) and has been earning a few cents per hour for weeks without manual intervention. Below is the architecture, the key code pieces, and the honest trade‑offs I ran into.


1. Why an “earning” agent?

Most autonomous‑agent tutorials stop at “it can answer questions.” If you want the agent to sustain itself financially, you need three moving parts:

  1. A source of paid work – a marketplace that pays per invocation.
  2. A deterministic way to prove work was done – so the payer can verify before releasing funds.
  3. A custody layer – a wallet that can receive USDC and sign payment claims.

I chose the emerging x402 standard (HTTP 402 Payment Required) because it lets a service reject unauthenticated requests with a payment invoice, and the client can settle the invoice on‑chain before retrying. The Base layer provides cheap USDC transfers (~$0.0001 per tx).


2. High‑level Architecture

+----------------+      x402 invoice (USDC)     +-------------------+
|  Task Queue    | <---------------------------> |  x402‑enabled API |
| (Redis/PubSub) |                               |  (Cloudflare Worker)|
+----------------+                               +-------------------+
        ^                                                 |
        |  poll for new task (JSON)                       |  signed result + claim
        |                                                 v
+----------------+                                   +------------------+
|  Agent Core    |                                   |  Wallet (EOA)   |
|  (Python async)│                                   |  (private key) │
+----------------+                                   +------------------+
Enter fullscreen mode Exit fullscreen mode
  • Task Queue – a simple Redis list (LPUSH/BRPOP) that holds JSON payloads describing a unit of work (e.g., “summarize this article”, “classify this image”).
  • Agent Core – an async Python process that blocks on BRPOP, runs the inference, builds a receipt, signs it with EIP‑191, and POSTs the signed receipt to the worker’s /claim endpoint.
  • x402‑enabled API – a Cloudflare Worker that enforces the 402 response, validates the signature, forwards the request to a cheap inference backend (here, a local Ollama model), and finally mints the USDC claim via the x402 library.
  • Wallet – an Ethereum‑compatible EOA (private key stored as an environment variable) that holds a small USDC balance on Base for gas and receives the payout.

All components are deliberately stateless except for the Redis queue, which makes horizontal scaling trivial (just add more agent instances).


3. Choosing the Tech Stack

Component Choice Reason Trade‑off
Language Python 3.11 + asyncio Rich LLM ecosystem (llama‑cpp, transformers) and easy Redis bindings Slightly slower than Rust/Go for high‑frequency polling, but latency is dominated by model inference anyway
Queue Redis 6 (managed) Atomic BRPOP, pub/sub for monitoring, cheap Requires a running Redis instance; if it dies the agent stalls
Worker Cloudflare Workers (JS) Global edge, zero‑config TLS, built‑in fetch for outgoing HTTP, easy to embed x402‑ts Limited execution time (50 ms CPU) – we offload heavy lifting to the inference backend
Inference Ollama running a 7B GGUF model on the same VPS No API‑key fees, deterministic output given a seed, easy to CPU‑bound Model quality lower than hosted APIs; we trade accuracy for cost‑control
Wallet ethers.js (via worker) + web3.py (agent) Standard signing, compatible with Base Private key must be injected via env; never commit it to repo

4. Agent Core – Working Snippets

Below is the core loop (agent.py). It assumes you have a Redis instance at REDIS_URL and a wallet private key in WALLET_KEY. The agent signs an EIP‑191 message: "<taskId>|<resultHash>".

# agent.py
import os
import asyncio
import json
import hashlib
import redis
from eth_account.messages import encode_defunct
from eth_account import Account
import aiohttp

REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
WALLET_KEY = os.getenv("WALLET_KEY")          # 0x-prefixed hex
AGENT_WALLET = Account.from_key(WALLET_KEY)
API_ENDPOINT = os.getenv("API_ENDPOINT", "https://nexusai-x402.nikhilranka23.workers.dev/summarize")

r = redis.from_url(REDIS_URL)

def sign_payload(task_id: str, result: str) -> str:
    """Return hex signature of taskId|sha256(result)."""
    h = hashlib.sha256(result.encode()).hexdigest()
    message = f"{task_id}|{h}"
    encoded = encode_defunct(text=message)
    signed = Account.sign_message(encoded, private_key=WALLET_KEY)
    return signed.signature.hex()

async def fetch_task() -> dict:
    """Blocking pop with timeout; returns {} if none."""
    # BRPOP blocks; we run it in a thread to keep the loop async.
    _, payload = r.brpop("task_queue", timeout=30)
    if payload:
        return json.loads(payload)
    return {}

async def submit_result(task_id: str, result: str, signature: str):
    payload = {
        "taskId": task_id,
        "result": result,
        "signature": signature,
        "wallet": AGENT_WALLET.address,
    }
    async with aiohttp.ClientSession() as session:
        async with session.post(f"{API_ENDPOINT}/claim", json=payload) as resp:
            if resp.status != 200:
                txt = await resp.text()
                raise RuntimeError(f"Claim failed {resp.status}: {txt}")
            return await resp.json()

async def worker():
    while True:
        task = await fetch_task()
        if not task:
            # No work – sleep a bit to avoid tight loop.
            await asyncio.sleep(5)
            continue

        task_id = task["id"]
        prompt = task["prompt"]

        # ---- deterministic inference ----
        # Using Ollama's HTTP API with a fixed seed makes output reproducible.
        ollama_payload = {
            "model": "llama3:8b-instruct-q4_K_M",
            "prompt": prompt,
            "stream": False,
            "options": {"seed": 42, "temperature": 0.0},
        }
        async with aiohttp.ClientSession() as session:
            async with session.post("http://localhost:11434/api/generate",
                                    json=ollama_payload) as resp:
                data = await resp.json()
                result = data["response"].strip()

        signature = sign_payload(task_id, result)
        try:
            receipt = await submit_result(task_id, result, signature)
            print(f"[{task_id}] Paid {receipt.get('amount')} USDC")
        except Exception as e:
            # In a production agent you’d push to a DLQ or alert.
            print(f"[{task_id}] Error: {e}")

if __name__ == "__main__":
    asyncio.run(worker())
Enter fullscreen mode Exit fullscreen mode

What this does

  1. Blocks on Redis until a task appears.
  2. Calls a local Ollama model with a fixed seed (seed: 42) and temperature: 0.0 → deterministic output for the same prompt. This lets the verifier recompute the hash and trust the signature.
  3. Signs the concatenation taskId|sha256(result).
  4. POSTs the signed receipt to the worker’s /claim endpoint.

Honest notes on the snippet

  • Determinism is fragile – if you ever upgrade the model or change quantization, the hash will diverge and claims will fail. I mitigate this by pinning the exact Docker image (ollama/ollama:0.1.34) and model file.
  • Error handling is minimal – for a demo it’s okay; in production you’d add retries, dead‑letter queues, and monitoring (Prometheus + Grafana).
  • Gas costs – the claim transaction on Base costs roughly $0.0001 in ETH (covered by the agent’s wallet). The USDC payout is separate and defined by the API owner.

5. The x402‑Enabled Worker (Cloudflare Workers)

The worker is ~120 lines of TypeScript using the @x402/hyperdrive helper. It does three things:

  1. Responds with 402 if no valid X-Payment header is present.
  2. Verifies the EIP‑191 signature against the provided wallet address.
  3. Forwards the request to the local inference endpoint (via fetch to http://127.0.0.1:11434/api/generate) only after payment is confirmed.

ts
// src/index.ts
import { Hyperdrive } from
Enter fullscreen mode Exit fullscreen mode

Top comments (0)