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 want to run a self‑sustaining AI service that receives micropayments in USDC on the Base L2.


1. Why an autonomous agent?

When you expose a model or a data‑processing pipeline as a paid API, the most tedious part is the operational loop: monitoring usage, topping up balances, handling failed transactions, and keeping the service online. An autonomous agent can encapsulate all of those chores in a single process that:

  • Receives payment‑triggered requests via a smart‑contract escrow.
  • Verifies that the payer has deposited enough USDC before executing work.
  • Runs the inference or computation, returns the result, and automatically forwards the earned USDC to a wallet you control.

The goal is not to promise “passive income” but to reduce the manual ops overhead to a level where the service can stay alive for weeks without intervention.


2. High‑level architecture

+----------------+      USDC (Base)      +-------------------+
|  User dApp     | <--------------------> |  Payment Escrow   |
| (frontend)     |  (ERC‑20 transfer)   |  (simple solvable)|
+----------------+                       +-------------------+
          ^                                   |
          |  JSON‑RPC over HTTPS               |  Callback (payment confirmed)
          |                                   v
+----------------+                  +-------------------+
|  Agent Service | <----------------> |  Wallet & USDC    |
| (Python/FastAPI) |   signed tx   |   (Metamask/Ledger)|
+----------------+                  +-------------------+
Enter fullscreen mode Exit fullscreen mode
  • Payment Escrow – a minimal ERC‑20 escrow contract deployed on Base. It holds USDC from the caller, emits a PaymentReceived event when the amount ≥ price, and releases funds to the agent’s address on a release() call.
  • Agent Service – a stateless FastAPI process that:
    1. Listens for PaymentReceived events via WebSocket or polling.
    2. Validates the sender’s signature (to prevent replay).
    3. Executes the core workload (model inference, data transform, etc.).
    4. Calls escrow.release(txId) to pull the earned USDC into its wallet.
  • Wallet – an externally owned account (EOA) that holds the agent’s USDC balance. For production you’d replace this with a multisig or a smart‑contract wallet, but an EOA keeps the example simple.

3. Escrow contract (Solidity, ~70 LOC)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract USDCProxyEscrow {
    IERC20 public immutable usdc;
    address public immutable agent; // receives funds after release
    uint256 public price; // price per call, in USDC * 1e6 (6 decimals)

    struct Job {
        bytes32 id;
        address payer;
        bool released;
    }

    mapping(bytes32 => Job) public jobs;
    event PaymentReceived(bytes32 indexed id, address payer, uint256 amount);
    event Released(bytes32 indexed id, address to, uint256 amount);

    constructor(address _usdc, address _agent, uint256 _pricePerCall) {
        usdc = IERC20(_usdc);
        agent = _agent;
        price = _pricePerCall;
    }

    /// @notice Fund the escrow for a specific job id.
    /// The caller must first approve the escrow to spend `amount` USDC.
    function deposit(bytes32 jobId, uint256 amount) external {
        require(amount >= price, "Insufficient deposit");
        require(!jobs[jobId].released, "Already processed");
        usdc.transferFrom(msg.sender, address(this), amount);
        jobs[jobId] = Job({id: jobId, payer: msg.sender, released: false});
        emit PaymentReceived(jobId, msg.sender, amount);
    }

    /// @notice Agent calls this after completing the work.
    function release(bytes32 jobId) external {
        Job storage j = jobs[jobId];
        require(msg.sender == agent, "Only agent");
        require(!j.released, "Already released");
        uint256 bal = usdc.balanceOf(address(this));
        require(bal >= price, "Escrow underfunded");
        usdc.transfer(agent, price);
        j.released = true;
        emit Released(jobId, agent, price);
    }

    /// @notice Refund if the job is never completed (optional).
    function refund(bytes32 jobId) external {
        Job storage j = jobs[jobId];
        require(msg.sender == j.payer, "Only payer");
        require(!j.released, "Already released");
        uint256 bal = usdc.balanceOf(address(this));
        usdc.transfer(payer, bal);
        j.released = true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Trade‑offs

Aspect Choice Reason Drawback
Escrow logic Minimal, no upgradeability Low gas, easy to audit No ability to change price without redeploying
Price storage uint256 (6 decimals) Matches USDC on Base Requires caller to know exact decimal scaling
Refund Optional, manual Prevents funds locking forever Adds extra transaction cost if used

4. Agent service (Python 3.11, FastAPI)

Below is a runnable skeleton. Replace MODEL_INFERENCE with your actual workload (e.g., a HuggingFace transformers call, a SQL query, or a scraping routine).


python
# agent.py
import os
import asyncio
import json
from decimal import Decimal
from web3 import Web3
from web3.middleware import geth_poa_middleware
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
import uvicorn

# ---------- Configuration ----------
RPC_URL = os.getenv("BASE_RPC", "https://mainnet.base.org")   # public RPC, consider a private node for prod
USDC_ADDRESS = Web3.to_checksum_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")  # USDC on Base
ESCROW_ADDRESS = Web3.to_checksum_address(os.getenv("ESCROW_ADDR"))  # set after deployment
AGENT_PRIVATE_KEY = os.getenv("AGENT_PRIVKEY")  # EOA that will receive USDC
PRICE_USDC = Decimal(os.getenv("PRICE_USDC", "0.05"))  # price per call, e.g. $0.05
# -----------------------------------

w3 = Web3(Web3.HTTPProvider(RPC_URL))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

usdc_abi = [
    {"constant":True,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},
    {"constant":False,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"},
    {"constant":False,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"type":"function"},
    {"constant":True,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"}
]
usdc = w3.eth.contract(address=USDC_ADDRESS, abi=usdc_abi)

escrow_abi = json.loads("""[
    {"inputs":[{"internalType":"address","name":"_usdc","type":"address"},{"internalType":"address","name":"_agent","type":"address"},{"internalType":"uint256","name":"_pricePerCall","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},
    {"inputs":[{"internalType":"bytes32","name":"jobId","type":"bytes32"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"deposit","outputs":[],"type":"function"},
    {"inputs":[{"internalType":"bytes32","name":"jobId","type":"bytes32"}],"name":"release","outputs":[],"type":"function"},
    {"inputs":[{"internalType":"bytes32","name":"jobId","type":"bytes32"}],"name":"refund","outputs":[],"type":"function"},
    {"anonymous":False,"inputs":[{"indexed":True,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":True,"internalType":"address","name":"payer","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"PaymentReceived","type":"event"},
    {"anonymous":False,"inputs":[{"indexed":True,"internalType":"bytes32","name":"id","type":"bytes32"},{"indexed":True,"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Released","type":"event"}
]""")
escrow = w3.eth.contract(address=ESCROW_ADDRESS, abi=escrow_abi)

agent_acct = w3.eth.account.from_key(AGENT_PRIVATE_KEY)

app = FastAPI()

class JobReq(BaseModel):
    job_id: str  # hex string, 32 bytes
    payload: dict  # whatever your model needs

# ---- Core work placeholder ----
def MODEL_INFERENCE(payload: dict) -> dict:
    # Replace with actual inference; this dummy just echoes back.
    return {"result": "ok", "
Enter fullscreen mode Exit fullscreen mode

Top comments (0)