oc · docs
bonds

Bonds

An attestation publishes an address, and with it a balance and a history. A bond keeps the coin private. The holder signs once to back one account with one unspent coin. Apps the person signs in to learn only that a coin of at least N sats, unspent at least D days, backs them. They also get a number that's the same for that coin at their app and unrelated everywhere else.

The coin stays in the holder's wallet. Spending it ends the backing. Nobody needs a bond to sign in: an account made with email, Google, GitHub or a wallet signs in everywhere without one. Normative spec: BOND.md.

The message

orangecheck-bond
address: bc1q…
outpoint: <txid>:<vout>
subject: did:oc:…
nonce: <32 hex>
issued_at: 2026-10-16T12:00:00Z
expires_at: 2026-10-16T12:10:00Z
ack: I back this subject with this coin. It stays in my wallet.

The coin's own address signs it with BIP-322. issued_at and expires_at bound the signing window, at most an hour, not the bond. bond_id is the SHA-256 of the message bytes. The header differs from orangecheck and orangecheck-auth, so no signature for one can be replayed as another.

Verifying

import { esploraSource, verifyBond } from '@orangecheck/sdk';

const r = await verifyBond({
    message,
    signature,
    subject: didOc,
    source: esploraSource(),
});
// r.ok → r.bond: { bond_id, subject, address, outpoint, sats, confirmed_height, tip }
// else r.reason: sig_invalid | coin_spent | coin_too_recent | coin_address_mismatch | …

The coin must be confirmed, at least 6 blocks deep, unspent, and an output of the signing address. ochk.io also refuses a coin under 1,000 sats, the lowest floor (coin_too_small).

A bond lasts exactly as long as its coin. ochk.io re-checks the coin when its last check is over ten minutes old, and every claim says when that was. checkBondCoin does that re-check. Python: orangecheck ≥ 2.2 has the same message, id, nullifier and floors.

One coin, one account

A coin backs one account at a time. The same account can back with it again at once. Released from one account, it can back a different account only 30 days later (coin_backed_another_recently), the longest a token carrying it can live, so two accounts never present the same coin at once.

What an app sees

"oc_bond": {
  "v": 1,
  "sats_floor": 100000,
  "days_floor": 30,
  "height": 921044,
  "checked_at": 1792200000,
  "nullifier": "5b1d…"
}
  • Floors, not amounts: 1000 · 10000 · 100000 · 1000000 · 10000000 sats and 7 · 30 · 90 · 365 days, 0 below the lowest.
  • nullifier is HMAC-SHA256(registry key, "orangecheck-bond-nullifier\n" + audience + "\n" + outpoint). Only ochk.io holds the key, so an app can't run every coin on chain through the formula to find the one behind its users. The same coin at your app gives the same number, across accounts and re-backing. Another app gets an unrelated one. Sites on *.ochk.io share one family session, so they share one nullifier (audience https://ochk.io).
  • checked_at and height say when ochk.io last found the coin unspent. You can't ask for a fresh check. You can refuse a stale claim: bondMeets(payload, { minSats, minDays, maxAgeSecs }) from @orangecheck/auth-core.

Keep one account per nullifier. That is the real bot check at your app: a floor says some coin backs the account, and the nullifier tells you whether that coin already backed another account of yours.

A registry computes the nullifier with bondNullifier(key, audience, outpoint) from @orangecheck/sdk ≥ 1.13. The key comes first, and only the registry has it.

Slashing without custody

Ban a nullifier and you've banned the coin. A new account or backing again won't get past it. Moving the coin will, but a moved coin is new and starts aging from zero, so it falls below every age floor again. Nothing is confiscated: the coin stays in the holder's wallet, and abuse costs time.

Privacy

WhoLearns
The registry (ochk.io)the address, coin, amount and age; it holds the key
An appfloors, when the coin was last checked, its own nullifier
The chainnothing; the signature is never published

Back with a coin at an address you use for nothing else. A zero-knowledge method that removes the registry from the picture is future work in the spec (§10).

Where people back their account

  • ochk.app: Account → Backing.
  • ochk.io: Dashboard → Identity.
  • Any family site: <OcBond /> from @orangecheck/auth-client ≥ 2.30.

To require one at your app, see Who can sign in.