Digital Trust Protocol

Single-Subject Rating#

draft optional

Status#

Experimental proposal for event kind 32014. Coordinate registration before treating it as a stable interoperable standard.

Attention documents this kind as the graded companion to NIP-32009. Attention ships kind 32014 as a ratings layer beside kind 32009 trust: publish, ingest, query, post star UI, and Side Panel Notes. Ratings are never Web-of-Trust hops.

Abstract#

Kind 32014 records one author's numeric score of exactly one subject: how much that subject is worth the reader's time (or, for a product, worth buying / using).

It is addressable (30000–39999). For each author and replaceable slot, a newer event replaces the older one by using the same d tag. The d tag is always a 64-character lowercase hex SHA-256 digest, built with the same subject:scope:context material as kind 32009.

The event is a subjective attribute, not a trust-graph hop. Clients MUST NOT use kind 32014 to expand Web-of-Trust traversal. That remains exclusive to positive kind 32009 p statements. Discovery uses kinds: [32014] (and #d / subject filters as needed).

The two questions these kinds answer are recorded in wot-questions.md.

Motivation#

Kind 32009 uses a coarse v (-1 / 0 / 1) for directed trust. Many use cases need a graded score without implying “follow this edge.” A dedicated rating kind keeps ternary trust and fine-grained scores separate and composable.

Hosts such as X already provide likes and replies. They do not score a post as “worth reading.” Kind 32014 is that missing indicator, consumed only from authors the local root already trusts.

This document is an evolution of the earlier Trust NIP-32014 (kind 32010 slot model). Attention uses kind 32009 slots. Cancel is an empty score (tombstone). "0" is a valid rating: worth no time.

i     = "<class>:<property>:<value>"   # typed subject; e.g. post:id:2080659774136291424
score = "" | "<0-100>"                 # required tag; empty = cancel; "0" is a value
k     = "<class>:<property>"           # optional; e.g. post:id
s     = "<domain>"                     # optional; e.g. x.com
c     = "<purpose>"                    # optional; omit = generic time-worth; e.g. product
l     = "<label>" [ "<description>" ]  # optional; zero or more; e.g. genuine
x     = "<unix-seconds>"               # optional activation time
y     = "<unix-seconds>"               # optional expiration time
TagRoleExample
iTyped subject — required for this shapepost:id:2080659774136291424
scoreRating or cancel — required tag80 / 0 / 50.598334 / (empty)
kTyped subject prefix (#k filter) — optionalpost:id
sDomain/namespace (#s filter; also in d) — optionalx.com
cPurpose (also in d) — optional; omit = generic(omit) / product
lAugments the rating with further clarification — not in dinsightful / spam
xActivation time (Unix seconds) — optional1672531600
yExpiration time (Unix seconds) — optional1672618000

Do not publish k / s / c / l without a subject and a score tag. k alone is not a subject. There is no v tag and no scale tag. Ternary trust stays on kind 32009. This kind’s scale is fixed.

Relationship to other events#

  • Kind 32009: trust, Neutral, distrust, Delete. Positive p MAY be a traversal edge. Rating is never a traversal edge. Kind 32009 uses empty v for Delete and "0" for Neutral — the same empty-vs-zero split as this kind's score.
  • Kind 3: following, not rating.
  • NIP-85: derived provider outputs (rank, …). Not a personal rating input. Do not publish “I score this 80” as kind 30385.
  • Kind 32010: earlier multi-subject trust draft.
  • Upstream 32014 (Trust / 32010 d): same intent and empty-score tombstone; different slot algebra (d material).

Publishing both a 32009 statement and a 32014 rating for the same subject is allowed; they mean different things. A rating MUST NEVER grant access or expand whose judgments the client reads.

Specification#

Kind#

kind: 32014

Address: 32014:<author-pubkey>:<d-tag>.

Content#

Optional human-readable note, ≤ 1024 Unicode characters. Not structured protocol data. Empty content is valid. Clients MUST NOT treat missing or empty content as an incomplete rating.

Attention product policy: content is the human comment on that claim. Empty content is a complete rating. On X posts, X replies remain the main discussion channel, so the composer is collapsed behind expand and is never required. Do not strip content on publish, ingest, or display. Compose cap and plain-text rules follow design.md § Trust statement content (~144 Unicode characters in UI). See wot-questions.md.

Tags#

Required:

  • exactly one d;
  • exactly one subject: p, e, or i;
  • exactly one score tag (the value MAY be empty).

For typed / external identifiers the subject MUST be i. k MUST NOT appear without i. When k is present, it MUST equal the <class>:<property> prefix of i.

Optional:

  • at most one k;
  • zero or more s;
  • at most one c (purpose; optional — omit for generic time-worth);
  • zero or more l (labels; not in d);
  • at most one x;
  • at most one y (if both x and y are present, x MUST be <= y).
["d", "<64-hex-sha256>"]
["p", "<64-hex-pubkey>", "<class>:<property>:<value>" | "npub1…", ...] |
["e", "<64-hex-event-id>", "<class>:<property>:<value>", ...] |
["i", "<class>:<property>:<value>", "<class>:<property>:<value>" | "npub1…", ...]
["score", "" | "<0-100>"]            // required; empty = cancel
["k", "<class>:<property>"]           // optional; only with i
["s", "<scope>"]                      // optional; zero or more
["c", "<context>"]                    // optional; at most one
["l", "<label>", "<human description>"]  // optional; zero or more; tag[2] optional
["x", "<unix-seconds>"]               // optional
["y", "<unix-seconds>"]               // optional

Unknown tags MAY be kept and ignored.

Subject tags, hints, k, and s#

Subject kinds, hint syntax, k/i grammar, and s (scope) rules are the same as NIP-32009 (p / e / i, structured class:property:value or bare npub1… hints, colon-stable scope string).

Differences from kind 32009:

  • A p subject on 32014 is terminal. It MUST NOT be used as a traversal edge.
  • e and i remain terminal (same as 32009).

X subjects stay user:id:<digits> and post:id:<digits> with s=x.com recommended. Put the site in s, not in i.

Context (c)#

Optional. Same grammar as kind 32009 (lowercase, : hierarchy, ≤ 128 UTF-8 bytes). Omit (or empty) for a generic time-worth rating of the subject. When present, at most one c tag. It enters d.

c is purpose, not a claim name. Labels such as spam belong on l, not on c.

No fallback. Clients MUST resolve kind 32014 with exact c (including empty/generic). They MUST NOT walk parent prefixes or fall back to a missing c. A c=product rating MUST NOT stand in for a generic post score. This is the intentional algebra break from kind 32009.

One purpose per event; more purposes ⇒ more events (different d).

Score (score)#

Required tag. Exactly one score tag. Clients MUST reject a missing or duplicate score. An empty value (["score", ""]) is valid: it is the tombstone (cancel). Whitespace-only values are invalid.

The scale is fixed. There is no scale tag.

Wire valueMeaning
""Cancel / retract. The slot contributes no active rating.
"0" … "100"Active rating. 0 = worth no time. Higher = more worth the reader's time (or, for a product, more worth buying / using).

This matches kind 32009, where empty v is Delete and "0" is Neutral. Here 0 is a point on the scale (an opinion). Empty means no opinion.

Rules:

  • Empty score is only cancel. "0" and "0.0" (canonical "0") are active ratings, not tombstones.
  • Active scores MUST be in [0, 100]. Values below 0 or above 100 are invalid.
  • Decimals are allowed in [0, 100] for fine aggregation, e.g. "50.598334". 0, 20, and 100 MAY be written as integers or with a fraction (canonical form drops trailing zeros).
  • Canonical form: no scientific notation; no leading zeros (50 not 050 or 050.0); optional fractional part with a single .; at most 6 decimal digits. Trailing zeros MAY be omitted (50 equals 50.0; 0.0 equals 0). The empty string is not a number.
  • A cancel (["score", ""]) MUST use the same subject, canonical scope (s), and context (c) as the rating it clears — the same replaceable d slot.

Quick star form#

UI default. Integers only:

Starsscore
120
240
360
480
5100

0 is a rating (worth no time), not a star button. Clients MAY map labels onto the scale (insightful → 100, genuine → 80, funny → 60, ai-slop → 40, misleading → 20, spam → 0) and MAY average trusted scores at the nearest hitting degree to a decimal for display.

Labels (l)#

Optional. Zero or more l tags. Each primary value uses the same grammar as c (lowercase, no spaces; display names such as “AI slop” become ai-slop). Optional tag[2] is a human description of that label, same shape as kind 32009 — not a class:property:value identity hint. Further elements MAY be ignored. Clients MUST treat descriptions and content as untrusted display text.

Labels are the why on a single review. They MUST NOT enter d and MUST NOT become Web-of-Trust hops. Changing labels without changing subject, scope, or c replaces the same slot (latest event wins stars, labels, and content together).

Suggested vocabulary (not closed): insightful, genuine, funny, ai-slop, misleading, spam, product. Unknown labels MUST be preserved and MAY be ignored for aggregation.

Duplicate l values on one event SHOULD be collapsed; if duplicate tokens carry descriptions, keep the first non-empty description. Clients MUST NOT treat duplicates as extra weight.

Building d (always SHA-256)#

Same formula as NIP-32009. score, l, hints, x, y, and content are not in material.

d = lowercase_hex( SHA-256( UTF-8( material ) ) )

Empty c is "". scope is "" when there are no s tags; otherwise unique s values, sorted lexicographically, joined with ,.

Subjectmaterial
i<i> + ":" + scope + ":" + <c>
p"p:" + <pubkey> + ":" + scope + ":" + <c>
e"e:" + <event-id> + ":" + scope + ":" + <c>

Examples of material (before hashing):

Setupmaterial
i=post:id:2080659774136291424, s=x.com, no cpost:id:2080659774136291424:x.com:
i=post:id:2080659774136291424, s=x.com, c=productpost:id:2080659774136291424:x.com:product
i=user:id:11348282, no s, no cuser:id:11348282::

Clients MUST recalculate d from tags. Mismatch ⇒ invalid. Adding, removing, or changing hints or l MUST NOT change d.

Do not use kind 32010 base_id|context plaintext as d.

Replacement#

Same author + same d ⇒ addressable replacement (latest created_at; NIP-01 tie-break on id). Changing subject, canonical scope, and/or c changes d (new slot). A cancellation MAY omit or change l and hints, but MUST preserve the primary subject, canonical scope, and context.

If the newest event has an empty score or lies outside its x/y window, that slot contributes no active rating. Clients MUST NOT fall back to another c. An active "0" does contribute: it is worth no time.

Time (x / y)#

Same semantics as NIP-32009. Omit both when the rating is always active. Outside the window: the event remains newest for the slot, but contributes no active rating. Do not revive older replaced events. A tombstone (empty score) has no effective rating regardless of x/y.

Validation checklist#

  1. Signature valid.
  2. Exactly one d, one subject (p / e / i), one score tag; the subject's second element is the primary subject.
  3. At most one k, c, x, y.
  4. k without i is invalid. If k is present with i: k equals the typed prefix.
  5. user:id / post:id value (in the primary i element) is decimal digits.
  6. Every additional p / e / i element is a structured hint or a canonical bare npub1….
  7. Each s and l value (when present) is non-empty and within grammar / size limits.
  8. score is exactly "" (cancel) or a canonical number in [0, 100] (optional fraction, ≤ 6 decimal digits, no scientific notation, no leading zeros). Whitespace-only is invalid.
  9. No v tag is required; if present, clients MUST ignore it for rating meaning (kind 32009 owns v).
  10. Time tags integer; x <= y if both.
  11. Non-empty c canonical ≤ 128 bytes.
  12. Recalculated d equals published d without using hints or l.
  13. Content within safety limit.

Querying#

{
  "kinds": [32014],
  "#k": ["post:id"],
  "#s": ["x.com"]
}
{
  "kinds": [32014],
  "authors": ["<wot-author>"],
  "#i": ["post:id:2080659774136291424"],
  "#s": ["x.com"]
}

Exact replaceable slot when d is known:

{
  "kinds": [32014],
  "authors": ["<author>"],
  "#d": ["<64-hex-sha256>"]
}

Do not mix 32014 into trust-graph frontier filters.

Examples#

d values below are illustrative placeholders; publishers MUST compute the real SHA-256 of material.

Rate an X post (worth reading)#

Generic time-worth, four stars, no comment (host already has replies):

{
  "kind": 32014,
  "pubkey": "<statement-author>",
  "created_at": 1672531350,
  "tags": [
    ["d", "<sha256(post:id:2080659774136291424:x.com:)>"],
    ["k", "post:id"],
    ["i", "post:id:2080659774136291424"],
    ["s", "x.com"],
    ["score", "80"]
  ],
  "content": ""
}

material = post:id:2080659774136291424:x.com:.

Cancel that rating#

{
  "kind": 32014,
  "pubkey": "<statement-author>",
  "created_at": 1672531400,
  "tags": [
    ["d", "<sha256(post:id:2080659774136291424:x.com:)>"],
    ["k", "post:id"],
    ["i", "post:id:2080659774136291424"],
    ["s", "x.com"],
    ["score", ""]
  ],
  "content": ""
}

Same d. Empty score is the tombstone. The slot contributes nothing.

Worth no time (score is 0, not cancel)#

{
  "kind": 32014,
  "pubkey": "<statement-author>",
  "created_at": 1672531410,
  "tags": [
    ["d", "<sha256(post:id:2080659774136291424:x.com:)>"],
    ["k", "post:id"],
    ["i", "post:id:2080659774136291424"],
    ["s", "x.com"],
    ["l", "spam"],
    ["score", "0"]
  ],
  "content": ""
}

Same d as the four-star example; this replaces 80 with an active zero. Aggregators MUST include 0 in the average. They MUST NOT treat it as a retracted slot.

Product-like subject with label and note#

{
  "kind": 32014,
  "pubkey": "<statement-author>",
  "created_at": 1672531500,
  "tags": [
    ["d", "<sha256(post:id:2080659774136291424:x.com:product)>"],
    ["k", "post:id"],
    ["i", "post:id:2080659774136291424"],
    ["s", "x.com"],
    ["c", "product"],
    ["l", "genuine"],
    ["score", "100"]
  ],
  "content": "Does what the listing says."
}

material = post:id:2080659774136291424:x.com:product (separate slot from generic time-worth). Exact-c resolve only.

Fine-tuned decimal#

Machine aggregate or a precise UI, still one author's slot:

{
  "kind": 32014,
  "pubkey": "<statement-author>",
  "created_at": 1672531600,
  "tags": [
    ["d", "<sha256(post:id:2080659774136291424:x.com:)>"],
    ["k", "post:id"],
    ["i", "post:id:2080659774136291424"],
    ["s", "x.com"],
    ["score", "50.598334"]
  ],
  "content": ""
}

Same generic slot as the four-star example; this event replaces 80.

Client behavior#

Clients SHOULD:

  • sync 32014 only for authors already in the local 32009 trust set (plus the user's own events), after the trust BFS freezes in interval mode, or as a separate live subscription that never expands hops; Subscribe all requests kind 32014 with #s=x.com and no author filter;
  • never add kind 32014 to trust-graph frontier expansion;
  • resolve one newest valid event per (author, d);
  • cancel with ["score", ""] on the same subject, scope, and c;
  • treat "0" as an active low rating, not a cancel;
  • resolve exact c only — no parent or global fallback;
  • treat ratings as author-scoped scores, not global truth;
  • for scroll: average active score values from trusted issuers at the nearest hitting degree (same stop rule as kind 32009) into one indicator (high → invest time, low / 0 → skip);
  • keep labels and content off the hot path; show them on tap-through;
  • bound ingest against Sybils the same way as trust sync.

Attention MAY hide items with trusted spam / ai-slop labels even when another trusted rater scored high. That policy is product, not a protocol MUST. See wot-questions.md.

Security considerations#

  • Numeric scores are cheap to publish; do not treat them as Sybil-resistant without a 32009 aggregation policy.
  • Results are root- and policy-relative. Never present a WoT average as an objective score.
  • Empty score (no opinion) MUST NOT be confused with "0" (worth no time).
  • Do not silently apply 32009 context fallback to 32014.
  • Interpret user:id / post:id together with s.
  • Compromised author keys can replace all of that author's rating slots.
  • A rating of a person is not trust and MUST NOT open access.

Implementation rationale#

One subject per event keeps slots simple. Kind 32014 isolates rating traffic from trust. d reuses kind 32009 material so empty s or c cannot collide, while score and l stay out of d so a review can be updated without forking slots. Cancel is an empty score so the closed interval [0, 100] stays numeric: 0 means worth no time, empty means retract. Quick stars sit on 20-point steps (20–100) so a five-star control maps cleanly onto that scale. Derived ranks belong in NIP-85, not here.

References#