oc · docs
docs / sdk reference

@orangecheck/me-client · SDK reference

Full reference: docs.ochk.io/sdk/me-client — auto-generated from the TypeScript source on every release. The narrative below is the curated overview; the SDK reference page is the source of truth for every export, type, and signature.

MIT. Version-pinned reference: see docs.ochk.io/sdk/me-client, auto-generated per release.

This is the canonical TypeScript SDK for integrating me.ochk.io. Three entry points, three audiences:

  • @orangecheck/me-client — React + JS surface: the OcSignInButton component plus the oc.* namespaces (session, payment, event, webhook, config, identity). The <OcSessionProvider> + useOcSession() React bindings ship in the companion package @orangecheck/auth-client — install both.
  • @orangecheck/me-client/server — server-side verification. withOcAuth (Next.js Pages), ocAuthExpress, ocAuthHono, getOcSession(headers), verifyOcToken(token). No env vars, no JWK handling, no /.well-known/jwks.json fetching by you — the SDK does that internally and caches.
  • @orangecheck/me-client/popup — browser-only. signInWithOc() opens the OC popup, listens for postMessage, returns the verified session result. The same shape Google / GitHub use.

install

yarn add @orangecheck/me-client @orangecheck/auth-client

@orangecheck/auth-client carries the React provider + hook (<OcSessionProvider>, useOcSession()); @orangecheck/me-client carries the data SDK (oc.* namespaces), the server adapters, the /popup helper, and the OcSignInButton component. They are separate packagesme-client does not re-export the provider or hook, so React code imports the provider/hook from auth-client and everything else from me-client. Install both. Server-side code reaches for @orangecheck/me-client/server and never touches React.

server-side verification (zero JWK handling)

withOcAuth for Next.js Pages Router:

// 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,
            display_name: req.ocSession.name ?? null,
            nostr_npub: req.ocSession.npub ?? null,
        },
    });
});

ocAuthExpress for Express:

import { ocAuthExpress } from '@orangecheck/me-client/server';
import express from 'express';

const app = express();
app.use(ocAuthExpress());

app.get('/api/profile', (req, res) => {
    if (!req.ocSession) return res.status(401).json({ error: 'sign in' });
    res.json({ address: req.ocSession.did_oc });
});

ocAuthHono for Hono / Cloudflare Workers / Bun / Deno:

import { ocAuthHono } from '@orangecheck/me-client/server';
import { Hono } from 'hono';

const app = new Hono();
app.use('*', ocAuthHono());

app.get('/api/me', (c) => {
    const session = c.get('ocSession');
    if (!session) return c.json({ ok: false }, 401);
    return c.json({ address: session.did_oc });
});

getOcSession(headers) is the framework-agnostic primitive — accepts a Web Headers object or a plain { cookie, authorization } object. Returns the verified SessionPayload or null. Cookie auth (family) and Bearer auth (cross-domain) are handled identically.

The wrappers all take an options bag: { required: true } short-circuits unauthenticated requests with a 401 before your handler runs.

browser popup signin

import { signInWithOc } from '@orangecheck/me-client/popup';

button.addEventListener('click', async () => {
    const result = await signInWithOc();
    if (!result) return; // user cancelled or popup blocked
    localStorage.setItem('oc-token', result.token); // optional · for cross-domain
    location.assign('/dashboard');
});

signInWithOc() MUST be called inside a user-gesture handler (browsers block window.open outside of gestures). Returns { account, token } on success, null on cancel / popup block / abort.

React surface

The provider and hook below are exported by @orangecheck/auth-client, the companion package — me-client carries the oc.* data namespaces and the OcSignInButton, auth-client carries the cross-subdomain session bindings. Install both.

<OcSessionProvider>

Wrap your app once at the root. Default authOrigin is https://ochk.io.

import { OcSessionProvider } from '@orangecheck/auth-client';

<OcSessionProvider>{/* … */}</OcSessionProvider>;

useOcSession() and useOptionalOcSession()

Returns { status, account, signInUrl, signOut, refresh, error }. Status is loading | authenticated | anonymous | error. Account is null when not authenticated.

import { useOcSession } from '@orangecheck/auth-client';

function Header() {
    const { status, account, signInUrl, signOut } = useOcSession();
    if (status === 'loading') return <Skeleton />;
    if (status !== 'authenticated') return <a href={signInUrl}>sign in</a>;
    return (
        <div>
            signed in as {account.didOc.slice(0, 8)}…
            <button onClick={() => signOut()}>sign out</button>
        </div>
    );
}

useOptionalOcSession() returns null instead of throwing — useful for libraries that want to read the session if it exists but shouldn't crash on apps that haven't mounted the provider.

oc.session.* · session lifecycle (Class C billable atom)

Sites pay per session, not per click. create() opens a new session; refresh() and invalidate() are free, telemetry-only.

import { oc } from '@orangecheck/me-client';

const session = await oc.session.create({
    scope: ['identity', 'payment'],
    sessionPolicy: { duration_seconds: 7 * 86400, refresh: 'sliding' },
});
await oc.session.refresh(session.id);
await oc.session.invalidate(session.id);
FunctionSignatureNote
oc.session.create(opts)(SignInOptions) => Promise<Session>Class C billable.
oc.session.refresh(id)(string) => Promise<Session>Free.
oc.session.invalidate(id)(string) => Promise<void>Free.

oc.payment.authorize · Class B billable atom

const result = await oc.payment.authorize({
    identity: 'bc1q...',
    amount_sats: 240_000,
    description: 'breez · march invoice',
});
// result.id, result.user_envelope_id, result.verify_url

The user is prompted by me.ochk.io to consent before this resolves.

oc.config.validate · local IntegratorPriceConfig validator

Pre-flight your config against the same rules the server enforces — no network call, no round-trip. Per-project read/write is owner-gated and lives at /api/me/projects/[id] (or me.ochk.io/me/projects/<key> in the UI); the SDK intentionally doesn't include a global oc.config.update() because configs are per-project, not per-account.

import type { IntegratorPriceConfig } from '@orangecheck/me-client';

import { oc } from '@orangecheck/me-client';

const cfg: IntegratorPriceConfig = {
    /* … */
};
const result = oc.config.validate(cfg);
if (!result.ok) console.error(result.errors);
FunctionSignatureNote
oc.config.validate(cfg)(IntegratorPriceConfig) => ValidationResultNo network call.

The same function is also exported as the top-level validateIntegratorConfig.

oc.webhook.verify · Ed25519 signature verification

Always pass the raw body bytes, not the parsed JSON. Frameworks that re-serialize before your handler produce a different byte sequence and the signature will not validate.

const result = await oc.webhook.verify(
    rawBody, // string | Uint8Array
    sigHex, // OC-Signature header
    kid // OC-Key-Id header
);
if (!result.ok) return res.status(401).end(result.reason);

Auto-fetches and caches ochk.io/.well-known/jwks.json for 1h when no jwk is passed. Stale-on-error: a transient JWKS outage returns the previous cached entry rather than rejecting every webhook.

Agent delegation (oc.delegation.*) is a separate primitive and lives on agent.ochk.io / docs.ochk.io/agent, not on me.ochk.io. me-client doesn't ship a delegation namespace; reach for @orangecheck/agent-client if you need to issue or revoke agent delegations from your integration.

oc.identity.verifyActivityAttestation · cross-integrator sybil-resistance gate

Verify a signed activity attestation bundle the user forwarded to you. The bundle wraps six cross-integrator activity values (event_count, human_event_count, lifetime_sats, distinct_integrator_count, oldest_event_at, active_days) plus a 24h expiry, signed with the me.ochk.io envelope key — the same key that signs every billable event, rebind, and trust attestation.

import { oc } from '@orangecheck/me-client';

const result = await oc.identity.verifyActivityAttestation(bundle);
if (!result.ok) return res.status(401).send(result.reason);

const a = result.attestation!;
if (a.lifetime_sats < 10_000) return res.status(403).send('low signal');
if (a.distinct_integrator_count < 3) return res.status(403).send('low breadth');
if (Date.parse(a.oldest_event_at!) > Date.now() - 90 * 86_400_000) {
    return res.status(403).send('too new');
}
// passed all four axes (depth × breadth × duration × habituality)

Runs five checks: bundle shape, canonical-bytes hash matches the content- addressed id, JWK with matching kid present in the envelope JWKS, ed25519 signature, and now < expires_at. Every check surfaces as a typed row in result.checks for logging.

Auto-fetches and caches me.ochk.io/.well-known/oc-envelope-jwks.json for 1h when no jwk is passed. The envelope JWKS is distinct from the auth-host JWKS that signs webhook deliveries — same Ed25519 curve, different URL, cached separately. Pass { jwk } to skip the network fetch for edge runtimes or air-gapped deployments.

Why this is the right primitive for sybil-resistance gating: the values are costly to fake (sats accrue from real integrator outflow, distinct count requires onboarding through N separate integrators, oldest_event_at can't be back-dated, active_days requires habitual presence over time) and trivial to verify (one function call, one signature check, identical posture on Node 20+, Vercel Edge, browser).

The verifier displaces the adjacent category of spend integrators currently pay for separately — fraud detection, anti-spam, account-takeover monitoring — with a single primitive that ships as a side effect of using OC for auth.

Same five checks run in-browser at me.ochk.io/verify-activity for any third party who receives a forwarded attestation but doesn't want to write the verifier themselves. The SDK and the browser page produce identical verdicts on the same input.

oc.event.fire · arbitrary billable subtypes

The escape hatch when you want to bill a subtype that isn't covered by oc.session.create or oc.payment.authorizestamp_signing, attest_verification_at_gate, scoped_action_authorization, kyc_tier_upgrade, etc. Class is determined by SUBTYPE_CLASS; cashback is computed via computeFees(); the envelope is recorded under your project_key and shows up in /developer/projects/[id]/events.

const stamp = await oc.event.fire({
    project_key: 'pk_live_yourcompany',
    subtype: 'stamp_signing',
    action_label: 'review · march invoice',
});
// stamp.id, stamp.gross_fee_sats, stamp.user_earned_sats, stamp.verify_url

// For percent_of_amount-priced subtypes, include the underlying amount:
const verify = await oc.event.fire({
    project_key: 'pk_live_yourcompany',
    subtype: 'attest_verification_at_gate',
    payment_amount_sats: 50_000,
});
FunctionSignatureNote
oc.event.fire(opts)(FireEventOptions) => Promise<BillableEvent>Returns the canonical BillableEvent the server recorded.

oc.scope.* · privacy-preserving scope grants

OC's privacy default: signing into your site reveals only an anonymous per-integrator subject (sub). The user's master identity (BIP-322 address or email) stays private; you cannot correlate users across sites. To read additional fields (their email, attest tier, bitcoin address, display name) you must explicitly request a scope and the user must explicitly consent.

Full design at PRIVACY-ARCHITECTURE.md.

oc.scope.granted

Read what scopes the currently-signed-in user has already granted to your project. Returns the per-integrator sub plus any granted field values.

import { oc } from '@orangecheck/me-client';

const { sub, scopes_granted, scopes } = await oc.scope.granted({
    project_key: 'pk_live_yourcompany',
});

// `sub` is the per-integrator anonymous id you should key user
// records on (NOT the master addr · OC doesn't reveal it).
console.log(sub); // "sub_3K7xQ2mN1pYwB8tRvE…"

// `scopes` is a partial map · only fields whose scope the user has
// granted are present.
console.log(scopes.email); // "user@example.com" · or undefined
console.log(scopes.attest_tier); // "bonded" · or undefined

oc.scope.request

Redirect the user to me.ochk.io's consent prompt. After they decide, browser returns to return_to. Subsequent oc.scope.granted() calls reflect their decisions.

oc.scope.request(['email', 'attest_tier'], {
    project_key: 'pk_live_yourcompany',
    return_to: window.location.href,
});
// ↑ this navigates · the line after never runs

The user picks grant once (1h expiry), grant always (until revoked), or deny per scope. Every grant is revocable any time on me.ochk.io/me/identity.

Valid scopes

ScopeRevealsUse case
bitcoin_addressMaster did:bip322:bc1q… (BIP-322 users only)On-chain holdings verification, payment-channel co-signing
emailEmail address (email-OTP users only)Transactional email · receipts, password reset
attest_tierCurrent AttestTier (anonymous / bonded / kyc_light / kyc_strong)Sybil-resistance gating, regulated KYC flows
display_nameA user-chosen handle (separate from real identity)Forum / social integrations where some name is needed

Migration from user_address (≤ 0.9.0)

Pre-0.10.0 webhook payloads delivered the user's master address as user_address. From 0.10.0 onward, payloads use sub (per-integrator)

  • optional scopes (granted fields). To migrate:
  1. Update SDK to ^0.10.0.
  2. Switch from event.user_addressevent.sub for user records in your database.
  3. If you actually need the user's email or address, call oc.scope.request(['email']) and read event.scopes.email / oc.scope.granted().scopes.email.

The user_address field is retired from outgoing payloads in the matching me.ochk.io deploy. Existing rows in your database keyed on old user_address values stay valid; just expect new events to use sub.

FunctionSignatureNote
oc.scope.granted(opts)(GrantedOptions) => Promise<GrantedResult>Read currently-granted scopes for the signed-in user.
oc.scope.request(scopes, opts)(Scope[], RequestOptions) => neverRedirect to consent prompt · throws after navigation.

Types

Every type the SDK exports:

EventClass · EventSubtype · ClassASubtype · ClassBSubtype · ClassCSubtype
AttestTier · SiteFeeShape · IntegratorEventConfig · IntegratorPriceConfig
ComputedFees · ValidationResult · BillableEvent · Session · SessionPolicy
SignInOptions · PaymentAuthorizeOptions · PaymentResult · TelemetryEvent
FireEventOptions · OcPublicJwk · VerifyOptions · VerifyResult
Scope · GrantedOptions · GrantedResult · RequestOptions

@orangecheck/me-client re-exports the React surface from @orangecheck/auth-client, so OcAccount, OcSessionState, OcSessionStatus, and OcAuthConfig are available at runtime via that re-export — import them directly from @orangecheck/auth-client for stable typed access.

Constants and helpers

ExportSignature
PLATFORM_FEE_POLICY{ pct: 0.2; min_floor_sats: 1; ratified: string }
MIN_INTEGRATOR_PRICE_SATS5
computeFees(cfg, payment_amount_sats?)(IntegratorEventConfig, number?) => ComputedFees
validateIntegratorConfig(cfg)(IntegratorPriceConfig) => ValidationResult
setBearerToken(token)(string | null) => void · cross-domain auth
getBearerToken() / clearBearerToken()() => string | null / () => void

Where it lives