live · mainnetoc · docs
specs · api · guides
docs / sdk reference

@orangecheck/me-client · SDK reference

v0.7.0 · MIT.

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

  • @orangecheck/me-client — React surface (provider + hook + button).
  • @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 is the React provider + hook. me-client re-exports it from the main entry point, so most React code only needs to import from me-client. 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,
            btc_address: req.ocSession.addr,
            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.addr });
});

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.addr });
});

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

<OcSessionProvider>

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

import { OcSessionProvider } from '@orangecheck/me-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/me-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.address.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 { oc } from '@orangecheck/me-client';
import type { IntegratorPriceConfig } 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.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.

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

@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