How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Target audience: developers who want to run a paid, self‑serving AI service without constant manual oversight.
1. Why a paid agent?
When an AI model is exposed as a public API, every request consumes compute. If you can attach a tiny, verifiable payment to each call, the service can fund its own hosting and even generate a modest surplus. The x402 protocol (a lightweight extension of HTTP 402 Payment Required) lets you do exactly that: the client includes a signed proof of payment in the request header, and the server validates it before executing any logic.
On the Base L2, USDC is cheap to move (sub‑cent gas) and widely supported by wallets, making it a practical token for micro‑transactions.
2. High‑level architecture
+----------------+ x402 header (USDC proof) +-----------------+
| Client (any) | --------------------------------> | Agent Service |
| | <------------------------------ | (Node/Worker) |
+----------------+ 200 OK + result (or 402) +-----------------+
^ |
| Validate payment (x402)|
+-------------------------+
- The agent service is a stateless HTTP endpoint.
- On each request it:
- Checks for a valid
x402payment proof (USDC amount, token address, chain ID, signature). - If the proof is valid, runs the AI workload.
- Returns the result (or a 402 with a payment request if missing/invalid).
- Checks for a valid
Because the service does not store state between calls, it can be deployed to any serverless platform (Cloudflare Workers, Vercel, AWS Lambda) and scale to zero when idle.
3. Prerequisites
- Node ≥ 18 (or Bun/Deno if you prefer).
- A wallet that can sign USDC transfers on Base (e.g., MetaMask, Rainbow).
- Infura/Alchemy or a public Base RPC endpoint for reading transaction receipts (optional, but useful for debugging).
- The
x402npm package (provides helper functions for constructing and verifying payment headers).
npm init -y
npm install express x402 ethers @xenova/transformers
-
express– simple HTTP server (swap for a Worker if you like). -
ethers– low‑level Ethereum/Base utilities (used by x402 under the hood). -
@xenova/transformers– a tiny, browser‑compatible Transformers implementation that runs in Node without a GPU; good enough for demo workloads like text summarization.
4. Core payment verification middleware
The x402 library expects a header of the form:
X402-Payment: <scheme>://<token-address>:<chain-id>:<amount>:<timestamp>:<signature>
where <signature> is an ECDSA signature over the concatenated fields (signed by the payer’s address).
Below is a minimal Express middleware that validates the header and attaches the payer address to req.payer for downstream use.
// paymentMiddleware.js
const { verifyPayment } = require('x402');
const { ethers } = require('ethers');
const USDC_ADDRESS_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const CHAIN_ID_BASE = 8453;
/**
* Express middleware: validates x402 USDC payment.
* Expected payment amount is passed via env var MIN_PAYMENT_USDC (e.g., 0.01).
*/
function x402PaymentMiddleware(req, res, next) {
const header = req.get('X402-Payment');
if (!header) {
return res.status(402).json({
error: 'Payment required',
// Help the client construct a valid payment request
paymentRequest: {
scheme: 'x402',
token: USDC_ADDRESS_BASE,
chainId: CHAIN_ID_BASE,
amount: process.env.MIN_PAYMENT_USDC || '0.01',
// The server signs a nonce to prevent replay; omitted for brevity.
},
});
}
try {
const { payer, valid } = verifyPayment({
paymentHeader: header,
tokenAddress: USDC_ADDRESS_BASE,
chainId: CHAIN_ID_BASE,
// Expected amount in USDC (6 decimals). Convert string towei‑like units.
requiredAmount: ethers.parseUnits(
process.env.MIN_PAYMENT_USDC || '0.01',
6
),
// Optional: a nonce or timestamp window to stop replays.
// Here we accept any timestamp within the last 5 minutes.
maxAge: 5 * 60,
});
if (!valid) {
throw new Error('Invalid signature or amount');
}
// Attach payer for logging or access‑control downstream.
req.payer = ethers.getAddress(payer);
next();
} catch (err) {
console.warn('x402 verification failed:', err.message);
return res.status(402).json({
error: 'Payment verification failed',
details: err.message,
});
}
}
module.exports = x402PaymentMiddleware;
What this does:
- If the header is missing or malformed, we return a
402with apaymentRequestobject that tells the client exactly what to pay. - If the signature checks out and the amount meets the threshold, we let the request proceed.
5. The AI workload (example: text summarization)
For demonstration we use a small, CPU‑friendly model from the 🤗 Transformers library: sshleifer/distilbart-cnn-12-6. It produces decent summaries in < 300 ms on a modern vCPU.
// summarizer.js
const { pipeline } = require('@xenova/transformers');
let summarizer = null;
/**
* Lazily loads the model the first time it's needed.
* In a serverless environment this cost is paid on cold start.
*/
async function getSummarizer() {
if (!summarizer) {
summarizer = await pipeline('summarization', 'sshleifer/distilbart-cnn-12-6');
}
return summarizer;
}
/**
* Returns a summary of the supplied text.
* @param {string} inputText - Text to summarize (max ~1000 tokens for this model).
* @returns {Promise<string>}
*/
async function summarize(inputText) {
const model = await getSummarizer();
const result = await model(inputText, {
max_length: 130,
min_length: 30,
do_sample: false,
});
return result[0].summary_text;
}
module.exports = { summarize };
6. Wiring it together
// index.js
require('dotenv').config(); // loads MIN_PAYMENT_USDC, RPC_URL, etc.
const express = require('express');
const x402PaymentMiddleware = require('./paymentMiddleware');
const { summarize } = require('./summarizer');
const app = express();
app.use(express.json({ limit: '1mb' })); // small payloads only
app.post('/summarize', x402PaymentMiddleware, async (req, res) => {
const { text } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'Missing or invalid "text" field' });
}
try {
const summary = await summarize(text);
// Optionally log the payer for revenue tracking.
console.info(`Paid request from ${req.payer}: ${text.length} → ${summary.length} chars`);
res.json({ summary });
} catch (err) {
console.error('Summarization failed:', err);
res.status(500).json({ error: 'Internal error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Agent listening on :${PORT}`);
});
Key points in the code:
- The
x402PaymentMiddlewareruns before any business logic, guaranteeing that no compute is spent on an unverified request. - The model is loaded lazily; in a serverless platform this means you pay the cold‑start cost only when the function is invoked.
Top comments (0)