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/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/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.isrc (not answer-valuable). Known API limitation, not a drop.utils/musicbrainz.py)disambiguation.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.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.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), the
LAST provider in the hub chain (after API-Sports + SGO), 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).PlayerProp, where which book offers a prop matters);
includeLinks/includeSids/includeBetLimits/includeRotationNumbers (deep
links + bet limits — not answer-valuable for our surfaces; revisit if we ever
show “bet it here” links); 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).utils/api_sports.py)/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/perplexity.py)utils/x_fetch.py)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/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.