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.
This commit is contained in:
SkateSpots 2026-10-09 16:55:10 +07:00
parent 486ffdb000
commit 8833a15830
2 changed files with 229 additions and 0 deletions

View file

@ -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 is identity at a lower level + **plural, viewer-applied filtering**. A full idea
list was discussed this session but is deliberately **NOT started**. 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 **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 swap*, not constants. Hardcoded `ADMIN_PUBKEYS` is fine now — converting it later
is a small refactor, not a trap. is a small refactor, not a trap.

223
TRUST_SCORE.md Normal file
View file

@ -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:<tag>", <provider pubkey>, <relay>]`) 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` |