From 87fd4f32e35cff72bc073218435f7e38b84b22e9 Mon Sep 17 00:00:00 2001 From: Keysat Date: Tue, 12 May 2026 09:25:57 -0500 Subject: [PATCH] Docs polish: active-pill sync, license-sidebar bug fix, pricing standardized, ~70 em-dashes removed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 - 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. --- agent.html | 31 +++--- docs.js | 49 ++++++++++ index.html | 41 ++++---- install.html | 41 ++++---- integrate.html | 25 +++-- license.html | 31 +++--- operate.html | 19 ++-- pricing.html | 245 ++++++++++++++++++++++++++--------------------- wire-format.html | 23 +++-- 9 files changed, 285 insertions(+), 220 deletions(-) create mode 100644 docs.js diff --git a/agent.html b/agent.html index 33854e7..b16d763 100644 --- a/agent.html +++ b/agent.html @@ -3,7 +3,7 @@ -Keysat Docs — Agent integration +Keysat Docs: Agent integration @@ -33,8 +33,6 @@
Reference
Wire format - Admin API - SDKs
Project
@@ -53,7 +51,7 @@
Get started · Agent integration

Agent integration guide.

How to build agents, bots, and automation that operate a Keysat instance. Keysat was designed from the start to be agent-friendly: the admin API uses plain HTTP + JSON with Bearer-token auth, an OpenAPI 3.1 spec drives discovery, scoped API keys grant least-privilege access without exposing the master credential, errors carry stable machine-readable codes, and webhooks let an agent react to events instead of polling.

-

This guide covers the operator side of Keysat — running, configuring, and performing day-to-day operations. For the buyer side (validating licenses inside your app), see Integrate the SDK.

+

This guide covers the operator side of Keysat: running, configuring, and performing day-to-day operations. For the buyer side (validating licenses inside your app), see Integrate the SDK.

Quick start

# 1. Discover the API surface
@@ -64,7 +62,7 @@ curl -X POST https://your-keysat-host/v1/admin/api-keys \
   -H "Authorization: Bearer $MASTER_ADMIN_KEY" \
   -H "Content-Type: application/json" \
   -d '{"label":"Support bot","role":"support"}'
-# Response includes `token: ks_...`. Save it — it's only shown once.
+# Response includes `token: ks_...`. Save it. It's only shown once.
 
 # 3. Use the scoped key
 curl https://your-keysat-host/v1/admin/licenses?status=active \
@@ -75,9 +73,9 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
     
Authorization: Bearer <token>

Two kinds of tokens are accepted.

-

Master admin API key — the env-configured KEYSAT_ADMIN_API_KEY (visible in StartOS Actions → Show credentials on first install). Full access to every endpoint. This is the operator's credential. Don't hand it to agents.

+

Master admin API key: the env-configured KEYSAT_ADMIN_API_KEY (visible in StartOS Actions → Show credentials on first install). Full access to every endpoint. This is the operator's credential. Don't hand it to agents.

-

Scoped API keys — additional tokens generated in admin UI → Settings → API keys. Each carries a role that bounds what it can do. Format: ks_<43 chars>. Operators can revoke any scoped key from the same UI; revoked tokens stop working immediately.

+

Scoped API keys: additional tokens generated in admin UI → Settings → API keys. Each carries a role that bounds what it can do. Format: ks_<43 chars>. Operators can revoke any scoped key from the same UI; revoked tokens stop working immediately.

Role to scope mapping

@@ -89,13 +87,13 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \
full-adminEvery scope. Equivalent to the master key for most endpoints.
-

Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A full-admin scoped key cannot, for example, generate another scoped key — that's a self-defeating elevation path.

+

Endpoints that touch settings (operator name, payment provider connections, self-license activation, scoped API key management) always require the master admin key. A full-admin scoped key cannot, for example, generate another scoped key. That's a self-defeating elevation path.

Discovering the API

Two complementary discovery mechanisms.

OpenAPI 3.1 spec

-

GET /v1/openapi.json — unauthenticated. Returns a curated spec covering the agent-relevant subset of endpoints. Use this with:

+

GET /v1/openapi.json. Unauthenticated. Returns a curated spec covering the agent-relevant subset of endpoints. Use this with:

  • OpenAI Custom GPTs: paste the URL as an Action.
  • OpenAI Assistants / Functions: feed the spec to tool definition generators.
  • @@ -177,7 +175,7 @@ curl https://your-keysat-host/v1/admin/licenses?status=active \

    Find a license by email

    curl "$KS/v1/admin/licenses?buyer_email=alice@example.com" \
       -H "Authorization: Bearer ks_..."
    -

    Returns matching licenses (without the license_key field — that's only returned on issue / recover). Use the id for follow-up operations.

    +

    Returns matching licenses (without the license_key field, which is only returned on issue / recover). Use the id for follow-up operations.

    Scope required: licenses:read.

    Cancel a buyer's subscription

    @@ -209,7 +207,7 @@ curl -X POST $KS/v1/admin/subscriptions/$SUB_ID/cancel \

    Always applies as comp (no invoice) from the admin path. Buyer-initiated paid upgrades go through /v1/upgrade (different endpoint, signed-license auth).

    Scope required: licenses:write.

    -

    Webhooks — react to events instead of polling

    +

    Webhooks: react to events instead of polling

    Configure webhook endpoints in admin UI → Webhooks. The daemon POSTs JSON payloads, HMAC-SHA256 signed with the endpoint's secret, on these events:

    @@ -238,21 +236,21 @@ def verify(body_bytes: bytes, signature_header: str, secret: str) -> bool:

    A few patterns that work well in practice.

    Idempotency

    -

    The daemon's mutation endpoints are idempotent where they can be. Revoke, suspend, unsuspend, archive, unarchive, subscription cancel — all return success on the second call without changing state. Your agent can safely retry on network errors.

    +

    The daemon's mutation endpoints are idempotent where they can be. Revoke, suspend, unsuspend, archive, unarchive, subscription cancel. All return success on the second call without changing state. Your agent can safely retry on network errors.

    Pagination

    List endpoints return up to ~100 rows by default. Use ?limit=N and ?offset=N for larger result sets. The OpenAPI spec documents the limits per endpoint.

    Rate limits

    -

    The admin endpoints have no per-IP rate limit today — operators are trusted. The public endpoints (/v1/validate, /v1/recover) are rate-limited per client IP (10/min for /recover; /validate is unlimited but a reasonable agent calls it once per app boot + once per hour).

    +

    The admin endpoints have no per-IP rate limit today. Operators are trusted. The public endpoints (/v1/validate, /v1/recover) are rate-limited per client IP (10/min for /recover; /validate is unlimited but a reasonable agent calls it once per app boot + once per hour).

    Master key handling

    If your automation needs full-admin because it touches operator-only operations (creating other API keys, changing payment providers), use the master key from a secret manager. If it can stay within license / product / policy operations, always use a scoped key. Operators can revoke a compromised scoped key without rotating the master credential.

    Backoff on 5xx

    -

    internal_error (500) is a bug or a transient DB lock. Retry with exponential backoff (1s, 2s, 4s, 8s, give up). Don't retry on 4xx — those are deterministic client errors.

    +

    internal_error (500) is a bug or a transient DB lock. Retry with exponential backoff (1s, 2s, 4s, 8s, give up). Don't retry on 4xx. Those are deterministic client errors.

    -

    Concrete recipe — "Comp a license to anyone who emails support@"

    +

    Concrete recipe: "Comp a license to anyone who emails support@"

    import os, requests, imaplib, email
     
     KS = os.environ["KEYSAT_URL"]
    @@ -274,7 +272,7 @@ def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str:
         return r.json()["license_key"]
     
     # Poll IMAP, parse incoming requests, call issue_comp_license, reply with the key
    -

    That's the entire pattern. The agent doesn't need full admin — just the license-issuer role. If it ever gets compromised, you revoke the scoped key in the admin UI and generate a new one in 30 seconds.

    +

    That's the entire pattern. The agent doesn't need full admin, just the license-issuer role. If it ever gets compromised, you revoke the scoped key in the admin UI and generate a new one in 30 seconds.

    What's NOT exposed to agents

    Some operations are deliberately operator-only and not accessible to any scoped key, including full-admin:

    @@ -306,5 +304,6 @@ def issue_comp_license(buyer_email: str, product_slug: str, reason: str) -> str: + diff --git a/docs.js b/docs.js new file mode 100644 index 0000000..c532844 --- /dev/null +++ b/docs.js @@ -0,0 +1,49 @@ +// Shared docs-site behaviors. Loaded at the bottom of every page. +// +// Sidebar active-pill sync: the .active class on sidebar links is +// baked into each HTML file as a baseline (page-level link only), +// which means clicking an in-page anchor link (e.g. "Discount codes" +// → index.html#discounts) leaves the pill stuck on whatever the +// statically-marked page link was. This script keeps the active +// state in sync with the current URL hash so the pill follows +// what the user clicked. +(function () { + var sidebarLinks = Array.prototype.slice.call( + document.querySelectorAll('aside.side a') + ); + if (!sidebarLinks.length) return; + + var currentFile = (location.pathname.split('/').pop() || 'index.html'); + + function findActive(hash) { + var desired = hash ? (currentFile + hash) : currentFile; + var match = null; + sidebarLinks.forEach(function (a) { + if (a.getAttribute('href') === desired) match = a; + }); + if (match) return match; + // Fallback: bare current-file link when no hash matches + sidebarLinks.forEach(function (a) { + if (a.getAttribute('href') === currentFile) match = a; + }); + return match; + } + + function setActive(link) { + if (!link) return; + sidebarLinks.forEach(function (a) { a.classList.remove('active'); }); + link.classList.add('active'); + } + + setActive(findActive(location.hash)); + + window.addEventListener('hashchange', function () { + setActive(findActive(location.hash)); + }); + + sidebarLinks.forEach(function (a) { + a.addEventListener('click', function () { + setActive(a); + }); + }); +})(); diff --git a/index.html b/index.html index be54cd9..6f6ea6e 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ -Keysat Docs — Introduction +Keysat Docs: Introduction @@ -33,8 +33,6 @@
    Reference
    Wire format - Admin API - SDKs
    Project
    @@ -52,8 +50,8 @@
    Get started · Introduction

    Welcome to Keysat.

    -

    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.

    -

    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.

    +

    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.

    +

    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.

    These docs cover both ends:

    @@ -78,9 +76,9 @@

    Architecture

    Keysat is the licensing layer sitting on top of your existing payments stack. Three boxes:

      -
    • BTCPay Server — takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.
    • -
    • Keysat — your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at /buy/<product>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.
    • -
    • Your software — the thing you sell. Ships with the Keysat public key embedded at compile time. On startup it reads the user’s license and verifies the signature offline. No network call.
    • +
    • BTCPay Server: takes the payment. On-chain Bitcoin or Lightning, settling to your wallet. Lives on your Start9.
    • +
    • Keysat: your private licensing service. Holds the Ed25519 signing key. Hosts the public purchase URLs at /buy/<product>. Listens for BTCPay payment webhooks and issues a signed license on each settlement. Lives on your Start9.
    • +
    • Your software: the thing you sell. Ships with the Keysat public key embedded at compile time. On startup it reads the user’s license and verifies the signature offline. No network call.

    The key word is offline. 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.

    @@ -92,7 +90,7 @@

    Products & policies

    You declare two things in Keysat: products and policies.

    -

    A product 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 entitlements catalog — the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.

    +

    A product 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 entitlements catalog: the typed list of feature slugs your software cares about, plus their display names and descriptions. Policies pick entitlements from this catalog.

    A policy is a license template attached to a product. It specifies:

    EventFires on
    @@ -104,13 +102,13 @@ - - + +
    is_trialSets a TRIAL bit so your app can show a "trial" banner.
    is_recurring + renewal_period_daysAuto-renew on a cycle (weekly / monthly / annual / custom). The daemon mints a fresh invoice + signed license per cycle.
    entitlementsSubset of the product’s catalog this policy grants. Baked into the signed license.
    metadata.marketing_bulletsOperator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy — not enforced.
    metadata.hidden_entitlementsSlugs 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.
    metadata.marketing_bulletsOperator-authored ✓ items rendered on the buy-page tier card. Pure marketing copy. Not enforced.
    metadata.hidden_entitlementsSlugs 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.

    A product can have one policy or many. 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 ?policy=<slug> URL hint, then the highlighted ("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 tier_rank in the API).

    -

    You can also attach private policies 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.

    +

    You can also attach private policies 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.

    Discount codes

    Four kinds:

    @@ -125,34 +123,34 @@ -

    Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a subset of policies 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.

    -

    Codes can also be marked featured — a "launch special" mode. A featured code:

    +

    Codes can be capped at N uses, dated to expire, restricted to one product (and optionally to a subset of policies 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.

    +

    Codes can also be marked featured: a "launch special" mode. A featured code:

    • Renders a diagonal "LAUNCH SPECIAL" ribbon + struck-through original price on the matching tier cards on the buy page.
    • Auto-applies for buyers who don’t type any code, with the input pre-filled so they can see what’s been applied.
    • -
    • Stops surfacing once it hits its max_uses cap or expires — the ribbon disappears and pricing reverts to standard automatically.
    • +
    • Stops surfacing once it hits its max_uses cap or expires: the ribbon disappears and pricing reverts to standard automatically.

    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.

    Revocation strategy

    This is the one piece of the architecture that requires a design decision from you.

    -

    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.

    +

    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.

    You have three options:

      -
    • Don’t support revocation at all. 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.
    • +
    • Don’t support revocation at all. 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.
    • Periodic online check. 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.
    • -
    • Short-lived licenses with renewal. 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 is_recurring=true + renewal_period_days and Keysat handles the cycle (invoice → settle → re-sign → webhook).
    • +
    • Short-lived licenses with renewal. 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 is_recurring=true + renewal_period_days and Keysat handles the cycle (invoice → settle → re-sign → webhook).
    -

    You decide the policy. 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.

    +

    You decide the policy. 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.

    Operator tiers

    -

    Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free Creator tier with caps that are generous for a solo developer; paid Pro and Patron 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.

    -

    As of this writing, Creator caps at 5 products / 5 policies per product / 10 active discount codes, 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:

    +

    Keysat itself ships under a tiered self-license. The daemon runs out of the box at the free Creator tier with caps that are generous for a solo developer; paid Pro and Patron 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.

    +

    As of this writing, Creator caps at 5 products / 5 policies per product / 10 active discount codes, 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:

    • The live tier cards on keysat.xyz (rendered dynamically from the master Keysat).
    • The pricing page on these docs for the human-readable breakdown.
    • @@ -193,5 +191,6 @@ + diff --git a/install.html b/install.html index 11e663a..5473d8d 100644 --- a/install.html +++ b/install.html @@ -3,7 +3,7 @@ -Keysat Docs — Install & setup +Keysat Docs: Install & setup @@ -33,8 +33,6 @@
      Reference
      Wire format - Admin API - SDKs
      Project
      @@ -61,7 +59,7 @@
    • About 2 GB of free disk for Keysat itself; BTCPay’s requirements are larger and depend on your Bitcoin node mode.
    -

    Step 1 — Install Keysat

    +

    Step 1: Install Keysat

    Two ways. Either gets you to the same place.

    Option A: from the Keysat marketplace (recommended)

    @@ -80,11 +78,11 @@

    BTCPay Server is declared as a required dependency. If you don’t have it installed yet, StartOS will prompt you to install it as part of the same flow.

    -

    Step 2 — Set your operator name

    -

    Open the Keysat service page in StartOS. Go to Actions → Set operator name. Pick a short label that identifies you as the seller — e.g. "aurora-software", "northpath", "my-name". This shows up on the public purchase pages and in the audit log.

    +

    Step 2: Set your operator name

    +

    Open the Keysat service page in StartOS. Go to Actions → Set operator name. Pick a short label that identifies you as the seller, e.g. "aurora-software", "northpath", "my-name". This shows up on the public purchase pages and in the audit log.

    This change is live-reloaded; you don’t need to restart the service.

    -

    Step 3 — Connect BTCPay

    +

    Step 3: Connect BTCPay

    Make sure BTCPay Server is running and has at least one store with a configured payment method (on-chain wallet or Lightning node). Without a payment method, BTCPay will reject Keysat’s invoice creation.

    In Keysat’s service page, click Actions → Connect BTCPay. You’ll be redirected to BTCPay’s authorize page, where you grant Keysat the permissions it needs:

    @@ -117,7 +115,7 @@ payment_methods: [BTC-OnChain, BTC-LightningNetwork]
If payment_methods is empty, head back to BTCPay and configure at least one before continuing.

-

Step 4 — Get your admin API key

+

Step 4: Get your admin API key

Go to Actions → Show admin API key. This reveals the 64-hex-character key that gates all /v1/admin/* endpoints, including the admin UI.

@@ -125,44 +123,44 @@ payment_methods: [BTC-OnChain, BTC-LightningNetwork]Treat this key like a password. Anyone with it can issue, revoke, or read every license you’ve ever sold. Don’t paste it into Slack. Don’t check it into Git.

-

Step 5 — Open the admin UI

+

Step 5: Open the admin UI

Click the Launch UI button on Keysat’s service page. (StartOS surfaces this for any service that defines a type: 'ui' interface.) Paste the admin key from the previous step into the sign-in form.

From here on, you mostly work in the admin UI. The StartOS Actions tab is reserved for setup-only operations (operator name, BTCPay connect/disconnect/check, show admin key).

-

Step 6 — Define your first product

+

Step 6: Define your first product

In the admin UI, go to Products → Create a new product and fill in:

    -
  • Slug — lowercase, hyphens, will appear in the public URL. e.g. bitcoin-ticker-pro.
  • -
  • Display name — shown on the buyer’s purchase page and on receipts.
  • -
  • Description — one or two sentences; rendered as plain text.
  • -
  • Price — the currency picker accepts sats, USD, or EUR. For sats, enter an integer (e.g. 50000). For USD/EUR, enter the amount in dollars/euros — Keysat converts to BTC at invoice creation and the buyer pays the locked-in BTC amount.
  • +
  • Slug: lowercase, hyphens, will appear in the public URL. e.g. bitcoin-ticker-pro.
  • +
  • Display name: shown on the buyer’s purchase page and on receipts.
  • +
  • Description: one or two sentences; rendered as plain text.
  • +
  • Price: the currency picker accepts sats, USD, or EUR. For sats, enter an integer (e.g. 50000). For USD/EUR, enter the amount in dollars/euros. Keysat converts to BTC at invoice creation and the buyer pays the locked-in BTC amount.

The product is created with no policies attached. Next:

-

Step 7 — Define one or more policies

+

Step 7: Define one or more policies

Go to Policies → Create a new policy. Pick the product, then fill in:

    -
  • Slug — lowercase id (e.g. basic, pro, annual). Not "special" in any way; the buy page renders a tier picker when a product has two or more public policies, with the initial tier chosen by ?policy=<slug> in the URL, then by the policy you mark "most popular", then by cheapest.
  • -
  • Duration. Common choices: perpetual, 30 days (trial), 1 year. Recurring subscriptions are a separate toggle on the same form — flip "Recurring subscription" + set a renewal cadence and Keysat handles the cycle (invoice → settle → re-sign) automatically.
  • +
  • Slug: lowercase id (e.g. basic, pro, annual). Not "special" in any way; the buy page renders a tier picker when a product has two or more public policies, with the initial tier chosen by ?policy=<slug> in the URL, then by the policy you mark "most popular", then by cheapest.
  • +
  • Duration. Common choices: perpetual, 30 days (trial), 1 year. Recurring subscriptions are a separate toggle on the same form. Flip "Recurring subscription" + set a renewal cadence and Keysat handles the cycle (invoice → settle → re-sign) automatically.
  • Max devices. 1 for single-seat, 0 for unlimited.
  • -
  • Entitlements — pick from the product's catalog (you set up the catalog when you created the product on the previous step). The picked entitlements are baked into the signed license and your app reads them at verify time. Optionally toggle the "hide on buy page" eye icon on any entitlement to drop it from the tier card without un-granting it — useful for higher tiers that use "Everything in Basic, plus:" marketing copy.
  • -
  • Marketing bullets (optional) — operator-authored ✓ items rendered on the tier card alongside the entitlements. Pure marketing copy, not enforced.
  • +
  • Entitlements: pick from the product's catalog (you set up the catalog when you created the product on the previous step). The picked entitlements are baked into the signed license and your app reads them at verify time. Optionally toggle the "hide on buy page" eye icon on any entitlement to drop it from the tier card without un-granting it. Useful for higher tiers that use "Everything in Basic, plus:" marketing copy.
  • +
  • Marketing bullets (optional): operator-authored ✓ items rendered on the tier card alongside the entitlements. Pure marketing copy, not enforced.

If you're selling a multi-tier product (e.g. Basic / Pro / Max), repeat this step for each tier. Drag the cards in the Policies grid to set the order shown to buyers.

-

Step 8 — Share your purchase URL

+

Step 8: Share your purchase URL

Your public purchase URL is now live at:

https://<your-keysat-host>/buy/<product-slug>

Buyers hit it, see your product, click "Pay", and BTCPay’s checkout takes over. On payment confirmation, Keysat receives a webhook from BTCPay, signs a license, and emails it to the buyer (if they entered an email) and shows it on the receipt page.

-

Test it end-to-end by creating a free-license discount code and redeeming it — the same code path runs, just without the payment leg.

+

Test it end-to-end by creating a free-license discount code and redeeming it: the same code path runs, just without the payment leg.

What’s next

@@ -195,5 +193,6 @@ payment_methods: [BTC-OnChain, BTC-LightningNetwork] + diff --git a/integrate.html b/integrate.html index 39e3e6b..f3f95ad 100644 --- a/integrate.html +++ b/integrate.html @@ -3,7 +3,7 @@ -Keysat Docs — Integrate the SDK +Keysat Docs: Integrate the SDK @@ -33,8 +33,6 @@
Reference
Wire format - Admin API - SDKs
Project
@@ -52,18 +50,18 @@
Get started · Integrate the SDK

Integrate the SDK.

-

Wire Keysat licenses into your software in under an afternoon. The verifier is pure-function, offline, and ships in five lines. What you do with the result — refuse to start without a license, unlock specific features, just show a "supporter" badge — is your call. The SDK is the primitive; the business model is yours.

+

Wire Keysat licenses into your software in under an afternoon. The verifier is pure-function, offline, and ships in five lines. What you do with the result (refuse to start without a license, unlock specific features, just show a "supporter" badge) is your call. The SDK is the primitive; the business model is yours.

Prerequisites

Before you start, you should have:

    -
  • A Keysat installation running on your Start9 — see Install & setup.
  • -
  • BTCPay Server connected to Keysat — ditto.
  • +
  • A Keysat installation running on your Start9; see Install & setup.
  • +
  • BTCPay Server connected to Keysat; ditto.
  • At least one product defined in the admin UI.

Pick an SDK

-

Four official SDKs ship today. They are wire-compatible — a license issued by your Keysat verifies identically in any of them. Cross-check fixtures in the daemon repo prove each SDK accepts the same bytes the daemon mints.

+

Four official SDKs ship today. They are wire-compatible. A license issued by your Keysat verifies identically in any of them. Cross-check fixtures in the daemon repo prove each SDK accepts the same bytes the daemon mints.

@@ -88,11 +86,11 @@ poetry add keysat-licensing-client +// stdlib only: no third-party Go dependencies

If your language isn’t covered, see Wire format. The format is small and porting takes about an afternoon.

-

Step 1 — Embed your public key

+

Step 1: Embed your public key

In the admin UI, open Overview and copy the issuer public key from the "Embed your public key" card. (Or fetch it from GET /v1/issuer/public-key.) Paste it into your application’s source code as a compile-time constant.

const ISSUER_PEM = `-----BEGIN PUBLIC KEY-----
@@ -110,7 +108,7 @@ MCowBQYDK2VwAyEAmz7q8r4t1v…h3k2pXq9wL
       

Embed it. Don’t fetch it. The whole point of offline verification is that your software can’t be tricked by a network-level attacker. If you fetch the public key at runtime, you’re back to trusting a server.

-

Step 2 — Verify a license at startup

+

Step 2: Verify a license at startup

Read the user’s license key from wherever you store it (a file in their data directory, the OS keychain, an env var) and verify it on application start.

import { Verifier, PublicKey } from '@keysat/licensing-client';
@@ -167,7 +165,7 @@ result = verifier.verify(license_key_from_user)
       
     
 
-    

Step 3 — Handle errors gracefully

+

Step 3: Handle errors gracefully

Verification can fail for benign reasons (the user hasn’t pasted a license yet) or hostile ones (someone tampered with a license file). Distinguish them in your UX:

try {
@@ -198,7 +196,7 @@ result = verifier.verify(license_key_from_user)
     show_input_error()

Renewals & revocation

-

Keysat licenses are signed at issue time and do not phone home. If a license is revoked in the admin UI, the existing key continues to verify in your app — that’s the trade-off for offline.

+

Keysat licenses are signed at issue time and do not phone home. If a license is revoked in the admin UI, the existing key continues to verify in your app. That’s the trade-off for offline.

If you need revocation, ship a thin online check that runs on a cadence (e.g. once a week) against your Keysat’s revocation feed:

@@ -232,7 +230,7 @@ result = verifier.verify(license_key_from_user)
-

You decide the policy. Many indie developers ship no revocation at all. Once a key is sold, it stays valid — refunds happen offline via BTCPay. That’s perfectly reasonable.

+

You decide the policy. Many indie developers ship no revocation at all. Once a key is sold, it stays valid. Refunds happen offline via BTCPay. That’s perfectly reasonable.

Admin API

@@ -284,5 +282,6 @@ result = verifier.verify(license_key_from_user) b.addEventListener('click', () => setLang(b.dataset.lang)); }); + diff --git a/license.html b/license.html index a36a13f..7d16641 100644 --- a/license.html +++ b/license.html @@ -3,7 +3,7 @@ -Keysat Docs — License +Keysat Docs: License @@ -33,8 +33,11 @@
Reference
Wire format - Admin API - SDKs +
+
+
Project
+ Pricing + License
Operate
@@ -42,11 +45,6 @@ Migrate hardware Troubleshooting
-
-
Project
- Pricing - License -
@@ -79,28 +77,28 @@

Why source-available for the daemon?

Two reasons, both pragmatic:

    -
  1. The work has real cost. Building Keysat takes time. The source-available model lets the project be funded by operators on the Pro / Patron tiers who get value from a maintained, evolving daemon — without forcing every operator onto a paid tier.
  2. -
  3. The "AWS-hosts-our-open-source" failure mode. Fully open-source self-hosted projects routinely get strip-mined by cloud providers who host them as a managed service and capture the revenue. The daemon license forbids this specific pattern. Everything else — running your own instance, modifying it, auditing the code, selling licenses for your own products through it — is permitted.
  4. +
  5. The work has real cost. Building Keysat takes time. The source-available model lets the project be funded by operators on the Pro / Patron tiers who get value from a maintained, evolving daemon, without forcing every operator onto a paid tier.
  6. +
  7. The "AWS-hosts-our-open-source" failure mode. Fully open-source self-hosted projects routinely get strip-mined by cloud providers who host them as a managed service and capture the revenue. The daemon license forbids this specific pattern. Everything else is permitted: running your own instance, modifying it, auditing the code, selling licenses for your own products through it.

The SDKs are MIT because they sit inside your software. License compatibility there is critical and the MIT license is the modern default for libraries you embed.

What you can do (daemon)

  • Audit the source. Read every line; understand the cryptography, the storage, the API surface.
  • -
  • Run an instance on infrastructure you control. A Start9 box at home, a VPS, a cloud instance — anywhere you deploy it.
  • +
  • Run an instance on infrastructure you control. A Start9 box at home, a VPS, a cloud instance: anywhere you deploy it.
  • Modify it for your needs. Add features, change defaults, integrate it more deeply with your StartOS package. Modifications remain under the same license.
  • -
  • Operate it as your private licensing service to issue signed license keys for software products you sell or distribute. This is the intended use case — Keysat exists for this.
  • +
  • Operate it as your private licensing service to issue signed license keys for software products you sell or distribute. This is the intended use case. Keysat exists for this.
  • Maintain a public fork. Forks on GitHub are fine as long as they carry the license unchanged and don't enable any of the prohibited uses below.

What you can't do without prior permission (daemon)

  • Distribute compiled binaries to third parties. Including free of charge. The intent is that operators run Keysat themselves; they don't hand pre-built copies to others.
  • -
  • Provide Keysat as a hosted / managed service to third parties. "Keysat-as-a-Service" run by a cloud provider for a fee, or by anyone other than the operator using it for their own products, is the one pattern explicitly forbidden. Your own customers receiving signed license keys from your instance are not a hosted service — that's the daemon's intended use case.
  • +
  • Provide Keysat as a hosted / managed service to third parties. "Keysat-as-a-Service" run by a cloud provider for a fee, or by anyone other than the operator using it for their own products, is the one pattern explicitly forbidden. Your own customers receiving signed license keys from your instance are not a hosted service. That's the daemon's intended use case.
  • Sell, sublicense, lease, or rent the daemon software itself. Distinct from selling licenses through the daemon, which is allowed.
  • Remove copyright notices or this license text.
-

If you have a use case that crosses one of these lines — commercial redistribution, white-label deployment, a managed-service offering — email licensing@keysat.xyz. The license isn't designed to be a wall; it's designed to make commercial expansion an explicit conversation rather than an implicit one.

+

If you have a use case that crosses one of these lines (commercial redistribution, white-label deployment, a managed-service offering), email licensing@keysat.xyz. The license isn't designed to be a wall; it's designed to make commercial expansion an explicit conversation rather than an implicit one.

Contributions

By submitting code, documentation, designs, or other contributions to the upstream daemon repo, you grant Keysat a perpetual, worldwide, non-exclusive, royalty-free license to use, modify, relicense, and redistribute your contribution under the same license (or any later version). You retain ownership of your contribution; this is a license-back, not an assignment. The full text is in LICENSE Section 4.

@@ -109,7 +107,7 @@

The authoritative text lives at github.com/keysat-xyz/keysat/blob/main/LICENSE. This page is a plain-English summary; the LICENSE file is what governs in any conflict.

SDK licenses

-

Each SDK ships under the MIT License — included verbatim in the LICENSE file of each repo:

+

Each SDK ships under the MIT License, included verbatim in the LICENSE file of each repo:

-

You can use these in any software — open-source, closed-source, commercial, free, anything. The only obligation MIT imposes is preserving the copyright notice when you redistribute the SDK source itself.

+

You can use these in any software: open-source, closed-source, commercial, free, anything. The only obligation MIT imposes is preserving the copyright notice when you redistribute the SDK source itself.

Commercial inquiries

For commercial redistribution, resale, hosted-service rights, white-label deployment, or any other use not expressly granted by the source-available license: licensing@keysat.xyz.

@@ -136,5 +134,6 @@
+ diff --git a/operate.html b/operate.html index d071ffb..b0f0331 100644 --- a/operate.html +++ b/operate.html @@ -3,7 +3,7 @@ -Keysat Docs — Operate +Keysat Docs: Operate @@ -33,8 +33,6 @@
Reference
Wire format - Admin API - SDKs
Project
@@ -55,7 +53,7 @@

Backups, migration, recovery, and the things that go wrong. The "you didn’t expect to need this page until you needed it" page.

Backups

-

StartOS handles backups for you. By default, every service in your StartOS install is included in the same backup snapshot — you set the destination once (encrypted external drive, S3-compatible cloud, etc.) and StartOS schedules nightly snapshots.

+

StartOS handles backups for you. By default, every service in your StartOS install is included in the same backup snapshot. You set the destination once (encrypted external drive, S3-compatible cloud, etc.) and StartOS schedules nightly snapshots.

The Keysat backup payload is intentionally tiny. It contains:

@@ -82,20 +80,20 @@
  • On the new Start9, complete first-time setup with a fresh password. Don’t install any services yet.
  • StartOS → Settings → Backups → Restore. Point at the same destination. Pick the most recent snapshot.
  • StartOS restores all services in dependency order. Keysat will restore alongside BTCPay and Bitcoin Core. Bitcoin will need to re-sync if you’re using Bitcoin Core (consider utxo.live for assumeutxo to skip IBD).
  • -
  • Once Keysat is running on the new box, your purchase URLs change — the LAN/Tor hostnames are different. Update any links you’ve published.
  • +
  • Once Keysat is running on the new box, your purchase URLs change: the LAN/Tor hostnames are different. Update any links you’ve published.
  • The signing keypair restores along with the database, so all previously-issued licenses verify identically against the same public key. You don’t need to re-distribute the public key to your customers.

    Rotating the signing key

    -

    You generally don’t want to rotate the signing key — doing so invalidates every license you’ve ever issued. There is no admin-UI affordance for rotation today; the key is generated once on first start (and persisted to the server_keys SQLite table) and stays there for the life of the instance.

    +

    You generally don’t want to rotate the signing key. Doing so invalidates every license you’ve ever issued. There is no admin-UI affordance for rotation today; the key is generated once on first start (and persisted to the server_keys SQLite table) and stays there for the life of the instance.

    If you absolutely need to rotate (e.g. you suspect the keypair has leaked off the box):

    1. Stop Keysat.
    2. Drop the row in the server_keys table (or move the database aside entirely if you also want to start clean).
    3. -
    4. Restart Keysat — it will generate a fresh keypair on first run.
    5. +
    6. Restart Keysat. It will generate a fresh keypair on first run.
    7. Re-issue all active licenses to existing customers using the new key. The admin UI doesn’t support bulk re-issuance yet; this is a manual SQL + scripted-API exercise.
    8. Push a software update that swaps the embedded public key in your downstream apps.
    @@ -112,7 +110,7 @@

    BTCPay rejects the invoice request because the store has no configured wallet. Open BTCPay, find your store, and configure either an on-chain wallet or a Lightning node before retrying.

    Webhook deliveries failing

    -

    In the admin UI go to Webhooks — failed deliveries past the 10-attempt retry budget land in the "Failed" filter (the DLQ), with the response status and an inline "Retry" button. The audit log is a secondary source. Common causes:

    +

    In the admin UI go to Webhooks. Failed deliveries past the 10-attempt retry budget land in the "Failed" filter (the DLQ), with the response status and an inline "Retry" button. The audit log is a secondary source. Common causes:

    • Endpoint URL no longer reachable. Hit it manually with curl from your laptop to confirm.
    • Endpoint rejecting on signature mismatch. Verify your endpoint is HMAC-validating against the secret you registered with.
    • @@ -120,13 +118,13 @@

    "database is locked" errors in logs

    -

    Almost always a sign that two daemon instances are racing on the same SQLite file — usually because of a misconfigured supervisor. Confirm only one Keysat container is running. If you’re seeing this on a fresh install with no customizations, file a bug report against the package version you’re running.

    +

    Almost always a sign that two daemon instances are racing on the same SQLite file, usually because of a misconfigured supervisor. Confirm only one Keysat container is running. If you’re seeing this on a fresh install with no customizations, file a bug report against the package version you’re running.

    Licenses verifying as "expired" immediately after issue

    Clock skew. Either the issuing host or the verifying host has the wrong time. Run NTP. StartOS keeps your Start9 in sync automatically; the issue is usually on the verifier side (e.g. an air-gapped buyer machine).

    Reading the logs

    -

    Keysat logs to stdout, captured by StartOS. Tail them from the StartOS dashboard — Service page → Logs → Live tail.

    +

    Keysat logs to stdout, captured by StartOS. Tail them from the StartOS dashboard: Service page → Logs → Live tail.

    Useful log lines to grep for:

    @@ -163,5 +161,6 @@ + diff --git a/pricing.html b/pricing.html index 0669603..db77c61 100644 --- a/pricing.html +++ b/pricing.html @@ -3,10 +3,11 @@ -Keysat Docs — Pricing +Keysat Docs: Pricing