From 463646d01312502b28e509d9bef93de1be31911e Mon Sep 17 00:00:00 2001 From: Xaxis Date: Wed, 11 Nov 2026 18:42:25 -0900 Subject: [PATCH 1/2] me docs: payment intents, fee rounding and sign-out refusals in exact words A payment authorization records an intent and settles nothing: the answer says mode 'stub', settles false, is_stub true. The API and SDK pages now say so, and point billing at the payment_authorization event. The fee is 20% of the price rounded to a whole sat and at least 1 sat, so the user gets about 80%; the price pages say that instead of a division that is only close. The charter's fee sentence is ratified text, so a marked note sits after its product list instead. The local validator checks shape and each event's price limits, and me.ochk.io checks more on save. A seed share of 0 earns nothing and is refused. The multi-account example shows what to say when ochk.io refuses a sign-out or a switch because the browser sent more than one sign-in. --- src/pages/charter.mdx | 4 ++ src/pages/me/api.mdx | 30 +++++++------- src/pages/me/glossary.mdx | 2 +- src/pages/me/integrations.mdx | 5 ++- src/pages/me/quickstart.mdx | 6 ++- src/pages/me/sdk.mdx | 29 ++++++++----- src/pages/reference/multi-account.mdx | 59 +++++++++++++++++---------- 7 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/pages/charter.mdx b/src/pages/charter.mdx index 4345ac6f..23c3dcba 100644 --- a/src/pages/charter.mdx +++ b/src/pages/charter.mdx @@ -132,6 +132,10 @@ review window, and a versioned re-ratification. `*.ochk.io` reference site. Commitments **03** (open spec), **04** (free forever), and **05** (offline-verifiable) bind these specifically. +> **A note, not charter text.** On ochk.app the 20% fee is rounded to a whole +> sat and is at least 1 sat, so on the smallest prices it is more than 20%: a +> 2-sat price pays a 1-sat fee. + ## If we ever break one of these The protocol layer continues to work without us. Every receipt we ever signed diff --git a/src/pages/me/api.mdx b/src/pages/me/api.mdx index 6f06ce90..84346478 100644 --- a/src/pages/me/api.mdx +++ b/src/pages/me/api.mdx @@ -198,13 +198,15 @@ instead. | `POST` | `/api/session/refresh` | refresh inside the policy window — free | | `POST` | `/api/session/invalidate` | tear down — free, needs no session | -## Authenticated · payment authorization (Per action) +## Authenticated · payment intents -| Method | Path | Purpose | -| ------ | ------------------------ | ---------------------------------------------------------------------------------- | -| `POST` | `/api/payment/authorize` | authorize a payment, at most 20 per account per site a UTC day · moves no sats yet | +| Method | Path | Purpose | +| ------ | ------------------------ | ------------------------------------------------------------------------------------------------- | +| `POST` | `/api/payment/authorize` | record a payment intent, at most 20 per account per site a UTC day · moves no sats, earns nothing | -Call it from your server with the person's sign-in token as +It records a payment intent and settles nothing: the answer carries +`mode: 'stub'`, `settles: false` and `is_stub: true`, no sats move, and nobody +earns from it. Call it from your server with the person's sign-in token as `Authorization: Bearer`: it sends no CORS headers, so a browser on another site can't call it, and without a session it answers `401 payment_not_authenticated`. The body takes `amount_sats` (or `usd_cents`) and a `description` of up to 280 @@ -594,15 +596,15 @@ to another change answers `400 unexpected_field`. `POST /api/me/projects` takes: -| Field | | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `display_name` | Required. 1–80 characters, under the naming rules above. | -| `domain` | Required. A DNS hostname. | -| `mode` | `'test'` (the default) or `'live'`. | -| `federation_slug` | Optional. One listed at `/api/federations`; without it, the first live one. | -| `enabled_subtypes` | Optional. Built-in events to start with, at their default prices; any other name answers `422 invalid event seed`. Without it, the three defaults. | -| `user_share_pct` | Optional, with `enabled_subtypes`: what your users earn of each default price, `0` to `0.8`. Below `0.8`, each event is saved at `0.8` with the lower price that earns them the same; a lower price outside the platform's price bounds answers `422 invalid event seed`. Missing or out of range, `0.8`. | -| `pay` | Optional. An `add` change, as for pay periods below, to pay from the start; anything else answers `400 invalid_pay`. | +| Field | | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `display_name` | Required. 1–80 characters, under the naming rules above. | +| `domain` | Required. A DNS hostname. | +| `mode` | `'test'` (the default) or `'live'`. | +| `federation_slug` | Optional. One listed at `/api/federations`; without it, the first live one. | +| `enabled_subtypes` | Optional. Built-in events to start with, at their default prices; any other name answers `422 invalid event seed`. Without it, the three defaults. | +| `user_share_pct` | Optional, with `enabled_subtypes`: what your users earn of each default price, above `0` and at most `0.8`. Below `0.8`, each event is saved at `0.8` with the lower price that earns them the same; a share of `0`, which earns nothing, or a lower price outside the platform's price bounds answers `422 invalid event seed`. Missing or out of range, `0.8`. | +| `pay` | Optional. An `add` change, as for pay periods below, to pay from the start; anything else answers `400 invalid_pay`. | It answers `200 { ok: true, project }`. The signing secret is not in it: reveal it on the project's **Keys & webhooks** tab. diff --git a/src/pages/me/glossary.mdx b/src/pages/me/glossary.mdx index b929bdae..f9a0c14b 100644 --- a/src/pages/me/glossary.mdx +++ b/src/pages/me/glossary.mdx @@ -36,7 +36,7 @@ names do not change. | **project** | Your app's record on orangecheck: its key, its domain, who can sign in, and whether it pays. | `project_key`, `IntegratorPriceConfig` | | **who can sign in** | Everyone, or only accounts backed by bitcoin above your bar. [Details](/me/require-bond). | `require_bond`, `projectKey` in `withOcAuth` | | **Pay your users: Off · Always · Promos** | Whether your events earn your users sats right now. [Details](/me/paying-users). | pay periods, `pay_periods`, `not_paying` | -| **Your user earns N sats** | What one event pays your user. You pay that ÷ 0.8, the 20% fee included: earns 100, you pay 125. | `site_pays`, `user_share_pct` | +| **Your user earns N sats** | What one event pays your user. You pay that plus a fee of 20%, at least 1 sat, so they get about 80%: earns 100, you pay 125. | `site_pays`, `user_share_pct` | | **One-time · Per action · Per session** | The three event groups: things done once (signing up), each time (a payment), and every session. | class A · B · C | | **balance** · **Top up** | The sats your project holds to pay its users. "About N days left" is how long it lasts at your current rate. | escrow, runway | | **Release schedule** | When your users' shares are theirs: right away (the default), or in batches, weekly, monthly or when you say. [Details](/me/drops). | drops, drop windows, `drop.closed` | diff --git a/src/pages/me/integrations.mdx b/src/pages/me/integrations.mdx index 44f294ce..607f158f 100644 --- a/src/pages/me/integrations.mdx +++ b/src/pages/me/integrations.mdx @@ -84,8 +84,9 @@ this is in the sign-in path, and none of it shows unless the person visits. ## Example prices Prices are yours to set, and only matter if you pay your users. For each event, -set what your user earns. You pay that ÷ 0.8, the 20% fee included: earns 100, -you pay 125. Some starting points, for made-up apps: +set what your user earns. You pay that plus the fee: 20% of the price, rounded +to a whole sat and at least 1 sat, so your user gets about 80%. Earns 100, you +pay 125. Some starting points, for made-up apps: | Example app | Event | Your user earns | You pay | | -------------- | ------------------------------------ | ------------------- | ------------------- | diff --git a/src/pages/me/quickstart.mdx b/src/pages/me/quickstart.mdx index efa0ed72..9b3c566b 100644 --- a/src/pages/me/quickstart.mdx +++ b/src/pages/me/quickstart.mdx @@ -346,8 +346,10 @@ signed in as yourself, or catch that error, until you go live. Details: ### 2 · what each event earns For each event, set what your user earns: a fixed number of sats, or a share of -a payment. You pay that ÷ 0.8, the 20% fee included: earns 100, you pay 125. The -smallest earning is 1 sat, and you pay 2. +a payment. You pay that plus the fee: 20% of the price, rounded to a whole sat +and at least 1 sat, so your user gets about 80%. Earns 100, you pay 125. The +smallest earning is 1 sat, and you pay 2. The **Paying** tab shows the exact +price for each earning you set. | Event | Group | Your user earns | You pay | | ----------------------- | ----------- | ------------------- | ------------------- | diff --git a/src/pages/me/sdk.mdx b/src/pages/me/sdk.mdx index a287ba8a..177f0c27 100644 --- a/src/pages/me/sdk.mdx +++ b/src/pages/me/sdk.mdx @@ -1143,13 +1143,17 @@ const paid = await oc.event.fire({ ## `oc.config.validate` · check a price config locally -Check your config against the same rules the server enforces, with no network -call. You edit a project's prices on its **Paying** tab at -[ochk.app](https://ochk.app/me/projects); the SDK has no `oc.config.update()`, -because configs belong to projects, not accounts. - -For each event you set what your user earns. You pay that ÷ 0.8, the 20% fee -included: earns 100, you pay 125. The smallest earning is 1 sat, and you pay 2. +Check your config's shape and each event's own price limits, with no network +call. me.ochk.io checks more when you save, among them the domain, the +operator's price bounds and a price that earns your user nothing, so a config +that passes here can still be refused. You edit a project's prices on its +**Paying** tab at [ochk.app](https://ochk.app/me/projects); the SDK has no +`oc.config.update()`, because configs belong to projects, not accounts. + +For each event you set what your user earns. You pay that plus the fee: 20% of +the price, rounded to a whole sat and at least 1 sat, so your user gets about +80%. Earns 100, you pay 125. The smallest earning is 1 sat, and you pay 2. The +**Paying** tab shows the exact price for each earning you set. ```ts import type { IntegratorPriceConfig } from '@orangecheck/me-client'; @@ -1344,7 +1348,7 @@ takes them only with an ochk.app session, so from your site they answer `401`. A | `oc.session.refresh(id)` | `(string) => Promise` | ochk.app only. Free. | | `oc.session.invalidate(id)` | `(string) => Promise` | ochk.app only. Free. | -## `oc.payment.authorize` · a payment (Per action) +## `oc.payment.authorize` · a payment intent Call it from your server, with the person's sign-in token. me.ochk.io answers no browser on another site, so your page posts the token from `signInWithOc()` to @@ -1370,8 +1374,13 @@ share one. The token decides whose payment it is: me.ochk.io uses its account's `401 payment_not_authenticated`. The token must come from a sign-in on your live project's verified domain; one from any other site answers `403 payment_site_not_registered`. `user_envelope_id` is the receipt's id, the -same as `id`, and `verify_url` opens that receipt. Today the call records the -authorization and moves no sats, because paying is in testing. +same as `id`, and `verify_url` opens that receipt. + +The call records a payment intent and settles nothing: the answer carries +`mode: 'stub'`, `settles: false` and `is_stub: true`. No sats move, your balance +is not charged, and the person earns nothing from it. To bill for a payment, +fire the `payment_authorization` event with `oc.event.fire` and its +`payment_amount_sats`. ## Other signals diff --git a/src/pages/reference/multi-account.mdx b/src/pages/reference/multi-account.mdx index 9922e600..bf73d7cd 100644 --- a/src/pages/reference/multi-account.mdx +++ b/src/pages/reference/multi-account.mdx @@ -155,41 +155,58 @@ import { useState } from 'react'; function Header() { const { account, roster, switchAccount, addAccountUrl, trySignOut } = useOcSession(); - const [failed, setFailed] = useState(false); + const [problem, setProblem] = useState(null); + + async function leave(scope?: 'current') { + const r = await trySignOut(scope ? { scope } : undefined); + if (r.ok) setProblem(null); + else if (r.reason === 'several_sessions') + setProblem( + "Your browser sent more than one ochk.io sign-in, so you weren't signed out. Check which account is signed in now, then try again." + ); + else setProblem("Couldn't sign you out. Try again."); + } + + async function switchTo(didOc: string) { + try { + await switchAccount(didOc); + setProblem(null); + } catch { + setProblem( + "Couldn't switch accounts. Check which account is signed in now, then try again." + ); + } + } return ( ); } ``` -Use `trySignOut` wherever a person signs out. It resolves to `{ ok: false }` -when the auth host refused or didn't answer, and the session may then still be -live, so say so. `signOut` resolves the same way either way and can't tell you. +Use `trySignOut` wherever a person signs out. It resolves to +`{ ok: false, reason }` when the auth host refused or didn't answer, and the +session may then still be live, so say so. `'several_sessions'` means the +browser sent more than one ochk.io sign-in: ochk.io cleared the extra cookies +and ended nothing, and the provider has read the session again, so the page +shows the account still signed in. `signOut` resolves the same way either way +and can't tell you. + +`switchAccount` throws when it did not switch. When the refusal was +`several_sessions`, it reads the session again first and its message is a +sentence for the person. Any other refusal's message names the reason for +developers, so show your own words, as above. No family package renders the roster for you. `AccountMenu` from `@orangecheck/app-design/chrome` draws the account button with the items you From 53d307abf383c3f6867b05894eeb2abe7ec0d5e0 Mon Sep 17 00:00:00 2001 From: Xaxis Date: Wed, 11 Nov 2026 18:45:01 -0900 Subject: [PATCH 2/2] charter note: a small price's fee rounds either way Rounding to a whole sat takes the fee above 20% on some small prices and below it on others: a 2-sat price and a 7-sat price both pay 1 sat. The note now says the fee is not exactly 20% there, with both examples. --- src/pages/charter.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/charter.mdx b/src/pages/charter.mdx index 23c3dcba..153938c0 100644 --- a/src/pages/charter.mdx +++ b/src/pages/charter.mdx @@ -133,8 +133,8 @@ review window, and a versioned re-ratification. forever), and **05** (offline-verifiable) bind these specifically. > **A note, not charter text.** On ochk.app the 20% fee is rounded to a whole -> sat and is at least 1 sat, so on the smallest prices it is more than 20%: a -> 2-sat price pays a 1-sat fee. +> sat and is at least 1 sat, so on the smallest prices it is not exactly 20%: a +> 2-sat price pays a 1-sat fee, and so does a 7-sat price. ## If we ever break one of these