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 2 —
identities:followed by a comma-separated list ofprotocol:identifierpairs, sorted lexicographically by the full pair string. Empty allowed. - Line 3 —
address:followed by a Bitcoin mainnet singlesig address (bc1q,bc1p,1…, or3…). Testnet/signet allowed with thenetwork:extension. - Line 4 — exact literal.
- Line 5 —
nonce:+ 32 lowercase hex characters (16 random bytes). - Line 6 —
issued_at:+ RFC-3339 UTC timestamp withZsuffix. - 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:
| Key | Type | Meaning |
|---|---|---|
aud | origin | RP MAY require equality to its own origin. |
bond | int (sats) | Declared bond. Verifiers MUST fail if confirmed balance < bond. |
expires | RFC-3339 UTC | Verifiers SHOULD warn or reject when in the past. |
network | mainnet / testnet / signet | Defaults to mainnet. |
scope | free-form label | Advisory; 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+ randomnonce+issued_atbound 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.