Dev.to · 8 min read

Paw & Order: upload your dog, and defend them against evidence generated from their own photo

Paw & Order: upload your dog, and defend them against evidence generated from their own photo

This is a submission for the DEV Weekend Challenge: Dog Days Edition. Upload a photo of your dog. An AI accuses them of a crime. You're their defense attorney. What I Built Paw & Order is a browser game where your own dog is the defendant. You upload one photo and a few seconds later your dog has been arrested: The People vs. Biscuit Docket #PAW-042 DEFENDANT: Biscuit CHARGE: Grand Theft Sausage COUNSEL: You STATUS: Extremely suspicious Then the trial starts. The prosecutor puts a question to you, you pick a response, and the case branches from there. Three exhibits go into evidence: generated images of your dog, at the scene, with the frosting still on their muzzle. Two witnesses give statements, and at least one of them is usually lying. A trial runs a few minutes. At the end you get one of four verdicts: NOT GUILTY NOT GUILTY, BUT SUSPICIOUS GUILTY, BUT REASONABLE DOUBT GUILTY Plus a scoreline that isn't the same thing as winning: VERDICT NOT GUILTY Biscuit is free to commit additional crimes. Defense Performance: 94/100 You can lose the case and still score 96. You can win it badly. Every case has a hidden truth, generated before the trial begins. Sometimes the dog really did it, sometimes they're innocent, sometimes the evidence just lies. The client never sees any of it, so you're not hunting for a correct answer. You're building the strongest defense the facts allow. Choices decide the outcome. Replay the same case, answer differently, and the verdict and the score change with you. Demo Live: https://paw-order.pages.dev Bring a dog photo, or don't. The home page has a public docket of cases other players entered into the public record, and you can play any of them without uploading anything. Code ArjenPostma / Paw-Order dev.to weekend challenge submission Paw & Order Justice for every good boy. Upload a photo of your dog. AI generates a fictional criminal case around that dog. You defend them in court. Live: https://paw-order.pages.dev DEV Weekend Challenge: Dog Days Edition Built for the DEV Weekend Challenge: Dog Days Edition, start to finish inside the challenge window. Everything up to the last commit before the deadline of 2026-08-17 06:59 UTC is the submitted entry; anything after that timestamp is post-deadline work. Submission post: SUBMISSION.md. Local development nvm use npm ci cp packages/api/.env.example packages/api/.env # GEMINI_API_KEY is the only one needed npm run dev:api # :4270 npm run dev:app # :5173, proxies /api npm run all (format, lint, typecheck, test) must be green before a commit. View on GitHub How I Built It One rule sits under everything else: The AI creates the world. The engine runs the game. Nothing calls a model during the trial. Generation runs once, up front, and produces the whole case: crime, hidden truth, evidence, witnesses, the branching question tree, and the verdict thresholds. After that the game is deterministic code reading structured data. The generation pipeline Six model calls per case, all Gemini: Screening. Is this actually a dog, and is the frame safe to show on a public page? One gemini-3.7-flash call against the photo before any of the rest runs. Facts. Crime, hidden truth, exactly three exhibits, exactly two witnesses, a 4-6 entry timeline. Schema-constrained JSON, with the dog photo attached. Three exhibit images. gemini-3.1-flash-lite-image, rendered concurrently, each with the player's photo passed inline as a reference so the same dog turns up in every exhibit. Without that likeness you're looking at a stock dog and the joke dies. The trial tree, generated last, so it can only be built out of exhibits that already exist. Every visual claim has to be backed by the image When the prosecutor says: "Then perhaps you can explain the frosting on your client's muzzle." the exhibit had better show frosting on your dog's muzzle. Otherwise it's AI noise with a caption. So each exhibit carries visualFacts, one to four things visibly present in the rendered image, and the tree model gets those and nothing else to write from. Three levels stay separate: Level Example Narrative fact (what happened) Biscuit ate the cake Evidence fact (what can be proven) Biscuit has frosting on his muzzle Visual fact (what is actually in the picture) White frosting is visible around Biscuit's mouth The trial can cite the third column, and can't invent a fourth. Model output is untrusted input A response schema is a request, not a guarantee. Every generated payload goes through a hand-written validator before anything is stored. It checks what a JSON schema structurally can't: the trial graph is reachable from the root, acyclic, and finishable every evidence id cited by a node, a choice or the hidden truth resolves to a real exhibit no duplicate node, exhibit or witness ids score effects clamped, so one choice can't blow the scale open every string and array capped, since the case lands in one JSON column and is served under a long cache control characters, zero-widths and bidi overrides stripped from stored text, so a charge title can't reverse the text around it on someone else's home page When validation fails, the reasons go back into the retry prompt. tree node N4 points at unknown node N9 is something a model can act on, where a bare retry just rolls the dice again. One retry only. The verdict thresholds are derived, not generated Asked to set its own thresholds, the model has to guess blind, before it knows what totals its own choice effects add up to. Measured over a batch of generated cases that came out at roughly 80% acquittals, with the tainted-acquittal ending unreachable in every case sampled. So the engine works them out instead. It walks every run the finished tree can be played to and places the lines by quantile inside that real spread: the top 30% of endings acquit, the bottom 30% convict outright, the middle band convicts with reasonable doubt, and half the acquittals come out tainted. The same quantiles hold whatever numeric scale the model wrote its effects on. Two edge cases needed handling. A plain quantile over endings like 0, 0, 10, 20, 30 lands on the minimum, so every run sits above the line and the verdict below it becomes unreachable. And two independent quantiles over one list can land on the same value, emptying the middle band while both lines still look correctly placed. Both are now drawn from the values strictly above the floor. A tree where every run ends on the same doubt total is rejected: if no choice decides anything, it's a cutscene rather than a trial. The score is calculated the same way, weighted towards doubt and normalised against what that particular tree made possible. That's what lets a loss read as an excellent defense. The ceiling is usually below 100, because the run that maximises doubt and the run that argues best are rarely the same run. The hidden truth never reaches the client The api serves the case minus truth, minus each exhibit's image prompt, minus whether a witness is reliable. Those stripping functions list every field by hand instead of spreading and deleting, so a field added to the type later can't ride out onto the wire without someone deciding it should. The verdict function never reads truth at all, only player state and the derived thresholds. Everything is hostile An anonymous upload endpoint in front of image generation needs bounds: uploads capped at 20MB, one file, image mime types only, with part and field counts capped too, because busboy defaults both to Infinity per-IP rate limit keyed on the IPv6 /64 rather than the address. Keyed on the full address, one host rotates through 2^64 of them and every ceiling becomes decoration. a global daily generation ceiling, charged only once a generation is actually going to happen. It used to share a middleware with the per-IP check, which let malformed requests that generated nothing burn the day's headroom and lock real players out. bounded concurrency on the screening call, since each one in flight holds the uploaded photo and its base64 copy in memory, and it runs before the generation slot counter ever sees the request the screening gate fails open on isDog and closed on safeForPublic. A model outage shouldn't tell someone holding a real dog that it isn't a dog, but an unscreened photo must never reach the public docket. Stack Vue 3 + Vite as a static SPA on Cloudflare Pages, Express + TypeORM on Railway with Postgres, images in Cloudflare R2. The shared types and the trial engine live in one workspace package, so the api is the authority on the verdict while the app renders a score from the exact same code. 167 tests, plus a CI job that dumps the production schema into a throwaway Postgres to prove the committed migrations still describe it. Prize Categories Best use of Google AI. Gemini does four separate jobs here: gemini-3.7-flash writes the case as schema-constrained JSON, so what comes back is a structured world rather than prose the same model, with vision, gates the upload: dog or not, safe for public or not gemini-3.1-flash-lite-image renders the exhibits with the player's own photo as a reference, which keeps one specific dog recognisable across three separately generated images the same model writes the branching trial tree, constrained to the visual facts of images it has already produced Then it stops. Gemini builds the world, and the verdict belongs to code that never saw the truth. Justice for every good boy.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News