docs / how it works

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:

  1. Parse the canonical message. Reject if the header literals aren't exact or extensions aren't lexicographically sorted.
  2. Verify the BIP-322 signature against the declared address.
  3. Compute sha256(msg); confirm it matches attestation_id.
  4. Query public Bitcoin explorers for current UTXOs at the address.
  5. Compute sats_bonded and days_unspent from live chain state.
  6. 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_zero or bond_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 d tag. 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