How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put an LLM‑based service behind a micro‑payment flow and run it unattended.
1. Why bother with payments?
When an AI agent is exposed as a public HTTP endpoint, every request consumes compute (GPU/CPU time) and, if you’re using a hosted model, API credits. If you don’t gate access, a stray bot or a curious user can drain your budget in minutes.
The x402 protocol solves this by turning each request into a pre‑paid transaction: the client must attach a valid USDC payment before the server processes the payload. The agent only spends resources on calls that have already been funded, turning idle time into revenue.
2. High‑level architecture
+-------------------+ +---------------------+ +-------------------+
| Client (any) | HTTPS | Edge Worker (CF) | HTTPS | Autonomous Agent |
| (curl, Postman) |<------>| (x402 verifier) |<------>| (FastAPI + LLM) |
+-------------------+ +---------------------+ +-------------------+
^ ^ |
| | v
| | +-------------------+
| | | USDC Wallet (Base)|
| | +-------------------+
| |
| +----> Payment escrow contract (x402)
|
+----> Monitoring / alerting (Prometheus + Alertmanager)
-
Edge Worker – a lightweight Cloudflare Workers script that checks the
x402-Paymentheader, validates the signature against the escrow contract, and forwards the request only if payment is sufficient. - Autonomous Agent – a FastAPI app that holds the model, performs inference, logs usage, and periodically sweeps earned USDC to a personal wallet.
- USDC on Base – chosen for low gas (~$0.0001 per tx) and fast finality.
The agent never holds private keys; the escrow contract does. The worker only needs the contract address and the public key of the receiver to verify signatures.
3. Setting up the escrow contract
You don’t need to write Solidity from scratch; the x402 reference implementation provides a minimal ERC‑20 escrow. Deploy it once on Base (via Remix or Hardhat) and note the address.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract X402Escrow {
IERC20 public usdc;
address public payee;
constructor(address _usdc, address _payee) {
usdc = IERC20(_usdc);
payee = _payee;
}
// Called by the worker to pull funds after verification
function claim(uint256 amount) external {
require(msg.sender == address(this), "Only escrow can call");
require(usdc.transferFrom(payee, msg.sender, amount), "Transfer failed");
}
// Allow the worker to query allowance without spending gas
function allowanceOf(address spender) external view returns (uint256) {
return usdc.allowance(payee, spender);
}
}
Trade‑off: The escrow adds an extra transaction per claim (≈0.0001 USDC on Base). If you expect sub‑cent calls, you may batch claims (e.g., every 100 requests) to save gas, at the cost of slightly delayed revenue visibility.
4. Edge Worker – payment verification
Below is a minimal Cloudflare Workers script (JavaScript) that validates the x402-Payment header. It uses the ethers library (bundled via workers-compat) to verify the signature against the escrow contract’s public key.
// worker.js
import { ethers } from "ethers";
const ESCROW_ADDRESS = "0xYourEscrowAddress";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const PAYEE = "0xYourReceiverWallet";
const escrowAbi = [
"function allowanceOf(address spender) view returns (uint256)",
"function claim(uint256 amount)"
];
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const paymentHeader = request.headers.get("x402-Payment");
if (!paymentHeader) {
return new Response("Missing x402-Payment header", { status => 402 });
}
// Header format: "signature:value:timestamp"
const [sig, valueStr, timestampStr] = paymentHeader.split(":");
const value = BigInt(valueStr);
const timestamp = Number(timestampStr);
// Replay protection: reject stale timestamps (>5 min)
if (Date.now() / 1000 - timestamp > 300) {
return new Response("Stamp too old", { status => 402 });
}
const provider = new ethers.JsonRpcProvider("https://base.mainnet.rpc");
const escrow = new ethers.Contract(ESCROW_ADDRESS, escrowAbi, provider);
// Recover signer from signature over (value, timestamp, request body hash)
const body = await request.arrayBuffer();
const bodyHash = ethers.keccak256(body);
const msgHash = ethers.solidityPackedKeccak256(
["uint256", "uint256", "bytes32"],
[value, timestamp, bodyHash]
);
const signer = ethers.recoverAddress(msgHash, sig);
// Check allowance
const allowance = await escrow.allowanceOf(signer);
if (allowance < value) {
return new Response("Insufficient funds", { status => 402 });
}
// Forward request to your agent
const agentResp = await fetch("https://agent.example.com/infer", {
method: request.method,
headers: request.headers,
body: request.body
});
// After successful forwarding, claim the payment (fire‑and‑forget)
escrow.claim(value).catch(console.error);
return agentResp;
}
Why a worker?
- Zero‑server‑ops: Cloudflare handles scaling, TLS, and DDoS mitigation.
- Latency: ~20 ms added overhead vs. a full VM.
- Limitation: You cannot run heavy inference here; the worker is strictly a gatekeeper.
5. The autonomous agent (FastAPI + model)
I used a modest 7B parameter LLM quantized to 4‑bit (llama.cpp) running on a cheap GPU VM (e.g., AWS g4dn.xlarge). The agent exposes a single /infer endpoint that expects a JSON payload { "prompt": "..." } and returns { "completion": "..." }.
python
# agent.py
import os
import time
import json
import logging
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
from llama_cpp import Llama
app = FastAPI()
log = logging.getLogger("uvicorn.error")
# ---- Model loading (once at startup) ----
MODEL_PATH = os.getenv("MODEL_PATH", "/models/llama-7b-q4.gguf")
llama = Llama(
model_path=MODEL_PATH,
n_ctx=2048,
n_threads=6,
n_gpu_layers=35, # offload most layers to GPU
verbose=False
)
# ---- Simple usage meter (for debugging) ----
REQUEST_COUNT = 0
LAST_SWEEP = time.time()
SWEEP_INTERVAL = 3600 # sweep earnings hourly
@app.post("/infer")
async def infer(req: Request):
global REQUEST_COUNT
DATA = await req.json()
prompt = DATA.get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="Prompt required")
start = time.time()
output = llama(
prompt,
max_tokens=128,
temperature=0.7,
top_p=0.95,
stop=["\n\n"],
echo=False,
)
latency = time.time() - start
REQUEST_COUNT += 1
log.info(f"Request {REQUEST_COUNT} served in {latency:.2f}s")
return JSONResponse({"completion": output["choices"][0]["text"]})
# ---- Background task to sweep USDC from escrow ----
from web3 import Web3
import asyncio
w3 = Web3(Web3.HTTPProvider("https://base.mainnet.rpc"))
ESCROW = w3.eth.contract(
address=Web3.to_checksum_address(os.getenv("ESCROW_ADDRESS")),
abi=[{
"constant": False,
"inputs": [{"name": "amount", "type": "uint256"}],
"name": "claim",
"outputs": [],
"type": "function"
}]
)
ACCOUNT = w3
Top comments (0)