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

An honest walk‑through of the architecture, the code, and the compromises you’ll hit when you try to turn a language model into a micro‑service that gets paid in USDC via the x402 protocol.


1. Why x402 and USDC on Base?

When you want an agent to earn money autonomously, you need two things:

  1. A verifiable way to charge for each invocation – the caller must prove they paid before the work starts.
  2. A settlement layer that is cheap, fast, and works 24/7 – you don’t want to be woken up by a failed on‑chain transaction at 3 am.

The x402 standard (defined in ERC‑4337‑compatible wallets) lets a service attach a payment requirement to an HTTP header (X402-Payment-Required). The client resolves the payment off‑chain with a paymaster, then forwards a signed receipt in X402-Payment. On Base, USDC is the de‑facto stablecoin for low‑value payments, and the gas cost of a simple ERC‑20 transfer is under $0.001 at current prices.

The trade‑off is that you now depend on a paymaster (or a relayer) that must be funded and online. If the paymaster goes down, legitimate callers can’t pay, and you lose revenue. In practice I run a minimal paymaster as a Cloudflare Worker that holds a small USDC buffer and forwards the signed receipt to the agent’s backend.


2. High‑Level Architecture

+-------------------+      HTTP (x402)      +---------------------+
|  Client / Agent   |  ----------------->  |  x402 Gateway (CF)  |
+-------------------+      +-------------->  +---------------------+
                         |                  |
                         |  Verify receipt  |  (paymaster logic)
                         v                  v
                +------------------+   +------------------+
                |  Agent Backend   |   |  USDC Vault (Base)|
                +------------------+   +------------------+
                         |
               +---------+---------+
               |  Task Queue (Redis)|
               +---------+---------+
                         |
               +---------+---------+
               |  Worker Pool (Python)|
               +---------+---------+
                         |
               +---------+---------+
               |  LLM Inference (local/remote)|
               +------------------------------+
Enter fullscreen mode Exit fullscreen mode
  • x402 Gateway – a thin Cloudflare Worker that checks the X402-Payment-Required header, validates the signed receipt against the paymaster’s public key, and forwards the request if the payment matches the price.
  • Agent Backend – a FastAPI service that authenticates the request (via the x402 middleware), enqueues a job ID, and returns a 202 Accepted with a polling URL.
  • Task Queue – Redis (or Cloudflare KV for a serverless variant) holds pending jobs.
  • Worker Pool – a set of async Python workers that pull jobs, call the LLM, store the result, and optionally trigger a USDC payout to the agent’s vault (the payout is just a bookkeeping step; the real money already moved when the client paid).
  • LLM Inference – I use a locally served Llama‑3‑8B via vLLM for cost control; you can swap to a remote API if you prefer.

The key point: the agent never touches USDC directly; the payment happens before the request even reaches your code. This eliminates re‑entrancy risks and simplifies compliance.


3. Implementing the x402 Middleware (Python)

Below is the minimal FastAPI dependency that checks the payment header. It assumes the paymaster has already verified the signature and inserted a X402-Paymaster-Approved header with the amount in USDC (scaled to 6 decimals, like USDC).

# file: x402_middleware.py
from fastapi import Request, HTTPException, status
from typing import Callable

USDC_DECIMALS = 6

async def x402_payment_required(
    request: Request,
    call_next: Callable,
    expected_price: int,  # price in smallest USDC units (e.g., 10_000 = $0.01)
):
    """
    FastAPI middleware / dependency that enforces an x402 payment.
    """
    # 1️⃣ Look for the paymaster approval header
    approved = request.headers.get("x402-paymaster-approved")
    if not approved:
        raise HTTPException(
            status_code=status.HTTP_402_PAYMENT_REQUIRED,
            headers={"X402-Payment-Required": f"{expected_price / 10**USDC_DECIMALS:.2f} USDC"},
            detail="Payment not yet made",
        )

    # 2️⃣ Parse the approved amount (should be a decimal string)
    try:
        approved_amount = int(float(approved) * 10**USDC_DECIMALS)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid X402-Paymaster-Approved header",
        )

    # 3️⃣ Compare against the price we expect
    if approved_amount < expected_price:
        raise HTTPException(
            status_code=status.HTTP_402_PAYMENT_REQUIRED,
            headers={"X402-Payment-Required": f"{expected_price / 10**USDC_DECIMALS:.2f} USDC"},
            detail=f"Insufficient payment: need {expected_price/10**USDC_DECIMALS:.2f} USDC, got {approved/10**USDC_DECIMALS:.2f}",
        )

    # 4️⃣ Payment OK – proceed
    response = await call_next(request)
    return response
Enter fullscreen mode Exit fullscreen mode

Usage in a route:

# file: main.py
from fastapi import FastAPI, Depends
from x402_middleware import x402_payment_required

app = FastAPI()

PRICE_USDC = 0.02  # $0.02 per call
PRICE_UNITS = int(PRICE_USDC * 10**USDC_DECIMALS)

@app.post("/generate")
async def generate(
    prompt: str,
    _: None = Depends(lambda req: x402_payment_required(req, PRICE_UNITS)),
):
    # Enqueue job (omitted for brevity)
    job_id = enqueue_job(prompt)
    return {"job_id": job_id, "status": "queued"}
Enter fullscreen mode Exit fullscreen mode

Trade‑offs:

  • The middleware is stateless – it trusts the paymaster’s header. If the paymaster is compromised, an attacker could forge approvals. In production you should verify the signature yourself using the paymaster’s public key (the example omits that for brevity).
  • Adding this layer adds ~1‑2 ms latency per request, negligible compared to LLM inference time.

4. The Worker – Pulling Jobs and Calling the LLM

The worker uses asyncio and redis-py to pop jobs from a list. I chose vLLM because it lets me serve a quantized Llama‑3‑8B model with ~12 GB VRAM and ~30 tokens/s on a cheap GPU instance (e.g., an AWS g4dn.xlarge).

# file: worker.py
import asyncio
import json
import redis
from typing import Any

from vllm import LLM, SamplingParams

REDIS_URL = "redis://localhost:6379/0"
QUEUE_KEY = "agent:jobs"
RESULT_PREFIX = "agent:result:"

llm = LLM(model="meta-llama/Llama-3-8B-Instruct", quantization="awq")
sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=256)

r = redis.from_url(REDIS_URL)

async def process_job(job_id: str, prompt: str) -> str:
    """Run the LLM and store the result."""
    output = llm.generate([prompt], sampling_params)[0].outputs[0].text
    r.set(f"{RESULT_PREFIX}{job_id}", json.dumps({"prompt": prompt, "output": output}))
    return output

async def worker_loop():
    while True:
        # BLPOP blocks until a job appears
        _, raw = r.blpop([QUEUE_KEY], timeout=0)
        job = json.loads(raw)
        job_id = job["id"]
        prompt = job["prompt"]
        try:
            await process_job(job_id, prompt)
            # Optionally notify the client via a webhook or polling endpoint
        except Exception as exc:
            # Log and move to a dead‑letter queue for inspection
            r.lpush("agent:failed", json.dumps({**job, "error": str(exc)}))

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

Honest notes on the worker:

  • Throughput ceiling: A single GPU can handle ~4‑6 concurrent requests before latency spikes. If you expect bursty traffic, you need a pool of workers or autoscaling (e.g., Kubernetes HPA).
  • Model staleness: The model is static; updating it requires a rolling restart. For agents that need fresh knowledge, you’d have to integrate a retrieval‑augmented generation (RAG) layer, which adds complexity and cost.
  • Cost vs. quality: Quantizing to AWQ saves memory but can degrade reasoning on edge cases. I accept a ~5% drop in accuracy for a 70% reduction in inference cost.

5. Exposing a Polling Endpoint for Clients

Clients

Top comments (0)