Docs polish: active-pill sync, license-sidebar bug fix, pricing standardized, ~70 em-dashes removed

- docs.js (new): sync sidebar .active pill with location.hash on load, click, and hashchange so in-page anchor links (Architecture, Discount codes, Backups, etc.) update the pill instead of leaving it stuck on whatever was statically marked
- Wire docs.js into every page just before </body>
- license.html: sidebar Project/Operate order matches every other page (Project first)
- pricing.html: rewritten to use the standard docs layout (full sidebar groups, prose main, breadcrumb) instead of a one-off shell that felt detached from the rest of the docs
- Reference section: remove Admin API + SDKs anchor links (they masqueraded as separate pages but just scrolled within integrate.html); Wire format stands alone
- Pricing copy: Zaprite reframed as "expanded payment options including card payment capabilities", "shipping in v0.3" removed (it shipped), Patron rephrased as perpetual (never expires or renews)
- "Toggling inactive" cap-evasion language replaced — admin UI exposes delete only, no soft-disable affordance for products
- ~70 em-dashes removed across 8 pages using a small pattern set (elaboration→period, list-intro→colon, tight clarification→comma, parentheticals→parens). Decorative stamp ornaments and references to actual third-party UI labels are kept verbatim.
This commit is contained in:
Keysat
2026-05-12 09:25:57 -05:00
parent 348a0b9f13
commit 87fd4f32e3
9 changed files with 285 additions and 220 deletions
+20 -21
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keysat Docs Introduction</title>
<title>Keysat Docs: Introduction</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="docs.css">
</head>
@@ -33,8 +33,6 @@
<div class="group">
<div class="glabel">Reference</div>
<a href="wire-format.html">Wire format</a>
<a href="integrate.html#api">Admin API</a>
<a href="integrate.html#sdks">SDKs</a>
</div>
<div class="group">
<div class="glabel">Project</div>
@@ -52,8 +50,8 @@
<main class="prose">
<div class="crumb">Get started · Introduction</div>
<h1>Welcome to Keysat.</h1>
<p class="lead">Keysat lets independent software creators sell their work on their own terms. You ship software open source, closed source, free / paid versions, whatever fits and Keysat handles the buy page, payment via BTCPay, and a signed license for each buyer.</p>
<p>How you use that license inside your software is up to you: a one-time purchase to unlock the whole app, a free + paid split with specific paid features, a tip-jar style supporter badge all legitimate. The licensing layer is a primitive, not a script.</p>
<p class="lead">Keysat lets independent software creators sell their work on their own terms. You ship software (open source, closed source, free / paid versions, whatever fits), and Keysat handles the buy page, payment via BTCPay, and a signed license for each buyer.</p>
<p>How you use that license inside your software is up to you: a one-time purchase to unlock the whole app, a free + paid split with specific paid features, a tip-jar style supporter badge: all legitimate. The licensing layer is a primitive, not a script.</p>
<p>These docs cover both ends:</p>
@@ -78,9 +76,9 @@
<h2 id="architecture">Architecture</h2>
<p>Keysat is the licensing layer sitting on top of your existing payments stack. Three boxes:</p>
<ul>
<li><strong>BTCPay Server</strong> &mdash; takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.</li>
<li><strong>Keysat</strong> &mdash; your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at <code>/buy/&lt;product&gt;</code>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.</li>
<li><strong>Your software</strong> &mdash; the thing you sell. Ships with the Keysat <em>public</em> key embedded at compile time. On startup it reads the user&rsquo;s license and verifies the signature offline. No network call.</li>
<li><strong>BTCPay Server</strong>: takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.</li>
<li><strong>Keysat</strong>: your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at <code>/buy/&lt;product&gt;</code>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.</li>
<li><strong>Your software</strong>: the thing you sell. Ships with the Keysat <em>public</em> key embedded at compile time. On startup it reads the user&rsquo;s license and verifies the signature offline. No network call.</li>
</ul>
<p>The key word is <em>offline</em>. Once a license is issued, your software does not need to phone home to verify it. The verification is a pure function of the license bytes and the public key. This is the same model used by signed JWTs, except wrapped in a small fixed-width format that&rsquo;s comfortable to print on a receipt.</p>
@@ -92,7 +90,7 @@
<h2 id="products-policies">Products &amp; policies</h2>
<p>You declare two things in Keysat: products and policies.</p>
<p>A <strong>product</strong> is the thing you sell &mdash; "Bitcoin Ticker Pro", "Aurora Plugin", whatever. It has a slug, a display name, a description, and a price (sats / USD / EUR). Each product also carries an <strong>entitlements catalog</strong> &mdash; the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.</p>
<p>A <strong>product</strong> is the thing you sell: "Bitcoin Ticker Pro", "Aurora Plugin", whatever. It has a slug, a display name, a description, and a price (sats / USD / EUR). Each product also carries an <strong>entitlements catalog</strong>: the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.</p>
<p>A <strong>policy</strong> is a license template attached to a product. It specifies:</p>
<table class="t">
@@ -104,13 +102,13 @@
<tr><td><code>is_trial</code></td><td>Sets a <code>TRIAL</code> bit so your app can show a "trial" banner.</td></tr>
<tr><td><code>is_recurring</code> + <code>renewal_period_days</code></td><td>Auto-renew on a cycle (weekly / monthly / annual / custom). The daemon mints a fresh invoice + signed license per cycle.</td></tr>
<tr><td><code>entitlements</code></td><td>Subset of the product&rsquo;s catalog this policy grants. Baked into the signed license.</td></tr>
<tr><td><code>metadata.marketing_bullets</code></td><td>Operator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy &mdash; not enforced.</td></tr>
<tr><td><code>metadata.hidden_entitlements</code></td><td>Slugs the license still grants but the buy-page card hides &mdash; useful when a higher tier uses "Everything in X, plus:" copy and doesn&rsquo;t want to repeat implied entitlements.</td></tr>
<tr><td><code>metadata.marketing_bullets</code></td><td>Operator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy. Not enforced.</td></tr>
<tr><td><code>metadata.hidden_entitlements</code></td><td>Slugs the license still grants but the buy-page card hides; useful when a higher tier uses "Everything in X, plus:" copy and doesn&rsquo;t want to repeat implied entitlements.</td></tr>
</tbody>
</table>
<p>A product can have <strong>one policy or many</strong>. Multi-tier ladders (think Basic / Pro / Max) are first-class: when a product has two or more public policies, the buy page renders a tier picker and the buyer chooses before paying. The displayed tier is selected from a <code>?policy=&lt;slug&gt;</code> URL hint, then the <code>highlighted</code> ("most popular") policy if any, then the cheapest. Tier ordering on the picker is operator-controlled via drag-and-drop in the admin UI (or <code>tier_rank</code> in the API).</p>
<p>You can also attach <strong>private policies</strong> for manual issuance &mdash; e.g. a longer-duration "Lifetime" comp for conferences, a richer-entitlement "Internal" tier for support cases. Private policies don&rsquo;t appear on the buy page; the admin API issues them directly.</p>
<p>You can also attach <strong>private policies</strong> for manual issuance, e.g. a longer-duration "Lifetime" comp for conferences, a richer-entitlement "Internal" tier for support cases. Private policies don&rsquo;t appear on the buy page; the admin API issues them directly.</p>
<h2 id="discounts">Discount codes</h2>
<p>Four kinds:</p>
@@ -125,34 +123,34 @@
</tbody>
</table>
<p>Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a <strong>subset of policies</strong> on that product &mdash; e.g. "applies to Pro and Max but not Basic"), and tagged with a referrer label so you can see which campaign drove which sales in the audit log.</p>
<p>Codes can also be marked <strong>featured</strong> &mdash; a "launch special" mode. A featured code:</p>
<p>Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a <strong>subset of policies</strong> on that product, e.g. "applies to Pro and Max but not Basic"), and tagged with a referrer label so you can see which campaign drove which sales in the audit log.</p>
<p>Codes can also be marked <strong>featured</strong>: a "launch special" mode. A featured code:</p>
<ul>
<li>Renders a diagonal "LAUNCH SPECIAL" ribbon + struck-through original price on the matching tier cards on the buy page.</li>
<li>Auto-applies for buyers who don&rsquo;t type any code, with the input pre-filled so they can see what&rsquo;s been applied.</li>
<li>Stops surfacing once it hits its <code>max_uses</code> cap or expires &mdash; the ribbon disappears and pricing reverts to standard automatically.</li>
<li>Stops surfacing once it hits its <code>max_uses</code> cap or expires: the ribbon disappears and pricing reverts to standard automatically.</li>
</ul>
<p>Operator-typed codes always take precedence: a buyer who pastes a non-featured code in the form gets that code instead of the auto-applied featured one.</p>
<h2 id="revocation">Revocation strategy</h2>
<p>This is the one piece of the architecture that requires a design decision from you.</p>
<p>Because verification is offline, a license that was once issued continues to verify forever &mdash; even if you mark it as revoked in the admin UI. The verifier in your app doesn&rsquo;t know about your admin actions.</p>
<p>Because verification is offline, a license that was once issued continues to verify forever, even if you mark it as revoked in the admin UI. The verifier in your app doesn&rsquo;t know about your admin actions.</p>
<p>You have three options:</p>
<ul>
<li><strong>Don&rsquo;t support revocation at all.</strong> Many indie developers do this. Once a key is sold, it stays valid. Refunds are still possible &mdash; you send sats back via BTCPay; the key still works but the customer agreed to stop using it.</li>
<li><strong>Don&rsquo;t support revocation at all.</strong> Many indie developers do this. Once a key is sold, it stays valid. Refunds are still possible. You send sats back via BTCPay; the key still works but the customer agreed to stop using it.</li>
<li><strong>Periodic online check.</strong> Your app fetches a small revocation list from your Keysat (or a CDN you point at it) once a week / month. Adds a "soft-online" requirement.</li>
<li><strong>Short-lived licenses with renewal.</strong> Issue 30-day licenses; the app fetches a fresh signed token before expiry. Recurring renewals are first-class in v0.2 &mdash; define a policy with <code>is_recurring=true</code> + <code>renewal_period_days</code> and Keysat handles the cycle (invoice → settle → re-sign → webhook).</li>
<li><strong>Short-lived licenses with renewal.</strong> Issue 30-day licenses; the app fetches a fresh signed token before expiry. Recurring renewals are first-class in v0.2: define a policy with <code>is_recurring=true</code> + <code>renewal_period_days</code> and Keysat handles the cycle (invoice → settle → re-sign → webhook).</li>
</ul>
<div class="callout">
<i data-lucide="key-round"></i>
<p><strong>You decide the policy.</strong> Keysat doesn&rsquo;t force a particular revocation model. The default is no revocation &mdash; that&rsquo;s the simplest, sovereign-by-default choice. If you need stronger guarantees, layer them on with the patterns above.</p>
<p><strong>You decide the policy.</strong> Keysat doesn&rsquo;t force a particular revocation model. The default is no revocation. That&rsquo;s the simplest, sovereign-by-default choice. If you need stronger guarantees, layer them on with the patterns above.</p>
</div>
<h2 id="operator-tiers">Operator tiers</h2>
<p>Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free <strong>Creator</strong> tier with caps that are generous for a solo developer; paid <strong>Pro</strong> and <strong>Patron</strong> tiers lift caps and unlock recurring billing + the Zaprite payment gateway. Caps are enforced by the daemon at create-time only &mdash; existing resources are always grandfathered if you downgrade.</p>
<p>As of this writing, Creator caps at <strong>5 products / 5 policies per product / 10 active discount codes</strong>, and Pro / Patron are unlimited. The exact tier list, prices, entitlements, and any active launch-special discount are operator-controlled on the master Keysat and may change &mdash; the canonical sources are:</p>
<p>Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free <strong>Creator</strong> tier with caps that are generous for a solo developer; paid <strong>Pro</strong> and <strong>Patron</strong> tiers lift caps and unlock recurring billing + the Zaprite payment gateway. Caps are enforced by the daemon at create-time only; existing resources are always grandfathered if you downgrade.</p>
<p>As of this writing, Creator caps at <strong>5 products / 5 policies per product / 10 active discount codes</strong>, and Pro / Patron are unlimited. The exact tier list, prices, entitlements, and any active launch-special discount are operator-controlled on the master Keysat and may change. The canonical sources are:</p>
<ul>
<li>The live tier cards on <a href="https://keysat.xyz#tiers">keysat.xyz</a> (rendered dynamically from the master Keysat).</li>
<li>The <a href="pricing.html">pricing page</a> on these docs for the human-readable breakdown.</li>
@@ -193,5 +191,6 @@
<script src="https://unpkg.com/lucide@latest"></script>
<script>lucide.createIcons();</script>
<script src="docs.js"></script>
</body>
</html>