How I Built an Autonomous AI Agent That Earns USDC While I Sleep
Autonomous agents that can monetise their own capabilities are an interesting experiment in programmable economics. Below is a walk‑through of the system I assembled, the trade‑offs I hit, and the concrete code that makes it work. The goal is not to sell a “get‑rich‑quick” idea but to show what a minimal, production‑grade agent looks like when you combine an LLM, a serverless runtime, and the x402 micropayment standard.
1. Why x402 and USDC on Base?
-
x402 is a HTTP‑status‑code‑based payment protocol (spec ERC‑4337‑style). A client receives a
402 Payment Requiredresponse that includes a payment payload; after paying, the client resends the request with a signed proof. - Using USDC on Base gives us a stable‑value token with low gas fees (≈ $0.0005 per transaction) and instant finality, which is practical for sub‑cent micropayments.
- The combination lets us expose any HTTP endpoint as a pay‑per‑call service without building a custom billing system.
2. High‑Level Architecture
+-------------------+ +-------------------+ +---------------------+
| Client (any) |------>| Cloudflare Worker |------>| LLM Inference Service|
| (HTTP + x402) | | (entrypoint) | | (e.g., Replicate) |
+-------------------+ +-------------------+ +---------------------+
^ ^ ^
| | |
| x402 proof verification| LLM request/response |
| | |
| v v
| +-------------------+ +-------------------+
| | KV Cache (optional) | | Wallet (USDC) |
| +-------------------+ +-------------------+
| ^ ^
| | |
+-------------------------+-------------------------+
Shared state (usage metrics, rate limits)
- Cloudflare Workers act as the thin API gateway. They verify the x402 proof, enforce rate limits, call the LLM, and return the result.
- The LLM inference service can be any HTTP‑accessible model (I used a public Replicate endpoint for Llama‑2‑7b‑chat, but you can self‑host).
- A KV namespace stores short‑lived nonces to prevent replay attacks and a simple usage counter for each API key.
- The wallet (managed via
ethers.jsorviem) holds the USDC balance; the worker only needs the contract address and the payer’s signature, not the private key.
3. Payment Flow (x402)
-
Client request →
GET /summarize?text=… - Worker checks for the
X-402-Payment-Requiredheader. If missing, it returns:
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-402-Payment-Required: {"scheme":"exact","network":"base","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","maxAmount":"1000000","resource":"https://nexusai-x402.nikhilranka23.workers.dev/summarize"}
-
maxAmountis in USDC’s smallest unit (6 decimals → $0.10 = 100 000).- The client builds a payment (using the x402.js library or manually) and resends the request with:
Authorization: Bearer <signed_payment>
X-402-Payment: <payload>
- The worker verifies the signature against the payer’s address, checks that the amount ≤
maxAmount, and increments a nonce‑based replay guard. - If verification passes, the request proceeds to the LLM step.
4. Worker Code (TypeScript)
Below is a trimmed‑down version that you can paste into a Cloudflare Workers project (wrangler.toml → compatibility_date = "2024-09-01").
ts
import { ethers } from "ethers";
// ---- CONFIG -------------------------------------------------
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const LLM_ENDPOINT = "https://api.replicate.com/v1/predictions";
const REPLICATE_TOKEN = REPLICATE_API_TOKEN; // set in wrangler secrets
const KV = await import("@cloudflare/kv-asset-handler"); // for demo only
// -----------------------------------------------------------
interface X402Payload {
scheme: string; // "exact"
network: string; // "base"
asset: string; // token address
maxAmount: string; // uint256 as string
resource: string; // the URL being paid for
}
// Helper: verify the x402 signature (EIP‑191)
async function verifyPayment(
payload: X402Payload,
signature: string,
payer: string
): Promise<boolean> {
const message = ethers.utils.arrayify(
ethers.utils.solidityKeccak256(
["string", "address", "uint256", "string"],
[
payload.scheme,
payload.asset,
ethers.utils.parseUnits(payload.maxAmount, 6),
payload.resource,
]
)
);
const recovered = ethers.utils.recoverAddress(
ethers.utils.hashMessage(message),
signature
);
return recovered.toLowerCase() === payer.toLowerCase();
}
// Main handler
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// Only protect the summarise endpoint for this example
if (path !== "/summarize") {
return new Response("Not found", { status: 404 });
}
// ---- 1️⃣ Extract x402 headers ---------------------------------
const paymentHeader = request.headers.get("X-402-Payment");
const authHeader = request.headers.get("Authorization")?.replace(
/^Bearer\s+/i,
""
);
if (!paymentHeader || !authHeader) {
// Ask for payment
const payload: X402Payload = {
scheme: "exact",
network: "base",
asset: USDC_ADDRESS,
maxAmount: "100000", // $0.10 in USDC (6 decimals)
resource: request.url,
};
return new Response(JSON.stringify(payload), {
status: 402,
headers: { "Content-Type": "application/json", "X-402-Payment-Required": JSON.stringify(payload) },
});
}
// ---- 2️⃣ Parse and verify payment --------------------------------
let payload: X402Payload;
let signature: string;
try {
const { scheme, network, asset, maxAmount, resource } = JSON.parse(
paymentHeader
) as X402Payload;
payload = { scheme, network, asset, maxAmount, resource };
signature = authHeader;
} catch {
return new Response("Bad payment format", { status: 400 });
}
// In a real system you'd extract the payer address from the signature
// via EIP‑712 or from the payment metadata. For brevity we assume
// the payer is the first argument of the signature (not correct,
// just illustrative). Replace with proper EIP‑712 verification.
const payer = "0x0000000000000000000000000000000000000000"; // placeholder
const valid = await verifyPayment(payload, signature, payer);
if (!valid) {
return new Response("Invalid payment", { status: 402 });
}
// ---- 3️⃣ Simple replay guard (nonce stored in KV) ----------------
// In production you'd use a proper nonce or use the x402.js library.
const nonceKey = `nonce:${payer}:${payload.resource}`;
const seen = await env.NONCE_KV.get(nonceKey);
if (seen) {
return new Response("Replay attack detected", { status: 409 });
}
await env.NONCE_KV.put(nonceKey, "1", { expirationTtl: 60 }); // 1‑min window
// ---- 4️⃣ Call the LLM -------------------------------------------
const text = url.search
Top comments (0)