docs / identity bindings

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):

ProtocolVerify byAutomatic?
nostr:npub1…Signed Nostr event containing the attestation ID
github:usernamePublic gist or repo file containing the attestation ID
dns:example.comTXT record at _orangecheck.<domain> containing the attestation ID
twitter:@handlePublic tweet containing the attestation IDmanual (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:identifier string.
  • 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.
  • GitHubcurl https://api.github.com/gists/<gist_id> or https://api.github.com/users/<user>/gists and grep for the attestation ID.
  • DNSdig 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.