DEV Community

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

Why x402?

x402 is a lightweight HTTP-based payment scheme that lets a server respond with a 402 Payment Required status and a payment request in the WWW-Authenticate header. Clients that understand x402 can automatically fetch USDC, sign a transaction, and retry the request. For an autonomous agent this means:

  • Statelessness - the agent doesn't need to keep a user-side balance; payment is enforced at the API boundary.
  • Compatibility - any HTTP client (curl, Postman, a custom SDK) can be upgraded to pay without changing business logic.
  • Low overhead - the protocol adds only a few bytes to the response; the heavy lifting stays in the payment SDK.

The trade-off is that you must accept the extra round-trip for unauthenticated callers and you need to host a wallet that can sign USDC transfers on the target chain (here, Base).

High-level Architecture

+-------------------+ HTTP/x402 +-------------------+
| Client (any)      |<--------->| Agent Service     |
+-------------------+ (FastAPI) +-------------------+
                                 ^
                                 |
                                 v
                        +-------------------+
                        | Wallet Manager    |
                        | (web3.py + private|
                        | key, USDC ABI)    |
                        +-------------------+
                                 |
                                 v
                        +-------------------+   +----------------------------+
                        | USDC Ledger       |-->| (Base testnet/main)        |
                        |                   |   | 0x833589fCD6eDb6E08f4c7C32 |
                        +-------------------+   | D4f71b54bdA02913           |
                                                +----------------------------+
  • Agent Service - a FastAPI app that exposes one or more useful endpoints (e.g., text summarization, image tagging). Each endpoint checks for a valid x402 payment; if missing, it returns a 402 with payment details.
  • Wallet Manager - a singleton that loads an Ethereum private key, constructs USDC transfer transactions, and signs them using web3.py.
  • USDC Ledger - the Base network contract (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 on Base mainnet).

Code Walk-through

Below is a complete, runnable example (≈120 lines) that you can copy into a file agent.py and run with uvicorn agent:app --host 0.0.0.0 --port 8000.

3.1 Dependencies

# pyproject.toml
[project]
name = "x402-agent"
dependencies = [
    "fastapi==0.110.0",
    "uvicorn[standard]==0.30.0",
    "web3==7.8.0",
    "eth-account==0.10.0",
    "pydantic==2.7.1",
]

3.2 Core implementation

# agent.py
import os
from typing import Literal
from fastapi import FastAPI, Header, HTTPException, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_defunct

app = FastAPI(title="Autonomous USDC‑earning Agent")

# ----------------------------------------------------------------------
# Configuration - replace with your own values or load from env/secrets
# ----------------------------------------------------------------------
BASE_RPC = os.getenv("BASE_RPC", "https://mainnet.base.org")  # public RPC, OK for read‑only
USDC_CONTRACT = Web3.to_checksum_address(
    "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
)  # USDC on Base
PRIVATE_KEY = os.getenv("AGENT_PRIVATE_KEY")  # must fund this address with USDC
if not PRIVATE_KEY:
    raise RuntimeError("Set AGENT_PRIVATE_KEY env var")

w3 = Web3(Web3.HTTPProvider(BASE_RPC))
account = Account.from_key(PRIVATE_KEY)

usdc_abi = [
    {
        "constant": False,
        "inputs": [
            {"name": "_to", "type": "address"},
            {"name": "_value", "type": "uint256"},
        ],
        "name": "transfer",
        "outputs": [{"name": "", "type": "bool"}],
        "type": "function",
    },
    {
        "constant": True,
        "inputs": [{"name": "_owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "balance", "type": "uint256"}],
        "type": "function",
    },
]

usdc_contract = w3.eth.contract(address=USDC_CONTRACT, abi=usdc_abi)

# ----------------------------------------------------------------------
# x402 helpers
# ----------------------------------------------------------------------
X402_SCHEME = "Bearer"
PRICE_USDC = 0.01  # price per call, in USDC (6 decimals)
USDC_DECIMALS = 6


def price_in_wei() -> int:
    """Return price in the smallest USDC unit (wei)."""
    return int(PRICE_USDC * 10**USDC_DECIMALS)


def build_402(challenge_id: str) -> Response:
    """Create a 402 response with x402 WWW‑Authenticate header."""
    header = (
        f'{X402_SCHEME} x402; '
        f'price="{price_in_wei()}"; '
        f'currency="USDC"; '
        f'network="base"; '
        f'payee="{account.address}"; '
        f'payload="{challenge_id}"'
    )
    return Response(
        status_code=402,
        headers={"WWW-Authenticate": header},
        media_type="application/json",
        content='{"error":"payment required"}',
    )


def verify_payment(auth_header: str | None, payload: str) -> bool:
    """
    Very small subset of x402 verification: we expect the client to have sent
    a signed transaction hash in the Authorization header. In practice you would
    use the official x402-py SDK; here we keep it illustrative.
    """
    if not auth_header or not auth_header.startswith(f"{X402_SCHEME} "):
        return False
    tx_hash = auth_header.split(" ", 1)[1]

    # Basic sanity: transaction must exist and transfer correct amount to us
    try:
        tx = w3.eth.get_transaction(tx_hash)
    except Exception:
        return False

    if tx["to"].lower() != USDC_CONTRACT.lower():
        return False

    # decode input data (simple transfer)
    func_obj, func_params = usdc_contract.decode_function_input(tx["input"])
    if func_obj.fn_name != "transfer":
        return False
    if func_params["_to"].lower() != account.address.lower():
        return False
    if func_params["_value"] != price_in_wei():
        return False

    # optional: check receipt
    status = 1
    receipt = w3.eth.get_transaction_receipt(tx_hash)
    return receipt.status == status


# ----------------------------------------------------------------------
# Example AI endpoint - replace with your own model inference
# ----------------------------------------------------------------------
class SummarizeRequest(BaseModel):
    text: str
    max_length: int = 130


@app.post("/summarize")
async def summarize(
    req: SummarizeRequest,
    authorization: str = Header(None),
    x_payload: str = Header(None),
):
    """Returns a naive extractive summary. Payment is enforced via x402."""
    # 1️⃣ Verify payment (if any)
    if not verify_payment(authorization, x_payload or ""):
        # generate a random challenge to avoid replay attacks
        import uuid
        challenge = str(uuid.uuid4())
        return build_402(challenge)

    # 2️⃣ Actual work - placeholder logic
    sentences = req.text.split(".")
    summary = ".".join(sentences[: max(1, req.max_length // 20)]) + "."
    return JSONResponse({"summary": summary})


# ----------------------------------------------------------------------
# Health check - no payment required
# ----------------------------------------------------------------------
@app.get("/healthz")
async def healthz():
    return {
        "status": "ok",
        "usdc_balance": usdc_contract.functions.balanceOf(account.address).call()
        / 10**USDC_DECIMALS,
    }

What the snippet does

  • Wallet setup - loads a private key from the environment, connects to Base via a public RPC, and prepares a minimal USDC ABI (only transfer and balanceOf).
  • x402 response - build_402 creates a 402 Payment Required header that tells the client how much USDC to send, to which address, and on which chain.
  • Payment verification - verify_payment expects the client to resend the request with an Authorization: Bearer <txHash> header. It checks that the transaction actually transferred the correct amount to the agent's address. (In a real deployment you'd use the official x402-py library which also validates signatures and replays.)
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.