Nostr publication
Every OrangeCheck sibling publishes its public artifacts as NIP-78 parameterized-replaceable addressable events. The original kind, 30078, is the namespake of this page and still carries Attest attestations, Lock device records, and Pledge envelopes. As more siblings shipped, the family expanded into the 30078–30099 range with one kind per artifact type. This page is the authoritative roll-up.
Source of truth: the kind table here mirrors
oc-agent-protocol/SPEC.md§4, which is the family's authoritative kind registry. Each protocol's own spec normatively claims the kind(s) it owns.
Kind registry
| Kind | Owner | Purpose |
|---|---|---|
30078 | OC Attest | Attestation envelope (the original family event kind) |
30078 | OC Lock | Device record (binds an X25519 device key to a Bitcoin address) |
30078 | OC Pledge | Pledge envelope, outcome envelope, abandonment envelope (disambiguated by d tag) |
30080 | OC Vote | Poll |
30081 | OC Vote | Ballot |
30082 | OC Vote | Reveal (secret-mode tally) |
30083 | OC Stamp | Stamp envelope |
30083 | OC Agent | Agent delegation (co-claim with OC Stamp; disambiguated by disjoint d-tag prefixes) |
30084 | OC Agent | Agent action (claimed exclusively; reuses OC Stamp envelope structure but on a distinct kind) |
30085 | OC Agent | Agent revocation |
30086 | OC Agent | Agent sub-delegation (v1.1) — chained narrowing of authority |
If you're allocating a new kind for a new sibling, claim the next unused number
(currently 30086) and update the registry in oc-agent-protocol/SPEC.md.
Kinds are never reused across sub-protocols.
Why NIP-78 / kind 30078
NIP-78 is the "application-specific data" event kind, designed precisely for what we need:
- Addressable —
(pubkey, kind, d)uniquely identifies an event across all relays. Republishing with the samedtag replaces the old version. - No reserved format — the
contentfield is application-defined, and we fill it with the JSON envelope each protocol specifies. - First-class in every Nostr client — no custom relay patches required.
The 30080–30085 kinds claimed by Vote/Stamp/Agent are also in NIP-78's
parameterized-replaceable range and behave identically; the only reason they
exist as separate kinds (rather than reusing 30078) is so a relay query like
{kinds: [30080]} returns only polls without the integrator having to
post-filter on the d tag.
d-tag conventions
Each protocol reserves a d-tag namespace so events from different siblings
don't collide on overlapping kinds.
| Protocol | Kind | d tag format |
|---|---|---|
| OC Attest | 30078 | <attestation_id> (64-char hex SHA-256) |
| OC Lock | 30078 | oc-lock:device:<btc_address> (or <addr>:<device_id> for multi-device) |
| OC Pledge | 30078 | oc-pledge:<pledge_id> |
| OC Pledge | 30078 | oc-pledge-outcome:<pledge_id> |
| OC Pledge | 30078 | oc-pledge-abandonment:<pledge_id> |
| OC Vote | 30080 | oc-vote:poll:<poll_id> |
| OC Vote | 30081 | oc-vote:ballot:<poll_id>:<voter> |
| OC Vote | 30082 | oc-vote:reveal:<poll_id> |
| OC Stamp | 30083 | oc-stamp:<envelope_id> |
| OC Agent | 30083 | oc-agent-del:<delegation_id> (co-claim with OC Stamp; disjoint prefix) |
| OC Agent | 30084 | oc-agent-act:<action_id> |
| OC Agent | 30085 | oc-agent-rev:<revocation_id> |
| OC Agent | 30086 | oc-agent-sub:<subdelegation_id> (v1.1) |
The relay doesn't enforce any of this — d is just a string. The convention
matters so that discovery queries (#d: oc-stamp:*, #d: oc-vote:poll:*) can
narrow by artifact type without enumerating kinds.
Required tags per protocol
Each sibling's spec defines which tags MUST appear on its events. These tags
exist to enable secondary-index discovery without parsing the JSON content.
Common across all protocols:
| Tag | Value |
|---|---|
d | The addressable identifier (per table above) |
OC Attest example:
| Tag | Purpose |
|---|---|
address | Bitcoin address the attestation is for |
scheme | bip322 or legacy |
i | One per identity binding, formatted as <protocol>:<identifier> |
expires | ISO timestamp (optional) |
For the exact normative tag set per protocol, consult that protocol's spec:
- OC Attest tags ·
oc-attest-protocolSPEC §3 - OC Lock device record tags ·
oc-lock-protocolSPEC §3 - OC Stamp envelope tags ·
oc-stamp-protocolSPEC §6 - OC Vote tags ·
oc-vote-protocolSPEC §3 - OC Agent tags ·
oc-agent-protocolSPEC §3 - OC Pledge tags ·
oc-pledge-protocolSPEC §16
Author pubkey — real npub vs. ephemeral
OrangeCheck events are authored by one of two key types:
- Real Nostr pubkey — when the user has a NIP-07 extension installed (nos2x, Alby, etc.), we sign with their actual key so they can find their own events in their Nostr client's history.
- Ephemeral key — when there's no NIP-07, the SDK generates a fresh BIP-340 Schnorr keypair per publish, signs the event, and throws the key away. The event is still valid (relays check only that the signature matches the pubkey); the pubkey is just a throwaway. OC Vote derives the ephemeral key deterministically via HKDF so the same poll re-publishes under the same npub.
In both cases, the artifact inside the event's content is cryptographically
bound to the Bitcoin address via BIP-322. Who authored the Nostr event is
independent from who controls the Bitcoin address. The inner artifact is what
matters.
Reading events
Any Nostr client or relay library works. With nostr-tools:
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.damus.io');
// Find an OC Attest attestation by Bitcoin address
const sub = relay.subscribe([{ kinds: [30078], '#address': ['bc1qalice…'] }], {
onevent(event) {
const envelope = JSON.parse(event.content);
// Verify locally — see /attest/verification
},
});
// Find an OC Vote poll by id
relay.subscribe([{ kinds: [30080], '#d': ['oc-vote:poll:<poll_id>'] }], {
/*…*/
});
// Find every OC Stamp by a specific signer (kind 30083, prefix-filtered to
// avoid catching co-claimed OC Agent delegations on the same kind)
relay.subscribe(
[
{
kinds: [30083],
'#d': ['oc-stamp:'],
'#address': ['bc1qsigner…'],
},
],
{
/*…*/
}
);
// Find every OC Agent delegation issued to a given agent
relay.subscribe(
[{ kinds: [30083], '#d': ['oc-agent-del:'], '#agent': ['bc1qagent…'] }],
{
/*…*/
}
);
// Find every revocation of a specific delegation
relay.subscribe(
[{ kinds: [30085], '#delegation': ['<64-hex delegation id>'] }],
{
/*…*/
}
);
Each protocol's reference core wraps these queries with typed helpers
(getAttestationsForAddress, fetchPoll, fetchStamp, fetchDelegation,
fetchPledge) that fan out to multiple relays and deduplicate.
Default relay set
The reference SDKs default to a small set of stable public relays:
wss://relay.damus.iowss://relay.nostr.bandwss://nos.lolwss://relay.snort.social
Override with relays: [...] on any SDK call. For production deployments you
SHOULD query ≥3 distinct relay operators — a single slow or partitioned
relay shouldn't break discovery. See
Security — relay censorship / partition
for why this matters.
See also
- Canonical message — what goes inside each
event's
contentfield - BIP-322 signing — what ties the Nostr event to the Bitcoin address
- Security model — relay assumptions and partition mitigation