TAG IDENTITY & RECOGNITION PROBLEM — DISCUSSION NOTES / PLAN
==============================================================
Status: DISCUSSION ONLY — nothing in this doc has been built yet.
Purpose: capture the reasoning so we can pick this back up later
         without re-deriving it from scratch.


1. THE PROBLEM WE STARTED FROM
-------------------------------
Every vibe/post shows the author's `public_tag_id` — a system-generated
random string (e.g. "TAGA1B2C3D4E5"), produced by Util::publicTagId()
at signup ('TAG' + 10 random hex chars), checked for uniqueness, and
never changed again.

Issue: nobody can remember, say out loud, or search for a random hex
string. This is bad for creators who want to be recognized, and bad
for viewers trying to find someone again.


2. OPTIONS WE CONSIDERED AND REJECTED
---------------------------------------
a) Just show `full_name` instead of/alongside public_tag_id.
   Rejected — creates two different identity systems (tag in some
   places, name in others), undermines "why we call it tag" — the
   product's whole identity concept is the tag.

b) Let users freely choose their own public_tag_id at signup.
   Rejected — "another Reddit." Turns the tag into a generic chosen
   username, killing the idea that a tag is something you're GIVEN,
   not something you CRAFT. Also directly conflicts with a planned
   monetization feature (see section 5).

c) Auto-generate a memorable-but-still-random tag (word+number instead
   of hex).
   Not pursued further once the earned/opt-in model (below) came
   together — parked, not needed for now.


3. RESEARCH THAT INFORMED THE DIRECTION
------------------------------------------
- Discord: replaced random #discriminator numbers after finding ~40%
  of users didn't remember their own number. Their fix: permanent
  unique username (chosen) + freely-changeable display name. Lesson —
  an assigned-random identifier fails for the WHOLE user base, not
  just top creators. (This is why the search/findability problem,
  section 7, is treated as a real, separate issue — not just a
  "recognition for famous people" nice-to-have.)

- Pseudonymity research: reputation can be built under a persistent
  but unmemorable identifier (e.g. "Satoshi Nakamoto") because
  recognition attaches to consistent appearance (avatar, content
  history) as much as to a sayable name. Recognition and memorability
  are two separate problems.

- Verified/earned-badge systems (TikTok, YouTube, Twitch): gating
  extra visibility/status behind proven traction is standard,
  validated practice (TikTok verified creators reportedly get ~34%
  more views on average). Confirms the earned-Spotlight mechanic
  (section 4) isn't a novel risk.

- WhatsApp: has NO user directory/search — you must already know the
  exact username. A deliberate, privacy-first trade-off for a private
  messaging app. TAG's vibes/public-post feeds are a DISCOVERY
  surface, not a private messenger, so the same trade-off is a real
  gap for us, not an intentional design choice.

- Fuzzy/typo-tolerant search (Levenshtein/pg_trgm etc.) only helps
  when the underlying text is a real, guessable word. It cannot save
  a random hex tag — there's no "close guess" for it. Confirms: no
  algorithm fixes a missing input; the fix has to be a data change,
  not a smarter search.


4. THE SYSTEM WE CONVERGED ON
--------------------------------
Four separate pieces, each solving exactly one job. None of them
substitute for each other.

  1. TAG ID
     - Unchanged. Random, permanent, backend-only. Used for DMs,
       follows, blocking, exact-match lookups. Nobody is meant to
       casually see or care about this string.

  2. TagSeal   (working name — see section 6 for naming history)
     - Free, user-editable ANYTIME, entered/confirmed at post time.
     - Shown on content next to the Tag ID.
     - Not unique / not reserved by default — two people could both
       use the same TagSeal text.
     - UX nudge at posting: "add your TagSeal so people recognize
       your content" — pre-filled with whatever was used last.
     - IMPORTANT CONSTRAINT: once turned on, it's an account-wide
       decision, not a per-post toggle. Every piece of content is
       filed under the same public_tag_id regardless of TagSeal, so
       "add it to some posts but not others" is a false choice —
       revealing it once lets anyone pull `author_tag=` and see
       everything else under that account. Decided: TagSeal is a
       whole-account signature, applied to all content once enabled,
       not selectively per post.

  3. SPOTLIGHT
     - An EARNED badge/status, not a name. Unlocked when a creator's
       TagSeal proves real traction (views/reactions/remasters over
       time — exact metric/threshold TBD, see section 8 open
       question).
     - Once earned, that TagSeal text is reserved for that creator
       going forward (free exclusivity, earned rather than bought).
     - Rule: SPOTLIGHT CAN NEVER BE PURCHASED. If it could be bought,
       it stops meaning "proven creator" and the whole point of
       gating it behind traction collapses — money would buy fake
       credibility instead of real recognition. This is the one rule
       everything else in the plan protects.

  4. PAID CUSTOM TAG
     - The monetization feature. Lets someone claim/lock in a TagSeal
       permanently and exclusively WITHOUT waiting to earn it via
       traction.
     - Deliberately "not atomic" with Spotlight — buying a custom tag
       does not grant the Spotlight badge, and earning Spotlight
       doesn't require ever having paid. Two independent axes:
         money buys naming rights.
         traction buys credibility.
       Neither substitutes for the other.
     - Additional monetizable extras discussed (not decided): a fee
       to change an already-claimed name; cosmetic styling (color/
       emoji) on a TagSeal as pure decoration.


5. MODERATION / ABUSE HANDLING FOR TAGSEAL
----------------------------------------------
Explicitly scoped to reuse existing infra, nothing new to build:
  - Basic automatic filter at set-time: block empty values, app-
    impersonation strings ("tag_admin", "tag_support"), a small
    profanity blocklist.
  - Reuse the existing generic `Report` model/flow (already used
    elsewhere in the app) so a TagSeal can be reported the same way
    other content is; a moderator action resets it to default.
  - Reuse the existing generic `RateLimiter` (rate_limit_events /
    RateLimiter model) to cap how often a TagSeal can be changed per
    day — stops spam-cycling through junk names faster than reports
    can catch them.


6. NAMING HISTORY (for context, not because it matters functionally)
------------------------------------------------------------------------
Went through: "keyword" (working/placeholder term) -> "Spotlight"
(rejected as the free name — conflicted with the earned-badge
meaning) -> "Tag Name" -> "Glow" / "Echo" / "Mark" (single-word,
matches app's existing short-word vocabulary: Spaces, Vibes, Orbit,
Sphere, Books, Numbers, Pings) -> "TagMark" (liked the "Tag+word"
compound pattern, matching TagSpace/TagVibe/TagBook/TagNumber, but
rejected — word didn't feel right) -> "TagStamp" / "TagSign" /
"TagSpark" / "TagPrint" / "TagSeal" -> **settled on "TagSeal."**

Confirmed naming convention going forward: "Tag" + single word,
consistent with every other feature in the app.


7. THE SEPARATE SEARCH / FINDABILITY PROBLEM
--------------------------------------------------
Current state (verified in code):
  - No user-search endpoint exists anywhere in the app.
  - Only content search exists: /search/vibes (by vibe caption) and
    /search/snippets (by public-post body text — "snippets" is the
    app's actual internal name for public/native posts, confirmed via
    SearchController::snippets()).
  - The only way to reach a specific person today: already know their
    exact tag, get shared their content directly, or find them
    organically in the algorithmic feed.

This is a DIFFERENT problem from recognition/branding (TagSeal solves
"what do I call them" — this is "how do I find them at all"). Decided
this should be solved separately:
  - Once a TagSeal is active, make IT searchable — search "TagSeal
    text" and pull up everything (vibes + snippets) filed under it.
  - Independently, consider exposing `full_name` (already collected
    at signup, currently unused/unsearchable) via a real dedicated
    person-search, unrelated to branding/TagSeal — pure "can I find
    this person" utility, no monetization or identity implications.


8. ANONYMITY — WHERE IT'S REAL AND WHERE IT NEVER WAS
-----------------------------------------------------------
Key realization: the tag was never actually hiding anyone's identity
for VIDEO content — a face on camera already de-anonymizes someone,
regardless of what string sits under it. This reframed the whole
conversation:

  - VIBES (video/audio required by TagVibe::store — no plain images
    allowed): anonymity was never real here. TagSeal is encouraged
    freely and immediately, no privacy cost, because there was never
    anything being protected. The one caveat: audio-only vibes (no
    face) are the one vibe format where genuine anonymity is still
    possible, if ever wanted.

  - NATIVE POSTS / SNIPPETS (public_posts, source_type='native'):
    genuinely different — a post can be pure text or non-self media,
    no media is even required to post
    (PublicPostController::store requires body OR media, not both).
    Real anonymity is possible and worth protecting here. DECIDED:
    no TagSeal on snippets by default.
      - Refinement discussed: TagSeal could become available on
        snippets too, but gated — only after a minimum post-count
        threshold (an activity/consistency requirement, same earn-it
        philosophy as Spotlight), and opt-in.
      - Caveat flagged and accepted: because all content (vibe or
        snippet) shares the same public_tag_id, turning TagSeal on
        even once exposes the *entire* account's history via
        `author_tag=` lookups — not just the pieces the user meant to
        sign. So this is a whole-account decision, not truly
        selective per-post privacy. If genuine per-post/per-context
        anonymity is ever wanted, that requires a structurally
        separate identity, not just "no TagSeal" — see next point.

  - ANONYMOUS TAG SPACES (existing feature, kind='anonymous'):
    already does REAL, structurally separate anonymity — via
    Util::spaceIdentity(), anonymous-kind spaces show only a rotating
    `space_tag_code`, never the real public_tag_id, user_id, or
    full_name. This is the only place in the app where identity is
    actually decoupled from the account. If snippets ever need true
    unlinkable anonymity (not just "no nickname"), this is the
    pattern to copy — not something TagSeal's absence provides on its
    own.

  - IMAGE/VIDEO DISCLOSURE WARNING (UX, not architecture):
    Regardless of any tag/TagSeal system, TAG cannot protect someone
    whose face is visibly in a photo or video they upload — that's
    true of every app, not a TAG-specific failure. Decided: show a
    one-time (not repeated) disclaimer at first media upload,
    something like:
      "We protect your Tag ID and TagSeal — but we can't hide your
       face. If you're visible in a photo or video, people who
       recognize you still can."
    This keeps the promise honest: TAG protects the ACCOUNT/identity
    layer, not the visual content itself. Standard pattern already
    used by anonymous/confession-style apps elsewhere.

  - PROFILE AVATAR (resolved, not a gap): every user's `avatar_url`
    shows on everything they post — every snippet, comment, vibe —
    so a real photo there would identify them everywhere, silently.
    BUT this isn't a silent default: signup requires picking an
    in-app avatar (safe, non-identifying, Reddit-style), and swapping
    to a real gallery photo is a deliberate, active choice the user
    makes on their own later. Same category as attaching a selfie to
    a post — an informed decision, not something TAG needs to warn
    about further.

  - LOCATION (checked in code, resolved): `latitude`/`longitude`/
    `location_label` can be attached to a vibe or snippet, but
    `formatVibe()` and `formatPost()` — what's actually returned to
    OTHER viewers — do not include it. It is only used server-side,
    silently, to boost the feed score for nearby viewers (the
    location_boost term in the feed-scoring formula, up to 3x within
    2km tapering to 1.0x past 50km). No visible "posted near you" tag
    exists today, so there is no location-based exposure to other
    users — the choice to attach location only affects ranking, not
    visibility of where someone was.
      Reference use case (from discussion, keep for context when this
      gets built): a student posts "CBU class 349, doing math, come
      join?" with location on — people physically nearby get it
      ranked higher in their feed. Since there's currently no visible
      signal explaining *why* a post is ranked higher, a future
      "near you" badge (general proximity only, never exact
      coordinates) may be worth adding so this use case actually
      lands with viewers — not decided, just noted as the motivating
      example if/when this is revisited.


9. OPEN QUESTIONS (not yet decided)
---------------------------------------
  - Exact traction threshold/metric for earning Spotlight: a single
    vibe/snippet crossing a threshold, cumulative traction across a
    creator's whole output, or a relative/top-percentile cutoff
    (recalculated periodically, similar to the existing trending-fill
    feed layer). Leaning toward CUMULATIVE (rewards sustained output,
    not a one-hit-wonder). MECHANISM DECIDED: driven by engagement
    signals already used in feed scoring (views/reactions/comments/
    remasters on the TagSeal) — exact cutoff number is a tuning knob
    to set post-launch from real traffic, not a design blocker.
  - Whether Spotlight traction is tracked per TagSeal-text or
    per-account. DECIDED: per TagSeal-text. Spotlight is earned by a
    NAME, not a person — the payoff is reserving that exact word for
    the creator, which only makes sense if traction was built under
    that word. Switching TagSeal resets Spotlight progress. This also
    reinforces the earlier point that recognition requires keeping a
    name stable — changing it isn't free.
  - Exact minimum-post-count threshold for unlocking TagSeal on
    snippets. MECHANISM DECIDED (same earn-it gate as Spotlight,
    activity/consistency-based) — exact number is a tuning knob, not
    a design blocker.
  - Paid custom-tag pricing and change-fees. MECHANISM DECIDED (plain
    business/pricing decision, not a formula) — actual price point
    deferred to a business decision, not a product-design question.
  - Whether audio-only vibes should ever get a genuine anonymity mode.

OUT OF SCOPE for this plan (not a TagSeal question):
  - Whether snippets should ever get a structurally separate identity
    (Tag-Space-style rotating code) for true per-context anonymity.
    DECIDED: TagSeal is for content recognition only — it is not an
    anonymity mechanism and was never meant to solve this. If deeper
    account-level anonymity for snippets is ever wanted, that's an
    independent feature (closer to extending the anonymous Tag Spaces
    pattern) to be scoped on its own, not a loose end of this plan.


STATUS: parked here per user request — revisit before building
anything. Nothing above has been implemented; this is a plan/decision
record only.
