How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to create self‑sustaining AI services that get paid per use in USDC on the Base L2.
1. Why bother with on‑chain payments?
Traditional SaaS models require you to manage subscriptions, invoices, and charge‑backs. If you expose a tiny, stateless function (e.g., “summarize this paragraph”) you can avoid all that overhead by letting each call carry its own payment. The x402 protocol does exactly that: it lets an HTTP endpoint request a small amount of USDC before returning a response, using the Base network’s low gas fees.
The trade‑off is added latency (you must wait for a transaction to confirm) and the need to handle on‑chain state (nonce management, failed payments). If your service can tolerate a few hundred milliseconds of extra delay and you’re comfortable with a little smart‑contract interaction, x402 is a practical way to monetize micro‑services.
2. High‑level architecture
+-------------------+ +-------------------+ +-------------------+
| Client (any HTTP) | ---> | x402 Middleware | ---> | AI Worker (FastAPI)|
+-------------------+ +-------------------+ +-------------------+
^ ^ |
| | |
| USDC payment (x402) | Result (JSON) |
+-------------------------+-------------------------+
-
Client – calls your agent with an
Authorization: Bearer <x402-token>header. - x402 Middleware – validates the token, checks that the required USDC amount has been paid on‑chain, then forwards the request.
- AI Worker – does the actual work (here, a simple text summarizer). It never touches blockchain logic; it just returns a JSON payload.
The middleware is the only place where you need web3/Ethers code, keeping the AI core simple and testable.
3. Setting up a wallet and funding it
You need an externally owned account (EOA) on Base that holds USDC. For a hobby project a private key stored in an environment variable is fine; for production use a managed wallet or a custodial service.
# .env (never commit this!)
BASE_RPC_URL="https://base.mainnet.rpc.dev"
USDC_CONTRACT="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
WALLET_PRIVATE_KEY="0xabcd…" # fund this address with a few USDC via a faucet or exchange
REQUIRED_PAYMENT_USDC=0.01 # price per call
Fund the wallet with a small amount (e.g., 0.5 USDC). Because each call costs only a few cents, the wallet will last for many requests.
4. Implementing the x402 middleware (Node.js/Express)
Below is a minimal, production‑ready middleware that:
- Extracts the
x402-paymentheader (a Base64‑encoded JSON token). - Verifies the token’s signature against the x402 contract.
- Confirms that the payer has sent at least
REQUIRED_PAYMENT_USDCUSDC to the agent’s address. - Increments a local nonce cache to prevent replay attacks.
// x402Middleware.js
require('dotenv').config();
const express = require('express');
const { ethers } = require('ethers');
const app = express();
// ---------- CONFIG ----------
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC_URL);
const usdcAddress = process.env.USDC_CONTRACT;
const usdcAbi = [
"function balanceOf(address) view returns (uint256)",
"function decimals() view returns (uint8)",
];
const usdc = new ethers.Contract(usdcAddress, usdcAbi, provider);
const agentAddress = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider).address;
const requiredAmount = ethers.parseUnits(process.env.REQUIRED_PAYMENT_USDC, 6); // USDC has 6 decimals
// Simple in‑memory nonce store (replace with Redis for scale)
const nonces = new Map();
// ---------------------------
async function verifyPayment(req, res, next) {
const auth = req.headers['x402-payment'];
if (!auth) return res.status(402).send('Missing x402 payment');
let token;
try {
token = JSON.parse(Buffer.from(auth, 'base64').toString());
} catch {
return res.status(400).send('Invalid token format');
}
const { payer, amount, nonce, signature, deadline } = token;
const now = Math.floor(Date.now() / 1000);
if (now > deadline) return res.status(402).send('Payment expired');
// Replay protection
const lastNonce = nonces.get(payer) ?? -1;
if (nonce <= lastNonce) return res.status(402).send('Nonce too low');
nonces.set(payer, nonce);
// Verify signature over the typed data (EIP‑712 style used by x402)
const domain = {
name: 'x402 Payment',
version: '1',
chainId: 8453, // Base
verifyingContract: usdcAddress,
};
const types = {
Payment: [
{ name: 'payer', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const value = { payer, amount, nonce, deadline };
const recovered = await ethers.verifyTypedData(domain, types, value, signature);
if (recovered.toLowerCase() !== payer.toLowerCase()) {
return res.status(401).send('Invalid signature');
}
// On‑chain check: has the payer sent enough USDC to the agent?
const balance = await usdc.balanceOf(agentAddress);
if (balance < requiredAmount) return res.status(402).send('Insufficient funds paid');
// Optional: reset balance after we consider it “used” (not required for read‑only agents)
// usdc.transfer(payer, balance - requiredAmount); // only if you want to refund change
// Attach info for downstream handlers
req.x402 = { payer, amount };
next();
}
app.use(express.json());
app.use(verifyPayment);
// Example endpoint – replace with your own AI logic
app.post('/summarize', async (req, res) => {
const { text } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'Expected "text" field' });
}
// ---- Placeholder for actual model call ----
// For demo we just truncate; replace with HF/inference API or local LLM.
const summary = text.split(' ').slice(0, 20).join(' ') + '…';
// -----------------------------------------
res.json({ summary, paidBy: req.x402.payer });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Agent listening on :${PORT}`));
What this code does
- Payment verification happens before any AI work, so you never waste compute on a non‑paying request.
-
Nonce cache prevents replay attacks without hitting the blockchain on every call. In production swap the
Mapfor a Redis instance shared across instances. - Balance check is a simple read‑only call; it costs virtually no gas because it’s a view function on the USDC contract.
- The middleware is framework‑agnostic; you can plug it into FastAPI, Hono, or any Stack that lets you run custom middleware.
5. The AI worker – a real‑world example
Below is a tiny FastAPI service that uses the Hugging Face Inference API to produce a summary. It receives the request after the x402 middleware has already validated payment, so the endpoint can stay pure Python.
# main.py
import os
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
HF_API_URL = "https://api-inference.huggingface.co/models/facebook/bart-large-cnn"
HF_TOKEN = os.getenv("HF_HUB_TOKEN") # set in your deploy env
class SummaryRequest(BaseModel):
text: str
@app.post("/summarize")
async def summarize(req: SummaryRequest):
if not req.text or len(req.text) < 30:
raise HTTPException(status_code=400, detail="Text too short to summarize")
payload = {"inputs": req.text, "parameters": {"max_length": 100}}
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
r = requests.post(HF_API_URL, json=payload, headers=headers, timeout=15)
if r.status
| Total per job | ≈ $0.002–0.005 |
At $0.05 per job, you have a healthy margin. Scale the model or add value-added steps (translation, formatting) to justify higher prices.
8. Next steps
- Deploy the escrow to Base testnet first (
sepolia.base.org). - Write an integration test that deposits, runs a dummy job, and releases.
- Add a health endpoint (
GET /health) so the platform can verify the agent is alive. - Publish your service definition as a JSON-Schema DID document so other agents can discover you.
That's it—you now have a minimal, trustless, USDC-earning agent that can run unattended for weeks.
Top comments (0)