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

Upstream integration field coverage

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.


Music / reference

Industry news feeds (utils/industry_feeds.py, #reporting-oversight)

The trade press read directly via public RSS (no key), so a first-party number is in hand rather than chased through a lagging web-confirm. Five sources in FEEDS: Pollstar news, IQ Magazine, Music Business Worldwide, Billboard music-news, and Billboard Chart Beat (added 2026-09-28: Billboard’s own chart announcements, which the music-news feed does not carry). One namespace-driven parser. The cache holds a feed 90 seconds, because the newsroom’s watcher lane reads every feed every 3 minutes (the feed_watch source of its EventPoster lane since #3834; see docs/ARCHITECTURE.md). The newsroom’s X watcher (#3824) reads the number authorities on X with ONE twitterapi.io advanced_search call every 3 minutes ((from:a OR from:b ...) -filter:replies), the same tweet_to_post mapping as fetch_recent, so it surfaces the same fields.

RIAA Gold & Platinum (utils/riaa.py, #2839)

RIAA’s own certification database, read directly (no key). Four views: the recent feed (every award newest first, 30 rows a page, paged by the site’s own “show more” JSON call), one award’s timeline (its full level history with dates), the top-tallies list (the 100 highest-certified albums, certified units in millions, 4 pages) and the awards-by-artist search (one row per credited act: certified units, Gold / Platinum / Multi-Platinum / Diamond counts, SOLO titles only per the site’s own note). The site holds no singles tally list (probed 2026-09-02: every sub-tab value answers the album list).

Pollstar charts (utils/pollstar_charts.py, #reporting-oversight)

Pollstar’s public chart JSON (data.pollstar.com, no key), the live-music numbers Billboard does not publish. Anon free-tier only — no auth is ever sent, and only the preview rows the endpoint returns are taken (top ~10 concert / ~50 Mediabase). One parser for every chart in CHARTS.

Billboard (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.

Chart-history store (utils/chart_history.py, cogs/chart_history.py, 2026-09-20)

Not a new upstream: the DURABLE copy of three integrations below plus Nora Music’s X posts, written to Postgres so the history is read from the store, never re-fetched.

HITS Daily Double (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).

kworb YouTube video charts (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.)

kworb HTTP response headers (utils/kworb.py, #3191)

Every kworb page serves ETag and Last-Modified, and until 2026-09-12 the client read neither. They are not chart content, so they reach no model and no card; they are read for two jobs.

kworb Spotify track page (utils/kworb.py, 2026-09-19)

The per-track chart-history page (spotify/track/<id>.html), read by track_streams. Since 2026-09 it renders TWO sections in the same table shape: <div class="weekly"> (one row per chart week) and then <div class="daily"> (one row per day); each has a Total and a Peak row and a per-country column set.

kworb Spotify Global daily chart totals (utils/kworb.py, #4491)

The all-time totals page (spotify/country/global_daily_totals.html), read by global_daily_totals. It has one row per song that ever charted on the Global daily chart (about 12,000 rows): Artist - Title, Days, T10, Pk, (x?), PkStreams, Total. The qualifier check (utils/qualifier_check._chart_history) reads it for a Spotify Global “first” claim, so the judge can name an earlier Global No. 1 (“greedy”, 34 days) that web research states wrongly.

kworb Spotify daily chart page date (utils/kworb.py, research round 4, 2026-09-21)

The Spotify daily chart pages (spotify/country/us_daily.html, global_daily.html) name the chart DATE in their title (Spotify Daily Chart - United States - 2026/09/19): the day the streams are FOR. kworb publishes a day one or two days after it (read 2026-09-21, the US page showed 2026/09/19), so the read’s date is not the streams’ date.

kworb Spotify artist / listeners rankings (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).

kworb US weekly YouTube chart (utils/kworb.py, owner ask 2026-08-31)

The per-country YouTube chart for the US (youtube/insights/us.html), behind the us_video_top + us_video_new boards. A DIFFERENT page and metric from the most-viewed video charts above: those are worldwide, daily, and by VIEWS; this is US, weekly, and by STREAMS. It is the only YouTube page we read that says whether a title is new, and since 2026-09-14 it is the only YouTube page that feeds a BOARD at all. The page size flips: kworb has served it at 100 rows and at 20, and the boards need it whole, so kworb._US_VIDEO_CHART_SIZES holds both measured sizes and a read matching neither darkens both boards with a chart_fetch shape_change.

kworb YouTube artist ranking (utils/kworb.py, owner ask 2026-08-30)

The all-time most-viewed YOUTUBE ARTISTS leaderboard (youtube/archive.html — kworb files it under “archive”, but its own nav calls it Artists). ~1,732 acts. It backs the market card’s ranking strip on a YouTube market, so the scale is read on the same platform the card’s number is on. No consumer since 2026-09-29: the artist standing strip it backed was removed from every card (owner steer). The client read and parser stay, but nothing calls them.

kworb per-artist songs page (utils/kworb.py, album card, owner ask 2026-08-16)

The per-artist songs page (spotify/artist/<id>_songs.html, parse_artist_songs): every tracked song of one artist with its cumulative total + latest daily. It backs the fresh-drop album-streams card (chart_boards.album_streams_board) — match an album’s iTunes tracklist to these rows and each track’s daily comes free.

kworb per-artist albums page (utils/kworb.py, owner ask 2026-08-28)

The per-artist ALBUMS page (spotify/artist/<id>_albums.html, parse_artist_albums): every tracked ALBUM of one artist with its cumulative total + latest daily – the same shape as the songs page above, one level up. It is the PREFERRED source for an album’s lifetime streams (utils/stream_totals.py), because it is Spotify’s own published per-release figure: the older tracklist sum only approximates it (a track’s own total carries the plays it took as a single, so summing over-counts a release that has one), and iTunes does not return every act’s albums at all, so for some acts the sum could never resolve (measured 2026-08-28: an iTunes album search for Bad Bunny returns his singles and features and none of his albums).

Two feeds kworb publishes that the client did not read, added 2026-09-06 after a survey of every path the site exposes.

Genius (utils/genius.py)

KTT2 forum (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.

Apple Music / iTunes (utils/apple_music.py)

Deezer (utils/deezer.py)

Deezer — new-releases board — REMOVED (owner steer 2026-09-05, “never use Deezer”)

Apple Music Top Albums RSS (utils/apple_releases.py) — the releases board’s source

iTunes Store Top Albums RSS (utils/itunes_charts.py, #3469) — the iTunes ALBUMS chart

iTunes Store GENRE charts (utils/itunes_charts.py GENRE_CHARTS, #3799)

Apple Music — artist photo (utils/artist_photo.py, #5054)

Spotify Web API — artist profile photo (utils/spotify.py:artist_image, #5054)

Spotify Web API — new releases (utils/spotify.py) — DORMANT (#2447)

Songstats (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).

MusicBrainz (utils/musicbrainz.py)

Wikidata (utils/wikidata.py)

Wikipedia chart parser (utils/chart_data.py)

Wikipedia reader (utils/reference.py)

Steam store art (utils/steam_art.py, #2741)

The catalog rung for a VIDEO GAME subject, the way TMDB is film’s and Deezer is music’s. Public store endpoints, no key. It exists because the Wikipedia lead-image rung structurally cannot serve a game: pageimages EXCLUDES non-free files, and a game article leads with its copyrighted box art (measured 2026-08-31 — every game tested returns no image, while free-licensed subjects return one).


Sports

SGO — SportsGameOdds (utils/markets.py, utils/sportsdata/board.py)

Polymarket (utils/markets.py)

Kalshi (utils/markets.py)

The Odds API (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):

API-Sports (utils/api_sports.py)

Highlightly (utils/highlightly.py)

Sleeper fantasy (utils/fantasy/sleeper.py, #4853)

The weekly fantasy recap’s source for the Sleeper league. Public read API, no key. No model reads it: the recap is deterministic text, so “surfaced” here means “in the post”.

ESPN fantasy (utils/fantasy/espn.py, #4853)

Fantasy bets (cogs/bookie_fantasy.py, basketball only): the same league read prices the /bet Fantasy board from every decided game’s totalPoints, and picks the next period from status.currentMatchupPeriod plus each side’s pointsByScoringPeriod (present once a scoring day of the period has started; measured 2026-11-10 on the live league: period 1 = days 1-6, periods 2 and 3 = 7 days, period 4 day 21 started). An ESPN football league offers no bets (no NFL week calendar on this read).

The weekly fantasy recap’s source for the ESPN league. The owner’s league is FANTASY BASKETBALL: league 330737366, fba, shown as “ESPN Paid League” (owner steer 2026-10-31; ESPN’s own settings.name is “WAVES PAID LEAGUE” and is never shown: every surface takes the config display name),, season 2027 (a basketball season is named by its end year). It is in the default config (espn/fba:330737366:ESPN Paid League); the client also reads football (ffl). The game code picks the URL, .../apis/v3/games/<ffl|fba>/seasons/<year>/.... A private league needs the espn_s2 and SWID cookies, which the owner sets as the Railway variables ESPN_FANTASY_S2 and ESPN_FANTASY_SWID. Without them the league is skipped and a log line names the missing variables. ESPN_FANTASY_LEAGUE_ID (+ _LEAGUE_NAME, _SPORT) still replaces the default entry. Basketball: a matchup period spans several daily scoring periods (about Monday to Sunday), so the recap is the last COMPLETED matchup period (every game has a winner), labelled “matchup”, with points to 1 decimal; the top-scorer line is OMITTED for it (the boxscore read covers one scoring period, so one day’s points would be a partial number; the skip keys on the sport, because the basketball matchupPeriods map lists the matchup id, not the days: “1” -> [1]). A period posts only once every game in it is decided: winner is HOME / AWAY / UNDECIDED / TIE, and a TIE is decided. playoffTierType is absent on regular-season games and reads as NONE. The league has 8 active teams, 4 games per period. The standings (W-L-T, points for) are rebuilt from the schedule over the decided regular-season games through the recapped period (espn.records_through), NOT read from record.overall: ESPN updates that LIVE during the matchup in progress (real data, 2026-10-31: PF 2,185.0 = 1,337.0 for matchup 1 + 848.0 of matchup 2 so far), so a Tuesday post would carry Monday’s partial points under “after matchup N”. This applies to football too. The live read is pending the cookies on Railway; the basketball fixture (espn_fantasy_fba_league.json) is shaped on the REAL response (the coordinator read the owner’s snapshot on 2026-10-31 and relayed its shape and the period-1 scores, no cookies); the football one (espn_fantasy_league.json) on the public v3 shape.

ESPN scoreboard (utils/espn.py, #1113)

Endpoints covered:

The STATS reads (#2268). The same host publishes, free and keyless, the team-league data the sports stats boards are built on. Three more endpoints, each its own market_fetch source so one dead read is separable from a healthy one:


Media / AI / enrichment

Box Office Mojo (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

TMDB (utils/tmdb.py, the cinema desk’s release + enrichment source)

OMDb (utils/omdb.py)

Daily cap (#4926): "Error": "Request limit reached!" (on a 401, or a 200 with "Response": "False"; OMDb publishes no error page). Named limit_reached, never cached, and latched until 00:00 UTC (omdb, day mode). Critic scores and posters stay absent meanwhile; an “Invalid API key!” 401 is not latched.

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.

Rotten Tomatoes (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.

Perplexity (utils/perplexity.py)

TypeSafe Jev (utils/typesafe_client.py, #4310)

Jev answers typed questions about a state; it returns numbers, not prose. The “answer-valuable fields” are the ones a caller decides on.

response field surfaced in JevAnswers note
answers[k].noul (P(true)) nouls[k]  
answers[k].choice + .confidence choices[k]  
answers[k].score + .confidence scores[k] the probability-weighted level
model (versioned id that answered) model also on the event
usage.input_tokens input_tokens also on the event (the only billed tokens)
answers[k].probabilities (choice / score) omitted No caller reads the per-label distribution yet; confidence carries the decision signal. Add it when a pilot needs the runner-up.
answers[k].legend (score) omitted It echoes the caller’s own rubric back.
usage.output_tokens omitted Output tokens are free and say nothing about cost.
box answers omitted The Box type and images need TypeSafe’s Danger-Zone-Images header, which TypeSafe says is not ready for production. The client sends text/JSON state only.

xAI Grok x_search (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). Two lanes are carved OUT of that wiring, all structurally (withhold the input, do not only ban the output). The rule: a card that REPORTS does not get the crowd read (owner steer, 2026-08-31). The crowd read serves the crowd-vs-reality angle, and that angle needs a live question to be about. Three kinds of card have none:

  1. A FORECAST of a number (#2745) – market_alert’s forecast_move plus its closing range / ladder ending_soon branches, AND market_drop’s forecast / forecast_rank projection angles. The post IS a projected number. This block is what turned a clean figure into “…forecast at ~7.1M, well below the streaming dominance the timeline keeps citing” (live 2026-08-31).
  2. A settled or locked RESULT – market_alert’s settled_out report, and market_drop’s >= 95% report lock. The question is answered.
  3. A confirmed LEADER – market_alert’s closing rank-leader read, and market_drop’s dominant frontrunner. The card states who is out front; the crowd does not get a vote on that.

The gate flag (forecast_lane, which drives the self-gate’s opinion-clause hard fail) stays scoped to (1) alone, because its wording is forecast-specific. Which INPUT the compose gets and which GATE rubric applies are two separate questions. The withheld input is the WHOLE fix, and that is measured, not assumed. An added “do not editorialize” instruction shipped alongside it at first and was then cut: on the real blob, n=8 per arm, crowd-read-out scored 0/8 opinion tails with the rule and 0/8 without, while crowd-read-in scored 3/8 without. Either lever alone fixes it, so the instruction was pure prompt cost – and the enumerated-ban shape #1140 warns about. tests/test_market_alert.py ratchets it: a test now FAILS if the ban is added back.

Every carve-out is known BEFORE the fetch, so the Grok call (and Perplexity, on the market_drop path) is skipped outright, not paid for and discarded. 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). grok-4.6 was measured the same way (2026-09, #model-upgrade) and stays OFF the default for the same reason: on the same two grounding queries it ran 4-6× slower (48-69s vs 12s), read 1.5-4× the input tokens (12.0k/42.2k vs 8.0k/10.2k), and returned the SAME or FEWER citations (5 vs 7 on one, 4 vs 4 on the other) — on top of a rate 1.6×/2.4× above 4.3. Citation yield is the only thing a grounding block is bought for, so the newer model is a straight loss here. Gated on GROK_API_KEY; fail-open. Emits grok_search.

The real cost is about $0.17 per call, not $0.03 (#4720). The $0.03 above counted only tokens and x_search calls. xAI also charges $5 per 1,000 X posts and $10 per 1,000 X users that a search pulls into the context. On the bill for 2026-09-24 to 2026-10-24 those two fees were $1,093 of $1,214; tokens were about $121. A live probe on 2026-10-24 (one grok-4.3 call: 4 x_search calls, 29 posts, 3 users) cost $0.19. Every response states its billed cost in usage.cost_in_usd_ticks (10^10 ticks = $1, tokens and tool fees included), and the posts and users in usage.server_side_tool_usage_details.x_posts_fetched / x_users_fetched. The grok_search event records all three (detail.cost_usd, detail.x_posts_fetched, detail.x_users_fetched).

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:

twitterapi.io search as the Grok fallback (utils/x_search_judge.py, 2026-11-03)

When Grok is out, GrokSearchClient answers a request from a twitterapi.io advanced_search (through the shared XProvider.search, so its limiter, breaker and page cache apply) plus a Haiku answer step. The search runs queryType=Top and Latest, with until_time: = NOW and, for the short-window kinds, since_time:.

X / fxtwitter (utils/x_fetch.py)

twitterapi.io (utils/twitterio.py, the content curator’s X source, #curator)

X API v2 write (utils/x_poster.py, the @tootsiesbar crosspost target)

ScrapeCreators (utils/scrapecreators.py, utils/social_search.py, #1272/#1273)

Out of credit (#4926): HTTP 402 (“You ran out of credits and need to buy more”, scrapecreators.com tutorials). Named credits_exhausted on every caller’s event and latched (scrapecreators); the curator’s X backup, TikTok and Instagram reads return nothing meanwhile, and /stickers says the scans are paused.

One SCRAPECREATORS_API_KEY, one guarded REST client, TWO roles:

yt-dlp video (utils/video_fetch.py)

EnsembleData (utils/tiktok_stickers.py:check_comment, 2026-10-30)

One ENSEMBLEDATA_TOKEN (a token query param). /stickers uses ONE endpoint, /apis/tt/post/comments (aweme_id, cursor; 30 comments and 1 unit per call; the next cursor is data.nextCursor), to check a linked comment before the ScrapeCreators search. It reads the app’s comment list, so it sees comments the logged-out list hides (measured 2026-10-30: the 11.6K-like top comment first, and 44 sticker comments in the first 89). Fields read: cid, content_type (16 and 17 are stickers) and the inline reply_comment preview. Omitted on purpose: commenter identity and text (the check only needs “is it there, does it carry a sticker”). NOT available: the sticker image. The response has no cmt_sticker_struct; the text carries only a “[Sticker]” prefix. So the image still comes from ScrapeCreators. Not used yet: /tt/post/comments-replies (a reply walk on the app list). Probe: /apis/customer/get-used-units (0 units). Health: the ops-monitor ensembledata key.

Giphy (utils/gifs.py)

OpenAI image (utils/image_gen.py)

OpenAI image: the arcade fighter sheet (utils/arcade/fighter_models.py:SheetClient, #4012)

Out of credit (#4926): the OpenAI billing refusal is reason=quota; Grok’s credit block (HTTP 403 permission-denied, “used all available credits or reached its monthly spending limit”, seen in Axiom 2026-10-31) is reason=xai_quota. Neither is retried, each has its own latch, and the member row is deferred with no try counted. A second, separate client for one surface. It calls /v1/images/edits with the member’s avatar (512 px PNG) as the reference image and SHEET_PROMPT (likeness first: the same face, hair, skin tone, glasses, facial hair, accessories, colors and style; a non-human avatar stays in its own style), at 1024x1536 and quality medium (FIGHTER_IMAGE_MODEL / FIGHTER_IMAGE_QUALITY override). It has its own breaker, limiter and retry (a 429/5xx or a dropped connection; a timeout is NOT retried, because the image may be drawn and billed). Grok fallback (owner, 2026-10-14: “whatever fails with openai, just retry with grok at highest level possible”): when the OpenAI call fails for ANY reason (a safety refusal, a 429/5xx after its retries, an open breaker, another HTTP error, no image) and GROK_API_KEY is set, the same call is made on xAI /v1/images/edits with the same avatar (a base64 data URI) and the same SHEET_PROMPT: grok-imagine-image-2.0, quality: medium, resolution: 2k, aspect_ratio: 2:3, response_format: b64_json (the highest tier on 2026-10-14, the same as the #4016 artist sheets; FIGHTER_GROK_IMAGE_MODEL overrides the model). It has its own breaker (xai_image_fighter) and the same retry rule. Surfaced: data[0].b64_json, judged like an OpenAI sheet. A Grok 400 is safety_reject_xai (final); a 429/5xx is xai_http_<status> (retried later). Only a missing avatar is never sent. Until 2026-10-14 this client had no failover. The OpenAI client retries only a failure the server never saw (a 429/5xx, after its Retry-After, or a connection that could not open): a timeout or a dropped connection may mean the sheet was drawn and billed. input_fidelity is not sent: gpt-image-2 rejects it with 400 (invalid_input_fidelity_model, measured 2026-10-14).

Tripo image-to-3D + auto-rig (utils/arcade/fighter_models.py:TripoProvider, #4012)

The balance and the day’s spend also reach the usage poll (utils/usage.py, source tripo, via cogs.arcade._FighterUsage). The v3 API, https://openapi.tripo3d.ai/v3 (developers.tripo3d.ai), keyed on TRIPO_API_KEY. Keys are shared between v2 and v3. Use the .ai host: openapi.tripo3d.com is a different region and answers this key with 401 (measured 2026-10-14). Calls:

A task create retries only a 429 (a 5xx may have made the task, so a retry could pay twice). Each finished task id is stored as it finishes; a retry of the same avatar re-reads those tasks (GET /tasks/{id}) and reuses every one still success, so it pays only for what is left. A saved task that can never be used is paid for again, with every later step: expired/failed/cancelled, a read Tripo refuses (a 4xx or code != 0 in a JSON body), or one still unfinished after 5 minutes. Any other read failure is a short outage and a retryable failure, never a second payment: a 5xx (including a Cloudflare 520/522/524), a 429, a dropped connection, or an HTTP 200 with an empty or non-JSON body. A saved task Tripo banned, or a saved rig check that is not a biped, is final at once. When a run stops waiting for a task (a cancel, or a saved task it drops), a background read waits for that task and adds its credits. That read adds credits only: it never writes a task id. A 429 first waits the server’s time: Retry-After (code 2000, a full concurrency pool) or X-RateLimit-Reset (code 1007, the rate limit), at most 32 s. Measured on the live run (2026-10-14): 60 + 0 + 25 + 10 + 10 = 105 credits, about 160 s. hi: 19,189 triangles, 2.0 MB; lo: 6,075 triangles, 441 KB.

Tripo for the chart-artist fighters (fighter_models.import_artist, #4016)

The artist models were built once, by hand, with scripts/arcade_artist_pilot.py (the same settings as above, plus the free rig check), and the bot only RESUMES those tasks. The pilot reads fighter_models.TRIPO_MODEL and TRIPO_TEXTURE_MODEL, so it builds on v3.1 with geometry_quality detailed, the same as a member run. All 33 artists were rebuilt on v3.1 on 2026-10-17 (P1 before that). The import: build(create=frozenset()) re-reads each saved task id in artist_roster.TRIPO_TASKS and never creates one, except one convert_lo when the saved lo was made at another face count (TRIPO_LO_FACES against LODS). A saved task Tripo no longer holds fails the import with not_reusable (final), and the artist stays absent.

xAI / Grok Imagine (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).

Grok image edit for the artist fighter sheets (scripts/arcade_artist_pilot.py, #4016)

Pilot-script only, never called by the bot: the owner approved Grok (grok-imagine-image-2.0, the top tier on 2026-10-14; grok-imagine-image-quality retires on 2026-11-02 and then redirects to 2.0 at low) with each artist’s official photo as the reference, for fully clothed, arcade-only fighter sheets. POST /v1/images/edits with quality=medium, resolution=2k, aspect_ratio=2:3, the photo as a base64 data URI, and response_format=b64_json.

OpenAI embeddings (utils/embeddings.py)

ElevenLabs STT (utils/stt.py)

ElevenLabs TTS (utils/tts.py)

Giphy usage (utils/gifs.py, #753)

OpenAI usage (utils/embeddings.py + utils/image_gen.py, #753)

Perplexity / Genius usage (no machine-readable budget, #753)


Ops (model-facing via tools)

GitHub (utils/github.py, check_fixes)

Railway (utils/railway.py, check_deploys)


Known real gaps (documented, not yet closed)

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.

Source-watcher cache speeds (2026-09-28)

The music desk’s source watcher (docs/ARCHITECTURE.md) must see a new edition within minutes, so four reads got a faster re-read. Field coverage is unchanged.