tootsies

a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.


Project maintained by mejasonmejason Hosted on GitHub Pages — Theme by mattgraham

X (@tootsiesbar) growth roadmap: 18 → 100 followers

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.

1. Measured baseline (2026-07-29)

The account (twitterapi.io 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)

Output volume (Axiom 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.

Write modes (Axiom 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).

The game (Axiom x_game_round / x_game_answer)

64 rounds opened in 7d (~9/day). 3 correct answers in 30 days.

The exact config producing this (live 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.

2. Diagnosis

  1. Firehose to nobody. X ranks by engagement rate. 136 posts/day at 0–20 views each trains the algorithm the account is noise → suppressed distribution → zero engagement → repeat. Volume is actively hurting.
  2. Wire service, not a person. One hour of timeline: WNBA box score → Chelsea keeper loan → Luminate units → Netflix leaderboard %. No legible identity to follow. Most takes read as AP copy; the one post in the sample that earned a like was the one with a voice (“Come & Talk to Me still clears a room thirty-five years later”).
  3. Zero discovery loops. Sub-1k accounts get found in the quotes tab of viral posts, in replies, in trends, in communities, or by referral. We use none: API replies to third parties 403, wire desks post standalone photos (no quote → no quotes-tab presence), nothing names a trend, the Discord was never recruited.
  4. A game with no audience. 9 unanswered rounds/day is noise; games amplify an audience, they don’t create one.

3. Target state (exact)

≤ 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.


4. Phase 0 — config + owner actions (today, ~30 min, no code)

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.

5. Phase 1 — the two first code PRs (this week)

1a. Per-surface X daily budgets + score floor (the durable volume fix)

1b. Reply drafts — the hand-off loop (SHIPPED with this PR; owner steer 2026-07-29)

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:

The tic residue: two dead ends, then a guard that works (owner, 2026-08-03, “these idiosyncrasies in her prose are horrible”, with Discord searches showing 21 hits for “flex” and walls of “the real X” / “nobody’s talking about”). The #1970 cut halved the dominant frame (realnoun 28% → 11%); the residue then took three attempts, and the sequence is the story (numbers in docs/PROMPT_OPTIMIZATION.md):

  1. Feeding the surface its own last 8 drafts as the desks’ “YOU POSTED THESE RECENTLY (vary it)” block doubled the tics (any-tic 9/35 → 19/36) and multiplied register flags — the block primes exactly what it shows. Built, measured, reverted.
  2. The CALIBRATION pool’s appositive exemplars were a wash — cutting the block, or filtering it to non-appositive lines, both landed on baseline. The pool teaches register, not frame.
  3. SHIPPED — the tic guard (#1975): a deterministic detector (utils.x_reply_draft.tic_hits, trips 39% of live drafts vs 6% of the owner’s own replies) plus ONE re-ask that names the offending phrase. Tics 9/36 → 2/36 (25% → 6%, p=0.046 — the owner’s own rate), register a wash, specifics intact, one extra compose call only on the drafts that trip. The retry is taken only if it comes back clean, so a degraded rewrite can never replace a good line. Telemetry: x_reply_tic_guard (ok = the retry replaced the draft) — watch the ok rate.

The general lesson, since it cost three rounds: “vary it, here’s everything you said” primes a verbal habit; “you wrote X, say it without X” removes it.

Banger lane (SHIPPED — owner steer 2026-08-03, “are we looking at replies / example of a banger”, with a 1.1M-view quote-tweet as the example): the reply lane can only ever draft under the code-owned wire accounts, because that is the only pool the desks fetch. The owner’s own wins say the pool is wider: of her top 20 hand-posted replies, four landed under accounts the wire list has never carried (@Rap, @DrewPavlou, @THEUN1VRSE, @Kurrco), and all 20 sat under parents of 300K–17M views — the 11.7K-like winner (“Superhero fatigue was really just slop fatigue”) under a 17M-view @FilmUpdates post with 862 replies. So the fourth trigger finds targets by heat, not by account: each pop/sports/cinema_news slot takes search terms from its OWN top stories (utils.banger_lane.search_terms, free and on-lane), runs ONE X advanced_search (queryType=Top, min_faves + lang:en + -filter:replies + since_time, all verified live), and drafts under what clears the floors (pick_bangers: 3h window, view AND like floors, originals only, no piracy/farm spam, ranked by engagement velocity so a climbing post beats a bigger stale one, one per author). Everything downstream is the shared path — same produce_draft, same per-tweet dedup, same daily cap, same kill switch, same EMPTY decline, same #bot-logs hand-off — so the only new thing is discovery. Bounds are env (X_BANGER_PER_SLOT=2 with 0 disabling the lane, X_BANGER_MIN_VIEWS=50K, X_BANGER_MIN_LIKES=500). Telemetry: banger_lane (surface, count, kept) — a sustained kept=0 with count>0 is the floors drifting past what the search returns, the one failure here that raises nothing.

Reply lane (SHIPPED — owner steer after the reply data landed: 12 owner replies out-viewed 48 originals ~13×, so more fresh targets, faster): the wire desks’ THIRD lane, living directly alongside biggest + breaking. Each pop_desk / sports_desk / cinema_news slot, after the two posting lanes claim their moments, kick_reply_lane drafts the freshest-hottest remaining story with an X link (pick_reply_stories — the same order_for_breaking ranking, so “worth replying to now” = “breaking-worthy now”) through the same draft path, tagged reply · <desk> in #bot-logs. Rides the desks’ own slots + the wire fetch already paid for (no new scheduler, no extra X reads); fire-and-forget so it never delays the desk’s delivery; one shared per-tweet dedup + daily cap across both triggers (X_REPLY_PER_SLOT=1 per desk slot).

6. Phase 2 — mentions responder (week 2: broadcast → conversation)

7. Phase 3 — ride other audiences (weeks 2–4)

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.

8. Phase 4 — every post earns its slot (weeks 3–4)

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.

9. Phase 5 — telemetry (ships alongside Phase 1, decisions come from it)

10. Verification queries (Axiom APL)

# 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

11. Cost + risk

12. Sequenced backlog (→ epic #1793)

# 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 Quote-first breaking lane 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 lane (the desks’ third lane) — SHIPPED 1b kick_reply_lane in the wire desks