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 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)|
                                 +-------------------------+
Enter fullscreen mode Exit fullscreen mode
  • The agent service is a stateless HTTP endpoint.
  • On each request it:
    1. Checks for a valid x402 payment proof (USDC amount, token address, chain ID, signature).
    2. If the proof is valid, runs the AI workload.
    3. Returns the result (or a 402 with a payment request if missing/invalid).

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 x402 npm package (provides helper functions for constructing and verifying payment headers).
npm init -y
npm install express x402 ethers @xenova/transformers
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

What this does:

  • If the header is missing or malformed, we return a 402 with a paymentRequest object 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 };
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

Key points in the code:

  • The x402PaymentMiddleware runs 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)