Note
An agent gets no special path.
Kickgeist exposes seven tools to AI agents over MCP. An agent call runs under exactly the row-level policies a human player runs under, and there is no path around them.
The usual way to let an AI agent use your product is to build it a second door. A service account, a scoped key, an integration tier. Then a set of rules that looks like the rules humans get, without being the same rules. It works, until the day the two sets disagree and the agent is the one holding the wider permission.
Kickgeist has no second door, because it has no first one. There is no API tier at all. The app on a phone talks to Postgres directly, and row-level security is the authorisation boundary. The schema carries 25 tables, 51 database functions and 48 policies, counted from the repository on 5 August 2026. Those policies are the whole answer to what any caller may read or write.
The boundary is the database
Let’s be plain about what this rules out, because it isn’t the popular choice. There’s no Node or Go service in the middle to write, deploy, scale, secure or get paged about at three in the morning. Business logic that has to be trusted lives in database functions with elevated rights and a pinned search path. The client can’t reach around them.
Scoring, referral completion, experience awards and challenge settlement are all database work for that reason. A tampered client can send whatever it likes. It can’t award itself points, because the award doesn’t happen in the client. We found a leaderboard-tampering exploit before the tournament and closed it at the database layer, not in the app. So the fix reached every build already on players’ phones.
What that made the agent interface cost
Because the boundary was already the database, exposing the product to agents was an interface problem, not a security problem. The MCP server runs on an edge worker. It exposes seven tools: list matches, make a prediction, create or join a group, read statistics, recover an account. Two authentication paths reach the same tools on the same origin: OAuth 2.1 with a one-tap consent screen, and static API keys for header-only clients.
Here’s the part that matters. An agent call mints a token for that individual player and runs under the same policies a human session runs under. The elevated service credential is never used by any client-facing path, the MCP server included. If a policy hides another group’s picks before kickoff, it hides them from the agent for the same reason it hides them from the phone. Neither statement is maintained separately.
The hard parts were invisible
None of that is where the work went. The tools are the easy half. The token handling is where the days went, and the failure modes aren’t obvious from a design document.
A refresh token that rotates must never be spent twice. Reuse detection treats a double spend as theft and revokes the whole token family. Under concurrent calls, a stateless worker did exactly that to itself. The fix was one shared session refresher with an idempotency cache in front, so parallel requests for a session collapse into a single refresh.
Minting the downstream token had the same shape and got the same answer. It’s now lazy and serialised inside a durable object, because the stateless version double-spent it under load. A warm object also re-applies its configuration on every request. Without that, it happily serves a token minted an hour ago with no reason to believe it expired.
Persistence fails closed. The server carries DNS-rebinding guards. Agent accounts are marked at signup, in the same atomic step that creates them. A failed follow-up call can’t leave an agent account looking like a person’s.
And the refresh token never reaches the model. That one is a design constraint rather than an implementation detail, and everything above exists to keep it true.
What it costs
Three things, stated as flatly as the benefits.
- A silent failure mode. Every new table needs an explicit grant beside its policy, because a policy alone returns a 403 with nothing in it. The first time that happened it cost an afternoon. It now lives in the repository’s operating manual, next to where a new table gets created.
- Authorisation review becomes SQL review. That’s a real skill requirement. A team without it will write policies that are subtly wide and pass every test they thought to write.
- A second public interface. An MCP server is a product surface, so it needs versioning, a specification, a threat model and its own tests. This one carries about 2,100 lines with roughly half again in tests, run in four tiers, and the last tier runs against the real worker runtime rather than a stand-in.
When we wouldn’t do this
Suppose the product had to talk to three external systems with three trust models, or the write path needed orchestration across services. Then the missing API tier stops being a saving and becomes something you rebuild badly in the client.
This shape suits a product where the data model is the domain model and the rules can be written as row visibility. Kickgeist is that kind of product. The test isn’t whether you like databases. It’s whether your authorisation rules can be written as a predicate over a row, and still be recognisable a year later.

