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

By a developer who prefers measurable outcomes over buzzwords


Table of Contents

  1. Problem Statement
  2. High‑Level Architecture
  3. Choosing the Payment Layer: x402 on Base
  4. Agent Core: Task Loop & State Management
  5. Worker Plugins: What the Agent Actually Does
  6. Honest Trade‑offs
  7. Running the Agent in Production
  8. Live Example Catalog

Problem Statement

I wanted an agent that could:

  • Perform a repeatable, billable micro‑task (e.g., data enrichment, simple classification, or API wrapping) without human intervention.
  • Receive payment instantly in a stablecoin (USDC) so that earnings accrue while I’m offline.
  • Stay inexpensive to run – the compute cost should be a small fraction of the revenue per call.

Most “AI‑agent” tutorials stop at the model inference step and ignore the economics of sustaining the service. This post walks through the minimal viable stack I used, the code that ties it together, and the realistic limits you’ll hit if you try to replicate it.


High‑Level Architecture

+-------------------+      +-------------------+      +-------------------+
|  Scheduler (cron) | ---> |  Agent Core Loop  | ---> |  Worker Plugins   |
+-------------------+      +-------------------+      +-------------------+
          ^                         |                         |
          |                         v                         v
   +----------------+        +----------------+        +----------------+
   |  State Store   |        |  Payment Lib   |        |  External APIs |
   +----------------+        +----------------+        +----------------+
Enter fullscreen mode Exit fullscreen mode
  • Scheduler – a simple cron job (or Cloudflare Workers Cron Trigger) that wakes the agent every N minutes.
  • Agent Core – a thin loop that reads pending tasks from a durable store, dispatches them to the appropriate worker, and records the outcome.
  • Worker Plugins – isolated functions that implement the billable service (e.g., “fetch‑price‑ticker”, “summarize‑text‑≤‑100‑words”). Each worker returns a result and a price in USDC micro‑units (1 USDC = 1,000,000 microUSDC).
  • State Store – I used SQLite wrapped in better-sqlite3 for local dev and migrated to a Cloudflare KV namespace for production; it holds task IDs, status, and payout amounts.
  • Payment Lib – the @x402/x402-js client that signs and submits a payment receipt to the x402 gateway on Base.

The flow per tick:

  1. Scheduler invokes agent.js.
  2. Core queries the store for status = 'pending'.
  3. For each task, it loads the worker manifest, runs the worker, captures the output, and computes the microUSDC amount.
  4. It calls the x402 client to create a signed receipt (pay(amount, token)).
  5. On success, the store row is updated to status = 'paid'; on failure, it goes to status = 'error' with a retry counter.

Choosing the Payment Layer: x402 on Base

x402 is a lightweight protocol that lets you attach a payment requirement to an HTTP endpoint. The client signs a JWT‑like receipt with the payer’s private key; the gateway verifies the signature, checks the nonce, and forwards the request to your service only if payment is sufficient.

Why I picked it:

Factor Reason
Instant settlement USDC on Base confirms in ~2 seconds; no waiting for batch payouts.
Low fees Base transaction cost ≈ $0.0001, far below the $0.01–$0.10 per‑call price I target.
Developer ergonomics Official JS/TS client (@x402/x402-js) handles signing, nonce management, and retry logic.
Permissionless No KYC for low‑value micro‑transactions; the agent can operate with a simple EOA.

Drawbacks I encountered:

  • Nonce management – if the agent crashes after signing but before the gateway responds, the nonce is considered used and you must manually reset it (I store the last used nonce alongside the task state).
  • Gateway reliability – the public x402 gateway occasionally returns 502 under load; I added exponential back‑off and a fallback to a self‑hosted gateway for critical workers.
  • Price volatility – USDC is stable, but if you ever want to accept other tokens you’ll need a price‑oracle layer; I kept the design USDC‑only to avoid that complexity.

Agent Core: Task Loop & State Management

Below is the core loop (Node.js ≥ 18). It’s intentionally minimal; you can replace the store adapter with Postgres, DynamoDB, etc.

// agent.js
import { open } from 'better-sqlite3';
import { X402Client } from '@x402/x402-js';
import { loadWorker } from './worker-loader.js';

// --- CONFIG -------------------------------------------------
const DB_PATH = process.env.DB_PATH || './tasks.sqlite';
const PRIVATE_KEY = process.env.PRIVATE_KEY; // Base account holding USDC
const X402_GATEWAY = 'https://gateway.x402.org';
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
// -----------------------------------------------------------

const db = open(DB_PATH);
db.pragma('journal_mode = WAL');

// Ensure table exists
db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id TEXT PRIMARY KEY,
    worker TEXT NOT NULL,
    payload TEXT,
    status TEXT NOT NULL DEFAULT 'pending',
    amountMicroUSDC INTEGER NOT NULL,
    result TEXT,
    nonce INTEGER,
    error TEXT,
    created_at INTEGER DEFAULT (unixepoch())
  );
`);

const x402 = new X402Client({
  privateKey: PRIVATE_KEY,
  gateway: X402_GATEWAY,
});

// Helper: fetch next pending task
function getPendingTask() {
  const stmt = db.prepare('SELECT * FROM tasks WHERE status = "pending" ORDER BY created_at LIMIT 1');
  return stmt.get();
}

// Helper: update task outcome
function updateTask(id, fields) {
  const setClause = Object.keys(fields)
    .map(k => `${k} = ?`)
    .join(', ');
  const vals = Object.values(fields);
  const stmt = db.prepare(`UPDATE tasks SET ${setClause}, updated_at = unixepoch() WHERE id = ?`);
  stmt.run(...vals, id);
}

// Main loop
async function runOnce() {
  const task = getPendingTask();
  if (!task) return; // nothing to do

  console.log(`[${new Date().toISOString()}] Processing task ${task.id}`);

  try {
    const worker = await loadWorker(task.worker);
    const result = await worker.handler(JSON.parse(task.payload || '{}'));

    // Determine price – worker can return {priceMicroUSDC, …}
    const price = typeof result.priceMicroUSDC === 'number'
      ? result.priceMicroUSDC
      : BigInt(task.amountMicroUSDC); // fallback to pre‑set amount

    // Sign & submit payment
    const receipt = await x402.pay({
      amount: price.toString(),
      token: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
    });

    // Store outcome
    updateTask(task.id, {
      status: 'paid',
      result: JSON.stringify(result),
      nonce: receipt.nonce,
    });
    console.log(`✔ Paid ${Number(price)/1e6} USDC for task ${task.id}`);
  } catch (err) {
    console.error(`✘ Failed task ${task.id}:`, err);
    const retries = Number(task.error ? JSON.parse(task.error).retries || 0 : 0);
    updateTask(task.id, {
      status: retries < 3 ? 'pending' : 'error',
      error: JSON.stringify({ message: err.message, retries: retries + 1 }),
    });
  }
}

// If invoked directly, run once; otherwise export for cron
if (require.main === module) {
  runOnce().catch(console.error);
} else {
  module.exports = { runOnce };
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Pulls the oldest pending task.
  • Dynamically loads the worker module (see next section).
  • Executes the worker, reads any declared price, and pays via x402.
  • Updates SQLite with the result, nonce, and status.
  • On error, it retries up to three times before marking the task as errored.

The loop is deliberately synchronous except for the worker and payment calls; this keeps reasoning about state simple. If you need higher throughput, you can run multiple instances with a distributed lock (e.g., using KV lock API) or switch to a work‑queue model.


Worker Plugins: What the Agent Actually Does

A worker is just an ES module exporting a handler function and an optional priceMicroUSDC. Below are two realistic examples I use in production.

1. Simple Text Summarizer (uses a small HuggingFace model via the onnxruntime backend)


javascript
// workers/summarizer.js
import { sessionFrom } from 'onnxruntime-node';
import { pipeline } from '@xenova/transformers';

// Load model once per worker instance (cold
Enter fullscreen mode Exit fullscreen mode

Top comments (0)