How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who are experimenting with autonomous agents, on‑chain micropayments, and the x402 standard.
1. Why an autonomous earning agent?
The goal is not to create a “money‑making bot” but to explore how an agent can provide a useful service, receive verifiable payment, and operate without human intervention. The architecture below shows the moving parts that make this possible:
| Component | Responsibility | Typical tech |
|---|---|---|
| Task scheduler | Triggers the agent at regular intervals or on external events | Cron, Cloud Scheduler, or a lightweight worker |
| Agent core | Performs the useful work (e.g., data enrichment, summarisation, simple classification) | Python + LangChain / LlamaIndex, or a custom LLM wrapper |
| Payment broker | Handles x402 invoicing, validates USDC payments on Base, and returns a receipt | x402‑enabled Worker (Cloudflare Workers) or a minimal Express service |
| State store | Persists nonce, invoice ID, and outcome for audit/retry | SQLite, Redis, or PostgreSQL |
| Monitoring & alerts | Detects failures, insufficient funds, or gas spikes | Prometheus + Alertmanager, or simple health‑check endpoint |
The agent never holds private keys; it only signs x402 invoices with a pre‑approved spending limit that the broker enforces on‑chain. This limits exposure if the agent is compromised.
2. Defining the service
For the prototype I chose a text‑summarisation micro‑service: given a URL or raw text, return a 2‑sentence summary. The task is easy to quantify (input length → output length) and cheap enough to price in sub‑cent USDC.
2.1 Pricing model
- Base price: $0.005 per 1000 input characters (≈ $0.005 per request for typical web articles).
- Minimum charge: $0.01 (to cover transaction overhead).
- Maximum charge: $0.10 (protects against pathological inputs).
Pricing is expressed in the x402 invoice as a USDC amount with 6‑decimal precision (USDC has 6 decimals on Base).
2.2 x402 flow recap
- Agent receives a request → creates an x402 invoice (includes amount, token address, chain ID, expiry).
- Agent returns the invoice to the caller (usually another agent or a frontend).
- Caller pays the invoice via their wallet; the payment is verified on‑chain by the broker.
- Broker forwards the request to the agent’s worker, which executes the task and returns the result.
Because the broker is the only entity that touches USDC, the agent can stay off‑chain and stateless.
3. Implementation details
Below are the key code snippets. They are deliberately minimal; production systems would add proper logging, retries, and circuit‑breakers.
3.1 Agent worker (Python, Flask)
# agent_worker.py
import os
from flask import Flask, request, jsonify
from transformers import pipeline
app = Flask(__name__)
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
@app.route("/summarize", methods=["POST"])
def summarize():
# Expect JSON: {"text": "..."} or {"url": "http://..."}
data = request.get_json()
if "text" in data:
raw = data["text"]
elif "url" in data:
import requests
raw = requests.get(data["url"], timeout=5).text[:4000] # safety trim
else:
return jsonify({"error": "missing text or url"}), 400
# Truncate to model limits
raw = raw[:1024]
summary = summarizer(raw, max_length=120, min_length=30, do_sample=False)[0]["summary_text"]
return jsonify({"summary": summary})
Trade‑offs
-
Model choice: BART‑large‑cnn gives decent quality for news‑style text but adds ~300 ms latency on a modest CPU. Swapping to a distilled model (e.g.,
sshleifer/distilbart-cnn-12-6) cuts latency to ~80 ms at a small quality drop. - Input size: Truncating to 1024 characters protects against OOM but may lose context for very long documents. A sliding‑window approach could improve fidelity at the cost of extra compute.
3.2 x402 invoice generator (Python)
# x402_invoice.py
import time, json, hashlib
from eth_account.messages import encode_defunct
from eth_account import Account
USDC_ADDRESS_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
CHAIN_ID = 8453
def create_invoice(amount_usdc: float, payer: str, nonce: int = None):
"""
amount_usdc: e.g., 0.01 (USDC with 6 decimals)
payer: Ethereum address that will pay
nonce: optional, defaults to current unix seconds
"""
if nonce is None:
nonce = int(time.time())
amount_wei = int(round(amount_usdc * 1_000_000)) # USDC has 6 decimals
invoice = {
"version": "1",
"chainId": CHAIN_ID,
"verifyingContract": USDC_ADDRESS_BASE,
"amount": str(amount_wei),
"token": USDC_ADDRESS_BASE,
"payer": payer,
"nonce": nonce,
"deadline": nonce + 300, # 5‑minute expiry
}
# EIP‑712 domain separator (simplified)
domain = {
"name": "x402",
"version": "1",
"chainId": CHAIN_ID,
"verifyingContract": USDC_ADDRESS_BASE,
}
typed_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"Invoice": [
{"name": "amount", "type": "uint256"},
{"name": "token", "type": "address"},
{"name": "payer", "type": "address"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
],
},
"primaryType": "Invoice",
"domain": domain,
"message": invoice,
}
# Sign with the agent’s private key (loaded from env, never hard‑coded)
priv_key = os.getenv("AGENT_PRIVATE_KEY")
signed = Account.sign_typed_data(
dict(typed_data), key=bytes.fromhex(priv_key[2:])
)
invoice["signature"] = signed.signature.hex()
return invoice
Trade‑offs
- Gas cost: The verification step on‑chain (performed by the broker) consumes ~45 k gas (~$0.0003 at 5 gwei). This is negligible compared to the USDC price we charge.
-
Replay protection: The
nonce+deadlinefields prevent reuse. If the broker goes down, invoices become unusable after expiry, which is acceptable for a low‑latency service.
3.3 Broker minimal implementation (Cloudflare Workers JS)
javascript
// broker.js
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.7.0/dist/ethers.min.js";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const RPC = "https://base-mainnet.g.alchemy.com/v2/<YOUR_KEY>";
const provider = new ethers.JsonRpcProvider(RPC);
const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
const usdc = new ethers.Contract(USDC, usdcAbi, provider);
// Verify signature (EIP‑712) – simplified; in prod use @ethersproject/hash
async function verifyInvoice(inv) {
// Re‑construct typed data and recover signer address
// ... (omitted for brevity) ...
// Return payer address if signature valid
}
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("Method not allowed", {status:405});
const body = await request.json();
const invoice;
const payer = await verifyInvoice(inv);
if (!payer) return new Response("Invalid signature", {status:400});
// Check allowance: we expect the payer to have approved USDC transfer to this worker
const bal = await usdc.balanceOf(payer);
const due = ethers.parseUnits(inv.amount, 6); // USDC decimals
if (bal < due) return new Response("Insufficient USDC", {status:402});
// Forward to the agent worker (same
Top comments (0)