# I vibe coded an LM Arena for AI companion apps with Claude Code.

> Source: <https://dev.to/samm_nofilterreview/i-vibe-coded-an-lm-arena-for-ai-companion-apps-with-claude-code-fbo>
> Published: 2026-07-25 17:52:04+00:00

I can't write code. I've never written a line of it by hand.

I still wanted something that didn't exist: a leaderboard for AI companion sites that shows what was tested, when, and how many people voted. Most "Top 10 AI girlfriend apps" pages are affiliate lists with a number stuck on top. LMSYS Chatbot Arena does it properly, so I described that to Claude Code and we built it: [seven leaderboards with community voting and dated monthly snapshots](https://nofilterreview.com/rankings/). Astro, Cloudflare Pages Functions, D1. No accounts, no login, no client framework. Voting is a static page and one POST.

Two things I learned from specifying all of it and writing none of it.

They all pay me affiliate commissions, so they all have a reason to send fake votes.

Ballots over the rate limit don't get rejected. They get written like any other ballot, they just never reach the table the score reads from. If I rejected them I'd have no record, and I couldn't tell you later how many got filtered or why. This way each board can show votes received and votes counted.

Ratings work the same way. Every submission is kept, and the current state is a derived table with one composite key:

```
ON CONFLICT(voter_id, app_id, category_slug)
DO UPDATE SET rating_event_id = excluded.rating_event_id, updated_at = excluded.updated_at
```

Vote again next month and the event is appended and the pointer moves, so nothing counts twice. If I find a cheating pattern later, the raw events are still there to rebuild from.

The code works. What almost broke it was a link between two things that look unrelated.

Each rating dimension sent a key to the API, and that key came from slugifying the label on screen. "Filter strength" turned into `filter-strength`

, which matched. Then I renamed the label for a reason that had nothing to do with the API: a 5 meant "never blocks you", so high was good, but "filter strength" sounds like high means more filtering. It read backwards, so I changed the words.

Every ballot in that category would have failed with a 400, because `spicy`

isn't a key the API knows, and nothing would have told me why.

If you're shipping code you can't read, that's the thing to watch for. Not code that's broken when it lands. Code that works and is wired to a word you'll change later without thinking about it.

Next is an agent harness that runs the same script against every site and scores the transcripts on memory, character consistency and image consistency, publishing the prompts and the transcripts with each run.

The boards are live at [nofilterreview.com/rankings](https://nofilterreview.com/rankings/). Adult AI sites, so consider that your content warning. Try to break the vote system, I want to know where it gives.
