@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: theOcSignInButtoncomponent plus theoc.*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.jsonfetching 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 packages — me-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);
| Function | Signature | Note |
|---|---|---|
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);
| Function | Signature | Note |
|---|---|---|
oc.config.validate(cfg) | (IntegratorPriceConfig) => ValidationResult | No 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-clientif 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.authorize — stamp_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,
});
| Function | Signature | Note |
|---|---|---|
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
| Scope | Reveals | Use case |
|---|---|---|
bitcoin_address | Master did:bip322:bc1q… (BIP-322 users only) | On-chain holdings verification, payment-channel co-signing |
email | Email address (email-OTP users only) | Transactional email · receipts, password reset |
attest_tier | Current AttestTier (anonymous / bonded / kyc_light / kyc_strong) | Sybil-resistance gating, regulated KYC flows |
display_name | A 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:
- Update SDK to
^0.10.0. - Switch from
event.user_address→event.subfor user records in your database. - If you actually need the user's email or address, call
oc.scope.request(['email'])and readevent.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.
| Function | Signature | Note |
|---|---|---|
oc.scope.granted(opts) | (GrantedOptions) => Promise<GrantedResult> | Read currently-granted scopes for the signed-in user. |
oc.scope.request(scopes, opts) | (Scope[], RequestOptions) => never | Redirect 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
| Export | Signature |
|---|---|
PLATFORM_FEE_POLICY | { pct: 0.2; min_floor_sats: 1; ratified: string } |
MIN_INTEGRATOR_PRICE_SATS | 5 |
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
- npm: @orangecheck/me-client
- source: oc-packages/me-client
- canonical type module:
@orangecheck/me-client/types(mirror ofoc-me-web/src/lib/events/types.ts)