a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.
Focus lanes: music, pop culture, sports (the bio already says it: “music, pop culture, and sports served neat”). Goal: a compelling account that earns 100+ followers. Every number below is measured (live account + Axiom + the live DB), and every work item names its exact lever — file, function, env var, config page, or owner action.
user/info + last_tweets)| Fact | Value |
|---|---|
| Created | 2026-07-20 (9 days old) |
| Followers / following | 18 / 100 |
| Total posts | 1,204 (932 with media) |
| Tier | X Premium (subscription_type=Premium, 25k-char cap) |
| Pinned tweet | none |
| Views per tweet (last 20) | 0–20, median ~3 |
| Likes per tweet (last 20) | 0 (one tweet: 1 like) |
crosspost, delivered=posted)Per day, last 7: 178 · 135 · 105 · 135 · 128 · 135 · 138 (avg ~136/day).
Per surface (3d): sports_desk 77 · music_alert 62 · cinema_news 51 · pop_desk 45 · music_news 39 · music 32 · discourse 29 · market_alert 26 · bet_board 16(+32 skip) · market_drop 13 · music_desk 12 · cinema_desk 9.
tweet_posted, 3d)media 387 ok · upload 355 ok · tweet 52 ok · upload_video 48 ok · like 31 ok · quote 28 ok / 17 rejected (38% fail — the #1575 third-party 403) · reply 3 ok / 1 fail (own-thread only; replies to non-mentioners are tier-blocked, #1732).
x_game_round / x_game_answer)64 rounds opened in 7d (~9/day). 3 correct answers in 30 days.
settings rows, master guild 1482402399342825522)| Setting | Current value |
|---|---|
hours_start_et / hours_end_et |
0 / 23 — the bot posts 24/7 |
scheduler_firehose_surfaces |
9 surfaces on the every-30-min firehose: cinema_desk, cinema_news, curator, discourse, market_drop, music_desk, music_news, pop_desk, sports_desk |
scheduler_firehose_hourly_surfaces |
music (hourly) |
post_schedule (calendar) |
yaps: 12× market_drop + 11× x_game; chill: 6× x_game (17 game slots/day) |
x_crosspost_surfaces (X picker) |
music, music_news, discourse, market_drop, market_alert, cinema_desk, pop_desk, sports_desk, cinema_news (bet_* already off X) |
So the ~136/day isn’t a bug — it’s 9 firehosed surfaces × 24/7 working hours, mirrored to X, thinned only by each surface’s own self-gate/dedup.
≤ 25 X posts/day, budgeted per surface, every post the surface’s best:
| Surface (X) | Budget/day | Notes |
|---|---|---|
| pop_desk | 4 | breaking + biggest lanes |
| sports_desk | 4 | breaking + biggest lanes |
| music (drops + alerts + desk, one toggle group) | 5 | the alert stream is today’s 21/day — the budget forces best-of |
| music_news | 3 | |
| cinema_news | 3 | |
| discourse (trending reacts) | 2 | |
| x_game | 2 rounds (≈5 writes: 2 opens + boards/reveals) | 12:30 + 20:30 ET |
| trend beat (new, Phase 5) | 2 | 11:00 + 19:00 ET |
| Total | ≈ 23–25 writes/day | verified by the APL query in §10 |
market_drop + market_alert: off X entirely (Kalshi swings are room content). Discord rooms keep their current firehose untouched — the budgets live X-side only.
Follower ladder (dated): ≥35 by Aug 5 · ≥45 by Aug 12 · ≥70 by Aug 26 · 100+ by Sep 9. Each gate blocks the next phase until telemetry (§9) explains a miss.
0.1 X picker trim (/menu → experiments page → x_crosspost picker): uncheck
market_drop and market_alert. Saves ~13 posts/day immediately (39/3d measured).
0.2 Firehose off → packed calendar cycle (owner decision 2026-07-29). Clear
scheduler_firehose_surfaces + scheduler_firehose_hourly_surfaces and pack the
calendar grid instead. Recommended ratio over a 28-slot day (working hours
trimmed to 10:00–24:00 ET — overnight slots earn zero US engagement; if hours
stay 24/7, keep this same ratio in the daytime block and give overnight slots only
to the Discord-only surfaces):
| Surface | Slots/day | Placement notes |
|---|---|---|
| pop_desk | 4 | spread 11:00–22:00, one in 18:00–21:00 prime |
| sports_desk | 4 | one near lunch, two in the 19:00–23:00 game window |
| music (drops) | 3 | mid-morning, afternoon, evening |
| music_news | 3 | |
| music_desk | 2 | |
| cinema_news | 2 | |
| cinema_desk | 2 | |
| discourse (trending reacts) | 3 | |
| x_game | 2 | 12:30 + 20:30 ET (US lunch + prime) |
| curator (Discord-only on X picker) | 2 | |
| market_drop (off X) | 1 | |
| Total | 28 | X-mirrored ≈ 25 writes/day incl. game boards |
In ratio terms: pop : sports : music-group : cinema : discourse : game ≈
4 : 4 : 8 : 4 : 3 : 2, plus 3 Discord-only slots. The remaining X-side leak is the
event-driven music_alert stream (rides the music toggle, ~21/day measured) — cap
it via its tunable or let the Phase 1a budget close it.
0.3 Discord recruit (owner). One announcement in the master guild: follow @tootsiesbar, tap the notification bell, like/reply when you see her — first-hour engagement is what buys distribution. Target: +15–25 followers from the server by Aug 5. Optional: X game wins get a server shoutout.
0.4 Pin an intro tweet (owner, 5 min). In her voice: who she is, what the account serves, the Discord link. No pinned tweet exists today.
utils/x_crosspost.py — maybe_crosspost gains score: float | None = None
(each calling surface already holds its self-gate score; thread it through).score is not None and score < X_CROSSPOST_SCORE_FLOOR (default 0.75; room floor
stays 0.6, X takes the top band only); (2) per-surface daily budget — a DB server-rate
counter (the image_daily_cap pattern) keyed x:{surface_group}, defaults from the
§3 table via _X_DAILY_BUDGETS (env-overridable X_BUDGET_<SURFACE>); count only on
a successful post.crosspost gains delivered=skip, reason=budget|below_floor (both
reason values already fit the shared vocab; no new field names).tests/test_x_crosspost.py (floor skip, budget skip, budget resets
daily, score=None surfaces unaffected by the floor).The account’s API tier can’t reply to third-party tweets (#1732), but the owner can — and replies are the one discovery channel that grows a sub-100 account. So Toots does the drafting, the owner does the posting, and X stays visually unchanged:
cogs/x_reply_draft.py (+ pure core utils/x_reply_draft.py, unit-tested):
an on_message listener — whenever SHE posts a message in the master guild
that links an X status (wire desks, curator, music news — every tweet she already
sourced as engaging), it fetches the tweet (fxtwitter, keyless), drafts the reply
she’d leave under it (claude.draft_x_reply, Sonnet, purpose=x_reply_draft),
and posts the draft to #bot-logs — the owner’s one working queue — with the
draft in a copy-block + the tweet’s bare fixup link (Discord unfurls the
tweet inline under the draft; tapping it lands on the real x.com post to reply),
pinging the master user only (users-only allowed-mentions), tagged with the
room the content shipped in.gather_context_beat, Grok X-crowd + Perplexity, framing="breaking") on the
tweet’s subject, so the reply rides or subverts the angle the conversation is
already on — context only (never recites the beat’s numbers as facts), fail-open
to a tweet-only draft when unprovisioned or the pull misses.X_REPLY_DRAFT_DAILY_CAP (100 — a backstop for a
bulk-op burst, not a throttle; the decline + dedup are the real pace), scoped
to an allowlist of the channels explicitly configured for the X-post surfaces
(the /menu channel sets — curator rooms and unwired channels are out by
construction; owner steer), her own tweets skipped, master kill switch, fully
fail-open. Zero X writes — read-only + Discord.x_reply_draft event (delivered room|skip, reason
dedup|daily_cap|fetch_failed|declined|send_failed|error).(The earlier “reply-scout” variant — proactively hunting posts she has NOT already covered, 3×/day into the discourse channel — stays in the backlog as a follow-up once the draft loop proves out; the drafts cover her already-sourced topics first.)
to:tootsiesbar (the x_game webhook infra, #1497, already
polls ~60s). Dispatcher order in /x/webhook: a reply targeting an open game round →
the game (unchanged); anything else → the new cogs/x_mentions.py./ask brain (persona, constitution, grounding, abuse gates) →
XPoster.post_tweet(reply_to=...) — allowed on our tier because the author mentioned
her. Self-gate ≥0.6 like every surface.X_MENTIONS_DAILY_CAP=24 (≈1/hr), per-author cap 3/day, never reply to
bots or to her own account, conversation-level dedup.x_mention_reply event + ops-monitor line.3a. Quote-first breaking lane — REJECTED (owner steer 2026-07-29). Quotes
weren’t earning traction when on, and visually they read dead-similar to the tweet
they quote — a bad look. The owner replies manually instead, powered by the 1b
draft loop. (Kept here so the decision is on record; don’t re-propose.)
3b. In-lane follow-on-amplify. When a quote ships, follow the quoted account if in-lane, capped 8/day (durable counter). Follow-backs are real at this size; aggressive following is a suspension vector, so the cap is hard.
3c. Communities dry run. POST /2/tweets accepts community_id; write
scripts/dryrun_x_community.py, post one test into a large music Community. If the
tier 403s, the item dies; if it lands, wire community_id through
XPoster.post_tweet and give the x_game 12:30 round a Community home.
4a. “Take, not headline” X prompt pass. The desks’ compose prompts get an X-side nudge: lead with the verdict in her voice; a box score is garnish, never the lead. Measured by §9’s per-surface views.
4b. Weekly thread. One music/pop deep-dive thread per week (reply-to-self works; Premium longform distributes). Anniversary, chart story, sample-lineage.
4c. Polls. POST /2/tweets supports polls — “which album wins the week” / “who
takes tonight” — small XPoster addition, 2–3/week.
maybe_crosspost stores tweet_id → surface in a
kv namespace (x_tweet_surface, 14-day TTL) — the attribution the sweep needs.user/info (followers /
following) + last_tweets (~100 tweets) → emit x_audience (followers,
following, tweets_24h, views_median, views_sum, likes_sum, engagement_rate) + one
x_surface_perf row per surface (views_median, posts). Fields registered in
utils/event_schema.py.x_stalled (7-day follower
delta < +5 with ≥50 posts) and x_engagement_zero (median views < 25 over ≥50
posts). Deterministic core unit-tested like every finding.crosspost surface="trend_beat".# posts/day (acceptance for Phase 0/1: ≤25)
['tootsies'] | where event == 'crosspost' and delivered == 'posted' | summarize count() by bin(_time, 1d)
# per-surface budget adherence + skip reasons
['tootsies'] | where event == 'crosspost' | summarize count() by surface, delivered, reason
# mention-responder volume
['tootsies'] | where event == 'x_mention_reply' | summarize count() by bin(_time, 1d)
# audience curve (once x_audience ships)
['tootsies'] | where event == 'x_audience' | project _time, followers, views_median, engagement_rate
utils/x_crosspost.py has_link); cutting ~110 posts/day cuts the X bill roughly
5×, and the twitterapi.io credit burn with it. The scout + sweep add only reads
(memoized wire fetches + 1 trends call + ~6 last_tweets pages/day).| # | Item | Phase | Effort | Type |
|---|---|---|---|---|
| 1 | X picker: market_drop + market_alert off | 0.1 | 2 min | /menu config |
| 2 | Firehose off → packed calendar per the §4 ratio | 0.2 | 15 min | /menu config (owner) |
| 3 | Discord follow campaign + pinned intro tweet | 0.3–0.4 | 30 min | owner |
| 4 | Per-surface X budgets + 0.75 score floor | 1a | S–M | code (utils/x_crosspost.py) |
| 5 | Reply drafts under every linked tweet — SHIPPED | 1b | — | cogs/x_reply_draft.py |
| 6 | X mentions responder (standing rule → /ask → in-thread) | 2 | L | code (cogs/x_mentions.py) |
| 7 | 3a | — | rejected (owner steer) | |
| 8 | Follow-on-amplify, 8/day cap | 3b | S | code |
| 9 | Communities dry run → community_id |
3c | S | script, then S |
| 10 | “Take, not headline” X prompt pass | 4a | S | prompt |
| 11 | Weekly thread surface | 4b | M | code |
| 12 | Poll support + 2–3 polls/week | 4c | S–M | code |
| 13 | x_audience + x_surface_perf telemetry + ops line |
5 | M | code |
| 14 | Trend beat (2/day, names the trend) | 5 | M | code |
| 15 | Reply-scout (posts she has NOT already covered) | later | M | follow-up to 1b |