live · mainnetoc · docs
specs · api · guides
docs / nostr publication

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

KindOwnerPurpose
30078OC AttestAttestation envelope (the original family event kind)
30078OC LockDevice record (binds an X25519 device key to a Bitcoin address)
30078OC PledgePledge envelope, outcome envelope, abandonment envelope (disambiguated by d tag)
30080OC VotePoll
30081OC VoteBallot
30082OC VoteReveal (secret-mode tally)
30083OC StampStamp envelope
30083OC AgentAgent delegation (co-claim with OC Stamp; disambiguated by disjoint d-tag prefixes)
30084OC AgentAgent action (claimed exclusively; reuses OC Stamp envelope structure but on a distinct kind)
30085OC AgentAgent revocation
30086OC AgentAgent 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 same d tag replaces the old version.
  • No reserved format — the content field 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.

ProtocolKindd tag format
OC Attest30078<attestation_id> (64-char hex SHA-256)
OC Lock30078oc-lock:device:<btc_address> (or <addr>:<device_id> for multi-device)
OC Pledge30078oc-pledge:<pledge_id>
OC Pledge30078oc-pledge-outcome:<pledge_id>
OC Pledge30078oc-pledge-abandonment:<pledge_id>
OC Vote30080oc-vote:poll:<poll_id>
OC Vote30081oc-vote:ballot:<poll_id>:<voter>
OC Vote30082oc-vote:reveal:<poll_id>
OC Stamp30083oc-stamp:<envelope_id>
OC Agent30083oc-agent-del:<delegation_id> (co-claim with OC Stamp; disjoint prefix)
OC Agent30084oc-agent-act:<action_id>
OC Agent30085oc-agent-rev:<revocation_id>
OC Agent30086oc-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:

TagValue
dThe addressable identifier (per table above)

OC Attest example:

TagPurpose
addressBitcoin address the attestation is for
schemebip322 or legacy
iOne per identity binding, formatted as <protocol>:<identifier>
expiresISO timestamp (optional)

For the exact normative tag set per protocol, consult that protocol's spec:

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.io
  • wss://relay.nostr.band
  • wss://nos.lol
  • wss://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