87fd4f32e3
- 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.
197 lines
14 KiB
HTML
197 lines
14 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Keysat Docs: Introduction</title>
|
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
|
<link rel="stylesheet" href="docs.css">
|
|
</head>
|
|
<body>
|
|
|
|
<div class="topnav">
|
|
<a href="https://keysat.xyz" class="brand" title="Back to keysat.xyz"><img src="assets/keysat-mark.svg" alt=""><span>Keysat</span></a>
|
|
<span class="docs-tag">Docs</span>
|
|
</div>
|
|
|
|
<div class="layout">
|
|
<aside class="side">
|
|
<div class="group">
|
|
<div class="glabel">Get started</div>
|
|
<a href="index.html" class="active">Introduction</a>
|
|
<a href="install.html">Install & setup</a>
|
|
<a href="integrate.html">Integrate the SDK</a>
|
|
<a href="agent.html">Agent integration</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Concepts</div>
|
|
<a href="index.html#architecture">Architecture</a>
|
|
<a href="index.html#products-policies">Products & policies</a>
|
|
<a href="index.html#discounts">Discount codes</a>
|
|
<a href="index.html#revocation">Revocation strategy</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Reference</div>
|
|
<a href="wire-format.html">Wire format</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Project</div>
|
|
<a href="pricing.html">Pricing</a>
|
|
<a href="license.html">License</a>
|
|
</div>
|
|
<div class="group">
|
|
<div class="glabel">Operate</div>
|
|
<a href="operate.html#backups">Backups</a>
|
|
<a href="operate.html#migrate">Migrate hardware</a>
|
|
<a href="operate.html#troubleshooting">Troubleshooting</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<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>These docs cover both ends:</p>
|
|
|
|
<div class="next-grid">
|
|
<a class="next-card" href="install.html">
|
|
<span class="eyebrow">Operator</span>
|
|
<h4>Install & setup →</h4>
|
|
<p>Sideload the package, connect BTCPay, define your first product. About an afternoon, end to end.</p>
|
|
</a>
|
|
<a class="next-card" href="integrate.html">
|
|
<span class="eyebrow">Developer</span>
|
|
<h4>Integrate the SDK →</h4>
|
|
<p>Add the SDK to your app, embed your public key, verify a license at startup. About five lines of code.</p>
|
|
</a>
|
|
<a class="next-card" href="agent.html">
|
|
<span class="eyebrow">Agent / automation</span>
|
|
<h4>Agent integration →</h4>
|
|
<p>OpenAPI spec, scoped API keys, webhooks. Build bots that issue comp licenses, react to events, or automate support flows.</p>
|
|
</a>
|
|
</div>
|
|
|
|
<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>: 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/<product></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’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’s comfortable to print on a receipt.</p>
|
|
|
|
<div class="callout">
|
|
<i data-lucide="info"></i>
|
|
<p><strong>Why offline matters.</strong> Online license servers are a single point of failure for every customer who ever bought your software. With Keysat, if your Start9 disappears tomorrow, every previously-issued license still verifies. That’s sovereignty.</p>
|
|
</div>
|
|
|
|
<h2 id="products-policies">Products & policies</h2>
|
|
<p>You declare two things in Keysat: products and policies.</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">
|
|
<thead><tr><th>Field</th><th>Meaning</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>duration_seconds</code></td><td>How long the license is valid. <code>0</code> means perpetual.</td></tr>
|
|
<tr><td><code>grace_seconds</code></td><td>Extra time after expiry before the verifier rejects.</td></tr>
|
|
<tr><td><code>max_machines</code></td><td>Seat cap. <code>0</code> means unlimited.</td></tr>
|
|
<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’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. 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’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=<slug></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, e.g. a longer-duration "Lifetime" comp for conferences, a richer-entitlement "Internal" tier for support cases. Private policies don’t appear on the buy page; the admin API issues them directly.</p>
|
|
|
|
<h2 id="discounts">Discount codes</h2>
|
|
<p>Four kinds:</p>
|
|
|
|
<table class="t">
|
|
<thead><tr><th>Kind</th><th>What it does</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>percent</code></td><td>Buyer appends <code>?code=FOUNDERS50</code> to the purchase URL; price drops by N%.</td></tr>
|
|
<tr><td><code>fixed_sats</code></td><td>Like above, but a flat amount comes off. Denominated in the code’s <code>discount_currency</code> (sats / USD / EUR), so the same code can sit on top of multi-currency products.</td></tr>
|
|
<tr><td><code>set_price</code></td><td>Overrides the tier price with a flat number, regardless of base. Useful for "first 100 buyers at 25k sats" promos where you want the price to be a specific round number rather than a percentage off.</td></tr>
|
|
<tr><td><code>free_license</code></td><td>No payment at all. Buyer redeems the code via <code>POST /v1/redeem</code> and gets a signed license back.</td></tr>
|
|
</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, 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’t type any code, with the input pre-filled so they can see what’s been applied.</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, even if you mark it as revoked in the admin UI. The verifier in your app doesn’t know about your admin actions.</p>
|
|
<p>You have three options:</p>
|
|
|
|
<ul>
|
|
<li><strong>Don’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: 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’t force a particular revocation model. The default is no revocation. That’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; 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>
|
|
<li><code>GET https://licensing.keysat.xyz/v1/products/keysat/policies</code> for the machine-readable shape (entitlements, marketing bullets, featured discount, etc.).</li>
|
|
<li>Your local daemon’s <code>GET /v1/admin/tier</code> for current tier + caps + usage from inside the admin context.</li>
|
|
</ul>
|
|
|
|
<h2 id="next">Where to next</h2>
|
|
<div class="next-grid">
|
|
<a class="next-card" href="install.html">
|
|
<span class="eyebrow">Step 1 for operators</span>
|
|
<h4>Install & setup →</h4>
|
|
<p>Get Keysat running on your Start9, connect BTCPay, define your first product.</p>
|
|
</a>
|
|
<a class="next-card" href="integrate.html">
|
|
<span class="eyebrow">Step 1 for integrators</span>
|
|
<h4>Integrate the SDK →</h4>
|
|
<p>Embed your public key, add the SDK to your app, verify a license offline.</p>
|
|
</a>
|
|
<a class="next-card" href="agent.html">
|
|
<span class="eyebrow">Step 1 for agent builders</span>
|
|
<h4>Agent integration →</h4>
|
|
<p>Operate your Keysat instance programmatically. OpenAPI spec, scoped keys, webhooks, recipes.</p>
|
|
</a>
|
|
</div>
|
|
</main>
|
|
|
|
<aside class="toc">
|
|
<div class="label">On this page</div>
|
|
<a href="#architecture">Architecture</a>
|
|
<a href="#products-policies">Products & policies</a>
|
|
<a href="#discounts">Discount codes</a>
|
|
<a href="#revocation">Revocation strategy</a>
|
|
<a href="#operator-tiers">Operator tiers</a>
|
|
<a href="#next">Where to next</a>
|
|
</aside>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script>lucide.createIcons();</script>
|
|
<script src="docs.js"></script>
|
|
</body>
|
|
</html>
|