{"slug": "everything-i-got-wrong-building-an-autonomous-x-bot", "title": "Everything I got wrong building an autonomous X bot", "summary": "An engineer built BuzzEngine, an autonomous bot that writes and publishes tech posts to X three times daily, and documented six design flaws uncovered through testing. The project achieved zero running costs by routing posts through Buffer's free plan, avoiding X's paid API tier. Key lessons included misinterpreting X's billing unit for link-containing posts and resolving contradictory prompt instructions that caused the bot's quality gate to reject drafts.", "body_md": "It writes a tech post three times a day and publishes without me. The code took an afternoon. The interesting part was the six times testing proved a design I was confident about was wrong.\n\nRunning cost |\n$0.00 |\nRuntime dependencies |\n0 |\nTypeScript |\n4,054 lines |\nBugs found by testing |\n6 |\n\nBuzzEngine watches Hacker News and GitHub Trending, works out what's actually gaining traction, fetches the primary source, writes a post about it, judges its own work, and publishes to X. On a schedule. Without anyone approving anything.\n\n```\nCOLLECT ──▶ DEDUPE ──▶ SCORE ──▶ RESEARCH ──▶ DRAFT ──▶ GATE ──▶ PUBLISH\n HN,GitHub   subject +  velocity   fetch the     LLM      LLM +    Buffer\n Reddit, X   URL + text + topic    primary                7 checks  ──▶ X\n                                   source                + rules\n                                                            │\n                                                     fail ──┘\n                                                   (try next candidate)\n```\n\nThat diagram is the boring part. Every box in it works. What follows is the story of the six times a box *looked* like it worked and didn't — because that's where all the actual engineering was.\n\nX shut down its free API tier in February 2026. Posting now costs **$0.015 a post** — or **$0.20** if the post contains a link. No subscription, no monthly minimum; you buy credits and they drain.\n\nThree posts a day with links is about $18 a month. Not much, but the brief was zero.\n\nSo I proposed putting the link in a self-reply instead: a clean main post at $0.015, link underneath. About 93% cheaper.\n\nThat was wrong, and it took saying it out loud to notice. **A reply is also a post, and it contains a link.** So it bills at $0.20 too — meaning link-in-reply costs $0.215 per story, *more* than putting the link in the main post.\n\nLesson:The unit a price applies to is part of the price. I'd optimised the wrong axis for an hour before checking what \"post\" meant to the biller.\n\nThe only genuinely cheap option was omitting links entirely. Which is a content decision disguised as a billing one, and a bad one for a bot whose job is pointing at things.\n\nThe unlock wasn't a cheaper tier. It was noticing that social schedulers hold *their own* X API access and absorb that cost as part of their product.\n\nMost had closed up. Zapier restored its X integration but now requires your own developer credentials. Make.com killed theirs outright in 2025. IFTTT still works but throttles hard on free.\n\nBuffer's free plan turned out to include the one thing that mattered:\n\n| Buffer free plan | Limit |\n|---|---|\n| Channels | 3 |\n| Queued posts | 10, refillable |\nAPI requests / month |\n3,000 |\n| Cost | $0, permanent |\n\nThree posts a day uses about 90 of those 3,000 requests — 3% of the quota. Buffer talks to X; we never touch X's API, never hold X credentials, never see a bill.\n\nThere was one scare: Buffer stopped accepting new OAuth app registrations, which kills third-party integrations. But the new GraphQL API uses **personal API keys**, which is exactly what a personal bot needs. No app registration, no approval queue.\n\n```\nstatus : sent\nlink   : https://x.com/i/status/2086783660578029792\nerror  : None\n```\n\nFirst real post, published through Buffer, $0 spent.\n\nThe first version produced posts like this:\n\nPersona-based prompt packaging claiming proven deliverables. Shell language suggests text templates, not runtime agents. The value is in the process documentation, not automation.\n\nThe gate kept rejecting drafts for asserting things the source didn't support. I assumed the writer was being careless. It wasn't.\n\nI was handing the writer a headline and a one-line blurb, then instructing it to \"say something the headline doesn't.\" There is no honest way to satisfy both. The only moves available are speculation — which the gate then correctly refused.\n\n**The prompts were in direct contradiction and I'd written both.**\n\nThe fix was a step I'd left out: fetch the actual primary source before writing. A repo's README, an article's body. Suddenly there was real material to be interesting *about*.\n\n**Before research:**\n\nPersona-based prompt packaging claiming proven deliverables…\n\n**After research:**\n\nDocker Sandboxes wrap agents in microVMs so you can safely run\n\n`--dangerously-skip-permissions`\n\n. Everyone reads this as security; it's really about removing the approval bottleneck.\n\nLesson:When a model keeps failing a check, suspect the brief before the model. Two instructions that can't both hold will produce garbage forever.\n\nThen the gate started rejecting *accurate* posts. It flagged \"858 stars\" as an unsupported claim — a number sitting right there in the source material.\n\nBecause I'd built the two prompts separately, and the gate's version of the material was missing the engagement figures the writer had been given.\n\nThe gate was doing its job perfectly. It was judging against a strictly smaller context than the writer had, so genuinely-sourced facts looked invented. Three candidates rejected in a row; the run produced nothing and logged no error.\n\nBoth stages now build their material from one shared function. Not because it's tidier — because it makes the mismatch *structurally impossible* rather than merely unlikely.\n\nLesson:If one component checks another's work, they must see identical inputs. A verifier with less context than the thing it verifies invents failures.\n\nA run rejected all three candidates:\n\n```\ngate rejected  too long: 326/280\ngate rejected  too long: 284/280\ngate rejected  too long: 307/280\nrun complete — outcome: skipped\n```\n\nOne of those missed by **four characters**.\n\nLength was in the same bucket as fabrication: hard fail, discard, move on. But \"slightly too long\" is a *mechanical* problem — exactly what the editor's revision path exists for.\n\nRules are now split by whether a rewrite can fix them:\n\n| Examples | Action | |\n|---|---|---|\nFatal |\nInvented facts, duplicates, blocked terms | Reject before spending a model call |\nFixable |\nToo long, stray hashtag, wall of prose | Send back to the editor, then re-check |\n\nThe next run showed it working: *\"33 characters too long\"* → the editor cut hedges, kept the argument, and the re-check passed. That post published.\n\nThe bot runs on Kimi K2.5 through Azure. Early on, calls came back successful and empty:\n\n```\nHTTP 200\ncontent          : \"\"\nreasoning_content: \"The user wants me to say OK. This is a very sim…\"\nfinish_reason    : length\n```\n\nReasoning models spend the token budget on an internal scratchpad *before* writing anything. Run out mid-thought and you get a valid 200 with an empty answer. One request produced **21,568 characters of reasoning** and no output.\n\nThree layers up, that surfaced as `no JSON value found in model output`\n\n— an error pointing nowhere near the cause.\n\nThe adapter now detects the exact shape (empty content + `finish_reason: length`\n\n) and says what actually happened, naming the fix. The guard fired on its very first real run, which is the only reason I found the next bug.\n\nLesson:A successful status code is not a successful outcome. Check the shape of what came back, and make the error message name the cause, not the symptom.\n\nThe account posted about the same repository twice.\n\nDedupe existed — canonical URL matching, fuzzy title similarity, a 45-day memory. It just answers a narrower question than the one that matters. Different repo from the same lab? Passes. Second Docker announcement that week? Passes. Same story from a different outlet? Passes.\n\nI tried fixing it with judgement first: show the editor the last ten posts, tell it to reject repetition. Then I replayed a near-duplicate against real history.\n\nIt approved. Scores of 7–9 across the board, flagging nothing.\n\n**Repetition lost to seven positive criteria.** One instruction among many is not a constraint.\n\nSo candidates now carry subject keys, and anything matching one used in the last seven days is dropped before a model ever sees it:\n\n```\ngithub.com/labX/agent          ──▶  repo:labx/agent\n                               ──▶  org:labx\n\ndocker.com/blog/thing          ──▶  site:docker.com\n\nnews.ycombinator.com  ──▶  arstechnica.com/x  ──▶  site:arstechnica.com\n```\n\nThe subtlety is granularity. Keying on the domain would make `github.com`\n\na subject and block every repository on earth. Aggregators — GitHub, HN, Reddit, X — are containers, not subjects, so they resolve to the *linked* article's domain instead.\n\nVerified against the real failure:\n\n```\n✅ vercel/ai-sdk                    kept — genuinely new subject\n🚫 PrimeIntellect-ai/prime-agent    already posted\n🚫 PrimeIntellect-ai/other-thing    different repo, same org\n🚫 Docker ships another feature     different story, same company\n```\n\nLesson:Deterministic rules don't drift, don't get talked round, and cost nothing to run. Use judgement for taste; use code for anything you can define.\n\nImages seemed like a straightforward win. They aren't, for a reason specific to the platform: **X gives attached media precedence over the link card.**\n\nWhen a post carries a link, X already renders the page's og:image — with the headline and domain attached, for free. Upload that same image and you *replace* that card with a bare picture. Strictly worse.\n\nSo the rule became: only attach an image the card wouldn't already show.\n\n| Source | Attached |\n|---|---|\n| GitHub repo | nothing — the card already shows it |\n| Product page with a screenshot | the screenshot |\n\nThen the first test attached `gray.png`\n\n. Body images arrive in document order, and page furniture comes first — logos, banners, background textures. They're now filtered and ranked, using dimensions in the filename as the signal: a 2320×1205 screenshot is content, a 1110×326 strip is decoration.\n\nThe last change came from a question I didn't have a good answer to at first: *why not just post whatever it finds?*\n\nSo I read back every rejection. Four of five were invented facts — benchmark scores that don't exist, a timeframe the source never gave, a draft saying *\"We built\"* about someone else's repository. One was mechanical.\n\nNone were \"not funny enough.\" The gate had never been fussy about taste — but the *threshold* was, because a single number governed all seven criteria. An honest, unremarkable post got binned for scoring 6 on humour.\n\n| Criterion | Floor | Why |\n|---|---|---|\n| Accuracy | 8 |\nPublishes under a real name |\n| Funny, interesting, memorable, human, clear, concise | 5 |\nA flat post costs nothing |\n\nTwo bars, not one. A merely-decent post publishes. A fabrication never does — and never trades against \"we need something today.\"\n\nLesson:\"Quality\" isn't one number. Separate the failures that cost something from the ones that are just disappointing.\n\nDocker built a cage so AI agents can run 'YOLO mode' safely. That's the actual name for\n\n`--dangerously-skip-permissions`\n\n.Your bot gets root on a fake computer where it can delete files freely. We gave robots autonomy, then immediately grounded them.\n\n*Published · understandable 9 · accurate 9 · human 8*\n\nSomeone built a staffing agency for AI coding tools. Hire a frontend wizard or Reddit ninja through an app. Each agent has a personality and KPIs.\n\nWe finally automated the jobs, then immediately recreated the corporate structure to manage them.\n\n*Published · found on GitHub Trending*\n\nThe formatting matters more than it looks. Early drafts came out as one block of prose, which reads as machine-written regardless of how good the observation is. Posts are now 2–4 short beats separated by blank lines — enforced in code, because the break before the last line is what makes a punchline land.\n\nSix bugs, and five of them share a shape: **a design that was reasonable in the abstract and wrong against reality.** None were caught by reading the code. All were caught by running it and looking hard at the output.\n\n**What I'd carry to the next one:**\n\nThe bot has been running since. It publishes up to three times a day, skips days when there's nothing worth saying, and hasn't yet posted anything I'd want to delete.\n\nThat last part is the gate doing its job — which mostly means refusing to publish things a more eager system happily would.\n\n**Stack:** TypeScript with zero runtime dependencies · GitHub Actions · Buffer free tier\n\n**Sources:** Hacker News, GitHub Trending, Reddit, X\n\n**Model:** provider-agnostic — Anthropic, OpenAI, Gemini, or any OpenAI-compatible endpoint", "url": "https://wpnews.pro/news/everything-i-got-wrong-building-an-autonomous-x-bot", "canonical_source": "https://dev.to/kanurkarprateek/everything-i-got-wrong-building-an-autonomous-x-bot-5hk9", "published_at": "2026-08-11 13:39:50+00:00", "updated_at": "2026-08-11 13:47:07.800706+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "ai-products"], "entities": ["BuzzEngine", "X", "Buffer", "Zapier", "Make.com", "IFTTT", "GitHub", "Hacker News"], "alternates": {"html": "https://wpnews.pro/news/everything-i-got-wrong-building-an-autonomous-x-bot", "markdown": "https://wpnews.pro/news/everything-i-got-wrong-building-an-autonomous-x-bot.md", "text": "https://wpnews.pro/news/everything-i-got-wrong-building-an-autonomous-x-bot.txt", "jsonld": "https://wpnews.pro/news/everything-i-got-wrong-building-an-autonomous-x-bot.jsonld"}}