First slice of TRUST_SCORE.md. - lib/nip85.ts: parse kind-10040 provider designations, resolve a provider with a house fallback, parse kind-30382 assertions (rank/followers/ reporters/muters/hops; absent hops means no path, absence is not distrust), and author-check the provider. - hooks/useTrustAssertions.ts: batch read from the resolved provider, signature-verified via onlyVerified + the author check. - lib/trustLadder.ts: three buckets and thresholds (flagged = trusted reporters >= 2; verified rank >= 2). - hooks/useTrustBucket.ts: prefers a NIP-85 assertion (verify + flag), falls back to the local WoT ladder (verify only). - TrustBadge.tsx: now renders Verified / Unknown / Flagged with glyph, colour and tooltip; Unknown hidden by default, Flagged always shown. Reaches the map/tour/event cards, marketplace card, listing detail and coach detail through the shared component. Open follow-ups (in TRUST_SCORE.md): batch the bucket at list level, a 'who reported' breakdown, a provider-choice setting, alerts, confidence scoring.
13 KiB
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_THRESHOLD2,VERIFIED_RANK_THRESHOLD2).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.tsandsrc/hooks/useBatchTrustScores.tscompute a static ladder from the viewer's follow graph:100self,90direct follow,60friend-of-friend (2 hops),30has a NIP-05,0unknown. Signature-verified viaonlyVerified.src/lib/trustFilter.ts+TrustFilter.tsxexpose the levelsall / verified (>=90) / trusted (>=60) / following.useSpotCommunityScoreweights content signals by distinct accounts, web-of-trust and activity (per-account cap of 3);useTradeIntegrityadds ring detection. Reports driveuseAutoHiddenSpots(>=3 distinct reporters, no community signals) and the review queue.
NIP-85 consumption is half-built:
src/hooks/useRelatrScore.ts(anduseBatchRelatrScores) query kind30382from one hardcoded provider pubkey + relay (auto-enabled), verify signatures, and read only theranktag. It ignores provider designation, thefollowers/reporters/muters/hopstags, and the spec's absence rules.src/lib/relatr.tsis a placeholder REST client, disabled (RELATR_ENABLED = false); it was never finished and is the wrong shape (REST, not events).
Gaps versus the specs
- No provider designation: we neither read the viewer's own kind
10040nor let them choose providers, and we cannot show which provider a score came from. - Negative signals are ignored: mutes (kind
10000) and user-level reports (NIP-561984) are not inputs to an author's score. - 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. - No trust-driven alerts: the app filters content, but never tells a user "someone you follow has been reported by trusted people."
- 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 == providerand 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
hopsmeans "no known path", never0; - 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).
- an absent
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/muterscounts (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/.
- 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
- 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
trustFilteralready 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.tsshould be retired (REST, disabled, unconfirmed);useRelatrScore.tsis the seed to grow from.
Recommended first slice
- 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. KeepuseRelatrScoreworking by expressing it as a provider. - 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 |