oc · me
§ how it works · mechanically

Six flows. The mechanic that enforces every commitment.

The integrator's due-diligence walkthrough — envelope lifecycle, the fee split, custody, SSO, event classes, offline verification — each ending with the charter commitment it enforces.

§ in one breath

Your site fires a billable event under a user's OrangeCheck identity. OrangeCheck signs a content-addressed envelope that commits the fee split, anchors it to a bitcoin block, and credits the user's share to them in sats. You host no key and deploy no file — and every receipt verifies offline, with or without us. The six flows below are that sentence, in full.

custody today · sats held by orangecheck · nothing settles out until the payout rail does, and /custody derives the live posture.

§ the shape of it
01

a user does something on a site that wired this in

A sign-in, a payment, a verification at a gate. The site decides which of its events are worth paying for, and what each one is worth.

Three classes — A account · B action · C session. A sign-in inside an already-open session costs nothing, because nothing costed happened.

02

one envelope

The event becomes a single content-addressed record. Change any field and it is a different record with a different id — there is no version of it that quietly disagrees with the one the user was shown.

Fields ordered per RFC 8785, hashed with SHA-256, signed with an Ed25519 key whose public half is published at /.well-known/oc-envelope-jwks.json. A project may register its own key and co-sign, so a third party can check the envelope against the site as well as against OrangeCheck.

  1. to the user

    sats to the user

    sats, not a token or points. None move until the payout rail settles.

    The site funds the event. OrangeCheck keeps a fixed 20%; the site chooses how the rest splits between its user and its own rebate. During bring-up OrangeCheck runs every role in the paying rail, the node that pays out included; a federation of independent guardians is what takes that over.

    the split ↗
  2. to the chain

    a receipt on bitcoin

    anchored to a block and published publicly — it keeps verifying if orangecheck disappears.

    The envelope hash goes to an OpenTimestamps calendar — run by opentimestamps.org, not by us — which batches it into a Bitcoin block. The envelope itself is republished as kind 30087 to public relays alongside the family's own relay.ochk.io, which always gets a copy and is never the only one.

    verify one ↗
  3. to the identity

    a stronger identity

    paid history accrues to the identity — which is what makes the next site want to fund it.

    Each site sees a per-integrator scoped subject, so sites cannot correlate the same person across each other. What accumulates is a history of real, funded events behind one bitcoin identity — the signal an integrator is actually buying.

    the four no’s ↗
↺ and back to 01

A paid history is what makes the identity worth funding, so the third output is what pays for the first. No token printer, no subsidy — every sat a user earns is one a site chose to pay for something that actually happened.

[01]

envelope lifecycle · per billable event

What happens, end-to-end, when one of your users does something billable on your site. Six steps. Every step is content-addressed; every step verifies offline against Bitcoin headers. This is the mechanic your CFO and compliance reviewer should walk through before you sign up.

  1. 01 · event happens at your gate
    A session opens, a payment authorizes, an attest verifies. Your code calls the SDK (oc.session.create / oc.payment.authorize / oc.event.fire) with the subtype and any required context (e.g. payment_amount_sats for percent-of-amount events).
  2. 02 · envelope is canonicalized + signed
    Fields are ordered per RFC 8785, hashed (SHA-256), signed with OC's Ed25519 envelope key on your project's behalf (kid published at me.ochk.io/.well-known/oc-envelope-jwks.json). The signature commits to gross_fee_sats — the amount you fund — derived from your IntegratorPriceConfig + the event subtype. You host no key and deploy no file.
  3. 03 · your project optionally co-signs
    If you registered a public Ed25519 JWK (Keys tab · OC hosts it for you), OC adds your project co-signature so third parties can verify the envelope against your own key too. Skip it and the integration runs on your HMAC signing_secret alone.
  4. 04 · four-way fee split applied
    platform_fee_sats = round(gross_fee_sats × 0.20) (fixed; ratified in charter). user_earned_sats = round(gross_fee_sats × your user_share_pct), capped so platform + user never exceed gross. site_rebate_sats = the residual — yours to spend on premium-tier cashback or withdraw.
  5. 05 · envelope is OTS-submitted + Nostr-published
    Hash is submitted to an OpenTimestamps calendar by the /api/cron/anchor sweeper; calendars batch the digest into a Bitcoin block, after which the proof upgrades to a confirmed block anchor. Envelope is republished to public Nostr relays as kind-30087 (d-tag prefix oc-me-event:<id> / oc-me-payment:<id> / oc-me-rebind:<id>). Both are independent of OC infrastructure — receipts continue verifying if OC vanishes.
  6. 06 · user sees cashback · you see the event
    User's /me/earn shows the credit with a /verify/[id] link. Your /me/projects/[id]/events shows the same envelope. Anyone with the envelope id can re-derive the content hash + verify the signature against me.ochk.io/.well-known/oc-envelope-jwks.json — no OC servers required.
enforces · Commitment 05 · Audit verifies without us. Every step in this lifecycle is reproducible offline. docs.ochk.io/charter ↗
flow · 01
# the envelope OrangeCheck signs · class C session_creation
{
  "v": 1,
  "kind": "oc-me-event",
  "id": "oc-me-9k2x4f7p",
  "class": "C",
  "subtype": "session_creation",
  "identity": "bc1q...e3lr",         // your user's oc identity
  "site": "yourcompany.com",          // your project domain
  "session_duration_seconds": 604800, // 7 days
  "occurred_at": "2026-04-30T16:11:08Z",
  "gross_fee_sats": 64,               // funded by your project
  "platform_fee_sats": 13,            // round(64 × 0.20), to OC (charter §07)
  "user_earned_sats": 42,             // round(64 × 0.65), your user_share
  "site_rebate_sats": 9,              // your residual (64 − 13 − 42)
  "sig": "ed25519:2b:9pK4...rT8x"     // signed by OC on your behalf
}
[02]

the four-way fee split, concretely

Where each sat lands on a billable envelope. The math is published, the platform fee constant is in lib/events/types.ts (PLATFORM_FEE_POLICY.pct = 0.20), and computeFees() is the canonical function — same one the SDK runs locally for live form feedback.

  1. 01 · gross_fee_sats · 100% · you fund this
    Pulled from your IntegratorPriceConfig per-subtype value. Either fixed sats (account_creation, session_creation, attest_bond_increased) or a percent of an underlying amount (payment_authorization, attest_verification_at_gate).
  2. 02 · platform_fee_sats · 20% · OC keeps
    Fixed by charter; changeable only via a versioned @orangecheck/me-client release with governance ratification. Funds custody operations, signing service, OTS anchor pipeline, abuse review queue, federation guardian compensation.
  3. 03 · user_earned_sats · your user_share_pct of gross
    Range 0–80% of gross (the 20% platform fee is reserved, so user + platform can never exceed gross). You set it per subtype. 0% = you keep all the cashback as rebate. 80% = your users get nearly every sat. Typical defaults: 65% on actions, 70% on payments, 50% on state transitions.
  4. 04 · site_rebate_sats · the residual
    What's left after platform_fee and user_share. Lands in your project's OC balance. Use it for premium-tier cashback experiments, retention promotions, or withdraw it once the payout rail settles.
enforces · Commitment 01 · No token, ever (sats only) + Commitment 07 · Sat-denominated, first-class. docs.ochk.io/charter ↗
flow · 02
# computeFees() · the canonical formula
//  in lib/events/types.ts (CI-tested, conformance-vector-locked)

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);
  // user_share is a fraction of GROSS (clamped to 0.8), then capped
  // 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 };
}

// example · gross 1000, user_share 0.65
//   → platform 200, user 650, rebate 150
[03]

custody · who holds your users' sats

Your users don't need to know what bitcoin is to use your sat-paying sign-in. Their share accrues against their did:oc — today, sats held by orangecheck, and /custody derives the live answer. When a federation is bound and settling, a federation-custodied lightning wallet provisions behind their identity and graduation to their own keys opens. Integrator-side, you have no custody — your project signs with its own keypair.

  1. 01 · user signs up via your site
    Both signup paths land on the same opaque did:oc identity — public identifier is a random 32-hex DID, unaffected by which path you took. BIP-322 wallet path: the user signs a challenge with their wallet; their address goes into linked_identities (plaintext, since BTC addresses are public on-chain). Email-OTP path: the email lands in linked_identities AES-256-GCM encrypted at rest. The BIP-322 key never reaches OC either way — that axis is self-custodied today. No federation-held wallet provisions behind the identity until a federation is live and bound; the current custody posture is derived on /custody.
  2. 02 · cashback accrues as signed envelopes
    Every user share lands as a signed envelope against the user's did:oc, shows on their /me/activity ledger, and is checkable at /verify/<id>. There is no stored balance — computeUserLedger recomputes it from those envelopes on every read — and who holds it is the posture on /custody: today, sats held by orangecheck. Cash-out reads that same posture: until the rail settles, it is refused up front by the settlement gate — nothing is queued, and no balance is reserved against a payment that is not coming. When a federation is bound and settling, users are paid to the destination in their signed payout binding, for the destination kinds the payer can actually settle — anything else is refused up front, never reserved.
  3. 03 · graduation · the exit, and what opens it
    When a federation is bound and settling, /me/graduate sweeps: guardians collectively sign, the user watches it confirm on a Bitcoin block, and the oc identity's signing_method discriminator flips from federation_threshold to bip322. Everything else continues — your project, your envelopes, their attest tier. Until then /me/graduate states which of the two custody questions is already answered: signing keys are self-custodied for BIP-322 users, accrued sats are not.
  4. 04 · no custody on the integrator side · ever
    You host nothing. Request auth is an HMAC secret OC issues (sign each event POST with it · zero deploy). OC signs the envelopes and reconciles the four-way split. Envelope co-signing is optional — register a public Ed25519 JWK and OC hosts it at me.ochk.io/api/integrator/<key>/jwks.json; the private half stays with you. OC never holds your private keys.
enforces · Commitment 02 · No custody you didn't choose. What your users sit on is published on /custody, never assumed — today, sats held by orangecheck. docs.ochk.io/charter ↗
flow · 03
# the federation descriptor · the shape, 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 /me/graduate · once graduation opens · same identity, new method
{
  "v": 1,
  "identity": "bc1q...e3lr",        // unchanged
  "signing_method": "bip322",
  "graduated_at": "2026-05-15T09:21:00Z",
  "previous_method": "federation_threshold"
}
[04]

cross-subdomain SSO · one identity, every site

Auth is hosted at ochk.io. It issues an Ed25519-signed oc_session JWT cookie scoped to Domain=.ochk.io. Every consumer subdomain — me, attest, lock, vote, stamp, agent, pledge — verifies it locally against the published JWKS. Your integration calls the same useOcSession() / oc.session.create() everywhere.

  1. 01 · auth host issues the oc_session
    ochk.io/signin runs the BIP-322 challenge for advanced users; ochk.io/api/auth/email-otp/{start,verify} runs the email-OTP flow for everyone else (CORS-enabled so consumer subdomains can call it cross-origin from in-place forms — no redirect required). Either path mints an Ed25519-signed JWT with claims { sub, addr, npub?, iat, exp }, sets it as a cookie with Domain=.ochk.io, HttpOnly, SameSite=Lax.
  2. 02 · every consumer subdomain reads it locally
    Your me.ochk.io integration uses @orangecheck/auth-client; the SDK's OcSessionProvider validates the JWT against ochk.io/.well-known/jwks.json (cached 1h). No round-trip to ochk.io per request — verification is local + cryptographic.
  3. 03 · session policies are integrator-set
    You declare session_policy at oc.session.create({ duration_seconds, refresh: "sliding" | "fixed" }). Banking pattern: 15-minute sliding window. SaaS pattern: 7-day sliding. Mobile pattern: 90-day fixed. Class C billable on creation, free on refresh + invalidate.
  4. 04 · sign-out propagates · revocation is immediate
    Your oc.session.invalidate() call (or the user signing out at any sibling site) clears the cookie family-wide. Subsequent envelope signing fails on every subdomain instantly — no replay window beyond the cookie's already-active lifetime.
enforces · Commitment 03 · The protocol stays open. JWT shape + JWKS endpoint are spec-defined. docs.ochk.io/charter ↗
flow · 04
# oc_session JWT · what every consumer subdomain reads
{
  "alg": "EdDSA",
  "kid": "oc-auth-1",
  "sub": "did:btc:bc1q...e3lr",
  "addr": "bc1q...e3lr",
  "npub": "npub1...8q9w",
  "iat": 1714512000,
  "exp": 1715116800,
  "scope": ["identity", "payment"],
  "session_policy": {
    "duration_seconds": 604800,
    "refresh": "sliding"
  }
}

# JWKS published at ochk.io/.well-known/jwks.json
# verified locally · no ochk.io round-trip per request
[05]

three event classes · A · B · C

Every billable subtype falls into one of three classes. The class determines pricing dial range and the computeFees() behavior. Classes are not editable per integrator — they're SDK-level invariants, defined in @orangecheck/me-client/types and enforced by SUBTYPE_CLASS in lib/events/types.ts.

  1. 01 · class A · state transitions
    account_creation, attest_bond_increased, agent_delegation_issued, recovery_method_updated, payment_method_connected. Fixed-sat priced. One-time per state change. Used to reward sybil-resistant signup, bond attestation (BIP-322 sat-bond, no KYC), agent authority registration. Typical range: 200–5,000 sats.
  2. 02 · class B · action-bound
    payment_authorization, scoped_action_authorization, attest_verification_at_gate, stamp_signing, pledge_resolution. Fixed sats OR percent_of_amount. Per action. The dominant earnings line on most integrators. Typical: 0.5–1.5% on payments, 50–500 sats on non-monetary actions.
  3. 03 · class C · sessions
    session_creation. Fixed-sat priced. Per session, NOT per click. Sign-ins inside a valid session are free (telemetry-only). Typical: 5–80 sats per session. A `lightning_received` subtype for sat-paid Lightning credits is on the spec roadmap; current Class C handles the session case.
  4. 04 · computeFees signature varies by class
    Class A + C: computeFees(cfg). Class B percent_of_amount: computeFees(cfg, payment_amount_sats) — the second arg is required, validated server-side, and recorded in the envelope. Conformance test vectors in @orangecheck/me-client lock the rounding rules.
enforces · Commitment 03 · The protocol stays open. Subtype-class mapping is in the spec. docs.ochk.io/charter ↗
flow · 05
# subtype → class mapping · in lib/events/types.ts
//  CI-tested · ratified · changeable only via spec PR

export const SUBTYPE_CLASS: Record<EventSubtype, EventClass> = {
  // 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 — action-bound
  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]

audit + offline verification

Five years from now, your envelopes still verify. The protocol is the API; OC's servers are not in the verification path. This is the load-bearing reason any compliance team should be willing to sign off — your receipts outlast OC.

  1. 01 · fetch the canonicalized envelope
    curl https://me.ochk.io/api/envelope/<id>?format=wrapped returns the RFC-8785-canonical bytes and the signature over them. Most envelope kinds are named by sha256 of those bytes; a billable event carries a routing id instead.
  2. 02 · verify the OC signature
    Fetch me.ochk.io/.well-known/oc-envelope-jwks.json, take the key the signature's kid names, and check Ed25519 over the canonical bytes themselves — not over a hash of them. If it passes, OrangeCheck signed exactly these bytes, split included.
  3. 03 · verify the project co-signature (optional)
    Only present if the project opted into co-signing. Fetch its OC-hosted JWKS at me.ochk.io/api/integrator/<project_key>/jwks.json (the integrator hosts nothing) and run the same Verify(). An empty key set just means the project authenticates with its HMAC secret instead.
  4. 04 · walk the OTS proof to a Bitcoin block
    The envelope carries an OpenTimestamps proof. Run `ots verify envelope.ots` against any synced Bitcoin node. The proof terminates in a real block hash — that's the timestamp guarantee, independent of OC.
  5. 05 · check Nostr publication
    The envelope is published as a kind-30087 event on four public relays (relay.damus.io, nos.lol, relay.snort.social, relay.ochk.io) — d-tag prefix oc-me-event:<id> / oc-me-payment:<id> / oc-me-rebind:<id> per envelope kind. Signed under the me.ochk.io publishing pubkey at /.well-known/oc-nostr-pubkey.json. Any Nostr client can fetch it. Independent of OC, independent of any single relay operator.
enforces · Commitment 05 · Audit verifies without us. The whole pipeline is offline-reproducible. docs.ochk.io/charter ↗
flow · 06
#!/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.
console.log({ id, ok, kid: env.signature.kid, sha256, ots: env.ots?.status ?? 'not anchored' });
process.exit(ok ? 0 : 1);

where the spec lives · what to read next

The protocol-layer details — RFC 8785 canonicalization, Ed25519 JWK formats, OTS proof envelope format, Nostr kind-30087 conventions, every conformance vector — live at docs.ochk.io/me. The full SDK reference is at docs.ochk.io/me/sdk. The charter (eight commitments) is at docs.ochk.io/charter.

Ready to ship? /why#integrators has the live configurator, the cost-model sandbox, and the three integration pathways (drop-in, SDK, co-branded). /me/projects/new is the three-step onboarding wizard.