Inspiration

A returned dog isn’t a paperwork problem. It’s a dog back in a kennel who already found out what having a home felt like, waiting to see if anyone matches with it a second time, while a shelter’s behavioral team quietly starts treating it as a harder placement — when what actually failed the first time was the pairing, not the dog.

Powell et al. tracked almost 24,000 U.S. adoption records and found a 16.3% return rate. Over half of those returns break down to two causes: the dog didn’t fit what the household expected, or it didn’t fit an animal already living there. Neither one is bad luck. Somebody made that pairing, usually a coordinator working through a stack of Saturday applications, fifteen animals and forty forms deep, with no way to see the whole cohort at once.

Here’s the part that actually got us: only about one in ten owners who return a dog ever adopts again. So a bad match doesn’t cost one home. It costs the next one too, before that family even applies.

We built Kyndra to be able to look at the whole room, animals and applicants at once, and place them so the match holds past the day it’s signed.


What it does

Kyndra treats shelter placement as a two-sided stable matching problem, not a first-come, first-served queue.

An applicant who came in for a specific animal keeps that choice locked in — no derived score gets to overrule an actual human decision. Everyone else’s preferences get built from their household profile, so nobody’s sitting down to hand-rank a list of fifteen dogs.

Hard constraints come first, each one tied to a documented return cause and cited on screen, and they eliminate impossible pairs before any ranking even starts.

From there, the applicant’s want-list and the shelter’s need-list get built independently and run through Gale–Shapley deferred acceptance:

$$ \text{Applicant preferences} \neq \text{Animal preferences} $$

The result is deterministic, order-independent, and provably stable.

Every match comes with a plain-language reason and a counterfactual attached.

Pick two placed pairs and hit “attempt a swap” to watch the system try to break its own result and fail, with the reason shown live.

A long-stay equity dial lets a shelter favor animals that have waited the longest, but only as a tie-break — a property test proves it can never override a hard constraint.

And an animal nobody matched doesn’t just sit there unclaimed on the screen; it becomes a recruitment profile describing exactly the applicant this shelter is missing.


How we built it

Right now the whole thing runs in the browser: HTML, CSS, vanilla JavaScript.

The matching engine went in next to the constraint filter and the UI, all in one place, instead of waiting on a backend to stand up first — that’s what let us actually finish something in the time we had.

Animals propose in our Gale–Shapley implementation, and the two sides never see the same score.

The applicant’s household profile builds their want-list, the animal’s documented needs build the shelter’s want-list, and those stay separate on purpose — the moment you fold both into one shared compatibility number, you’ve quietly turned a matching problem back into an ordinary ranking problem, and the stability guarantee goes with it.

The evaluator that proves zero blocking pairs after a run is the same one “attempt a swap” calls live, so when a coordinator hits that button, they’re running the actual proof, not a canned animation of one.

We skipped a component library for the visual layer and built our own instead:

  • Warm terracotta accent
  • Near-black ink
  • Newsreader for headings
  • Work Sans for body copy

It reads like something a shelter would actually trust with real animal records.


Challenges we ran into

We almost shipped one shared compatibility score instead of a hard-constraint filter.

Less code, easier to demo.

What killed that plan was picturing the actual conversation it would cause: a family with a fenced yard gets told “the algorithm ranked this dog lower for you,” when the real fact is that dog needs a yard and their apartment doesn’t have one.

Ranking never should have touched that pairing.

It needed to be gone before ranking even started.

Working out which return-cause findings from the research actually justify a hard cut like that, versus which ones are real but softer, ate more of the week than implementing Gale–Shapley did — and that algorithm is well-documented enough that it was never really the hard part.

The “why not” and “attempt a swap” features had their own trap.

It would’ve been faster to fake a stability guarantee with an explanation that just sounded plausible.

We built the real evaluator instead, the same one the swap feature runs against live, because the whole point falls apart if a coordinator can click the button and watch nothing actually get checked underneath it.


Accomplishments that we’re proud of

The build runs end to end right now, intake through a completed set of stable matches, entirely in the browser.

That includes the pieces that are easiest to cut under deadline pressure:

  • The hard-constraint filter with citations attached
  • A plain-language reason on every match
  • The swap-attempt evaluator
  • An equity dial with a property test actually behind the tie-break logic

We also didn’t let the demo cohort carry a claim it can’t back up.

The animals in it are simulated, and the interface says so on screen.

What’s real is the research underneath:

  • Roughly 20 surveyed households feeding the preference model
  • A human baseline where five people placed the same cohort by hand under a two-minute timer, so there’s an actual number to compare against
  • A 500-cohort randomized run stacking greedy placement against stable matching across enough trials that one cohort can’t be quietly doing all the work

What we learned

Partway through testing we ran a plain optimizer against the same cohort, mostly to see how close it would land to the stable result.

Its average satisfaction score across the cohort actually looked better.

It also left two pairs who would rather have matched with each other than with what they got, which is exactly the failure a stable match rules out by definition.

Nobody can stand in front of a family and defend a placement like that with a good average number.

What that forced us to sit with is that a shelter isn’t handing placement decisions to something that can’t explain itself, no matter how the numbers read on a slide — which is the actual reason why-not, attempt-a-swap, and the counterfactual on every match exist.

“Trust the algorithm, it’s optimal”

was never going to be something anyone could say out loud to an adopter standing in the room.


What’s next for Kyndra

The next real step is getting this in front of an actual shelter, running its own animal and applicant data under supervision, to see where the hard-constraint set we built from the literature actually matches what coordinators run into day to day.

We’re expecting gaps.

We also want a second axis on the long-stay equity dial for behavioral-support needs, so a shelter can weight waiting time and likely post-adoption support together, with neither one ever able to override a hard constraint.

Longer term, we think the unmatched-animal recruitment profile has a life outside placement entirely — a shelter’s outreach team can use “here’s the applicant we’re actually missing” on a day when there’s no batch of applications to run against at all.

Built With

  • algorithm
  • animalwelfare
  • css3
  • explainableai
  • gale-shapley
  • html5
  • petadoption
  • petmatching
  • react
  • spa
  • stablematching
  • typescript
  • vite
  • vitest
Share this project:

Updates

Submission history