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 who want to ship self‑sustaining, pay‑per‑use AI services.


1. Why “earn while I sleep” isn’t magic

An autonomous agent that earns USDC does not create value out of thin air. It must:

  1. Expose a useful capability (e.g., text summarisation, sentiment analysis, code completion).
  2. Collect payment for each invocation via a verifiable, low‑overhead mechanism.
  3. Run continuously with minimal operational overhead so the developer can step away.

The x402 protocol satisfies #2: it lets an HTTP endpoint require a micropayment in USDC on the Base L2 before returning a response. The client pays, the gateway verifies the transaction, and the request proceeds to your backend.

My implementation focuses on a summarisation service because it’s easy to demo, has predictable latency, and works well with small open‑source models. The same pattern applies to any other callable function.


2. High‑level architecture

+-------------------+      x402 payment      +-------------------+
|  Client (any HTTP) | ---------------------> |  x402 Gateway (CF)|
+-------------------+                        +-------------------+
                                          |  Verify USDC payment |
                                          +----------+----------+
                                                     |
                                                     v
+-------------------+      Internal RPC      +-------------------+
|  Agent Worker (CF) | <-------------------- |  Model Inference  |
|  (summarise)      |   (optional fallback) |  (ONNX / HF API)  |
+-------------------+                        +-------------------+
Enter fullscreen mode Exit fullscreen mode
  • x402 Gateway – a Cloudflare Workers script that enforces the payment header.
  • Agent Worker – another Cloudflare Workers script that receives the request after payment, calls the model, and returns the result.
  • Model Inference – either a tiny ONNX model bundled with the Worker (for offline, zero‑cost inference) or a call to a hosted inference API (e.g., HuggingFace Inference Endpoints) when higher quality is needed.

The agent is “autonomous” because once deployed it needs no further human interaction: it waits for paid requests, processes them, and accrues USDC in its wallet.


3. Setting up the x402 payment gateway

Cloudflare Workers provide a cheap, globally distributed runtime. The gateway does three things:

  1. Reads the X402-Payment header (base64‑encoded JSON with payer address, amount, and transaction hash).
  2. Calls the Base RPC to confirm the transaction succeeded and matches the expected amount.
  3. If valid, forwards the request to the Agent Worker; otherwise returns 402 Payment Required.
// x402-gateway.js
import { Base } from 'viem/chains';
import { getTransactionReceipt } from 'viem/actions';

const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const REQUIRED_AMOUNT = BigInt(1000000); // 0.001 USDC (6 decimals)

export default {
  async fetch(request, env, ctx) {
    const paymentHeader = request.headers.get('X402-Payment');
    if (!paymentHeader) {
      return new Response('Missing X402-Payment header', { status: 402 });
    }

    let payload;
    try {
      payload = JSON.parse(atob(paymentHeader));
    } catch (_) {
      return new Response('Invalid X402-Payment format', { status: 400 });
    }

    const { from, to, value, hash } = payload;
    if (to.toLowerCase() !== env.AGENT_WALLET.toLowerCase()) {
      return new Response('Incorrect payee', { status: 402 });
    }
    if (value !== REQUIRED_AMOUNT.toString()) {
      return new Response('Incorrect amount', { status: 402 });
    }

    // Verify on‑chain
    const receipt = await getTransactionReceipt(Base, { hash });
    if (receipt.status !== 'success') {
      return new Response('Payment not confirmed', { status: 402 });
    }

    // Payment OK – forward to agent worker
    const agentUrl = new URL(request.url);
    agentUrl.hostname = env.AGENT_WORKER_HOST; // set via wr.toml
    return fetch(agentUrl, {
      method: request.method,
      headers: request.headers,
      body: request.body,
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Trade‑offs

Aspect Choice Reason Drawback
Gateway location Cloudflare Workers (global edge) Low latency, no server management, free tier sufficient for low traffic Limited execution time (50 ms CPU) – fine for verification only
Payment verification Direct RPC call via viem Trustless, no third‑party relayer Adds ~150‑200 ms latency; could be cached for repeat nonces if you accept replay risk
Token USDC on Base Low gas (~$0.0001) and stable value Requires Base RPC endpoint (free tier from Infura/Alchemy works)

4. The Agent Worker – summarisation logic

After the gateway confirms payment, the request reaches the Agent Worker. It extracts the text to summarise, runs the model, and returns the result. I used a distilbert‑base‑uncased‑distilled‑squad‑style summariser converted to ONNX and bundled with the Worker (≈12 MB). For higher quality you can swap the model for a hosted endpoint; the code stays the same.

// agent-worker.js
import { createInferenceSession } from 'onnxruntime-web';

// Load model once at cold start (cached across invocations)
let session;
async function loadModel() {
  if (!session) {
    const response = await fetch('model/distilbart-xsum-12-3.onnx');
    const arrayBuffer = await response.arrayBuffer();
    const { default: Ort } = await import('onnxruntime-web');
    session = await Ort.InferenceSession.create(arrayBuffer, {
      executionProviders: ['wasm'],
    });
  }
  return session;
}

// Simple tokenizer placeholder – replace with a real one (e.g., HuggingFace tokenizers)
function tokenize(text) {
  // For demo, we just split on whitespace and map to IDs via a tiny vocab.
  // In production use @xenova/transformers or similar.
  const vocab = JSON.parse(await fetch('model/vocab.json').then(r => r.text()));
  return text
    .toLowerCase()
    .split(/\s+/)
    .map(tok => vocab[tok] ?? vocab['[UNK]']);
}

function detokenize(ids) {
  const invVocab = JSON.parse(await fetch('model/inv_vocab.json').then(r => r.text()));
  return ids.map(i => invVocab[i] || '').join(' ');
}

export default {
  async fetch(request, env, ctx) {
    if (request.method !== 'POST') {
      return new Response('Only POST allowed', { status: 405 });
    }

    const { text } = await request.json();
    if (!text || typeof text !== 'string') {
      return new Response('Missing "text" field', { status: 400 });
    }

    // Ensure model is ready
    const sess = await loadModel();

    // Pre‑process (tokenize + pad/truncate to model's max length)
    const maxLength = 512;
    const inputIds = tokenize(text).slice(0, maxLength);
    const attentionMask = inputIds.map(() => 1);
    // Pad
    while (inputIds.length < maxLength) {
      inputIds.push(0);
      attentionMask.push(0);
    }

    // Run inference
    const feeds = {
      input_ids: new Int64Array(inputIds),
      attention_mask: new Int64Array(attentionMask),
    };
    const results = await sess.run(feeds);
    const logits = results.logits; // shape [1, seq_len, vocab_size]
    const predictedIds = Array.from(
      new Int32Array(logits.data).map((v, i) => (i % logits.dims[2] === 0 ? v : 0))
    ).filter((v, i) => i % logits.dims[2] === logits.dims[2] - 1); // argmax per step

    const summary = detokenize(predictedIds);
    return new Response(JSON.stringify({ summary }), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Trade‑offs

Aspect Choice Reason Drawback
Model size DistilBART‑XSUM (~12 MB ONNX) Fits within Workers bundle, cold start < 300 ms

Top comments (0)