Scout — Full Product Context → feature documentation

Patch rarity

Every Scout patch carries an authored rarity tier — Common, Rare, Epic or Legendary — describing how hard the place is to physically reach.

Summary

Every real-place Scout patch carries one of four authored tiers — Common, Rare, Epic, Legendary — describing how hard the place is to physically reach.

The tier is Scout's own editorial judgement, not a measured statistic. It is not how many people have collected the patch, not how famous the place is, not how good it is, and not how nice the art is.

THE CRITERIA

These are the rules. Everything else in the rarity system — the judging rubric the tiering agents are given, the admin dropdown, the badge — derives from this section. If a tier and this section disagree, this section wins and the tier is a bug.

The question

What is the single hardest thing standing between an average traveler and standing on that spot?

Not the last mile. Not the total distance. The one obstacle you cannot avoid.

The four tiers

Tier The obstacle Canonical example
Common None. It is on the drivable road network, you can go today, and you book nothing. St. Louis · Gateway Arch · Yellowstone
Rare One real obstacle. A mandatory flight to somewhere not road-connected; a scheduled ferry; a seasonal-only road; a long unpaved drive; a serious hike; or a reservation that sells out. Isle Royale (ferry) · Denali (you cannot drive to Alaska in a day) · Alcatraz (ferry, books out)
Epic An obstacle with no scheduled service. A chartered boat or plane, or a permit plus genuine backcountry effort. Dry Tortugas (boat or seaplane) · Glacier Bay · Buck Island Reef
Legendary No scheduled service exists at all. Bush plane, air taxi, or an expedition. Kobuk Valley (bush plane) · Gates of the Arctic · Noatak

Rules that follow from the question

  1. A long drive is not an obstacle — but isolation is. Distance alone never lifts a tier. What lifts it is a trip an ordinary person would call a hassle rather than a drive: hours from any city of size, no real services near the destination, and planning needed around fuel, water or daylight. The test: is this somewhere you just go, or somewhere you organise?

    • Yellowstone is Common. Long drive, but there are gateway towns with fuel and beds at the entrances, and four million people a year simply turn up.
    • Big Bend is Rare. Paved the whole way, and still five hours from any city, with almost nothing between. You plan that trip.

    The remoteness number is actively misleading here and must be ignored. Big Bend reads 18.6 mi from the nearest populated place; Yellowstone reads 30.2. The gazetteer counts Study Butte the same way it counts Gardiner. Judge the character of what is nearby — does it have fuel and beds — not the mileage.

  2. A mandatory flight IS an obstacle. Anywhere not connected to the contiguous road network — Hawaii, the territories — is at least Rare, because no amount of driving gets you there. (Alaska is road-connected via the Alaska Highway, but only as a multi-day international drive, which is itself the obstacle. Alaska is at least Rare.)

  3. Water without a bridge is an obstacle. An island reached by scheduled ferry is Rare no matter how close it is to a city. Alcatraz sits 4.2 miles from downtown San Francisco and is Rare, for Isle Royale's exact reason.

  4. Scheduled beats chartered. If you can book a seat on something that runs to a timetable — a ferry, an airline, a concession boat — that is Rare. If you must charter the whole vessel or aircraft, that is Epic. If nothing scheduled serves the place at all, that is Legendary.

  5. Count the hardest obstacle, not how many there are. Two Rare obstacles do not make an Epic. A place needing a flight and a ferry is still Rare.

  6. Judge the place, not the visitor. A Fairbanks local finds Denali easy; the tier is not relative to where anyone lives. Use the average American traveler.

  7. Judge the patch's boundary, not its deepest corner. You earn a patch by standing anywhere inside its geofence. Denali's park road past mile 15 needs a shuttle, but the entrance does not — and the entrance is inside the boundary. Tier the easiest legitimate way to earn it.

  8. Reputation is not an obstacle. Fame, beauty, danger, and "bucket-list" marketing copy in a description are all irrelevant. Ask only about access.

Worked precedents

These are settled. Any new judgement should be consistent with them.

Place Tier Why
Yellowstone Common Gateway towns at five entrances; you just turn up
Big Bend Rare Paved all the way, and still five hours from any city with nothing in between
Great Basin Rare Four hours from Salt Lake or Vegas, and Baker is a hamlet
Rogers Centre Common Downtown Toronto — the 32-mile remoteness reading is a gazetteer artifact
Shoshone National Forest Common The 32-mile reading is its centroid; trailheads are on the highway
Hoover Dam Common Park in the garage off US 93
Denali National Park Rare Reaching Alaska at all is the obstacle; the entrance itself is drive-up
Haleakalā Rare Paved road to the summit, but Maui needs a flight
Guam · Saipan Rare Ocean flight mandatory, then you drive
Alcatraz Island Rare Ferry only, and it sells out
Cumberland Island Rare Passenger ferry from St. Marys
Grand Canyon-Parashant Rare No paved roads; high-clearance 4WD and your own water
Death Valley Common Two hours from Las Vegas, with fuel and lodging inside the park
Baxter State Park Rare Gravel roads, day-use reservation, gates fill by dawn
American Samoa Epic Twice-weekly flight from Honolulu, and the Ofu unit needs a small plane
Dry Tortugas Epic Boat or seaplane from Key West
Glacier Bay Epic No road in; fly to Gustavus or arrive by ship
Kobuk Valley Legendary No roads, no scheduled service, bush plane
Aniakchak Legendary Floatplane, weather permitting; among the least-visited units in the system

Expected shape of the catalog

Revised 2026-09-24 against the 204-patch pilot; the earlier estimate was written before these criteria were tightened and predicted far more Rare than they produce.

Projected over 1,989 real-place patches: roughly 10–15 Legendary, 5–10 Epic, 150–250 Rare, and ~1,700 Common (≈85%).

The basis, measured rather than guessed:

Patches by remoteness rank Legendary Epic Rare Common
1–100 (the most remote in the catalog) 11 5 17 67
101–200 0 0 10 89

Remoteness stops predicting difficulty past about 35 miles. Every Legendary and every Epic in the catalog is in the top 100, because those tiers require the absence of scheduled service — which in practice means roadless Alaska and a handful of islands. Below that, tiers are driven entirely by mode (a ferry, a mandatory flight, a seasonal closure, a permit), not by distance, and the great majority of American places are simply drive-up.

A Common-heavy catalog is the correct outcome, not a failure. Most places really are easy to reach, and a ladder where a third of the rungs are "hard" would not be believed. The tiers earn their meaning from being rare.

This is a calibration guide, not a quota. If a batch comes back far outside it, the bar has moved — say so and recalibrate, rather than forcing patches up or down a tier to hit a number.

When in doubt

Return no tier. A missing tier renders no badge and is a safe, deliberate state. A wrong Legendary is not safe — it is the one claim a user will challenge.

Status

Built and complete, but not yet live on prod. scout_prod carries zero rows for rarity/rarity_note/rarity_source and has never run the migration that adds them — the tier data exists only in local scout_dev databases so far. Publishing content to prod is the content owner's job, not part of shipping the code (see CLAUDE.md § Content publishing) — once the code merges, the backend deploys, and the patches are published, this feature goes live with no further code change.

No feature flag. rarity is a nullable column (backend/prisma/schema.prisma:154), and NULL renders no badge on any surface — that nullability is the rollout control in place of a flag, which is what let the whole 1,989-patch real-place catalog be tiered in one pass without gating anything behind a toggle.

All 1,989 real-place patches in the local catalog are tiered:

Tier Count Share
Common 1,888 94.9%
Rare 83 4.2%
Epic 7 0.4%
Legendary 11 0.6%

All 11 Legendary patches are in Alaska. The other 172 rows in the catalog (2,161 total patches) are store-only merch — Animals, Japan, Dinosaurs and similar product-only sets, collectionType: null at coordinates exactly (0,0) — and carry no tier; see Edge cases.

User-facing surfaces

Surface Shows Common? File
Patch detail hero (Overview tab) Yes mobile/src/components/patch-detail-v2/tabs/OverviewTabV2.tsx:45-48 — RarityBadge at size="hero", gated only on patch.rarity !== null
Unlock celebration (app/celebration.tsx, CelebrationStage) No — rare+ only mobile/app/celebration.tsx:548,751-756 — showRarityBadge = patchRarity !== null && patchRarity !== 'common'
Onboarding first-unlock reveal (PatchUnlockReveal, mounted only by app/onboarding-reveal.tsx) No — rare+ only, shown for the rarest patch in the unlock batch mobile/src/components/celebration/PatchUnlockReveal.tsx:177-179,310-312
Browse grid card pip (PatchCard.tsx) No — rare+ only mobile/src/components/patch-browser-v2/PatchCard.tsx:43,72-73 — top-left corner, pointerEvents="none"
Campaign-collection stop grid card pip (art view) No — rare+ only mobile/src/components/campaign/CampaignCollectionScreen.tsx:84,523 — same PatchCard as the browse grid, fed by CampaignCollectionPatch.rarity
Trophy Case badge pip No — rare+ only mobile/app/collected-patches.tsx:262-264
Profile "Your rarest" card Yes, plus the full tier-count breakdown mobile/app/(drawer)/profile.tsx:363-416 (viewModel), :799-834 (render)
Trophy Case "Your rarest" card Yes, plus the full tier-count breakdown mobile/app/collected-patches.tsx:376-416 (viewModel), :480-495 (render)
Admin patch editor — Rarity panel (select, note, source pill, Confirm button) n/a (internal) backend/admin-ui/src/pages/PatchDetailPage.tsx:1376-1439
Admin patches list — Rarity column + filter (the review queue) n/a (internal) backend/admin-ui/src/pages/PatchesPage.tsx:736-761,1139,1168-1171,1267-1290

No surface has its own scout:// deep link — the badge is an embellishment on existing routes (patch-modal/<id>, celebration, onboarding-reveal, collected-patches, profile), so nothing new was added to mobile/src/dev/deepLinkRoutes.ts.

How it works

Authoring (no server-side computation). No rule or model assigns a tier — every one is judged by a human or an LLM agent reading the place against the criteria above. backend/scripts/enrichment/rarity.ts builds a RarityBatchRecord per unjudged patch (buildRarityBatchRecord, lines 32-53) carrying name, type, city/state, description excerpt, coordinates, and nearestTownMi (from backend/src/locations/remoteness.ts's nearestPopulatedPlaceMi, which returns null — never Infinity — outside the US or over open ocean, so an unmeasurable distance can't be misread as "remote"). rarity-batch/rarity-apply (backend/scripts/patch-enrich.ts, wired at lines 2034-2049) emit a batch most-remote-first and write back verdicts; parseRarityVerdicts rejects any row with an unknown tier by name rather than writing it (enrichment/rarity.ts:86-89), and cmdRarityApply refuses to apply the whole file at all if parsing produced any errors (enrichment/rarity.ts:168-173). Per-row, it also never overwrites a patch whose raritySource is already 'confirmed' — a confirmed tier is a human decision that nothing automated overrides (enrichment/rarity.ts:186-191). The one-time 1,989-patch pass used 18 independent judging agents plus a 13-anchor gate that rejects a whole batch on one wrong anchor answer — mechanics documented in docs/superpowers/plans/rarity-audit/, not part of the shipped runtime.

New patches get judged too. enrich-full (backend/scripts/patch-enrich.ts:1541-1599) emits a patch's RarityBatchRecord whenever its rarity is still null, so a patch created after the initial audit doesn't ship un-judged forever — the verdict is written back through rarity-apply, landing raritySource: 'suggested' for a human to confirm later. verifyOne reports a missing rarity as a WARNING, never a hard error (patch-enrich.ts:1719-1724) — null is a deliberate, safe state, not a defect.

Delivery. mapSyncPatch (backend/src/sync/sync.service.ts:110-114) copies rarity/rarity_note onto every synced patch and deliberately omits rarity_source — authoring bookkeeping the app has no use for. On the client, transformPatch (mobile/src/domain/store.ts:337-338) re-validates the tier through isPatchRarity before trusting it, so an unrecognized string from a newer backend renders null (no badge) instead of crashing or falling back to a default.

Rendering. RarityBadge (mobile/src/components/rarity/RarityBadge.tsx) draws 1–4 amber diamond pips (rarityPips, one per tier step) plus the tier word, and at size="hero" the note — it never renders a default, so every caller gates on patch.rarity !== null itself. buildRarityBreakdown (mobile/src/domain/rarityBreakdown.ts:29-49) walks a batch of patches, returning zero-filled per-tier counts and the single rarest patch held (by rarityRank), explicitly skipping any patch whose rarity is null rather than counting it as Common — used by the celebration/reveal surfaces (rarest of a nested unlock) and by the profile/Trophy Case breakdown (rarest of the whole collection).

Admin editing. The Rarity panel's plain Save button posts buildPatchSavePayload (backend/admin-ui/src/lib/patchRarity.ts:90-…), which never includes a raritySource key — so an unrelated field edit can never silently flip a confirmed tier back to suggested. Only the dedicated "Confirm this tier" button sends buildConfirmRarityPayload (patchRarity.ts:49-55), which always sets raritySource: 'confirmed'. On the server, UpdatePatchDto validates rarity against the four tiers plus null, rarityNote against a 120-char bound, and raritySource against the two source values (backend/src/admin/dto/index.ts:323-352) — raritySource uses @ValidateIf, not @IsOptional(), so an explicit raritySource: null is rejected with a 400 instead of reaching Prisma as a NOT NULL violation; the controller (backend/src/admin/api/patches-api.controller.ts:432-448) spreads only the DTO keys actually present in the request body into the Prisma data object, which is the mechanism that lets a save that never mentions rarity leave a confirmed row untouched. The patches list's Rarity filter (backend/admin-ui/src/pages/PatchesPage.tsx:445-451) — All / Not judged / Common / Rare / Epic / Legendary / Suggested only — is the review queue: "Suggested only" surfaces every auto-judged tier still waiting on a human to confirm it.

CreatePatchDto (backend/src/admin/dto/index.ts:215-230) also validates rarity/rarityNote — a new patch's Rarity panel value used to be silently stripped by the global ValidationPipe's whitelist: true (the DTO had no rarity fields at all) and never reached the row. It deliberately has no raritySource field: a created patch always starts at the column default ('suggested'), and only the dedicated Confirm action may ever stamp 'confirmed'. The create handler (backend/src/admin/api/patches-api.controller.ts:368-426) passes dto.rarity/dto.rarityNote straight into the same explicit Prisma data object as its other authored fields.

Data model

backend/prisma/schema.prisma, on Patch (table patches):

Migration: backend/prisma/migrations/20260923120000_patch_rarity/migration.sql.

Content-publish classification. All three columns are publishedColumns on the patch content-publish entity (backend/src/content-publish/content-entities.ts:286-291) — authored content, published from local to prod exactly like short_description. rarity_source publishes too, deliberately: a tier confirmed locally must stay confirmed on prod, the same way wikipedia_url_manually_set travels with the URL it describes, even though the field itself never reaches the mobile app.

Vocabulary is hand-mirrored, not shared. This repository is not a monorepo, so the four-tier vocabulary exists as two independent copies with identical shape: backend/src/patches/rarity.ts (PATCH_RARITIES, isPatchRarity, rarityRank) and mobile/src/domain/rarity.ts (the same three exports, plus display-only rarityLabel/rarityPips).

API surface

Endpoint Auth Notes
GET /api/sync/content None (optional bearer unlocks admin-only rows) Every patch object carries rarity/rarity_note; rarity_source is never included (sync.service.ts:110-114)
GET /api/sync/patch/:id None Same shape, single patch — deep-link fallback
PATCH /api/admin/patches/:id AdminGuard + ContentWriteGuard Accepts rarity/rarityNote/raritySource; 400 on an unknown tier/source or an over-length note (backend/src/admin/dto/index.ts:323-352, backend/src/admin/api/patches-api.controller.ts:432-448). Read-only in prod like every content-write route (see admin-and-content-ops)
POST /api/admin/patches AdminGuard + ContentWriteGuard Accepts rarity/rarityNote (no raritySource — a created patch always starts 'suggested') (backend/src/admin/dto/index.ts:215-230, backend/src/admin/api/patches-api.controller.ts:368-426). Read-only in prod like every content-write route

Key files

Vocabulary and judging

Delivery

Rendering

Admin

Configuration and flags

Edge cases and known limits

Common shows on two surfaces and is suppressed on two others — deliberately, not inconsistently. This is the single most important thing to know before "fixing" it:

Surface Common shown? Why
Patch detail hero Shows You opened this one specific place; "Common · Drive in, park downtown." is useful information about it
Unlock celebration + onboarding reveal Suppressed A moment, one patch (or the rarest of a batch) per modal — at 94.9% Common, badging every ordinary unlock would fire on ~19 out of 20
Grid cards + Trophy Case badges Suppressed 1,888 cards all saying "unremarkable" is noise; a pip means "this one is special," and showing it on almost every card would defeat that
Profile + Trophy Case breakdown Shows A summary of a whole collection; an honestly-Common "your rarest" is real information, not noise

The rule: show Common when the user is looking at one specific patch or asking for statistics; suppress it when the badge is competing for attention against many other things on screen. The justification is the measured 94.9% share above — a number the code itself does not contain — so a future edit that "fixes" this asymmetry without re-deriving that number would be undoing a deliberate decision.

What this feature does NOT do

Tests that cover it

Backend

Mobile

End-to-end