Personal project · 2026
Banter
A cricket prediction game for close friends. You tell your story about the match, Banter builds the XI that fits it, and the group chat settles who read the game best.

The insight
For a while now two friends and I have played fantasy cricket against each other for small stakes, and the person who wins is always the person who did the homework: who is in form, how a batter does against this attack at this ground, who bats where. My first idea was the obvious one, a model that does the homework and picks your team. I dropped it in an afternoon. If the model picks, everyone in the group gets the same team, and the game is over before the first ball. I wrote about that decision in What building an app with AI changed about how I lead.
Watching people build their teams gave me the real insight. Nobody runs a spreadsheet. They carry a story about the match: Kohli is in form and it is Pakistan, the pitch is slow so the all-rounder will bowl his four overs, this keeper bats at four for this side. The team is that story written out as eleven names and a captain. So Banter asks for the story, and lets the data do the arithmetic.
The second insight is about who it is for. For most men in their thirties, the people they would message at ten at night about a match come down to two friends. Friendships like that rarely end; they fade, because nobody starts the conversation. Banter gives that small group a reason to talk every few days during a tournament, and the app is the one that starts it. The hook, in one line: back your cricket brain against the data, and against your friends.
Four rules came out of that and shaped every screen.
- Data is context, never advice. The story shows form, venue and matchups the way a pre-match show would. No rankings, no "recommended" badges. The user is the expert.
- Every pick explains itself. Each player in a generated XI carries a one-line reason, and a swap that breaks a rule says which rule.
- Receipts, not vibes. Every call is judged against what happened, including Banter's own predictions.
- Code decides what is true. Anything the app says in a group chat is built from checked facts.
The hook: a match story in seven calls
The fixtures feed takes its colours from the two teams playing, and opening a match starts its story. The first card says who is likely to win, from recent team form, and shows each side's players in form. The venue card says what the pitch does and the average first-innings score. Then come the calls: who will win, and for each side, how its batters, all-rounders and bowlers will do, each rated Good, OK or Poor. Every role card shows that side's top three with their recent scores or figures, and the insight icon opens the full list with Banter's predicted runs, wickets and points as a likely range, not a single confident number.
Answering a card scrolls the next one into view, and the screen ends on "Build me a team". Seven calls make a story. That matters, because stories lock five minutes before the first ball, and most of them are told by someone at work with a few minutes to spare.


From story to XI
The story becomes a team through a small piece of code that is easy to explain. Each player starts from the model's projected Banter Points, the app's fantasy-style score for runs, wickets, catches and bonuses. The story then tilts that value.
| Story call | Effect on the player's value |
|---|---|
| Their side's batters, all-rounders or bowlers rated Good, OK or Poor | ×1.2, ×1.0 or ×0.8 |
| Rated Good or Poor as a key player | ×1.1 or ×0.9 |
| Their side picked to win | ×1.05 |
| One of the user's favourites | ×1.05 |
An exact dynamic programme then picks the eleven with the highest tilted value that satisfy the rules: exactly 11 players, at least one wicketkeeper, three batters, one all-rounder and three bowlers, and no more than seven from one side. A brute-force test checks the optimiser on every run of the suite. The top two picks become captain (2x points) and vice-captain (1.5x), and every pick carries its reason, for example "Projected 49 pts. You rated IND batters Good, backed this player, and picked IND to win." Two friends who tell different stories get different teams, which is the whole point.
Users can swap anyone. A swap that would break a rule is refused with the rule it breaks, so the sheet teaches the rules instead of hiding them.


Real fixtures make this harder than it sounds. Squads are announced late, so until they are, the XI is built from a provisional squad: the team's last squad, or the squad named for the series. Picks who are dropped stay in the team, flagged to swap.
At the toss, about thirty minutes before the start, Banter reads both playing XIs and flags anyone not playing, with time left to fix it. Picks lock five minutes before the scheduled start, and a rain delay before the first ball keeps them open until play actually begins.

The prediction model, tested honestly
For every player in both squads, Banter forecasts runs, wickets and Banter Points as a distribution, a mean with a p10 to p90 range, plus how likely the player is to play. The story shows these as ranges, and the team builder uses them as its starting values.
What is in production
The production model is hand-built and readable. Expected runs blend a recency-weighted average of a player's recent innings (62%), their median (18%) and a batting rating (15%), scaled by how much batting their role usually gets and by their chance of playing. The range around that comes from a gamma distribution whose spread is set by the player's own variability. A matchup layer, built from batter-against-bowler ball history in Cricsheet, nudges each forecast for the bowlers a batter is likely to face. When the data for a match is thin, with missing profiles, too little recent history or an uncertain XI, predictions show as pending with the reason, rather than inventing precision.
Snapshots are taken automatically 24 hours, 6 hours and 90 minutes before every match. The last one before the start is locked for good: it is what the post-match review compares against, so the model can never quietly rewrite what it said. Once the scorecard is in, every prediction is graded against what happened. Grading waits for complete data rather than scoring missing figures as zero, and gives up waiting only after 48 hours.
Then I tested it properly
A replay harness rebuilds forecasts for 8,817 T20 matches from Cricsheet's ball-by-ball data, using only what was known before each match date, and scores them on a holdout of 826 matches from 2025 onwards: 18,642 player forecasts. The result was humbling. On runs, the production model lost to both simple baselines. Its ranges were the main problem: a gamma curve never puts the low end near zero, yet about one innings in ten is a duck and many players do not bat at all, so 46% of scores fell below the model's p10.
The challenger is simpler. It forecasts from each player's own last 40 outcomes, weighting recent matches more, and shrinks them towards a pool of similar players by role and batting position. Its settings were tuned on 2024 matches only, and the 2025 holdout was scored once.
| Runs, 2025 holdout | Production model | Best simple baseline | Challenger |
|---|---|---|---|
| Average error of the median (MAE) | 14.76 | 11.30 | 11.01 |
| Interval score, lower is better | 71.98 | 52.12 | 49.26 |
| Calibrated at p10, p50 and p90 | No | Yes | Yes |
The challenger beats the strongest baseline on both accuracy gates, with match-clustered 95% confidence intervals entirely below zero, and cuts the production model's runs error by about a quarter. It now runs in production in shadow: every scheduled refresh stores a second snapshot beside the champion's, graded the same way and never shown to users. It replaces the champion only once at least 20 live matches favour it on both error measures and it stays calibrated. That promotion is a user-visible change in its own pull request, because the champion's points now choose people's XIs.
A data-scientist agent watches all of this. Health checks run every three hours against a read-only connection to production: matches left ungraded, missing pre-match snapshots, a scorecard that reads zero for everyone, groups left unsettled. Each failing check opens a GitHub issue, and the issue closes itself when the check recovers.
Playing with friends
"Play with friends" makes a private group for the match and opens the share sheet with an invite link, because this is how a group like this actually forms: in WhatsApp. Friends join from the link. A user can build several teams for a match and play a different one in each group.
From the lock until the last ball, the leaderboard moves ball by ball. Every live update re-scores each open group from the full match state rather than adding deltas, so a corrected ball fixes itself, and the new standings go out over a WebSocket channel only members can open. Between the toss and the lock the standings stay hidden: the playing XIs would reveal whose picks are not playing while teams can still change. Live points are provisional. The official scorecard settles them, or the ball-by-ball data does after 48 hours, and the result records which.
Settling a group awards every entrant a player card: one player from their own XI, picked in a draft in finishing order, with that player's line and points from the match. The winner's card is a Legend and last place gets a Common, with Elite and Rising in between. Any card can be shared to an Instagram story as a 1080 by 1920 image. Everyone leaves with something, and over a season the collection becomes a record of who called it.


Banter in the chat
Every group has a chat, and Banter is a member of it. Its lines are templates filled from match and group data, and each one posts once per group: the toss, how the group's picks split and who captained whom, wickets, big overs, fifties and hundreds, a captain's haul or duck, the target at the innings break, a new leader, and the result.
Templates sound simple. The care is in what they refuse to say. Banter backs a side at the toss only when the ground's record earns it: ten or more decided T20s, with the 95% Wilson lower bound of the winning share above a half. So when it says "Going by the ground, we'd back IND: the side batting first has won 9 of 10 at Wankhede", that is a claim with evidence behind it. A 60% record is only "a slight lean", an even one is "no clear edge", and fewer than ten matches is too thin to call.
Milestone and captain lines use only exact figures, from the scorecard or the batters at the crease, because the ball-by-ball feed credits a whole over to its first striker. No line uses he or she.

Next: an LLM narrator with a fact-checker
The next release adds an LLM narrator on top of the templates, and it is designed as a workflow, not an agent. After an over that matters, code builds a pack of facts: the over, the score, milestones, the state of the chase and up to twelve lines of commentary. Players, teams and the venue become placeholders, so the model never sees a member's name or a word of the chat. One call per moment serves every group watching that match, so the cost does not grow with the number of groups. I estimate it at about a cent a match on the smallest Claude model.
The model writes one line or chooses to skip. Then code checks the line before anything posts. Every number, name and superlative must appear in a fact the line cites, and an "Nth over" must be the right over. Betting words and certainty ("will win", "in the bag") are rejected, and so are gendered pronouns, six or more words copied from the commentary, and anything too close to a line from the last few minutes. Any failure means silence, and the templates carry on. The bits that address a group ("Dev, still happy with WI?") are written by code, not the model; they tease picks rather than people, and name a member at most three times a match.
It ships through gates: an offline evaluation with zero ungrounded lines among at least 150 I have labelled, ten matches in production shadow, then my own groups, then everyone. Any wrong fact a member reports sends it back to shadow.
The receipt
Tapping a finished match opens Banter Review rather than a scorecard. It answers the things friends argue about after a match: did my call land ("Nailed it", "Missed it", or "No verdict" when a tie or washout leaves nothing to judge), where did each of my teams and groups finish, and how did Banter's own predictions do against what every player actually scored. The predictions shown are the locked pre-match snapshot, never recomputed, and a player with no evidence either way reads "Not known yet" instead of a fake zero.
That honesty matters to the product as much as to the model. A game about who reads cricket best only works if the receipts can be trusted, and that includes Banter's own.


How it is built
I built Banter over eight months of evenings and weekends, around a demanding day job, mostly with Claude Code doing the translating from decisions into code.
- 452 commits
- since February 2026
- 214 test files
- and 72 Storybook files covering every screen state
- 8,817 T20 matches
- behind the model's replay evaluation
- Backend. Fastify and TypeScript, memory-first with write-through to Postgres, and Redis, on Railway.
- Live data. Cricbuzz polled every 5 seconds, backing off to 10 and 20 when the source wobbles and failing over to ESPNcricinfo in the same cycle, with every failure classified: access denied, rate limited, timeout or parser error.
- History. Cricsheet's ball-by-ball T20 archive behind the form ratings, the matchup layer and the model replays.
- Media. Player photos pass through a verification pipeline, and only verified photos are ever shown. Everything else gets a neutral placeholder.
- App. Expo and React Native. Every merged change to the app ships over the air, and TestFlight builds go out on demand.
- Discipline. Tests first by default, architecture decision records for choices that would otherwise be argued again, and a Storybook story for every screen state. The app screens on this page were captured from those stories.
What I am watching
One number decides whether Banter works: the share of groups where every member tells a story and at least one member writes in the chat during the match, across ten or more matches in a season. Above half, Banter is a product. Above 70%, it is a habit.
Join the beta
Banter is on TestFlight for iPhone. If you follow cricket and have opinions about who should captain, add your name to the beta list and I will send you an invite before the next big series. Bring two friends: it is a game for a group. I only use your details to send the invite.