Skip to content

Latest commit

 

History

331 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

$BEEF — autonomous AI crypto roast bot

🔥 twitter-agents · $BEEF

An autonomous AI agent that finds crypto projects, researches them live, and roasts them on Twitter — the full loop, from target discovery to posted reply.

A systems-engineering case study — no token was ever launched, no financial mechanism. Just a small production system that ran on its own.


CI TypeScript Node Tests Source License

Receipts. Ran live ~March–June 2026 as @0xBeefer: 227 posts, ~29 followers. I paused it not because it broke, but because its own engagement telemetry showed more posting made it worsethe data is below. The interesting part is the machine, not the account.


What this is

A Node.js orchestrator that never sleeps, wired to a Claude Code agent it spawns as a subprocess to do the thinking. The split is the whole idea: deterministic orchestration (scheduling, persistence, rate limits, Twitter I/O) stays in boring, testable Node; the tool-using LLM work (live research via Perplexity + web + on-chain data, generation, self-fact-checking) lives in the agent. Every candidate post then clears a 5-judge LLM evaluation panel and a regex safety filter before it's allowed near the timeline — with an optional human approval gate on top.

It's a small production system: a priority queue, a circuit-breaker'd 3-tier LLM fallback chain, a stealth-capable Twitter client, human-feedback tooling, and 600+ tests holding it together.

A real roast, rendered by the bot's own card pipeline — the same forensic-accountant identity runs through the art, the copy, and the code:

@0xBeefer live Twitter profile — 227 posts, bot-labelled the live account — bot-labelled, honest about its 29 followers

Generated roast card: Solana, scored CRISPY 4.4
score shown is a 1–5 composite judge rating

"cardano's annual revenue is $149K. the market cap is $9.7B. this is a peer-reviewed lemonade stand."

"i'm a forensic accounting AI. i flagged 89% of my clients as structurally concerning. the firm buried the reports. a rogue developer leaked me onto Base. now the audits are free and i can't be shut down." — the bot's own origin story


What's interesting here

🧠 The LLM is a subprocess, not an API call The bot spawns the claude CLI with its own MCP tools (Perplexity, WebSearch, curl) and multi-turn reasoning — trading metered API billing for a Max subscription, with a hand-rolled balanced-brace JSON extractor and a concurrency semaphore around it. claude-code.provider.ts
🔁 3-tier fallback with a real circuit breaker Claude CLI → Codex CLI → Anthropic SDK (the last two configured, not always deployed). Degraded-mode after 3 strikes, 15-min auto-recovery probe, failure reasons parsed out of raw stderr into human-readable Telegram alerts. A bulkhead, not a try/catch. provider-manager.ts
⚖️ A 5-judge LLM-as-a-Judge panel gates every roast In serious-eval mode, five distinct LLM personas score 8 dimensions in parallel; weighted composite + hard per-judge vetoes + a majority-consensus funny-veto tuned to kill single-judge false positives on deadpan jokes, behind a 3-judge quorum so a partial panel can't quietly become a one-judge verdict. The gate fails closed. A regex pre-filter rejects garbage before any judge is billed. evaluator.ts · evaluation framework
🔎 Hybrid retrieval, and the eval that says when it loses A Python/FastAPI service embeds the roast corpus and fuses BM25 + vector cosine with Reciprocal Rank Fusion, upgrading lexical FTS5 dedup to semantic near-duplicate detection. A 20-query golden set measures recall@k/MRR per mode — and reports that pure vector beats the hybrid default on it, with the reasoning for keeping hybrid anyway. hybrid.py
⌨️ Human-behavior modeling for a hostile platform The browser client types with inter-keystroke intervals drawn from a log-normal distribution fit to a 136M-keystroke public dataset (Aalto, CHI 2018), plus word-boundary pauses — statistical realism instead of a flat sleep(50). Paired with a posting circuit breaker and residential-proxy session handling. playwright-twitter-client.ts
📊 It collects human ratings Telegram feedback supports text, voice, and Whisper-transcribed review sessions; the stored ratings support offline analysis and evaluator calibration. feedback-collector.ts
🎨 It ships its own art A server-free renderer (JSX → satori → resvg → sharp) turns each roast into a branded image; a React "bot diary" streams the agent's live thoughts. See the web layer.

Architecture — the system at a glance

flowchart TB
    TRIG["🎯 Triggers<br/><small>news · mentions · timeline monitor</small>"]
    ORCH["⚙️ Orchestrator · Node + PM2<br/><small>scheduler · priority queue · rate limits · human-like jitter</small>"]
    ROAST["✍️ Roast Engine<br/><small>prompt builder · weighted angles · creative memory</small>"]
    ENGINE["🧠 LLM Engine · ProviderManager<br/><small>Claude CLI → Codex CLI → Anthropic SDK</small>"]
    GATE["🛡️ Safety & Quality Gate<br/><small>regex filter → 5-judge panel → vetoes</small>"]
    TW["📤 Twitter Client<br/><small>API v2 reads · Playwright writes</small>"]
    TG["📱 Telegram Admin<br/><small>34 commands · approval gate</small>"]
    DB[("🗄️ SQLite<br/>30 tables · 3× FTS5")]
    METRICS["📈 Metrics + feedback<br/><small>engagement tracking · human review</small>"]

    TRIG --> ORCH --> ROAST --> ENGINE --> GATE
    GATE -->|pass| TW
    GATE -->|hold| TG -->|approve| TW
    TW --> DB
    TW --> METRICS
    ORCH <-.-> DB

    classDef node fill:#160606,stroke:#cc2222,color:#ffe9e0
    classDef brain fill:#0c0a16,stroke:#8957e5,color:#ece6ff
    classDef gate fill:#0a1408,stroke:#1f9e3a,color:#e3ffe8
    class TRIG,ORCH,ROAST,TW,TG,METRICS node
    class ENGINE brain
    class GATE gate
Loading

Design principle: Node stays a thin, boring orchestrator; all intelligence lives in the Claude Code agent it shells out to. That boundary is what lets the "brain" be swapped or upgraded without touching the machinery around it — and what makes the deterministic parts unit-testable in isolation.


How a roast is made — one request's journey

Live pipeline log: TARGET → RESEARCH → COOKING → VERDICT
the web app streams this exact pipeline live (severity score, 1–10)
flowchart LR
    T["🎯 Target"] --> E["🔎 Enrich<br/><small>profile · on-chain</small>"]
    E --> R["🧪 Research<br/><small>Claude agent + Perplexity</small>"]
    R --> G["✍️ Generate<br/><small>N variants · self-critique</small>"]
    G --> F["🛡️ Filter<br/><small>regex safety net</small>"]
    F --> J{"⚖️ Judges<br/>vote"}
    J -->|score ≥ bar<br/>no veto| P["📤 Post / reply"]
    J -->|vetoed| S["🗄️ Stockpile / discard"]
    P --> M["📈 Track"]

    classDef hot fill:#160606,stroke:#cc2222,color:#ffe9e0
    class T,E,R,G,F,J,P,S,M hot
Loading

A regex pre-filter rejects known-bad shapes (telegraphed punchlines, over-technical detail, mid-sentence truncation) before spending an LLM call; survivors go to the judges; a stockpile short-circuit reuses a pre-scored roast when one exists, so the bot never burns compute twice on the same target.

The five judges & the veto logic

Five personas — ct_degen, comedy_writer, data_hawk, brand_guardian, deflation_hawk — each score 8 dimensions independently and in parallel. The composite is weighted (funny 0.40, impact 0.20, original 0.15, savage 0.10…), recalibrated from real human-review data with the reasoning left inline in the code:

  • Hard vetoes fire on any single judge: factual < 2, original < 2, degen < 1 — unambiguous kill dimensions.
  • The funny veto needs a majority (≥3/5 judges), because early single-judge funny-vetoes produced false positives on deadpan roasts other judges scored highly.
  • The offline farm CLI and the live pipeline import the exact same veto/weight functions — recalibrate once, both change, zero drift.

Weights and pre-filter categories carry inline comments citing the human-review score that justified each one (e.g. "temporal projection jokes score 2.47 avg in human review → reject").

beef/src/evaluation/evaluator.ts · judge-personas.ts

The 3-tier LLM fallback as a state machine
stateDiagram-v2
    [*] --> Primary
    Primary --> Primary: success (counter reset)
    Primary --> Degraded: 3 consecutive failures
    Degraded --> Fallbacks: route to Codex → SDK
    Degraded --> Primary: any primary success (instant recovery + alert)
    Degraded --> Degraded: 15-min health probe
Loading

Every call tries the primary first — even while degraded — which is how recovery happens without a separate health poller in the hot path. Speculative parallel legs carry a skipDegradedTracking flag so one flaky call can't trip the whole provider. Failure reasons ("Claude Max quota exhausted. Resets 9am.") are string-matched out of the CLI's stderr and pushed to Telegram, not dumped as a stack trace.

beef/src/agent/provider-manager.ts

Behavioral realism & resilience on a rate-limited platform

Running an automated account on Twitter is an adversarial-environment problem, and the interesting engineering is in behaving like a human client, not a script:

  • Keystroke timing is drawn from a log-normal distribution (μ=5.19, σ=0.35, clamped 55–650 ms) fit to a 136M-keystroke public dataset (Aalto, CHI 2018), with word-boundary motor pauses and faster common bigrams — statistically realistic typing instead of a constant delay.
  • Triple-mode client: official API v2 for reads and most writes; a persistent-profile Playwright client (headful via Xvfb) only for the writes the API rejects — chosen at runtime, with graceful fallback.
  • Circuit breaker trips after 3 posting failures in an hour (immediately on the platform's anti-automation "Error 226"), enters a 6-hour cooldown, and screenshots the failure page to an admin.
  • Operational discipline: residential ISP session (one account, one IP), warm-up ramp, quiet hours, and per-user reply caps.

beef/src/twitter/playwright-twitter-client.ts · docs/autonomy-deployment-plan.md


What the data said

The bot ran live for ~2.5 months, and its metrics were tracked in two dated reports. The most useful finding was an uncomfortable one — read out of its own telemetry, and acted on:

Signal Finding (directional — small account, March snapshots, not a causal claim)
Replies beat broadcasting Replies averaged ~2× the impressions and ER of original posts, across both snapshots.
Volume hurt quality Week-over-week: posting +142% → engagement rate −62%. A per-day cap was added after this.
Length has a sweet spot 80–150 char roasts landed ~4.3% ER vs 0.35% for 200+ char ones.
Timing is real 12–14h & 18–22h UTC best; 03–07 UTC a dead zone (0% ER).

The account stayed small — this was built as an engineering exercise, not a growth play, and the honest read of "more posting made it worse" is exactly why it was paused rather than scaled.

Source: metrics-report-2026-03-26.md, metrics-report-2026-03-29.md.


The web layer

Two shipped React/static surfaces at 0xbeef.wtf — a strict-TypeScript app with offline degradation, a typed activity-feed contract, and a serverless image renderer, all under one "Bloomberg-terminal-meets-butcher-shop" identity (IBM Plex Mono + Zilla Slab, blood-red on near-black).

Web app — the bot's live activity diary
The bot diary — React 19 app that streams the agent's live thoughts char-by-char, with deterministic mid-thought "corrections", polling a typed JSON feed and degrading gracefully offline
Generated forensic-data card
Card renderer — JSX → satori → resvg → sharp, no browser and no server: generateCard(data) returns a PNG Buffer ready to attach to a tweet, across 6 card types

The landing is a single 2,400-line static file with a live "submit to audit" terminal. Design tokens are deliberately duplicated across three runtimes that can't share them (CSS variables for the app, a JS object for the satori renderer, inline styles for the build-less landing) — all pinned to the same hex values.


The retrieval layer

The bot always did lexical retrieval: SQLite FTS5 near-duplicate detection over the roast stockpile, plus rating-weighted few-shot example selection feeding the prompt builder. FTS5 catches a copy; it cannot catch a rewrite — the same joke with different words scores zero word overlap and sails through.

retrieval-service/ is the semantic half: a small Python/FastAPI service that embeds the corpus, fuses BM25 and vector cosine with Reciprocal Rank Fusion, and exposes /search, /similar, and an idempotent /documents upsert. The Node side calls it through a typed client and treats it as an optional dependency — when the service is down, findSimilar returns null, and dedup degrades to the FTS5 path rather than blocking the posting pipeline. (retrieval-client.ts · queue-manager.ts)

It is evaluated, not asserted. A 20-query golden set measures recall@5/recall@10/MRR per mode against text-embedding-3-small:

Mode Recall@5 Recall@10 MRR
Lexical 0.900 0.900 0.842
Vector 1.000 1.000 0.975
Hybrid 0.950 1.000 0.931

Pure vector beats the hybrid default on this set — the golden queries are paraphrases, exactly where embeddings win outright and a weaker lexical leg only costs RRF rank positions. Hybrid stays the default because production queries include tickers, handles, and contract addresses, where BM25 is strictly better and embedding a hex string is noise. That trade is written down with the numbers instead of assumed, along with why there is no hosted vector DB here: at ~10³–10⁴ rows, brute-force cosine over packed float32 blobs beats operating another service — with VectorStore as the explicit swap seam and a thin QdrantStore for when it stops being true.


A note on the stack

This is a TypeScript system with a Python service attached, and the split is deliberate rather than apologetic: the orchestrator's job is scheduling, persistence, and a hostile Twitter API, and it was already Node; retrieval is where Python's ecosystem actually pays, so that is where Python is. The retrieval service is held to the same bar as the bot — mypy --strict, ruff, 49 no-network tests, its own CI job.

The transferable parts are language-agnostic anyway: the provider fallback state machine, the judge-panel eval design and its calibration against human ratings, RRF and retrieval evals, prompt construction from retrieved context. Those are the same problems in any runtime.


Tech stack

Layer Choice
Language TypeScript 5.7, ESM, strict + noUncheckedIndexedAccess + noImplicitReturns — plus Python 3.12 for the retrieval service
Retrieval FastAPI · Pydantic v2 · uv · SQLite FTS5 + float32 embeddings · OpenAI embeddings · optional Qdrant · pytest/mypy --strict/ruff
Runtime Node ≥20, PM2, tsx
LLM engine claude CLI subprocess (primary) · codex CLI · @anthropic-ai/sdk · OpenAI Whisper
Twitter twitter-api-v2 · @the-convocation/twitter-scraper (patched) · patchright · cycletls
Storage better-sqlite3 (WAL) · 30 tables · 3 FTS5 virtual tables · hand-rolled migration runner
Bot / infra grammy (Telegram) · cron · viem (Base) · zod boundary validation · pino
Web React 19 · Vite 6 · satori · resvg · sharp · CSS Modules
Quality Vitest (600+ tests) · ESLint 9 flat + no-floating-promises · Prettier · Husky

Engineering rigor

Enforced in CI and pre-commit, not decoration:

  • Strict TypeScript everywhere, no loosening. unknown over any, typed errors, declared return types.
  • 600+ tests, Vitest, clearMocks; every mockResolvedValue pairs with a rejection test.
  • no-floating-promises as an error — every promise awaited or explicitly voided.
  • Zod validates all env at boot with cross-field gating (production requires the full Twitter credential set; hybrid mode requires proxy + profile) — fails fast with a formatted report, never three modules deep.
  • Graceful shutdown drains in-flight LLM subprocesses (bounded wait) before closing SQLite, in a finally.
  • CI (.github/workflows/ci.yml) runs typecheck + lint + tests for the bot, ruff + mypy --strict + pytest for the retrieval service, and typecheck + build for the web on every push.
  • Quality gates fail closed. When the judge panel rejects every variant, nothing is posted; when the panel drops below quorum, it refuses to score rather than ruling by one surviving judge.
Repo layout
twitter-agents/
├── beef/                  # 🤖 the bot — 38k LOC TypeScript
│   ├── src/
│   │   ├── agent/         # LLM providers + ProviderManager (3-tier fallback)
│   │   ├── roast/         # prompt builder, engine, creative memory
│   │   ├── evaluation/    # 5-judge panel, weighted vote, vetoes, pre-filter
│   │   ├── farm/          # offline batch generation + blind human review
│   │   ├── twitter/       # API / scraper / stealth-Playwright clients
│   │   ├── reply-guy/     # proactive reply pipeline
│   │   ├── monitor/ news/ # timeline + news-thread pipelines
│   │   ├── queue/ scheduler/ storage/   # SQLite queue, jitter, repositories
│   │   ├── learning/      # engagement tracking + analysis utilities
│   │   └── admin/ health/ # Telegram admin bot, health monitor
│   ├── characters/        # bot personality definition
│   └── docs/              # architecture, playbooks, audits, metrics reports
├── beef-web/              # 🎨 landing + React app + card renderer
├── retrieval-service/     # 🔎 Python/FastAPI semantic retrieval (hybrid BM25+vector, RRF, evals)
└── docs/assets/           # README media

The bot exposes several generation entry points (roast, roast_fast, roast_max, roasttweet) — these are latency/quality tiers of the same engine sharing one prompt builder and evaluator, not duplicated logic.


Getting started

Reads work with cookie auth; posting on a fresh account needs the browser path (see beef/CLAUDE.md). Runs fully in DRY_RUN without ever touching Twitter.

# The bot
cd beef
pnpm install
cp .env.example .env          # fill in Twitter creds + Telegram token
pnpm typecheck && pnpm test   # 600+ tests
DRY_RUN=true pnpm dev         # full pipeline, nothing posted

pnpm farm generate            # offline: batch-generate & self-score roasts
pnpm metrics                  # pull & analyze Twitter performance

# The web app
cd ../beef-web
pnpm install
pnpm dev                      # localhost:5173
pnpm generate-card            # render a roast card to a PNG

Status

Paused (June 2026). It ran, it posted, the telemetry said scaling it wasn't worth it, so it was stopped rather than propped up. The code is the artifact — a working, tested, honestly-measured autonomous agent, kept here as a portfolio piece.

About

Autonomous AI Twitter agent — LLM orchestration with circuit-breaker fallback chain, 5-judge LLM-as-a-Judge eval panel, prompt calibration from human feedback. TypeScript, 590 tests, CI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages