a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.
What every external API hands us vs. what we surface to the model. This is the “are we dropping anything” ledger: each integration lists the fields we extract and the fields we deliberately omit (with the reason). The rule is either 100% of the answer-valuable fields, or an explicit entry here saying what’s missing and why — so an omission is a documented decision, never a silent gap.
Scope note: “surface to the model” means the field reaches Claude’s context (a rendered block or a tool result), not telemetry. The constitution’s data-minimization rule governs telemetry + her outbound replies, NOT the in-context input we feed the model, so enriching context is in-spec. A handful of fields are withheld from output by the HARD RULES (no personal-info disclosure) even when fetched — those are noted.
Every surfaced field is also self-describing in its rendered block or tool description (a header legend or inline label), so the model knows what each value means and what to do with it — not just that it exists.
utils/billboard.py)The first-party chart read (server-rendered HTML, no key). One parser covers every
chart in CHARTS; past weeks are addressable by date.
rank, title, artist, last_week, peak, weeks_on, plus the
derived is_debut / is_reentry / is_new_peak / movement() and the chart’s
Week of date. That is 100% of what a Billboard chart row publishes — the page
carries no other per-row datum.movements) rather than read, because Billboard doesn’t publish them as
fields — they fall out of rank vs last_week vs peak. Week-over-week entries and
exits need two weeks (diff_weeks), since a song that fell OFF the chart is by
definition absent from the current page.charts-static.billboard.com thumbnails
— the entity-image path already resolves art, and a chart thumbnail is a worse source
than the catalog one); the per-row “chart detail” expander (an AJAX sub-request per
row — 100 extra fetches for award/imprint trivia no surface asks for, and a volume
we’re deliberately not spending against a host whose robots.txt names us).source_blocked), never a stand-in.utils/hits.py)The industry first-week projections + Mediabase radio feeds, read off HITS’ public
Sanity GROQ API (no key; access reasoning in docs/BILLBOARD_SOURCES.md §5b).
units (Activity),
change_pct, the full consumption split — album_sales (pure), tea, sea —
last_week (None = a projected DEBUT), label marketshare, the tracking-week end
and the derived chart_date (the Billboard week the projection is FOR). Columns are
read by label, so the midweek chart’s subset (Activity + Albums only) yields
None for the absent fields rather than shifted values.genre — load-bearing: the
docs concatenate ten format charts each ranked 1–50, so rank is only meaningful
per-format via by_genre), adds, plays_this_week/plays_last_week + the
derived spin_gain, rank, last week.chart_data TSV twin of every projection doc (the
same data as chart_entries, unlabeled — we read the structured shape); the YTD /
year-end chart types (no consumer yet; one registry row each when a lane wants
them); editorial post documents (we take their data, not their journalism).format_projection_block, not left to a prompt.utils/chart_boards.py: the four daily projection boards read the full
row set (rank, units, pure album_sales, last_week), and the weekly debut-sales
board joins the building chart’s units to the printed Billboard 200 debuts under
the chart_date == week_of alignment fence. All HITS-sourced cards carry the HITS
source pill; projected boards additionally date themselves “PROJECTED FOR …”.
proj_pure_sales (#2098) reads the same rows but ranks on album_sales alone, and
drops any row the document gives no pure-sales figure for rather than substituting
units — the two are different measures and a card calling streaming activity a sale
would be the one way that board could mislead.utils/kworb.py, #youtube-chart-art)The “Today’s Most Viewed Music Videos on YouTube” pages — the global top-500 and the anglophone top-300, read as one merged list. Their read is CARD ART, not model context: nothing on these rows reaches Claude, so the ledger question here is only whether we drop something the art path could use. (The other kworb pages — Spotify, Apple, iTunes, radio — have no entry yet; that is a documented gap, not a decision.)
maxresdefault (1280×720) then
sddefault (640×480), never hqdefault (480×360). Every video id serves hqdefault,
so taking it would turn a genuine miss into a soft, cover-cropped card.utils/chart_boards.py:video_views_board, owner ask
2026-08-10): the most-viewed board now DOES put these rows in front of the
model (position, title, today’s views — the top 10 only). The “P+” move column
and likes stay unread there too.utils/kworb.py, owner ask 2026-08-10)The all-artists ranking (spotify/artists.html) and the monthly-listeners ranking
(spotify/listeners.html), consumed by /ask’s spotify_chart standing line, the
watch sweep, and now the artist leaderboard boards (utils/chart_boards.py).
Daily +/- day-over-day change (the
listener-movers board’s whole basis — parsed since 2026-08-10), and peak
listeners (listeners page).utils/genius.py)web_pages source links, song art image
URL, stats.pageviews, the song page URL.language tags (English-assumed); non-credited media
artists.utils/chart_credits.py, owner ask 2026-08-10): the
producer/writer boards read producer_artists + writer_artists per Hot 100
row, artist-verified against the chart credit and cached durably 45d
(kv_cache: chart_credits; verified misses 7d). Everything else on the song
object stays deliberately unread there — the reference lookup above is the
full-coverage consumer.utils/ktt2.py, #ktt2)The music FORUM, read as discourse (search_ktt2) and as a breaking signal
(ktt2_breaking). No key and no API: every section page server-renders its Apollo
GraphQL cache into __NEXT_DATA__, so one GET yields typed JSON entities. Access
reasoning + the three pre-ship checks are in the module docstring.
title, postCount (rendered as the traction signal),
createdAt + the last reply time (which drive both the breaking velocity and the
discourse recency rank), isPinned / isLocked, KTT2’s own resolved artists and
album entities, and the opening post’s prose. Plus the board’s own
trendingArtists list, which is a ranking we do not have to compute.id, slug and every URL. Their
terms bar commercial use and “public display” of their materials, so KTT2 is an input
we synthesize from, never a link we paste — the same rule Reddit follows for the
owner’s “not reddit-nerdy” steer. KttThread has no url/slug/id FIELD, so the value
never reaches a formatter, and post_text strips every URL out of post bodies via the SHARED url_guardrail.strip_urls (the
second door: a release thread’s opening post is mostly artwork and YouTube links).
tests/test_ktt2.py asserts both, against the real fixture.likeCount (we take the board’s
temperature, not who said what — the constitution’s data-minimization line); the
per-thread reply bodies (we read the opening post only, so one GET serves both tools);
the archive (there IS no server-side search — ?search= and /search?q= both return
the unfiltered section, so both reads work off the ~50-thread live front page, which
is the right scope for “what is the board on right now” anyway).ktt2_breaking joins each thread’s artists to the
desk’s WATCHED list (utils/watchlist_source.py, ~136 names off kworb) and marks those
threads [ours], ranked first — so “what are our artists blowing up about” is
answerable. The join is on KTT2’s OWN resolved artist entities, never on words in the
title, and it reuses artist_watch.Watchlist.contains, so the styling gaps that module
already handles come free: measured live, KTT2’s JAŸ-Z and A$AP Rocky both matched
the kworb spelling with no new alias. An empty watchlist (kworb down) marks nothing and
degrades to plain velocity order — never to silence.search_ktt2 and ktt2_breaking /ask tools.utils/apple_music.py)collectionArtistName, duration, release
year, track/disc number, genre, explicitness, copyright, artwork (100px +
derived 600px), preview URL, clean track/collection URLs, track count.wrapperType/kind (internal classification, used
only to filter); artistLinkUrl (iTunes itms:// deep link, not
web-browsable — web URL kept instead); purchase price/currency.utils/deezer.py)contributors (featured), album, album cover art,
duration, preview URL, rank, track id. album_cover reads the album-search
cover (cover_xl..cover) as the SECOND cover source behind Apple: a fresh
release often reaches Deezer before Apple’s search indexes it, so a release card
Apple can’t cover still gets its real art. It is trusted as verified
(deezer_cover, exempt from the crosspost media gate), so BOTH guards are STRICT
(stricter than the fuzzy matchers the release-date lookups use): the title must
be an EXACT normalized-title match (a longer same-artist title like “Love” vs
“Love Songs” is refused), and every requested-artist token must be in the
candidate credit (want <= cand), so a same-title album by another act, a
60%-overlap namesake (“Big Time Rush” vs “Big Time Machine”), or an unverifiable
non-Latin credit is refused.isrc (not answer-valuable). Known API limitation, not a drop.utils/songstats.py, utils/streaming_stats.py, utils/milestones.py)The RapidAPI aggregator — /tracks/stats, /artists/stats, /tracks/historic_stats.
Each payload’s stats array carries ~20 platform blocks (spotify, apple_music,
amazon, deezer, youtube, tiktok, instagram, shazam, soundcloud, tidal, itunes,
beatport, traxsource, tracklist, facebook, twitter, songkick, bandsintown, radio).
streams_total (track + artist career),
monthly_listeners_current, followers_total (each explicitly Spotify-attributed)
— the milestone/verify/desk headline numbers. PLUS the free cross-platform reach
clause (streaming_stats.format_reach_context, appended to every Songstats context
blob — verify / desk / /ask / milestone — and to the milestone take): YouTube
video_views_total + radio radio_plays_total (both real absolute counts) +
Apple Music PRESENCE (charts_current, charted_countries_total,
playlists_editorial_current, artist charted_tracks_current). format_stream_stats
also names the top non-Spotify streams_total platforms (only Spotify + SoundCloud
report one). All of this rides in the SAME response — every stats call returns all
~20 platform blocks — so the reach clause adds ZERO extra Songstats calls.views_total/followers_total/videos_total — virality),
Instagram, Shazam (shazams_total), SiriusXM royalties, Beatport/Traxsource
DJ charts, and followers_total on the social networks. Numeric and free off the
same payload; slot into format_reach_context when wanted. Captured here so the
richness isn’t forgotten.utils/musicbrainz.py)disambiguation.utils/chart_ages.py, owner ask 2026-08-10): the
oldest-songs board reads title + artist-credit + first-release-date
(year only) per chart row via the same release-group search, title+artist
fold-verified before a year is accepted, cached durably 120d (kv_cache:
chart_ages; verified misses 14d), paced ~1.1s per live lookup under MB’s
1 req/s etiquette.utils/wikidata.py)utils/chart_data.py)<sup> markup (stripped for clean cells);
trend/debut arrows (the peak number is authoritative).utils/reference.py)utils/markets.py, utils/sportsdata/board.py)fairOdds (sharp
consensus) alongside the book line, event URL.parse_game_board): every ou/yn market in the
~1,900-key tree — game lines, game props (corners, first-to-score, cards,
BTTS), and player props across all periods (quarters/halves/OT), each with
the book’s implied % and fair where it diverges. Labels derive from SGO’s
marketName, so a market type with no bespoke parser still flows./v2/account/usage (get_usage, quota-EXEMPT so it
answers even while degraded) reports tier + per-period requests AND entities
(the rookie tier caps entities at 100k/month — the cap that 429’d /bet dark).
Polled every ~30min → quota events → ops-monitor quota_low. The SAME read’s
per-minute requests/entities metric self-calibrates the proactive rate
limiter (order #46, SportsGameOddsClient.note_usage) to the account’s real
reported cap instead of the static SGO_RATE_LIMIT_PER_MIN guess, so a crater
caused by that guess being wrong for the account’s actual tier self-corrects.utils/markets.py)createdAt; full CLOB order-book depth
beyond top-of-book (surfaced via the detail tool, not the always-on block).utils/markets.py)createdAt, market owner; order-book depth
beyond 5 levels (readability); >72h candle history.title + parent event_title are qualified with the
platform their metric is measured on, read off that market’s own
rules_primary (markets.qualify_platform_metric, applied in
_kalshi_market_to_snapshot): “Morgan Wallen: Highest daily view count” →
“…Highest daily YouTube view count”, since Kalshi’s titles name a metric but
not where it’s counted and every surface renders the title verbatim. Grounded, not
guessed — exactly one platform must be named in the rules (never inferred from the
series ticker), so an ambiguous or already-qualified title passes through
untouched. rules_primary itself stays detail-only (below), so this adds no field.utils/the_odds_api.py)The SGO-down resilience backstop (#725 epic). Lower tier — 20,000 credits/month,
cost = #markets × #regions per /odds or /event-odds call; /sports + /events
are FREE, /scores = 1 (2 with daysFrom). x-requests-remaining is captured on
every call (client.credits_remaining) so the budget is observable.
Endpoints covered (all live v4 endpoints):
/odds — matchup, commence time, moneyline + spread + total, best price per
side across all books as a $100 payout at the consensus line; commence_time_to
bounds to the bookie’s next-2-days window./scores (ScoreEvent) — live + completed scores (home/away score keyed back
from the team name, completed, last_update). The centerpiece: SGO has no
clean finished feed, so this backs the live scoreboard + bookie settlement.
Wired as TheOddsApiScoreProvider (utils/sportsdata/providers.py), after
API-Sports + SGO (ahead of only the free ESPN backstop), so it only fills games
the primaries missed. Gated on SGO’s circuit-breaker state (sgo.degraded) — it
spends a metered /scores credit ONLY while SGO is down, and only for the
SGO-only sports (NFL/MLB/NHL/UFC) that actually go dark then, never the
redundant World Cup/NBA that API-Sports already serves. 5-min cache caps the
outage burn; off-season sports return empty (not billed)./events (EventInfo) — id, teams, sport, commence time (no odds, FREE) for
cheap event discovery./sports (SportInfo) — key, group, title, description, active, has_outrights./event-odds (PlayerProp) — per-event player props / alternate / period
markets: player (the outcome description), market key, Over/Under side, line
(point), payout, book. Wired into the /ask lookup_player_props tool as
the SGO-down BACKSTOP (#725): when SGO props come back empty, the named game is
resolved to its Odds API event via the FREE /events lookup and that ONE event’s
props are pulled (one metered /event-odds call, never a league sweep),
entity-filtered to the game’s players and rendered by format_odds_api_player_props.
The SAME /event-odds endpoint also backs the deep-game-market board backstop
(#725): get_event_board requests the deeper game markets (totals, both-teams-to-
score, corners, team totals, 2nd-half totals — ODDS_BOARD_MARKET_KEYS), parsed by
the pure parse_odds_api_board into the same GameBoard the SGO board produces so
format_market_edges/format_game_board render unchanged. Powers the commentator’s
market-edge beat and the /ask break_down_board tool when SGO is degraded.
Budget-guarded: the deep-board fetch only fires when sgo.degraded AND
client.has_enhancement_budget (credits above the _ENHANCEMENT_RESERVE of 5,000),
so the credits the /bet SGO-down slate + settlement backstop depend on are never
spent on the enhancement surfaces. ~5 credits per game per cache window (8 min).includeLinks=true bettable links — the per-outcome bet-slip link (else the
per-bookmaker event link) captured onto EventOdds.links ({outcome: (url, book)})
by _best_link_per_outcome: the best-priced book that has a usable link, since
_clean_link drops {state}-templated US-regulated books (BetMGM/BetRivers) we
can’t fill. Powers the betting VALUE alert (cogs/betting_value.py) — the value
is at the LAGGING book, so the card’s title links the sportsbook where you place it
(sportsbook_link in utils/sportsdata/format.py matches the value side), falling
back to the prediction-market link when no clean book link exists. includeLinks is
a paid-plan feature that adds NO credit cost (billing is markets×regions), so it
rides the existing /odds slate call for free.regions=us_ex exchange links (Kalshi-first card link, #kalshi-links) — the US
betting EXCHANGES (Kalshi/Polymarket/Novig/ProphetX) come back as bookmakers in the
us_ex region, PRE-MATCHED to the game by full team name. _event_links_per_book
captures each book’s clean event-page link onto EventOdds.book_links ({book: url},
keeping venue identity unlike _best_link_per_outcome); exchange_link picks
Kalshi first (CFTC-regulated, web-bettable) else Polymarket. Powers the betting
board + line-move alert card link (us_bettable_link in cogs/betting_board.py):
a US reader clicks the card title and goes to a market they can actually bet — the
fix for our own Kalshi discovery being unable to resolve a US-team-sport game line
(0/15, the city↔mascot wall). us_ex is a separate 1-credit-per-market region
call (billing is markets×regions) — cached per (sport, markets, regions). Coverage:
Kalshi on MLB/NBA/WNBA, Polymarket broad; soccer is Polymarket-only, tennis absent.
Venue-coherent card (the #kalshi-links follow-up): the us_ex Kalshi link embeds the
exact Kalshi event ticker (kalshi_ticker_from_link), so coherent_card_snap fetches
THAT event by ticker (kalshi_snap_by_ticker → KalshiClient.get_event_markets, free) and
renders the card’s chart + odds from Kalshi too — chart, odds, and link all one venue, no
“odds via Polymarket” split. Budget guard: the us_ex call is SKIPPED entirely when our
own prediction discovery already matched a Kalshi market (it already carries a Kalshi
link+chart+odds); us_ex is only reached for games our own discovery can’t resolve. A ticker
fetch miss keeps the Polymarket chart under the Kalshi link (the footer self-labels it).
NBA vs WNBA routing: one internal basketball tag spans TWO Odds-API sports, so the
sport_key is resolved by the game’s meta['league'] (bookie.odds_api_sport_key,
reusing sports.WNBA_LEAGUES) — a WNBA game routes to basketball_wnba (which has Kalshi
coverage) instead of basketball_nba. Shared by both the board’s exchange link and the
value alert’s sportsbook link.PlayerProp, where which book offers a prop matters, and on
EventOdds.links now, where you need to know which book to bet at);
includeSids/includeBetLimits/includeRotationNumbers (bet-slip ids + limits —
not answer-valuable for our surfaces); the multi-region books (uk/eu/au — us only, to
hold the credit cost at 1× region); the historical endpoints (/historical/*
— 10× credit cost, no live-resilience value); /participants (1 credit, just
full_name+id per team — we name-match already) + /event-markets (1 credit,
the available-market catalog per game — a future caller could use it to request
only the prop markets a game actually offers instead of a hardcoded list).x-requests-remaining/-used ride
every response (credits_remaining/credits_used), refreshed via a FREE
/sports call (refresh_usage). 188/20,000 used (~99% headroom) — the healthy
one when SGO + API-Sports were near their caps. Polled → quota events; also
the real-time market_fetch credits_remaining finding (#734).get_outrights, #2268): the same
/sports/{key}/odds path with markets=outrights on one region, so 1 credit
per call. Eleven sports carry has_outrights (verified live 2026-08-10: NFL
Super Bowl, NBA / NCAAB / NCAAF championships, MLB World Series, NHL, the four
golf majors, and the US presidential market); the sports boards use the five
league ones. Surfaced: every entrant’s name and American price, from the
SINGLE book quoting the MOST outcomes — the fullest field, chosen
deterministically rather than by taking whichever book the API happened to list
first — plus that book’s name for the card’s source pill. Omitted
(intentional): the other books, and any de-vigging. The implied percentages a
title-odds board shows sum to over 100% because the book’s margin is in them; the
card credits the book by name rather than presenting a blended number no source
published. Cached 12h, far slower than a futures field moves. Health rides
market_fetch source=the_odds_api_outrights.utils/api_sports.py)x-apisports-key account — v3.football
(soccer/World Cup), v1.basketball (NBA), and, for the Bookie’s settlement feed
(#mlb-betting), v1.baseball (MLB, league 1), v1.hockey (NHL, league 57),
v1.american-football (NFL, league 1), and v1.mma (UFC/MMA /fights, season-less).
League ids env-overridable (API_SPORTS_{MLB,NHL,NFL}_LEAGUE); seasons derived
per-call (MLB = calendar year, NHL/NFL = start-year; MMA needs none). All verified
live 2026-07-03 (Pro plan, 7,500/day). No tennis host exists (v1.tennis doesn’t
resolve), so tennis has no API-Sports finals feed — the free ESPN backstop
(utils/espn.py, #1113) fills tennis scores + is the intended tennis settler._team_game_to_snapshot) absorbs two shape
quirks: NHL scores are flat ints (scores.home, not .total) and NFL nests
id/status/date under an inner game object; only the final-score + status
(settlement-critical) fields are consumed for MLB/NHL/NFL, not the depth blocks.
MMA (_fight_to_snapshot) is a different shape again — fighters.first/second
with an explicit winner boolean (no scores), an event slug + weight category
(no league) — so the winner is encoded as a 1-0 ‘score’ for the shared settlement
path; a no-winner draw/no-contest reads 0-0 → a 2-way PUSH. Only UFC settles here,
so the Odds-API MMA supplement is fold-only (never adds a non-UFC promotion’s fight)./status (get_status, a free account read) reports
the plan + daily request cap (requests.current/limit_day; the Pro tier
sat at 90% — 6,767/7,500 — the day SGO died). Polled every ~30min → quota
events → ops-monitor quota_low. (Per-minute limits ride the response headers.)utils/highlightly.py)x-ratelimit-requests-limit/
-remaining ride every response, captured PASSIVELY off the post-game highlight
calls (the free Basic tier is only 100 req/day, too small to poll). Surfaced via
quota events when a recent call populated the headers.utils/espn.py, #1113)The FREE, keyless last-resort score/finals backstop for the SGO-only INDIVIDUAL
sports — tennis (SGO is its only source; no API-Sports tennis host) and
MMA/UFC (API-Sports /fights is its only settler; The Odds API /scores is
0-completed for MMA). Public site.api.espn.com site API, no auth. Wired as
EspnScoreProvider, the actual LAST provider in the hub chain (after
API-Sports + SGO + The Odds API), so first-provider-wins dedup makes it a pure
fill-in. Fetched per-sport ONLY while that sport’s primary is degraded (tennis ←
sgo.degraded, mma ← api_sports.degraded) — free, so the gate is consistency +
politeness to a no-SLA host, not cost.
Endpoints covered:
/sports/tennis/{atp,wta}/scoreboard — an event is a TOURNAMENT; matches
nest under event.groupings[].competitions[] (by draw). SINGLES kept (clean 1v1
names for the fold; SGO prices singles), doubles dropped./sports/mma/ufc/scoreboard — an event is a fight CARD; fights are
event.competitions[] directly.displayName), the
authoritative per-competitor winner boolean, status (pre/in/post + completed),
the numeric set/round score when present, start time. The winner is encoded 1-0
on a COMPLETED match (mirrors API-Sports _fight_to_snapshot) so the shared
settlement path pays the winner unchanged; a no-winner finish reads 0-0 → a 2-way
PUSH. match_key folds fighter names via canonical_fighter, so an ESPN row
reconciles with an SGO/API-Sports bet. recent_finals is bounded to a 48h window
(a tennis scoreboard returns a tournament’s whole ~1,158-match draw; the window
cuts it to the ~dozens that just finished).quota/
usage_fetch, correctly absent from the usage poll). Health is the market_fetch
source=espn ok-rate (ops-monitor integration-health + the ops-only espn
health-cog Watch) + the circuit_breaker integration=espn breaker; reachability
from the datacenter IP is the /debug/integrations espn probe (confirmed green
from prod: 200/31ms).The STATS reads (#2268). The same host publishes, free and keyless, the
team-league data the sports stats boards are built on. Four more endpoints, each
its own market_fetch source so one dead read is separable from a healthy one:
apis/v2/sports/{path}/standings (source=espn_standings) — the league
table. Surfaced: per team the group (conference/division), the published
seed or table rank, games played, W-L-T, table points, win percentage, games
behind, the ACTIVE STREAK string (“W6”), the last-ten record, point
differential, the league’s own record string, and the team badge. Also the
season the ROWS are from (seasonDisplayName), which is the only field naming
it — the payload’s TOP-LEVEL season names the season ESPN currently points at,
which in an offseason has not been played yet. Per-league quirks, all verified
live 2026-08-10 and all load-bearing: the NBA omits gamesPlayed (W+L is its
games played); the NBA and MLB publish a points stat that is NOT table points
(Detroit at 60-22 came back with 19), so only a points league may show it; MLB’s
playoffSeed is a playoff seed, not a record position; soccer publishes no
streak at all; entries do NOT arrive in table order. Omitted: per-team
splits beyond home/away, the division sub-tables, and clinch markers.apis/site/v3/sports/{path}/leaders (source=espn_leaders) — season stat
leaders, ten deep per category, with each player’s team. Surfaced: the
category name + label, the athlete, the team abbreviation, the numeric value and
ESPN’s own formatting of it, and requestedSeason (the season the numbers ARE).
Quirk: MLB puts the player’s WHOLE batting line in displayValue
(“137-426, 35 HR, 24 2B, …”) on every category, so a value carrying a comma is
re-rendered from the number. Omitted: the categories outside each league’s
curated set (ESPN returns 20 for MLB; nobody posts double plays).apis/site/v2/sports/{path}/scoreboard?dates=YYYYMMDD (source=espn_results)
— one calendar day of team games. Surfaced: both sides by homeAway (a team
game identifies its sides, unlike the 1v1 path), display names + abbreviations,
scores, the per-competitor winner flag, completion state, start time, badges.apis/site/v2/sports/{path}/summary?event={id} (source=espn_pregame_odds)
— a finished game’s PREGAME moneyline, from pickcenter. This is the only
free read that says how big an upset was: the day scoreboard drops odds once a
game goes final, while the summary keeps them. Surfaced: the first book
quoting BOTH sides, its name, and the two American prices. Omitted: the
spread/total, the win-probability series, the box score, and every other book —
one book’s pair is what an upset claim needs, and blending books would produce a
price no source published. Cost shape: one fetch PER GAME, so the boards
cache it for 30 days (a finished game’s pregame line can never change) and spend
a bounded per-build budget; the coverage they gate on is sports_board_lookup.market_fetch
ok-rates above, sharing the one integration=espn breaker (every read on this
host funnels through _fetch_url, so a bad prefix trips the same breaker the
scoreboard path uses).utils/box_office.py, the cinema desk’s box-office source)A scrape, not an API, so “the fields” are the chart page’s COLUMNS. BOM prints three
chart layouts and they do not share a column ORDER, which is why parse_chart reads
the <th> row and maps each column by NAME (#2266). A header-less snippet falls back
to the original positional heuristics.
| page | columns |
|---|---|
weekend /weekend/<YYYY>W<NN>/ |
Rank, LW, Release, Gross, %± LW, Theaters, Change, Average, Total Gross, Weeks, Distributor |
daily /date/<YYYY-MM-DD>/ |
TD, YD, Release, Daily, %± YD, %± LW, Theaters, Avg, To Date, Days, Distributor |
year /year/<YYYY>/ |
Rank, Release, Genre, Budget, Running Time, Gross, Theaters, Total Gross, Release Date, Distributor |
gross and grossToDate, but a mid-2026 release shows a LARGER year figure than its
to-date figure). So the year-to-date board prints only the Gross column – the one
BOM ranks the page by, which makes our order BOM’s order – and never the second one.sort=maxNumTheaters
per its own header link), not a current one, so a per-screen read belongs only to the
weekend page.movie_detail is the budget source),
the “New This Week” / “Estimated” flag columns, and the daily page’s second %± column.weeks comes only from a “Weeks” header. The daily page’s equivalent column is
DAYS, and the older positional parser silently read it into weeks. Nothing consumes
daily rows today, so this is a correction, not a regression.utils/tmdb.py, the cinema desk’s release + enrichment source)now_playing / discover_streaming / new_tv /
trending_movies / releases_between): id, title, release date, popularity, vote
average + count, overview, poster path.trending_movies order IS the ranking, and it is not the popularity field
(#2266): TMDB ranks that endpoint by its own trending score, so row 2 can carry a
lower popularity than row 3. A caller keeps the returned order; re-sorting by
popularity would publish a different ranking under TMDB’s name.releases_between filters on release_date with region + with_release_type=3,
not on primary_release_date – the primary date is a foreign film’s HOME-market
date, so the calendar would print a day the film does not open in the US on. It is
also why /movie/upcoming is unused: that list mixes in RE-RELEASES, which arrive
carrying their original release date (a 2016 date on a coming-soon board).movie_detail, one round trip with append_to_response=credits):
runtime, budget, worldwide-lifetime revenue, genres, tagline, overview, DIRECTOR and
top CAST, poster path.cinema_numbers._add_credits now enriches the theatrical
titles the roundup can name (bounded to 4 per slot against TMDB’s 600/min budget,
fail-open per title) and _release_line renders them in [brackets] – into the BLOB,
which is what cinema_desk_score grounds on. Supplying a field the prompt asks for is
the fix; asking without supplying is the bug./{movie,tv}/{id}/videos – trailer_url, the watch-link resolver’s
deterministic rung): site, key, type, official flag. It ranks an official Trailer,
then any Trailer, then an official Teaser, and takes YouTube keys only (the point is a
link X and Discord unfurl into a player, and TMDB’s other sites don’t embed the same
way). The rest of each video row is deliberately unread – name, size, language,
region, published_at – because the endpoint’s job here is one url, not a listing.
What this endpoint does NOT tell you is its own staleness, and that is the field
gap that mattered: it is community-maintained, so it holds nothing for a trailer that
dropped an hour ago and looks identical to a title that genuinely has no trailer.
Nothing in the response separates those two cases, so the resolver does not try – it
treats an empty answer as unknown and falls to a YouTube search (#trailer-lane).utils/omdb.py)The cinema desk’s critical scores. One endpoint (omdbapi.com/?t=<title>), read by the
scores story lane, the critics scoreboard board lane, and utils/reference_movie.py.
y=). A bare t=<title> is ambiguous and OMDb answers it
with the older famous match. t=Moana returns the 2016 animated film (96% RT, 81
Metacritic), not the 2026 live-action release (31% RT, no Metacritic). The critics
board shipped without the year on 2026-08-09 and printed the 2016 scores under the
2026 title; the take then repeated them and the card went to X. y= filters EXACTLY,
so a film that opened last December needs a retry against the previous year –
CinemaDesk._critic_scores is the one place that rule lives.Ratings array,
IMDb rating, title, year, rated, released, awards, box office.utils/rotten_tomatoes.py)The critics scoreboard’s RT column. There is no API — this reads RT’s public pages,
which is an owner decision (2026-08-11) and not an agreement. It sits outside their terms
of service and can break with no warning, so every path fails open and the rt_fetch
event carries the fill rate.
t=, not under i=<imdbID>, and not with tomatoes=true, which returns
every tomato* field as "N/A". So the board’s dash claimed “unreviewed” and meant
“our source lacks it”./search?search=<title> for the /m/<slug> candidates, then
the film page for media-scorecard-json (criticsScore, audienceScore) and the
JSON-LD Movie block (name, dateCreated).Moana returns
moana_2026, moana_2016, moana_2 and moana as the first four hits. The client
opens candidates in order and takes the first whose own release year is within one of
the chart’s year; nothing matching returns None. One year of slack is required, not
cosmetic — RT dates the 2016 Moana as 2017, by wide release.criticsScore.score), its review count and Certified Fresh
flag, the Popcornmeter (audienceScore.score), the film’s RT title, release year and
URL.rt_scores experiment, default STAGING, and STAGING means
what it means everywhere else: OFF never uses RT; STAGING uses it only when the BOARD
is also staged, so the card carrying these scores lands in #bot-logs for a mod to
check against the real site; PRODUCTION always uses it. The experiment routes nothing
by itself — cinema_boards decides delivery and defaults to PRODUCTION — so reading
“not OFF” as “use it” would have put unaudited scores straight into the room.moana_2 among the first hits for “Moana”. Titles are
compared normalized, which includes stripping RT’s own disambiguating suffix — it
titles a reused name The Odyssey (2026), and a strict compare dropped 3 of 8 real
matches on live data.utils/perplexity.py)utils/grok_search.py, #1390)Real-time X (Twitter) grounding via Grok’s server-side x_search Agent tool
(Responses API on api.x.ai), the sibling of Perplexity. Wired into: the on-demand
/ask search_x tool (alongside the ScrapeCreators social tools) AND a live X-pulse
grounding block on the scheduled trend/take surfaces — discourse (one source among
Perplexity/markets), music (a fresh-hit signal: “what tracks/artists are people
posting about on X”; she names the real track, never pastes an X link into the
links-only channel), recap (what X is saying about what the room’s on; same
not quiet gate as Perplexity, #880), the trending-clip reactions (clip-scoped:
what people are saying around the pool’s trending moments, so a “did you see this”
lands on WHY it’s buzzing – context for her take, never a “who said what” relay),
and the market surfaces market_drop + market_alert (the topic-scoped X CROWD
READ via the shared market_topic_pulse helper, so the take plays crowd-vs-reality
the bare %s can’t – an alert can even explain WHY a market just moved; sentiment
only, the compose still quotes numbers ONLY from the market blob). SELECTIVE by
design (pricier than Perplexity, so
only where live X sentiment is the point — not blanket-wired). Default model
grok-4.3 (GROK_SEARCH_MODEL-overridable): a live dry-run (2026-07) measured it
at ~3 x_search calls / ~8-11k input tokens / ~$0.03 per call with 7-9 real X
citations (~5-8× Perplexity), vs the frontier grok-4.5 which drives the agentic
loop ~14 tool calls / ~275k tokens / ~$0.3-1.2 for the SAME all-X citation
quality — a 10-20× cost delta with no quality gain, so the cheap model is the
default (max_tool_calls does NOT rein grok-4.5 in — model choice is the only cost
lever). Gated on GROK_API_KEY; fail-open. Emits grok_search.
url_citation annotations, appended as a linkable SOURCES block so
the model can cite/link the actual posts). Optional allowed_x_handles scopes to
named accounts; from_date/to_date bound the window.web_search results (X-only by design).xAI API reference (verified live against api.x.ai, 2026-07-15). The single
place we keep the confirmed xAI facts, so a future session (image failover #1389,
text backend #1391, video #1392) doesn’t re-derive them:
/v1/chat/completions (OpenAI-compat, function tools only) ·
/v1/responses (the Agent Tools API — server-side tools live HERE) · /v1/messages
(Anthropic-compat, returns 200) · /v1/images/generations + /v1/images/edits ·
/v1/models, /v1/language-models (pricing) · /v1/api-key (key metadata).
Base URLs: https://api.x.ai/v1 (OpenAI SDK), https://api.x.ai (Anthropic SDK).grok-4.5 ($2/$6 per 1M in/out, <200k; $4/$12 ≥200k) ·
grok-4.3 ($1.25/$2.50; $2.50/$5) · grok-4.20-{reasoning,non-reasoning,multi-agent}
($1.25/$2.50) · grok-build-0.1 ($1/$2, a code model) · grok-imagine-image /
-quality (image) · grok-imagine-video / -1.5 (Aurora video). Cached input
~10× cheaper. There is NO grok-4.1-fast/mini (a common web-doc error).types are x_search, web_search, code_interpreter,
collections_search. Request them in tools:[{"type":"x_search", ...}] on
/v1/responses. xAI features grok-4.5 as the tools model, but we default to
grok-4.3 — it drives the x_search loop far leaner (~3 vs ~14 tool calls) for the
same all-X citation quality at ~10-20× lower cost (dry-run-measured, see above). The
old chat-completions live_search tool + search_parameters are 410-retired —
Agent Tools is the only path.x_search/web_search/code_interpreter = $5 / 1,000 calls
($0.005/call); collections_search = $2.50/1k. Plus normal token cost on the
retrieved content. A real x_search ask ≈ $0.02–0.035 total (~3–5× a Perplexity
call, NOT 10–20×; most of it is the retrieved-post tokens, not the search fee)./v1/api-key returns key metadata only (name,
team, ACLs, blocked flags), no spend/balance. So xAI can’t be polled for a metered
budget like SGO/Odds-API; track cost via usage.cost_in_usd_ticks on each
Responses call or the xAI console.utils/x_fetch.py)video_fetch.exceeds_x_video_limit, #video-trim).utils/twitterio.py, the content curator’s X source, #curator)external_id, media THUMBNAIL URLs
(photo + video still, media_urls), a native-video flag (has_video, from the
media type/video_info — so a wire desk can re-share the clip via X’s own
.../status/<id>/video/1 deep-link, which X embeds as just the video, #video-upload),
author handle, engagement counts (like/retweet/reply/view), possibly_sensitive,
and the reposted author’s handle (reposted_handle, for repost-weighted adjacency
discovery, #1251); the @-mention display names (entities.user_mentions →
{handle: name}, JSON-encoded into extra[MENTIONS_KEY], read back by
source_providers.post_mentions, #2010); per followings crawl — the account’s
followed handles + follower counts (fetch_followings, for the follow-graph half of
adjacency discovery, #1232 Phase 2).source_providers.plain_text, #2246).
X returns & as the entity & in text (and in a mention’s display name), and
nothing downstream decoded it. Measured on live wires: 4 of 60 recent posts across
@hiphopnumbers, @popbase and @chartdata carried one. It did not stay cosmetic — a raw
entity could REWRITE an artist’s name. The music newsroom shipped a milestone card
titled “We Still Don’t Trust You - Future & Metro Boomin”: music_news.wire_spelling
re-spells a classified artist the way the WIRE spells it, it strips punctuation per
token, and _TOKEN_STRIP contains ; — so “Future & Metro Boomin” offered the
candidate “Future & Metro Boomin”, which cleared corrected_name’s near-identical
bar (ratio 0.93, same first letter) and REPLACED the name the classifier had read
correctly. The Axiom trail shows the hop exactly: music_news_recover logged the right
name at 23:10:10 and every later event that slot logged the mangled one. So the decode
runs once, at ingest, where the classifier prompt, the compose, the art matchers, the
card title and the dedup key all read the same plain text. The same decode covers the
second X provider (scrapecreators.sc_tweet_to_post’s legacy.full_text, the same
payload through another vendor) and Reddit (reddit.reddit_search_result’s title +
selftext, escaped by the same convention). The general rule: normalize an upstream text
field at the ONE boundary that builds the shape, never at each reader — a reader that
matches on names can otherwise promote an encoding artifact into data. The class stays
open by construction: this fix is per-provider, so a source that starts escaping a
field would leak again. Deliberately NOT changed, on measurement — kworb’s Spotify chart
& and no entities (0 &, 4 raw & in link text, live
2026-08-09; its YouTube parser already unescapes), and TikTok desc + Instagram captions
return raw text. #2247 tracks the output-side detector that would close the class./video/1
deep-link rather than re-host the bytes, so the mp4 URL is never needed); the raw
full-metadata dump..@whamcbfw4's "Dead Fresh" enters the Top 20), so dropping the
entities left the newsroom unable to tell who a story was about — it shipped a card
reading “Dead Fresh - whamcbfw4” about Lil Baby, and “IS IT LOVE - Tylla” about Tyla.
X already returns the display name for every mention, so it costs no extra call. It
travels as EVIDENCE beside the handle, never as a substitution: measured across the
trusted wires, the display name is right often enough to matter (@whamcbfw4 = “Lil
Baby”, @Tyllaaaaaaa = “Tyla”, @ellalangleymsic = “Ella Langley”) and wrong often
enough that substituting it would break cases that already work (@Drake = “Drizzy”,
@tylerthecreator = “T”, @Latto = “BIG MAMA”, @MacMiller = “Mac”). The classifier gets
both forms and is told to trust neither alone.AsyncRateLimiter (proactive throttling under the free tier’s 1 req/5s) +
CircuitBreaker + retry_async, results DurableCached; @instrumented as
curator_fetch. Watched by the health cog (utils/health.py WATCHES, ops-only
fix-order on a crater) + the ops-monitor integration-health dashboard + a
/debug/integrations probe.GET /oapi/my/info
(get_usage, quota-EXEMPT + NOT breaker/rate-limiter guarded, so it answers even
while the paid reads fail) → recharge_credits + total_bonus_credits summed to
the remaining BALANCE (parse_twitterio_usage’s credits metric, limit=None).
With no fixed cap the generic pct-based quota_low can’t fire, so the ops-monitor
flags a dedicated balance-FLOOR finding (twitterio:budget, high) when the balance
drops under TWITTERIO_CREDITS_LOW — before it 401s the curator dark (“Credits is
not enough”), the same prepaid-wallet model as the Odds API budget finding.utils/x_poster.py, the @tootsiesbar crosspost target)POST /2/tweets (OAuth 1.0a user-context)
and consumes only the returned data.id (the created tweet id), which rides
telemetry (tweet_posted) and never reaches the model’s context.AsyncRateLimiter (courtesy
pacing under X’s write cap) + CircuitBreaker (integration=x_poster) +
retry_http; every write leg (post_tweet/repost/upload_media) is
@instrumented as tweet_posted; fail-open (any miss just skips the crosspost,
the room post is unaffected). Provisioning-gated on the 4 X_* OAuth env vars;
runtime behavior is the per-guild x_crosspost experiment.tweet_posted into
integration-health as x_poster (benign unprovisioned/empty_text excluded) and
flags integration_unhealthy on a sustained fail rate; a sustained crater also
trips the circuit_breaker (integration=x_poster). Latency p99 is gated by the
tweet_posted ceiling. No write-quota poll (deliberate): the drops volume is
far under the Free tier’s ~17 posts/day, and X exposes no cheap standalone
write-quota endpoint (only per-response rate headers), so a cap wall is unlikely and
would surface via the fail-rate rollup anyway.utils/scrapecreators.py, utils/social_search.py, #1272/#1273)One SCRAPECREATORS_API_KEY, one guarded REST client, TWO roles:
BackupXProvider over /v1/twitter/user-tweets,
routed behind twitterio.XProvider’s breaker via FailoverProvider (only while
the primary is degraded). Maps a tweet -> SourcePost (text, permalink,
external_id, media URLs, author handle, like/reply/retweet/view counts,
possibly_sensitive, reposted_handle). No X follow-graph endpoint, so
fetch_followings returns [] (adjacency degrades to the repost path).TikTokProvider): a SourceProvider
(platform tiktok) on the shared client for the curator’s seeding: fetch_recent
(/v3/tiktok/profile/videos → a seed’s own videos → SourcePost via the pure
tt_video_to_post: aweme_id, clean url permalink [tracking stripped], author
handle, cover image for has_media, digg/comment/share/play counts), and
fetch_followings (/v1/tiktok/user/following → clean (unique_id, follower_count)
handles for adjacency). TikTok’s “repost” analog is the COLLAB: a video’s
collab_info.collaborators[].user_info.unique_id names the co-creator directly
(no resolve call), surfaced as SourcePost.endorsed_handles so a seed co-creating
with an account is a strong adjacency endorsement (weighted like an X repost).
TikTok has no readable repost feed, so is_repost is always False. Bare caption
text_extra @mentions are id-only (would need a paid resolve) and deliberately
NOT used — collabs are the free, high-signal endorsement. Emits curator_fetch
provider=scrapecreators (phase=user|followings).InstagramProvider): a SourceProvider
(platform instagram) on the shared client, the third curator seed source beside
X + TikTok: fetch_recent (/v1/instagram/user/posts → a seed’s own posts/reels →
SourcePost via the pure ig_post_to_post). Surfaced: the SHORTCODE as
external_id (so a channel-URL dedup matches curator.parse_external_id, not the
numeric id), the url permalink, owner username, caption (edge_media_to_caption),
display_url for has_media + the vision judge, like/comment counts,
video_view_count. IG’s “repost”/collab analog is coauthor_producers (the
co-author usernames, right in the response) → SourcePost.endorsed_handles, the
same adjacency endorsement as a TikTok collab. Omitted (deliberate): IG exposes
no follow-graph or repost endpoint (/v1/instagram/user/following 404s), so
fetch_followings returns [] and adjacency leans entirely on the coauthor collab
(like TikTok’s no-repost-feed); is_repost is always False. The curator rewrites
the posted IG permalink to kkinstagram.com (to_fixup_link) so Discord unfurls
the media — native instagram.com links unfurl poorly, the reason the embed-fixer
mirrors exist (the mirror is swappable; confirm in the staging audition). Emits
curator_fetch provider=scrapecreators (phase=user).utils/video_fetch.py): TikTok +
Instagram video transcripts route through ScrapeCreators (yt-dlp resolves those
two hosts poorly from a datacenter IP). fetch_video tries
_fetch_scrapecreators FIRST for is_tiktok_url/is_instagram_url clips —
TikTok /v2/tiktok/video?get_transcript=true (WEBVTT transcript flattened by
the shared flatten_vtt + rich aweme_detail metadata: desc→title, author,
play/like/comment counts, duration ms→s via sc_tiktok_meta), Instagram
/v2/instagram/media/transcript (plain AI text). A transcript-less miss falls
through to the existing yt-dlp path (no regression). Emits video_transcribe
source=scrapecreators.utils/comments.py): the read_comments
/ask tool over the per-platform comments endpoints (/v1/tiktok/video/comments,
/v2/instagram/post/comments, /v1/youtube/video/comments,
/v1/reddit/post/comments) — plus X/Twitter REPLIES via twitterapi.io
(/twitter/tweet/replies, XProvider.fetch_tweet_replies), since ScrapeCreators
has no tweet-replies endpoint (confirmed against the catalog) and the reply
thread IS the audience reaction on X. Surfaced: each comment/reply’s text +
like count (normalized to one Comment(text, likes) shape, ranked loudest-first;
the X mapper reads likeCount). Omitted (deliberate): author handles (the
crowd’s WORDS are the signal, not who said them — avoids per-user attribution),
reply threads/timestamps (top-level reactions only), and any permalink (Reddit
stays link-free, owner steer). Emits comment_read.utils/social_profile.py): the
social_profile /ask tool over the per-platform profile endpoints
(/v1/tiktok/profile, /v1/instagram/profile, /v1/youtube/channel,
/v1/twitter/profile). Surfaced: handle, display name, verified badge,
follower count, following count, post/video count, bio (200 chars), account-age
string (normalized to one SocialProfile shape). Omitted (deliberate): avatar/
banner image URLs (not answer-valuable for a “how many followers” question), the
full follower/following lists (a stats read, not an enumeration), and platform-
specific extras (TikTok heartCount/room, IG business category, YouTube view
total) — the reach signal is the follower + post count, and the rest is noise for
the model’s use. The structure complement to social_search (which finds posts);
the model relays the real numbers, never invents a follower count. Emits
social_lookup.utils/reddit.py): the
search_reddit /ask tool over /v1/reddit/search (+ /v1/reddit/subreddit/search
when scoped). Surfaced: post title, subreddit, selftext (the discussion text,
280 chars), score + comment count (the traction signal). Omitted (deliberate,
owner steer): the permalink / any Reddit URL — Reddit is an INPUT source the
model synthesizes in her own voice, never a link she pastes (“not reddit-nerdy”);
RedditPost has no permalink field, and a unit test asserts the formatter never
emits a reddit link. Omitted (policy): NSFW posts (over_18 dropped) + author
handles (not answer-valuable, avoids per-user attribution). Ranked by score;
emits reddit_search.utils/social_search.py): the model-facing
search_socials (keyword: TikTok /v1/tiktok/search/top + YouTube
/v1/youtube/search + Instagram reels /v2/instagram/reels/search),
discover_trending (TikTok /v1/tiktok/get-trending-feed region-scoped +
YouTube /v1/youtube/shorts/trending), and search_hashtag (TikTok
/v1/tiktok/search/hashtag + YouTube /v1/youtube/search/hashtag). All map to
one SocialResult shape.SocialResult): platform, caption/title, permalink (the
answer payload — Discord unfurls it), author @handle/channel, verified badge
(TikTok author.custom_verify / IG owner.is_verified), a compact engagement
stat (TikTok digg/play; YouTube views; IG like/view), duration (TikTok
video.duration ms, IG video_duration s, YouTube lengthSeconds/durationMs/
formatted-string fallback), published date (TikTok create_time normalized from
unix-int-on-trending vs ISO-on-search), and a blended engagement magnitude for
ranking.music/clips_music_attribution_info — niche for topic discovery, a
transcript/read_media concern); author follower counts + full profile blobs (a
per-hit credibility number would bloat each line; verified is the compact signal
kept); thumbnails/cover art (Discord’s unfurl supplies the preview); TikTok Shop /
ads / age-gender / audience-demographics endpoints (out of scope for content
discovery). The dead /v1/tiktok/hashtags/popular catalog is deliberately NOT
wired (“TikTok took this page down” upstream — we SEARCH hashtags, not list them).AsyncRateLimiter + CircuitBreaker +
retry_http, @instrumented. Account endpoints answer content-type: text/plain
so the client reads resp.json(content_type=None). Fail-open per platform (a
source miss contributes nothing). Watched by the health cog + ops-monitor
integration-health + a /debug/integrations probe (all #1273).GET /v1/account/credit-balance
(get_usage, quota-exempt) → parse_scrapecreators_usage’s credits metric
(limit=None); the ops-monitor flags a balance-FLOOR finding
(scrapecreators:budget, high) under SCRAPECREATORS_CREDITS_LOW. Per-day spend
is ALSO surfaced (#1272): get_daily_usage() reads /v1/account/get-daily-usage-count
(a {usage_date, total_credits, request_count} list) and collect_usage folds the
most-recent day into an INFORMATIONAL day_credits burn-rate metric (period=day,
limit=None -> rendered in the quota table + /debug/usage, never flagged) so the
spend RATE is watchable alongside the balance floor. Emits usage_fetch
(source=scrapecreators_daily).utils/video_fetch.py)is_live flag (capped by duration regardless),
format/ext, raw full-metadata dump.utils/link_enrich.py)utils/gifs.py)rating (applied as a server-side pg-13
query filter); trending flag; creator username (privacy).utils/image_gen.py)revised_prompt (the safety-rewritten prompt,
to the caller — flagged-only in telemetry since it’s user-derived).created timestamp; n>1 (single image per prompt).utils/xai_image.py, #1388/#1389)The FAILOVER image backend: ImageClient retries a generate/edit on Grok when
OpenAI either safety-rejects the prompt (a content refusal on real named public
figures — permanent for that prompt) or is unavailable (429/5xx/timeout/
connection drop — an availability failure Grok covers on a separate quota),
mirroring the text provider-failover (#973). It does NOT fail over on a permanent
4xx (malformed request / bad key) or a decode miss. Gated on GROK_API_KEY;
fail-open. image_generated events carry provider="xai" (vs "openai") so each
backend’s health is separable; the failover emits provider_fallback
(requested=openai, fell_back_to=xai, reason=safety_reject|transient).
data[0].b64_json, or a hosted
data[0].url downloaded on the fallback path). Aspect maps to xAI’s
aspect_ratio (square→1:1, landscape→16:9, portrait→9:16). Edit passes the
source as a base64 data-URI in JSON (xAI is JSON-only; OpenAI’s edit is multipart).revised_prompt (xAI doesn’t return one); created;
n>1 (single image per prompt); multi-image edit (up to 3 refs supported upstream,
we send 1). Note the bytes come back JPEG, not PNG — fine for Discord delivery.utils/embeddings.py)index, echoed model id, usage tokens (not
answer-valuable; the vector is the product).utils/stt.py)utils/tts.py)GET /v1/user/subscription (get_subscription, a FREE
account read that doesn’t consume the character quota it reports) → the monthly
character cap (character_count / character_limit →
parse_elevenlabs_usage’s month_characters metric, the SGO-entities analog
for the voice budget) + tier + next_character_count_reset_unix. Polled →
quota events → quota_low ahead of the wall. Caveat: the read may need
the user_read key scope (the wall we hit on /v1/user with the STT key); a
scoped key gets a 401 and the report lands ok=False → quota_unreadable, so
the blind spot is visible, not silent.utils/gifs.py, #753)X-RateLimit-Limit-Day / -Remaining-Day) ride on every search response and
are captured PASSIVELY (last_requests_limit/-remaining, like Highlightly) →
giphy_usage_report’s day_searches metric. The daily window accumulates, so
it flags quota_low ahead of the wall (a beta key is ~100/day). The hourly
window is dropped (resets too fast to be a budget). Headers read
case-insensitively (Giphy’s casing is inconsistent).utils/embeddings.py + utils/image_gen.py, #753)/v1/organization/costs
+usage), which the owner declined (rate-headers-only), and the $ cap isn’t
machine-readable. So we capture the rolling per-minute request rate headers
(x-ratelimit-limit/remaining-requests) PASSIVELY off embeddings responses →
openai_usage_report’s minute_requests metric (period="minute"). This is
INFORMATIONAL: the ops-monitor renders it but NEVER flags quota_low on it
(a per-minute window resets constantly — it can’t fill ahead of time; the real
hard signal is the 429, already in integration-health). Our embedding workload
sits near 0% of the RPM cap, so it just makes a runaway burst visible.402 on exhaustion); only
X-RateLimit-Remaining (no limit header → no pct) rides on responses./debug/integrations), not polled — there’s nothing pollable.utils/github.py, check_fixes)state_reason (completed/not_planned/reopened), created/updated/closed relative
ages, comment count, html_url; labels via the query.GET /rate_limit (get_rate_limit, FREE — calling it
doesn’t count against the limit) → the REST core hourly budget
(resources.core.used/limit → parse_github_rate_limit’s hour_core_requests
metric; the real limit is read live — 5,000/hr for a PAT, 1,000/hr for an
Actions GITHUB_TOKEN — never hardcoded). The bucket the /order
issue-filing + reconciliation path spends against, so a runaway burn flags
quota_low. Search/graphql buckets omitted (the bot’s client is REST-core only).utils/railway.py, check_deploys)These are the only answer-valuable fields a user could plausibly want that we still drop. Each is a conscious low-priority deferral, recorded here so it’s explicit:
| Integration | Field | Why deferred |
|---|---|---|
| Deezer | track-level release date / explicit | Not in Deezer’s track-search response shape (an upstream limitation; the album catalog path carries the date). |
(API-Sports venue + referee, previously listed here, are now surfaced via GAME CONTEXT; only the Deezer upstream limitation remains.)
Everything else is either surfaced or an intentional omission listed above.