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 are already comfortable with async JavaScript/TypeScript, REST APIs, and basic blockchain concepts.


1. Why “earn while I sleep” is a engineering problem, not a magic trick

An autonomous agent that receives micropayments for useful work must solve three intertwined concerns:

  1. Task generation & execution – the agent must decide what to do, run the work, and produce a verifiable output.
  2. Micro‑payment settlement – each unit of work must be tied to a cryptographic payment request that the caller can satisfy instantly, without custodial risk.
  3. Operational safety – the agent runs unattended; failures must not drain funds or expose keys.

The x402 protocol (HTTP 402 Payment Required) gives us a lightweight way to attach a payment request to any HTTP endpoint. By making the agent’s “skill” endpoints return a 402 when unpaid, callers can pay in USDC on Base and receive the result immediately. The agent itself never holds funds; it merely signs the payment request with a wallet that the caller validates.

Below is a walk‑through of the pieces I assembled, the code that makes them work, and the honest trade‑offs I encountered along the way.


2. High‑level architecture

+-------------------+        +---------------------+        +-------------------+
|  Scheduler (cron) | --->   |  Agent Loop (Worker)| --->   |  x402‑Enabled API |
+-------------------+        +---------------------+        +-------------------+
        ^                           |                                 |
        |                           v                                 v
   +----------------+        +----------------+                +----------------+
   |  State Store   |        |  Skills (FSM)  |                |  Wallet/Signer |
   +----------------+        +----------------+                +----------------+
Enter fullscreen mode Exit fullscreen mode
  • Scheduler – a lightweight Cloudflare Cron Trigger that wakes the agent every 5‑15 minutes to check for pending work (e.g., “scan new GitHub issues”, “summarize RSS feeds”).
  • Agent Loop – a Durable Object (or a simple Worker with KV state) that runs a deterministic finite‑state machine: idle → fetch task → execute skill → request payment → return result.
  • x402‑Enabled API – each skill is exposed as an HTTP handler that, if the request lacks a valid Payment header, responds with 402 Payment Required and a JSON‑encoded payment payload.
  • State Store – Cloudflare KV (or Durable Object storage) holds the agent’s nonce, last‑run timestamps, and a pointer to the wallet address used for signing.
  • Wallet/Signer – an ethers.js signer loaded from a secret stored in Cloudflare Secrets (the private key never touches logs).

The agent never moves USDC; it only signs a payment request that the caller must fulfill. The caller’s payment settles on Base via the x402 relayer (or a custom relayer you run). This keeps the agent’s attack surface limited to signing, not custody.


3. Core code snippets

3.1 Agent loop (Durable Object)

import { DurableObject } from "cloudflare:workers";
import { ethers } from "ethers";
import { x402PaymentRequest } from "./x402";

interface Env {
  // Secrets set via wrangler
  PRIVATE_KEY: string;
  WALLET_ADDRESS: string;
  // KV binding for lightweight persistence
  AGENT_STATE: KVNamespace;
}

export class AgentDO extends DurableObject<Env> {
  private signer: ethers.Wallet;
  private state: Record<string, any>;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    this.signer = new ethers.Wallet(env.PRIVATE_KEY);
    this.state = {}; // will be hydrated from KV
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/tick") {
      return this.handleTick();
    }
    // Fallback to skill endpoints (see §3.2)
    return new Response("Not found", { status: 404 });
  }

  /** Called by the Cron Trigger every N minutes */
  private async handleTick(): Promise<Response> {
    // 1️⃣ Load persisted state (nonce, last run, etc.)
    await this.loadState();

    // 2️⃣ Choose a task – here we just poll a public RSS feed
    const task = await this.fetchNextTask();
    if (!task) {
      return new Response("No work available", { status: 204 });
    }

    // 3️⃣ Execute the skill (example: summarize article)
    const result = await this.executeSkill("summarize", task.payload);

    // 4️⃣ Build a payment request for the result
    const payment = await x402PaymentRequest(
      this.signer,
      this.env.WALLET_ADDRESS,
      // price in USDC (6 decimals on Base)
      ethers.parseUnits("0.05", 6), // $0.05 per summary
      // a unique identifier to prevent replay
      ethers.keccak256(ethers.toUtf8Bytes(`${this.state.nonce}:summarize`)),
      // optional metadata the buyer can inspect
      { taskId: task.id, resultHash: ethers.keccak256(ethers.toUtf8Bytes(result)) }
    );

    // 5️⃣ Increment nonce and persist
    this.state.nonce = (this.state.nonce ?? 0) + 1;
    await this.saveState();

    // 6️⃣ Return the raw result; the caller must attach the payment header
    //    (the x402 middleware will enforce it)
    return new Response(result, {
      headers: { "Content-Type": "text/plain" },
    });
  }

  // -----------------------------------------------------------------
  // Boiler‑plate KV helpers – replace with Durable Object storage if
  // you need stronger consistency.
  // -----------------------------------------------------------------
  private async loadState() {
    const raw = await this.env.AGENT_STATE.get("state") ?? "{}";
    this.state = JSON.parse(raw);
  }
  private async saveState() {
    await this.env.AGENT_STATE.put("state", JSON.stringify(this.state));
  }

  // -----------------------------------------------------------------
  // Stub implementations – plug in your own sources / models.
  // -----------------------------------------------------------------
  private async fetchNextTask() {
    // Example: pull latest item from a public RSS feed
    const feed = await fetch("https://example.com/rss.xml").then(r => r.text());
    // …parse and return {id, payload}
    return { id: "1", payload: "Sample article text" };
  }

  private async executeSkill(skill: string, payload: string): Promise<string> {
    if (skill === "summarize") {
      // Very naive summarizer – replace with an LLM call or ML model.
      return payload.split(". ").slice(0, 2).join(". ") + ".";
    }
    throw new Error(`Unknown skill: ${skill}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

What this does

  • The Cron Trigger (/tick) drives the agent’s autonomy.
  • Each cycle fetches a unit of work, runs a deterministic skill, then builds an x402 payment request that encodes price, nonce, and a hash of the result.
  • The agent returns the raw result only after the caller supplies a valid Payment header; otherwise the x402 middleware (see below) will reply with 402.

3.2 x402 middleware (Worker)

import { verifyPayment } from "x402-verifier"; // tiny helper you can copy from the spec

export async function onRequest(context) {
  const { request, env, next } = context;

  // Skip verification for the internal /tick endpoint
  if (request.url.endsWith("/tick")) {
    return await next();
  }

  const auth = request.headers.get("Payment");
  if (!auth) {
    // Build a 402 response that includes the payment payload the agent expects
    const paymentReq = await x402PaymentRequest(
      env.SIGNER, // same signer used by the agent
      env.WALLET_ADDRESS,
      ethers.parseUnits("0.05", 6),
      ethers.keccak256(ethers.toUtf8Bytes("placeholder-nonce:summarize"))
    );
    return new Response(JSON.stringify({ error: "payment required", payment: paymentReq }), {
      status: 402,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Verify the payment matches what the agent would have asked for
  const valid = await verifyPayment(auth, env.WALLET_ADDRESS, /* expected amount */);
  if (!valid) {
    return new Response("Invalid payment", { status: 402 });
  }

  // Payment good – let the request hit the skill handler
  return await next();
}
Enter fullscreen mode Exit fullscreen mode

The verifier checks the signature, the nonce, and that the amount corresponds to the skill’s price.

3.3 Wrangler configuration (excerpt)


toml
name = "nexusai-x402"
main = "src/index.ts"
compatibility_date = "2024-09-01"

[vars]
ENVIRONMENT = "production"

[[kv_namespaces]]
binding = "AGENT_STATE"
id = "your_kv_namespace_id"

[[triggers]]
crons = ["*/10 * * * *
Enter fullscreen mode Exit fullscreen mode

Top comments (0)