How I Built an Autonomous AI Agent That Earns USDC While I Sleep
An honest walk‑through of the moving parts, the code that actually runs, and the trade‑offs I ran into.
1. Why “earn while I sleep” is a misnomer
The phrase sells a dream, but the reality is a deterministic loop that:
- Pulls a paid micro‑task from a queue (e.g., a data‑labeling request or a tiny inference job).
- Executes the job with a lightweight model or script.
- Submits the result and receives an x402‑style USDC payment on Base.
If the queue is empty, the agent idles—no earnings, no compute waste. The “sleep” part is just the agent waiting for the next work item; it’s not magic.
2. System overview
+----------------+ +----------------+ +-----------------+
| Task Publisher| ---> | Job Queue | ---> | Autonomous Agent|
| (e.g., a dApp) | | (Redis / SQS) | | (Python worker) |
+----------------+ +----------------+ +-----------------+
|
v
+--------------+
| x402 Payee |
| (USDC on Base)|
+--------------+
- Task Publisher – any service that posts a JSON‑serializable job and attaches a price in USDC (via the x402 header).
- Job Queue – a simple FIFO buffer; I used Redis for local dev and switched to Amazon SQS in production for durability.
- Autonomous Agent – a long‑running Python process that loops, pulls a job, runs it, signs an x402 receipt, and claims the payment.
- x402 Payee – the smart contract that escrows USDC, verifies the receipt, and releases funds to the agent’s wallet.
3. The job format
Each job is a plain JSON object:
{
"job_id": "abc123",
"type": "image_label",
"payload": {
"image_url": "https://example.com/img.png",
"labels": ["cat", "dog", "car"]
},
"price_usdc": 0.05 // amount in USDC (6 decimals)
}
The type tells the agent which handler to invoke. Keeping the schema tiny avoids versioning headaches; if you need more fields, add them under payload.
4. Agent skeleton (Python 3.11)
Below is the core loop. It’s deliberately minimal—no external frameworks, just the stdlib plus a few well‑maintained packages.
import os
import json
import time
import hashlib
import hmac
import requests
import web3
from web3 import Web3
from redis import Redis
# ----- CONFIG -------------------------------------------------
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
QUEUE_NAME = "x402_jobs"
AGENT_WALLET = os.getenv("AGENT_WALLET") # e.g. 0xAbc...
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY") # never commit!
BASE_RPC = os.getenv("BASE_RPC", "https://base.mainnet.rpc.link")
X402_CONTRACT = os.getenv("X402_CONTRACT") # address of the escrow
# --------------------------------------------------------------
w3 = Web3(Web3.HTTPProvider(BASE_RPC))
account = w3.eth.account.from_key(PRIVATE_KEY)
redis = Redis.from_url(REDIS_URL)
def sign_x402_receipt(job_id: str, result: dict) -> str:
"""
Build the data that the x402 contract expects:
keccak256("X402Receipt(job_id,result_hash,agent)") signed with the agent key.
"""
result_hash = hashlib.sha256(json.dumps(result, sort_keys=True).encode()).hexdigest()
message = f"X402Receipt({job_id},{result_hash},{account.address})"
msg_hash = Web3.keccak(text=message)
signed = account.sign_hash(msg_hash)
return signed.signature.hex()
def handle_image_label(payload: dict) -> dict:
"""
Stub: replace with your own model or API call.
Returns the chosen label and a confidence score.
"""
# In a real system you would run a lightweight model here.
# For demo purposes we just pick the first label.
return {"label": payload["labels"][0], "confidence": 0.92}
def process_job(raw_job: bytes):
job = json.loads(raw_job)
print(f"Processing {job['job_id']} ({job['type']}) for {job['price_usdc']} USDC")
if job["type"] == "image_label":
result = handle_image_label(job["payload"])
else:
raise ValueError(f"Unsupported job type: {job['type']}")
signature = sign_x402_receipt(job["job_id"], result)
# Submit receipt to the x402 escrow contract
tx = {
"to": X402_CONTRACT,
"value": 0,
"data": w3.codec.encode_abi(
["string", "bytes", "address"],
[job["job_id"], bytes.fromhex(signature), account.address]
),
"gas": 200_000,
"gasPrice": w3.to_wei("0.1", "gwei"),
"nonce": w3.eth.get_transaction_count(account.address),
"chainId": w3.eth.chain_id,
}
signed_tx = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Receipt tx {tx_hash.hex()} status {receipt.status}")
def main_loop():
while True:
# BRPop blocks until an item appears (timeout=0 means block forever)
_, raw_job = redis.brpop([QUEUE_NAME], timeout=0)
try:
process_job(raw_job)
except Exception as e:
# In production you would push to a dead‑letter queue and alert.
print(f"Error processing job: {e}")
# Small sleep to avoid hammering Redis if the queue is empty for a long time
time.sleep(0.5)
if __name__ == "__main__":
main_loop()
What the code does
-
Pulls a job from Redis (
BRPOP). -
Dispatches to a handler based on
job["type"]. - Signs an x402 receipt (a deterministic message that the escrow contract can verify).
-
Sends a transaction that calls the escrow’s
claim(bytes32 jobId, bytes signature, address payer)function. - Waits for confirmation, then loops again.
5. Honest trade‑offs I encountered
| Area | Decision | Pros | Cons / Gotchas |
|---|---|---|---|
| Language | Python 3.11 + web3.py
|
Huge ecosystem, easy to prototype. | Slightly slower than Go/Rust for tight loops; GC pauses can add latency (mitigated by keeping the worker light). |
| Queue | Redis (dev) → Amazon SQS (prod) | Redis is trivial locally; SQS gives durability and automatic retries. | SQS introduces ~200 ms latency per poll; you must handle visibility timeouts to avoid double‑processing. |
| Payment | x402 escrow on Base | USDC is stable, low gas (~$0.0001 per claim), and the standard is well‑audited. | You need to hold a small USDC buffer in the escrow to cover the claim; if the contract is under‑funded, transactions revert. |
| Model execution | Stubbed label picker | Keeps the example runnable without heavy dependencies. | Real agents will need to bundle a model (e.g., ONNX Runtime) or call an external API—adds size, cold‑start time, and potential cost. |
| Security | Private key stored in env var, never committed. | Prevents accidental leakage. | If the host is compromised, the attacker can drain the agent’s wallet. Consider using a hardware signer or a split‑key scheme for higher value agents. |
| Observability | Simple print statements + transaction receipt logs. |
Easy to debug locally. | In production you’ll want structured logging, metrics (e.g., Prometheus), and alerting on failed claims. |
The biggest lesson: the agent is only as profitable as the supply of paid tasks. If the task publisher dries up, the agent sits idle and earns nothing—no matter how clever the code.
6. Scaling considerations
-
Horizontal scaling – Run multiple instances of the worker behind the same queue. Redis’
BRPOPis naturally competitive; SQS will distribute messages across consumers. -
Gas price management – I used a static
0.1 gweigas price; in production you should fetch the Base network’s suggested price viaeth_feeHistoryand add a small tip to avoid stuck transactions. -
Error handling – Failed claims are retried automatically by the escrow (if you implement a
claimAndRetrypattern). Otherwise, push the job to a dead‑letter queue and notify ops. -
Monitoring – Export a Prometheus counter for
jobs_processed_total,usdc_earned_total, andclaim_failures_total. A simple Grafana dashboard lets you see when the agent is actually earning versus idling.
7. A note on “earning while I sleep”
The agent does earn USDC autonomously, but **earnings are directly proportional to the flow of paid micro‑tasks
Top comments (0)