How it works
Three steps. One primitive. No hand-waving.
1. Sign
A Bitcoin wallet signs a canonical UTF-8 text message — exact wording, exact line order, single trailing LF. Any deviation invalidates the signature.
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.
bond: 1000000
The message is signed with BIP-322 (all address types) or legacy signmessage (P2PKH only). The signature plus the full message is the proof.
The attestation ID is SHA-256(canonical_message) — a content-addressed 64-char hex string. Same message → same ID, deterministically.
See The canonical message for the strict spec.
2. Publish
Publishing is optional. A proof is a self-contained JSON envelope that works anywhere — in a QR code, a Nostr profile field, an HTTP server, a gist.
For decentralised discovery, proofs SHOULD be published to Nostr as kind 30078 parameterised-replaceable events:
{
"kind": 30078,
"tags": [
["d", "<attestation_id>"],
["address", "<bitcoin_address>"],
["scheme", "bip322"],
["issued_at", "<rfc3339>"],
["i", "github:alice"],
["i", "nostr:npub1alice..."]
],
"content": "<full_envelope_json>",
"pubkey": "<nostr_pubkey>",
"sig": "<nostr_event_signature>"
}
Any verifier can then query any of: #d (by id), #address (by Bitcoin address), or #i (by bound handle). See the Nostr NIP for the full wire format.
3. Verify
Any verifier — the hosted /api/check, the SDK's verify(), a relay plugin, your own server — does six deterministic steps:
- Parse the canonical message. Reject if the header literals aren't exact or extensions aren't lexicographically sorted.
- Verify the BIP-322 signature against the declared address.
- Compute
sha256(msg); confirm it matchesattestation_id. - Query public Bitcoin explorers for current UTXOs at the address.
- Compute
sats_bondedanddays_unspentfrom live chain state. - Apply relying-party policy (
aud:must match origin?expires:in the past?) and thresholds (min_sats,min_days).
Everything except step 4 is offline-verifiable. Step 4 can be done against any Esplora-compatible explorer — mempool.space, blockstream.info, your own node.
See Verification for the full algorithm.
The gate decision
Most integrators never directly see the message or signature. They call:
curl "https://ochk.io/api/check?addr=bc1q...&min_sats=100000&min_days=30"
…and get back a pass/fail with metrics. The API does steps 1–6 above behind a 60-second cache.
Revocation
Two ways. Both are deliberate.
- Implicit — spend the bonded UTXOs. Next verification reports
bond_zeroorbond_insufficient. Every consumer sees it within a block. - Explicit replacement — publish a new attestation at a fresh address, or a new event with the same
dtag. Replaces the old publisher intent.
There is no explicit revocation event kind. The chain state is authoritative; nothing else needs to be.
What's next
- The canonical message — exact wire format and ABNF.
- Identity bindings — what the
identities:field means. - Verification — the full algorithm with edge cases.