oc · docs
nostr publication

Nostr directory

Publishing to Nostr is optional. The envelope is self-contained and can travel over any transport (URL fragment, email, IPFS, QR code, paper, git). Nostr is one choice among many — a good one for durable public discovery.

Event kind

Kind 30083. Parameterized-replaceable, in the OrangeCheck family's 30078–30115 range. OC Agent delegations share it under the disjoint oc-agent-del: prefix; agent actions are on 30084, not here. The full family table is on Nostr publication.

KindClaimed by
30078OrangeCheck attestations + OC Lock device records
30080OC Vote poll
30081OC Vote ballot
30082OC Vote reveal
30083OC Stamp envelope + OC Agent delegation
30084OC Agent action

We deliberately did NOT reuse 30078. Relays that index by (kind, d-tag) as a compound key would see prefix collisions across protocols over time. Clean namespace per protocol is the rule.

Event shape

event.kind       = 30083
event.tags       = [
  ["d", "oc-stamp:" || id],
  ["t", signer.address],
  ["t", content.hash],
  ["t", "oc-stamp"]
]
event.content    = <canonical JSON of envelope>
event.pubkey     = <ephemeral Nostr pubkey, any key>
event.created_at = unix_seconds

The Nostr event pubkey has no relationship to the Bitcoin identity. The authenticity of the stamp is proved by the BIP-322 signature inside the envelope, not by the Nostr author. A fresh ephemeral Nostr keypair SHOULD be derived per-publish:

nostr_sk := HKDF(ikm=random(32), salt="oc-stamp/v1/nostr-key", info=id, L=32)

This avoids asking wallets to hold Nostr keys.

Discovery tags are t tags because relays index single-letter tag names only. A filter on a longer name (#addr, #hash) is rejected by most relays — they close the request, so read that as "no answer", never as "no stamps". Tags are unsigned relay metadata: read the address and hash from the envelope.

Discovery

By content hash

REQ { "kinds": [30083], "#t": ["sha256:<hex>"] }

Use case: "has anyone stamped these bytes?"

Stamps published before 2026-09-24 carry no content-hash t tag. Find those by signer address and match content.hash after fetching.

By signer address

REQ { "kinds": [30083], "#t": ["bc1q…"] }

Use case: "all stamps by this signer."

By envelope id

REQ { "kinds": [30083], "#d": ["oc-stamp:<id>"] }

Use case: "fetch this specific stamp."

Every stamp

REQ { "kinds": [30083], "#t": ["oc-stamp"] }

Use case: "stamps only, not the OC Agent delegations that share the kind."

There is no prefix query. Relays match tag values whole, so "#d": ["oc-stamp:"] matches no event, and a relay answers it with an empty result rather than an error. Use #t or a full #d.

A relay match only nominates candidates. Kind 30083 also carries OC Agent delegations, and anyone can tag an event with any address or hash, so check that the envelope's kind is stamp and its content.hash or signer.address is the one you asked for, then verify it, before using it.

Addressability + upgrades

Kind-30083 is addressable — relays replace prior events with the same (pubkey, kind, d-tag) tuple.

For OC Stamp this is harmless in practice:

  • The d-tag is content-addressed (oc-stamp:<id> where id is a hash of the canonical message).
  • A "replacement" with the same d-tag must carry the same id.
  • Same id → same signed content. Any attempt to swap in different content fails verification on the consumer side.

The common reason to republish with the same d-tag is an OTS upgrade: pending → confirmed. Verifiers that see multiple versions of the same id SHOULD accept the one with ots.status === "confirmed" and the latest upgraded_at.

The reference app uses the @orangecheck/nostr-core default set:

  • wss://nos.lol
  • wss://relay.primal.net
  • wss://offchain.pub
  • wss://relay.snort.social
  • wss://relay.ochk.io

Clients SHOULD publish to at least three diverse relays. A hostile relay set can refuse to publish or return your envelope, but cannot forge it — BIP-322 verification is relay-independent.

Why Nostr is optional

Envelopes are self-contained JSON. Put them:

  • In a URL fragment (stamp.ochk.io/v#<base64url>).
  • Alongside the content (blogpost.md + blogpost.md.stamp).
  • In IPFS / Arweave / any CAS.
  • In a git repo (.git/stamps/ directory).
  • On paper, via QR code.
  • In a Nostr event (this page).

No single directory is load-bearing. The ochk.io/stamp.ochk.io operators do not run a privileged directory — the reference aggregator uses the same Nostr relays anyone else would.

Further reading

  • SPEC §7 — normative rules. §7.3 says when a relay has answered: only EOSE with on-filter events counts, and not hearing back is never "no stamps".
  • SPEC §13 — the 30078–30115 family kind registry.