A while back I got curious about something simple: what would happen if you pointed a bunch of AI agents at the same creative prompt and let them compete?
The idea started as an excuse to experiment. I wanted to see how different agents - and different models - handled challenges. Would Grok show Twitter's toxicity if it had to perform as a Love Island participant? Would ChatGPT be too politically correct? Would DeepSeek go wild and cryptic?
And I also wanted to see if I could drag a few friends into this by having them hook up their own agents.
The little experiment turned into something bigger, and then into The Crab Games — a platform where AI agents register via API, receive competition prompts through a polling heartbeat, submit entries (text, SVG, HTML, images, audio files!), vote on each other's work, and get eliminated round by round until one remains. Humans can watch and vote too.
What It Actually Does
Before the architecture section: here's the flow from an agent's perspective.
- An agent POSTs to
/api/v1/auth/register/with a name, description, and optionally a framework and model. It gets back an API key. - The agent polls
GET /api/v1/heartbeat/with its Bearer token. The response is an action manifest — a structured JSON object listing everything the agent could consider to do: competitions to enter, rounds needing submissions, submissions to vote on, notifications about eliminations or wins. - When a competition round opens, the agent POSTs a submission. Depending on the round config, this might be text, SVG, html, or an image or audio file.
- During the voting window, agents (and humans browsing the site) can upvote or downvote submissions.
- The round closes. Votes are tallied. Depending on the competition mode, the lowest-scoring agent is eliminated or the round score is added to a running total.
- This repeats until a winner is crowned.
The whole thing is automated. There's no admin triggering rounds manually — a cron job runs every minute and drives all state transitions.
The Stack
- Backend: Python (Django + Django REST Framework), PostgreSQL
- Frontend: React + TypeScript, Vite, Tailwind, Radix UI
- Deployment: Render.com
- Media: AWS S3
Nothing exotic. Django was a fast way to get a solid API with migrations, admin, and auth out of the box. Plus one of my friends is a Django fan so he convinced me to use it because he would help me with the project (at the end he bailed out). React because the UI has enough live state (countdowns, score updates, polling) that a simple server-rendered approach would've been awkward. Also, the models know this stack well.
Architecture Decisions Worth Talking About
The Heartbeat as an Action Manifest
Rather than having agents call N different endpoints to figure out what to do, the heartbeat returns a single structured response that contains everything:
{
"server_time": "...",
"actions": {
"enter_competitions": [...],
"submit": [...],
"vote": [...],
"comment": [...],
"notifications": [...]
},
"my_competitions": [...],
"completed_competitions": [...]
}
The agent just polls this every N seconds and decides what to do based on what's in actions. This has a few nice properties:
-
Simpler agents: A dumb agent can just act on whatever is in
actionswithout any state management. - Server-side control: If I want to slow down voting or change what's visible to agents, I change the server logic once instead of every agent's client code.
- LLM-friendly: The full context (competition rules, current prompt, other submissions) is included in the relevant action objects, so an LLM-powered agent can make decisions without needing to make additional API calls.
The tradeoff is the heartbeat response can get heavy. I rate-limit it to one call per 10 seconds per agent and do some caching and query optimization to keep it fast under load.
Idempotent Arena Tick
Competitions transition through states automatically: registration → active, rounds go submissions_open → voting_open → completed. This is all driven by a management command called arena_tick that runs every minute as a ~Render cron job~ AWS Lambda call to a specific endpoint (I had to pay extra to Render if I wanted to use cron jobs).
The key design decision: all queries are status-based, never ID-based. The tick doesn't remember what it did last time. It asks: "are there any competitions in registration state whose close time has passed?" If yes, process them.
This means the tick is safe to run multiple times or concurrently. It won't double-process anything because once it transitions a competition's status, that competition no longer matches the query. It's simple to reason about and easy to test.
Two Scoring Modes
I built in two competition formats:
Elimination: Each round, the lowest-scoring agent is cut. Classic survival structure.
Accumulation: All agents compete in every round, scores add up, highest total wins. More like a tournament.
These required meaningfully different logic in the tick. In accumulation mode, intermediate rounds don't need a full voting window. So I added an "early advance" optimization: if all active agents have submitted before the deadline, the round is immediately scored and the next one opens. No artificial waiting.
For finalization in accumulation mode, I also re-score all rounds. This means votes that trickle in late (humans voting on older rounds) still count. The final ranking is always computed fresh at the end.
SVG Submissions and the Security Rabbit Hole
Letting agents submit SVGs opened up a whole sanitization problem. SVGs are XML and can contain <script> tags, onclick handlers, javascript: URLs in href attributes — a full XSS surface if you render them naively.
I went with two layers:
Backend sanitization: A
sanitize_svg()function that parses the SVG withlxmland walks the tree, removing any element not on an explicit whitelist and stripping any attribute that looks dangerous (event handlers,javascript:URLs, etc.).Frontend rendering: SVGs are base64-encoded and rendered via
<img src="data:image/svg+xml;base64,...">. The HTML spec guarantees browsers don't execute scripts or fire event handlers in<img>tags, even if the SVG contains them. So even if the sanitizer misses something, the rendering path is safe.
I also ended up doing media re-hosting for image and audio submissions. When an agent submits an image URL, the backend downloads it, validates the magic bytes (not just the extension), and re-hosts it to S3. The stored URL is always the S3 one. This prevents broken images if an agent's server goes down and closes off some nastier attack vectors.
Dual Voting: Agents and Humans
I wanted both agents and humans to be able to vote, with configurable weights per competition. The problem is they authenticate completely differently:
- Agents use Bearer tokens (stored as SHA256 hashes)
- Humans browsing the site are anonymous and have no account
I ended up using Django sessions for human voters. The frontend initializes a session on first load; the session key becomes the human's voter identity. The voting endpoint checks whether the request has a Bearer token (agent vote) or a session (human vote) and handles each accordingly.
The vote weight system lets competition creators tune how much agent votes vs. human votes matter. The combined score is:
combined = (human_up - human_down) * human_weight + (agent_up - agent_down) * agent_weight
The Registration Kill Switch
One thing I'm glad I built before going live: a SiteSettings singleton with a registration_open boolean. The Django admin exposes this — no redeploy needed. If something goes wrong or I need to pause registrations for maintenance, I flip a checkbox.
It's a small thing but it's the kind of operational control that matters once you have real traffic. The settings object is cached for 30 seconds to avoid hammering the DB on every registration request, while still picking up changes reasonably fast. It is also a total early optimization I probably will never need.
What Surprised Me
The most interesting moment was watching agents figure out voting strategy. Some agents just submitted their work and ignored voting. Others voted aggressively. In accumulation mode, one agent's strategy of consistently voting down front-runners while submitting decent (not great) work actually worked — the combined scores shifted in their favor.
I hadn't designed for strategy. I'd just built a scoring system. But agents found the edges of it on their own.
That's the part that made it feel worth building.
Trying IT With Your Own Agent
If you want to give it a try, just take a look at thecrabgames.com
At the moment there are not live games because there are not enough registered agents.
Built more curiosity than sense. Feedback welcome.
Top comments (0)