docs / the canonical message

The canonical message

The canonical message is the exact UTF-8 text that a Bitcoin wallet signs. It is strict by design — any deviation invalidates the signature. This strictness is what lets any verifier re-derive the same attestation ID from the same inputs, without negotiation.

Core format

Seven lines. LF line endings. Exactly one trailing LF. Field wording and order are fixed.

orangecheck
identities: <IDENTITY_BINDINGS>
address: <BITCOIN_ADDRESS>
purpose: portable reputation attestation (non-custodial)
nonce: <RANDOM_16B_HEX_LOWER>
issued_at: <ISO8601_UTC_Z>
ack: I attest control of this address and bind it to my identities.

Rules

  • Line 1 — exact literal orangecheck. No version number.
  • Line 2identities: followed by a comma-separated list of protocol:identifier pairs, sorted lexicographically by the full pair string. Empty allowed.
  • Line 3address: followed by a Bitcoin mainnet singlesig address (bc1q, bc1p, 1…, or 3…). Testnet/signet allowed with the network: extension.
  • Line 4 — exact literal.
  • Line 5nonce: + 32 lowercase hex characters (16 random bytes).
  • Line 6issued_at: + RFC-3339 UTC timestamp with Z suffix.
  • Line 7 — exact literal.

Issuers MUST prevent edits after generation and MUST emit exactly one trailing LF.

Extensions

Optional key: value lines follow the core, sorted lexicographically by key. Every extension is part of the signed message.

Registered keys:

KeyTypeMeaning
audoriginRP MAY require equality to its own origin.
bondint (sats)Declared bond. Verifiers MUST fail if confirmed balance < bond.
expiresRFC-3339 UTCVerifiers SHOULD warn or reject when in the past.
networkmainnet / testnet / signetDefaults to mainnet.
scopefree-form labelAdvisory; human-readable context.

Unknown extensions MUST be ignored safely by verifiers unless local policy opts in.

Example with extensions

orangecheck
identities: github:alice,nostr:npub1alice...
address: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
purpose: portable reputation attestation (non-custodial)
nonce: a1b2c3d4e5f6789012345678901234ab
issued_at: 2026-04-20T12:00:00Z
ack: I attest control of this address and bind it to my identities.
aud: https://example.com
bond: 1000000
expires: 2027-04-20T12:00:00Z
scope: forum-post

Why it's this strict

  • Determinism. Any deviation produces a different SHA-256, so verifiers never accept a message the signer didn't sign.
  • Offline-verifiability. Given only (addr, msg, sig), anyone with a Bitcoin library can verify — no negotiation with a server.
  • Replay resistance. Fixed header + purpose + random nonce + issued_at bound a signature to its issuance context. A message that verifies for this purpose can't be re-used for another.

Attestation ID

attestation_id = SHA-256(canonical_message)  // 64 lowercase hex chars

Content-addressed. URL-safe. Universal. Two clients cannot produce the same ID for different messages.

What you never put in the message

  • API keys, secrets, or session tokens.
  • Personal data not intended to be public forever.
  • Anything that rotates. The message is immutable once signed.

ABNF

message         = core extlines LF
core            = "orangecheck" LF
                  "identities: " identities LF
                  "address: " addr LF
                  "purpose: portable reputation attestation (non-custodial)" LF
                  "nonce: " nonce LF
                  "issued_at: " isotime LF
                  "ack: I attest control of this address and bind it to my identities." LF
extlines        = *( extline )
extline         = key ": " value LF
key             = 1*( %x61-7A )                     ; a-z
value           = *( %x20-7E )                      ; printable ASCII (UTF-8 allowed)
identities      = [ identity-binding *( "," identity-binding ) ]
identity-binding = protocol ":" identifier
nonce           = 32hexdig-lower
LF              = %x0A

Full normative spec: protocol spec.