Files
Keysat 843ff0e5d7 Initial backup of root workspace files
Glue files not covered by subproject repos: top-level docs, logo,
keysat-design-system, and crosscheck tests. Subproject folders are
gitignored (each has its own Gitea remote).
2026-06-12 17:51:40 -05:00

12 KiB

How Keysat works

This is a plain-English walkthrough of what Keysat actually does, written for people who do not want to read code. It follows two people, Alice and Bob. Alice has written a piece of software she wants to sell. Bob wants to buy it.

The cast

Alice runs her software on her own Start9 server at home. She has two services there: her BTCPay Server (a self-hosted Bitcoin payment processor), and a new one she just installed called licensing-service — the thing this whole project is about. Her licensing-service is single-tenant: it sells her software only, not anyone else's. If a fellow developer wants the same functionality, they install their own copy on their own Start9, just like Alice did.

Bob is a customer. He may or may not own a Start9. Whether he does affects the experience, not the mechanics — we'll get to that.

What a license actually is

At the center of this system is a short string of characters. It looks like this:

LIC1-AEAW6RVE6YGS6SRIW2VD5D57N4UPA...-FV56FI7ZTB5GIFQHIPQ35QVVE5AO5...

That string is the whole license. There is nothing else — no account, no login, no subscription server to reach. Think of it as a cryptographically signed receipt. Anyone who has Alice's public signing key (which she publishes openly) can look at the string and tell, with mathematical certainty, three things: who signed it, what it licenses, and whether anyone has tampered with it. The underlying cryptography is Ed25519, the same family used by SSH, GitHub commits, and Tor.

This matters because it means two things can happen without any server being online. First, Bob's computer can verify the key is real at every startup without phoning home — Alice does not need to run anything on the day Bob wants to use her software. Second, if Alice gets hit by a bus, every customer's copy keeps working forever.

Alice's day-1 setup

Alice installs licensing-service on her Start9 from the marketplace. On first boot it generates its own signing keypair, and SQLite database, and gives her a random admin API key through a StartOS action called "Show admin credentials." She copies that API key into her password manager.

Then she clicks "Connect BTCPay." This is the part everybody expected to be painful and isn't. Instead of asking her to go to BTCPay, generate an API key with the right permissions, register a webhook, copy a webhook secret, and paste all of those things back into licensing-service — which is the flow every BTCPay integration has historically demanded — the action returns a single URL. Alice opens it. BTCPay shows her a consent page listing exactly the permissions being requested. She clicks "Authorize." BTCPay sends the approval straight back to licensing-service, which automatically detects her store, picks a webhook secret out of a hat, and registers the webhook on her behalf. Zero pasting. This works because BTCPay's own authorize endpoint (/api-keys/authorize) was designed for OAuth-style handshakes and almost no integration uses it.

Last step: Alice creates a product. She picks a slug, like bitcoin-ticker-pro, and a price in satoshis. Now she can sell.

Bob buys a license

The buying experience depends on what Bob is running.

Bob has a Start9 and Alice's software is a Start9 package

This is the nicest path. Bob installs Alice's package from whatever marketplace he uses. When it comes up, the package dashboard shows a "License" section with four actions: Buy license, Finish license purchase, Check license status, Deactivate license. These actions come from the drop-in template in activate-license-template/, which Alice added to her package when she built it.

Bob clicks "Buy license." His Start9 talks to Alice's licensing-service, which opens a BTCPay invoice and returns a checkout URL. Bob opens that URL, pays with Lightning (or on-chain if he prefers), and closes the tab. A minute later he clicks "Finish license purchase." His Start9 polls Alice's service, sees the payment settled, pulls down the license key, verifies its signature locally, and stores it in his package's own database. Next time Alice's software boots on Bob's machine, it picks up the key from the local store and launches.

Nothing was copied or pasted. Bob never saw the license string. The key got captured as a side effect of payment.

Bob is on a regular computer, and Alice's software is not a Start9 package

This is the more common case for today's world. Bob goes to Alice's website, clicks "Buy," and is taken to a BTCPay checkout. He pays. BTCPay shows a success page that includes the license string — at that point it's the only way he could get it, short of an email. Bob copies the string and pastes it into Alice's app's settings dialog. Alice's app (which has embedded her public key at build time) verifies the signature and unlocks. From then on, no network is required.

Bob never paid — Alice gave him a comp

Alice's licensing-service has an admin action called "Issue license manually," which hands her a signed license for free, with an optional note field ("comped for friend," "press reviewer," and so on). Useful for reviewers, test machines, friends, and early-access customers.

What stops Bob from sharing the key with a friend

This is where the design has to be honest. No licensing system reliably prevents sharing. Determined people will always figure something out. What good licensing does is make casual sharing annoying enough that it doesn't happen often, while keeping the honest customer's experience painless. We have two tools.

The first is fingerprint binding. When Alice's app first runs, it collects a stable identifier for the machine — something like the machine-id on Linux, or a hash of hardware specs on other platforms. It sends that fingerprint to Alice's licensing-service the first time it validates a key online. The service stores the fingerprint alongside the license row. Trust-on-first-use, like SSH. The key can't be moved to a second machine and work online — when the second machine checks in, the service notices a mismatch and returns "rejected." The second machine also can't pretend to be the first, because the fingerprint is hashed on-device and that hash gets baked into the signed part of the key at issuance. Forging it would require Alice's private signing key, which lives only on Alice's Start9.

The gap in that story: if Bob's friend never goes online, nothing stops him. The signed key still verifies, and signature verification is all that runs at boot. A motivated offline-only user can share keys. In practice this is a small fraction of the market, and the design treats it as acceptable collateral for never holding legitimate customers hostage when their internet drops.

The second tool is revocation. If Alice learns a specific key has been leaked — she sees it posted on a forum, or a customer asks for a refund — she clicks "Revoke license" on her admin dashboard and marks the key as dead. The next time anyone using that key does an online check, they get rejected. Offline users running on stolen keys are unaffected, but the key becomes unsellable going forward.

These two tools, used together, make key sharing an unpleasant experience for the sharer (fingerprint mismatches), put a ceiling on the damage a leaked key can do (revocation), and leave legitimate users completely unaffected. That is the best the industry has, and it's what reputable commercial licensing systems like FastSpring's or Paddle's do too.

What if Alice's server is offline

The offline path is the default path. When Bob's copy of Alice's app starts up, it runs the signature check against Alice's public key, which is embedded at build time, and if the signature is valid the app launches. The licensing-service does not need to be up. Alice's Start9 can be powered off for a year and no legitimate customer is inconvenienced.

The online path is layered on top as an opportunistic enhancement: every so often, when the network is up, the app calls Alice's licensing-service's /v1/validate endpoint. That endpoint applies revocation and fingerprint binding. If the service is unreachable, nothing breaks — the app treats that as "can't tell" and continues on the last known offline-good status.

This is the correct tradeoff for paid software that has already been paid for. If the licensing-service is a hard dependency, Alice's going offline for a weekend locks every customer out, which is the failure mode that gives online-licensing its bad reputation.

What if Bob wipes his machine

Two cases.

If Bob has a Start9, his Start9 backup includes his package's store, which includes his license key. Restoring the backup restores the license. He does not need to contact Alice. Keys are, in effect, Start9-backup-compatible.

If Bob is on a regular computer, he kept the license key string somewhere — ideally in his password manager, since it is functionally a password. He pastes it back into Alice's app. Done.

If Bob lost the key entirely, Alice can look him up in her licensing-service admin by invoice id or email and reissue (or he can buy again, depending on Alice's policy). The service keeps every license in its database indefinitely, so "I lost my key" is a five-second fix from Alice's end.

What prevents Alice from being ripped off at the payment layer

BTCPay is a real non-custodial payment processor. When Bob pays, the bitcoin goes straight to Alice's wallet (an xpub or a Lightning node she controls), not to a middleman. BTCPay then notifies licensing-service via a signed webhook that a specific invoice settled. The signed-webhook part matters — without a signature, a malicious actor could fire fake "invoice settled" events and trick licensing-service into issuing free keys. The webhook secret, which was generated automatically during the authorize flow and is never exposed to Alice or any HTTP client, is the shared signing key. Every incoming webhook is rejected if its HMAC-SHA256 signature doesn't verify in constant time.

Webhook deliveries are also not the only signal. A background task inside licensing-service, every 60 seconds, lists every invoice that is still in a pending state and polls BTCPay directly for its current status. If BTCPay says "settled" but the service didn't see a webhook for it (maybe BTCPay's webhook delivery dropped, or the service was down), the background task catches up and issues the license. This closes the window where a customer has paid but the licensing-service missed the event.

What this setup is not

Not a subscription service. A license here is perpetual — one payment, one key, forever. If Alice wants subscriptions she would need to add expiry to the payload and a renewal flow; the payload format has a flags byte and a future version bump to support that, but it is not in scope for the current version.

Not a DRM system. It does not prevent someone with a debugger from patching out the license check. Nothing running on the user's machine can, by construction — the user controls their CPU. This is a licensing system for reasonable people who want to pay.

Not multi-tenant. Each Alice runs her own licensing-service. Two Alices cannot share one server to sell two different products. That simplification is deliberate — it keeps the data model small, makes disaster recovery obvious ("back up one SQLite file"), and keeps Alice in control of the signing key. If someone wants a SaaS version that hosts many sellers, that's a different product.

The bet

The bet of this whole project is that once the rough edges above are smooth — one-click BTCPay, drop-in Start9 actions, offline verification by default, proper webhook signing, automatic reconciliation — a developer looking to monetize their Start9 package will choose to install this licensing-service instead of writing their own. Not because it's unique, but because it is finished. The competition is other developers writing their own from scratch, and the first-mover advantage is that every hour they spend building theirs is an hour Alice has already spent on her product.