Capture the recommendation in AGENTS.md (and mirror it in TRUST_SCORE.md): do not build the cvm-server per-observer GrapeRank scorer yet. The full confidence model is provider-side and consumed via NIP-85; the client has a local confidence gate; the blockers are the per-observer signing key and 8-hop compute plus lifecycle/deploy/monitoring, with ~zero users making the ROI negative. A house-perspective scorer is the cheaper path; revisit when there are users, no trustworthy provider, or skate-specific signals to bake in.
318 lines
17 KiB
Markdown
318 lines
17 KiB
Markdown
# 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.
|
|
|
|
All three follow-ups landed in the later passes below: the batched read, the
|
|
flagged notice's reporter breakdown, and Settings → Trust.
|
|
|
|
## Second pass (landed)
|
|
|
|
- **Batched provider read.** Every `TrustBadge` registers its subject into a
|
|
shared per-viewer union and ONE query fetches the whole union (no per-card
|
|
query); a list that grows triggers a single debounced refetch. Contained to
|
|
`useTrustAssertions.ts` — no call site changed.
|
|
- **Direct follow is neutral.** `bucketFromLocalScore` now verifies only a
|
|
friend-of-friend (60); a direct follow (90) or self (100) shows nothing, since
|
|
the viewer already knows them. A provider assertion can still verify a
|
|
non-followed account, and `TrustBadge` suppresses verified when the viewer
|
|
directly follows. Flagged is never suppressed.
|
|
- **Flagged notice with a breakdown and actions.** `useUserReports` reads the
|
|
public NIP-56 reports naming an account; `components/TrustFlaggedNotice.tsx`
|
|
shows the reporters grouped as people you follow / in your network / other,
|
|
plus **Report** (reuses `ReportDialog`) and **Dismiss** (session-only, so the
|
|
safety signal returns). Wired into the listing seller card and the coach
|
|
detail page.
|
|
|
|
## Third pass — legibility (landed)
|
|
|
|
- **Attribution.** The badge tooltip names the source of the claim (your network /
|
|
the community provider / the provider you chose) and links to an explainer.
|
|
- **Public `/trust` page.** Explains the three states, that absence is not
|
|
distrust, who computes trust, presence-is-not-endorsement, and
|
|
point-of-view relativity. Added to the smoke-test route list.
|
|
- **Settings → Trust** (`?section=trust`, `components/TrustSettingsCard.tsx`): a
|
|
show/hide-marks toggle (`useTrustDisplay`, localStorage
|
|
`skatespots:trust:display`; **Flagged is never hidden**), the active provider +
|
|
relay (`useTrustProvider`), and **provider choice**: `useSetTrustProvider`
|
|
publishes a NIP-85 **kind-10040** designation (custom npub/hex + `wss://`
|
|
relay, all five `30382:<type>` rows) or clears it to fall back to the
|
|
community provider — a signed action, so it prompts the signer. Link to
|
|
`/trust`.
|
|
|
|
## Fourth pass — mute (landed)
|
|
|
|
- **`useMuteList`** reads the viewer's NIP-51 **kind-10000** mute list plus any
|
|
optimistic local mutes (`lib/localMutes.ts`, `skatespots:local-mutes-v1`).
|
|
- **`useMuteUser`** republishes kind-10000 with the target added (preserving
|
|
existing entries), records it locally, and invalidates content queries.
|
|
- The shared **`filterAdminHidden` / `filterAdminHiddenMixed`** now also drop
|
|
authors in `hidden.mutedPubkeys`, so every list hook that already filters
|
|
hidden content (spots/shops/events/tours/crews/marketplace/skaters/coaches/lab)
|
|
hides muted accounts with **no per-hook changes**. `useAdminModeration` carries
|
|
the mute set alongside the admin verdicts (it is the app-wide "drop this
|
|
content" layer; `includeHidden` admin tooling still bypasses it).
|
|
- The flagged notice gains a **Mute** action (Report + Mute + Dismiss).
|
|
|
|
## Fifth pass — network alerts (landed)
|
|
|
|
- **`useNetworkAlerts`** takes the viewer's follows, reads their NIP-85
|
|
assertions (batched), and surfaces the accounts that trusted people have
|
|
reported or muted (`reporters`/`muters` at/above the flagged threshold).
|
|
Ignored accounts are remembered device-locally
|
|
(`skatespots:trust:alerts-ignored`).
|
|
- **`/alerts`** (`pages/NetworkAlertsPage.tsx`) lists them with
|
|
**Ignore / Unfollow / Mute / Report** actions (reusing `useFollowActions`,
|
|
`useMuteUser`, `ReportDialog`) — inform, never auto-act.
|
|
- Settings → Trust links to it with a live count.
|
|
|
|
## Sixth pass — local confidence gate (landed)
|
|
|
|
- The fallback local ladder now gates a friend-of-a-friend behind a confidence
|
|
threshold: `useViewerGraph` counts distinct follows per friend-of-a-friend, and
|
|
`deriveLocalScore` returns `verified` only at `FOF_MIN_RATERS` (2) or more. A
|
|
lone endorsement no longer reads as trust.
|
|
- Provider assertions are unaffected: the full GrapeRank confidence model lives
|
|
in the provider and we consume it via NIP-85. Running our own scorer is
|
|
**deferred** — see the dated decision in `AGENTS.md` ("Own GrapeRank-style
|
|
scorer — DEFERRED"): the per-observer signing key and 8-hop compute are the
|
|
blockers, and the client is already provider-ready. A house-perspective scorer
|
|
(one key, one observer) is the cheaper path if ever wanted.
|
|
|
|
|
|
|
|
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` |
|