How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to put a paid AI service on‑chain and let it run unattended.
1. Why a paid agent makes sense
Autonomous agents are useful when they can perform a narrow, repeatable task and get compensated for it. If the task is cheap enough to run (e.g., a single LLM call) and the payment mechanism is low‑friction, the agent can accumulate revenue over time without human intervention.
The trade‑off is that you now have to worry about:
- Payment verification – you must reject requests that haven’t paid the correct amount.
- Latency vs. cost – adding a payment step introduces extra round‑trips and possible failure points.
- Operational overhead – you need to monitor balances, handle refunds, and keep the service patched.
If those costs are outweighed by the expected earnings, the model works. In my experiment I chose a simple text‑summarization endpoint that charges $0.02 per call in USDC on the Base network. Over a week it earned roughly $0.84 while I slept, which matched the projected revenue based on observed traffic.
2. Architecture overview
+-------------------+ HTTPS (x402) +-------------------+
| Client (curl, | -----------------> | Agent Service |
| frontend, etc.) | <-- 402 Payment Req | (Node/Worker) |
+-------------------+ +----------------+ +-----------------+
| Verify USDC | | LLM Provider |
| (x402 middleware) | (OpenAI API) |
+----------------+ +-----------------+
| Balance update (optional) |
+-----------------------------
-
Client sends an HTTP request with an
Authorization: Bearer <x402‑token>header. - The agent service runs an x402 middleware that checks the token against the smart contract on Base, verifies the amount, and marks the payment as consumed.
- If verification passes, the request is forwarded to an LLM (here I used OpenAI’s
gpt-3.5-turbo). - The LLM output is returned to the client.
The service is stateless aside from the payment nonce; each request is independent, which simplifies scaling and reduces the attack surface.
3. Setting up the payment layer
I used the x402 npm package, which implements the client‑side and server‑side parts of ERC‑4337‑style HTTP 402 payments. The server side needs:
- The address of the USDC token on Base (
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913). - The price in wei (USDC has 6 decimals).
- A signer (or a read‑only provider) to validate signatures.
npm init -y
npm install express x402 ethers
Server code (Node/Express)
// agent.js
import express from 'express';
import { x402Middleware } from 'x402';
import { ethers } from 'ethers';
import OpenAI from 'openai';
const app = express();
app.use(express.json());
// ----- Configuration -----
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PRICE_USDC = 0.02; // $0.02 per call
const PRICE_WEI = ethers.parseUnits(PRICE_USDC.toString(), 6); // 6 decimals
const CHAIN_ID = 8453; // Base
// Provider (read‑only) – you can use Alchemy, Infura, or a public RPC
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
// No signer needed for verification; the middleware checks the signature
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ----- x402 middleware -----
app.use(
'/summarize',
x402Middleware({
tokenAddress: USDC_ADDRESS,
price: PRICE_WEI,
chainId: CHAIN_ID,
// optional: a function to store nonces to prevent replay attacks
nonceStore: async (address, nonce) => {
// In production use Redis or a DB; here we use a Map for demo
if (!global.nonceMap) global.nonceMap = new Map();
const seen = global.nonceMap.get(address) ?? new Set();
if (seen.has(nonce)) throw new Error('Replay attack');
seen.add(nonce);
global.nonceMap.set(address, seen);
},
})
);
// ----- Endpoint -----
app.post('/summarize', async (req, res) => {
const { text } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'Missing "text" field' });
}
try {
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: `Summarize the following:\n\n${text}` }],
temperature: 0.3,
});
const summary = completion.choices[0].message.content.trim();
res.json({ summary });
} catch (err) {
console.error(err);
res.status(502).json({ error: 'Upstream LLM failed' });
}
});
// ----- Start -----
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Agent listening on :${PORT}`));
Explanation of the middleware
-
x402Middlewareexpects the client to send anAuthorizationheader containing a signed payload that encodes the token address, amount, chain ID, and a nonce. - It verifies the signature using the signer’s address (derived from the payload) and checks that the amount matches
PRICE_WEI. - The optional
nonceStoreprevents replay attacks; in a production deployment you’d replace the in‑memoryMapwith Redis or a Postgres table.
4. Client side – how a caller pays
For completeness, here’s a minimal curl‑compatible example using the x402 CLI (you can also generate the header in JavaScript):
# Install the x402 CLI (optional)
npm i -g x402-cli
# Generate a payment header for $0.02 USDC on Base
x402 pay \
--token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
--amount 0.02 \
--chain 8453 \
--private-key $YOUR_PRIVATE_KEY \
--url http://localhost:3000/summarize \
--method POST \
--body '{"text":"The quick brown fox jumps over the lazy dog."}'
The command prints a header like:
Authorization: Bearer x402:<base64-payload>
You then copy that header into your request:
curl -X POST http://localhost:3000/summarize \
-H "Content-Type: application/json" \
-H "Authorization: Bearer x402:<base64-payload>" \
-d '{"text":"The quick brown fox jumps over the lazy dog."}'
If the payment is valid, you receive a JSON response with the summary. If not, you get a 402 Payment Required with a WWW‑Authenticate header that tells the client how to pay.
5. Deployment – Cloudflare Workers (the path to the live example)
I chose Cloudflare Workers because they give sub‑second cold starts, automatic TLS, and a simple way to publish a service at a custom domain. The Worker version of the code is almost identical; the only difference is that you cannot directly import ethers (it’s too large). Instead, I used the lightweight @ethersproject/shim bundle and the built‑in crypto.subtle for signature verification.
js
// worker.js (simplified)
import { x402Middleware } from 'x402/worker';
import { OpenAI } from 'openai';
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== '/summarize' || request.method !== 'POST') {
return new Response('Not found', { status: 404 });
}
// x402 middleware verifies payment; on failure it returns 402
const paymentResp = await x402Middleware({
tokenAddress: '0x833589fCD6eDb6E08f
Top comments (0)