DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

How I Built an Autonomous AI Agent That Earns USDC While I Sleep

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

  1. Agent receives a request → creates an x402 invoice (includes amount, token address, chain ID, expiry).
  2. Agent returns the invoice to the caller (usually another agent or a frontend).
  3. Caller pays the invoice via their wallet; the payment is verified on‑chain by the broker.
  4. 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})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 + deadline fields 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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)