BlatBlat

Basketball intelligence · blatblat.com

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.

Identity
Manager, not team
Names are season-scoped
Structure
From Yahoo
Not hard-coded
Storylines
31 plugins
Scored, versioned by hash
Rendering
Server side
No client warm-up

The league changes its name every year

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.

One manager, four seasons Invented league · identifiers illustrative
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
Four seasons, four league ids, three team names, one manager. Keying on the middle column gives three managers with fragments of a history. Keying on the fourth gives one manager with all of it. The league ids are kept in a history list precisely because Yahoo does not reuse them, so the older seasons stay addressable.

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.

Returning

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.

Incoming

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.

What that buys you: the keeper clock

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.

Live screen
Keepers. Each run drawn as one box per season, so the shape of the run is the reading. Gold is a keeper pick, outlined is a fresh auction buy, dashed is a season the manager did not hold them. Below, the same manager under three team names, and the managers who have left. The limit reads 4 seasons because that is what this league's stored configuration says; it is not a constant in the code.

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.

Read from configuration

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.

Read from Yahoo

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 desk

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.

Live screen
The league desk. Leaders on the average tab with a games-played qualifier stated for percentage categories, both conferences with playoff, play-in and lottery bands marked structurally rather than by colour alone, and the storylines that fired tonight with the plugin that produced each one named.

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.

Janorisms

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.

Live screen
Janorisms. The ladder at the top is one candidate narrowing: 47 players hold the raw milestone, 9 once born outside the USA is added, 3 once drafted outside the first round is added, 1 once a height bound is added, and 0 at the next rung, which is rejected. The kept rung is the one in gold. Below it, the live feed ranked by novelty with each fact's qualifiers and the source behind it named.

Answering at the right altitude

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.

Live screen
Game Finder. Five filters across two levels, the resolved altitude stated with the reason, and the funnel showing which filter did the work: 94 rows survive the points and assists conditions, and the turnover condition cuts that to 16. The filters the product cannot yet honour are listed rather than offered.

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.

How it is put together

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.

Fail fast, fall through

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.

Nothing hard-coded

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.

Dense, and accessible with it

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.

What the product is arguing

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.

Get in touch

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.