Mirai agent

The Telegram bot that turns AI news into posts. Your Grok bot finds the stories, Mirai drafts each one, you approve it and pick where it goes. It also helps with the MasterAI newsletter. Message @BuddyIntels_Bot in a private chat; it answers only you.

Posting

01 · Source
Grok bot
Finds a story, sends it to POST /news.
02 · Agent
Mirai drafts
Stores it once, reads the source, writes the post.
03 · Review
Card in Telegram
Approve, Edit or Skip.
04 · You
Pick destinations
Groups, topics, website.
05 · Live
Posted
Each tap posts right away.
Code, no modelModel at work
Review card
Cloudflare Agents SDK now runs the Pi Durable harness on WorkersNew PiHarness class hosts Pi Durable sessions in a Durable Object, so long-running agents survive restarts, even mid-turn.developers.cloudflare.com/changelog/…
ApproveEditSkip
Approve
Destination picker
✓ DefAI (Builders Only) / AI Insight DefAI (Builders Only) / Agentic Coding Agent Playground / Agent Play ✓ On masteraicoding.dev/news Done
  1. Grok sends a story

    Your Grok bot calls Mirai's ingest API. Mirai stores the story once, even if Grok sends it again.

  2. Mirai drafts the post

    It reads the source when the summary is thin, writes the post and sends a review card that looks exactly like the final post.

  3. You change it or approve it

    Reply to the card, or just say what to change, and a new card replaces the old one. Skip drops it.

  4. You pick where it goes

    Approve turns the buttons into a picker of every linked group and topic, a toggle for masteraicoding.dev/news, and Done. Each tap posts right away and gets a check mark.

Send /link inside a group or forum topic where the bot is a member to add it to the picker. /unlink removes it. Nine destinations came over from the old bot.

What it does

Drafts posts from Grok

Every story Grok sends becomes a ready-to-publish post with a review card. Replies revise it.

Shorter, two bullets, keep the link.
propose_post

Pulls the news

Searches every stored story: the 270 imported from the old site plus everything Grok sends. Filters by days back, type and whether it's on the website.

What happened in AI in the last 7 days?
get_news

Drafts the newsletter

Groups two weeks of news by theme, proposes a structure, then writes the copy in a direct voice with no hype.

Draft the newsletter from the last 14 days.
get_news

Reads links

Fetches an article or docs page as clean text (up to 40,000 characters) before summarizing it, and cites the URL.

Summarize https://… in three bullets.
scrape_website

Makes images

Generates an image and posts it in the chat, in the brutalist black and white MasterAI style, at the right ratio for where it's going.

Make a 16:9 header for the Gemini 4 section.
generate_image

Edits and combines images

Changes an image it made or one you send, or merges up to four. Every image can be referenced again, so edits chain.

Invert it: white on black.
edit_image

Sees your photos

Photos and image files go to the model directly, ready to discuss, edit or use as a reference.

[photo] Turn this into an Instagram slide in our style.
vision · edit_image

Manages the website feed

Shows or hides any story on masteraicoding.dev/news. That's how you edit the feed now.

Take the GLM-5.1 story off the website.
set_news_published

It keeps the whole conversation, so follow-ups like "make the second one darker" work. Long chats get summarized automatically. /reset starts over.

Images

Images come from openai/gpt-image-2.5-sunburst through OpenRouter's Images API, at high quality. Ask for a destination or a ratio.

16:9X posts, newsletter headers
3:4Instagram feed, carousels
9:16Stories, Reels
1:1Square, the default
4:3Blog images
21:9Wide banners

OpenRouter bills per image token, and edits also pay for the input images. Instagram's 4:5 isn't offered by this model, so feed posts use 3:4.

Commands

/resetStarts a fresh conversation. The old one stays stored but the model stops seeing it.
/newSame as /reset.
/startReplies with a one-line hint.
/linkSent by you in a group or topic: adds it to the destination picker.
/unlinkRemoves it again.

Anything else is a normal message. Messages sent while it's working queue up and get answered in order.

Access

ServiceWhat it can do thereCredential
TelegramReceive your private messages, photos and button taps. Send messages, live drafts, photos and review cards. Post approved posts to linked groups and topics. In groups it only listens for your /link and /unlink.TELEGRAM_BOT_TOKEN
OpenRouterChat with ~anthropic/claude-fable-latest (low reasoning). Generate images with GPT Image 2.5 Sunburst. Spends credits on the account.OPENROUTER_API_KEY
Cloudflare D1Read and write the mirai database: news stories, linked destinations, drafted posts and where each was posted.DB binding
FirecrawlScrape any public web page. Spends Firecrawl credits.FIRECRAWL_API_KEY
Grok botCan only add stories through POST /news. It can't read anything back or post.NEWS_INGEST_TOKEN

Allowed senders: BOT_OWNER_TELEGRAM_ID, plus any ids in ALLOWED_TELEGRAM_USER_IDS. Webhook calls without the Telegram secret header get a 401.

Not yet

Things a media machine needs that Mirai can't do today:

  • Post outside Telegram and the website. No X, Instagram or Substack publishing yet.
  • Video. No video generation, editing or captions.
  • Save media. Images live in the Telegram chat only. There's no library or storage bucket.
  • Schedule posts. Approved posts go out the moment you tap a destination.
  • Remember across /reset. Facts you want kept go in the prompt.

Grok API

Point your Grok bot at this endpoint, or tap Set up in my agent at the top and paste the instructions into it. The token is in Doppler under masterai / prd as MIRAI_NEWS_INGEST_TOKEN.

HTTP
POST https://mirai-agent.oddofrancesco000.workers.dev/news
Authorization: Bearer $MIRAI_NEWS_INGEST_TOKEN
Content-Type: application/json

{ "items": [
  { "url": "https://x.com/OpenAI/status/123",
    "summary": "What happened, in a sentence or two.",
    "headline": "Optional title",
    "text": "Optional original post text",
    "author": "Optional handle",
    "publishedAt": "2026-10-31T09:00:00Z" }
] }

Only url and summary are required. Send one item or up to 50 in items. X links, YouTube links and articles each get a stable id, so resending a story does nothing. The reply lists every item as new, duplicate or invalid.

How it runs

Callers
Telegrammessages, taps
Grok botnew stories
Website/news feed
Worker
/telegram
/news
/api/news
Durable Object, one per chat
MiraiChat
Pi Durable sessiontranscript, retries, resume after restarts
Toolsget_news · propose_post · set_news_published · generate_image · edit_image · scrape_website
Telegram relaylive drafts, final messages
Services
D1 mirainews, destinations, posts
OpenRouterFable, GPT Image 2.5
Firecrawlreads pages
Telegram Bot APIsends, posts
  1. One door

    Telegram, Grok and the website all call the same Worker. It checks secrets and senders, then routes.

  2. One agent per chat

    Each chat has its own Durable Object running one Pi Durable session through the Agents SDK's PiHarness. Pi stores every input before replying, keyed by its event id, so a retried webhook is never answered twice.

  3. Answers stream back

    While the model writes, the reply streams into the chat as a live draft. Narration before tool calls is skipped; only final answers and review cards arrive.

  4. It survives restarts

    If Cloudflare evicts or restarts the object mid-answer, Pi picks the run up from its last checkpoint on the next alarm, and the reply still arrives.

Make an agent

Every agent like Mirai is four pieces: a home, a brain, hands and a door. Mirai is about 1,200 lines of them. Start with one tool and one channel, then grow it.

04 DoorWorker route: Telegram, API, cron
01 Home · Durable Object, one per conversation, own SQLite
02 Brain
PiHarness + Pi Durable + a model
03 Hands
Tools: name, description, schema, function
  1. A home: one Durable Object per conversation

    A Durable Object is a tiny server with its own SQLite database. Name it after the chat (getByName(chatId)) and every conversation gets its own memory, isolated from the rest. Declare it in wrangler.jsonc with a new_sqlite_classes migration.

  2. A brain: Pi Durable through PiHarness

    PiHarness runs the agent loop (model call, tool calls, next model call) and saves every step before showing it. If Cloudflare restarts the object mid-answer, the run continues where it stopped. You choose the model; Mirai uses OpenRouter.

    TS
    // bun add agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
    export class MyAgent extends DurableObject<Env> {
      registry = createRegistry();
      harness = new PiHarness({
        harness: ({ storage, context, onWake }) => {
          this.registry.install({ name: 'mine', sections: [prompt], tools: [getNews] });
          const models = createModels();
          models.setProvider(openrouterProvider());
          return Harness.open(storage, { models, registry: this.registry, onWake }, context);
        },
        defaults: { model: { provider: 'openrouter', id: '~anthropic/claude-fable-latest' } },
      });
      lifecycle = Lifecycle.install(this).use(this.harness);
    }
  3. Hands: tools

    A tool is a name, a one-line description, a typed input schema and a function. The model reads the description to decide when to call it, so write it for the model. Mark tools that are safe to run twice with replay: 'safe'; anything that posts or spends money stays unsafe.

    TS
    const getNews: ToolRegistration<typeof Input> = {
      name: 'get_news',
      description: 'Stored AI news stories, newest first.',
      parameters: Type.Object({ sinceDaysAgo: Type.Optional(Type.Number()) }),
      replay: 'safe',
      async execute({ sinceDaysAgo }) {
        const rows = await db.listNews({ sinceDaysAgo });
        return { output: [{ type: 'text', text: JSON.stringify(rows) }] };
      },
    };
  4. A door: how messages get in and answers get out

    A Worker route receives the message (a Telegram webhook, an API call from Grok) and hands it to the right Durable Object with harness.submit(text, { operationId }). Use the incoming event id as the operation id so retries never run twice. Answers come back through session.events(): stream drafts while the model writes, send the final message at the end.

  5. Keep the model out of what code can do

    Buttons, permissions and posting run as plain code in Mirai (src/review.ts), so they're instant, free and predictable. Give the model the judgment work: reading, writing, choosing.

  6. Ship it

    Put keys in Doppler, push them with wrangler secret, deploy with wrangler deploy, then point the channel at the Worker URL. Test with a fake event before switching real traffic.

Operate

Code lives in agent/ in the MirAI repo. The chat model and the image model are plain vars in wrangler.jsonc, so switching models is a one-line change and a deploy.

Bash
# check types
cd agent && bun run check

# deploy
bun run deploy

# after adding a migration
bunx wrangler d1 migrations apply mirai --remote

# point the bot's webhook at the worker
bun run set-webhook https://mirai-agent.oddofrancesco000.workers.dev
SettingWhereNow
AGENT_MODELwrangler.jsonc~anthropic/claude-fable-latest
IMAGE_MODELwrangler.jsoncopenai/gpt-image-2.5-sunburst
System promptsrc/prompt.tsPost format, newsletter voice, image style
Toolssrc/tools.tsSearch news, propose post, show on website, generate image, edit image, read a page
Review buttonssrc/review.tsApprove, Edit, Skip, destination picker
Databasemigrations/D1 mirai: news, destinations, posts, deliveries