Identity bindings
The identities: field inside a signed canonical message is a comma-separated list of self-asserted handle claims. It is the bridge between a Bitcoin address and external identity namespaces.
identities: github:alice,nostr:npub1alice...,twitter:@alice
Claims, not proofs
A signed attestation tells you:
- The address-holder controls the Bitcoin address. (Mathematically proven by BIP-322.)
- The address-holder chose to assert these handles. (Self-asserted inside the signed message.)
It does not prove the signer also controls those handles.
If your application's security depends on "is this really GitHub user alice?", you must verify out-of-band.
Supported protocols
Four are first-class (all have out-of-band verification strategies):
| Protocol | Verify by | Automatic? |
|---|---|---|
nostr:npub1… | Signed Nostr event containing the attestation ID | ✅ |
github:username | Public gist or repo file containing the attestation ID | ✅ |
dns:example.com | TXT record at _orangecheck.<domain> containing the attestation ID | ✅ |
twitter:@handle | Public tweet containing the attestation ID | manual (tweet URL needed) |
Other strings (email:…, did:…, web:…, custom) are preserved but not first-class. Verifiers MAY ignore them.
Format rules
- Protocol names are lowercase alphanumeric.
- Identifier is any printable ASCII — but identifier must not contain a comma.
- The full list is sorted lexicographically by the
protocol:identifierstring. - Maximum total length: 512 UTF-8 bytes.
- Empty allowed:
identities:(single space, no pairs). - Duplicates allowed (e.g. two GitHub accounts).
Why sorted?
The sort order is fixed so any tool that produces the same set of bindings produces the same canonical message — and therefore the same attestation ID. Two clients building "the same" proof independently must produce byte-identical messages.
Verifying a binding
Use the SDK:
import { verifyIdentity } from '@orangecheck/sdk';
const r = await verifyIdentity({
protocol: 'github',
identifier: 'alice',
attestationId: envelope.attestation_id,
proof: 'https://gist.github.com/alice/abc123',
});
if (r.verified) {
// gist contains the attestation ID → handle ownership proven
}
Or do it manually per-protocol:
- Nostr — query relays for
{"kinds":[1,30078], "authors":["<hex-of-npub>"], "#e":["<attestation_id>"]}or look for a kind-1 note from that pubkey containing the attestation ID as plaintext. - GitHub —
curl https://api.github.com/gists/<gist_id>orhttps://api.github.com/users/<user>/gistsand grep for the attestation ID. - DNS —
dig TXT _orangecheck.<domain>; any answer that equals the attestation ID proves it. - Twitter — the tweet URL is user-supplied; the app scrapes the tweet text for the attestation ID.
Multi-handle strategy
A user binds many handles into one proof when they want the same cryptographic commitment to back each handle. A gate on a GitHub-gated forum and a Nostr-gated relay can both honor the same underlying proof.
Conversely, a user may issue separate attestations for separate contexts when linkability matters. The identities: field is small and self-contained; nothing forces you to put every handle in one proof.
Privacy
Every bound handle publicly links the Bitcoin address to that handle. Use:
- Fresh, single-purpose addresses per attestation if linkability matters.
- Empty
identities:for pseudonymous proofs ("someone holds this bond"). - Scoped attestations — separate proofs for different contexts instead of one mega-proof.
Why not verify handles inside the protocol
Two reasons. First, every handle verification depends on an external system (GitHub API, DNS resolver, Twitter scraping) that the protocol itself has no control over — baking it in would couple the protocol to platform-specific APIs. Second, the relying party is the right place to decide what handle verification means for their app — a forum cares about GitHub, a DNS-gated API cares about DNS; neither needs the protocol's opinion.