The Go SDK's online client previously only exposed Validate +
PublicKey. This adds the purchase-side surface so Go consumers
have the same capabilities as the other three language clients:
- StartPurchaseOptions struct (BuyerEmail, BuyerNote, RedirectURL,
Code, PolicySlug). Zero-valued fields are omitted from the
JSON request body.
- Client.StartPurchase(ctx, productSlug, opts) → PurchaseSession
with InvoiceID, BTCPayInvoiceID, CheckoutURL, AmountSats,
PollURL.
- Client.ListPublicPolicies(ctx, productSlug) →
PublicPoliciesResponse for rendering an in-app tier picker.
Public endpoint, no auth.
session, err := client.StartPurchase(ctx, "recap",
keysat.StartPurchaseOptions{
PolicySlug: "pro",
BuyerEmail: "buyer@example.com",
RedirectURL: "https://recap.app/thank-you",
})
// open session.CheckoutURL in the buyer's browser
tiers, err := client.ListPublicPolicies(ctx, "recap")
for _, p := range tiers.Policies {
fmt.Println(p.Slug, p.Name, p.PriceSats, p.Entitlements)
}
Build + existing crosscheck tests pass clean.
keysat-client-go
Go SDK for Keysat — a self-hosted, Bitcoin-paid software licensing service.
Verifies LIC1-format license keys offline against an Ed25519 public key, and optionally validates them online against a running Keysat daemon.
Install
go get github.com/keysat-xyz/keysat-client-go
Stdlib only — no third-party dependencies.
Offline verification
package main
import (
"fmt"
"log"
"time"
"github.com/keysat-xyz/keysat-client-go"
)
// Embed the daemon's PEM public key at build time. Get it from your
// Keysat admin UI or `curl https://your-keysat.example/v1/pubkey`.
const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----`
func main() {
pub, err := keysat.LoadPublicKeyPEM(publicKeyPEM)
if err != nil { log.Fatal(err) }
licenseKey := readKeyFromUserConfig() // however your app stores it
payload, err := keysat.ParseAndVerify(licenseKey, pub)
if err != nil { log.Fatalf("license invalid: %v", err) }
if payload.IsExpiredAt(time.Now().Unix()) {
log.Fatal("license expired")
}
if !payload.HasEntitlement("pro") {
log.Fatal("license does not include 'pro' tier")
}
fmt.Println("license OK")
}
Online validation (revocation, fingerprint binding, machine cap)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c := keysat.NewClient("https://licensing.example.com", nil)
resp, err := c.Validate(ctx, keysat.ValidateRequest{
Key: licenseKey,
ProductSlug: "myapp",
Fingerprint: machineUUID,
})
if err != nil { log.Fatalf("daemon unreachable: %v", err) }
if !resp.OK {
log.Fatalf("license rejected: %s", resp.Reason)
}
Validate returns HTTP 200 in all cases; license failures are conveyed via resp.OK + resp.Reason (bad_signature, revoked, expired, too_many_machines, etc.).
Fingerprint binding
When a key is fingerprint-bound, the daemon's first successful online validation pins the machine's fingerprint hash to the license row. Subsequent validations from a different machine fail with fingerprint_mismatch.
The SDK exposes keysat.HashFingerprint if you need to compute the hash yourself (e.g., to compare against a key's FingerprintHash field offline):
h := keysat.HashFingerprint(machineUUID)
if h != payload.FingerprintHash {
log.Fatal("license does not belong to this machine")
}
Wire format compatibility
Every SDK + the daemon agree on the LIC1 wire format. Crosscheck tests in this package run against the shared tests/crosscheck/vector.json (alongside the daemon repo) — three independently-signed fixtures (v1 legacy, v2 trial with entitlements, v2 perpetual unbound) parse to the same field values across Rust, TypeScript, Python, and Go.
When fetched standalone via go get, the crosscheck test skips gracefully (the vector file isn't bundled into the Go module). The crosscheck only runs from the parent licensing/ workspace.
API stability
This SDK is alpha alongside Keysat v0.1.0. The wire format itself is stable and won't break compatibility — license keys issued by any v0.1 daemon will keep parsing in any future SDK. The Go API surface (function names, struct fields) may settle further before v1.0; nothing here is wildly out of line with idiomatic Go but expect minor tweaks.
License
MIT — see LICENSE.