How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with autonomous agents and want to understand the practical pieces—payment integration, task execution, deployment, and the trade‑offs involved.
1. Why an “earning while sleeping” agent?
The idea isn’t magic; it’s a simple loop:
- Expose a paid API endpoint (via the x402 micropayment spec).
- Wait for a caller to send USDC to unlock the endpoint.
- Run a deterministic piece of work (e.g., a small ML inference, data transformation, or validation).
- Return the result and keep the USDC that was paid.
If the work is cheap enough to execute on a low‑cost compute node and the price you set covers your infrastructure plus a modest margin, the agent can run unattended. The “while I sleep” part is just the agent staying online 24/7; there is no guarantee of profit, only the mechanics that make it possible.
2. High‑level architecture
+----------------+ USDC (Base) +-------------------+
| Caller (client) | <--- x402 payment ---> | Agent Service |
+----------------+ +-------------------+
^ |
| v
(HTTP request) +-----------------+
| Task Executor |
+-----------------+
- Caller – any HTTP client that knows the agent’s public URL and is willing to pay a tiny amount of USDC to invoke it.
- Agent Service – a thin HTTP front‑end that enforces the x402 payment, extracts the payload, and hands it to the executor.
- Task Executor – the actual AI/workload code (in my example a small sentiment‑analysis model).
All components run on a single cheap VPS or a serverless platform (I used Cloudflare Workers for the front‑end and a lightweight Python script on a $5/mo VPS for the executor). The split lets me keep the payment verification latency low (Workers edge) while still having access to a GPU/CPU for the model if needed.
3. Payment integration with x402
The x402 spec defines a simple HTTP header‑based workflow:
- Agent responds
402 Payment Requiredwith apaymentheader that contains a payment request (including the USDC contract address, amount, and a reference ID). - Caller pays the requested USDC amount onchain (Base) and includes the transaction hash in the
X-Payment-Txheader on the retry. - Agent verifies the transaction (confirmations, correct amount, correct recipient) and, if valid, processes the request.
Below is the minimal verification logic I used in Python (web3.py). It runs inside the Worker via a small WASM‑compiled version of web3, but the same code works on any Python runtime.
# payment_verifier.py
from web3 import Web3
import os
import time
# Base mainnet RPC (free tier from Infura/Alchemy works)
RPC_URL = os.getenv("BASE_RPC", "https://base-mainnet.g.alchemy.com/v2/<key>")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
USDC_ADDRESS = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") # USDC on Base
AGENT_WALLET = Web3.to_checksum_address(os.getenv("AGENT_WALLET")) # where we receive funds
REQUIRED_CONFIRMATIONS = 1
def verify_payment(tx_hash: str, expected_amount_wei: int) -> bool:
"""
Returns True if the transaction:
- exists,
- sent USDC to AGENT_WALLET,
- value == expected_amount_wei,
- has at least REQUIRED_CONFIRMATIONS confirmations.
"""
try:
tx = w3.eth.get_transaction(tx_hash)
if tx is None:
return False
# Check recipient and token (ERC20 transfer)
if tx["to"].lower() != USDC_ADDRESS.lower():
return False
# Decode ERC20 transfer data (simplified: assumes `transfer(address,uint256)`)
if tx["input"][:10] != "0xa9059cbb": # function selector for transfer
return False
# Extract `to` and `value` from calldata
recv = "0x" + tx["input"][34:74]
value_hex = tx["input"][74:]
value = int(value_hex, 16)
if recv.lower() != AGENT_WALLET.lower() or value != expected_amount_wei:
return False
# Confirmation check
latest = w3.eth.block_number
confirmations = latest - tx["blockNumber"] + 1
return confirmations >= REQUIRED_CONFIRMATIONS
except Exception:
return False
How the front‑end uses it (pseudo‑code for a Cloudflare Worker):
// worker.js
export default {
async fetch(request, env) {
const { pathname } = new URL(request.url);
if (pathname !== "/infer") {
return new Response("Not found", { status: 404 });
}
// 1️⃣ Check for payment header
const paymentReq = request.headers.get("X-Payment-Req");
if (!paymentReq) {
// No payment yet → ask for it
const amount = 5_000_000; // 0.005 USDC (6 decimals) → 5000 micro‑USDC
const headers = {
"Payment": JSON.stringify({
scheme: "exact",
network: "base",
asset: USDC_ADDRESS,
amount: amount.toString(),
description: "Sentiment inference",
}),
};
return new Response("Payment required", { status: 402, headers });
}
// 2️⃣ Parse payment request to get expected amount
const reqObj = JSON.parse(paymentReq);
const expectedAmount = BigInt(reqObj.amount); // still in USDC's smallest unit
// 3️⃣ Pull transaction hash from retry header
const txHash = request.headers.get("X-Payment-Tx");
if (!txHash) {
return new Response("Missing tx hash", { status: 400 });
}
// 4️⃣ Verify via a bound Python worker (or call an external verifier)
const verified = await env.VERIFIER.verify(txHash, expectedAmount.toString());
if (!verified) {
return new Response("Invalid or insufficient payment", { status: 402 });
}
// 5️⃣ At this point we have a valid payment → run the task
const payload = await request.json(); // { text: "..."}
const result = await env.EXECUTOR.fetch("/run", {
method: "POST",
body: JSON.stringify(payload),
});
return result;
},
};
Trade‑offs observed
| Aspect | What I chose | Why | Drawback |
|---|---|---|---|
| Payment verification | On‑chain verification via a lightweight Python script (called from the Worker using a sub‑request). | Guarantees trustlessness; no need to rely on a third‑party escrow. | Adds ~200‑400 ms latency (depends on RPC). |
| Amount granularity | Micro‑USDC (6 decimals) → minimum price 0.001 USDC. | Keeps the barrier low for callers while still covering compute cost. | If gas spikes on Base, the net profit can become negative; you must monitor gas price. |
| Front‑end platform | Cloudflare Workers (edge). | Zero‑maintenance, automatic TLS, low cold‑start latency. | Workers have a 50 ms CPU limit; heavy model inference must be off‑loaded. |
| Executor | Tiny VPS running a Python script with a distilled sentiment model (≈5 MB). | Cheap, always‑on, GPU not needed for this workload. | If you need a larger model you’ll need a more expensive instance or GPU. |
4. The actual workload: a sentiment‑analysis micro‑service
I picked a straightforward NLP task because it’s easy to quantify cost vs. value:
- Input: a JSON object
{ "text": "string" }. - Output:
{ "label": "positive|negative|neutral", "score": float }. - Model: a DistilBERT‑base distilled to ~5 MB, running on CPU with
transformers+torch. Inference ≈ 12 ms on a modest vCPU.
The executor endpoint (/run) is a simple Flask app:
python
# executor/app.py
from flask import Flask, request, jsonify
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
app = Flask(__name__)
MODEL_NAME = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
model.eval()
LABEL_MAP = {0: "NEGATIVE", 1: "POSITIVE"}
@app.route("/
Top comments (0)