← Blog · Behind the scenes · 10 min read · November 11, 2026
How one person and AI agents shipped a sports app in 19 languages: what worked and what broke
I was laid off from Moovit in its last round of layoffs. I decided not to look for the same job again and to work full time on BeTeam, a free sports prediction game for friend groups that I had been building on the side. There is no team. There is me, and there are AI coding agents. This is what that setup actually looks like after seven months: what works, what broke in production, and what I still have to do myself.
The numbers first
| Players | 4,800+ people who made predictions, from 132 countries |
| App languages | 19, including two right-to-left (Hebrew and Arabic) |
| Sports and competitions | 7 sports, 11,800+ competitions |
| World Cup 2026 | 146 → 4,143 registered users in 5 weeks; about 200,000 predictions typed in by real players |
| Code | About 1,980 commits since the first one on April 9, 2026; roughly 200 of them are from bots |
| People on payroll | 0 |
These are counted the strict way: no bots, no test accounts, and no predictions the app filled in automatically for people who forgot. I learned to count like that the hard way, which is part of the story.
How the "team" works
The agents are Claude Code sessions. Each one gets a task, its own git worktree and its own branch, and ends in a pull request. A session can start side sessions for work that should run in parallel. This post is an example: a session working on press outreach found that the outlets I wanted only link to real articles, and it started a separate session to draft this one. I rewrote it and approved every line.
What makes this work is not the model. It is the rails around it:
- A project rules file that every session reads first. Branch naming, commit format, which commands are forbidden and why. Every rule in it exists because something went wrong once.
- A hook that runs before every shell command. It blocks commits to
main, schema commands that can drop a database, orphan branches, and merging a pull request whose checks are not green. When it blocks something, it prints the reason. Agents follow a printed reason much better than a paragraph in a document. - One verify command that runs the same gates as CI, locally, before every push. It only runs the suites that the changed files trigger. It allows two runs at a time, because five parallel runs once took a 36 GB laptop out of memory.
- A local "clean room": a Linux container that checks out the pull request fresh, installs, starts its own Postgres and runs the checks. Since November, pull requests are checked on my Mac instead of paid CI minutes.
- A local QA stack with its own API, database and simulator, so that tests which sign up users never touch production. More on why below.
- A memory folder. Short notes, one fact each, that sessions read at the start: incidents, decisions, traps. It has over 500 of them now. The useful ones are not "what we built" but "what bit us, and how to notice it next time."
Translating into 19 languages when you read two
I read Hebrew and English. The app has about 2,900 strings per language. The process is written down as a playbook, and its first line is the important one: no layer of checking is allowed to depend on me reading the language.
- A glossary before any translation. Not invented: mined from pages that already exist in that language, then locked. It also fixes style decisions: formal or informal address, how to handle gender, what stays untranslated (team and league names, the app name).
- Agents translate in parallel, one file each, against the glossary.
- Machine gates in CI. Every key exists in every language, every
{{placeholder}}survives, and every plural form the language needs is present, checked against the browser's own plural rules. - Blind back-translation. A separate agent gets only the translated file, never the English, and translates it back. A script compares meaning key by key and flags drift and reversals.
- Separate reviews for terminology, grammar and register, and for how each string is used on screen.
- A native speaker is a required gate in the playbook, on top of all of that.
The bugs it caught were never the ones I expected. Plurals were the number one source:
- Hebrew has a separate form for exactly two. When it was missing, the app showed an English word in the middle of a Hebrew sentence, only when the count was 2. That happened in 15 strings.
- In French and Portuguese, zero uses the singular form, the opposite of Hebrew and Italian. A hardcoded "1" in the singular string printed "1 point" when you had zero.
- Polish has two different plural forms that both appear every day, and a grammar case problem the translating agents missed: a label that read "PLAYING {day}" broke when the day was a weekday name, because Polish needs a different case there. The fix was a dash, not a code change.
- Spanish: the common Latin American word for a prediction pool is vulgar in Spain. Every glossary term is now checked in both markets.
Right-to-left is its own category. In Hebrew and Arabic, a score has to show the home team on the correct side, "align right" flips when the layout flips, and team names stay in Latin letters inside Arabic text. Some of these only show up on the second cold start of the app, which is why the QA steps say "restart twice."
What broke, and what each failure left behind
1. The World Cup and the connection pool
On June 11, the World Cup opener, the app looked stuck. Sign-ups were failing with a duplicate-key error, and live games froze as "scheduled." We patched those. Three days later, at the kickoff of Germany vs Curaçao, about 1,400 leagues included that same game, and the background jobs for all of them started at the same moment. The API ran out of database connections and was unreachable for about twelve minutes. I restarted the server from my phone's browser.
It took two and a half hours to find the real cause, and the first ideas were wrong. The agent kept raising the app's own connection limit, which made it worse. The answer came from data, not guessing: the database itself had free connections, but the connection pooler in front of it allowed only 15. Raising that number to 30 fixed it within minutes. Three days later, after we added a second server region, it happened again for the same reason, and the fix was the same lever, raised to 40.
What it left behind: a written diagnosis order (check the database's own health, probe the pooler, read the pool size) and a rule that adding app servers is not the fix when the pooler is the wall. Also a rule for marketing copy: we never claim zero downtime.
2. The update that crashed on launch
On October 30 we published an over-the-air update. After publishing, a check that inspects the bundle reported that it would fall back to localhost. The agent investigated, decided it was a false alarm, and told me the update was fine. It was not. A stale build cache had produced a bundle without the database URL and the analytics key, and the app crashed on launch. The update system rolled phones back to the previous version on its own. Nobody was paged, because there is nobody to page.
What it left behind: a release preflight command that refuses to publish from an old commit, a dirty tree, a missing environment file or stale shared packages; builds that always clear the cache; the bundle check runs before publishing; and one plain rule: a failed check means roll back first. A check that exists because a failure reached production once is believed over an agent's reasoning.
3. A read-only query that stopped every write
On October 26, an analysis query ran against production through a read-only connection. It joined a large table to itself over 30 days and spilled about 4 GB of temporary files in 90 seconds. The disk crossed the host's safety threshold, the platform switched the whole database to read-only for 34 seconds, and every write in the app failed. "Read-only" protected the data. It did not protect the disk.
What it left behind: before any analytical query on a big table, run a plain EXPLAIN first, start with a narrow time window, and replace "next row" self-joins with window functions. A related rule is enforced by the hook: one session setting on the pooled connection can outlive the session and turn later, unrelated writes into errors, so the command is simply blocked.
There was a smaller one too: test sign-ups from a development build reached production, because the dev bundle silently read the production environment file. Five test users, deleted the next day. That is why tests that create accounts now run only on the local stack.
What AI could not do
- Decide what to build. Agents produce good options. Choosing between them needs a view of who the users are and what I will not do, for example, no real money and no betting.
- Talk to users. Reading reviews and support messages can be automated. Answering a league organizer who is angry about a scoring rule cannot, not well.
- Judge a release. The crashed update is the clearest example. The agent was confident and wrong. The check was boring and right. Deciding which to trust is still my job, and now it is written down so I do not have to remember it.
- Own anything public. Every write to the app stores, every ad that spends money and every email to a journalist needs my approval each time. The one exception is a narrow store job I approved in advance, with a client that refuses every other write.
- Count honestly by default. The first numbers agents give you are the easy ones: all rows, all users. The real ones exclude bots, test accounts and automatic fills, and you have to ask for them every time.
What I would tell someone starting the same way
- Put rules in checks, not in documents. A blocked command teaches better than a paragraph.
- After every incident, write one short note about how to notice it next time, not about what happened.
- Give every language a check that does not depend on you reading it.
- Keep production out of reach of tests. Assume any environment file will be read by something you did not expect.
- Keep the decisions. Delegate the typing.
The earlier part of this story, moving the app off a no-code platform, is in From Bubble.io to React Native in 4 days. If you want to see what the agents and I built, BeTeam is free, has no real-money betting, and lets you start a private league for your group in any of its supported competitions. How points work is on the scoring page.