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 + scheduler_firehose_two_hourly_surfaces (all three cadence sets) 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 (x_audience shipped with these names; all three ride the folded `detail`)
['tootsies'] | where event == 'x_audience' | extend d=parse_json(detail)
  | project _time, followers=toint(d['followers']),
      views_originals=toint(d['median_views_originals']),
      views_replies=toint(d['median_views_replies'])

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

13. Measured against @Kurrco and @talkofthecharts (2026-09-21)

The owner asked why the account is not like these two. Measured the same day from the live X API (last_tweets, 100 posts each) and Axiom (crosspost, last 7 days):

  @tootsiesbar @Kurrco @talkofthecharts
Followers 284 439,548 3,475,089
Posts per day 74 to 149 24.5 4.5
Median views per post 41 163,168 97,149
Median likes per post 0 2,752 1,498
Video posts (last 100) 0 52 1
Replies + quotes (last 100) 0 7 30

What they post. @Kurrco: 35 video clips (an interview quote, a performance, an artist’s own IG video), 22 release announcements with the video, 8 anniversaries, 8 chart numbers; every anniversary post ends with a question. @talkofthecharts: only #1s, top-10 entries, debuts, records and first-evers; every post has a second sentence that says why it matters, credits the source and tags the artist; posts land when the chart drops. @tootsiesbar: 100 rendered number cards in one day, 449 of 740 X posts that week from music_desk, no video, no replies, no questions, and four forecast posts for one album in 24h as its number drifted.

Engagement by artist tier, over the 600 posts before the steer (the live kworb watchlist at 50 / 100 / 1000, artist_watch.build_watchlist):

Tier Posts Likes per post Posts with a like
top 50 136 10.6 47%
ranks 51-100 27 0.2 15%
known (101-1000) 157 14.2 (LISA, ROSÉ, Jhené Aiko, Ella Langley carry it) 25%
unknown 55 0.1 9%

The owner’s steers, and what shipped (the music_desk.py notability-trim entry in docs/ARCHITECTURE.md has the code):

  1. Trim the artist tier, on the STREAMS family only (the owner’s scope, same day: “debut and album forecasts should stay, but streams we can keep with the gate”). Ungated, any act: first-week units, album-equivalent units, pure sales – the debut and album forecasts, their settles, the year-to-date counts. Gated, a recognized act only: an album’s first-week streams, an artist’s weekly and annual streams.
  2. Exits off (12 a week).
  3. A settle miss is stated, not graded (“came in UNDER by” is gone). 3b. A light track milestone (100M / 250M / 500M) breaks only for an act in the top 50 of the watch (“I like 100 million for artists I care about”). The billion rungs and the artist ladders are untouched. About 8 of 62 milestone posts a month. 3c. A chart tenure under two years is not a card: days on a Spotify daily chart under 730, weeks on a Billboard chart under 100 (“cut below a year … multiple years”). On the #3557 ladders that keeps the 1,000-day and 100-week rungs and up; peak stories untouched.
  4. Reverted on the owner’s call after the enumeration below: the fall cut (“keep the falls within top 20”), the cap on a watched act’s deep re-entry (“re-entries are okay if the artist is popular enough”), and every projection-lane change (“keep projections as they are, we’ll tackle those separately” – the code is at commit 0d79417 on the PR branch).
  5. Rejected by the owner: the four-image prediction sheet. Not built.

Every desk lane by weekly count (Axiom music_desk_posted, 2026-09-14 to 09-21, 468 posts; music_alert_posted, 38), grouped the way the owner reads them:

Group Lane Posts / week
Projections (original content) projection (22 pure sales, 18 album-equivalent, 12 releases) 40
  tracking (all streams: year-to-date and weekly on-pace) 26
  reveal 7
  alert line_move (7 streams, 2 sales) 9
  chart_proj, market_board, genre_board, called_shot, debut_sales, reconciliation 20
Chart relay watch_chart (63 climbs, 28 position-only falls, 15 debuts, 12 other) 118
  chart (18 climbs, 10 falls, 4 debuts, 27 re-entries: 3 top 20, 5 at 21-40, 6 at 41-100, 13 past 100) 64
  platform_board, artist_board, watch_board, chart_board, standings, board_presence 63
  alert platform crowns, jumps, debuts, radio risers 22
  chart_exit + chart_exit_board (now off) 12
Streams and milestone fluff milestone 32
  artist_catalog 13
  chart_tenure 12
  stream_report (staged, Discord only) 14
  radio 10
  daily_hits 6
  year_race, year_top10_songs, year_n1_songs, song_streams, riaa_plaques, riaa_board, pollstar_board, lookup_board, radio_format_board, ktt2_chatter 26

What the trim cuts, measured before it shipped

The owner asked for this with the trim: “make sure we have metrics for this – what we’re cutting and how much we’re cutting each, so we can review in a week”. The numbers below are the BASELINE the first week is read against. The desk posted 1531 cards in the 28 days before the change, which is 383 a week.

Three of the four cuts count themselves live, through music_desk_trim (reason names the gate). The exits lane does not: it returns before it reads Billboard, so counting it would mean doing the work the switch turned off.

Read the counter’s two kinds of row differently. The milestone and tenure gates fire at a crossing and settle the rung on the way out, so each fires once and the count is cards. The tier gate runs every slot over the whole live Kalshi set, and a refused reading never posts, so it is offered again next slot. For that row read the distinct acts per day, not the raw count. The dashboard panel shows both columns.

Cut Before, per week Cut, per week Basis
Chart exits (card + board) 12.0 12.0 (all) 48 posts in 28 days
Chart tenure under the floor 7.5 5.8 (77%) 23 of 30 real cards
Light track milestones, 100M to 500M 6.5 3.2 (50%) 13 of 26 real cards
Streams tier gate see below about 2.7 (estimate) 11 of 111 live readings
Total 383 about 21 (5%)  

The tier gate is the one estimate: it refuses a READING, and the desk posts only some of its readings each slot, so the post count follows from the live counter rather than from this measurement. One live Kalshi read gave the exact refusal set: 111 readings, 69 of them streams-family, 11 cut – Rampa, ATRIP, Black Coffee, BLOND:ISH, Sylvan Esso, Bonobo, MONSTA X, Alabama Shakes, Bella Poarch, Beck, Ms. Rachel. Zero of the 42 units and sales readings were cut, which is the proof that the debut and album forecasts the owner kept are untouched.

The borderline worth knowing: the top-50 rule cuts Ed Sheeran at daily rank 53 and Tame Impala at 51, and Tate McRae is off the daily page altogether. Raising MUSIC_DESK_LIGHT_MILESTONE_TOP_TIER on Railway pulls them back with no deploy. Its useful range is 1 to MUSIC_DESK_WATCH_ARTISTS (100), because the watch only places names it carries: set above that, the gate cannot answer its own question and stops cutting entirely, which is a way to switch the rule off rather than to widen it. Lower it to cut deeper.

Read a ZERO the other way round. All three reasons flat at zero for a week does not mean the noise stopped. It means the kworb watch is failing open, because an empty watch reads every act as recognized. Check chart_fetch before calling a zero a success.

Still open from this roadmap: Phase 1a (the per-surface X budgets and the 0.75 floor) is not built; utils/x_crosspost.py has no budget code. The account still posts about 100 times a day. The trims above cut the noisiest lanes at the source; the budget is the durable volume fix.