# Trust scores — ideas and plan Status: first slice landed; the rest is proposal. No breaking changes. Scope: better trust for people and content across the map, The Lab, the marketplace, and moderation. ## First slice (landed) - `src/lib/nip85.ts` — kind-10040 designation parsing, provider resolution with a house fallback, kind-30382 assertion parsing (with the absence rules baked in), and batch parsing keyed by subject. - `src/hooks/useTrustAssertions.ts` — batch read from the resolved provider; signature-verified and author-checked. Absence yields no entry (= unknown). - `src/lib/trustLadder.ts` — the three buckets and the two thresholds (`FLAGGED_REPORTERS_THRESHOLD` 2, `VERIFIED_RANK_THRESHOLD` 2). - `src/hooks/useTrustBucket.ts` — combines a NIP-85 assertion (verify + flag) with the local ladder (verify only) into one bucket. - `src/components/TrustBadge.tsx` — now renders Verified / Unknown / Flagged (glyph + colour + tooltip). Unknown is hidden by default; Flagged always shows. Because this is used by SpotCard, TourCard, EventCard, MarketplaceItemCard, ListingDetailPage and CoachDetailPage, the mapping, Lab and marketplace all get it for free. Still open from this slice: precompute the bucket at list level and pass it down (one provider query per list instead of per card); a "who reported" breakdown (the assertion carries counts, not identities); and a Settings control to pick the provider instead of only using the designation or house default. Source material: the **GrapeRank** and **Trusted Assertions** specifications in the `NosFabrica/protocols` repository, plus a public reference implementation and client by the same group. NIP-85 (Trusted Assertions) is the published, stable contract; GrapeRank is one pre-NIP scoring model that produces the published scores. We should treat NIP-85 as the interface and GrapeRank as a model we may adopt or borrow from, not a hard dependency. ## Where we are today Positive signals (per-viewer, client-side): - `src/hooks/useTrustScore.ts` and `src/hooks/useBatchTrustScores.ts` compute a static ladder from the viewer's follow graph: `100` self, `90` direct follow, `60` friend-of-friend (2 hops), `30` has a NIP-05, `0` unknown. Signature-verified via `onlyVerified`. - `src/lib/trustFilter.ts` + `TrustFilter.tsx` expose the levels `all / verified (>=90) / trusted (>=60) / following`. - `useSpotCommunityScore` weights content signals by distinct accounts, web-of-trust and activity (per-account cap of 3); `useTradeIntegrity` adds ring detection. Reports drive `useAutoHiddenSpots` (>=3 distinct reporters, no community signals) and the review queue. NIP-85 consumption is half-built: - `src/hooks/useRelatrScore.ts` (and `useBatchRelatrScores`) query kind `30382` from one **hardcoded** provider pubkey + relay (auto-enabled), verify signatures, and read only the `rank` tag. It ignores provider designation, the `followers` / `reporters` / `muters` / `hops` tags, and the spec's absence rules. - `src/lib/relatr.ts` is a placeholder REST client, disabled (`RELATR_ENABLED = false`); it was never finished and is the wrong shape (REST, not events). ## Gaps versus the specs 1. No **provider designation**: we neither read the viewer's own kind `10040` nor let them choose providers, and we cannot show which provider a score came from. 2. **Negative signals are ignored**: mutes (kind `10000`) and user-level reports (NIP-56 `1984`) are not inputs to an author's score. 3. **No confidence gate**: a single follow instantly reads as `90`. A cluster of accounts that only follow each other is indistinguishable from a well-connected account. This is the biggest sybil hole in the current ladder. 4. **No trust-driven alerts**: the app filters content, but never tells a user "someone you follow has been reported by trusted people." 5. **Display is a bare ladder**: no viewer choice in how (or whether) trust is shown, and no distinct "flagged" state next to "verified" and "unknown". ## Ideas, highest value first ### 1. Make the NIP-85 consumer spec-correct Replace the hardcoded single-provider lookup with a real consumer: - Read the viewer's **kind 10040** designation (rows are `["30382:", , ]`) and resolve the provider and relay per assertion type. Fall back to a configured "house" provider (the community default) when the viewer has no designation or is logged out. - Verify `event.pubkey == provider` **and** the signature before use (the signature check already exists; the author check is the trust boundary). - Read the normative tags: `rank` (0-100), `followers`, `reporters`, `muters`, `hops`. Treat producer extensions as optional. - Enforce the honesty rules the spec is emphatic about: - an absent `hops` means "no known path", never `0`; - absence of an assertion is **not** distrust; - ranks from different observers or providers are **not** comparable; - presence of an assertion is **not** endorsement (read the counts). Sketch of the shape: `src/lib/nip85.ts` (designation parse, provider resolve, assertion parse, verification) + `src/hooks/useTrustAssertions.ts` (batch read via `querySkateContent`/relay hints, disabled when no provider). `useRelatrScore.ts` becomes one configured provider behind that interface rather than a special case. A Settings section ("Trust providers") shows the active provider + relay, allows choosing the house provider or a personal one, and can be switched off. This alone turns trust from our fixed 0/30/60/90/100 into "whatever provider the viewer chooses", with no new infrastructure. ### 2. Three buckets and a viewer display mode Adopt a simple, honest presentation on top of whatever score source is active: - **Verified / Unknown / Flagged**, with color **and** a glyph so the state is legible without color. "Flashy intermediates" (highly verified vs verified) were deliberately rejected — actionable, not graded. - **Flagged = verified reporters >= threshold**, where "verified reporters" are reporters who themselves clear the trust bar. Not a raw report count. Show *who* reported, grouped as people you follow / verified / others, never what the account "is". - Let the viewer choose how it renders: number / level / word / **off**. "Off" is a legitimate choice for people who do not want others graded; safety warnings (flag/report) remain a separate channel and always show. - Reuse the same ladder in the trust filter, author badges on the map/Lab/ marketplace, and the community-stats page, so a score reads identically everywhere. Where it lands: a small `lib/trustLadder.ts` (bucket + ladder helpers), a viewer preference stored per account (the `useLocalStorage` pattern), and reuse of the existing `Filter` UI. Content that is `Flagged` can keep our current soft behaviour (deprioritize, badge, and feed the review queue) rather than hard delete. ### 3. Trust-driven alerts for counterparties The alert pattern is a strong fit for a marketplace of strangers and paid Lab services: - Compute "accounts in your network that trusted people report/mute" from the provider's `reporters` / `muters` counts (or locally, from reports weighted by reporter trust). - Surface it as a dismissible banner plus a page of alerts, with actions **ignore / unfollow / mute / report** and a short interaction history (your follows, prior trades, prior reports). One row per account; a "new since you last looked" marker. - The posture is "inform, then let the user decide" — never auto-hide a seller or coach on a single report. This complements the existing reputation cards and the >=3-distinct-reporter auto-hide, not replace them. ### 4. Confidence-gated scoring The single most important algorithmic idea from the specs: trust is `influence = average x confidence`, with `confidence = 1 - rho^input` and each edge weighted by the rater's own influence and an attenuation factor. A lone endorsement scores near zero; breadth of already-trusted raters is what accumulates. Defaults: `rho 0.5`, `attenuation 0.85`, follows `+1`, mutes and reports negative. Two ways to get it, in order of preference: - **Consume it** from a provider (the house provider, or a personal one) — no client cost, and the score is already published as a signed assertion. - **Port it** into `cvm-server/` (it is ~a dozen parameters and a bounded iteration) and publish our own assertions only if we decide to run a provider. This keeps the heavy work off the phone while staying inside our existing service. Either way, the win is: stop treating one follow as trust. Our existing per-account cap and activity weighting in `useSpotCommunityScore` are the same instinct applied to content; this applies it to people. ### 5. Presets and negative signals as policy - Expose scoring policy as named **presets** (Relax / Default / Strict) that move the follower/muter/reporter cutoffs together, rather than hardcoded constants. If we ever run a provider, persist presets server-side with an append-only change history and show the active one in Settings. - Add **mutes (10000)** and **user-level reports (1984)** as negative edges — weighted, so a report from a trusted skater counts and a report from a brand-new account does not. We already gather reports; this is where they should feed a person-level signal, not only content moderation. ### 6. Vocabulary and honesty copy Borrow the precise wording these specs insist on, because it matches our own convention ("Conditions · weather only", "attested, not verified"): - counts are "verified followers / reporters / muters" (trusted raters only); - "No one in your network has vouched for this account yet." for Unknown; - "presence is not endorsement", "absence is not distrust"; - "scores are point-of-view-relative" — always attribute a score to a provider and a perspective, never present it as an objective fact. ## What we are not doing - Standing up the reference infrastructure (graph store + cache + search engine + Java worker + Python orchestrator + bundled relays). It contradicts the project's zero-server stance for this feature and is unnecessary if we consume a provider. At most, a provider we operate would live in `cvm-server/`. - Adopting an external search relay as a dependency. The portable idea is ranking our own search and Lab results by the viewer's trust, which the `trustFilter` already gestures at. ## Caveats - **Licensing.** The reference implementation, server and client are AGPL-3.0. Reading the specifications and reimplementing the algorithm is fine; copying their code into our shipped client would pull AGPL obligations onto SkateSpots. Prefer the specs and our own implementation. - **Spec status.** GrapeRank and Trusted Assertions are pre-NIP drafts; NIP-85 itself is published. Code against NIP-85 (`10040` / `30382`) and treat GrapeRank as one producer. - **Privacy.** The current client-side ladder fetches only the viewer's own follows and keeps the graph local. Consuming a provider discloses "this viewer designates this provider, score this subject"; operating a provider means someone holds the whole graph. Whatever we pick must be disclosed on `/privacy`, the way geocoding and the AI proxy already are. - **Existing code.** `lib/relatr.ts` should be retired (REST, disabled, unconfirmed); `useRelatrScore.ts` is the seed to grow from. ## Recommended first slice 1. **Spec-correct NIP-85 consumer** (`src/lib/nip85.ts` + `src/hooks/useTrustAssertions.ts`), with kind-10040 resolution, a configured house provider, author + signature verification, and the four normative tags. Keep `useRelatrScore` working by expressing it as a provider. 2. **A Flagged bucket** fed by verified reporter counts, shown with a glyph in the trust filter and on author badges, with a "who reported" breakdown. This reuses code we already have, needs no new infrastructure, and improves the map, Lab, marketplace and moderation without changing the scoring model. The confidence model (idea 4) and alerts (idea 3) are the natural next steps. ## Touch points | Concern | File(s) | | --- | --- | | NIP-85 designation + assertion parsing | new `src/lib/nip85.ts` | | Assertion read hook (batch) | new `src/hooks/useTrustAssertions.ts` | | Current single-provider score | `src/hooks/useRelatrScore.ts` | | Placeholder REST client (retire) | `src/lib/relatr.ts` | | Local ladder (keep, or blend) | `src/hooks/useTrustScore.ts`, `src/hooks/useBatchTrustScores.ts` | | Bucket/ladder helpers + viewer mode | new `src/lib/trustLadder.ts` | | Filter levels | `src/lib/trustFilter.ts`, `src/components/TrustFilter.tsx` | | Author badges | map / Lab / marketplace author surfaces | | Alerts | new alerts page + banner; report hooks already exist | | Policy presets | Settings; server-side only if we run a provider | | Privacy disclosure | `/privacy` |