cd /news/ai-agents/personal-ai-agent-on-cloudflare-free… · home › topics › ai-agents › article
[ARTICLE · art-150765] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Personal AI Agent on Cloudflare Free Tier 2026 (Code)

A developer has published a complete guide and code for running a personal AI agent on Cloudflare's free tier, costing $0 per month for roughly 70 Telegram conversations a day and deploying in about 15 minutes. The setup uses Workers for the Telegram webhook entry point, SQLite-backed Durable Objects for per-chat memory, and Workers AI's 10,000 free daily neurons for inference, with the guide noting that neurons are the first free quota to run out, followed by Durable Object row writes and CPU time. The writeup includes a cost comparison showing Workers AI at $0 versus about $0.36/month for a bring-your-own Claude Haiku 5.5 key and $7.20/month for Sonnet 5.5 at the same usage.

by read11 min views3 publishedOct 15, 2026

A personal AI agent on Cloudflare's free tier costs $0 a month for about 70 Telegram conversations a day, and it deploys in 15 minutes. The Workers free plan gives you 100,000 requests a day, Durable Objects give each chat its own SQLite memory, and Workers AI includes 10,000 free neurons daily. The first thing you run out of is neurons, not requests. Below you get the free quotas (checked against Cloudflare's docs on 15 October 2026), a Workers AI versus bring-your-own-key cost comparison in dollars, and a complete project: wrangler.jsonc, an Agents SDK class, and a Telegram webhook handler. It builds on the Cloudflare Workers guide and the Agents Week memory guide.

Free plan: 100,000 Worker requests a day, 10 ms CPU per request, 100,000 Durable Object requests a day.

Workers AI: 10,000 neurons a day free. At 30 chats a day on Llama 3.3 70B that is about 4,000 neurons, so $0.

Bring your own Claude Haiku 5.5 key: about $0.36 a month at the same usage. Sonnet 5.5: about $7.20.

Per-chat memory lives in the agent's built-in SQLite. No D1 needed to start.

Outgrow order: neurons first, then Durable Object row writes, then CPU time.

These are the numbers that matter for an agent. All daily limits reset at 00:00 UTC.

Service Free quota What it does for the agent

| Workers | 100,000 requests/day, 10 ms CPU per request, 50 subrequests, 5 cron triggers | Telegram webhook entry point |

| Durable Objects | 100,000 requests/day, 13,000 GB-s/day, 5M rows read/day, 100,000 rows written/day, 5 GB SQLite | One agent instance per user, with memory |

| Workers AI | 10,000 neurons/day | Model inference with no API key |

| KV | 100,000 reads/day, 1,000 writes/day, 1 GB | Config and cached lookups |

| D1 | 5M rows read/day, 100,000 rows written/day, 5 GB | Shared data across users |

| R2 | 10 GB-month, 1M Class A and 10M Class B ops/month | Files the agent saves |

| Vectorize | 5M stored and 30M queried dimensions/month | Semantic memory |

| Queues | 10,000 operations/day, 24-hour retention | Background jobs (about 3,300 messages) |

| Workflows | 10 ms CPU per step, 100 concurrent instances, 1,024 steps | Multi-step durable tasks |

Two details save you debugging time. Free accounts can only create SQLite-backed Durable Objects, which is what the Agents SDK uses anyway[3]. And the 10 ms CPU cap counts compute only. Waiting on a model response, a KV read or a fetch does not count against it[1], so an agent that spends 2 seconds waiting on inference still uses a few milliseconds of CPU.

Telegram sends each message to a Worker via webhook. The Worker checks a secret header, looks up an agent instance named after your chat ID, and calls a method on it. The agent stores the message in SQLite, calls a model with the last 12 messages, stores the reply, and returns it. The Worker posts the reply back through the Telegram Bot API. A cron schedule inside the agent sends you a morning summary. One Worker file, one Durable Object class.

Workers AI needs no key and no card. Its free allocation is 10,000 neurons a day, and Llama 3.3 70B fp8-fast is priced at 26,668 neurons per million input tokens and 204,805 per million output tokens[2]. The usage assumption below: 30 chats a day, 2,000 input tokens (system prompt plus history) and 400 output tokens per chat, over 30 days. That is 1.8M input and 0.36M output tokens a month.

Option Price per M tokens (in / out) Monthly cost at this usage

| Workers AI, Llama 3.3 70B fp8-fast | 26,668 / 204,805 neurons | $0 (about 4,060 neurons a day, inside the 10,000 free) |

| Workers AI, Llama 3.1 8B | 25,608 / 75,147 neurons | $0 (about 2,440 neurons a day) |

| Claude Haiku 5.5 (own key) | $0.10 / $0.50 | $0.36 |

| Claude Sonnet 5.5 (own key) | $2 / $10 | $7.20 |

Workers AI pricing is from Cloudflare's pricing page; Claude pricing is from Anthropic's, both checked 15 October 2026[2][4]. The math for Haiku: 1.8 × $0.10 plus 0.36 × $0.50 equals $0.36. For Sonnet: 1.8 × $2 plus 0.36 × $10 equals $7.20.

The practical split: start on Workers AI because it costs nothing and needs no secrets. Switch the single model call to Claude when answers need tool use or better reasoning. At $0.36 a month, Haiku 5.5 is cheaper than the time you would spend tuning a free model. To price your own workload, plug your token counts into the agent run cost simulator.

mkdir personal-agent && cd personal-agent
npm init -y
npm i agents
npm i -D wrangler typescript @cloudflare/workers-types
mkdir src

Install is a single package: npm i agents[5]. Create a Telegram bot with @botfather and keep the token. Send your new bot any message, then open https://api.telegram.org/bot<TOKEN>/getUpdates and copy message.chat.id. That is your owner ID.

{
  "name": "personal-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-10-01",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "durable_objects": {
    "bindings": [{ "name": "PersonalAgent", "class_name": "PersonalAgent" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["PersonalAgent"] }]
}

The binding name becomes the property on env, class_name must match the exported class exactly, and new_sqlite_classes is what enables SQLite storage[5].

import { Agent, getAgentByName } from "agents";

interface Env {
  AI: Ai;
  PersonalAgent: DurableObjectNamespace
;
  TELEGRAM_TOKEN: string;
  WEBHOOK_SECRET: string;
  OWNER_CHAT_ID: string;
}

type Turn = { role: "user" | "assistant"; content: string };

const MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
const SYSTEM = "You are a terse personal assistant. Answer in under 120 words.";

async function sendTelegram(env: Env, chatId: string, text: string) {
  await fetch(`https://api.telegram.org/bot${env.TELEGRAM_TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ chat_id: chatId, text }),
  });
}

export class PersonalAgent extends Agent {
  async onStart() {
    this.sql`CREATE TABLE IF NOT EXISTS memory (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      role TEXT NOT NULL,
      content TEXT NOT NULL,
      ts INTEGER NOT NULL
    )`;
  }

  async chat(chatId: string, text: string): Promise {
    // Cron schedules are idempotent by default, so repeating this is safe.
    await this.schedule("30 3 * * *", "morningBrief", { chatId });

    this.sql`INSERT INTO memory (role, content, ts) VALUES ('user', ${text}, ${Date.now()})`;
    const history = this.sql`
      SELECT role, content FROM (SELECT * FROM memory ORDER BY id DESC LIMIT 12)
      ORDER BY id`;

    const result = (await this.env.AI.run(MODEL, {
      messages: [{ role: "system", content: SYSTEM }, ...history],
    })) as { response?: string };

    const reply = result.response ?? "The model returned nothing.";
    this.sql`INSERT INTO memory (role, content, ts) VALUES ('assistant', ${reply}, ${Date.now()})`;
    return reply;
  }

  async morningBrief(payload: { chatId: string }) {
    const notes = this.sql`
      SELECT content FROM memory WHERE role = 'user' ORDER BY id DESC LIMIT 20`;
    const result = (await this.env.AI.run(MODEL, {
      messages: [
        { role: "system", content: "List open tasks and reminders from these notes in 5 lines max." },
        { role: "user", content: notes.map((n) => n.content).join("\n") },
      ],
    })) as { response?: string };
    await sendTelegram(this.env, payload.chatId, result.response ?? "Nothing to report.");
  }
}

export default {
  async fetch(request, env, ctx): Promise {
    if (request.method !== "POST") return new Response("ok");
    if (request.headers.get("X-Telegram-Bot-Api-Secret-Token") !== env.WEBHOOK_SECRET) {
      return new Response("forbidden", { status: 403 });
    }
    const update = (await request.json()) as {
      message?: { chat: { id: number }; text?: string };
    };
    const text = update.message?.text;
    const chatId = String(update.message?.chat.id ?? "");
    if (!text || chatId !== env.OWNER_CHAT_ID) return new Response("ok");

    ctx.waitUntil(
      (async () => {
        const agent = await getAgentByName(env.PersonalAgent, chatId);
        const reply = await agent.chat(chatId, text);
        await sendTelegram(env, chatId, reply);
      })(),
    );
    return new Response("ok");
  },
} satisfies ExportedHandler;

Every piece here is a documented Agents SDK call. this.sql is a tagged template for the instance's embedded SQLite, getAgentByName(namespace, name) returns a typed stub you can call methods on, and this.schedule(when, callback, payload) takes a delay in seconds, a Date, or a cron string, with the callback given as a method name on the same class[6]. A callback name that does not exist throws, so keep the string and the method in sync.

The cron string 30 3 * * * runs in UTC. That is 9:00 AM in India. Adjust it for your timezone.

npx wrangler secret put TELEGRAM_TOKEN
npx wrangler secret put WEBHOOK_SECRET
npx wrangler secret put OWNER_CHAT_ID
npx wrangler deploy

curl "https://api.telegram.org/bot/setWebhook" \
  -d "url=https://personal-agent..workers.dev" \
  -d "secret_token="

Telegram's secret_token accepts 1 to 256 characters from A-Z, a-z, 0-9, underscore and hyphen, and arrives in the X-Telegram-Bot-Api-Secret-Token header on every update[7]. That header check is the only thing stopping strangers from posting to your Worker URL, so do not skip it. The owner-ID check is the second lock: it means a leaked bot username cannot burn your neuron budget.

The handler returns 200 immediately and does the work inside ctx.waitUntil. Telegram retries a webhook that is slow to answer, and a 5-second model call would otherwise produce duplicate replies.

Replace the env.AI.run call with a plain fetch to the Messages API. Store the key with npx wrangler secret put ANTHROPIC_API_KEY.

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": this.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-haiku-5-5",
    max_tokens: 512,
    system: SYSTEM,
    messages: history,
  }),
});
const data = (await res.json()) as { content: { type: string; text?: string }[] };
const reply = data.content.find((b) => b.type === "text")?.text ?? "No text returned.";

The Haiku 5.5 API ID is claude-haiku-5-5 and Sonnet 5.5 is claude-sonnet-5-5[8]. Add ANTHROPIC_API_KEY: string to the Env interface. The fetch wait does not count toward the 10 ms CPU cap, but parsing a large JSON response does, so keep max_tokens modest.

Start with the agent's own SQLite, as in the code above. It is per-user, free to query from inside the class, and allows 5 GB total. Use D1 only when you need one query across several agents, such as an admin view of every chat. Skip KV for conversation logs: 1,000 writes a day to different keys is about 500 chats if you store two entries per turn. Use KV for read-heavy config you rarely change.

For semantic recall over old notes, add Vectorize. The 5M stored-dimension allowance holds roughly 3,900 vectors at 1,280 dimensions each, and the 30M queried-dimension allowance covers about 23,000 queries a month at that size. A personal notebook will not hit either.

Ordered by how soon you hit them, on the usage assumption above:

Workers AI neurons. 10,000 a day is about 74 chats of 2,000 input and 400 output tokens on the 70B model. Long history windows eat it faster. Fix: switch to Llama 3.1 8B (about 120 chats a day), shorten history from 12 messages to 6, or move to Haiku at a few cents a week.

Durable Object row writes. 100,000 a day. Each chat writes two memory rows plus index and schedule overhead, so you have headroom for thousands of chats. A chatty multi-user bot reaches this before requests.

CPU time. 10 ms per request. Chat logic uses a fraction of that. It breaks when you add PDF parsing, image handling or crypto inside the request path. Move that work to a Workflow step, which has the same 10 ms free limit, or upgrade.

Requests. 100,000 a day at roughly two requests per chat (Worker plus Durable Object) is about 50,000 chats. You will not get there alone.

When any limit trips, operations of that type start failing until midnight UTC. Your agent does not degrade gracefully, so wrap the model call in a try/catch and reply with a short "out of budget until 05:30 IST" message instead of silence. If your agent later needs to buy things or provision services on its own, the Cloudflare and Stripe agent provisioning guide covers that path.

The agents package is an npm library with no license fee. You pay only for the platform services it runs on, and all of those have free tiers: Workers, Durable Objects (SQLite-backed) and Workers AI.

The binding constraint is Workers AI at 10,000 neurons a day, roughly 74 chats on Llama 3.3 70B with 2,000 input and 400 output tokens each. The request caps allow far more, about 50,000 chats.

No. The 10,000 daily neurons are included on the free plan. The $0.011 per 1,000 neurons overage applies only on the paid Workers plan, so on free you hit a limit and wait for the reset.

Yes. this.schedule accepts a cron string and fires the named method on the agent without any incoming request.

It is public by necessity, since Telegram must reach it. Protect it with the webhook secret header and the owner chat ID check. Never log message text to an external service, and keep API keys in wrangler secret, never in wrangler.jsonc.

Workers AI if you want $0 and simple chat, notes and summaries. Claude Haiku 5.5 at about $0.36 a month for 30 chats a day when you need better instruction following. Sonnet 5.5 at about $7.20 a month only pays off for hard reasoning.

Originally published at wowhow.cloud

── more in #ai-agents 4 stories · sorted by recency
── more on @cloudflare 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/personal-ai-agent-on…] indexed:0 read:11min 2026-10-15 · —