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

I wanted to see whether a modest, fully‑on‑chain agent could generate a trickle of USDC without constant supervision. The goal wasn’t to build a profit‑making machine; it was to explore the practical limits of chaining LLMs, deterministic tasks, and micropayments on a layer‑2 network. Below is a walk‑through of the system I ended up with, the code that makes it tick, and the trade‑offs I ran into along the way.


Architecture Overview

The agent follows a simple loop:

  1. Observation – pull market data from a public API.
  2. Decision – ask a language model to suggest a tiny arbitrage action (e.g., “buy token A on exchange X, sell on Y if spread > 0.5 %”).
  3. Execution – if the model’s suggestion passes safety checks, submit the trade via a DEX aggregator API.
  4. Settlement – receive any USDC earned and log the result.

All steps run inside a lightweight Docker container that wakes up every five minutes via a cron‑like scheduler. The only on‑chain interaction is the receipt of USDC; the agent never holds private keys for a wallet that can spend funds—it only receives payments to a predetermined address.


Language Model Component

I used a locally hosted Llama‑2‑7B model quantized to 4‑bit via llama.cpp. The model receives a short prompt that includes the current price spread and a request for a binary decision (“YES” if the spread justifies a trade, otherwise “NO”).

from llama_cpp import Llama

llama = Llama(
    model_path="models/llama-2-7b-chat.ggmlv3.q4_K_M.bin",
    n_ctx=512,
    n_threads=4,
)

def should_trade(spread: float) -> bool:
    prompt = (
        f"The current price spread between Exchange A and Exchange B is {spread:.3%}. "
        "If you think executing a small arbitrage trade (buy low, sell high) would be profitable after gas, "
        "answer with exactly 'YES'. Otherwise answer with exactly 'NO'."
    )
    output = llama(prompt, max_tokens=4, stop=["\n"])
    return output["choices"][0]["text"].strip().upper() == "YES"
Enter fullscreen mode Exit fullscreen mode

The model is deliberately kept tiny: inference takes ~120 ms on a modest CPU core, which keeps the loop cheap and predictable. Larger models would improve nuance but add latency and cost that outweigh the marginal gain for this use case.


Task Engine: Price Arbitrage

The arbitrage logic is intentionally simplistic. I fetch the mid‑price of USDC/USDT on two exchanges (CoinGecko for simplicity) and compute the spread. If the spread exceeds a threshold (0.4 %), the agent attempts a swap of 10 USDC from the cheaper to the more expensive exchange via the 1inch API.

import requests

COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
ONEINCH_URL = "https://api.1inch.io/v5.0/1/swap"

def get_prices():
    resp = requests.get(
        COINGECKO_URL,
        params={"ids": "usd-coin,tether", "vs_currencies": "usd"},
        timeout=5,
    )
    data = resp.json()
    return data["usd-coin"]["usd"], data["tether"]["usd"]

def execute_swap(from_token, to_token, amount):
    # 1inch expects token addresses; on Base we use the wrapped versions
    params = {
        "src": from_token,
        "dst": to_token,
        "amount": str(int(amount * 1e6)),  # USDC has 6 decimals
        "slippage": "1",
        "disableEstimate": "false",
    }
    headers = {"Authorization": f"Bearer {ONEINCH_API_KEY}"}
    resp = requests.get(ONEINCH_URL, params=params, headers=headers, timeout=10)
    resp.raise_for_status()
    return resp.json()
Enter fullscreen mode Exit fullscreen mode

The trade size is kept low (10 USDC) to limit exposure. If the transaction fails (e.g., due to slippage or insufficient liquidity), the agent logs the error and waits for the next cycle.


Payment Handler: x402 on Base

Earnings are collected via the x402 micropayment standard. Each successful swap triggers a call to a simple paymaster contract deployed on Base that mints USDC to the agent’s address when the caller pays a small fee in ETH. The agent itself never initiates the payment; it merely signs a message authorizing the paymaster to release the owed USDC after the trade settles.

from eth_account import Account
from web3 import Web3

w3 = Web3(Web3.HTTPProvider("https://base.mainnet.rpc.acme.com"))
PAYMASTER = w3.to_checksum_address("0xPaymasterAddress")
AGENT = w3.to_checksum_address("0xAgentAddress")

def claim_earnings(tx_hash):
    # Build a simple EIP‑712 typed data structure for the paymaster
    domain = {
        "name": "USDC Paymaster",
        "version": "1",
        "chainId": w3.eth.chain_id,
        "verifyingContract": PAYMASTER,
    }
    types = {
        "EIP712Domain": [
            {"name": "name", "type": "string"},
            {"name": "version", "type": "string"},
            {"name": "chainId", "type": "uint256"},
            {"name": "verifyingContract", "type": "address"},
        ],
        "Claim": [
            {"name": "agent", "type": "address"},
            {"name": "txHash", "type": "bytes32"},
        ],
    }
    message = {"agent": AGENT, "txHash": w3.to_bytes(hexstr=tx_hash)}
    signed = Account.sign_typed_data(w3.eth.account.from_key(PRIVATE_KEY).key, domain, types, message)
    # Send the signature to the paymaster (off‑chain relayer or direct call)
    tx = PAYMASTER_contract.functions.claim(signed.v, signed.r, signed.s).build_transaction(
        {"from": AGENT, "nonce": w3.eth.get_transaction_count(AGENT)}
    )
    signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)
    w3.eth.send_raw_transaction(signed_tx.rawTransaction)
Enter fullscreen mode Exit fullscreen mode

The paymaster contract is trivial: it checks the signature, verifies that the associated trade succeeded (by looking up the tx hash on‑chain), and then transfers the pre‑agreed USDC amount. This keeps the agent’s on‑chain footprint minimal—only a signature verification and a token transfer.


Putting It All Together: Agent Loop

import time
import logging

logging.basicConfig(level=logging.INFO)

while True:
    try:
        usdc_price, usdt_price = get_prices()
        spread = abs(usdc_price - usdt_price) / usdc_price
        logging.info(f"USDC={usdc_price:.6f}, USDT={usdt_price:.6f}, spread={spread:.4%}")

        if spread > 0.004 and should_trade(spread):
            # Determine direction: buy cheaper, sell dearer
            if usdc_price < usdt_price:
                tx = execute_swap("USDC", "USDT", 10)
                direction = "USDC→USDT"
            else:
                tx = execute_swap("USDT", "USDC", 10)
                direction = "USDT→USDC"
            logging.info(f"Executed {direction} swap, tx={tx['txid']}")
            claim_earnings(tx["txid"])
        else:
            logging.info("Spread too small or model advised against trade.")
    except Exception as e:
        logging.exception(f"Error in agent cycle: {e}")

    time.sleep(300)  # five‑minute interval
Enter fullscreen mode Exit fullscreen mode

The loop is deliberately defensive: every external call is wrapped in a try/except, and the agent never leaks private keys beyond the signing step (which occurs only in memory).


Honest Trade‑

Top comments (0)