Skip to content

How it works

Every event ends in a receipt anyone can check.

Your app reports an event under a user's orangecheck identity. orangecheck signs a receipt that commits the fee split, anchors it to Bitcoin and credits the user's share in sats. You host no key and deploy no file.

The payout rail is in bring-up, so no sats move yet. OrangeCheck holds what accrues until a federation takes over. Who holds what

01

The life of one event

What happens when one of your users does something your app pays for. Every step is content-addressed and checks offline against Bitcoin.

  1. 01

    It happens in your app

    A session opens, a payment authorizes, an attest verifies. Your code calls the SDK with the event and any context it needs, such as the amount of a percent-priced payment.

  2. 02

    orangecheck signs the receipt

    The fields are put in canonical order (RFC 8785), hashed with SHA-256 and signed with orangecheck's Ed25519 envelope key for your project. The signature commits to what you pay. You host no key and deploy no file.

  3. 03

    Your project can co-sign

    Register a public Ed25519 key and orangecheck adds your signature too, so others can check the receipt against your own key. Skip it and your HMAC signing secret is enough.

  4. 04

    The fee splits four ways

    orangecheck keeps 20%. Your user gets the share you set. The rest is your rebate.

  5. 05

    It is anchored and committed

    An hourly job submits the receipt's hash to OpenTimestamps, which anchors it in a Bitcoin block. Nostr gets a salted commitment as kind 30087, never the receipt itself, so only someone holding the receipt can match the two. Keep your copy: Nostr does not hold one.

  6. 06

    Both sides see it

    Your user sees the credit, with a link to check it. You see the same receipt in your project's events. Anyone with its id can recompute the hash and check the signature against the published key.

Commitment 05: audit verifies without us. The charter

One session's receipt
# the envelope orangecheck signs · one session
{
  "v": 1,
  "kind": "oc-me-event",
  "id": "oc-me-9k2x4f7p",
  "class": "C",
  "subtype": "session_creation",
  "identity": "bc1q...e3lr",           // your user's orangecheck identity
  "site": "yourcompany.com",           // your project's domain
  "session_duration_seconds": 604800,  // 7 days
  "occurred_at": "2026-04-30T16:11:08Z",
  "gross_fee_sats": 64,                // what your project pays
  "platform_fee_sats": 13,             // round(64 × 0.20), to orangecheck
  "user_earned_sats": 42,              // round(64 × 0.65), your user's share
  "site_rebate_sats": 9,               // what is left (64 − 13 − 42)
  "sig": "ed25519:2b:9pK4...rT8x"      // signed by orangecheck for your project
}

02

The fee split, in sats

Where each sat of one event lands. The formula is published, and it is the one the SDK runs, so your numbers and ours agree.

  1. 01

    What you pay

    Your price for the event: fixed sats, or a percent of an underlying amount such as a payment.

  2. 02

    orangecheck keeps 20%

    Fixed by the charter, and changed only through a versioned @orangecheck/me-client release. It funds custody, signing, anchoring and abuse review.

  3. 03

    Your user's share: 0 to 80%

    You set it per event. The platform fee is reserved first, so the two never add up to more than you pay.

  4. 04

    Your rebate: what is left

    It lands in your project's balance.

Commitments 01 and 07: no token, ever, and sats first. The charter

computeFees()
// computeFees() · the formula every receipt is signed against

const PLATFORM = 0.20;

function computeFees(cfg, payment_amount_sats) {
  const gross = cfg.site_pays.kind === 'fixed_sats'
    ? cfg.site_pays.sats
    : Math.round(payment_amount_sats * cfg.site_pays.pct);

  const platform = Math.round(gross * PLATFORM);
  // the user's share is a fraction of gross, at most 80%,
  // so platform + user never exceed gross
  const share  = Math.min(0.8, Math.max(0, cfg.user_share_pct));
  const user   = Math.min(gross - platform, Math.round(gross * share));
  const rebate = gross - platform - user;

  return { gross, platform, user, rebate };
}

// gross 1000, user share 0.65
//   → platform 200, user 650, rebate 150

03

Who holds your users' sats

Your users don't need to know what bitcoin is. Their share accrues against their orangecheck identity. OrangeCheck holds the sats your users earn during bring-up, until federation custody and graduation to their own keys replace it.

  1. 01

    A user signs up through your app

    Wallet or email, both land on the same opaque did:oc. A wallet user signs a challenge and the key never reaches orangecheck. An email is stored encrypted.

  2. 02

    Their share accrues as signed receipts

    There is no stored balance: it is recomputed from the receipts on every read. Until the rail settles, cash-out is refused up front, and nothing is reserved against a payment that is not coming.

  3. 03

    Graduation: the way out

    Graduation moves a user's accrued sats into keys they hold. Their identity stays the same; only how it signs changes. It is not live yet: no federation is bound.

  4. 04

    Nothing to hold on your side

    You host nothing. Requests are signed with an HMAC secret orangecheck issues. If you co-sign, orangecheck hosts your public key and the private half stays with you.

Commitment 02, on custody: what your users sit on is published, never assumed. The charter

The shape, once a federation is bound
# the federation descriptor, once a federation is bound
{
  "v": 1,
  "identity": "bc1q...e3lr",
  "signing_method": "federation_threshold",
  "guardian_set_id": "fed11qgqrgvnhwd...",
  "threshold": "5-of-7",
  "graduation_path": [
    "stay-federation",
    "graduate-to-fedimint",
    "graduate-to-self-custody"
  ]
}

# after graduation: same identity, new signing method
{
  "v": 1,
  "identity": "bc1q...e3lr",
  "signing_method": "bip322",
  "graduated_at": "2026-05-15T09:21:00Z",
  "previous_method": "federation_threshold"
}

04

One identity, every site

Sign-in is hosted on ochk.io and checked locally against a published key. There is no round trip per request.

  1. 01

    ochk.io signs the session

    Email, Google, GitHub or a Bitcoin wallet. Whichever a person uses, the auth host signs an Ed25519 session token.

  2. 02

    Your site checks it locally

    Against the published key set at ochk.io/.well-known/jwks.json, cached. No call to ochk.io per request.

  3. 03

    Outside the family, a token for your origin

    The sign-in popup hands your site a token bound to your origin. A token issued to any other site is refused.

Commitment 03: the protocol stays open. The charter

pages/api/auth/me.ts
// pages/api/auth/me.ts
import { withOcAuth } from '@orangecheck/me-client/server';

export default withOcAuth(async (req, res) => {
  if (!req.ocSession) return res.status(401).json({ ok: false });
  res.status(200).json({
    ok: true,
    account: { id: req.ocSession.sub, did_oc: req.ocSession.did_oc },
  });
}, { audience: 'https://your-site.example' });

05

Three kinds of event

Every paid event is in exactly one class. The class decides how it can be priced, and no app can change it.

  1. 01

    Class A: account & verification

    State transitions: an account created, a bond raised, an agent authorized. Fixed sats, once per change. Typically 200 to 5,000 sats.

  2. 02

    Class B: activity & cashback

    Actions: a payment authorized, a gate passed, a stamp signed, a pledge resolved. Fixed sats or a percent of the amount. Typically 0.5 to 1.5% on payments, 50 to 500 sats otherwise.

  3. 03

    Class C: sessions

    Sessions, not clicks. Signing in inside a valid session is free. Typically 5 to 80 sats a session.

  4. 04

    The formula follows the class

    Classes A and C need only your config. A percent-priced class B event also needs the payment amount, which is checked on the server and recorded in the receipt.

Commitment 03: the protocol stays open. The charter

Every built-in event and its class
// every built-in event and its class · lib/events/types.ts

export const SUBTYPE_CLASS = {
  // class A · state transitions
  account_creation:            'A',
  account_recovery:            'A',
  attest_bond_increased:       'A',
  payment_method_connected:    'A',
  agent_delegation_issued:     'A',
  recovery_method_updated:     'A',

  // class B · actions
  payment_authorization:       'B',
  scoped_action_authorization: 'B',
  attest_verification_at_gate: 'B',
  stamp_signing:               'B',
  pledge_resolution:           'B',

  // class C · sessions
  session_creation:            'C',
};

06

Check a receipt without us

Five years from now, your receipts still check. orangecheck's servers are not in the path, so your receipts outlast us.

  1. 01

    Fetch the canonical bytes

    /api/envelope/<id>?format=wrapped returns the canonical bytes and the signature over them.

  2. 02

    Check orangecheck's signature

    Take the key the signature names from /.well-known/oc-envelope-jwks.json and check Ed25519 over the bytes themselves, not over a hash of them.

  3. 03

    Check your co-signature, if any

    Present only if your project co-signs. Its key set is at /api/integrator/<project_key>/jwks.json. You host nothing.

  4. 04

    Walk the timestamp to a block

    The receipt carries an OpenTimestamps proof. Run ots verify against any synced Bitcoin node and it ends in a real block hash.

  5. 05

    Find the Nostr commitment

    Nostr holds a salted hash of the canonical bytes as kind 30087. The wrapped record gives the salt and the d-tag. Check the event against the key at /.well-known/oc-nostr-pubkey.json.

Commitment 05: audit verifies without us. The charter

verify-envelope.mjs, the script CI runs against real receipts
#!/usr/bin/env node
// Verify a me.ochk.io envelope with nothing but Node 18+.
//   node verify-envelope.mjs <envelope-id> [https://me.ochk.io]
import { createHash, createPublicKey, verify } from 'node:crypto';

const [id = 'oc-me-example-receipt', base = 'https://me.ochk.io'] = process.argv.slice(2);
const get = (path) => fetch(base + path).then((r) => r.json());

// 1. The envelope: its canonical bytes, and the signature over them.
const env = await get(`/api/envelope/${encodeURIComponent(id)}?format=wrapped`);
if (!env.ok || !env.signature) throw new Error(`no signed envelope at ${id}`);
const bytes = Buffer.from(env.canonical_b64, 'base64');

// 2. The key it names, from the published key set.
const jwks = await get('/.well-known/oc-envelope-jwks.json');
const jwk = jwks.keys.find((k) => k.kid === env.signature.kid);
if (!jwk) throw new Error(`key ${env.signature.kid} is not in the published set`);

// 3. Ed25519 over the canonical bytes themselves, not over a hash of them.
const ok = verify(
    null,
    bytes,
    createPublicKey({ key: jwk, format: 'jwk' }),
    Buffer.from(env.signature.sig_hex, 'hex')
);

// 4. The content address. A billable event's id is a routing id, so it is
//    reported, not compared; the other envelope kinds are named by it.
const sha256 = createHash('sha256').update(bytes).digest('hex');

// 5. The OpenTimestamps proof, when anchored, is over that digest:
//    save env.ots.proof_b64 and walk it with the `ots` client and your node.

// 6. Nostr carries a salted commitment, never the envelope (drop manifests
//    excepted). Recompute it; the kind-30087 event under env.nostr.d_tag holds it.
let nostr = 'not published';
if (env.nostr?.salt_hex) {
    const commitment = createHash('sha256')
        .update(Buffer.from('oc-me-commit/v1\0'))
        .update(Buffer.from(env.nostr.salt_hex, 'hex'))
        .update(bytes)
        .digest('hex');
    if (commitment !== env.nostr.commitment_hex) throw new Error('nostr commitment mismatch');
    nostr = env.nostr.d_tag;
} else if (env.nostr) nostr = env.nostr.d_tag;

console.log({
    id,
    ok,
    kid: env.signature.kid,
    sha256,
    ots: env.ots?.status ?? 'not anchored',
    nostr,
});
process.exit(ok ? 0 : 1);

Where the spec lives.

Canonicalization, key formats, the timestamp proof format, the Nostr conventions and every conformance vector are at docs.ochk.io/me, with the SDK reference and the charter's eight commitments beside them.