oc · docs
integrations

orangecheck · Integrations

orangecheck sits next to the sign-in providers you already offer. A first-time user who picks it needs no wallet, no seed phrase and no "what is a sat" conversation. If your app pays its users, what they earn shows up in their account with a receipt for each event. orangecheck holds earned sats while payouts are in testing; see custody for who holds what and what replaces it.

See it live first

Before you read another paragraph, me-demo.ochk.io is a real orangecheck integration we built ourselves — a fictional newsletter site (Lumen Notes), styled deliberately not-orangecheck. Its sign-in button opens the orangecheck popup. Sign in once and you'll see the whole loop end to end. Source at github.com/orangecheck/oc-me-demo-web. ~280 lines of meaningful integration code, including the /api/auth/me wrapper and the popup-signin handler.

Beside your other providers

import { signInWithOc } from '@orangecheck/me-client/popup';
import { signIn } from 'next-auth/react';
import { useState } from 'react';

export function SignInOptions() {
    const [busy, setBusy] = useState(false);
    async function onOc() {
        setBusy(true);
        const result = await signInWithOc();
        setBusy(false);
        if (!result) return;
        // Your server verifies the token and keeps it in its own cookie.
        const r = await fetch('/api/auth/session', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ token: result.token }),
        });
        if (r.ok) location.assign('/dashboard');
    }
    return (
        <div className="auth-options">
            <button onClick={() => signIn('google')}>
                continue with google
            </button>
            <button onClick={() => signIn('apple')}>continue with apple</button>
            <button onClick={() => signIn('email')}>email magic link</button>
            <button onClick={onOc} disabled={busy}>
                {busy ? 'opening…' : 'sign in with orangecheck'}
            </button>
        </div>
    );
}
What your user seesWhat your user does NOT see
a button next to Google / Applea seed phrase
one orangecheck sign-in popupa wallet creation flow
signed in on your appa "buy bitcoin" CTA
if you pay your users, sats they earnedany crypto vocabulary at all

When do they find out? When they want to. ochk.app/me shows what they earned and where it goes, and ochk.app/custody says who holds it today. None of this is in the sign-in path, and none of it shows unless the person visits.

Example prices

Prices are yours to set, and only matter if you pay your users. For each event, set what your user earns. You pay that ÷ 0.8, the 20% fee included: earns 100, you pay 125. Some starting points, for made-up apps:

Example appEventYour user earnsYou pay
a wallet appaccount_creation (One-time)1,600 sats2,000 sats
a wallet apppayment_authorization (Per action)0.4% of the payment0.5% of the payment
a social appsession_creation (Per session)40 sats50 sats
a creator sitestamp_signing (Per action)80 sats100 sats

There's no "right" price; try numbers in the sandbox first. The config schema is open: see the SDK reference for the full shape. You edit prices on your project's Paying tab, under What each event earns.

Errors and edge cases

Signing in:

What you seeMeaningFix
signInWithOc() returns nullThe person closed the popup, the browser blocked it, or your signal aborted.Call it inside the click handler, so the browser allows the popup. Leave the button there to try again.
401 right after a sign-inThe token is bound to your origin, and your server's audience is missing or a different origin (http for https, www, a port). me-client logs a warning naming audience when it can tell.Pass your exact origin as audience, in the session route and in withOcAuth.
403 bond_requiredThe account isn't backed above the bar you set under Who can sign in. The body has need, have and a back_url.Send the person to back_url, where they can back their account.
500 unknown_project_keyprojectKey names no project. me-client logs which key, once. (Before 0.32 this read as a 503.)Copy the key from your project at ochk.app/me/projects.
503 bond_policy_unavailableYour project's sign-in requirement couldn't be read from me.ochk.io, and none had been read before, so the request is refused, not let through.Retry; the wrapper asks again after ten seconds. Check your server can reach me.ochk.io.

Paying your users:

What you seeMeaningFix
IntegratorPriceConfig invalid (422)Validation failed server-side.Run oc.config.validate(cfg) locally first; the response includes details: [{ subtype, message }].
signature does not match from oc.webhook.verifyEither you parsed the body before verifying, or you're verifying against a stale JWK.Use raw body bytes; re-fetch JWKS via oc.webhook.fetchJwks().
rate_limit_exceeded (429)You exceeded a per-IP bucket.Honor Retry-After; back off exponentially.
domain not verified (403)Your project_key fired in live mode but verification.state !== 'verified'.Verify your domain to go live (DNS TXT or meta tag), then retry.

Self-host considerations

orangecheck is one implementation of the canonical OC primitives. Anyone can run their own equivalent against the same receipt contracts — that's the product strategy. If you're considering self-hosting, the contracts to honor are at:

  • oc-attest-protocol/SPEC.md (identity)
  • oc-attest-protocol/FEDERATION-CUSTODY.md (v1.2-draft-1 federation custody)
  • oc-stamp-protocol/SPEC.md (anchored receipts)
  • oc-agent-protocol/SPEC.md + FEDERATION.md (delegations)
  • oc-lock-protocol/SPEC.md (encrypted DMs)

The hosted service is a convenience, not a moat. Charter commitment 04 keeps every reference site free and self-hostable forever.