AlphaRank -> Battle Arena Of Arts
This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange Live app: alpharank.art Sanity project ID: j6bntm0r (dataset: production) Test it: sign in with any Google account at alpharank.
This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange
Live app: alpharank.art
Sanity project ID:
j6bntm0r(dataset:production)Test it: sign in with any Google account at alpharank.art/submit and upload a PNG or JPG
What I built
AlphaRank is an art competition that scores itself.
You upload a PNG or JPG, a vision model looks at the piece, and it lands on a public leaderboard with country flags. There are daily, weekly and all-time boards, a score breakdown across four categories with written feedback, and a feed where I hand-pick the piece I liked most that day.
If the art has a real transparent background, it gets +10 points. So cutouts tend to climb.
Daily, weekly and all-time standings, with country flags.
Composition, colour, technique and presentation, each with written feedback.
A real transparent background earns ten bonus points.
The piece I liked most that day, written up as a post.
The part I care about explaining is how the judge knows what to look for. It isn't hardcoded. Every rule it follows is a Sanity document.
The one idea
I wanted the judge to be programmable by someone who doesn't write code.
The rulebook lives in Sanity as four
judgingCriteriadocuments. Each one has a weight, a description, a list of rules, a good example, a bad example, and per-category adjustments for things like cutouts, posters and photos.
When a submission arrives, lib/judging.ts fetches those documents with GROQ and builds the prompt from whatever is in them at that moment.
Sanity Studio you edit judgingCriteria documents
↓
lib/judging.ts GROQ fetches the rules at request time
↓
Vision model prompt assembled from those rules
↓
artwork document scores, feedback, status: scored
↓
Boards and feed daily / weekly / all time
If I wake up tomorrow and want a hundred more rules, I open the Studio, type them in, publish, and the next person who submits gets judged by the new rules. No code change and no deploy.
The judge also checks the transparency claim against the image itself. If a submission says it has a transparent background but the pixels behind the subject look like a baked white matte, Presentation gets punished. That is a rule living in content, and it is doing real work.
Demo
The video walks through the tour, then shows a judging rule being edited in the Studio and taking effect on the next submission.
Try it yourself
You don't need an account to look around. The leaderboard and feed are public and fully populated.
To submit your own piece:
- Sign in with Google at alpharank.art/submit
- Complete the short profile setup: avatar, display name, country
- Upload a PNG or JPG, add a title, pick a category, submit
Judging takes 15 to 60 seconds, and the artwork page refreshes itself while it runs.
One limit: one scored submission per account per 24 hours. If judging fails, you can try again immediately.
To see the rulebook, open the Studio at alpharank.art/studio. It needs a Sanity account with access to the project, so message me and I'll add you. Open any Judging Criteria document: that is the entire rulebook the judge reads, and editing it changes the next judgement.
The repository is private at the moment. I'm happy to open it for the Sanity team or anyone reviewing this submission, just ask. If it gets opened, start with schemaTypes/, lib/judging.ts and sanity/lib/queries.ts.
How I built it
I built this in Freebuff, the free coding agent from Codebuff. It runs in the terminal and lets you pick the model behind it, so I moved between a few: GLM 5.3 Flash for most everyday work, and DeepSeek V4.1 for the stubborn debugging. The whole thing cost nothing to run, which is part of why I kept going.
AlphaRank was my second idea. I first built a car version with a hard PNG-only rule, then pivoted to all kinds of art once I realised the interesting mechanic was transparency, not cars.
The stack is Next.js 16 and Sanity v6 on Vercel, with no other database.
A few prompts earned their keep:
"Build every screen with mock data first, we will wire it up later."
I had the whole thing on screen in a day and iterated on the design before any real data existed.
"Reads must work on an empty dataset."
Every page got a proper empty state, so a judge opening a fresh clone still sees something sensible.
"Verify every change with tsc and eslint before you tell me it's done."
Caught a lot.
What went wrong
A prompt that sounded responsible and taught me nothing.
I told the agent: "Keep the public projections in one file and never project the email field." It made the queries tidy and hid the actual leak, because a projection was never what protected the email. More on that below.
maxDurationin a server action file.A
'use server'module can only export async functions, so that one line made the whole module export nothing and the production build failed. Route segment config belongs on the page.An email as a document ID.
The daily counter used the account email as its Sanity
_id. IDs can't contain@, so the write failed on every submission from day one. A generic catch block made it look like a flaky network for hours.Judging inside the submit action.
Uploading the file and calling the vision model in one request blew Vercel's 60-second function limit, and the model call never fired. I split judging into its own action and sent a downsized Sanity CDN URL instead of a base64 data URI.
A transparency check that proves less than it sounds.
It reads the PNG colour type byte, which proves the file has an alpha channel, not that any pixel is transparent. A file with alpha and a solid white background passes. I left it, because the judge catches that case in Presentation, and the pair is honest about what is verified and what is judged.
Time spent on things that didn't matter.
I hand-wrote a portable text renderer instead of installing the official package, since I only needed paragraphs, headings, quotes and bullets.
The privacy bug I shipped
This is the mistake I'm most glad I caught.
Early on I stored artistEmail on each artwork document and told myself it was safe because none of my GROQ queries projected it. That was false comfort. The dataset has a public read role, so anyone can run this with no token and no projection:
*[_type == "artwork"]
GROQ returns the whole document, every field, including the one I'd promised myself to keep out. Five email addresses were sitting there.
A projection in your own queries is a habit, not a security control.
I found it while putting this post together. I ran the public query without a projection, the way a judge would, just to check the link worked, and the emails came back in the JSON.
The fix was to stop storing the address at all, instead of trying to filter it. lib/account.ts now derives a peppered SHA-256 hash from the session email, using AUTH_SECRET as the pepper, and that hash (accountId) is the only identity that reaches the Content Lake. The rate limit only needs "has this account submitted today", so a one-way hash is the whole requirement, and the pepper stops a leaked digest from being checked against a list of candidate emails.
A one-time migration unset the old field on existing documents, and I re-ran the public query to confirm no email appears anywhere.
Past the Studio
I did not build an App SDK app, and I did not use Sanity Workflows.
What I did instead was model the judging process as data next to the content. Every submission carries a status that moves through pending, scoring, scored and rejected. Boards only read scored documents, so the process is legible in the dataset and visible in the Studio instead of hidden inside a function.
That is the same instinct as the Workflows bonus, but it is not the Workflows product, and I don't want to overclaim it.
The feed is also a custom surface. The public site reads it with GROQ and resolves linked artwork through coalesce, so a post can feature a real submission without copying any of its data.
Studio customization. The Studio is embedded in the site at /studio with a custom structure: Artwork, Judging Criteria, Feed Post and the submission counter. Fields carry plain-language descriptions, and the linked artwork picker previews the thumbnail, title and artist. Publishing a feed post updates /feed with no code change. I kept to schema-level customization and did not build custom input components or document actions.

Linking the artwork in the Studio

The published post, with artist, flag and score filled in automatically.
The Sanity project
Project ID: j6bntm0r | Dataset: production
Public dataset query, no token needed:
https://j6bntm0r.api.sanity.io/v2025-01-01/data/query/production?query=*%5B_type%20%3D%3D%20%22artwork%22%5D
The public read role exposes artwork and feedPost. judgingCriteria and artworkSubmissions are filtered out and need the read token, which lives in the server env and never reaches the client. No email address is stored on any publicly readable document, so the raw response is safe to browse.
There are four schemas:
judgingCriteriais the rulebook that gets injected into the AI prompt, one per judging category. Fields:title,description,weight,rules[],goodExample,badExample,categoryModifiers[].
artworkis a submission and its verdict. Fields:artistName,accountId,title,category,image,countryCode,hasTransparency,transparencyBonus, the four category scores, per-category feedback, andstatus.
feedPostis the hand-written editorial layer. Fields:title,slug,publishedAt,eyebrow,featured,excerpt,tags[], a portable textbody, and an optional reference to anartwork. If a post links an artwork, the hero image, artist, country and score resolve from it. If it doesn't, the post supplies its own.
artworkSubmissionsis a per-account counter keyed byaccountId, so the daily cap is a document rather than a cron job.
A few decisions behind them:
transparencyBonusis written by the server at submit time and never accepted from the client.The feed's
coalescelets an editorial post spotlight a live submission and stay correct as its score changes.
statusis the workflow. It's the only field the board queries filter on, so a stuck submission can never leak into a ranking.
Agent sessions
There is no single session to share, and I'd rather explain why than leave it vague.
I worked in Freebuff with separate sessions per area (schema, judging, design iterations, production debugging) and switched models between them. Stitching those into one transcript would fabricate a session that never existed. Freebuff also isn't one of the tools the Agent Sessions uploader accepts, so I can't embed one the way the challenge suggests.
I can share individual transcripts. These three are the most interesting:
-
The judging timeout: working out that one action did an upload plus a vision call, splitting them, and shrinking the payload to a CDN URL. -
The privacy fix: realising the public read role exposed every field, writing the peppered hash, and migrating the data. -
The document ID bug: an email used as an_id, which failed on every submission because IDs can't contain@.
Say the word and I'll send any of them over.
Closing
I set myself one rule for this project:
If something about the app needed a code change to be adjustable, it was in the wrong place.
The judging criteria and the feed both moved into Sanity for exactly that reason, and both can now be changed by me at midnight without touching the repo.
Upload something at alpharank.art and see what score the judge gives it.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.