Content-addressed names: RFC 6920 ni: URIs over canonical CBOR, with worked hashes
A content address should name a value, not one spelling of it. The 561 Group's draft specification of the semiotic content address (SCA) names a piece of data by the RFC 6920 ni:///sha-256;… URI of the SHA-256 digest of its deterministic CBOR encoding, after Unicode NFC normalization of every string and key. Two JSON objects that differ only in key order, or two strings that differ only in composed versus decomposed accents, then have the same address; hashing the raw JSON text, the common shortcut, gives them different ones. This article works the examples with real hashes you can reproduce in a terminal.
What is an RFC 6920 ni: URI?
RFC 6920 defines "Naming Things with Hashes". Its ni: URI scheme names content by a digest. The Group uses the SHA-256 form only, with an empty authority:
ni:///sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGkAfter sha-256; comes the 43-character unpadded base64url encoding of the 32-byte digest. The specification adds one strict rule: the last character must be one of the sixteen canonical final sextets, so four spellings cannot name one digest. A validator that accepts non-canonical padding bits allows aliasing, which defeats the point of a name. You can produce the name above yourself:
ni() { openssl dgst -sha256 -binary | basenc --base64url | tr -d '=' | sed 's|^|ni:///sha-256;|'; }
printf 'Hello World!' | ni
# ni:///sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGkThat name commits to exact bytes. It is a hash of a file. For structured data it is not yet enough.
Why isn't hashing the JSON text enough?
Take the same two-field object written two ways:
printf '{"name":"x402","version":2}' | ni
# ni:///sha-256;W6OZAGyazZCr7el9hfVf59AR6VwuIIE49ET6d2xPcGk
printf '{"version":2,"name":"x402"}' | ni
# ni:///sha-256;-kcErArIKfOq9rm9ns6QzSPYFnI-eoFAr2qVqiDWnzgThey are the same value and two different names. Whitespace, key order and escape choices each mint a new hash. The Group's draft calls out the existing instances in its own tree as defects: ni: names minted over JSON.stringify output are "valid RFC 6920 names of those bytes, but key-order dependent, so not names of the value". The remedy is to hash a canonical form, in two steps.
How is a value canonicalized?
Step 1: Unicode NFC. Every string and every map key is normalized to NFC. A key collision introduced by normalization is rejected rather than merged. The composed é (U+00E9) and e followed by a combining acute (U+0301) become one string.
Step 2: deterministic CBOR. The value is encoded with RFC 8949 core deterministic encoding: shortest-form heads, definite lengths, map keys sorted by their encoded bytes, shortest exact floats, no tags. A decoder refuses non-canonical bytes. Our object becomes 20 bytes:
a2 64 6e616d65 64 78343032 67 76657273696f6e 02That is a 2-entry map (a2), the 4-character text name (64 6e616d65) with value the 4-character text x402 (64 78343032), and the 7-character text version (67 76657273696f6e) with value the integer 2 (02). Hash it and name it:
ni:///sha-256;S14vjN44zabSMQb8L_7tqK6kAI2gtzu-I_4lb58v930Written in either key order, the value encodes to those same 20 bytes, and so has that one address. You can check it:
printf '\xa2\x64name\x64x402\x67version\x02' | ni
# ni:///sha-256;S14vjN44zabSMQb8L_7tqK6kAI2gtzu-I_4lb58v930The same holds for Unicode. The value {"café": 1} written with a precomposed é and with e plus U+0301 has one address under this scheme, ni:///sha-256;1Nl25GfeDsGRq0U2MSY4k-GJX0eSCrT69KwzucuPHZQ, whereas hashing the JSON text of the two spellings gives two unrelated names.
We confirmed our encoder against the reference implementation's published test vectors: the canonical encodings of null, true and 0 hash to ni:///sha-256;sLKYi2u-ckus2l6eUkc23gvH2uQcRrQhPFDh011OXxM, …J6ve3f6FA0lq3rYjRmyqR9pfY6vSvG-hn2z8tz7P7XA and …bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0 respectively, and the reference's own tests assert that the NFC and NFD spellings of café have equal digests. The two-field example above was computed with a short independent encoder, not the reference code, so treat the example addresses as illustrations that you can verify rather than as vectors from the reference suite.
What does the specification add on top?
The draft (version 0.1.0, dated 2026-09-23, status "realized in part") describes three layers.
- Carrier commitment (data). A semiotic carrier is a JSON-shaped value: null, booleans, integers, big integers, NFC strings, byte strings, arrays and plain objects with string keys.
semanticId(value)is theni:name of its canonical CBOR. This layer is implemented and has test vectors. - Semantic identity (code). The identity of a typed claim is the commitment of a normalized envelope with fields
kind,version,objectKind,semanticType,normalizationProfile,settlementProfile,settledBodyandwitnessRoot(theni:name of the evidence bundle that the settlement consumed). Two envelopes are the same content exactly when their bodies reduce to the same normal form under the same type, kind, profiles and witness root. This layer needs the Group's expression notation and is partly implemented in its runtime. - Proof-path coordinate (code). A readable resource name whose segments are generating, witnessing or obstructing steps (
A.B,A:B,A-B), with a triple of surface, token and claim that must agree. The draft says plainly that "every protocol surface (HTTP route, MCP resource URI, ActivityPub IRI, package export key, filesystem path) is a reading of an address term", and that the requirement that all readings agree "is a formal obligation, not yet a theorem". We include it for completeness; it is research, not a shipped feature.
One rule runs through all three: a token is opaque. No consumer decodes an address to learn what it names; it reads the claim or carrier the address commits to and verifies the commitment.
What does not count as an address?
The draft lists its own non-conformances, which is the most credible part of it:
ni:names overJSON.stringifyoutput (key-order dependent), found in resource reservations and package audits.ni:///sha-256;<hex>strings, which are not RFC 6920 (hexadecimal instead of base64url) and fail the validator; they appear in a journal of semantic records and tool evidence, and converting them "is a migration of those journals, not a renaming".- Bare
sha256:<hex>strings andurn:identifiers, which belong to the protocols that define them (OCI digests, EVM events) and are never house content names.
We publish this because a name that cannot be checked is not evidence. If a record says it is addressed, a reader should be able to recompute.
Where do these addresses appear in public?
- Every offer in the market catalog carries a
sku, acelland alaw, each anni:///sha-256;…name (for example, the JSON Pointer offer's SKU isni:///sha-256;fAnZehn7tM6Qdq9XoP2A8cv7zISE0iPitrf0iq9yn5oin the live catalog). - The ActivityPub outbox of that market publishes each offer as a document of type
application/sen+cbornamed by its address (One catalog, five machine faces). - This site. Each article's JSON-LD includes an
identifierthat is theni:name of its cell (how the site is built). - Public challenge observations. The x402 boundary reference returns an
observationDigestas anni:name over the normalized challenge it saw (payee verification). - Six paid content-address offers create, inspect and verify SHA-2 multihashes and CIDs (base conversion, raw-block CIDs), at $0.0012 per call. They state their limits: "There is no upload, pinning, retrieval or authorship claim", and raw verification refuses DAG and UnixFS.
A public consumer of the carrier-address function is the MIT-licensed cell-identity-verification-service, which addresses capability-cell manifests and refuses undefined members, fractional numbers, malformed Unicode, __proto__ keys, NFC key collisions, cycles and hidden properties.
What are the caveats?
The specification is a draft with a declared stability of "draft". Number handling is its sharpest edge: the written text admits integers, while the current reference implementation also admits finite floats and encodes them as shortest exact floats, so check the version when you depend on a float. A content address proves that two carriers are the same value; it does not prove who made them or that they are true. Semantic identity (layer 2) is meaningful only relative to a normalization profile and a settlement profile, and two envelopes with different profiles do not become equivalent by sharing digest syntax. Finally, several of the repositories involved are not public; we describe behavior from source and tests we ran, and the examples in this article are reproducible without any access.
Continue to evidence in the publishing pipeline for where the address meets web publication.