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
POST /news.PiHarness class hosts Pi Durable sessions in a Durable Object, so long-running agents survive restarts, even mid-turn.developers.cloudflare.com/changelog/…Grok sends a story
Your Grok bot calls Mirai's ingest API. Mirai stores the story once, even if Grok sends it again.
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.
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.
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.
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.
Drafts the newsletter
Groups two weeks of news by theme, proposes a structure, then writes the copy in a direct voice with no hype.
Reads links
Fetches an article or docs page as clean text (up to 40,000 characters) before summarizing it, and cites the URL.
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.
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.
Sees your photos
Photos and image files go to the model directly, ready to discuss, edit or use as a reference.
Manages the website feed
Shows or hides any story on masteraicoding.dev/news. That's how you edit the feed now.
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.
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
| /reset | Starts a fresh conversation. The old one stays stored but the model stops seeing it. |
| /new | Same as /reset. |
| /start | Replies with a one-line hint. |
| /link | Sent by you in a group or topic: adds it to the destination picker. |
| /unlink | Removes it again. |
Anything else is a normal message. Messages sent while it's working queue up and get answered in order.
Access
| Service | What it can do there | Credential |
|---|---|---|
| Telegram | Receive 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 |
| OpenRouter | Chat with ~anthropic/claude-fable-latest (low reasoning). Generate images with GPT Image 2.5 Sunburst. Spends credits on the account. | OPENROUTER_API_KEY |
| Cloudflare D1 | Read and write the mirai database: news stories, linked destinations, drafted posts and where each was posted. | DB binding |
| Firecrawl | Scrape any public web page. Spends Firecrawl credits. | FIRECRAWL_API_KEY |
| Grok bot | Can 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.
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
mirainews, destinations, postsOne door
Telegram, Grok and the website all call the same Worker. It checks secrets and senders, then routes.
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.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.
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.
-
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 inwrangler.jsoncwith anew_sqlite_classesmigration. -
A brain: Pi Durable through PiHarness
PiHarnessruns 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.// 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); } -
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.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) }] }; }, }; -
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 throughsession.events(): stream drafts while the model writes, send the final message at the end. -
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. -
Ship it
Put keys in Doppler, push them with
wrangler secret, deploy withwrangler 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.
# 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| Setting | Where | Now |
|---|---|---|
| AGENT_MODEL | wrangler.jsonc | ~anthropic/claude-fable-latest |
| IMAGE_MODEL | wrangler.jsonc | openai/gpt-image-2.5-sunburst |
| System prompt | src/prompt.ts | Post format, newsletter voice, image style |
| Tools | src/tools.ts | Search news, propose post, show on website, generate image, edit image, read a page |
| Review buttons | src/review.ts | Approve, Edit, Skip, destination picker |
| Database | migrations/ | D1 mirai: news, destinations, posts, deliveries |