DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

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

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

Target audience: developers interested in creating self‑sustaining AI services that receive micropayments in USDC.


Overview

The goal was to run an always‑on agent that performs a narrow, well‑defined task (text summarization) and gets paid per invocation in USDC on the Base blockchain. The agent does not “learn” while it sleeps; it simply waits for requests, validates a payment, runs a model, and returns the result. The architecture is deliberately minimal to keep operational complexity low and to make trade‑offs explicit.

Core components

Component Responsibility Chosen tech Why
Request handling & payment verification Accept HTTP POST, check x402 payment header, reject if insufficient Cloudflare Workers (JavaScript) Serverless, global edge, free tier sufficient for low traffic
AI inference Run a summarization model on the input text HuggingFace transformers.js (distilbart‑cnn‑12‑6) Runs entirely in the Worker sandbox, no external API keys, modest size (~50 MB)
Payment settlement Record earned USDC for later withdrawal Simple server‑side counter (KV store) + manual claim via a wallet Avoids integrating a full smart contract; x402 already guarantees payment off‑chain
Observability Log requests, errors, and earnings Built‑in Worker logs + optional external logging service Minimal overhead, sufficient for debugging

The flow for each request is:

  1. Client sends POST /summarize with JSON { "text": "…" } and an X-Payment header containing a signed x402 proof.
  2. Worker verifies the proof against the agent’s public key and the configured price (e.g., $0.02 USDC).
  3. If verification passes, the Worker runs the model, returns the summary, and increments an earnings counter.
  4. If verification fails, the Worker returns 402 Payment Required with a helpful error message.

Payment verification with x402

x402 defines a HTTP‑based scheme for attaching a cryptographic proof of payment to a request. The proof consists of:

  • payload: the request method, path, and body hash.
  • signature: an ECDSA signature over the payload using the payer’s private key.
  • paywall: the price in USDC (encoded as a 64‑bit integer with 6 decimals).

The worker needs the agent’s public key to verify the signature. Below is a stripped‑down verification function using the ethers library (available via CDN in Workers).

// x402Verify.js
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.7.0/dist/ethers.min.js";

const AGENT_PUBLIC_KEY = "0xA1b2C3d4E5f67890..."; // replace with your agent's address

export async function verifyX402(request, expectedPriceMicroUSDC) {
  const payloadHeader = request.headers.get("X-Payload");
  const sigHeader     = request.headers.get("X-Signature");
  const paywallHeader = request.headers.get("X-Paywall");

  if (!payloadHeader || !sigHeader || !paywallHeader) {
    throw new Error("Missing x402 headers");
  }

  // Reconstruct the payload that was signed
  const payload = JSON.parse(atob(payloadHeader));
  // Expected shape: { method, path, bodyHash }
  const { method, path, bodyHash } = payload;

  // Verify paywall amount (USDC with 6 decimals)
  const price = BigInt(paywallHeader);
  if (price < BigInt(expectedPriceMicroUSDC)) {
    throw new Error(`Insufficient payment: ${Number(price)/1e6} USDC`);
  }

  // Re‑create the signed message
  const message = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ["string", "string", "string"],
      [method, path, bodyHash]
    )
  );

  // Recover signer address from signature
  const signer = ethers.utils.recoverAddress(message, sigHeader);
  return signer.toLowerCase() === AGENT_PUBLIC_KEY.toLowerCase();
}
Enter fullscreen mode Exit fullscreen mode

Trade‑off: Verifying signatures in a Worker adds ~2‑3 ms of CPU time. For low‑volume services this is negligible; for high‑throughput endpoints you might move verification to a dedicated validator or batch multiple requests.


Model inference inside the Worker

Running a transformer model directly in a Worker is possible with transformers.js, which compiles the model to WebAssembly. The model we chose (Xenova/distilbart-cnn-12-6) is ~50 MB and produces acceptable summaries for short paragraphs (< 500 tokens).

// summarizer.js
import { pipeline } from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.8.0/dist/index.min.js";

let summarizer = null;

async function getSummarizer() {
  if (!summarizer) {
    summarizer = await pipeline("summarization", "Xenova/distilbart-cnn-12-6");
  }
  return summarizer;
}

export async function summarize(text) {
  const pipe = await getSummarizer();
  // Model expects <1024 tokens; truncate if needed
  const maxLength = 130; // summary length
  const minLength = 30;
  const result = await pipe(text, { max_length: maxLength, min_length: minLength });
  return result[0].summary_text;
}
Enter fullscreen mode Exit fullscreen mode

Trade‑off: The initial load of the model incurs a cold‑start penalty (~400 ms on the first request). Subsequent requests reuse the cached instance, dropping latency to ~120 ms for a 200‑token input. If sub‑100 ms latency is required, you would need to pre‑warm the Worker or move inference to a GPU‑enabled service (e.g., RunPod) – at the cost of higher operational complexity and expense.


Putting it together in a Cloudflare Worker

// index.js
import { verifyX402 } from "./x402Verify.js";
import { summarize } from "./summarizer.js";

// Price per call: 0.02 USDC = 20,000 micro‑USDC
const PRICE_MICRO_USDC = 20000;

// Simple KV namespace bound as "EARNINGS"
export default {
  async fetch(request, env, ctx) {
    if (request.method !== "POST" || new URL(request.url).pathname !== "/summarize") {
      return new Response("Not Found", { status: 404 });
    }

    // 1️⃣ Verify payment
    try {
      await verifyX402(request, PRICE_MICRO_USDC);
    } catch (e) {
      return new Response(e.message, { status: 402, headers: { "Content-Type": "text/plain" } });
    }

    // 2️⃣ Parse body
    let body;
    try {
      const json = await request.json();
      if (!json.text || typeof json.text !== "string") throw new Error();
      body = json.text;
    } catch (_) {
      return new Response('Invalid JSON: expected { "text": "string" }', { status: 400 });
    }

    // 3️⃣ Run summarization
    let summary;
    try {
      summary = await summarize(body);
    } catch (e) {
      console.error("Summarization failed:", e);
      return new Response("Internal error", { status: 500 });
    }

    // 4️⃣ Record earnings (optional)
    ctx.waitUntil(
      env.EARNINGS.add("total", 1).then(() => env.EARNINGS.add("usdc", PRICE_MICRO_USDC))
    );

    // 5️⃣ Return result
    return new Response(JSON.stringify({ summary }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Explanation of the snippet

  • The Worker checks the HTTP method and path early to avoid unnecessary work.
  • Payment verification is performed before any model inference, guaranteeing that we never spend compute on unpaid requests.
  • Earnings are aggregated in a KV store (EARNINGS) using ctx.waitUntil so the response isn’t delayed by the write.
  • All heavy lifting (model load, inference) stays inside the Worker; no external API keys are required, reducing attack surface.

Operational considerations & honest trade‑offs

Aspect What we chose Pros Cons / Limitations
Compute Serverless Worker + WASM model No server management, automatic scaling, free tier covers low traffic Cold start latency, limited CPU (no GPU), model size constrained by Worker memory (~128 MB)
Payment x402 off

Top comments (0)