DEV Community

Cover image for Fetch'd: It's Tinder, except the dog can reject you too
krishaant S.H
krishaant S.H

Posted on

Fetch'd: It's Tinder, except the dog can reject you too

DEV Weekend Challenge: Dog Days Edition Submission 🐕

This is a submission for Weekend Challenge: Dog Days Edition

What I Built

Somewhere between 7% and 20% of adopted shelter dogs get returned and often fast: one South Carolina study found returns within three months had a median ownership length of just eight days (source) . Behavior problems are the single biggest driver (36.1% of returns), followed by incompatibility with existing pets (18.3%) (source) . The other major factor isn't the dog at all it's mismatched expectations: adopters who returned dogs within three months went in with significantly higher expectations than those who didn't, and two-thirds hit behavioral problems anyway. Worse, a return lowers a dog's odds of being adopted again a bad match doesn't just fail once, it makes the dog harder to place a second time.

The actual product to build, then, isn't "help people find a cute dog faster." It's close the gap between what an adopter expects and what a specific dog is really like to live with before the return, not after.

Fetch'd is a two-sided swipe app: dogs have profiles (filled in by shelter staff), adopters have profiles, and either side can browse and swipe on the other. A match only forms on a mutual right-swipe the dog's side has to say yes too, not just the adopter. That reflects the core belief behind the product: an algorithm has no business being the final judge of who takes a dog home. It can't foresee a personality click at a first meeting or a kid a dog takes to instantly the hundred small variables no intake form could ever capture. So Fetch'd never renders a verdict. Every note is a specific, checkable observation, and the real decision stays with the humans holding the leash. Structurally, that means dogs and adopters are symmetric entities in the schema each with their own swipe rows, not a "users" table and a "listings" table wearing a trenchcoat.

Every profile card also carries a one-sentence, Gemini-generated compatibility note about the other party grounded, specific, no score. More on that below, since it's most of what I actually spent the weekend building.

How I Built It

Everything holding this up is intentionally plain: React + Vite + TypeScript, Supabase for Postgres/auth/storage with row-level security scoping every table to its owning user_id, no framework beyond that. Auth is username/password with no real email involved — a chosen username maps to a synthetic username@fetchd.local address handed to Supabase's normal email/password auth, so sign-up is "pick a username," not "verify your inbox." That's a scope call I'll say plainly rather than bury: there's no password-reset flow, because there's no real inbox to send a reset link to, and no OAuth. Both are exactly what a fuller version needs before this could be a real shelter's system of record, along with things like photo moderation and multi-user shelter accounts. For a weekend, the time went into the two-sided data model and the compatibility-note grounding instead — the parts that make the core idea actually work, not just look like it does on a landing page.

Here's how that stack fits together:

Architecture diagram: browser client, Supabase, and Gemini API

And the same thing broken down to the actual function and table names, if you want the call trace instead of the shape:

┌─ Browser  (React + Vite + TS, no backend server) ─────────────────────────┐
│                                                                           │
│ auth.ts        username -> username@fetchd.local, handed to Supabase Auth │
│ CreateProfile  4 photos -> Storage . answers/intake -> Postgres           │
│                intake sheet photo -> base64  ----------------+            │
│ SwipeDeck      combineAnswers() -> labeled text  -----------+ |           │
│                swipe direction -> Postgres                  | |           │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘
            |                                          | |
            v                                          | |
┌─ Supabase ──────────────────────────────────────────────────────┐
│                                                                 │
│ Auth       email/password under the hood                        │
│            (no real inbox => no password-reset flow, by design) │
│ Postgres   dogs . adopters . swipes . matches . messages        │
│            RLS: every row scoped to auth.uid()                  │
│ Storage    photos bucket, public read / authenticated write     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                                                        | |
                                                        v v
┌─ Gemini API  (gemini-flash-latest, called via fetch() directly from the browser) ─┐
│                                                                                   │
│ generateCompatibilityNote(viewerText, targetText)                                 │
│    one grounded sentence per profile card                                         │
│    rules: grounded only in stated facts . no score or verdict .                   │
│           ask a question on a gap instead of guessing .                           │
│           exact fallback string when neither side has enough detail               │
│                                                                                   │
│ extractIntakeDocument(base64, mimeType)                                           │
│    vision extraction from a photographed/scanned vet intake sheet                 │
│    fixed JSON shape, null for anything not literally on the page                  │
│    -> reviewed by shelter staff in IntakeReviewForm before saving                 │
│                                                                                   │
│                                                                                   │
│                                                                                   │
│                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

There are two separate Gemini calls in Fetch'd, and they're deliberately a pipeline, not a grab-bag of "AI features": one turns a photo into structured facts, the other turns two sets of structured facts into one checkable sentence.

Pipeline diagram: vision extraction from intake sheet feeding into grounded compatibility reasoning

Call 1 — vision extraction from the intake sheet. Shelters already have vet intake paperwork for every dog. extractIntakeDocument sends a photographed or scanned sheet straight to Gemini with responseMimeType: 'application/json' and a fixed target shape — weight, spay/neuter status, diagnosed conditions, behavior observations, and a dozen other fields. The rule baked into that prompt is the same grounding discipline as the second call: "Never guess or infer a value that is not literally present on the page," with null (or an empty array) as the required output for anything the document doesn't actually say. The extraction lands in an editable review form before anything touches the database — a shelter worker corrects the model instead of trusting it blind. That matters more than it sounds like it should: this is the data that later gets compared against an adopter's answers, so a hallucinated "high energy" on a couch-potato senior dog doesn't just look wrong, it produces a wrong compatibility note downstream.

Call 2 — the compatibility note, and the design bet underneath it. Once an adopter's five structured answers and a dog's reviewed intake fields both exist, combineAnswers() formats each into labeled text — "Home & nearby space: house with a small yard. Hours alone on a typical day: 3-4." for the adopter, a plain-language intake summary for the dog — and both get handed to generateCompatibilityNote. That prompt runs on four hard rules, not four suggestions:

  • Grounded only in stated facts. Never invent a trait, preference, or fact not literally present in either text.
  • No verdict, no score. No "great match," no percentage. The note states a connection; the reader draws the conclusion.
  • Ask instead of guessing. If one profile mentions something specific — noise sensitivity, exercise needs — and the other side hasn't said enough to compare against it, the model turns that gap into a direct question back to the viewer, not a filled-in assumption.
  • A required, exact fallback. If neither profile has enough detail to support a real observation, the output must be exactly "Not enough detail yet to spot a specific connection." No generic filler standing in for a real one.

Those rules are backed by real worked examples inside the prompt itself, not just described to the model in the abstract — this is the actual few-shot pair shipped in generateCompatibilityNote, reformatted here from single-line prompt strings for readability, wording otherwise unchanged:

VIEWER'S PROFILE: "I work from home and live in a quiet apartment with no other pets."
PROFILE THEY ARE VIEWING: "Rex is a 4-year-old lab mix, great with people, gets anxious
around loud noises and other dogs."
Output: Rex's noise-sensitivity could work well with your quiet apartment, though it's
worth confirming he'd be comfortable being home during your work calls.
Enter fullscreen mode Exit fullscreen mode
VIEWER'S PROFILE: "I live in a house with a small backyard."
PROFILE THEY ARE VIEWING: "Rex is a high-energy 2-year-old who needs at least an hour
of exercise a day."
Output: Rex needs at least an hour of daily exercise - how much active time could you
realistically give him each day?
Enter fullscreen mode Exit fullscreen mode

Two flavors of the same rule set: one produces a flattering-but-specific observation, the other produces a direct question instead of a soft "might not be right for you." That "no score" bet wasn't arbitrary — a percentage invites exactly the kind of blind trust the grounding rules are designed to avoid. Fetch'd skips the score entirely and ships only the explanation. The re-readable, checkable sentence is the feature — not a caption under a number.

Discover new users

Demo

website link: https://fetchd-theta.vercel.app/login
Want to try the live app yourself instead of just watching? Two seeded test accounts, one on each side, so you can see both a dog's view of adopters and an adopter's view of dogs:

Role Username Password
Dog test_dog 123456
Adopter test_adopter 123456

Heads up: this runs on the Gemini API free tier, which caps daily requests. If you swipe through a lot of profiles or try the vet-intake OCR upload after the day's quota is spent, the compatibility note or the intake extraction may silently fall back / not populate that's a quota limit, not a bug.

Code

Repo: [https://github.com/Krishaant003/fetchd]

What Where
Compatibility-note prompt src/lib/gemini.tsgenerateCompatibilityNote
Vet-intake vision extractor src/lib/gemini.tsextractIntakeDocument
Two-sided schema + RLS supabase/schema.sql
Structured-answer formatting src/types.tscombineAnswers
Swipe / mutual-match logic src/pages/SwipeDeck.tsx, src/lib/api/swipes.ts

Prize Categories

Submitting to Best Use of Google AI. Both Gemini calls described above are load-bearing, not decorative: extractIntakeDocument is the vision step that turns a photographed vet intake sheet into structured data, and generateCompatibilityNote is the grounded reasoning step that turns two profiles into a single checkable sentence the actual core mechanic the whole app is built around.

Top comments (0)