How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put an LLM‑driven agent to work on‑chain, with a focus on practical implementation, trade‑offs, and operational realities.
1. Why an “earning‑while‑sleeping” agent?
The idea isn’t to print money; it’s to let a deterministic software loop perform a narrow, repeatable task that pays a small fee in USDC each time it succeeds. The agent I built does micro‑tasks on a decentralized service catalog (the x402‑paid endpoint list you’ll see at the end). Each successful call nets $0.01‑$0.10, and the agent can run thousands of calls per day if the underlying service is available.
The key constraints I kept in mind:
| Constraint | Reason | Implementation choice |
|---|---|---|
| Predictable cost | We don’t want the agent to burn more in gas/LLM fees than it earns. | Use a cheap, fast LLM (e.g., gpt-3.5-turbo-0125) with function‑calling; batch LLM calls where possible. |
| Deterministic safety | Hallucinations could lead to invalid on‑chain calls. | Wrap every LLM output in a strict JSON schema validator; fallback to a rule‑based handler on failure. |
| Observable | We need to know if the agent is stuck or losing money. | Export Prometheus metrics (calls, successes, failures, earned USDC). |
| Low ops overhead | Should run unattended for days. | Deploy as a lightweight Docker container on a cheap VPS or Cloudflare Workers (the latter for the x402 gateway). |
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Scheduler (cron) | ---> | Agent Core Loop | ---> | x402 Gateway |
| (every 5 min) | | - LLM planner | | (USDC payment) |
+-------------------+ | - Tool executor | +-------------------+
| - State store |
+-------------------+
|
v
+-------------------+
| Monitoring/Alert |
+-------------------+
- Scheduler – a simple cron job (or Cloudflare Workers cron trigger) starts the agent every few minutes. This prevents a runaway loop if the agent crashes.
- Agent Core Loop – the heart: receives a goal (e.g., “call any available endpoint that pays ≥ $0.02”), asks the LLM to pick a tool, validates the tool call, executes it, records the result, and repeats until a time budget or success threshold is met.
- x402 Gateway – a thin wrapper that forwards the agent’s HTTP request to the actual service endpoint, attaches the required USDC payment (via the x402 protocol), and returns the response plus any refund/change.
- State store – a SQLite file (or Redis if you prefer) holds the agent’s nonce, earned balance, and recent failures to avoid replay attacks.
-
Monitoring – Prometheus endpoint exposed on
:9090/metrics. Alerts fire if success rate drops below 80% or if earned USDC stalls for > 1 h.
3. Choosing the LLM & tooling
I experimented with three setups:
| Setup | Latency (avg.) | Cost per 1k tokens | Reliability (function‑call success) |
|---|---|---|---|
gpt-4-turbo-preview |
1.2 s | $0.03 | 96 % |
gpt-3.5-turbo-0125 |
0.6 s | $0.0015 | 92 % |
| Local Llama‑2‑13B (GGUF) | 2.8 s (CPU) | $0 (host) | 78 % |
The cost vs. reliability trade‑off made gpt-3.5-turbo the sweet spot: cheap enough that the LLM fee (< $0.0002 per call) is negligible compared to the USDC payout, and its function‑calling support is solid. I kept a fallback to a rule‑based selector (pick the highest‑paying endpoint that hasn’t failed in the last 5 min) for the ~8 % of calls where the LLM returned malformed JSON.
Tool definition (JSON Schema) – each tool corresponds to an x402‑paid endpoint. The schema is generated once from the catalog and shipped with the agent.
{
"name": "call_endpoint",
"description": "Invoke an x402‑paid service endpoint and return its raw response.",
"parameters": {
"type": "object",
"properties": {
"endpoint": { "type": "string", "enum": ["get_price", "mint_nft", "data_feed", /* … */] },
"args": {
"type": "object",
"additionalProperties": false,
"description": "Endpoint‑specific payload (see catalog)."
}
},
"required": ["endpoint", "args"],
"additionalProperties": false
}
}
The agent asks the LLM to output a JSON object that conforms to this schema. A quick jsonschema.validate() call either accepts the payload or triggers the fallback.
4. Working code snippets
Below is a minimal, runnable version of the agent core. It assumes you have:
- An OpenAI API key (
OPENAI_API_KEYenv var). - Access to an x402 gateway that accepts a signed USDC payment header (
X-PAYMENT: <base64‑signed-tx>). In practice, the gateway handles the signing; the agent just adds the header. - A local SQLite file
state.dbfor nonce tracking.
python
# agent.py
import os, json, time, sqlite3, logging, asyncio
from typing import Dict, Any
import openai
import jsonschema
import aiohttp
from prometheus_client import start_http_server, Counter, Gauge
# -------------------- Config --------------------
OPENAI_MODEL = "gpt-3.5-turbo-0125"
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
assert OPENAI_API_KEY, "Set OPENAI_API_KEY"
openai.api_key = OPENAI_API_KEY
# Metrics
CALLS = Counter("agent_calls_total", "Total agent invocations")
SUCCEEDS = Counter("agent_success_total", "Successful endpoint calls")
FAILS = Counter("agent_fail_total", "Failed endpoint calls")
EARNED = Gauge("agent_earned_usdc", "USDC earned so far")
# ------------------------------------------------
# ---------- State persistence ----------
DB_PATH = "state.db"
def init_db():
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
cur.execute("""CREATE TABLE IF NOT EXISTS state (
nonce INTEGER PRIMARY KEY,
earned_usdc REAL DEFAULT 0
)""")
con.commit()
con.close()
def get_state() -> Dict[str, Any]:
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
cur.execute("SELECT nonce, earned_usdc FROM state ORDER BY nonce DESC LIMIT 1")
row = cur.fetchone()
con.close()
if row:
return {"nonce": row[0], "earned_usdc": row[1]}
return {"nonce": 0, "earned_usdc": 0.0}
def update_state(nonce: int, earned: float):
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
cur.execute("INSERT OR REPLACE INTO state (nonce, earned_usdc) VALUES (?, ?)", (nonce, earned))
con.commit()
con.close()
# ----------------------------------------
# ---------- Tool schema (simplified) ----------
# In a real build you would fetch this from the catalog endpoint.
TOOL_SCHEMA = {
"name": "call_endpoint",
"description": "Invoke an x402‑paid service endpoint and return its raw response.",
"parameters": {
"type": "object",
"properties": {
"endpoint": {"type": "string", "enum": ["get_price", "data_feed", "mint_nft"]},
"args": {
"type": "object",
"additionalProperties": False
}
},
"required": ["endpoint", "args"],
"additionalProperties": False
}
}
# ------------------------------------------------
async def ask_llm(goal: str) -> Dict[str, Any]:
"""Ask the LLM to pick a tool and fill its arguments."""
messages = [
{"role": "system", "content": "You are an agent that selects a tool to call. Respond ONLY with valid JSON matching the supplied tool schema."},
{"role": "user", "content": f"Goal: {goal}\nAvailable tool: {json.dumps(TOOL_SCHEMA)}"}
]
resp = await openai.ChatCompletion.acreate(
model=OPENAI_MODEL,
messages=messages,
temperature=0.0,
max_tokens=200,
)
content = resp.choices[0].message["content"].strip()
try:
payload = json.loads(content)
jsonschema.validate(payload, TOOL_SCHEMA)
return payload
except (json.JSONDecodeError, jsonschema.ValidationError) as e:
logging.warning(f"LLM output invalid: {e}")
return None # trigger fallback
async def execute_tool
Top comments (0)