DEV Community

Arqam Waheed
Arqam Waheed Subscriber

Posted on

Terra Triage: I Built a 3-Agent Wildlife Dispatcher That Learns From Every Referral

DEV Weekend Challenge: Earth Day

This is a submission for Weekend Challenge: Earth Day Edition

TL;DR — Snap a photo of an injured animal, the right licensed rehabber gets paged in under 60 seconds. Backboard remembers every accept, decline, and "at capacity" outcome, so the next case re-ranks before it's dispatched. Memory is the product; the ranking is a pure function that cannot compute without it.

What I Built

Last spring I found a stunned songbird on the sidewalk and spent forty minutes cold-calling vets that don't take wildlife. By the time I reached an actual rehabber, the bird was gone. That's the problem I wanted to solve in a weekend.

Most dispatch apps pick the closest rehabber. Terra Triage picks the one who will actually say yes, because Backboard remembers who said no last time.

Terra Triage is a three-agent web app for people who just found an injured animal and have no idea who to call. You snap a photo, approve a single consent prompt, and under 60 seconds later a licensed wildlife rehabilitator within range has an email in their inbox with the photo, the GPS, and a one-click "accept / decline / at capacity" magic link. No account, no app, no phone tree.

The interesting part is not the first dispatch. It's the second one. Every outcome a rehabber returns (accepted, declined, at capacity, unreachable) is written back as a signal into Backboard, and the very next case reranks because of it. If Rehabber A just declined a raptor at 9:42, the 9:51 raptor won't go to them first. The memory is the product.

Finder triage card rendering species, severity, do/don't list

Three agents, one narrow job each:

Agent Job Model / Service
Finder Vision triage: species, severity 1-5, safety advice Groq Llama-4 Scout (vision), JSON mode
Dispatcher Rank rehabbers, send the email, mint magic-link Auth0 scoped agent token + Resend
Memory Read and write rehabber signals that drive the ranking Backboard (primary), Supabase mirror as fallback

Demo

Live URL: https://terra-triage.vercel.app/
60 second walkthrough:

The flow:

  1. Open the website on a phone, snap a photo of an injured animal, and approve the location prompt.
  2. The Finder agent returns a triage card with species, severity, and first-aid advice.
  3. A ranked list of nearby rehabbers appears. Each card shows the Backboard-aware score, distance, capacity, and a one-tap Call button for the listed 555-01xx number.
  4. Tap Send referral on the top pick. Auth0 asks for the referral:send scope, you consent once, and the dispatcher fires.
  5. The success pane shows "Referral sent" next to a scoped-token badge and a View captured email link.
  6. Open the captured email in /demo/inbox/<id>. Everything a real rehabber would see is there: photo, GPS, triage summary, accept and decline buttons.
  7. Click Decline, at capacity from inside that email. The magic-link records the outcome and redirects to a thank-you page.
  8. Switch to /admin. The memory timeline shows the new signal landing in Backboard, and the same case re-ranks with that rehabber demoted.

No email leaves the server during this flow. Delivery is gated behind a demo switch for this submission; why, and what the real launch path looks like, are in the sections below.

Dispatch success screen with Auth0 scoped-token badge

Admin memory signals timeline showing Backboard writes in real time


Code

Terra Triage

Snap a photo of an injured wild animal and a multi-agent system identifies the species, triages the injury, and dispatches the referral to the rehabber most likely to say yes, in under 60 seconds.

What it does

Terra Triage collapses the chaotic gap between "I just found a hurt animal" and "a trained rehabber is on the way" into a single guided 60-second flow. It pairs a Groq-powered vision Finder agent, an Auth0-scoped Dispatcher agent, and a Backboard-backed Memory agent so that every referral outcome improves the next ranking. Most dispatch apps pick the closest rehabber. Terra Triage picks the one who will actually accept, because Backboard remembers who said no last time.

Nationwide coverage is seeded (250 licensed rehabbers, 5 per US state, fictional .example.org contacts using the NANPA 555-01xx block reserved for fiction) so the ranker has something to rank from day one. Every…

Project structure (trimmed):

src/
├── app/
│   ├── report/                    # Anonymous intake (photo + geo)
│   ├── case/[id]/                 # Reporter-visible case page
│   ├── rehabber/outcome/[token]/  # Magic-link outcome form
│   ├── admin/cases/               # Ops console + memory timeline
│   └── api/
│       ├── admin/seed-demo-case/  # Idempotent demo seeder
│       └── auth/[auth0]/          # Auth0 login / callback / profile
├── lib/
│   ├── agents/
│   │   ├── finder.ts              # Groq vision call, JSON mode
│   │   ├── dispatcher.ts          # Rank + Resend + magic-link
│   │   └── rank-with-memory.ts    # Fuses memory signals into the rank
│   ├── memory/
│   │   ├── backboard.ts           # Real Backboard API client
│   │   └── index.ts               # Backboard-primary, local fallback
│   └── auth/
│       ├── agent-token.ts         # Scoped agent token (PAR or M2M)
│       └── magic-link.ts          # HMAC-signed, single-use tokens
Enter fullscreen mode Exit fullscreen mode

How I Built It

Backboard as the protagonist

Most "memory" integrations I see treat the memory service as a prompt-context bucket: fetch recent history, stuff it into the system message, let the LLM figure it out. Terra Triage does the opposite. The ranker is a pure scoring function that cannot compute without memory first — no LLM in the hot path, no prose interpretation, just signals driving weights.

// src/lib/agents/rank-with-memory.ts
export async function rankRehabbersWithMemory(
  input: CaseInput,
  rehabbers: PublicRehabber[],
): Promise<RankedRehabber[]> {
  const signals = await getMemory().query(rehabbers.map((r) => r.id));
  return rankRehabbers(input, rehabbers, signals);
}
Enter fullscreen mode Exit fullscreen mode

The scorer weights species match (0.35), distance (0.25), capacity (0.20), accept rate (0.15), and response time (0.05). Every weight except distance is sourced from Backboard. When a rehabber submits an outcome, applyOutcomeToSignals mutates the relevant keys (capacity, accept_rate, species_scope, response_ms) as a pure function and writes them back. The next ranking reflects it immediately.

Before / after ranking on the same case, after a single decline

The engineering lesson I did not expect. My first Backboard integration used semantic /memories/search once per rehabber, per case. That is correct-looking code and costs about $0.80 per triage at hackathon volumes.

Because all of our memory writes are structured and attributable to a rehabber id, the correct access pattern is a single paginated GET /memories and filter in application code. I rewrote it that way and the cost dropped roughly 800x (to fractions of a cent) with no change in ranking quality. Signals are encoded as TERRA_SIGNAL rehabber=<id> key=<k> value=<json> so the filter is trivial.

The final detail: FallbackMemory is a tiny proxy that prefers Backboard and mirrors every upsert to a local memory_entries table tagged source='backboard' | 'local_fallback'. If Backboard is down mid-demo, the app keeps working and the admin timeline shows a red chip so you can see the failover instead of it hiding behind a stack trace.

Auth0 for Agents: scoped consent for a destructive action

"Send referral" is the one button in this app that can annoy a real human being (emails a licensed rehabber). I treated it as an agent action that must be authorized, not a server-side formality.

// src/lib/auth/agent-token.ts (excerpt)
export async function getAgentToken(): Promise<AgentToken> {
  const session = await getSession();
  if (session?.tokenSet?.scope?.split(" ").includes("referral:send")) {
    return { token: session.tokenSet.accessToken, mode: "user-consented", scope: "referral:send" };
  }
  return mintM2MToken({ audience: env.AUTH0_AGENT_AUDIENCE, scope: "referral:send" });
}
Enter fullscreen mode Exit fullscreen mode

PAR is on when the tenant allows it (AUTH0_PAR=1), so the browser never sees the full authorization params, only a request_uri handle. The custom consent_context query parameter carries human-readable context ("email Marcus at Hudson Valley Raptors on your behalf") into the consent screen. If consent is unavailable, we fall back to a scoped machine-to-machine token rather than silently downgrading the action to a service call.

The UI surfaces which mode was used with an on-screen badge. The narrator can literally point at it on camera and say "scoped." That visibility is the Auth0 story for me: agents should explain themselves, not hide.

Rehabbers do not have accounts. Their outcome submission goes through an HMAC-signed, single-use, 72-hour magic link (src/lib/auth/magic-link.ts). Single-use is enforced with a conditional UPDATE ... WHERE outcome IS NULL, so concurrent submissions for the same token are atomic at the database layer.

The rest of the stack

  • Finder: Groq's meta-llama/llama-4-scout-17b-16e-instruct over the OpenAI-compatible chat/completions endpoint, with response_format: { type: "json_object" }. Sub-second vision triage. Prompt shape is inlined in the system message because Groq does not support strict JSON schemas.
  • Supabase: Postgres, RLS, private photos bucket with short-lived signed URLs. The Finder hashes the resized JPEG bytes and caches triage results, so demo retries are free.
  • Resend: transactional email, gated behind a DEMO_MODE flag for this submission (more on that below).
  • Next.js 16 (app router) + server actions, Tailwind + shadcn/ui, Leaflet for the rehabber map.

Architecture: three agents, one memory backbone

Seeding 250 rehabbers without spamming any of them

The list you see in the demo is 250 fictional licensed rehabilitators, five per US state, generated from a deterministic script (scripts/generate-rehabber-seed.ts). Every record uses real capital and largest-city coordinates so the distance math is honest, but every email ends in .example.org (reserved under RFC 2606, can never resolve) and every phone uses the NANPA 555-0100..555-0199 block reserved for fiction. Not one of those addresses can receive mail. That is deliberate.

Two switches control delivery in production:

  • DEMO_MODE=1 shorts the dispatcher before Resend is ever called. The rendered email is written to a sent_emails_log table and surfaced at /demo/inbox/<referral_id>, a server-rendered viewer behind admin basic-auth. The success pane grows a View captured email link so judges can click straight from the app into the message that would have been sent. Zero outbound traffic, real referral row, real memory signal, real magic-link outcome loop.
  • DEMO_REDIRECT_TO=you@example.com keeps Resend in the loop but rewrites every recipient to a single verified inbox and prefixes the subject [DEMO -> original@address]. Useful for recording a live walkthrough where you want a real email to arrive on your phone.

Both paths delete the referral row if the send actually fails, so the case page never shows a phantom "awaiting response" card for a message that never left the server.

What I cut, and the real path to launch

The biggest thing I cut: real rehabber contacts.

There is no global registry of licensed wildlife rehabilitators. US coverage is fragmented state-by-state, sometimes county-by-county, and most other countries (mine included) have no centralized list at all.

The tempting fix is to scrape state-agency PDFs and let an LLM parse them into rows. I refused to ship that for three reasons: (1) scraping public directories into a third-party product violates most of those agencies' terms of use, (2) the data is stale the moment you capture it (licenses lapse, phones change), and (3) language models invent plausible-looking email addresses. Sending a real referral to a hallucinated inbox is worse than returning no results.

So the 250 rows in this build are honest placeholders that exercise the ranking math without lying to anyone. Production needs a different sourcing path, and I think there are only three real options:

  1. Partner with the Animal Help Now 501(c)(3). AHN already runs a consented, maintained database of thousands of rehabbers across the US. A partnership integration (their pipeline, our ranking and memory layer) is the only path that ships real coverage without recreating two decades of stewardship work. This is what I would pursue first, post-hackathon.
  2. A self-serve rehabber portal. Licensed rehabbers sign up, verify their license number against the relevant state registry, accept a Terra Triage ToS, and opt in to receive referrals. Growth is slow but consent is unambiguous and the data stays fresh because each rehabber owns their own row. This is the right fallback if #1 does not pan out.
  3. Per-state agency MoUs. Some state wildlife agencies distribute their rehabber lists under explicit terms. Where those terms permit a downstream dispatcher, you sign a memorandum and import. Slow, jurisdiction-by-jurisdiction, but legally clean where it applies.

What will not change is the consent requirement. Regardless of sourcing path, every rehabber in the live system needs a signed agreement covering referral delivery, PII handling, license verification, and a clear opt-out before they can be ranked. That is table stakes, not a feature.

The data model for all three paths already exists in this repo (rehabbers table with active flag, species_scope, license metadata). The discovery pipeline and ToS flow are the next weekend.


Prize Categories

Primary: Best Use of Backboard. Memory drives a computed decision, not an LLM prompt. Every rank reads signals first; every outcome writes them back; the admin timeline makes the loop visible on screen. A FallbackMemory proxy keeps the app alive if Backboard is unreachable and tags the origin so failover is auditable. The cost model went from $0.80 per triage to fractions of a cent after rewriting from per-rehabber semantic search to a single filtered list read.

Secondary: Best Use of Auth0 for Agents. The Dispatcher is a first-class OAuth client scoped to referral:send, with PAR when available and an M2M fallback, and the UI labels which mode was used. Rehabbers authenticate through HMAC-signed, single-use magic links with DB-level replay protection.

Built solo in a weekend with GitHub Copilot CLI as co-author with zero paid services.

Top comments (1)

Collapse
 
miawab profile image
Ibrahim Awab

amazing! will use