From 8833a1583006e774ab6af2cab89187ed718b190f Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Fri, 9 Oct 2026 16:55:10 +0700 Subject: [PATCH] docs: add TRUST_SCORE.md (trust-scoring review + plan) Ideas-only review of a personalized web-of-trust approach: a spec-correct NIP-85 consumer (kind-10040 provider designation, house fallback, author + signature verification, the rank/followers/reporters/muters/hops tags, and the absence rules), a Verified/Unknown/Flagged display with a viewer mode, counterparty alerts, confidence-gated scoring, presets and negative signals. Notes what we already have, concrete file touch points, license and privacy caveats, and a recommended first slice. Links it from the parked identity/conflict section. --- AGENTS.md | 6 ++ TRUST_SCORE.md | 223 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 TRUST_SCORE.md diff --git a/AGENTS.md b/AGENTS.md index ee3d440..b5ead4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2512,6 +2512,12 @@ downstream of **centralized identity + centralized conflict resolution**; the fi is identity at a lower level + **plural, viewer-applied filtering**. A full idea list was discussed this session but is deliberately **NOT started**. +**Related, also ideas-only:** `TRUST_SCORE.md` collects the trust-scoring review +(NIP-85 consumer with kind-10040 provider choice, a Verified/Unknown/Flagged +display, confidence-gated scoring, counterparty alerts). It is the concrete form +of the "labeler choice / plural filtering" thesis below. Not started; read it +before touching `useTrustScore` / `useRelatrScore` / `trustFilter`. + **Keep the door open (cheap):** admin keys and relay sets must stay *data you can swap*, not constants. Hardcoded `ADMIN_PUBKEYS` is fine now — converting it later is a small refactor, not a trap. diff --git a/TRUST_SCORE.md b/TRUST_SCORE.md new file mode 100644 index 0000000..2bb7a24 --- /dev/null +++ b/TRUST_SCORE.md @@ -0,0 +1,223 @@ +# Trust scores — ideas and plan + +Status: ideas / proposal. No code yet. +Scope: better trust for people and content across the map, The Lab, the +marketplace, and moderation. + +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` |