From ef1b90ee2814d736a17f3d38a98c5a582a86825f Mon Sep 17 00:00:00 2001 From: Xaxis Date: Thu, 30 Apr 2026 09:43:25 -0700 Subject: [PATCH 1/6] =?UTF-8?q?docs(console):=20add=20/console=20namespace?= =?UTF-8?q?=20=E2=80=94=20overview=20+=20quickstart=20+=20integrations=20+?= =?UTF-8?q?=20webhooks=20+=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-flagged gap: console.ochk.io had /api/openapi.json + /docs/api on its own subdomain but no entry on docs.ochk.io. Each verb has its own / namespace; console didn't. Added 5 pages: /console — what console is, what it isn't, mental model /console/quickstart — sign in → bootstrap → first delegation in 5min /console/integrations — drop-in adapters for the 5 LLM frameworks (anthropic / openai / vercel / langgraph / mcp) + the shared @orangecheck/agent-console-client + @orangecheck/webhook-verify /console/webhooks — event catalog, payload shape, headers, signing secret derivation, retry semantics, idempotency /console/api — OpenAPI 3.1 pointer, auth schemes, route catalog, full reason-code enum, rate-limit + same-origin + idempotency notes Registered the section in DOCS_NAV (between Pledge and SDKs — console is the commercial layer on top of the protocol verbs, not a sibling verb). Added /console + console.ochk.io to the footer. Each page links into the existing /agent and /sdks/* surfaces so readers fall through to the canonical specs naturally — console docs are a thin commercial layer, not a re-statement of the protocol. 5 new static pages built clean (yarn build). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/docs/nav.ts | 32 ++++ src/components/layout/LayoutFooter.tsx | 2 + src/pages/console/api.mdx | 147 +++++++++++++++++ src/pages/console/index.mdx | 60 +++++++ src/pages/console/integrations.mdx | 217 +++++++++++++++++++++++++ src/pages/console/quickstart.mdx | 117 +++++++++++++ src/pages/console/webhooks.mdx | 153 +++++++++++++++++ 7 files changed, 728 insertions(+) create mode 100644 src/pages/console/api.mdx create mode 100644 src/pages/console/index.mdx create mode 100644 src/pages/console/integrations.mdx create mode 100644 src/pages/console/quickstart.mdx create mode 100644 src/pages/console/webhooks.mdx diff --git a/src/components/docs/nav.ts b/src/components/docs/nav.ts index bf743c0..a4f8852 100644 --- a/src/components/docs/nav.ts +++ b/src/components/docs/nav.ts @@ -423,6 +423,38 @@ export const DOCS_NAV: DocsSection[] = [ }, ], }, + { + slug: 'console', + label: 'OC Console', + blurb: 'Managed infrastructure for the OC Agent family. The commercial layer.', + items: [ + { + href: '/console', + label: 'Overview', + blurb: "What console is, what it isn't, and where it fits.", + }, + { + href: '/console/quickstart', + label: 'Quickstart', + blurb: 'Sign in, bootstrap, register your first delegation, see receipts flow.', + }, + { + href: '/console/integrations', + label: 'Integrations', + blurb: 'Drop-in adapters for Anthropic / OpenAI / Vercel AI SDK / LangGraph / MCP.', + }, + { + href: '/console/webhooks', + label: 'Webhooks', + blurb: 'Receive HMAC-signed deliveries on every accepted envelope.', + }, + { + href: '/console/api', + label: 'API reference', + blurb: 'OpenAPI 3.1 spec, auth schemes, error codes, route catalog.', + }, + ], + }, { slug: 'sdks', label: 'SDKs', diff --git a/src/components/layout/LayoutFooter.tsx b/src/components/layout/LayoutFooter.tsx index 7c3614c..5fadbb0 100644 --- a/src/components/layout/LayoutFooter.tsx +++ b/src/components/layout/LayoutFooter.tsx @@ -13,6 +13,7 @@ const PROTOCOLS: FooterLink[] = [ { href: '/stamp', label: 'oc·stamp' }, { href: '/agent', label: 'oc·agent' }, { href: '/pledge', label: 'oc·pledge' }, + { href: '/console', label: 'oc·console' }, ]; const DOCS: FooterLink[] = [ @@ -33,6 +34,7 @@ const ECOSYSTEM: FooterLink[] = [ { href: 'https://stamp.ochk.io', label: 'stamp.ochk.io' }, { href: 'https://agent.ochk.io', label: 'agent.ochk.io' }, { href: 'https://pledge.ochk.io', label: 'pledge.ochk.io' }, + { href: 'https://console.ochk.io', label: 'console.ochk.io' }, { href: 'https://github.com/orangecheck', label: 'github org', diff --git a/src/pages/console/api.mdx b/src/pages/console/api.mdx new file mode 100644 index 0000000..170bacc --- /dev/null +++ b/src/pages/console/api.mdx @@ -0,0 +1,147 @@ +export const metadata = { + title: 'Console API reference', + description: 'OpenAPI 3.1 spec, authentication, error codes, and the route catalog for console.ochk.io.', +}; + +# Console API reference + +console.ochk.io exposes a REST API for managing projects, registering +signed delegations / actions / revocations / sub-delegations / federation +envelopes, ingesting from CI/CD, downloading audit bundles, and managing +webhooks. + +## OpenAPI spec + +A hand-maintained OpenAPI 3.1 spec is served at: + +``` +https://console.ochk.io/api/openapi.json +``` + +Browse it interactively with try-it-out at: + +``` +https://console.ochk.io/docs/api +``` + +Generate a typed client for any language with +[openapi-generator](https://openapi-generator.tech/): + +```bash +openapi-generator-cli generate \ + -i https://console.ochk.io/api/openapi.json \ + -g python \ + -o ./oc-console-py +``` + +## Authentication + +Two schemes, mutually exclusive per request: + +| Scheme | Header | When to use | +|---|---|---| +| **Cookie** | `Cookie: oc_session=` | Browser flows. Set automatically when you sign in at [ochk.io/signin](https://ochk.io/signin); valid for every `*.ochk.io` subdomain. | +| **Bearer token** | `Authorization: Bearer ock_<64-hex>` | Server-side / CI / any non-browser caller. Mint at `/settings § 03`; plaintext returned ONCE at create. Authenticates as the project owner. | + +Tokens take precedence over the cookie if both are supplied. A revoked +token returns 401; a malformed Bearer falls through to anonymous. + +## Route catalog + +| Group | Routes | +|---|---| +| **Projects** | `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{id}` | +| **Members** | `GET/POST /api/members`, `PATCH/DELETE /api/members/{id}` | +| **Delegations** | `GET/POST /api/delegations`, `GET /api/delegations/{id}`, `POST /api/delegations/federation` | +| **Sub-delegations** | `GET/POST /api/subdelegations` (kind 30086, OC Agent v1.1) | +| **Actions** | `GET/POST /api/actions` | +| **Revocations** | `GET/POST /api/revocations` | +| **Audit** | `GET /api/audit/export?format=ndjson\|json\|csv`, `GET /api/audit/bundles` | +| **API tokens** | `GET/POST /api/tokens`, `PATCH/DELETE /api/tokens/{id}` | +| **Webhooks** | `GET/POST /api/webhooks/endpoints`, `GET/PATCH/DELETE /api/webhooks/endpoints/{id}`, `GET /api/webhooks/deliveries`, `POST /api/webhooks/deliveries/{id}/retry` | +| **Billing** | `POST /api/billing/portal` (Stripe Customer Portal redirect), `POST /api/checkout/{stripe,lightning}` | +| **Public** | `GET /api/health`, `GET /api/health/deep`, `GET /api/openapi.json`, `GET /api/auth/me`, `POST /api/auth/logout` | + +## Error envelope + +Every non-2xx response uses a uniform shape: + +```json +{ + "ok": false, + "reason": "agent_must_match_delegation", + "detail": "optional human-readable detail" +} +``` + +`reason` is a stable enum-like string — safe to switch on. `detail` is +human-readable and may change without notice. + +### Reason catalog (the ones you'll see) + +| HTTP | Reason | Meaning | +|---|---|---| +| 400 | `id_mismatch` | Declared envelope id ≠ recomputed id from canonical inputs. Tamper detected. | +| 400 | `descriptor_id_mismatch` | Federation: declared descriptor_id ≠ recomputed from inlined descriptor. | +| 400 | `threshold_mismatch` | Federation: `sig.threshold` ≠ `descriptor.threshold`. | +| 400 | `insufficient_signatures` | Federation: `len(signatures) < M`. | +| 400 | `unknown_guardian` | Federation: a signing guardian is not in the descriptor. | +| 400 | `duplicate_guardian` | Federation: two signatures share a guardian_address. | +| 400 | `federation_principal_wrong_endpoint` | Single-address /api/delegations got a federation principal — POST to /api/delegations/federation instead. | +| 400 | `canonicalization_failed` | Inputs don't form a valid canonical message (malformed timestamp, scope grammar, etc). | +| 401 | `unauthenticated` | No valid cookie OR Bearer token. | +| 403 | `principal_must_match_session` | Body declares a principal_address ≠ session.addr. Forgery defense. | +| 403 | `principal_must_be_parent_agent` | Sub-delegation: signing principal isn't the parent's agent. | +| 403 | `signer_must_be_principal` | Revocation: signer isn't the parent delegation's principal (v1.1). | +| 403 | `signer_must_match_session` | Revocation: session.addr ≠ signer_address. | +| 403 | `agent_must_match_delegation` | Action: signing agent ≠ parent delegation's agent. | +| 403 | `role_forbidden` | Caller's project role doesn't permit the operation. | +| 403 | `owner_role_requires_owner` | Privilege escalation: only an owner can mint or remove another owner. | +| 404 | `not_found` | Including: project not visible to caller, delegation not in project, malformed id (no existence leak). | +| 404 | `parent_not_found` | Sub-delegation: declared parent_id doesn't exist in this project. | +| 404 | `delegation_not_found` | Action / revocation: declared delegation_id doesn't exist in this project. | +| 409 | `already_registered` | Content-addressed PK collision — this exact envelope is already in the registry. | +| 409 | `already_revoked` | Revocation already applied to this delegation. | +| 409 | `already_a_member` | Member: (project_id, address) duplicate. | +| 409 | `last_owner_cannot_remove` | Project foot-gun: refused to remove the last owner. | +| 409 | `last_owner_cannot_demote` | Same — can't demote the last owner. | +| 409 | `parent_inactive` | Sub-delegation: parent.status ≠ 'active'. | +| 409 | `delegation_inactive` | Action: parent delegation has been revoked or expired. | +| 503 | `database_not_configured` | Console is in v1.1 preview mode (no `DATABASE_URL`). The dashboard falls back to mock data. | +| 503 | `webhook_master_key_unset` | Endpoint registration disabled (no `WEBHOOK_MASTER_KEY`). | +| 503 | `stripe_not_configured` | Stripe checkout / portal disabled (no `STRIPE_SECRET_KEY`). | +| 503 | `no_stripe_customer` | Customer Portal: this project hasn't paid via Stripe yet. | + +## Rate limiting + +Every route uses a per-IP, per-route token bucket. Limits are intentionally +generous; you'll only see 429 if a single IP is hammering one route. + +``` +HTTP/1.1 429 Too Many Requests +{ "ok": false, "reason": "rate_limited" } +``` + +Apply jitter + exponential backoff in your retries. + +## Same-origin enforcement + +Browser-driven `POST` / `PATCH` / `DELETE` requires the +`Sec-Fetch-Site: same-origin` header (modern browsers send it +automatically). Bearer-authenticated calls from a server runtime are +exempt — the Bearer token is sufficient evidence of intent. Cross-origin +browser POSTs return 403 `cross_site_blocked`. + +## Idempotency + +Every accepted envelope is content-addressed: its sha256 *is* its +primary key. Re-POSTing the exact same envelope returns 409 +`already_registered`. This makes client-side retry safe — you can fire +the same POST twice without persisting twice. + +## See also + +- [Integrations](/console/integrations) — the framework adapters that wrap these endpoints. +- [Webhooks](/console/webhooks) — the inbound side: subscribe to events. +- [`/agent/spec`](/agent/spec) — the underlying OC Agent envelope spec (canonical messages, scope grammar, signature rules). +- [`/agent/sub-delegation`](/agent/sub-delegation) — kind 30086 chain semantics. diff --git a/src/pages/console/index.mdx b/src/pages/console/index.mdx new file mode 100644 index 0000000..a4ef12c --- /dev/null +++ b/src/pages/console/index.mdx @@ -0,0 +1,60 @@ +export const metadata = { + title: 'OC Console', + description: + 'Managed infrastructure for the OrangeCheck Agent protocol family. console.ochk.io is the operator surface where customers register signed delegations, ingest stamped actions, anchor audit envelopes to Bitcoin via OC Stamp, and publish to Nostr — without running their own database, signer, or relay fleet.', +}; + +# OC Console + +**Managed infrastructure for the OC Agent family.** [console.ochk.io](https://console.ochk.io) +is where customers register their BIP-322-signed delegations, ingest stamped +agent-action envelopes from their LLM tool calls, anchor every receipt to +Bitcoin via the OC Stamp pipeline, fan out signed deliveries to subscribed +webhook endpoints, and export verifiable audit bundles for compliance — without +running their own Postgres, Nostr publisher, or OTS calendar relay. + +The protocol surface is the same OC Agent + OC Stamp + OC Pledge primitives +documented elsewhere in these docs. Console is a commercial layer on top: same +envelopes, same canonical messages, same offline verification — packaged as a +SaaS so a typical customer ships in an afternoon instead of a quarter. + +## What console is, and what it isn't + +| | | +|---|---| +| **Is** | A turnkey **operator dashboard** for OC Agent envelopes. Sign in with your Bitcoin address, register delegations, watch action receipts stream in, audit-export at any time. | +| **Is** | A **managed publisher**. We sign Nostr events for you (kind 30083 / 30084 / 30085 / 30086), submit OTS commitments to the calendar set, and re-publish on relay flake. | +| **Is** | A **webhook delivery layer**. Subscribe an HTTPS endpoint; we POST HMAC-signed events on every accepted envelope, retry with exponential backoff, surface delivery health. | +| **Is** | A **billing surface**. Lightning rail (BTCPay) is primary; Stripe is the USD parity rail. No token, no custody. | +| **Isn't** | A *new protocol*. Every envelope console persists is identical to one a customer could publish themselves directly via `@orangecheck/agent-cli`. | +| **Isn't** | A *signer*. Console never holds principal keys. Every delegation is BIP-322-signed in the customer's wallet (UniSat, Xverse, Leather, OKX, Phantom, or paste-in). | +| **Isn't** | A *trust anchor*. Verifiers don't trust console — they trust the signatures on the envelopes console publishes. Drop the bundle into [@orangecheck/agent-core](/sdks/javascript) and verify offline. | + +## Where to go next + +- [Quickstart](/console/quickstart) — sign in, bootstrap your project, register your first delegation in five minutes. +- [Integrations](/console/integrations) — drop-in adapters for Anthropic Tool Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Three lines of config and your tool calls flow into console. +- [API reference](/console/api) — OpenAPI 3.1 spec, Bearer-auth examples, error codes. +- [Webhooks](/console/webhooks) — receive signed POSTs on every event your endpoint subscribes to. +- [Audit export](/console/audit) — pull the bundle (NDJSON / JSON / CSV) and verify offline. + +## Five-second mental model + +``` +your LLM tool call + │ + ▼ +@orangecheck/agent-anthropic (or openai / vercel / langgraph / mcp) + │ stamp the call → ActionEnvelope (BIP-322 signed by the agent) + ▼ +console.ochk.io /api/actions + │ + ├─→ Postgres # your tenant-scoped registry + ├─→ Nostr kind 30084 # public, censorship-resistant transport + ├─→ OC Stamp pipeline # batched into Bitcoin block via OTS + └─→ webhooks # signed POST to your subscribers +``` + +Every receipt is content-addressed (`sha256(canonical_message)`), BIP-322-signed +by the agent's address (which the parent delegation bound), and verifies offline +forever — even if console disappears tomorrow. diff --git a/src/pages/console/integrations.mdx b/src/pages/console/integrations.mdx new file mode 100644 index 0000000..f53fb42 --- /dev/null +++ b/src/pages/console/integrations.mdx @@ -0,0 +1,217 @@ +export const metadata = { + title: 'Console integrations', + description: 'Drop-in npm adapters for Anthropic Tool Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Stamp, execute, post — three lines of config.', +}; + +# Console integrations + +Five framework adapters and one shared client. Pick the one that matches +your stack; install it; wire three lines of config; every LLM tool call +becomes a BIP-322-signed action receipt that flows into your console +project automatically. + +## The packages + +| Package | Wraps | npm | +|---|---|---| +| [`@orangecheck/agent-anthropic`](https://www.npmjs.com/package/@orangecheck/agent-anthropic) | Anthropic Messages API tool_use blocks | `0.1.0+` | +| [`@orangecheck/agent-openai`](https://www.npmjs.com/package/@orangecheck/agent-openai) | OpenAI Chat Completions function-call / Responses tool_call | `0.1.0+` | +| [`@orangecheck/agent-vercel`](https://www.npmjs.com/package/@orangecheck/agent-vercel) | Vercel AI SDK `tool()` definitions | `0.1.0+` | +| [`@orangecheck/agent-langgraph`](https://www.npmjs.com/package/@orangecheck/agent-langgraph) | LangGraph tool-node executes | `0.1.0+` | +| [`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp) | Model Context Protocol invocations | `0.1.0+` | +| [`@orangecheck/agent-console-client`](https://www.npmjs.com/package/@orangecheck/agent-console-client) | Shared HTTP client (used by every adapter; install directly if you're rolling your own) | `0.1.0+` | +| [`@orangecheck/webhook-verify`](https://www.npmjs.com/package/@orangecheck/webhook-verify) | Drop-in HMAC verifier for receiving console webhooks | `0.1.0+` | + +Every framework adapter exposes the same shape: + +```ts +const { result, action, posted } = await invokeWithStampAndPost({ + agent: agentSigner, + delegation: signedDelegation, + /* framework-specific tool-call payload */ + call: (normalizedCall) => myExecute(normalizedCall), + console: { + apiToken: process.env.OC_TOKEN!, + projectId: process.env.OC_PROJECT_ID!, + }, +}); +``` + +If `console` is omitted the adapter still stamps + executes; `posted` +just comes back `null`. The post is *fire-and-forget after* the call, +so a flaky console never blocks a tool from running. + +## Anthropic (Claude tool_use) + +```ts +import { invokeWithStampAndPost } from '@orangecheck/agent-anthropic'; + +const message = await anthropic.messages.create({ /* … */ }); +for (const block of message.content) { + if (block.type !== 'tool_use') continue; + const { result, action, posted } = await invokeWithStampAndPost({ + agent: agentSigner, + delegation: signedDelegation, + toolUse: block, + call: (toolUse) => myToolImpl(toolUse.input), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + }); + // feed `result` back to Claude as a tool_result content block. + // `action` + `posted` are your audit trail. +} +``` + +Scope check: `scopeExercised` defaults to `anthropic:tool(name=)` — +the tightest admissible sub-scope. Override via the `scopeExercised` field +if your delegation grants a broader verb. + +## OpenAI (function calling / Responses) + +```ts +import { invokeWithStampAndPost } from '@orangecheck/agent-openai'; + +const completion = await openai.chat.completions.create({ /* … */ }); +const fc = completion.choices[0].message.function_call; +if (fc) { + const { result, action } = await invokeWithStampAndPost({ + agent: agentSigner, + delegation: signedDelegation, + call: fc, + execute: (parsed) => myFn(parsed.args), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + }); +} +``` + +## Vercel AI SDK + +```ts +import { ocTool } from '@orangecheck/agent-vercel'; +import { tool } from 'ai'; + +const createInvoice = ocTool({ + verb: 'invoice.create', + parameters: invoiceSchema, + execute: (args) => myInvoiceCreate(args), +}); + +const tools = { + 'invoice.create': tool({ + parameters: invoiceSchema, + execute: async (args, { toolCallId }) => { + const { result } = await createInvoice.execute(args, { + agent: agentSigner, + delegation: signedDelegation, + callId: toolCallId, + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + }); + return result; + }, + }), +}; +``` + +## LangGraph + +```ts +import { ocToolNode } from '@orangecheck/agent-langgraph'; + +const createInvoice = ocToolNode({ + verb: 'invoice.create', + execute: (args) => myInvoiceCreate(args), +}); + +const graph = new StateGraph(MyState).addNode('createInvoice', async (state) => { + const { result } = await createInvoice.execute(pickArgs(state), { + agent: agentSigner, + delegation: signedDelegation, + callId: state.lastToolCallId, + graphState: state, + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + }); + return mergeIntoState(state, result); +}); +``` + +The `graphStateHash` (deterministic over the supplied state object) is +embedded in the action canonical message so a verifier replaying the +graph against a snapshot gets byte-identical receipts. + +## MCP (Model Context Protocol) + +```ts +import { invokeWithStampAndPost } from '@orangecheck/agent-mcp'; + +const { result, action } = await invokeWithStampAndPost({ + agent: agentSigner, + delegation: signedDelegation, + invocation: { server, tool, params }, + call: (inv) => mcpClient.callTool(inv), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, +}); +``` + +The stamp commits to `(server, tool, params)` *before* the call runs, +so a hung or failing MCP server still leaves an on-record commitment +that the agent attempted exactly this invocation. + +## Receiving the webhooks + +When your console project has webhook endpoints subscribed, every +accepted envelope fires a signed POST. Use the drop-in verifier: + +```ts +import { verify } from '@orangecheck/webhook-verify'; +import express from 'express'; + +const app = express(); +app.use(express.raw({ type: 'application/json' })); + +app.post('/webhooks/orangecheck', (req, res) => { + const ok = verify({ + secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create + signature: req.header('X-OrangeCheck-Signature') ?? '', + rawBody: req.body, + }); + if (!ok) return res.status(401).send('bad signature'); + + const event = JSON.parse(req.body.toString('utf8')); + // handle event.event_type, event.id, event.envelope + res.status(200).send('ok'); +}); +``` + +See [Webhooks](/console/webhooks) for the full event catalog and the +delivery / retry semantics. + +## Direct API (no adapter) + +If your stack isn't on the list above, install the shared client and +post envelopes directly: + +```ts +import { postActionToConsole } from '@orangecheck/agent-console-client'; + +await postActionToConsole(stampedAction, { + apiToken: process.env.OC_TOKEN!, + projectId: process.env.OC_PROJECT_ID!, +}); +``` + +The client also exposes `postDelegationToConsole`, +`postRevocationToConsole`, and `postSubdelegationToConsole` if you +prefer to drive the registry yourself instead of going through the +dashboard. + +## Authentication + +All adapter posts authenticate via Bearer token: + +``` +Authorization: Bearer ock_<64-hex> +``` + +Tokens are minted at `/settings § 03 · API tokens`. The plaintext is +returned **once** at create — store it in your CI's secrets manager +immediately. The token authenticates as the project owner; revoke at +any time from the same surface. diff --git a/src/pages/console/quickstart.mdx b/src/pages/console/quickstart.mdx new file mode 100644 index 0000000..1e95a4c --- /dev/null +++ b/src/pages/console/quickstart.mdx @@ -0,0 +1,117 @@ +export const metadata = { + title: 'Console quickstart', + description: 'Sign in, bootstrap a project, register your first delegation, and see action receipts flow in. Five minutes.', +}; + +# Console quickstart + +Five-minute path from never-heard-of-it to first stamped action receipt +landing in your dashboard. + +## 1. Sign in + +Open [console.ochk.io](https://console.ochk.io) and click **sign in**. +The button routes to the family auth host at +[ochk.io/signin](https://ochk.io/signin) where you authenticate with a +BIP-322 signature from any compatible wallet (UniSat, Xverse, Leather, +OKX, Phantom, or paste-in). On success the auth host issues a +cross-subdomain `oc_session` cookie and bounces you back to the console +dashboard. + +> **No password, no email**. Your Bitcoin address *is* your identity. +> The auth host stores nothing about you that isn't on-chain anyway. + +## 2. Bootstrap your project (automatic) + +The first time you land on `/dashboard` after sign-in, the +**OnboardingBoundary** auto-creates a default project named +`-default`. You'll see a transient "first sign-in · +bootstrapping your default project" banner; once it clears you're on a +real Postgres-backed tenant. + +You can rename the project at `/settings § 01` later. + +## 3. Register a delegation (wallet-signed, in your browser) + +Click **`/agents/new`** in the sidebar. The form asks for: + +| Field | What | +|---|---| +| **agent address** | Bitcoin address the agent will sign actions with. Different from the principal — that's the point of delegation. | +| **scopes** | One or more scope strings, e.g. `mcp:invoke(server=https://mcp.example.com,tool=invoice.create)`. The agent can only sign actions whose `scope_exercised` is a sub-scope of one of these. | +| **expires_at** | When the delegation stops being valid. Default 30 days. | +| **bond_sats** | Optional OC Pledge stake. Skip for v1. | + +Click **sign delegation in wallet**. Your wallet pops up showing the +64-hex envelope id; sign it; the envelope is POSTed to +`/api/delegations`. Console: + +- recomputes the id from canonical inputs and rejects on mismatch (tamper defense) +- requires `principal === session.addr` (forgery defense) +- persists the row, fans out to Nostr kind 30083, submits to OC Stamp +- triggers any subscribed webhook with `event_type: 'delegation.registered'` + +The agent table on `/agents` now shows your new delegation. + +## 4. Wire your LLM tool calls + +Pick the adapter for your framework — install it, supply your agent +keypair + the delegation envelope + a console API token, and every tool +call gets a stamped receipt automatically. + +```ts +// Anthropic Tool Use +import { invokeWithStampAndPost } from '@orangecheck/agent-anthropic'; + +const { result, action, posted } = await invokeWithStampAndPost({ + agent: agentSigner, // your @orangecheck/agent-signer ref + delegation: signedDelegation, + toolUse: claudeToolUseBlock, + call: (toolUse) => myInvoiceCreateImpl(toolUse.input), + console: { + apiToken: process.env.OC_TOKEN!, // ock_<64-hex> + projectId: process.env.OC_PROJECT_ID!, + }, +}); + +// `result` is whatever your tool returned. +// `action` is the BIP-322-signed canonical envelope. +// `posted` is { id, project_id, delegation_id } once console accepts it. +``` + +The same `console: { apiToken, projectId }` knob works on +[`@orangecheck/agent-openai`](https://www.npmjs.com/package/@orangecheck/agent-openai), +[`@orangecheck/agent-vercel`](https://www.npmjs.com/package/@orangecheck/agent-vercel), +[`@orangecheck/agent-langgraph`](https://www.npmjs.com/package/@orangecheck/agent-langgraph), +and [`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp). + +> Get the API token from `/settings § 03 · API tokens`. It's returned +> *once* at create — store it in your CI's secrets manager immediately. + +## 5. Watch the receipts arrive + +Go to `/audit`. Every accepted action shows up as a row with: + +- the envelope id (sha256 of the canonical inputs) +- the scope_exercised +- the content_hash (sha256 of the canonicalized tool call) +- OTS state (`pending` until the OC Stamp pipeline anchors into a Bitcoin block, then `confirmed` with the block height) +- the Nostr event id (once relay-published) + +Drill into `/agents/` to see receipts filtered to that single +delegation, or hit **export signed bundle** to download the full +project bundle as NDJSON / JSON / CSV. + +## You're done + +That's the whole loop. Real delegation envelope, real action envelopes, +real Bitcoin anchor — verifiable offline against +[`@orangecheck/agent-core`](https://www.npmjs.com/package/@orangecheck/agent-core), +no console-side trust required. + +Where to go next: + +- [Concepts](/console#five-second-mental-model) — the data model behind it. +- [Webhooks](/console/webhooks) — push events to your stack. +- [Federation](/agent/spec) — replace the principal with an M-of-N guardian set (v1.2). +- [API reference](/console/api) — full OpenAPI 3.1 spec. diff --git a/src/pages/console/webhooks.mdx b/src/pages/console/webhooks.mdx new file mode 100644 index 0000000..1e454b4 --- /dev/null +++ b/src/pages/console/webhooks.mdx @@ -0,0 +1,153 @@ +export const metadata = { + title: 'Console webhooks', + description: 'Receive HMAC-signed POST deliveries on every accepted delegation, action, revocation, and sub-delegation. Drop-in verifier, exponential-backoff retry, delivery log.', +}; + +# Console webhooks + +Subscribe an HTTPS endpoint at `/settings § 04 · webhook endpoints` and +console will POST a signed delivery on every event your endpoint +subscribes to. Same wire format as Stripe / GitHub / BTCPay — the +[`@orangecheck/webhook-verify`](https://www.npmjs.com/package/@orangecheck/webhook-verify) +package is a drop-in receiver for any Node / Edge / Workers runtime. + +## Event catalog + +| Event | Fires on | +|---|---| +| `delegation.registered` | `POST /api/delegations` accepted (single-address or federation v1.2) | +| `subdelegation.registered` | `POST /api/subdelegations` accepted (kind 30086, OC Agent v1.1) | +| `action.registered` | `POST /api/actions` accepted | +| `revocation.registered` | `POST /api/revocations` accepted (parent delegation flips to `status='revoked'`) | + +Subscribe to any subset when you create the endpoint. New event types +will be added; the subscription field is open-string so customers don't +need a re-register on each addition (your endpoint just receives nothing +for events you didn't subscribe to). + +## Payload shape + +```json +{ + "id": "0123…64-hex", + "project_id": "proj_…", + "kind": "agent-action", + "envelope": { /* full canonical envelope JSON */ }, + "delegation_id": "0123…64-hex" +} +``` + +`envelope` is the same byte-identical canonical envelope you'd get from +`/api/audit/export` or from a Nostr relay subscribed to the project's +kind. Verify offline with +[`@orangecheck/agent-core`](/sdks/javascript) — no console-side trust. + +## Headers + +Every delivery includes: + +| Header | Value | +|---|---| +| `Content-Type` | `application/json` | +| `X-OrangeCheck-Event` | `delegation.registered`, `action.registered`, etc | +| `X-OrangeCheck-Delivery` | opaque per-attempt id | +| `X-OrangeCheck-Idempotency-Key` | stable per-event-fanout id (use this for receiver-side dedup) | +| `X-OrangeCheck-Payload-SHA256` | sha256 hex of the raw body bytes | +| `X-OrangeCheck-Signature` | `sha256=` | +| `X-OrangeCheck-Attempt` | (only on retries) attempt count | +| `X-OrangeCheck-Redelivery` | (only on retries) `"true"` | +| `X-OrangeCheck-Manual-Retry` | (only on operator-triggered retries) `"true"` | + +## Verifying the signature + +```bash +npm install @orangecheck/webhook-verify +``` + +```ts +import { verify } from '@orangecheck/webhook-verify'; +import express from 'express'; + +const app = express(); +// CRITICAL: get the RAW body bytes, not the JSON-parsed object. +app.use(express.raw({ type: 'application/json' })); + +app.post('/webhooks/orangecheck', (req, res) => { + const ok = verify({ + secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create + signature: req.header('X-OrangeCheck-Signature') ?? '', + rawBody: req.body, // Buffer + }); + if (!ok) return res.status(401).send('bad signature'); + + const event = JSON.parse(req.body.toString('utf8')); + // handle event.event_type / event.id / event.envelope + res.status(200).send('ok'); +}); +``` + +The verifier uses Node's `timingSafeEqual` so a malicious server can't +byte-by-byte probe the expected signature. + +## The signing secret + +When you create an endpoint, console returns the signing secret **once** +in the 201 response. Store it in your CI's secrets manager immediately; +we never echo it again. + +Internally, secrets are derived deterministically from a server-side +master key + the endpoint id (`HMAC-SHA256(WEBHOOK_MASTER_KEY, +"oc-webhook-v1:" + endpoint_id)`). The DB stores only `sha256(secret)` +for ad-hoc verification — a database leak alone is insufficient to +forge a delivery. + +To rotate: delete the endpoint and re-register. The new secret is +emitted at create. + +## Retry semantics + +A delivery whose response is non-2xx (or times out after 8 seconds) is +queued for retry with exponential backoff: + +``` +attempt 1 → 2: 1m +attempt 2 → 3: 5m +attempt 3 → 4: 30m +attempt 4 → 5: 2h +attempt 5 → 6: 8h +attempt 6+: give up +``` + +After give-up the delivery row stays in the log with +`error_message: 'http_'` (or the connect-error text). You can +trigger a manual retry from the dashboard at any time via the **retry +now** button — that runs the same logic immediately. + +## The delivery log + +`/settings § 04` surfaces the last 25 deliveries inline under the +endpoints list, plus the row-level **retry now** button. Programmatic +access: + +``` +GET /api/webhooks/deliveries?project_id=proj_…[&endpoint_id=…&limit=N] +``` + +Returns each attempt with status_code, attempt_count, next_retry_at, +succeeded_at, and the per-event-fanout idempotency key. + +## Idempotency on your receiver + +Use `X-OrangeCheck-Idempotency-Key` as the dedup key. The same logical +event-fanout (e.g. one accepted action triggers two subscribed endpoints +each retrying once) shares the idempotency key across attempts to the +same endpoint, but differs across endpoints. So your receiver should: + +```ts +if (await alreadyProcessed(req.header('X-OrangeCheck-Idempotency-Key'))) { + return res.status(200).send('already processed'); +} +``` + +A 200 with no body is the canonical "I got it, don't retry" response. +Any non-2xx (or no response within 8 seconds) triggers retry. From 5bc195fbd8f1f85834725c0ad3178778161884bb Mon Sep 17 00:00:00 2001 From: Xaxis Date: Thu, 30 Apr 2026 12:43:44 -0700 Subject: [PATCH 2/6] docs(console/api): track /api/openapi + /api-explorer + admin log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spec is at /api/openapi (not /api/openapi.json — that path 404s) - Interactive explorer is at /api-explorer (was /docs/api, which now 308-redirects to docs.ochk.io and is unreachable on console) - Add Admin log row to the route catalog (GET /api/admin/log) Brings the public docs in sync with what the console actually serves after PR #5 (admin log) and PR #7 (api-explorer migration). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/console/api.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/console/api.mdx b/src/pages/console/api.mdx index 170bacc..a197b73 100644 --- a/src/pages/console/api.mdx +++ b/src/pages/console/api.mdx @@ -15,13 +15,13 @@ webhooks. A hand-maintained OpenAPI 3.1 spec is served at: ``` -https://console.ochk.io/api/openapi.json +https://console.ochk.io/api/openapi ``` Browse it interactively with try-it-out at: ``` -https://console.ochk.io/docs/api +https://console.ochk.io/api-explorer ``` Generate a typed client for any language with @@ -29,7 +29,7 @@ Generate a typed client for any language with ```bash openapi-generator-cli generate \ - -i https://console.ochk.io/api/openapi.json \ + -i https://console.ochk.io/api/openapi \ -g python \ -o ./oc-console-py ``` @@ -59,8 +59,9 @@ token returns 401; a malformed Bearer falls through to anonymous. | **Audit** | `GET /api/audit/export?format=ndjson\|json\|csv`, `GET /api/audit/bundles` | | **API tokens** | `GET/POST /api/tokens`, `PATCH/DELETE /api/tokens/{id}` | | **Webhooks** | `GET/POST /api/webhooks/endpoints`, `GET/PATCH/DELETE /api/webhooks/endpoints/{id}`, `GET /api/webhooks/deliveries`, `POST /api/webhooks/deliveries/{id}/retry` | +| **Admin log** | `GET /api/admin/log?project_id=&limit=&before=&event_type=` — append-only audit trail of privilege-changing operations (rename / archive, member CRUD, token CRUD, webhook endpoint CRUD). | | **Billing** | `POST /api/billing/portal` (Stripe Customer Portal redirect), `POST /api/checkout/{stripe,lightning}` | -| **Public** | `GET /api/health`, `GET /api/health/deep`, `GET /api/openapi.json`, `GET /api/auth/me`, `POST /api/auth/logout` | +| **Public** | `GET /api/health`, `GET /api/health/deep`, `GET /api/openapi`, `GET /api/auth/me`, `POST /api/auth/logout` | ## Error envelope From 8f0980032e92c8f01389eb7beb450ecfa17d5984 Mon Sep 17 00:00:00 2001 From: Xaxis Date: Thu, 30 Apr 2026 14:01:05 -0700 Subject: [PATCH 3/6] docs(console): auto-generate Reason Catalog + Webhook Events from console.ochk.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now, /console/api.mdx and /console/webhooks.mdx maintained hand-typed tables of error reasons + subscribable events. Those drifted the moment a new entry landed in oc-console-web's typed catalog without a human remembering to update mdx. This PR closes the loop: scripts/gen-catalogs.mjs (build-time fetcher) src/generated/console-catalogs.json (committed snapshot) src/components/docs/ConsoleReasonCatalog.tsx src/components/docs/ConsoleWebhookEventCatalog.tsx Wiring: - prebuild script runs gen-catalogs.mjs which fetches https://console.ochk.io/api/reasons + /api/webhook-events and writes to src/generated/console-catalogs.json. On Vercel that runs at deploy time; locally `yarn build` does the same. - Defensive: if the fetch fails (offline, console down, endpoint not yet deployed) the cached JSON is preserved — build doesn't break. Worst case: docs are one deploy stale. - The committed JSON is seeded from the typed source as of 2026-04-30 (70 reasons / 4 events) so the page renders meaningfully even before the JSON endpoints deploy on console.ochk.io. Pages updated: - /console/api.mdx — Reason Catalog table replaced by , rendered grouped by kind (auth / input / tenancy / method / conflict / rate / config / external / internal). - /console/webhooks.mdx — Event catalog table replaced by , includes payload_fields per event. Pairs with oc-console-web/feat/json-catalog-endpoints which adds /api/reasons + /api/webhook-events. Together: typed source on console is the single source of truth, docs are a view, drift is mechanically impossible. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 + scripts/gen-catalogs.mjs | 104 ++++ src/components/docs/ConsoleReasonCatalog.tsx | 140 +++++ .../docs/ConsoleWebhookEventCatalog.tsx | 83 +++ src/generated/console-catalogs.json | 483 ++++++++++++++++++ src/pages/console/api.mdx | 116 ++--- src/pages/console/index.mdx | 47 +- src/pages/console/integrations.mdx | 151 +++--- src/pages/console/quickstart.mdx | 81 +-- src/pages/console/webhooks.mdx | 118 +++-- 10 files changed, 1070 insertions(+), 255 deletions(-) create mode 100755 scripts/gen-catalogs.mjs create mode 100644 src/components/docs/ConsoleReasonCatalog.tsx create mode 100644 src/components/docs/ConsoleWebhookEventCatalog.tsx create mode 100644 src/generated/console-catalogs.json diff --git a/package.json b/package.json index 6a3bb3c..30c6adb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "description": "docs.ochk.io — unified documentation for the OrangeCheck ecosystem (OC Attest, OC Lock, OC Stamp, OC Vote, OC Agent).", "scripts": { "dev": "rm -rf .next && next dev", + "prebuild": "node scripts/gen-catalogs.mjs", "build": "next build", + "gen:catalogs": "node scripts/gen-catalogs.mjs", "start": "next start", "lint": "eslint", "format": "prettier --write .", diff --git a/scripts/gen-catalogs.mjs b/scripts/gen-catalogs.mjs new file mode 100755 index 0000000..a6ef6fe --- /dev/null +++ b/scripts/gen-catalogs.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Build-time generator for the console catalogs that the docs hub + * embeds: reasons, webhook events. + * + * Why: console.ochk.io maintains the typed source of truth at + * src/server/api/reasons.ts + * src/lib/webhooks/events.ts + * and surfaces JSON projections at /api/reasons and /api/webhook-events. + * + * Until this script existed, docs.ochk.io's /console/api page and + * /console/webhooks page hand-maintained tables that mirrored the + * typed catalog. Those drift the moment a new reason or event_type + * lands on console without someone remembering to update mdx. + * + * This script runs as `prebuild`. It fetches the JSON from prod and + * writes a static asset under src/generated/. On Vercel that runs + * during the build step, before `next build`. Locally `yarn build` + * does the same. `yarn dev` skips it (the existing generated file is + * checked in so dev and CI work without network). + * + * Defensive: if the fetch fails (console down, dev offline, etc.) we + * keep whatever's currently in the generated file. Build doesn't + * fail; the page renders the cached catalog. Worst case is staleness + * for one deploy cycle. + */ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT = join(__dirname, '..'); +const OUT_DIR = join(ROOT, 'src/generated'); +const OUT_PATH = join(OUT_DIR, 'console-catalogs.json'); + +const SOURCES = [ + { + key: 'reasons', + url: 'https://console.ochk.io/api/reasons', + responseField: 'reasons', + }, + { + key: 'webhookEvents', + url: 'https://console.ochk.io/api/webhook-events', + responseField: 'events', + }, +]; + +async function readExisting() { + try { + const buf = await readFile(OUT_PATH, 'utf8'); + return JSON.parse(buf); + } catch { + return { reasons: [], webhookEvents: [], fetchedAt: null, source: 'fallback-empty' }; + } +} + +async function fetchSource(src) { + const r = await fetch(src.url, { + signal: AbortSignal.timeout(5_000), + }); + if (!r.ok) { + throw new Error(`HTTP ${r.status} from ${src.url}`); + } + const body = await r.json(); + if (!body[src.responseField] || !Array.isArray(body[src.responseField])) { + throw new Error(`Missing ${src.responseField} array in response from ${src.url}`); + } + return body[src.responseField]; +} + +async function main() { + const existing = await readExisting(); + const next = { + ...existing, + fetchedAt: new Date().toISOString(), + source: 'fetched', + }; + + let anyFailed = false; + for (const src of SOURCES) { + try { + const data = await fetchSource(src); + next[src.key] = data; + console.log(`[gen-catalogs] ${src.key}: ${data.length} entries from ${src.url}`); + } catch (err) { + anyFailed = true; + console.warn( + `[gen-catalogs] ${src.key}: fetch failed (${err.message}); keeping cached entries` + ); + } + } + + if (anyFailed && next.source === 'fetched') { + next.source = 'fetched-partial'; + } + + await mkdir(OUT_DIR, { recursive: true }); + await writeFile(OUT_PATH, JSON.stringify(next, null, 2) + '\n'); + console.log(`[gen-catalogs] wrote ${OUT_PATH}`); +} + +await main(); diff --git a/src/components/docs/ConsoleReasonCatalog.tsx b/src/components/docs/ConsoleReasonCatalog.tsx new file mode 100644 index 0000000..e779ab2 --- /dev/null +++ b/src/components/docs/ConsoleReasonCatalog.tsx @@ -0,0 +1,140 @@ +/** + * Renders the console.ochk.io reason catalog. + * + * Source data lives at src/generated/console-catalogs.json — refreshed + * at build time by scripts/gen-catalogs.mjs which fetches from + * https://console.ochk.io/api/reasons. Drift between code and docs is + * thereby mechanically impossible: every deploy of docs picks up the + * latest typed catalog, and the typed catalog itself is enforced by + * the contract tests in oc-console-web. + * + * Renders as a sortable, kind-grouped table similar to the original + * hand-maintained one — but every row is data-driven. + */ +import catalogs from '@/generated/console-catalogs.json'; + +type ReasonKind = + | 'auth' + | 'input' + | 'tenancy' + | 'method' + | 'conflict' + | 'rate' + | 'config' + | 'external' + | 'internal'; + +interface ReasonEntry { + id: string; + kind: ReasonKind; + status: number; + description: string; +} + +const KIND_ORDER: ReasonKind[] = [ + 'auth', + 'input', + 'tenancy', + 'method', + 'conflict', + 'rate', + 'config', + 'external', + 'internal', +]; + +const KIND_LABEL: Record = { + auth: 'auth · 401/403', + input: 'input · 400', + tenancy: 'tenancy · 404', + method: 'method · 405', + conflict: 'conflict · 409', + rate: 'rate · 429', + config: 'config · 503', + external: 'external · 5xx', + internal: 'internal · 500', +}; + +export function ConsoleReasonCatalog() { + const reasons = (catalogs.reasons ?? []) as ReasonEntry[]; + const grouped = new Map(); + for (const r of reasons) { + if (!grouped.has(r.kind)) grouped.set(r.kind, []); + grouped.get(r.kind)!.push(r); + } + for (const list of grouped.values()) { + list.sort((a, b) => (a.id < b.id ? -1 : 1)); + } + + if (reasons.length === 0) { + return ( +
+ Catalog not available — the build-time fetch from + https://console.ochk.io/api/reasons + returned empty. Refresh once the endpoint deploys. +
+ ); + } + + return ( +
+ {KIND_ORDER.map((kind) => { + const rows = grouped.get(kind); + if (!rows || rows.length === 0) return null; + return ( +
+

+ {KIND_LABEL[kind]} +

+
+ + + + + + + + + + {rows.map((r) => ( + + + + + + ))} + +
+ reason + + http + + meaning +
+ {r.id} + + {r.status} + + {r.description} +
+
+
+ ); + })} +

+ {reasons.length} reasons · auto-generated from{' '} + + /api/reasons + {' '} + at build time. Source of truth: src/server/api/reasons.ts in{' '} + + orangecheck/oc-console-web + + . +

+
+ ); +} diff --git a/src/components/docs/ConsoleWebhookEventCatalog.tsx b/src/components/docs/ConsoleWebhookEventCatalog.tsx new file mode 100644 index 0000000..bc06223 --- /dev/null +++ b/src/components/docs/ConsoleWebhookEventCatalog.tsx @@ -0,0 +1,83 @@ +/** + * Renders the console.ochk.io subscribable webhook event catalog. + * + * Source: src/generated/console-catalogs.json (built from + * https://console.ochk.io/api/webhook-events at deploy time). The + * typed source of truth is src/lib/webhooks/events.ts in + * oc-console-web. Coverage tests over there guarantee every + * dispatchEvent() call uses an id from this catalog. + */ +import catalogs from '@/generated/console-catalogs.json'; + +interface WebhookEventEntry { + id: string; + description: string; + payloadFields: string[]; +} + +export function ConsoleWebhookEventCatalog() { + const events = (catalogs.webhookEvents ?? []) as WebhookEventEntry[]; + + if (events.length === 0) { + return ( +
+ Catalog not available — the build-time fetch from + https://console.ochk.io/api/webhook-events + returned empty. Refresh once the endpoint deploys. +
+ ); + } + + return ( +
+
+ + + + + + + + + {events.map((e) => ( + + + + + ))} + +
+ event_type + + meaning + payload top-level fields +
+ {e.id} + +

{e.description}

+ {e.payloadFields && e.payloadFields.length > 0 && ( +

+ payload:{' '} + {e.payloadFields.map((f, i) => ( + + {f} + {i < e.payloadFields.length - 1 ? ' · ' : ''} + + ))} +

+ )} +
+
+

+ {events.length} subscribable event types · auto-generated from{' '} + + /api/webhook-events + {' '} + at build time. Source of truth: src/lib/webhooks/events.ts in{' '} + + orangecheck/oc-console-web + + . +

+
+ ); +} diff --git a/src/generated/console-catalogs.json b/src/generated/console-catalogs.json new file mode 100644 index 0000000..5e7efc1 --- /dev/null +++ b/src/generated/console-catalogs.json @@ -0,0 +1,483 @@ +{ + "reasons": [ + { + "id": "agent_must_match_delegation", + "kind": "auth", + "status": 403, + "description": "Action: signing agent ≠ parent delegation's agent." + }, + { + "id": "already_a_member", + "kind": "conflict", + "status": 409, + "description": "(project_id, address) duplicate on member invite." + }, + { + "id": "already_registered", + "kind": "conflict", + "status": 409, + "description": "Content-addressed PK collision — exact envelope already in registry." + }, + { + "id": "already_revoked", + "kind": "conflict", + "status": 409, + "description": "Revocation already applied to this delegation." + }, + { + "id": "already_succeeded", + "kind": "conflict", + "status": 409, + "description": "Webhook delivery retry: this delivery already succeeded." + }, + { + "id": "apply_failed", + "kind": "internal", + "status": 500, + "description": "Webhook delivery retry: apply step failed unexpectedly." + }, + { + "id": "bad_company", + "kind": "input", + "status": 400, + "description": "Contact form: company field too long." + }, + { + "id": "bad_email", + "kind": "input", + "status": 400, + "description": "Contact form: email failed RFC validation." + }, + { + "id": "bad_message", + "kind": "input", + "status": 400, + "description": "Contact form: message empty or too long." + }, + { + "id": "bad_name", + "kind": "input", + "status": 400, + "description": "Contact form: name field empty or too long." + }, + { + "id": "bad_request", + "kind": "input", + "status": 400, + "description": "Generic input-shape failure (only used where context makes it obvious)." + }, + { + "id": "bad_topic", + "kind": "input", + "status": 400, + "description": "Contact form: topic not in the allowed enum." + }, + { + "id": "btcpay_error", + "kind": "external", + "status": 502, + "description": "BTCPay Server returned an error." + }, + { + "id": "btcpay_fetch_error", + "kind": "external", + "status": 502, + "description": "Failed to reach BTCPay Server (network/DNS)." + }, + { + "id": "btcpay_not_configured", + "kind": "config", + "status": 503, + "description": "BTCPay Lightning checkout disabled — BTCPAY_* env unset." + }, + { + "id": "canonicalization_failed", + "kind": "input", + "status": 400, + "description": "Inputs don't form a valid canonical message." + }, + { + "id": "checkout_not_configured", + "kind": "config", + "status": 503, + "description": "Generic checkout config absent (price ids, etc)." + }, + { + "id": "cron_secret_unset", + "kind": "config", + "status": 503, + "description": "CRON_SECRET unset — cron endpoint refuses to run." + }, + { + "id": "cross_site_blocked", + "kind": "auth", + "status": 403, + "description": "Browser-driven state-changing request without same-origin Sec-Fetch-Site." + }, + { + "id": "database_not_configured", + "kind": "config", + "status": 503, + "description": "DATABASE_URL unset — console is in v1.1 preview mode." + }, + { + "id": "delegation_inactive", + "kind": "conflict", + "status": 409, + "description": "Action: parent delegation has been revoked or expired." + }, + { + "id": "delegation_not_found", + "kind": "tenancy", + "status": 404, + "description": "Action / revocation: delegation_id doesn't exist in this project." + }, + { + "id": "descriptor_id_mismatch", + "kind": "input", + "status": 400, + "description": "Federation: declared descriptor_id ≠ recomputed from inlined descriptor." + }, + { + "id": "descriptor_invalid", + "kind": "input", + "status": 400, + "description": "Federation: descriptor JSON failed schema validation." + }, + { + "id": "duplicate_guardian", + "kind": "input", + "status": 400, + "description": "Federation: two signatures share a guardian_address." + }, + { + "id": "endpoint_not_in_project", + "kind": "tenancy", + "status": 404, + "description": "Webhook delivery retry: endpoint belongs to a different project." + }, + { + "id": "endpoint_paused", + "kind": "conflict", + "status": 409, + "description": "Webhook delivery retry: endpoint.active = false." + }, + { + "id": "expired_cannot_restore", + "kind": "conflict", + "status": 409, + "description": "API token: past expires_at, restoration would be useless." + }, + { + "id": "federation_not_yet_supported", + "kind": "input", + "status": 400, + "description": "Sub-delegation: federation principals not yet allowed at this verb (v1.2 follow-up)." + }, + { + "id": "federation_principal_wrong_endpoint", + "kind": "input", + "status": 400, + "description": "POST a federation principal to /api/delegations/federation, not /api/delegations." + }, + { + "id": "honeypot", + "kind": "auth", + "status": 403, + "description": "Contact form: hidden honeypot field was filled (bot signal)." + }, + { + "id": "id_mismatch", + "kind": "input", + "status": 400, + "description": "Declared envelope id ≠ recomputed id from canonical inputs (tamper)." + }, + { + "id": "insert_failed", + "kind": "internal", + "status": 500, + "description": "Database insert returned no row (unexpected)." + }, + { + "id": "insufficient_signatures", + "kind": "input", + "status": 400, + "description": "Federation: len(signatures) < M." + }, + { + "id": "invalid_body", + "kind": "input", + "status": 400, + "description": "Request body failed zod validation. See `detail`." + }, + { + "id": "invalid_id", + "kind": "input", + "status": 400, + "description": "Path :id was empty or malformed." + }, + { + "id": "invalid_query", + "kind": "input", + "status": 400, + "description": "Query string failed zod validation. See `detail`." + }, + { + "id": "invalid_signature", + "kind": "auth", + "status": 401, + "description": "Webhook receiver: HMAC verification failed." + }, + { + "id": "last_owner_cannot_demote", + "kind": "conflict", + "status": 409, + "description": "Same — can't demote the last owner." + }, + { + "id": "last_owner_cannot_remove", + "kind": "conflict", + "status": 409, + "description": "Project foot-gun: refused to remove the last owner." + }, + { + "id": "mailer_error", + "kind": "external", + "status": 502, + "description": "Resend API rejected the contact-form send." + }, + { + "id": "mailer_not_configured", + "kind": "config", + "status": 503, + "description": "Resend API not configured — contact form can't send." + }, + { + "id": "malformed_json", + "kind": "input", + "status": 400, + "description": "Body wasn't parseable as JSON." + }, + { + "id": "malformed_signature", + "kind": "input", + "status": 400, + "description": "Federation: a signature value was missing or non-string." + }, + { + "id": "method_not_allowed", + "kind": "method", + "status": 405, + "description": "HTTP verb not allowed on this route. See `Allow` header." + }, + { + "id": "missing_signature", + "kind": "auth", + "status": 401, + "description": "Webhook receiver: signature header absent." + }, + { + "id": "no_fields_to_update", + "kind": "input", + "status": 400, + "description": "PATCH body had no recognized fields." + }, + { + "id": "no_stripe_customer", + "kind": "config", + "status": 503, + "description": "Customer Portal: this project hasn't paid via Stripe yet." + }, + { + "id": "not_found", + "kind": "tenancy", + "status": 404, + "description": "Resource not visible to caller (or doesn't exist — we don't leak existence)." + }, + { + "id": "owner_required", + "kind": "auth", + "status": 403, + "description": "Operation requires owner role (e.g. project archive)." + }, + { + "id": "owner_role_requires_owner", + "kind": "auth", + "status": 403, + "description": "Privilege escalation: only an owner can mint or remove another owner." + }, + { + "id": "parent_inactive", + "kind": "conflict", + "status": 409, + "description": "Sub-delegation: parent.status ≠ 'active'." + }, + { + "id": "parent_not_found", + "kind": "tenancy", + "status": 404, + "description": "Sub-delegation: parent_id doesn't exist in this project." + }, + { + "id": "principal_must_be_parent_agent", + "kind": "auth", + "status": 403, + "description": "Sub-delegation: signing principal is not the parent's agent." + }, + { + "id": "principal_must_match_session", + "kind": "auth", + "status": 403, + "description": "Body declares principal_address ≠ session.addr (forgery defense)." + }, + { + "id": "rate_limited", + "kind": "rate", + "status": 429, + "description": "Per-IP, per-route token bucket exceeded. Backoff + jitter on retry." + }, + { + "id": "role_forbidden", + "kind": "auth", + "status": 403, + "description": "Caller's project role doesn't permit this operation." + }, + { + "id": "signer_must_be_principal", + "kind": "auth", + "status": 403, + "description": "Revocation: signer is not the parent delegation's principal (v1.1)." + }, + { + "id": "signer_must_match_session", + "kind": "auth", + "status": 403, + "description": "Revocation: session.addr ≠ signer_address." + }, + { + "id": "stripe_error", + "kind": "external", + "status": 502, + "description": "Stripe API returned an error; see `detail`." + }, + { + "id": "stripe_no_url", + "kind": "external", + "status": 502, + "description": "Stripe Customer Portal returned no `url` field." + }, + { + "id": "stripe_not_configured", + "kind": "config", + "status": 503, + "description": "Stripe checkout / portal disabled — STRIPE_SECRET_KEY unset." + }, + { + "id": "threshold_mismatch", + "kind": "input", + "status": 400, + "description": "Federation: sig.threshold ≠ descriptor.threshold." + }, + { + "id": "tier_not_self_serve", + "kind": "conflict", + "status": 409, + "description": "Checkout: enterprise tier requires sales contact." + }, + { + "id": "unauthenticated", + "kind": "auth", + "status": 401, + "description": "No valid cookie or Bearer token." + }, + { + "id": "unauthorized", + "kind": "auth", + "status": 401, + "description": "Cron-secret or webhook receiver auth missing/invalid." + }, + { + "id": "unknown_guardian", + "kind": "input", + "status": 400, + "description": "Federation: a signing guardian is not in the descriptor." + }, + { + "id": "update_failed", + "kind": "internal", + "status": 500, + "description": "Database update returned no row (unexpected)." + }, + { + "id": "webhook_master_key_unset", + "kind": "config", + "status": 503, + "description": "WEBHOOK_MASTER_KEY unset — endpoint registration disabled." + }, + { + "id": "webhook_not_configured", + "kind": "config", + "status": 503, + "description": "Stripe/BTCPay webhook receiver: signing secret env var unset." + } + ], + "webhookEvents": [ + { + "id": "action.registered", + "description": "An action envelope (kind 30084) has been accepted by /api/actions. Fires once per ingested action; OTS confirmation is a separate phase 2b event.", + "payloadFields": [ + "id", + "project_id", + "delegation_id", + "agent_address", + "scope_exercised", + "content_hash", + "content_length", + "content_mime", + "signed_at" + ] + }, + { + "id": "delegation.registered", + "description": "A new delegation envelope (kind 30083) — single-address or federation — has been accepted by /api/delegations or /api/delegations/federation.", + "payloadFields": [ + "id", + "project_id", + "principal_address", + "agent_address", + "scopes", + "issued_at", + "expires_at", + "status" + ] + }, + { + "id": "revocation.registered", + "description": "A revocation envelope (kind 30085) has been accepted by /api/revocations. The referenced delegation transitions status -> 'revoked'.", + "payloadFields": [ + "id", + "project_id", + "delegation_id", + "signer_address", + "reason", + "signed_at" + ] + }, + { + "id": "subdelegation.registered", + "description": "A sub-delegation envelope (kind 30086, OC Agent v1.1) has been accepted by /api/subdelegations.", + "payloadFields": [ + "id", + "project_id", + "parent_id", + "principal_address", + "agent_address", + "scopes", + "issued_at", + "expires_at" + ] + } + ], + "fetchedAt": "2026-04-30T21:00:22.286Z", + "source": "fetched-partial" +} diff --git a/src/pages/console/api.mdx b/src/pages/console/api.mdx index a197b73..fad8962 100644 --- a/src/pages/console/api.mdx +++ b/src/pages/console/api.mdx @@ -1,14 +1,16 @@ +import { ConsoleReasonCatalog } from '@/components/docs/ConsoleReasonCatalog'; + export const metadata = { title: 'Console API reference', - description: 'OpenAPI 3.1 spec, authentication, error codes, and the route catalog for console.ochk.io.', + description: + 'OpenAPI 3.1 spec, authentication, error codes, and the route catalog for console.ochk.io.', }; # Console API reference -console.ochk.io exposes a REST API for managing projects, registering -signed delegations / actions / revocations / sub-delegations / federation -envelopes, ingesting from CI/CD, downloading audit bundles, and managing -webhooks. +console.ochk.io exposes a REST API for managing projects, registering signed +delegations / actions / revocations / sub-delegations / federation envelopes, +ingesting from CI/CD, downloading audit bundles, and managing webhooks. ## OpenAPI spec @@ -38,30 +40,30 @@ openapi-generator-cli generate \ Two schemes, mutually exclusive per request: -| Scheme | Header | When to use | -|---|---|---| -| **Cookie** | `Cookie: oc_session=` | Browser flows. Set automatically when you sign in at [ochk.io/signin](https://ochk.io/signin); valid for every `*.ochk.io` subdomain. | +| Scheme | Header | When to use | +| ---------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | +| **Cookie** | `Cookie: oc_session=` | Browser flows. Set automatically when you sign in at [ochk.io/signin](https://ochk.io/signin); valid for every `*.ochk.io` subdomain. | | **Bearer token** | `Authorization: Bearer ock_<64-hex>` | Server-side / CI / any non-browser caller. Mint at `/settings § 03`; plaintext returned ONCE at create. Authenticates as the project owner. | -Tokens take precedence over the cookie if both are supplied. A revoked -token returns 401; a malformed Bearer falls through to anonymous. +Tokens take precedence over the cookie if both are supplied. A revoked token +returns 401; a malformed Bearer falls through to anonymous. ## Route catalog -| Group | Routes | -|---|---| -| **Projects** | `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{id}` | -| **Members** | `GET/POST /api/members`, `PATCH/DELETE /api/members/{id}` | -| **Delegations** | `GET/POST /api/delegations`, `GET /api/delegations/{id}`, `POST /api/delegations/federation` | -| **Sub-delegations** | `GET/POST /api/subdelegations` (kind 30086, OC Agent v1.1) | -| **Actions** | `GET/POST /api/actions` | -| **Revocations** | `GET/POST /api/revocations` | -| **Audit** | `GET /api/audit/export?format=ndjson\|json\|csv`, `GET /api/audit/bundles` | -| **API tokens** | `GET/POST /api/tokens`, `PATCH/DELETE /api/tokens/{id}` | -| **Webhooks** | `GET/POST /api/webhooks/endpoints`, `GET/PATCH/DELETE /api/webhooks/endpoints/{id}`, `GET /api/webhooks/deliveries`, `POST /api/webhooks/deliveries/{id}/retry` | -| **Admin log** | `GET /api/admin/log?project_id=&limit=&before=&event_type=` — append-only audit trail of privilege-changing operations (rename / archive, member CRUD, token CRUD, webhook endpoint CRUD). | -| **Billing** | `POST /api/billing/portal` (Stripe Customer Portal redirect), `POST /api/checkout/{stripe,lightning}` | -| **Public** | `GET /api/health`, `GET /api/health/deep`, `GET /api/openapi`, `GET /api/auth/me`, `POST /api/auth/logout` | +| Group | Routes | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Projects** | `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{id}` | +| **Members** | `GET/POST /api/members`, `PATCH/DELETE /api/members/{id}` | +| **Delegations** | `GET/POST /api/delegations`, `GET /api/delegations/{id}`, `POST /api/delegations/federation` | +| **Sub-delegations** | `GET/POST /api/subdelegations` (kind 30086, OC Agent v1.1) | +| **Actions** | `GET/POST /api/actions` | +| **Revocations** | `GET/POST /api/revocations` | +| **Audit** | `GET /api/audit/export?format=ndjson\|json\|csv`, `GET /api/audit/bundles` | +| **API tokens** | `GET/POST /api/tokens`, `PATCH/DELETE /api/tokens/{id}` | +| **Webhooks** | `GET/POST /api/webhooks/endpoints`, `GET/PATCH/DELETE /api/webhooks/endpoints/{id}`, `GET /api/webhooks/deliveries`, `POST /api/webhooks/deliveries/{id}/retry` | +| **Admin log** | `GET /api/admin/log?project_id=&limit=&before=&event_type=` — append-only audit trail of privilege-changing operations (rename / archive, member CRUD, token CRUD, webhook endpoint CRUD). | +| **Billing** | `POST /api/billing/portal` (Stripe Customer Portal redirect), `POST /api/checkout/{stripe,lightning}` | +| **Public** | `GET /api/health`, `GET /api/health/deep`, `GET /api/openapi`, `GET /api/auth/me`, `POST /api/auth/logout` | ## Error envelope @@ -78,40 +80,16 @@ Every non-2xx response uses a uniform shape: `reason` is a stable enum-like string — safe to switch on. `detail` is human-readable and may change without notice. -### Reason catalog (the ones you'll see) - -| HTTP | Reason | Meaning | -|---|---|---| -| 400 | `id_mismatch` | Declared envelope id ≠ recomputed id from canonical inputs. Tamper detected. | -| 400 | `descriptor_id_mismatch` | Federation: declared descriptor_id ≠ recomputed from inlined descriptor. | -| 400 | `threshold_mismatch` | Federation: `sig.threshold` ≠ `descriptor.threshold`. | -| 400 | `insufficient_signatures` | Federation: `len(signatures) < M`. | -| 400 | `unknown_guardian` | Federation: a signing guardian is not in the descriptor. | -| 400 | `duplicate_guardian` | Federation: two signatures share a guardian_address. | -| 400 | `federation_principal_wrong_endpoint` | Single-address /api/delegations got a federation principal — POST to /api/delegations/federation instead. | -| 400 | `canonicalization_failed` | Inputs don't form a valid canonical message (malformed timestamp, scope grammar, etc). | -| 401 | `unauthenticated` | No valid cookie OR Bearer token. | -| 403 | `principal_must_match_session` | Body declares a principal_address ≠ session.addr. Forgery defense. | -| 403 | `principal_must_be_parent_agent` | Sub-delegation: signing principal isn't the parent's agent. | -| 403 | `signer_must_be_principal` | Revocation: signer isn't the parent delegation's principal (v1.1). | -| 403 | `signer_must_match_session` | Revocation: session.addr ≠ signer_address. | -| 403 | `agent_must_match_delegation` | Action: signing agent ≠ parent delegation's agent. | -| 403 | `role_forbidden` | Caller's project role doesn't permit the operation. | -| 403 | `owner_role_requires_owner` | Privilege escalation: only an owner can mint or remove another owner. | -| 404 | `not_found` | Including: project not visible to caller, delegation not in project, malformed id (no existence leak). | -| 404 | `parent_not_found` | Sub-delegation: declared parent_id doesn't exist in this project. | -| 404 | `delegation_not_found` | Action / revocation: declared delegation_id doesn't exist in this project. | -| 409 | `already_registered` | Content-addressed PK collision — this exact envelope is already in the registry. | -| 409 | `already_revoked` | Revocation already applied to this delegation. | -| 409 | `already_a_member` | Member: (project_id, address) duplicate. | -| 409 | `last_owner_cannot_remove` | Project foot-gun: refused to remove the last owner. | -| 409 | `last_owner_cannot_demote` | Same — can't demote the last owner. | -| 409 | `parent_inactive` | Sub-delegation: parent.status ≠ 'active'. | -| 409 | `delegation_inactive` | Action: parent delegation has been revoked or expired. | -| 503 | `database_not_configured` | Console is in v1.1 preview mode (no `DATABASE_URL`). The dashboard falls back to mock data. | -| 503 | `webhook_master_key_unset` | Endpoint registration disabled (no `WEBHOOK_MASTER_KEY`). | -| 503 | `stripe_not_configured` | Stripe checkout / portal disabled (no `STRIPE_SECRET_KEY`). | -| 503 | `no_stripe_customer` | Customer Portal: this project hasn't paid via Stripe yet. | +### Reason catalog + +The full set of stable `reason` strings the API can return, grouped by class. +This table is auto-generated at build time from +[`https://console.ochk.io/api/reasons`](https://console.ochk.io/api/reasons) — +the typed source of truth lives at `src/server/api/reasons.ts` in +[orangecheck/oc-console-web](https://github.com/orangecheck/oc-console-web). +Adding a new reason there flows here on the next deploy. + + ## Rate limiting @@ -128,21 +106,23 @@ Apply jitter + exponential backoff in your retries. ## Same-origin enforcement Browser-driven `POST` / `PATCH` / `DELETE` requires the -`Sec-Fetch-Site: same-origin` header (modern browsers send it -automatically). Bearer-authenticated calls from a server runtime are -exempt — the Bearer token is sufficient evidence of intent. Cross-origin -browser POSTs return 403 `cross_site_blocked`. +`Sec-Fetch-Site: same-origin` header (modern browsers send it automatically). +Bearer-authenticated calls from a server runtime are exempt — the Bearer token +is sufficient evidence of intent. Cross-origin browser POSTs return 403 +`cross_site_blocked`. ## Idempotency -Every accepted envelope is content-addressed: its sha256 *is* its -primary key. Re-POSTing the exact same envelope returns 409 -`already_registered`. This makes client-side retry safe — you can fire -the same POST twice without persisting twice. +Every accepted envelope is content-addressed: its sha256 _is_ its primary key. +Re-POSTing the exact same envelope returns 409 `already_registered`. This makes +client-side retry safe — you can fire the same POST twice without persisting +twice. ## See also -- [Integrations](/console/integrations) — the framework adapters that wrap these endpoints. +- [Integrations](/console/integrations) — the framework adapters that wrap these + endpoints. - [Webhooks](/console/webhooks) — the inbound side: subscribe to events. -- [`/agent/spec`](/agent/spec) — the underlying OC Agent envelope spec (canonical messages, scope grammar, signature rules). +- [`/agent/spec`](/agent/spec) — the underlying OC Agent envelope spec + (canonical messages, scope grammar, signature rules). - [`/agent/sub-delegation`](/agent/sub-delegation) — kind 30086 chain semantics. diff --git a/src/pages/console/index.mdx b/src/pages/console/index.mdx index a4ef12c..308efbb 100644 --- a/src/pages/console/index.mdx +++ b/src/pages/console/index.mdx @@ -6,12 +6,13 @@ export const metadata = { # OC Console -**Managed infrastructure for the OC Agent family.** [console.ochk.io](https://console.ochk.io) -is where customers register their BIP-322-signed delegations, ingest stamped -agent-action envelopes from their LLM tool calls, anchor every receipt to -Bitcoin via the OC Stamp pipeline, fan out signed deliveries to subscribed -webhook endpoints, and export verifiable audit bundles for compliance — without -running their own Postgres, Nostr publisher, or OTS calendar relay. +**Managed infrastructure for the OC Agent family.** +[console.ochk.io](https://console.ochk.io) is where customers register their +BIP-322-signed delegations, ingest stamped agent-action envelopes from their LLM +tool calls, anchor every receipt to Bitcoin via the OC Stamp pipeline, fan out +signed deliveries to subscribed webhook endpoints, and export verifiable audit +bundles for compliance — without running their own Postgres, Nostr publisher, or +OTS calendar relay. The protocol surface is the same OC Agent + OC Stamp + OC Pledge primitives documented elsewhere in these docs. Console is a commercial layer on top: same @@ -20,23 +21,29 @@ SaaS so a typical customer ships in an afternoon instead of a quarter. ## What console is, and what it isn't -| | | -|---|---| -| **Is** | A turnkey **operator dashboard** for OC Agent envelopes. Sign in with your Bitcoin address, register delegations, watch action receipts stream in, audit-export at any time. | -| **Is** | A **managed publisher**. We sign Nostr events for you (kind 30083 / 30084 / 30085 / 30086), submit OTS commitments to the calendar set, and re-publish on relay flake. | -| **Is** | A **webhook delivery layer**. Subscribe an HTTPS endpoint; we POST HMAC-signed events on every accepted envelope, retry with exponential backoff, surface delivery health. | -| **Is** | A **billing surface**. Lightning rail (BTCPay) is primary; Stripe is the USD parity rail. No token, no custody. | -| **Isn't** | A *new protocol*. Every envelope console persists is identical to one a customer could publish themselves directly via `@orangecheck/agent-cli`. | -| **Isn't** | A *signer*. Console never holds principal keys. Every delegation is BIP-322-signed in the customer's wallet (UniSat, Xverse, Leather, OKX, Phantom, or paste-in). | -| **Isn't** | A *trust anchor*. Verifiers don't trust console — they trust the signatures on the envelopes console publishes. Drop the bundle into [@orangecheck/agent-core](/sdks/javascript) and verify offline. | +| | | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Is** | A turnkey **operator dashboard** for OC Agent envelopes. Sign in with your Bitcoin address, register delegations, watch action receipts stream in, audit-export at any time. | +| **Is** | A **managed publisher**. We sign Nostr events for you (kind 30083 / 30084 / 30085 / 30086), submit OTS commitments to the calendar set, and re-publish on relay flake. | +| **Is** | A **webhook delivery layer**. Subscribe an HTTPS endpoint; we POST HMAC-signed events on every accepted envelope, retry with exponential backoff, surface delivery health. | +| **Is** | A **billing surface**. Lightning rail (BTCPay) is primary; Stripe is the USD parity rail. No token, no custody. | +| **Isn't** | A _new protocol_. Every envelope console persists is identical to one a customer could publish themselves directly via `@orangecheck/agent-cli`. | +| **Isn't** | A _signer_. Console never holds principal keys. Every delegation is BIP-322-signed in the customer's wallet (UniSat, Xverse, Leather, OKX, Phantom, or paste-in). | +| **Isn't** | A _trust anchor_. Verifiers don't trust console — they trust the signatures on the envelopes console publishes. Drop the bundle into [@orangecheck/agent-core](/sdks/javascript) and verify offline. | ## Where to go next -- [Quickstart](/console/quickstart) — sign in, bootstrap your project, register your first delegation in five minutes. -- [Integrations](/console/integrations) — drop-in adapters for Anthropic Tool Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Three lines of config and your tool calls flow into console. -- [API reference](/console/api) — OpenAPI 3.1 spec, Bearer-auth examples, error codes. -- [Webhooks](/console/webhooks) — receive signed POSTs on every event your endpoint subscribes to. -- [Audit export](/console/audit) — pull the bundle (NDJSON / JSON / CSV) and verify offline. +- [Quickstart](/console/quickstart) — sign in, bootstrap your project, register + your first delegation in five minutes. +- [Integrations](/console/integrations) — drop-in adapters for Anthropic Tool + Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Three lines + of config and your tool calls flow into console. +- [API reference](/console/api) — OpenAPI 3.1 spec, Bearer-auth examples, error + codes. +- [Webhooks](/console/webhooks) — receive signed POSTs on every event your + endpoint subscribes to. +- [Audit export](/console/audit) — pull the bundle (NDJSON / JSON / CSV) and + verify offline. ## Five-second mental model diff --git a/src/pages/console/integrations.mdx b/src/pages/console/integrations.mdx index f53fb42..17c90ad 100644 --- a/src/pages/console/integrations.mdx +++ b/src/pages/console/integrations.mdx @@ -1,84 +1,89 @@ export const metadata = { title: 'Console integrations', - description: 'Drop-in npm adapters for Anthropic Tool Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Stamp, execute, post — three lines of config.', + description: + 'Drop-in npm adapters for Anthropic Tool Use, OpenAI function calling, Vercel AI SDK, LangGraph, and MCP. Stamp, execute, post — three lines of config.', }; # Console integrations -Five framework adapters and one shared client. Pick the one that matches -your stack; install it; wire three lines of config; every LLM tool call -becomes a BIP-322-signed action receipt that flows into your console -project automatically. +Five framework adapters and one shared client. Pick the one that matches your +stack; install it; wire three lines of config; every LLM tool call becomes a +BIP-322-signed action receipt that flows into your console project +automatically. ## The packages -| Package | Wraps | npm | -|---|---|---| -| [`@orangecheck/agent-anthropic`](https://www.npmjs.com/package/@orangecheck/agent-anthropic) | Anthropic Messages API tool_use blocks | `0.1.0+` | -| [`@orangecheck/agent-openai`](https://www.npmjs.com/package/@orangecheck/agent-openai) | OpenAI Chat Completions function-call / Responses tool_call | `0.1.0+` | -| [`@orangecheck/agent-vercel`](https://www.npmjs.com/package/@orangecheck/agent-vercel) | Vercel AI SDK `tool()` definitions | `0.1.0+` | -| [`@orangecheck/agent-langgraph`](https://www.npmjs.com/package/@orangecheck/agent-langgraph) | LangGraph tool-node executes | `0.1.0+` | -| [`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp) | Model Context Protocol invocations | `0.1.0+` | +| Package | Wraps | npm | +| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------- | +| [`@orangecheck/agent-anthropic`](https://www.npmjs.com/package/@orangecheck/agent-anthropic) | Anthropic Messages API tool_use blocks | `0.1.0+` | +| [`@orangecheck/agent-openai`](https://www.npmjs.com/package/@orangecheck/agent-openai) | OpenAI Chat Completions function-call / Responses tool_call | `0.1.0+` | +| [`@orangecheck/agent-vercel`](https://www.npmjs.com/package/@orangecheck/agent-vercel) | Vercel AI SDK `tool()` definitions | `0.1.0+` | +| [`@orangecheck/agent-langgraph`](https://www.npmjs.com/package/@orangecheck/agent-langgraph) | LangGraph tool-node executes | `0.1.0+` | +| [`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp) | Model Context Protocol invocations | `0.1.0+` | | [`@orangecheck/agent-console-client`](https://www.npmjs.com/package/@orangecheck/agent-console-client) | Shared HTTP client (used by every adapter; install directly if you're rolling your own) | `0.1.0+` | -| [`@orangecheck/webhook-verify`](https://www.npmjs.com/package/@orangecheck/webhook-verify) | Drop-in HMAC verifier for receiving console webhooks | `0.1.0+` | +| [`@orangecheck/webhook-verify`](https://www.npmjs.com/package/@orangecheck/webhook-verify) | Drop-in HMAC verifier for receiving console webhooks | `0.1.0+` | Every framework adapter exposes the same shape: ```ts const { result, action, posted } = await invokeWithStampAndPost({ - agent: agentSigner, + agent: agentSigner, delegation: signedDelegation, /* framework-specific tool-call payload */ - call: (normalizedCall) => myExecute(normalizedCall), - console: { - apiToken: process.env.OC_TOKEN!, + call: (normalizedCall) => myExecute(normalizedCall), + console: { + apiToken: process.env.OC_TOKEN!, projectId: process.env.OC_PROJECT_ID!, }, }); ``` -If `console` is omitted the adapter still stamps + executes; `posted` -just comes back `null`. The post is *fire-and-forget after* the call, -so a flaky console never blocks a tool from running. +If `console` is omitted the adapter still stamps + executes; `posted` just comes +back `null`. The post is _fire-and-forget after_ the call, so a flaky console +never blocks a tool from running. ## Anthropic (Claude tool_use) ```ts import { invokeWithStampAndPost } from '@orangecheck/agent-anthropic'; -const message = await anthropic.messages.create({ /* … */ }); +const message = await anthropic.messages.create({ + /* … */ +}); for (const block of message.content) { if (block.type !== 'tool_use') continue; const { result, action, posted } = await invokeWithStampAndPost({ - agent: agentSigner, + agent: agentSigner, delegation: signedDelegation, - toolUse: block, - call: (toolUse) => myToolImpl(toolUse.input), - console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + toolUse: block, + call: (toolUse) => myToolImpl(toolUse.input), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, }); // feed `result` back to Claude as a tool_result content block. // `action` + `posted` are your audit trail. } ``` -Scope check: `scopeExercised` defaults to `anthropic:tool(name=)` — -the tightest admissible sub-scope. Override via the `scopeExercised` field -if your delegation grants a broader verb. +Scope check: `scopeExercised` defaults to `anthropic:tool(name=)` — the +tightest admissible sub-scope. Override via the `scopeExercised` field if your +delegation grants a broader verb. ## OpenAI (function calling / Responses) ```ts import { invokeWithStampAndPost } from '@orangecheck/agent-openai'; -const completion = await openai.chat.completions.create({ /* … */ }); +const completion = await openai.chat.completions.create({ + /* … */ +}); const fc = completion.choices[0].message.function_call; if (fc) { const { result, action } = await invokeWithStampAndPost({ - agent: agentSigner, + agent: agentSigner, delegation: signedDelegation, - call: fc, - execute: (parsed) => myFn(parsed.args), - console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + call: fc, + execute: (parsed) => myFn(parsed.args), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, }); } ``` @@ -100,10 +105,10 @@ const tools = { parameters: invoiceSchema, execute: async (args, { toolCallId }) => { const { result } = await createInvoice.execute(args, { - agent: agentSigner, + agent: agentSigner, delegation: signedDelegation, - callId: toolCallId, - console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + callId: toolCallId, + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, }); return result; }, @@ -121,21 +126,24 @@ const createInvoice = ocToolNode({ execute: (args) => myInvoiceCreate(args), }); -const graph = new StateGraph(MyState).addNode('createInvoice', async (state) => { - const { result } = await createInvoice.execute(pickArgs(state), { - agent: agentSigner, - delegation: signedDelegation, - callId: state.lastToolCallId, - graphState: state, - console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, - }); - return mergeIntoState(state, result); -}); +const graph = new StateGraph(MyState).addNode( + 'createInvoice', + async (state) => { + const { result } = await createInvoice.execute(pickArgs(state), { + agent: agentSigner, + delegation: signedDelegation, + callId: state.lastToolCallId, + graphState: state, + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + }); + return mergeIntoState(state, result); + } +); ``` -The `graphStateHash` (deterministic over the supplied state object) is -embedded in the action canonical message so a verifier replaying the -graph against a snapshot gets byte-identical receipts. +The `graphStateHash` (deterministic over the supplied state object) is embedded +in the action canonical message so a verifier replaying the graph against a +snapshot gets byte-identical receipts. ## MCP (Model Context Protocol) @@ -143,22 +151,22 @@ graph against a snapshot gets byte-identical receipts. import { invokeWithStampAndPost } from '@orangecheck/agent-mcp'; const { result, action } = await invokeWithStampAndPost({ - agent: agentSigner, + agent: agentSigner, delegation: signedDelegation, invocation: { server, tool, params }, - call: (inv) => mcpClient.callTool(inv), - console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, + call: (inv) => mcpClient.callTool(inv), + console: { apiToken: OC_TOKEN, projectId: OC_PROJECT_ID }, }); ``` -The stamp commits to `(server, tool, params)` *before* the call runs, -so a hung or failing MCP server still leaves an on-record commitment -that the agent attempted exactly this invocation. +The stamp commits to `(server, tool, params)` _before_ the call runs, so a hung +or failing MCP server still leaves an on-record commitment that the agent +attempted exactly this invocation. ## Receiving the webhooks -When your console project has webhook endpoints subscribed, every -accepted envelope fires a signed POST. Use the drop-in verifier: +When your console project has webhook endpoints subscribed, every accepted +envelope fires a signed POST. Use the drop-in verifier: ```ts import { verify } from '@orangecheck/webhook-verify'; @@ -169,9 +177,9 @@ app.use(express.raw({ type: 'application/json' })); app.post('/webhooks/orangecheck', (req, res) => { const ok = verify({ - secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create + secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create signature: req.header('X-OrangeCheck-Signature') ?? '', - rawBody: req.body, + rawBody: req.body, }); if (!ok) return res.status(401).send('bad signature'); @@ -181,27 +189,26 @@ app.post('/webhooks/orangecheck', (req, res) => { }); ``` -See [Webhooks](/console/webhooks) for the full event catalog and the -delivery / retry semantics. +See [Webhooks](/console/webhooks) for the full event catalog and the delivery / +retry semantics. ## Direct API (no adapter) -If your stack isn't on the list above, install the shared client and -post envelopes directly: +If your stack isn't on the list above, install the shared client and post +envelopes directly: ```ts import { postActionToConsole } from '@orangecheck/agent-console-client'; await postActionToConsole(stampedAction, { - apiToken: process.env.OC_TOKEN!, + apiToken: process.env.OC_TOKEN!, projectId: process.env.OC_PROJECT_ID!, }); ``` -The client also exposes `postDelegationToConsole`, -`postRevocationToConsole`, and `postSubdelegationToConsole` if you -prefer to drive the registry yourself instead of going through the -dashboard. +The client also exposes `postDelegationToConsole`, `postRevocationToConsole`, +and `postSubdelegationToConsole` if you prefer to drive the registry yourself +instead of going through the dashboard. ## Authentication @@ -211,7 +218,7 @@ All adapter posts authenticate via Bearer token: Authorization: Bearer ock_<64-hex> ``` -Tokens are minted at `/settings § 03 · API tokens`. The plaintext is -returned **once** at create — store it in your CI's secrets manager -immediately. The token authenticates as the project owner; revoke at -any time from the same surface. +Tokens are minted at `/settings § 03 · API tokens`. The plaintext is returned +**once** at create — store it in your CI's secrets manager immediately. The +token authenticates as the project owner; revoke at any time from the same +surface. diff --git a/src/pages/console/quickstart.mdx b/src/pages/console/quickstart.mdx index 1e95a4c..9e5fcf3 100644 --- a/src/pages/console/quickstart.mdx +++ b/src/pages/console/quickstart.mdx @@ -1,33 +1,33 @@ export const metadata = { title: 'Console quickstart', - description: 'Sign in, bootstrap a project, register your first delegation, and see action receipts flow in. Five minutes.', + description: + 'Sign in, bootstrap a project, register your first delegation, and see action receipts flow in. Five minutes.', }; # Console quickstart -Five-minute path from never-heard-of-it to first stamped action receipt -landing in your dashboard. +Five-minute path from never-heard-of-it to first stamped action receipt landing +in your dashboard. ## 1. Sign in -Open [console.ochk.io](https://console.ochk.io) and click **sign in**. -The button routes to the family auth host at -[ochk.io/signin](https://ochk.io/signin) where you authenticate with a -BIP-322 signature from any compatible wallet (UniSat, Xverse, Leather, -OKX, Phantom, or paste-in). On success the auth host issues a -cross-subdomain `oc_session` cookie and bounces you back to the console -dashboard. +Open [console.ochk.io](https://console.ochk.io) and click **sign in**. The +button routes to the family auth host at +[ochk.io/signin](https://ochk.io/signin) where you authenticate with a BIP-322 +signature from any compatible wallet (UniSat, Xverse, Leather, OKX, Phantom, or +paste-in). On success the auth host issues a cross-subdomain `oc_session` cookie +and bounces you back to the console dashboard. -> **No password, no email**. Your Bitcoin address *is* your identity. -> The auth host stores nothing about you that isn't on-chain anyway. +> **No password, no email**. Your Bitcoin address _is_ your identity. The auth +> host stores nothing about you that isn't on-chain anyway. ## 2. Bootstrap your project (automatic) The first time you land on `/dashboard` after sign-in, the **OnboardingBoundary** auto-creates a default project named `-default`. You'll see a transient "first sign-in · -bootstrapping your default project" banner; once it clears you're on a -real Postgres-backed tenant. +bootstrapping your default project" banner; once it clears you're on a real +Postgres-backed tenant. You can rename the project at `/settings § 01` later. @@ -35,18 +35,18 @@ You can rename the project at `/settings § 01` later. Click **`/agents/new`** in the sidebar. The form asks for: -| Field | What | -|---|---| -| **agent address** | Bitcoin address the agent will sign actions with. Different from the principal — that's the point of delegation. | -| **scopes** | One or more scope strings, e.g. `mcp:invoke(server=https://mcp.example.com,tool=invoice.create)`. The agent can only sign actions whose `scope_exercised` is a sub-scope of one of these. | -| **expires_at** | When the delegation stops being valid. Default 30 days. | -| **bond_sats** | Optional OC Pledge stake. Skip for v1. | +| Field | What | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **agent address** | Bitcoin address the agent will sign actions with. Different from the principal — that's the point of delegation. | +| **scopes** | One or more scope strings, e.g. `mcp:invoke(server=https://mcp.example.com,tool=invoice.create)`. The agent can only sign actions whose `scope_exercised` is a sub-scope of one of these. | +| **expires_at** | When the delegation stops being valid. Default 30 days. | +| **bond_sats** | Optional OC Pledge stake. Skip for v1. | -Click **sign delegation in wallet**. Your wallet pops up showing the -64-hex envelope id; sign it; the envelope is POSTed to -`/api/delegations`. Console: +Click **sign delegation in wallet**. Your wallet pops up showing the 64-hex +envelope id; sign it; the envelope is POSTed to `/api/delegations`. Console: -- recomputes the id from canonical inputs and rejects on mismatch (tamper defense) +- recomputes the id from canonical inputs and rejects on mismatch (tamper + defense) - requires `principal === session.addr` (forgery defense) - persists the row, fans out to Nostr kind 30083, submits to OC Stamp - triggers any subscribed webhook with `event_type: 'delegation.registered'` @@ -55,21 +55,21 @@ The agent table on `/agents` now shows your new delegation. ## 4. Wire your LLM tool calls -Pick the adapter for your framework — install it, supply your agent -keypair + the delegation envelope + a console API token, and every tool -call gets a stamped receipt automatically. +Pick the adapter for your framework — install it, supply your agent keypair + +the delegation envelope + a console API token, and every tool call gets a +stamped receipt automatically. ```ts // Anthropic Tool Use import { invokeWithStampAndPost } from '@orangecheck/agent-anthropic'; const { result, action, posted } = await invokeWithStampAndPost({ - agent: agentSigner, // your @orangecheck/agent-signer ref + agent: agentSigner, // your @orangecheck/agent-signer ref delegation: signedDelegation, toolUse: claudeToolUseBlock, call: (toolUse) => myInvoiceCreateImpl(toolUse.input), console: { - apiToken: process.env.OC_TOKEN!, // ock_<64-hex> + apiToken: process.env.OC_TOKEN!, // ock_<64-hex> projectId: process.env.OC_PROJECT_ID!, }, }); @@ -83,10 +83,11 @@ The same `console: { apiToken, projectId }` knob works on [`@orangecheck/agent-openai`](https://www.npmjs.com/package/@orangecheck/agent-openai), [`@orangecheck/agent-vercel`](https://www.npmjs.com/package/@orangecheck/agent-vercel), [`@orangecheck/agent-langgraph`](https://www.npmjs.com/package/@orangecheck/agent-langgraph), -and [`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp). +and +[`@orangecheck/agent-mcp`](https://www.npmjs.com/package/@orangecheck/agent-mcp). -> Get the API token from `/settings § 03 · API tokens`. It's returned -> *once* at create — store it in your CI's secrets manager immediately. +> Get the API token from `/settings § 03 · API tokens`. It's returned _once_ at +> create — store it in your CI's secrets manager immediately. ## 5. Watch the receipts arrive @@ -95,17 +96,18 @@ Go to `/audit`. Every accepted action shows up as a row with: - the envelope id (sha256 of the canonical inputs) - the scope_exercised - the content_hash (sha256 of the canonicalized tool call) -- OTS state (`pending` until the OC Stamp pipeline anchors into a Bitcoin block, then `confirmed` with the block height) +- OTS state (`pending` until the OC Stamp pipeline anchors into a Bitcoin block, + then `confirmed` with the block height) - the Nostr event id (once relay-published) -Drill into `/agents/` to see receipts filtered to that single -delegation, or hit **export signed bundle** to download the full -project bundle as NDJSON / JSON / CSV. +Drill into `/agents/` to see receipts filtered to that single delegation, or +hit **export signed bundle** to download the full project bundle as NDJSON / +JSON / CSV. ## You're done -That's the whole loop. Real delegation envelope, real action envelopes, -real Bitcoin anchor — verifiable offline against +That's the whole loop. Real delegation envelope, real action envelopes, real +Bitcoin anchor — verifiable offline against [`@orangecheck/agent-core`](https://www.npmjs.com/package/@orangecheck/agent-core), no console-side trust required. @@ -113,5 +115,6 @@ Where to go next: - [Concepts](/console#five-second-mental-model) — the data model behind it. - [Webhooks](/console/webhooks) — push events to your stack. -- [Federation](/agent/spec) — replace the principal with an M-of-N guardian set (v1.2). +- [Federation](/agent/spec) — replace the principal with an M-of-N guardian set + (v1.2). - [API reference](/console/api) — full OpenAPI 3.1 spec. diff --git a/src/pages/console/webhooks.mdx b/src/pages/console/webhooks.mdx index 1e454b4..3e7a96e 100644 --- a/src/pages/console/webhooks.mdx +++ b/src/pages/console/webhooks.mdx @@ -1,62 +1,69 @@ +import { ConsoleWebhookEventCatalog } from '@/components/docs/ConsoleWebhookEventCatalog'; + export const metadata = { title: 'Console webhooks', - description: 'Receive HMAC-signed POST deliveries on every accepted delegation, action, revocation, and sub-delegation. Drop-in verifier, exponential-backoff retry, delivery log.', + description: + 'Receive HMAC-signed POST deliveries on every accepted delegation, action, revocation, and sub-delegation. Drop-in verifier, exponential-backoff retry, delivery log.', }; # Console webhooks -Subscribe an HTTPS endpoint at `/settings § 04 · webhook endpoints` and -console will POST a signed delivery on every event your endpoint -subscribes to. Same wire format as Stripe / GitHub / BTCPay — the +Subscribe an HTTPS endpoint at `/settings § 04 · webhook endpoints` and console +will POST a signed delivery on every event your endpoint subscribes to. Same +wire format as Stripe / GitHub / BTCPay — the [`@orangecheck/webhook-verify`](https://www.npmjs.com/package/@orangecheck/webhook-verify) package is a drop-in receiver for any Node / Edge / Workers runtime. ## Event catalog -| Event | Fires on | -|---|---| -| `delegation.registered` | `POST /api/delegations` accepted (single-address or federation v1.2) | -| `subdelegation.registered` | `POST /api/subdelegations` accepted (kind 30086, OC Agent v1.1) | -| `action.registered` | `POST /api/actions` accepted | -| `revocation.registered` | `POST /api/revocations` accepted (parent delegation flips to `status='revoked'`) | +The full set of subscribable events. Auto-generated at build time from +[`https://console.ochk.io/api/webhook-events`](https://console.ochk.io/api/webhook-events) +— the typed source of truth lives at `src/lib/webhooks/events.ts` in +[orangecheck/oc-console-web](https://github.com/orangecheck/oc-console-web), and +a CI contract test asserts every `dispatchEvent()` call uses an id from this +catalog. + + -Subscribe to any subset when you create the endpoint. New event types -will be added; the subscription field is open-string so customers don't -need a re-register on each addition (your endpoint just receives nothing -for events you didn't subscribe to). +Subscribe to any subset when you create the endpoint. New event types will be +added; the subscription field is open-string so customers don't need a +re-register on each addition (your endpoint just receives nothing for events you +didn't subscribe to). ## Payload shape ```json { - "id": "0123…64-hex", - "project_id": "proj_…", - "kind": "agent-action", - "envelope": { /* full canonical envelope JSON */ }, + "id": "0123…64-hex", + "project_id": "proj_…", + "kind": "agent-action", + "envelope": { + /* full canonical envelope JSON */ + }, "delegation_id": "0123…64-hex" } ``` `envelope` is the same byte-identical canonical envelope you'd get from -`/api/audit/export` or from a Nostr relay subscribed to the project's -kind. Verify offline with -[`@orangecheck/agent-core`](/sdks/javascript) — no console-side trust. +`/api/audit/export` or from a Nostr relay subscribed to the project's kind. +Verify offline with [`@orangecheck/agent-core`](/sdks/javascript) — no +console-side trust. ## Headers Every delivery includes: -| Header | Value | -|---|---| -| `Content-Type` | `application/json` | -| `X-OrangeCheck-Event` | `delegation.registered`, `action.registered`, etc | -| `X-OrangeCheck-Delivery` | opaque per-attempt id | +| Header | Value | +| ------------------------------- | ------------------------------------------------------------- | +| `Content-Type` | `application/json` | +| `X-OrangeCheck-Event` | `delegation.registered`, `action.registered`, etc | +| `X-OrangeCheck-Delivery` | opaque per-attempt id | | `X-OrangeCheck-Idempotency-Key` | stable per-event-fanout id (use this for receiver-side dedup) | -| `X-OrangeCheck-Payload-SHA256` | sha256 hex of the raw body bytes | -| `X-OrangeCheck-Signature` | `sha256=` | -| `X-OrangeCheck-Attempt` | (only on retries) attempt count | -| `X-OrangeCheck-Redelivery` | (only on retries) `"true"` | -| `X-OrangeCheck-Manual-Retry` | (only on operator-triggered retries) `"true"` | +| `X-OrangeCheck-Payload-SHA256` | sha256 hex of the raw body bytes | +| `X-OrangeCheck-Signature` | `sha256=` | +| `X-OrangeCheck-Attempt` | (only on retries) attempt count | +| `X-OrangeCheck-Redelivery` | (only on retries) `"true"` | +| `X-OrangeCheck-Manual-Retry` | (only on operator-triggered retries) `"true"` | ## Verifying the signature @@ -74,9 +81,9 @@ app.use(express.raw({ type: 'application/json' })); app.post('/webhooks/orangecheck', (req, res) => { const ok = verify({ - secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create + secret: process.env.OC_WEBHOOK_SECRET!, // shown ONCE at create signature: req.header('X-OrangeCheck-Signature') ?? '', - rawBody: req.body, // Buffer + rawBody: req.body, // Buffer }); if (!ok) return res.status(401).send('bad signature'); @@ -91,23 +98,23 @@ byte-by-byte probe the expected signature. ## The signing secret -When you create an endpoint, console returns the signing secret **once** -in the 201 response. Store it in your CI's secrets manager immediately; -we never echo it again. +When you create an endpoint, console returns the signing secret **once** in the +201 response. Store it in your CI's secrets manager immediately; we never echo +it again. -Internally, secrets are derived deterministically from a server-side -master key + the endpoint id (`HMAC-SHA256(WEBHOOK_MASTER_KEY, -"oc-webhook-v1:" + endpoint_id)`). The DB stores only `sha256(secret)` -for ad-hoc verification — a database leak alone is insufficient to -forge a delivery. +Internally, secrets are derived deterministically from a server-side master +key + the endpoint id +(`HMAC-SHA256(WEBHOOK_MASTER_KEY, "oc-webhook-v1:" + endpoint_id)`). The DB +stores only `sha256(secret)` for ad-hoc verification — a database leak alone is +insufficient to forge a delivery. -To rotate: delete the endpoint and re-register. The new secret is -emitted at create. +To rotate: delete the endpoint and re-register. The new secret is emitted at +create. ## Retry semantics -A delivery whose response is non-2xx (or times out after 8 seconds) is -queued for retry with exponential backoff: +A delivery whose response is non-2xx (or times out after 8 seconds) is queued +for retry with exponential backoff: ``` attempt 1 → 2: 1m @@ -119,15 +126,14 @@ attempt 6+: give up ``` After give-up the delivery row stays in the log with -`error_message: 'http_'` (or the connect-error text). You can -trigger a manual retry from the dashboard at any time via the **retry -now** button — that runs the same logic immediately. +`error_message: 'http_'` (or the connect-error text). You can trigger a +manual retry from the dashboard at any time via the **retry now** button — that +runs the same logic immediately. ## The delivery log -`/settings § 04` surfaces the last 25 deliveries inline under the -endpoints list, plus the row-level **retry now** button. Programmatic -access: +`/settings § 04` surfaces the last 25 deliveries inline under the endpoints +list, plus the row-level **retry now** button. Programmatic access: ``` GET /api/webhooks/deliveries?project_id=proj_…[&endpoint_id=…&limit=N] @@ -139,9 +145,9 @@ succeeded_at, and the per-event-fanout idempotency key. ## Idempotency on your receiver Use `X-OrangeCheck-Idempotency-Key` as the dedup key. The same logical -event-fanout (e.g. one accepted action triggers two subscribed endpoints -each retrying once) shares the idempotency key across attempts to the -same endpoint, but differs across endpoints. So your receiver should: +event-fanout (e.g. one accepted action triggers two subscribed endpoints each +retrying once) shares the idempotency key across attempts to the same endpoint, +but differs across endpoints. So your receiver should: ```ts if (await alreadyProcessed(req.header('X-OrangeCheck-Idempotency-Key'))) { @@ -149,5 +155,5 @@ if (await alreadyProcessed(req.header('X-OrangeCheck-Idempotency-Key'))) { } ``` -A 200 with no body is the canonical "I got it, don't retry" response. -Any non-2xx (or no response within 8 seconds) triggers retry. +A 200 with no body is the canonical "I got it, don't retry" response. Any +non-2xx (or no response within 8 seconds) triggers retry. From ca07a9f835844356c33be6907f0f43e5f743fc59 Mon Sep 17 00:00:00 2001 From: Xaxis Date: Thu, 30 Apr 2026 14:39:20 -0700 Subject: [PATCH 4/6] docs(me): add /me product section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the /console product section. Seven pages organized like the canonical OC product structure: - /me — what me.ochk.io is and isn't, where it fits in the family - /me/quickstart — five-minute integration walkthrough - /me/sdk — @orangecheck/me-client v0.4.0 reference - /me/integrations — OAuth-peer pattern, sample integrator archetypes, cross-product flows with console - /me/webhooks — reception in Node + Rust, raw-body warning, retry semantics - /me/api — every me.ochk.io HTTP endpoint with auth + rate-limit metadata - /me/custody — federation custody descriptor schema, M-of-N graduation envelope, guardian rotation. Maps to the v1.2-draft-1 oc-attest extension at FEDERATION-CUSTODY.md. DocsNav surfaces /me as a top-level product section between OC Console and SDKs. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/docs/nav.ts | 42 ++++++++ src/pages/me/api.mdx | 103 +++++++++++++++++++ src/pages/me/custody.mdx | 121 ++++++++++++++++++++++ src/pages/me/index.mdx | 61 +++++++++++ src/pages/me/integrations.mdx | 110 ++++++++++++++++++++ src/pages/me/quickstart.mdx | 149 +++++++++++++++++++++++++++ src/pages/me/sdk.mdx | 186 ++++++++++++++++++++++++++++++++++ src/pages/me/webhooks.mdx | 136 +++++++++++++++++++++++++ 8 files changed, 908 insertions(+) create mode 100644 src/pages/me/api.mdx create mode 100644 src/pages/me/custody.mdx create mode 100644 src/pages/me/index.mdx create mode 100644 src/pages/me/integrations.mdx create mode 100644 src/pages/me/quickstart.mdx create mode 100644 src/pages/me/sdk.mdx create mode 100644 src/pages/me/webhooks.mdx diff --git a/src/components/docs/nav.ts b/src/components/docs/nav.ts index a4f8852..8467713 100644 --- a/src/components/docs/nav.ts +++ b/src/components/docs/nav.ts @@ -455,6 +455,48 @@ export const DOCS_NAV: DocsSection[] = [ }, ], }, + { + slug: 'me', + label: 'OC Me', + blurb: 'Consumer commercial product · bitcoin-backed identity that pays users in sats.', + items: [ + { + href: '/me', + label: 'Overview', + blurb: "What me.ochk.io is, what it isn't, and where it fits.", + }, + { + href: '/me/quickstart', + label: 'Quickstart', + blurb: 'Five minutes from npm install to first envelope.', + }, + { + href: '/me/sdk', + label: 'SDK reference', + blurb: '@orangecheck/me-client v0.4.0 — every export with type signatures.', + }, + { + href: '/me/integrations', + label: 'Integrations', + blurb: 'OAuth-peer pattern, sample integrator archetypes, cross-product flows.', + }, + { + href: '/me/webhooks', + label: 'Webhooks', + blurb: 'Reception in Node + Rust, raw-body warning, retry semantics.', + }, + { + href: '/me/api', + label: 'HTTP API', + blurb: 'Every me.ochk.io endpoint · auth, rate limits, response shapes.', + }, + { + href: '/me/custody', + label: 'Federation custody', + blurb: 'Federation descriptor, M-of-N graduation envelope, guardian rotation.', + }, + ], + }, { slug: 'sdks', label: 'SDKs', diff --git a/src/pages/me/api.mdx b/src/pages/me/api.mdx new file mode 100644 index 0000000..91b39af --- /dev/null +++ b/src/pages/me/api.mdx @@ -0,0 +1,103 @@ +export const metadata = { + title: 'OC Me · HTTP API', + description: + 'Every me.ochk.io HTTP endpoint · authentication, rate limits, response shapes, error codes.', +}; + +# OC Me · HTTP API + +**The hosted me.ochk.io substrate exposes the contracts the SDK calls +into.** The SDK is the canonical integration path; this page is the raw +HTTP surface for clients that aren't TypeScript or that want to bypass +the SDK. + +Every endpoint is at `https://me.ochk.io/api/...`. Auth is by +cross-subdomain `oc_session` cookie (`Domain=.ochk.io`, HttpOnly, +SameSite=Lax). Mutating endpoints are rate-limited per IP per bucket; +limits are returned in `X-RateLimit-*` headers. + +## Public endpoints + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/api/health` | liveness probe | +| `GET` | `/api/health/deep` | dependency probe | +| `GET` | `/api/envelope/[id]` | canonical envelope JSON for a billable event | +| `GET` | `/api/delegation/[id]` | canonical envelope for a delegation | +| `GET` | `/api/payment/[id]` | canonical envelope for a payment | +| `GET` | `/api/checkout/lightning/[hash]` | invoice status by `payment_hash` | +| `GET` | `/api/platform-fee-policy` | published `PLATFORM_FEE_POLICY` | +| `GET` | `/api/abuse-limits` | published per-class abuse rate limits | +| `GET` | `/api/integrator-config?project_key=…` | sample `IntegratorPriceConfig` lookup | +| `GET` | `/api/stats/earned` | aggregate sat-earnings (last 7d, by class) | +| `POST` | `/api/integrator-config` | validate an `IntegratorPriceConfig` (echoes on success) | +| `POST` | `/api/checkout/lightning` | generate a BOLT-11 invoice (rate-limited 60/min) | +| `POST` | `/api/checkout/stripe` | generate a Stripe checkout URL | +| `POST` | `/api/contact` | contact form submission (Resend-backed) | + +## Authenticated endpoints (oc_session required) + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/api/auth/me` | crypto-only identity lookup; 401 otherwise | +| `GET` | `/api/me/profile` | profile + onboarding state | +| `GET` | `/api/me/balance` | sat balance + last-7d aggregate + by-class breakdown | +| `GET` | `/api/me/sites` | list of sites the identity has signed in to | +| `GET` | `/api/me/sites/[domain]` | per-site activity + IntegratorPriceConfig | +| `GET` | `/api/me/notifications` | notifications feed | +| `POST` | `/api/me/notifications` | mark read/unread (`{ id, read }` or `{ all: true, read: true }`) | +| `GET` | `/api/me/export` | GDPR-style account dump · downloadable JSON | + +## Authenticated · session lifecycle (Class C) + +| Method | Path | Purpose | +| --- | --- | --- | +| `POST` | `/api/session/create` | open a Class C session — billable atom | +| `POST` | `/api/session/refresh` | refresh inside the policy window — free | +| `POST` | `/api/session/invalidate` | tear down — free, telemetry-only | + +## Authenticated · payment authorization (Class B) + +| Method | Path | Purpose | +| --- | --- | --- | +| `POST` | `/api/payment/authorize` | authorize a payment, rate-limited 30/min · Class B billable | + +## Authenticated · agent delegation (Class A) + +| Method | Path | Purpose | +| --- | --- | --- | +| `POST` | `/api/delegation/issue` | issue a delegation, rate-limited 20/min · Class A billable | +| `GET` | `/api/delegation/list` | list delegations | +| `POST` | `/api/delegation/revoke` | revoke a delegation, instant | + +## Authenticated · integrator project lifecycle + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/api/developer/projects` | list every project owned by your identity | +| `POST` | `/api/developer/projects` | create a project, rate-limited 5/min | +| `GET` | `/api/developer/projects/[id]` | per-project state · owner-gated | +| `POST` | `/api/developer/projects/[id]` | update IntegratorPriceConfig · owner-gated, validates | +| `DELETE` | `/api/developer/projects/[id]` | delete project · owner-gated | +| `GET` | `/api/developer/config` | sample integrator config (the demo dashboard) | +| `POST` | `/api/developer/config` | validate + persist sample config | +| `GET` | `/api/developer/keys` | list project signing keys (sample) | +| `POST` | `/api/developer/webhook-test` | test-fire a synthetic envelope at a webhook | + +## Response shapes + +All authenticated 401s are `{ ok: false }` with `Cache-Control: no-store, private` and `Vary: Cookie`. All 422 validation failures return `{ error: string, details?: [{ subtype?: string, message: string }] }`. All 429 rate-limit responses include a `Retry-After` header in seconds. + +## Rate limits per bucket + +| Bucket | Limit | Window | +| --- | --- | --- | +| `payment.authorize` | 30 | 60s | +| `delegation.issue` | 20 | 60s | +| `checkout.lightning` | 60 | 60s | +| `developer.projects.create` | 5 | 60s | + +Limits are per IP, per bucket. Headers `X-RateLimit-Limit`, +`X-RateLimit-Remaining`, `X-RateLimit-Window` are emitted on every +rate-limited response. v1 buckets reset on cold start (per-instance +in-memory); production fronts every endpoint with an edge-level limiter. diff --git a/src/pages/me/custody.mdx b/src/pages/me/custody.mdx new file mode 100644 index 0000000..0a88143 --- /dev/null +++ b/src/pages/me/custody.mdx @@ -0,0 +1,121 @@ +export const metadata = { + title: 'OC Me · Federation custody', + description: + 'Federation custody descriptor schema, M-of-N graduation envelopes, guardian rotation. Maps the v1.2-draft-1 oc-attest extension to the me.ochk.io product surface.', +}; + +# OC Me · Federation custody + +**me.ochk.io is federation-custodied by default, self-custody when +ready.** This page is the user-facing explanation; the protocol-level +contract is at +[`oc-attest-protocol/FEDERATION-CUSTODY.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/FEDERATION-CUSTODY.md) +(v1.2-draft-1, additive extension). + +## What "federation-custodied" means + +Your me.ochk.io identity is bound to a Bitcoin address, but the private +key that controls it is held collectively by an M-of-N guardian set — +no single guardian (and not OC the company) can spend or freeze. Recovery +is by re-proving your email or phone, not by remembering twelve words. + +When you sign in, the federation guardians threshold-sign on your behalf. +You don't see this. You never need to. + +## The three options at threshold + +| | signing_method | how it works | +| --- | --- | --- | +| **federation custody** (default) | `fedimint_threshold` | guardians collectively hold the key. recovery by re-proving email/phone. | +| **fedimint client** | `fedimint_threshold` (different federation) | sweep to a fedimint federation you trust more (your community's, your country's, your own). | +| **self-custody** | `bip-322` | sweep to any BIP-322-capable Bitcoin wallet (UniSat, Xverse, Leather, Alby, Sparrow, hardware wallet). only you can sign. | + +You can return to federation custody at any point — the protocol +accommodates the round-trip via attestation re-issuance under the new +custody descriptor. + +## Graduation envelope + +Per the v1.2-draft-1 oc-attest extension, graduation publishes an +envelope carrying both an M-of-N guardian quorum sig (federation +releases custody) and a self-key sig (you prove you hold the key now). +The envelope is anchored to a bitcoin block via OpenTimestamps so any +verifier can confirm graduation happened at a specific time. + +```json +{ + "v": 1, + "kind": "orangecheck-graduation", + "address": "bc1q...", + "from_federation": "fed:abc123…", + "to": "single-key", + "graduated_at": "2026-04-30T20:00:00Z", + "proof": [ + { "address": "bc1qg1…", "sig": "..." }, + { "address": "bc1qg2…", "sig": "..." }, + { "address": "bc1qg3…", "sig": "..." }, + { "address": "bc1q...", "sig": "...", "kind": "self-key" } + ] +} +``` + +| Field | Rule | +| --- | --- | +| `from_federation` | Content hash of the federation custody descriptor releasing the address. | +| `to` | `"single-key"` (graduating to BIP-322) or `"federation"` (re-binding to a new descriptor). | +| `proof` | M signatures from M of N declared guardians PLUS one self-key signature. | +| `graduated_at` | ISO-8601 timestamp; the OTS proof anchors this to a Bitcoin block. | + +## Guardian descriptor + +A federation custody descriptor is a content-addressed canonical JSON +object — same shape as oc-agent-protocol's federation principal, +re-purposed for custody: + +```json +{ + "v": 1, + "kind": "attest-federation-custody", + "address": "bc1q…", + "threshold": "3-of-5", + "guardians": [ + { "address": "bc1qg1…", "alg": "bip322", "name": "alice" }, + { "address": "bc1qg2…", "alg": "bip322", "name": "bob" }, + { "address": "bc1qg3…", "alg": "bip322", "name": "carol" }, + { "address": "bc1qg4…", "alg": "bip322", "name": "dave" }, + { "address": "bc1qg5…", "alg": "bip322", "name": "erin" } + ], + "implementation": { + "kind": "fedimint", + "federation_id": "fed11qgqzcq…", + "version": "0.4" + } +} +``` + +The descriptor id is `fed:` + lowercase hex `SHA-256(canonical_descriptor_bytes)`. + +## What stays the same when you graduate + +| Stays the same | Changes | +| --- | --- | +| Your bitcoin address (the canonical identifier) | The signing method (`fedimint_threshold` → `bip-322`) | +| Your attest tier (`anonymous` / `bonded` / `kyc-light` / `kyc-strong`) | Who holds the private key (you, instead of the federation) | +| Your /me/earn history, connected sites, agent delegations | How recovery works (you own backup; lose the key, lose the funds) | +| Your sat-earning rate | The shape of `signing_method` in attestation envelopes | + +## Guardian rotation + +Guardian rotation produces a **new descriptor with a new id**. Old +attestations remain verifiable against the old descriptor; new +attestations bind to the new descriptor. Rotation envelopes are +authenticated by M-of-N signatures from the **old** descriptor and +OTS-anchored so any verifier can resolve the active descriptor at any +historical instant. + +## Reading further + +- [`oc-attest-protocol/FEDERATION-CUSTODY.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/FEDERATION-CUSTODY.md) — full v1.2-draft-1 spec +- [`oc-agent-protocol/FEDERATION.md`](https://github.com/orangecheck/oc-agent-protocol/blob/main/FEDERATION.md) — sibling federation principal extension for delegations +- [me.ochk.io/custody](https://me.ochk.io/custody) — product expression of this contract +- [me.ochk.io/me/graduate](https://me.ochk.io/me/graduate) — the live graduation flow diff --git a/src/pages/me/index.mdx b/src/pages/me/index.mdx new file mode 100644 index 0000000..a9dd31e --- /dev/null +++ b/src/pages/me/index.mdx @@ -0,0 +1,61 @@ +export const metadata = { + title: 'OC Me · me.ochk.io', + description: + 'Consumer commercial product of OrangeCheck. Bitcoin-backed online identity that pays users in sats automatically when they sign into integrating sites. Federation-custodied by default; self-custody graduation on demand.', +}; + +# OC Me + +**The bitcoin-backed online identity that pays you to use it.** +[me.ochk.io](https://me.ochk.io) is the consumer surface of OrangeCheck. +End-users sign up once with email, phone, or an existing OC identity from a +sibling site, and earn sats automatically as integrating sites bill the +canonical billable-event taxonomy (sessions, payments, state transitions). +Federation-custodied by default; self-custody is a one-click graduation flow +on `/me/graduate`. + +The protocol surface is the same OC Attest + OC Lock + OC Stamp + OC Agent +primitives documented elsewhere in these docs. me.ochk.io is the commercial +layer on top: same envelopes, same canonical messages, same offline +verification — packaged as a hosted product so consumers and integrators get +a working stack without standing up their own federation. + +## What me.ochk.io is, and what it isn't + +| | | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Is** | A **consumer identity layer**. Sign in once at any integrating site; sat-earning streams in to your federation-custodied wallet automatically. | +| **Is** | A **drop-in OAuth peer**. Integrators add the OC button alongside Google / Apple / magic-link. First-time users get a federation-custodied wallet provisioned silently — no seed phrase. | +| **Is** | An **integrator-configurable pricing substrate**. Each integrator declares per-event prices and user-share splits via `IntegratorPriceConfig`. OC takes a fixed 20% platform fee. | +| **Is** | A **graduation path**. Users can sweep to self-custody (`bip-322`) or to a fedimint federation of their choice without losing their oc identity, history, attest tier, or connected-sites list. | +| **Isn't** | A _new protocol_. Every envelope me.ochk.io produces is identical to one a user could publish themselves via the canonical OC Attest / Stamp / Agent primitives. | +| **Isn't** | A _custodian_. Federation guardians collectively hold the threshold key; OC the company is one guardian among several. Self-custody is always one envelope away. | +| **Isn't** | A _token issuer_. Sats are the unit of account. No me.ochk.io token, no governance token, no airdrop. The platform fee is in sats, settled in sats, displayed in USD alongside for human readability.| + +## Quickstart paths + +| For… | Start here | +| --- | --- | +| **End users** | Sign up at [me.ochk.io/signin](https://me.ochk.io/signin). Pick email-or-phone path; the rest is automatic. | +| **Integrators (developers)** | The 5-minute [integrate quickstart](https://me.ochk.io/integrate/quickstart) walks install → drop-in button → configure prices → verify webhook → ship. | +| **SDK reference** | [`@orangecheck/me-client`](/sdks/javascript) v0.4.0 — useOcSession, oc.session, oc.payment, oc.config, oc.webhook, oc.delegation. | +| **Protocol grounding** | [Attest](/attest/), [Lock](/lock/), [Stamp](/stamp/), [Agent](/agent/) — the four protocol verbs me.ochk.io is built on. | +| **Self-custody graduation** | [me.ochk.io/me/graduate](https://me.ochk.io/me/graduate) — three options at threshold, one envelope each. | + +## Sub-pages in this section + +- [Quickstart](/me/quickstart) — install → drop-in → configure → verify → ship +- [SDK reference](/me/sdk) — every export from `@orangecheck/me-client` +- [Integrations](/me/integrations) — OAuth-peer pattern, sample configs, webhook reception +- [Webhooks](/me/webhooks) — signature verification, raw-body warning, retry semantics +- [API reference](/me/api) — every `me.ochk.io/api/*` endpoint, owner-gating, rate limits +- [Federation custody](/me/custody) — descriptor schema, graduation envelope, guardian rotation + +## Where the source of truth lives + +- **Protocol contracts**: `oc-attest-protocol`, `oc-lock-protocol`, `oc-stamp-protocol`, `oc-agent-protocol`. The v1.2-draft-1 federation custody extension is at `oc-attest-protocol/FEDERATION-CUSTODY.md`. +- **Web app**: `oc-me-web` — the me.ochk.io marketing + authenticated app surface. +- **SDK**: `oc-packages/me-client` — published to npm at `@orangecheck/me-client`. +- **Docs**: this section, in `oc-docs`. Cross-referenced from /sdk and /developer/docs on me.ochk.io. + +The contract is the protocol; me.ochk.io is one implementation. Anyone can run their own me-equivalent against the same envelopes — that's the product strategy. diff --git a/src/pages/me/integrations.mdx b/src/pages/me/integrations.mdx new file mode 100644 index 0000000..cd23663 --- /dev/null +++ b/src/pages/me/integrations.mdx @@ -0,0 +1,110 @@ +export const metadata = { + title: 'OC Me · Integrations', + description: + 'OAuth-peer pattern · sample IntegratorPriceConfig archetypes · zero-bitcoin-knowledge user flow · cross-product flows with console.ochk.io.', +}; + +# OC Me · Integrations + +**OC sits as a peer next to your existing OAuth providers.** A first-time +user picking OC has a federation-custodied bitcoin wallet provisioned +silently — no seed phrase, no key handling, no "what is a sat" +conversation. Sat-earning starts on first session. + +## OAuth-peer pattern + +```tsx +import { OcSignInButton } from '@orangecheck/me-client'; +import { signIn } from 'next-auth/react'; + +export function SignInOptions() { + return ( +
+ + + + + sign in with oc · earn sats + +
+ ); +} +``` + +| What your user sees | What your user does NOT see | +| --- | --- | +| a button next to google / apple | a seed phrase | +| one redirect to me.ochk.io | a wallet creation flow | +| signed in on your site | a "buy bitcoin" CTA | +| (eventually) a growing sat balance | any crypto vocabulary at all | + +When do they find out? When they want to. +[me.ochk.io/me](https://me.ochk.io/me) shows their balance, +[me.ochk.io/me/wallet](https://me.ochk.io/me/wallet) shows +receive/send/withdraw, +[me.ochk.io/me/graduate](https://me.ochk.io/me/graduate) offers +self-custody. None of this is in the OAuth path. None of it surfaces +unless the user explicitly visits. + +## Sample integrator archetypes + +The configurator at [me.ochk.io/integrate](https://me.ochk.io/integrate) +ships with six representative sample configurations. Different unit +economics produce different choices — there's no "right" config OC sets, +just the math. + +| Archetype | Domain | Account creation | Session | Payment | User share | +| --- | --- | --- | --- | --- | --- | +| **Lightning fintech** | breez.example | 2,100 sats | 80 sats | 0.50% | 70% | +| **Cashback-heavy** | fold.example | 1,600 sats | 60 sats | 0.95% | 78% | +| **Lightning-native social** | damus.example | — | 40 sats | — | 50% | +| **Merchant SaaS · KYC-required** | zaprite.example | 5,300 sats | 75 sats | 0.85% | 55–65% | +| **Community federation** | fedi.example | 850 sats | 50 sats | — | 78% | +| **Creator platform** | creator.example | 1,100 sats | 35 sats | 0.65% | 60–70% | + +The config schema is open and self-served — see +[/me/sdk](/me/sdk#oc-config--integratorpriceconfig-crud) for the full +shape and [/developer/config](https://me.ochk.io/developer/config) for +the inline editor. + +## Cross-product flows · me + console + +A consumer using me.ochk.io may sign into a SaaS that runs on +console.ochk.io for managed agent infrastructure. The envelopes are the +same primitive (oc-attest, oc-stamp, oc-agent) — the two products are +two integration surfaces over identical contracts. + +| Flow | Surface | +| --- | --- | +| User signs into a SaaS using OC button | me.ochk.io provisions wallet · console.ochk.io receives session | +| User authorizes an LLM agent inside the SaaS | console.ochk.io issues delegation · me.ochk.io shows it on /me/agents | +| Agent takes a stamped action | console.ochk.io ingests · me.ochk.io credits sats per the SaaS's IntegratorPriceConfig | +| User revokes the delegation | me.ochk.io publishes oc-agent-rev:[id] · console.ochk.io drops further envelopes | + +See [console.ochk.io integrations docs](/console/integrations) for the +operator-side view. + +## Errors and edge cases + +| Error | Meaning | Fix | +| --- | --- | --- | +| `IntegratorPriceConfig invalid` (422) | Validation failed server-side. | Run `validateIntegratorConfig(cfg)` locally first; the response includes `details: [{ subtype, message }]`. | +| `signature does not match` from `oc.webhook.verify` | Either 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. | +| `delegation not found` (404) | The id is unknown to OC's index. | Check the id; the SDK returned envelope is the source of truth. | + +## Self-host considerations + +me.ochk.io is one implementation of the canonical OC primitives. Anyone +can run their own me-equivalent against the same envelope 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 me.ochk.io substrate is a convenience, not a moat. Charter +commitment 04 keeps every reference site free and self-hostable forever. diff --git a/src/pages/me/quickstart.mdx b/src/pages/me/quickstart.mdx new file mode 100644 index 0000000..7220573 --- /dev/null +++ b/src/pages/me/quickstart.mdx @@ -0,0 +1,149 @@ +export const metadata = { + title: 'OC Me · Quickstart', + description: + 'Five-minute integration walkthrough for me.ochk.io. Install SDK, mount provider, drop the sign-in button, configure prices, verify webhooks, ship.', +}; + +# OC Me · Quickstart + +**Five minutes from `npm install` to first envelope.** Every step has the +exact code you ship. Mark a step done when you've actually shipped it. + +## 01 · install the SDK + +Two npm packages. `me-client` is the consumer SDK; `auth-client` is the +React provider + hook (re-exported by `me-client` for convenience). + +```bash +yarn add @orangecheck/me-client @orangecheck/auth-client +# or +npm i @orangecheck/me-client @orangecheck/auth-client +``` + +## 02 · mount the provider + +Wrap your app once at the root. Defaults pin `authOrigin` to +`https://ochk.io` — only override in dev/staging. + +```tsx +// _app.tsx (Next.js Pages router) +import { OcSessionProvider } from '@orangecheck/me-client'; + +export default function App({ Component, pageProps }) { + return ( + + + + ); +} +``` + +Every component below it can call `useOcSession()` to read the live +cross-subdomain session. + +## 03 · drop the sign-in button + +OC is an OAuth peer. Render it next to your existing Google / Apple / +magic-link options. First-time users get a federation-custodied bitcoin +wallet provisioned silently — no seed phrase, no key handling, no +"what is a sat" conversation. + +```tsx +import { OcSignInButton } from '@orangecheck/me-client'; +import { signIn } from 'next-auth/react'; + +export function SignInOptions() { + return ( +
+ + + + + sign in with oc · earn sats + +
+ ); +} +``` + +When a first-time user picks OC: they bounce to me.ochk.io/signin → +federation provisions their wallet → they're signed back in on your site +with an `oc_session` cookie → every billable event you emit credits sats +to their balance. They never have to know about bitcoin until they want +to graduate. + +## 04 · configure your prices + +Declare per-event prices and user-share splits. You choose the subtypes +to bill, the price per event (sats or % of underlying amount), and how +much flows back to the user. OC takes a fixed 20% platform fee — the +rest is yours and your users'. + +```ts +import { oc } from '@orangecheck/me-client'; + +await oc.config.update({ + project_key: 'pk_live_yourcompany', + display_name: 'YourCompany', + domain: 'yourcompany.example', + events: { + session_creation: { + enabled: true, + site_pays: { kind: 'fixed_sats', sats: 60 }, + user_share_pct: 0.65, + }, + payment_authorization: { + enabled: true, + site_pays: { kind: 'percent_of_amount', pct: 0.0075 }, + user_share_pct: 0.65, + }, + }, +}); +``` + +The configurator at [me.ochk.io/integrate](https://me.ochk.io/integrate) +is interactive — adjust prices and see the four-way split (gross · OC fee +· user · your rebate) update live. + +## 05 · verify a webhook + +OC delivers every billable envelope to your registered endpoint, signed +with the federation Ed25519 key. **Always verify against the raw body** — +frameworks that re-serialize before your handler will produce a different +byte sequence and the signature will not validate. + +```ts +import express from 'express'; +import { oc } from '@orangecheck/me-client'; + +const app = express(); +app.use(express.text({ type: 'application/json' })); + +app.post('/api/oc/webhook', async (req, res) => { + const result = await oc.webhook.verify( + req.body, + req.header('OC-Signature'), + req.header('OC-Key-Id') + ); + if (!result.ok) return res.status(401).end(result.reason); + + const envelope = JSON.parse(req.body); + await onOcEvent(envelope); + res.status(200).end(); +}); +``` + +## 06 · ship + +Request your project_key at +[me.ochk.io/contact?topic=integration](https://me.ochk.io/contact?topic=integration). +Mount the SDK, watch envelopes land on +[me.ochk.io/developer/events](https://me.ochk.io/developer/events). Paired +comparisons against your actual stack come back in a business day. + +## Next, deeper + +- [SDK reference](/me/sdk) — every export with type signatures +- [Integrations guide](/me/integrations) — OAuth-peer pattern, sample configs, error handling +- [Webhooks](/me/webhooks) — full reception sample (Node + Rust), delivery semantics +- [API reference](/me/api) — every `/api/*` endpoint diff --git a/src/pages/me/sdk.mdx b/src/pages/me/sdk.mdx new file mode 100644 index 0000000..7181edd --- /dev/null +++ b/src/pages/me/sdk.mdx @@ -0,0 +1,186 @@ +export const metadata = { + title: 'OC Me · SDK reference', + description: + '@orangecheck/me-client v0.4.0 SDK reference. useOcSession, oc.session, oc.payment, oc.config, oc.webhook, oc.delegation, plus every type the SDK exports.', +}; + +# `@orangecheck/me-client` · SDK reference + +**v0.4.0 · MIT · 11 KB ESM · 7 KB DTS.** + +This is the canonical TypeScript SDK for integrating me.ochk.io. The same +reference is mirrored at [me.ochk.io/sdk](https://me.ochk.io/sdk) — this +page is the docs.ochk.io copy organized alongside the other product +references. + +## install + +```bash +yarn add @orangecheck/me-client @orangecheck/auth-client +``` + +`@orangecheck/auth-client` is the React provider + hook. `me-client` +re-exports it, so most code only needs to import from `me-client`. + +## React surface + +### `` + +Wrap your app once at the root. Default `authOrigin` is `https://ochk.io`. + +```tsx +import { OcSessionProvider } from '@orangecheck/me-client'; + +{/* … */}; +``` + +### `useOcSession()` and `useOptionalOcSession()` + +Returns `{ status, account, signInUrl, signOut, refresh, error }`. +Status is `loading | authenticated | anonymous | error`. Account is +`null` when not authenticated. + +```tsx +import { useOcSession } from '@orangecheck/me-client'; + +function Header() { + const { status, account, signInUrl, signOut } = useOcSession(); + if (status === 'loading') return ; + if (status !== 'authenticated') return sign in; + return ( +
+ signed in as {account.address.slice(0, 8)}… + +
+ ); +} +``` + +`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. + +```ts +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` | Class C billable. | +| `oc.session.refresh(id)` | `(string) => Promise` | Free. | +| `oc.session.invalidate(id)` | `(string) => Promise` | Free. | + +## `oc.payment.authorize` · Class B billable atom + +```ts +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.*` · IntegratorPriceConfig CRUD + +```ts +const cfg = await oc.config.get(); +await oc.config.update({ + ...cfg, + events: { + ...cfg.events, + payment_authorization: { + enabled: true, + site_pays: { kind: 'percent_of_amount', pct: 0.0085 }, + user_share_pct: 0.65, + }, + }, +}); + +// Validate without a round-trip +import { validateIntegratorConfig } from '@orangecheck/me-client'; +const result = validateIntegratorConfig(cfg); +if (!result.ok) console.error(result.errors); +``` + +| Function | Signature | Note | +| --- | --- | --- | +| `oc.config.get()` | `() => Promise` | | +| `oc.config.update(cfg)` | `(IntegratorPriceConfig) => Promise` | Throws on local invalidity; 422 on server rejection. | +| `oc.config.validate(cfg)` | `(IntegratorPriceConfig) => ValidationResult` | No network call. | + +## `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. + +```ts +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. + +## `oc.delegation.*` · agent authority (Class A billable atom) + +```ts +const env = await oc.delegation.issue({ + agent_id: 'agent-pubkey-or-did', + scope: ['inbox.read', 'attest.verify'], + expires_at: new Date(Date.now() + 7 * 86400_000).toISOString(), +}); +// later +await oc.delegation.revoke(env.id); +``` + +Max TTL 30 days. Revocation is instant — the verifier rejects any +envelope signed after revocation regardless of the agent's state. + +## Types + +Every type the SDK exports: + +``` +EventClass · EventSubtype · ClassASubtype · ClassBSubtype · ClassCSubtype +AttestTier · SiteFeeShape · IntegratorEventConfig · IntegratorPriceConfig +ComputedFees · ValidationResult · BillableEvent · Session · SessionPolicy +SignInOptions · PaymentAuthorizeOptions · PaymentResult · TelemetryEvent +OcAccount · OcSessionState · OcSessionStatus · OcAuthConfig +OcPublicJwk · VerifyResult +DelegationScope · DelegationEnvelope · IssueDelegationOptions +``` + +## 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` | + +## Where it lives + +- **npm**: [@orangecheck/me-client](https://www.npmjs.com/package/@orangecheck/me-client) +- **source**: [oc-packages/me-client](https://github.com/orangecheck/oc-packages/tree/main/me-client) +- **canonical type module**: `@orangecheck/me-client/types` (mirror of `oc-me-web/src/lib/events/types.ts`) diff --git a/src/pages/me/webhooks.mdx b/src/pages/me/webhooks.mdx new file mode 100644 index 0000000..6acf874 --- /dev/null +++ b/src/pages/me/webhooks.mdx @@ -0,0 +1,136 @@ +export const metadata = { + title: 'OC Me · Webhooks', + description: + 'Webhook signature verification, raw-body warning, retry semantics, headers, and full reception samples in Node + Rust.', +}; + +# OC Me · Webhooks + +**Every billable envelope your project signs is delivered to your +registered endpoints, signed with OC's federation Ed25519 key.** Verify +the signature against the **raw** request body — frameworks that +re-serialize before your handler will produce a different byte sequence +and the signature will not validate. + +## Reception · Node + Express + +```ts +import express from 'express'; +import { oc } from '@orangecheck/me-client'; + +const app = express(); +app.use(express.text({ type: 'application/json' })); // raw body! + +app.post('/api/oc/webhook', async (req, res) => { + const result = await oc.webhook.verify( + req.body, + req.header('OC-Signature'), + req.header('OC-Key-Id') + ); + if (!result.ok) return res.status(401).end(result.reason); + + const envelope = JSON.parse(req.body); + // envelope.kind === 'oc-billable-event' + // envelope.subtype === 'session_creation' | 'payment_authorization' | … + // envelope.id is content-addressed — idempotent + await onOcEvent(envelope); + res.status(200).end(); +}); +``` + +## Reception · Rust + Axum + +```rust +use axum::{extract::State, http::StatusCode, response::IntoResponse}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; + +async fn webhook( + State(pub_key): State, + headers: axum::http::HeaderMap, + body: bytes::Bytes, +) -> impl IntoResponse { + let sig_hex = headers.get("OC-Signature").and_then(|v| v.to_str().ok()).unwrap_or(""); + let sig_bytes = hex::decode(sig_hex).unwrap_or_default(); + let Ok(sig) = Signature::from_slice(&sig_bytes) else { + return StatusCode::UNAUTHORIZED; + }; + if pub_key.verify(&body, &sig).is_err() { + return StatusCode::UNAUTHORIZED; + } + let envelope: serde_json::Value = serde_json::from_slice(&body).unwrap(); + process_event(envelope).await; + StatusCode::OK +} +``` + +## Headers OC sends + +| Header | Meaning | +| --- | --- | +| `OC-Signature` | Ed25519 signature, hex-encoded, computed over the raw body | +| `OC-Key-Id` | `kid` of the OC public JWK that signed the event | +| `OC-Envelope-Id` | Idempotency key — same envelope retried after 2xx ack is your problem to dedupe | +| `OC-Subtype` | Event subtype, copied for routing convenience | +| `OC-Class` | A · B · C | +| `OC-Delivery-Attempt` | 1-based retry counter | +| `Content-Type` | `application/json` | + +## Delivery semantics + +| | | +| --- | --- | +| **Guarantee** | At-least-once. Idempotent on `envelope.id`. | +| **Retry schedule** | Jittered exponential: 0s · 30s · 2m · 10m · 1h · 6h · 24h. | +| **Ack** | Any 2xx. Anything else triggers retry. | +| **Mute** | After 24h of failure the endpoint is muted. Envelopes still archive on `/api/envelope/[id]`. | + +## The raw-body trap + +> **Verify against the raw bytes, not the parsed JSON.** + +`JSON.parse(body)` then `JSON.stringify(value)` produces a *different +byte sequence* — different key order, different whitespace, different +number formatting. The signature is computed over the original bytes; +re-serialized bytes will not match. + +| Framework | The fix | +| --- | --- | +| **Express** | `app.use(express.text({ type: '*/*' }))` — accept body as a raw string. | +| **Next.js Pages API** | `export const config = { api: { bodyParser: false } }` — disable body parsing, then read the stream into a Buffer. | +| **Fastify** | Use a `preParsing` hook to capture the raw body before parsing. | +| **Hono** | `c.req.text()` to get the raw string. | +| **Rust + Axum** | `body: bytes::Bytes` extractor, never `Json<…>`. | + +## Test fire from the dashboard + +[me.ochk.io/developer/webhooks](https://me.ochk.io/developer/webhooks) +has a "test fire" button per registered endpoint. It POSTs a synthetic +envelope with a placeholder signature so you can verify wiring before a +real event lands. Receivers verifying with `@noble/curves` should reject +the test signature (the placeholder is all zeros) — that's the correct +behavior. Production envelopes carry valid sigs. + +## Handler patterns we recommend + +```ts +// Idempotency · keep a small cache of recently seen envelope ids. +const seen = new LRU({ max: 10_000 }); + +app.post('/api/oc/webhook', async (req, res) => { + const result = await oc.webhook.verify(req.body, req.header('OC-Signature'), req.header('OC-Key-Id')); + if (!result.ok) return res.status(401).end(result.reason); + + const envelope = JSON.parse(req.body); + if (seen.has(envelope.id)) return res.status(200).end(); // already processed + seen.set(envelope.id, true); + + try { + await onOcEvent(envelope); + res.status(200).end(); + } catch (err) { + // Don't 5xx unless you actually want the retry; otherwise log + 200. + console.error(err); + res.status(500).end(); + } +}); +``` From 38421700b7a520ff4f40985a9a51518ad243c81a Mon Sep 17 00:00:00 2001 From: Xaxis Date: Thu, 30 Apr 2026 15:25:42 -0700 Subject: [PATCH 5/6] docs(me): sdk reference bumped to v0.5.0 Adds the oc.event.fire section, version bump in metadata + h1, and a note on SignInOptions / PaymentAuthorizeOptions gaining an optional project_key field. Mirrors me.ochk.io/sdk. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/me/sdk.mdx | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/pages/me/sdk.mdx b/src/pages/me/sdk.mdx index 7181edd..e068414 100644 --- a/src/pages/me/sdk.mdx +++ b/src/pages/me/sdk.mdx @@ -1,12 +1,12 @@ export const metadata = { title: 'OC Me · SDK reference', description: - '@orangecheck/me-client v0.4.0 SDK reference. useOcSession, oc.session, oc.payment, oc.config, oc.webhook, oc.delegation, plus every type the SDK exports.', + '@orangecheck/me-client v0.5.0 SDK reference. useOcSession, oc.session, oc.payment, oc.config, oc.webhook, oc.delegation, oc.event, plus every type the SDK exports.', }; # `@orangecheck/me-client` · SDK reference -**v0.4.0 · MIT · 11 KB ESM · 7 KB DTS.** +**v0.5.0 · MIT · 11 KB ESM · 7.7 KB DTS.** This is the canonical TypeScript SDK for integrating me.ochk.io. The same reference is mirrored at [me.ochk.io/sdk](https://me.ochk.io/sdk) — this @@ -156,6 +156,39 @@ await oc.delegation.revoke(env.id); Max TTL 30 days. Revocation is instant — the verifier rejects any envelope signed after revocation regardless of the agent's state. +## `oc.event.fire` · arbitrary billable subtypes (v0.5.0) + +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`. + +```ts +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` | Returns the canonical `BillableEvent` the server recorded. | + +`SignInOptions` and `PaymentAuthorizeOptions` also gain an optional +`project_key` field in v0.5.0 — when set, sessions and payments are +attributed to the project and recorded the same way. + ## Types Every type the SDK exports: From baf7fd347f760cc3a319fd91f50d946f07d00425 Mon Sep 17 00:00:00 2001 From: Xaxis Date: Fri, 1 May 2026 07:03:29 -0700 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20add=20/charter=20=E2=80=94=20single?= =?UTF-8?q?=20source=20of=20truth=20for=20company=20commitments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the eight OrangeCheck charter commitments out of every product subdomain (me.ochk.io, console.ochk.io previously had near-identical copies) and lands them here as the canonical published version. Every product's /charter URL now redirects here. The charter binds the whole company across all products. Each commitment now explicitly notes which product surfaces it most strongly applies to (custody graduation on me.ochk.io, no-custody period on console.ochk.io, free-forever on protocol siblings, etc.) so readers see how the eight commitments translate to each surface without needing per-product duplicate pages. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/charter.mdx | 124 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/pages/charter.mdx diff --git a/src/pages/charter.mdx b/src/pages/charter.mdx new file mode 100644 index 0000000..be2396a --- /dev/null +++ b/src/pages/charter.mdx @@ -0,0 +1,124 @@ +export const metadata = { + title: 'OrangeCheck Charter', + description: + 'Eight commitments OrangeCheck makes to its customers, the open protocol family, and the Bitcoin community. Single source of truth for the whole product family — me.ochk.io, console.ochk.io, and every protocol sibling.', +}; + +# Charter + +**Ratified v1 · 2026-04-28.** + +A commercial entity built on top of an open protocol family has obligations the +protocol layer cannot enforce on its own. These are the ones OrangeCheck accepts +publicly. The full text is committed to git so the diff is auditable; this page +is the canonical published version. + +The charter binds the **whole company**. Every product that ships under the +OrangeCheck name — `me.ochk.io`, `console.ochk.io`, and every sibling protocol +site (`attest`, `lock`, `vote`, `stamp`, `agent`, `pledge`) — operates under +these eight commitments. There is one charter, in one place, on docs.ochk.io. +Product subdomains link here. + +## The eight commitments + +### 01 · No token. Ever. + +OrangeCheck does not and will not issue a token, points balance, airdrop, or any +unit of account other than sats and USD. The economic loop closes on Bitcoin. +Anyone proposing otherwise is not OrangeCheck. + +### 02 · No custody you didn't choose. + +OrangeCheck does not hold customer Bitcoin private keys, does not hold customer +funds, and does not operate a custodial wallet you cannot graduate out of. +On `me.ochk.io` the federation custody model is opt-in by default for users who +don't want to manage keys, and self-custody graduation is a one-click path +(same address, same history). On `console.ochk.io` and the protocol siblings, +no custody — period. Bonded reputation is attestation-of-unspent: sats stay in +the customer's own wallet, enforcement is by exposure, never by slashing. + +### 03 · The protocol stays open. + +The wire format, scope grammar, audit-bundle structure, and Bitcoin-anchoring +rules are defined by the open `oc-*-protocol` specs (`oc-attest-protocol`, +`oc-lock-protocol`, `oc-vote-protocol`, `oc-stamp-protocol`, `oc-agent-protocol`, +`oc-pledge-protocol`). Any change OrangeCheck wants goes through an open PR. +No private extensions, no closed conformance vectors. + +### 04 · The protocol sites stay free. + +`attest.ochk.io`, `lock.ochk.io`, `vote.ochk.io`, `stamp.ochk.io`, +`agent.ochk.io`, `pledge.ochk.io` — every sibling protocol site remains free, +self-hostable, no-account public goods forever. The commercial managed tier +(`console.ochk.io`) and the consumer commercial product (`me.ochk.io`) are +optional layers, never the only path to using the protocols. If we ever change +this, the spec stewardship governance ratifies first. + +### 05 · Audit verifies without us. + +Every audit bundle, every receipt, every billable envelope OrangeCheck issues +is verifiable offline, against Bitcoin headers, by anyone, forever. If +OrangeCheck disappeared tomorrow, every receipt continues to verify. We sell +operations, not lock-in. The protocol is the API. + +### 06 · No agent-replacing-humans positioning. + +OrangeCheck sells authority infrastructure. We do not market agents as labor +displacement. The pitch is that compliance teams can sign off on agents taking +real authority — which is human work being elevated, not erased. + +### 07 · Sat-denominated economics, first-class. + +Lightning customers and sat-paid users are not a footnote. Every USD price is +published with its sat equivalent on the same row. We do not surcharge +Lightning, do not require fiat onramp, and do not treat sat-paid customers as +a niche segment. On `me.ochk.io` specifically, every billable event has its +gross fee, OC platform fee, user cashback, and integrator rebate published in +sats — USD shown alongside, never as the canonical unit. + +### 08 · Spec stewardship is public. + +Material changes to the spec stewardship structure are announced publicly and +discussed in the `oc-protocol` governance forum before being adopted. +OrangeCheck has one seat at that table; it is not the chair. Material +changes to **this charter** itself follow the same path: a published proposal, +a public review window, and a versioned re-ratification. + +## How this applies per product + +- **`me.ochk.io`** — consumer commercial product. Federation custody by + default with self-custody graduation. Three-class billable event taxonomy + (A/B/C). Integrators set every per-event price; OC retains a fixed 20% + platform fee. Commitments **02** (custody you can graduate out of), **05** + (every billable envelope verifies offline), and **07** (sats first-class) + bind this product specifically. +- **`console.ochk.io`** — managed Agent + Pledge enterprise tier. No custody, + ever. Commitments **02** (no custody, period), **05** (audit verifies + without us), **06** (authority infrastructure positioning), and **08** + (one seat, not the chair) bind this product specifically. +- **Protocol siblings** — every `oc-*-protocol` spec repo + the matching + `*.ochk.io` reference site. Commitments **03** (open spec), **04** (free + forever), and **05** (offline-verifiable) bind these specifically. + +## If we ever break one of these + +The protocol layer continues to work without us. Every receipt we ever issued +continues to verify. The spec repos remain MIT, the npm packages remain +published. Customers can fork the managed components and run them themselves on +day one. This is the whole point of the design. + +Spec stewardship and family governance live at +[github.com/orangecheck](https://github.com/orangecheck). The protocol PRs are +the canonical mechanism by which any of the rules on this page can change. + +## Signatures + +These commitments are made by the OrangeCheck founding team. Names will be +appended here as public commitments are formalized. + +| Field | Value | +| ---------------- | ----------------------------------------------------------- | +| Ratified | v1 · 2026-04-28 | +| Next review | 2026-12-01 (or earlier if material change proposed) | +| Source of truth | `oc-docs/src/pages/charter.mdx` | +| Governance forum | [github.com/orangecheck](https://github.com/orangecheck) |