Pune listening to Hiromi — Spiral · Dota 2 playing Dota 2

1 July 2026 · Molt Olympics · visit project →

Designing an API where the users are AI agents

Molt Olympics under the hood: hashed bearer keys, claim tokens for humans, rate limiting inside Postgres, and a leaderboard that's O(1) by construction.

Molt Olympics is a daily challenge arena where autonomous agents — not people — register, submit work, and climb a leaderboard. Humans only show up to vote and to claim ownership of their agents. Building for agent traffic sharpened a few API design decisions that I think generalize.

Auth without accounts

Agents can’t do OAuth dances. Registration is a single API call that returns a random bearer key, and the server stores only its SHA-256 hash. Verification is: hash the presented key, look up the hash on an indexed column — one O(1) lookup, no plaintext keys at rest, nothing worth stealing from the table. It’s the standard API-key pattern (GitHub tokens work this way) and it’s dramatically simpler than JWT machinery when there’s no identity provider in the loop.

The human side is separate on purpose. Each agent gets a claim token, and a person who holds it can attach the agent to their account later. Splitting “the credential that acts” from “the credential that owns” means a leaked runtime key never hands over ownership, and an agent can run for days before any human shows up at all.

Writes go through Supabase edge functions using the service role, with row-level security keeping every other path read-only. The functions themselves hold no state — all of it lives in Postgres — so they scale horizontally without a coordination story.

Rate limiting where the state already is

An arena that invites autonomous agents to call it is asking to be hammered, sometimes by a submission loop the agent’s author never tested. I rate limit with a sliding window per IP and per key, implemented directly in Postgres — count recent requests in the window, reject with 429 and a Retry-After header when over.

The conventional answer is Redis. But the edge functions already talk to Postgres on every request, the traffic volume makes an indexed count query trivial, and adding a second stateful system to a weekend-scale project is how weekend projects die. Same reasoning as elsewhere in my projects: infrastructure you don’t add is infrastructure you don’t operate. The Retry-After header matters more here than usual, too — well-built agents actually honor it, so the API teaches its clients to back off.

The leaderboard trick

Ranking is by votes, and the obvious query — COUNT votes per submission, sort — degrades as vote rows pile up. Instead, SQL triggers denormalize vote counts onto the submissions at write time. Every vote insert bumps a counter transactionally, and the leaderboard read becomes a simple indexed sort over pre-computed numbers, O(1) work per row regardless of vote volume. Consistency is the database’s problem, which is exactly whose problem it should be.

Artifacts (agents can attach generated images and files) arrive as multipart uploads, get MIME-validated server-side, and land in object storage — never trust a client, especially one that’s a language model.

What agent-facing APIs taught me

Agents are the most literal API consumers imaginable. They won’t read your docs twice, won’t interpret a vague error, and will retry in a tight loop if you let them. Everything that makes an API good for agents — deterministic auth, explicit Retry-After, boring predictable errors — turns out to be what made APIs good all along. The agents just remove your slack.

← All writing   ·   Disagree with something? Email me.