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–30115 range with one kind per artifact type. This page is the public roll-up.
Source of truth: the family kind registry (
KINDS.mdin the workspace), mirrored inoc-agent-protocol/SPEC.md§4. 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) |
30078 | OC Chat | Device record (oc-chat:device:) |
30079 | — | Archived; do not use |
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 |
30087 | OC Me | me.ochk.io billable-event, payment, rebind, payout-binding, distribution and drop envelopes |
30088–30093 | — | Free |
30094–30109 | — | Reserved, unratified (OC Find, an unbuilt research idea) |
30110 | OC Chat | Channel descriptor |
30111 | OC Chat | Channel post |
30112 | OC Chat | Seal / block-height anchor |
30113 | OC Chat | Reserved (group-key rotation, private channels) |
30114 | OC Chat | Discoverability directory |
30115 | OC Chat | Reserved (pay-to-post postage policy) |
OC Chat direct messages travel as NIP-59 gift wraps (kind 1059), a standard Nostr kind rather than a family allocation.
If you're allocating a new kind for a new sibling, claim the next free number
(currently 30088) and update the family registry and
oc-agent-protocol/SPEC.md §4. 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–30087 and 30110–30115 kinds claimed by Vote, Stamp, Agent, Me and Chat
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) |
A public relay doesn't enforce any of this — d is just a string. Tag filters
match exactly, so #d finds one artifact when you know its full d value;
there is no prefix query. The prefix is what tells you, after fetching, which
protocol an event on a shared kind belongs to. Check it, and the envelope's
kind field, before you use the event.
Required tags per protocol
Each sibling's spec defines which tags MUST appear on its events. Relays index
single-letter tag names only, so the tags a query can use are d and t.
Longer tag names (address, agent, delegation) are for people reading the
event; a filter on one fails (see Reading events).
Common across all protocols:
| Tag | Value |
|---|---|
d | The addressable identifier (per table above) |
OC Attest example:
| Tag | Purpose |
|---|---|
t | The Bitcoin address, and oc-attest — the tags a query uses |
address | Bitcoin address the attestation is for (not indexed) |
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 §7 - OC Vote tags ·
oc-vote-protocolSPEC §3 - OC Agent tags ·
oc-agent-protocolSPEC §9.4 and §10 - 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');
// Every filter below uses `#t` or `#d`. Relays index single-letter tags only.
// Find an OC Attest attestation by Bitcoin address
const sub = relay.subscribe([{ kinds: [30078], '#t': ['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 also carries OC Agent
// delegations: keep events whose `d` starts with `oc-stamp:` and whose
// envelope `kind` is `stamp`.
relay.subscribe([{ kinds: [30083], '#t': ['bc1qsigner…'] }], {
/*…*/
});
// Find every OC Agent delegation naming an address, as principal or agent.
// Keep events whose envelope `kind` is `agent-delegation`, then check which
// side the address is on.
relay.subscribe([{ kinds: [30083], '#t': ['bc1qagent…'] }], {
/*…*/
});
// Find every revocation of a specific delegation (then keep only events whose
// content.delegation_id matches, since `t` also carries the signer's address)
relay.subscribe([{ kinds: [30085], '#t': ['<64-hex delegation id>'] }], {
/*…*/
});
// Find what cites a delegation: its actions (30084), revocations (30085) and
// sub-delegations (30086) all carry its id as `t`. On actions and
// sub-delegations that tag is SHOULD (spec 1.5.0), so this lists only events
// whose publisher set it. Route on the envelope `kind` and check the id.
relay.subscribe(
[{ kinds: [30084, 30085, 30086], '#t': ['<64-hex delegation id>'] }],
{
/*…*/
}
);
A filter on a multi-letter tag name does not quietly return nothing. Most relays
reject the whole request with CLOSED or NOTICE (unindexed tag filter),
taking any other filter in the same request with it, and some ignore the filter
and return unrelated events. So:
- Treat a rejection as "not heard back", never as "nothing there." A relay
has answered only when it sent
EOSEand every event it returned matches your filter. ACLOSED, an errorNOTICE, a dropped connection or a timeout says nothing about what exists. - Decide absence only from relays that answered. This matters most for revocations: if no relay answered, you have not checked revocation, so do not report the delegation as unrevoked. See OC Agent SPEC §10.4.
- Treat every returned event as a candidate. Anyone can publish an event tagged with anyone's address. Verify the envelope inside before using it.
@orangecheck/sdk wraps the attestation query as getAttestationsForAddress.
@orangecheck/nostr-core's queryEvents checks each event's id, signature and
filter match, and reports per relay whether it reached EOSE. OC Agent's core
has no network by design: fetch with any client and pass the envelopes in.
Default relay set
@orangecheck/nostr-core defaults to four public relays plus the family's own:
wss://nos.lolwss://relay.primal.netwss://offchain.pubwss://relay.snort.socialwss://relay.ochk.io
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