NIP-32014
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.
Canonical shape (recommended)#
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
| Tag | Role | Example |
|---|---|---|
i | Typed subject — required for this shape | post:id:2080659774136291424 |
score | Rating or cancel — required tag | 80 / 0 / 50.598334 / (empty) |
k | Typed subject prefix (#k filter) — optional | post:id |
s | Domain/namespace (#s filter; also in d) — optional | x.com |
c | Purpose (also in d) — optional; omit = generic | (omit) / product |
l | Augments the rating with further clarification — not in d | insightful / spam |
x | Activation time (Unix seconds) — optional | 1672531600 |
y | Expiration time (Unix seconds) — optional | 1672618000 |
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. PositivepMAY be a traversal edge. Rating is never a traversal edge. Kind32009uses emptyvfor Delete and"0"for Neutral — the same empty-vs-zero split as this kind'sscore. - Kind
3: following, not rating. - NIP-85: derived provider outputs (
rank, …). Not a personal rating input. Do not publish “I score this 80” as kind30385. - Kind
32010: earlier multi-subject trust draft. - Upstream 32014 (Trust / 32010
d): same intent and empty-scoretombstone; different slot algebra (dmaterial).
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, ori; - exactly one
scoretag (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 ind); - at most one
x; - at most one
y(if bothxandyare present,xMUST 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
psubject on32014is terminal. It MUST NOT be used as a traversal edge. eandiremain terminal (same as32009).
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 value | Meaning |
|---|---|
"" | 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
scoreis only cancel."0"and"0.0"(canonical"0") are active ratings, not tombstones. - Active scores MUST be in [0, 100]. Values below
0or above100are invalid. - Decimals are allowed in
[0, 100]for fine aggregation, e.g."50.598334".0,20, and100MAY be written as integers or with a fraction (canonical form drops trailing zeros). - Canonical form: no scientific notation; no leading zeros (
50not050or050.0); optional fractional part with a single.; at most 6 decimal digits. Trailing zeros MAY be omitted (50equals50.0;0.0equals0). 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 replaceabledslot.
Quick star form#
UI default. Integers only:
| Stars | score |
|---|---|
| 1 | 20 |
| 2 | 40 |
| 3 | 60 |
| 4 | 80 |
| 5 | 100 |
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 ,.
| Subject | material |
|---|---|
i | <i> + ":" + scope + ":" + <c> |
p | "p:" + <pubkey> + ":" + scope + ":" + <c> |
e | "e:" + <event-id> + ":" + scope + ":" + <c> |
Examples of material (before hashing):
| Setup | material |
|---|---|
i=post:id:2080659774136291424, s=x.com, no c | post:id:2080659774136291424:x.com: |
i=post:id:2080659774136291424, s=x.com, c=product | post:id:2080659774136291424:x.com:product |
i=user:id:11348282, no s, no c | user: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#
- Signature valid.
- Exactly one
d, one subject (p/e/i), onescoretag; the subject's second element is the primary subject. - At most one
k,c,x,y. kwithoutiis invalid. Ifkis present withi:kequals the typed prefix.user:id/post:idvalue (in the primaryielement) is decimal digits.- Every additional
p/e/ielement is a structured hint or a canonical barenpub1…. - Each
sandlvalue (when present) is non-empty and within grammar / size limits. scoreis exactly""(cancel) or a canonical number in[0, 100](optional fraction, ≤ 6 decimal digits, no scientific notation, no leading zeros). Whitespace-only is invalid.- No
vtag is required; if present, clients MUST ignore it for rating meaning (kind32009ownsv). - Time tags integer;
x <= yif both. - Non-empty
ccanonical ≤ 128 bytes. - Recalculated
dequals publisheddwithout using hints orl. - 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
32014only for authors already in the local32009trust 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 kind32014with#s=x.comand no author filter; - never add kind
32014to trust-graph frontier expansion; - resolve one newest valid event per
(author, d); - cancel with
["score", ""]on the same subject, scope, andc; - treat
"0"as an active low rating, not a cancel; - resolve exact
conly — no parent or global fallback; - treat ratings as author-scoped scores, not global truth;
- for scroll: average active
scorevalues from trusted issuers at the nearest hitting degree (same stop rule as kind32009) into one indicator (high → invest time, low /0→ skip); - keep labels and
contentoff 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
32009aggregation 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
32009context fallback to32014. - Interpret
user:id/post:idtogether withs. - 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#
- NIP-32009 — trust statements,
dmaterial, subjects. - wot-questions.md — the two questions.
- NIP-01 — events, addressables.
- NIP-73 —
i/kinspiration. - NIP-85 — derived assertions.
- Upstream kind 32014 — earlier rating draft on the 32010 slot model.