Departed
A manager who leaves is flagged, not filtered. Their drafts, trades and finishes stay in the record, resolved against the last team name they held rather than dropped for having no current one.
BlatBlat turns NBA and Yahoo Fantasy data into a working desk: what changed tonight, where the interesting edge is, and the analysis under it. The hard part is not the box scores. It is that a fantasy league rewrites its own identity every year, and the history has to survive that.
A fantasy basketball league looks stable from the inside and is not. Managers rename their teams between seasons and sometimes mid-season. Managers leave. Managers come back two years later. New ones arrive. And Yahoo issues a brand new league id at every renewal, so last season is not merely old, it is at a different address.
Most fantasy tools key their history on the team name, because that is the string on the screen. It works until someone renames a team, at which point one manager silently becomes two people, both with half a history. That is the failure this product is built around avoiding.
BlatBlat keys everything on the manager, using the opaque identifier Yahoo issues per account. The team name is demoted to what it actually is: an attribute of one season. Every record is season-scoped, carrying that season's team name and that season's league id alongside the manager it belongs to. A rename is then not an event that has to be caught, it is simply the same identifier appearing under a different name, which is a thing you can read rather than a thing you can lose.
| Season | Team name that season | League id that season | Manager | Read as |
|---|---|---|---|---|
| 2023-24 | Vasilenko’s Vandals | 418.l.20194 | MGR-7QK4 | Founding member |
| 2024-25 | Wodonga Wallabies | 428.l.11627 | MGR-7QK4 | Renamed, same manager |
| 2025-26 | Wodonga Wallabies | 454.l.30881 | MGR-7QK4 | Unchanged |
| 2026-27 | Wodonga Waratahs | 466.l.16464 | MGR-7QK4 | Renamed, same manager |
A manager who leaves is flagged, not filtered. Their drafts, trades and finishes stay in the record, resolved against the last team name they held rather than dropped for having no current one.
A manager who comes back after two seasons away matches their old identifier and resumes one continuous history, instead of arriving as a stranger who happens to share a name.
A new entrant is a new identifier with no prior seasons, which is different from a manager whose seasons have not been found. The model distinguishes the two rather than showing both as empty.
The league rule is that a player may be held by the same manager for a limited run of consecutive seasons, then must be released. Enforcing it means answering one question precisely: how many seasons in a row has this manager held this player?
Both halves of that question are traps. The manager half is the identity problem above. The player half is the same problem again, because Yahoo scopes its player keys per season too, so the numeric part is normalised out and the player becomes one continuous person across seasons. Then the run is walked forward: a keeper pick extends it, a fresh auction buy starts a new one at year one.
The subtle case is the run that stopped. If a player was held for three seasons and then traded away, that run is dead history. It must not surface as a warning against a manager who no longer has the player, and it must not travel to the manager who bought them. So a run only counts if it reaches the current season; otherwise the clock reset and the screen says so.
That last point is a house rule of the product. A keeper limit is a league decision, not a law of nature, so it lives in configuration and every screen and every sentence that mentions it reads from there. The prose is generated from the number too, which is why the page can say "the four-year keeper limit" without anybody editing a string.
Keeper limit, team count, week boundaries, scoring categories, the list of departing managers, and every league id the league has ever had. Changing any of them is a settings change.
Which seasons exist at all, how many teams were in each, the authoritative keeper flag on every draft pick, and the manager identifier behind every team. The season list is discovered, never enumerated.
The home screen is a console, not a landing page. A date strip across the top with tonight highlighted, league leaders down the left, both conference tables down the right, and the night's storylines under them. It is deliberately dense: the audience reads standings for pleasure and does not need the information spaced out.
The storylines are the opinionated part. Thirty-one plugins run against every game on the slate, each looking for one specific kind of narrative: a coach facing the club that sacked him, a winning streak meeting its bogey team, two lottery picks from the same draft starting opposite each other for the first time. Each returns a score, the scores sum per game, and the game with the highest total is the one worth watching.
Naming the plugin under each storyline is a small thing that matters. It makes the output auditable: a claim you disagree with can be traced to the rule that made it, and switched off. The whole plugin set is fingerprinted by hashing every plugin's identifier, score, active flag and parameters, so any change to the scoring bumps a version automatically and every cached night is stamped with the version that produced it. If the desk starts making different calls, you can tell whether the league changed or the rules did.
The product's own word, and its best idea. A Janorism is a statistical claim narrowed by qualifiers until it is actually rare: not "he is closing on 1,500 threes", which forty-odd players have done, but the version of that fact which is true of exactly one person.
The engine holds a vocabulary of qualifiers, each a small reusable predicate with a stable identifier: born in a given country, from a given continent, drafted in a given round or outside the lottery, undrafted, above or below a height, a rookie, a sophomore, a player who has only ever played for one franchise. It composes them onto a milestone one at a time and watches the qualifying population fall.
The judgement is in where it stops. One qualifier too few and the fact is shared with three other players, which nobody would repeat. One too many and the sentence is not true yet. So the engine keeps the narrowest claim that still has a name in it, and scores it on how far the population fell to get there. Facts also carry the moment their uniqueness was last verified and the date of the game they describe, because a saved fact can easily outlive the night it was written for.
Game Finder is the query builder under all of this, and it has one design problem worth describing. A box score can be read at three heights: one row per player per game, one row per team per game, or one row per game. Which one a question wants is not something the user should have to declare, because it is implied by what they asked.
So the filters are tagged by the level they belong to, and the query resolves to the least aggregated level any active filter requires. Ask about team results and it answers per team. Add a filter about a player's turnovers and it drops to the player row, because a summed team row cannot say who turned it over. The team filters still apply; each candidate player row is joined to its own team's row for that game.
Listing what it cannot do is the same instinct as naming the plugin under a storyline. An analysis tool earns its density by being legible about its own limits; a filter that silently does nothing is worse than a filter that is not there.
Pages render on the server with their data already in them. There is no client-side warm-up, no hydrating placeholder, no spinner filling in the primary content after first paint. If the data is not there, that is a server problem to fix rather than a state to design.
The data comes from several sources that disagree about availability, so every call falls through a chain rather than retrying: the primary statistics endpoint, then a CDN, then a third party. There is no exponential backoff and there are no circuit breakers that lock a source out for ten minutes. Every call, every fallback decision and every result is written to a handshake log with its duration, and that log is visible in the interface, so a slow night is something you can watch rather than guess at.
An unavailable source is a decision to make immediately, not a timeout to wait out. Each concern has its own chain and its own fallback, recorded with the milliseconds it cost.
No hardcoded rosters, no assumed team count, no invented player data. If the store is empty a query returns nothing, which is honest, rather than a plausible fallback, which is not.
Tables may scroll sideways, but membership status, playoff bands and every state are carried in text as well as colour, and the reading order survives a narrow screen.
Every part of this is the same argument in a different place. Identity belongs to the manager, not the label on their team. A league rule belongs in configuration, not in a constant. A storyline should name the rule that produced it. A statistical claim should state the qualifiers that make it rare. A query should answer at the altitude the question implies, and say which filter emptied it.
The audience is expert and reads at night, and the fastest way to lose them is to be confidently wrong about something they can check. So the product is built to be checkable: dense, opinionated, and legible about where every number came from.
Something with a history problem in it?
Attribution, identity and data that has to survive its own past.
The screens are real and running; the data in them is not. Every league, team name, manager, identifier, cost, standing, total and score shown here is invented, so nothing on this page discloses a real league or a real person. NBA player and team names are public fact.