Landing pricing: delete sovereign section, dynamic tier cards, FAQ fix
Pricing section overhaul: - Delete the "Sovereign by default / Everything stays on your hardware" section (left + right panels weren't really telling one story, and the lede repeats what the hero already says). - Tier intro: drop the dense self-license framing; keep just "Every Keysat instance ships at a highly functional Creator tier; pay only when you outgrow it." - Drop the "No fake feature gates" trailing blurb. - FAQ tier-difference answer: reflect Patron as a perpetual license + direct 1:1 support (not just "Pro with a badge"). Tier cards now render LIVE from the master Keysat: - On page load, fetches /v1/products/keysat (for the entitlements catalog) and /v1/products/keysat/policies (for the tier list), then re-renders #tier-grid-live with the operator-configured data — prices, marketing bullets, hidden-entitlement filter, and any active featured (launch-special) discount applied. - Matches the buy page's visual treatment: diagonal LAUNCH SPECIAL ribbon, struck-through original price, discounted headline price, "Limited: N of M remaining" meta line. Single source of truth between landing and /buy. - Static markup stays as a graceful fallback when the fetch fails (offline, daemon down, CORS hiccup). The script only mutates the DOM on a successful response from both endpoints.
This commit is contained in:
+200
-41
@@ -334,6 +334,32 @@ pre.code .p { color:rgba(245,241,232,0.55); }
|
||||
content:'\2713'; position:absolute; left:0; top:0;
|
||||
color:var(--gold-700); font-weight:700;
|
||||
}
|
||||
/* Launch-special discount treatment on the live tier cards. Matches
|
||||
the buy page's diagonal ribbon + slashed-price visual so the
|
||||
landing page and the buy page tell the same pricing story. */
|
||||
.tier-card.has-launch {
|
||||
position:relative;
|
||||
clip-path:polygon(0 -20px, 100% -20px, 100% 100%, 0 100%);
|
||||
}
|
||||
.tier-launch-ribbon {
|
||||
position:absolute; top:14px; right:-44px;
|
||||
background:var(--gold-500); color:var(--navy-950);
|
||||
font-family:var(--font-display); font-weight:700; font-size:10.5px;
|
||||
letter-spacing:0.14em; text-transform:uppercase;
|
||||
padding:4px 50px; transform:rotate(35deg);
|
||||
box-shadow:0 2px 6px rgba(14,31,51,0.15);
|
||||
z-index:2; pointer-events:none;
|
||||
}
|
||||
.tier-launch-meta {
|
||||
font-size:11.5px; color:var(--gold-700); font-weight:600;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.tier-price-original {
|
||||
font-family:var(--font-display); font-weight:500; font-size:14px;
|
||||
color:var(--ink-500); margin:0 0 4px;
|
||||
text-decoration:line-through; text-decoration-color:rgba(14,31,51,0.4);
|
||||
}
|
||||
.tier-card .tier-features-list { margin-top:4px; }
|
||||
|
||||
/* ---------- Install ---------- */
|
||||
.install-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; }
|
||||
@@ -859,47 +885,23 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block" id="sovereign">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Sovereign by default</span>
|
||||
<h2>Everything stays on your hardware.</h2>
|
||||
<p>Migrate Start9 boxes — Keysat goes with you. If the Keysat project disappears tomorrow, your already-issued licenses keep verifying: the public key is embedded in your software, the private key is on your machine.</p>
|
||||
</div>
|
||||
<div class="sov">
|
||||
<div class="panel">
|
||||
<h3>What you keep</h3>
|
||||
<div class="sub">On your Start9, in your normal backups.</div>
|
||||
<ul>
|
||||
<li>Signing keypair</li>
|
||||
<li>Customer email · npub list</li>
|
||||
<li>Sale records</li>
|
||||
<li>Audit log</li>
|
||||
<li>BTCPay invoice history</li>
|
||||
<li>Webhook subscribers</li>
|
||||
<li>Bitcoin (your wallet)</li>
|
||||
</ul>
|
||||
<p class="footnote">Backed up automatically by StartOS as part of your normal backup routine.</p>
|
||||
</div>
|
||||
<div class="panel dark">
|
||||
<h3>Or accept fiat, on your terms</h3>
|
||||
<div class="sub">Coming soon: opt-in card payments via Zaprite.</div>
|
||||
<p style="font-size:14px; line-height:1.55; color:rgba(245,241,232,0.85); margin:0 0 16px;">If your customers prefer paying with credit cards over Bitcoin, you’ll be able to plug Zaprite into Keysat as an alternative payment provider. Same Keysat license-issuance flow, but the payment can come through Stripe-via-Zaprite for cards or any of Zaprite’s Bitcoin rails (BTCPay, Strike, Unchained). Trades off some sovereignty — cards mean Stripe KYC and customer PII flowing through Zaprite — in exchange for a much wider addressable audience.</p>
|
||||
<p style="font-size:14px; line-height:1.55; color:rgba(245,241,232,0.85); margin:0 0 16px;">Keysat stays sovereign-by-default. Card payments are something you opt into per Keysat install if your business needs them. Shipping in v0.3.</p>
|
||||
<p class="footnote">Source-available license · pay your operator-style trade-offs deliberately, never by default.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block" id="tiers">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Pricing</span>
|
||||
<h2>Three tiers. Free forever for solo creators.</h2>
|
||||
<p>Keysat’s gating happens at the operator’s self-license, not at the buyer’s. Every Keysat instance ships at Creator (free); pay only when you outgrow it.</p>
|
||||
<p>Every Keysat instance ships at a highly functional Creator tier; pay only when you outgrow it.</p>
|
||||
</div>
|
||||
<div class="tier-grid">
|
||||
<!--
|
||||
Tier cards. Static markup is the FALLBACK shape (rendered on
|
||||
every page load, kept conservative so it stays correct even when
|
||||
offline). On load, `loadLiveTiers()` below fetches the master
|
||||
Keysat's product + policies and re-renders this grid with the
|
||||
live prices, marketing bullets, hidden-entitlement filtering, and
|
||||
any active launch-special featured discount applied — matching
|
||||
what the buy page renders.
|
||||
-->
|
||||
<div class="tier-grid" id="tier-grid-live">
|
||||
<div class="tier-card">
|
||||
<div class="tier-cap">Creator</div>
|
||||
<div class="tier-price"><span class="price-num">Free</span><span class="price-sub">forever</span></div>
|
||||
@@ -928,19 +930,17 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
|
||||
<div class="tier-card">
|
||||
<div class="tier-cap">Patron</div>
|
||||
<div class="tier-price"><span class="price-num">500k sats</span><span class="price-sub">/ year</span></div>
|
||||
<p class="tier-pitch">Honest supporter tier. Same features as Pro; you’re funding Keysat’s development.</p>
|
||||
<p class="tier-pitch">Perpetual license + direct one-on-one support, for creators who want Keysat to keep getting better.</p>
|
||||
<ul>
|
||||
<li>Everything in Pro</li>
|
||||
<li>Perpetual license (one-time, never renews)</li>
|
||||
<li>Direct one-on-one support</li>
|
||||
<li>“Patron” badge in your admin dashboard</li>
|
||||
<li>Listed on the Patrons page on keysat.xyz</li>
|
||||
<li>Early access to release-candidate builds</li>
|
||||
<li>Direct support line</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top:28px; text-align:center; font-size:13.5px; color:var(--ink-500);">
|
||||
No fake feature gates: Patron is feature-identical to Pro. Pricing is a flat annual fee — we don’t take a percentage of your sales.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -995,7 +995,7 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
|
||||
</div>
|
||||
<div>
|
||||
<h3>What’s the difference between the three tiers?</h3>
|
||||
<p>Creator is free forever, capped at 5 products / 5 policies per product / 10 active discount codes — plenty for a solo creator selling one-time or perpetual licenses. Pro lifts every cap and unlocks recurring subscriptions plus the Zaprite payment gateway (cards, Apple Pay, bank transfers, in addition to Bitcoin). Patron is Pro with a public supporter badge; same features, you’re funding development. See the tier comparison above for the full breakdown.</p>
|
||||
<p>Creator is free forever, capped at 5 products / 5 policies per product / 10 active discount codes — plenty for a solo creator selling one-time or perpetual licenses. Pro lifts every cap and unlocks recurring subscriptions plus the Zaprite payment gateway (cards, Apple Pay, bank transfers, in addition to Bitcoin), billed annually. Patron is everything in Pro, sold as a one-time perpetual license instead of an annual subscription, plus direct one-on-one support and a public supporter badge — you’re funding development. See the tier cards above for the full breakdown.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Do I have to use Bitcoin?</h3>
|
||||
@@ -1123,5 +1123,164 @@ fmt.<span class="f">Printf</span>(<span class="s">"licensed for %s, expires %s\n
|
||||
start();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Live tier-card render. Fetches the master Keysat's product + policies
|
||||
on page load and re-renders the #tier-grid-live grid with whatever
|
||||
the operator configured — prices, marketing bullets, hidden
|
||||
entitlements, and any active featured (launch-special) discount.
|
||||
Mirrors the buy page's tier-picker treatment so the landing page
|
||||
and /buy/keysat tell exactly the same pricing story.
|
||||
|
||||
Failure mode: if the fetch errors (offline visitor, daemon down,
|
||||
CORS hiccup), the static fallback grid renders as it does today.
|
||||
This script only mutates the DOM on success.
|
||||
-->
|
||||
<script>
|
||||
(function () {
|
||||
const KEYSAT_API = 'https://licensing.keysat.xyz';
|
||||
const PRODUCT_SLUG = 'keysat';
|
||||
|
||||
const fmtSats = (n) => {
|
||||
if (n === 0) return 'Free';
|
||||
if (n >= 1_000_000 && n % 100_000 === 0) return (n / 1_000_000) + 'M sats';
|
||||
if (n >= 1_000 && n % 1_000 === 0) return (n / 1_000) + 'k sats';
|
||||
return Number(n).toLocaleString('en-US') + ' sats';
|
||||
};
|
||||
const cadenceSuffix = (p) => {
|
||||
if (!p.is_recurring) return '';
|
||||
const d = p.renewal_period_days || 0;
|
||||
if (d === 7) return '/ wk';
|
||||
if (d === 30) return '/ mo';
|
||||
if (d === 90) return '/ qtr';
|
||||
if (d === 180) return '/ 6mo';
|
||||
if (d === 365) return '/ year';
|
||||
if (d > 0) return '/ ' + d + 'd';
|
||||
return '';
|
||||
};
|
||||
const discountLabel = (fd) => {
|
||||
if (!fd) return '';
|
||||
if (fd.kind === 'percent') return Math.round(fd.amount / 100) + '% OFF';
|
||||
if (fd.kind === 'free_license') return 'FREE';
|
||||
if (fd.kind === 'set_price') return 'LIMITED PRICE';
|
||||
return 'LAUNCH SPECIAL';
|
||||
};
|
||||
|
||||
function renderCard(pol, catalog) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'tier-card' + (pol.highlighted ? ' featured' : '');
|
||||
const fd = pol.featured_discount;
|
||||
if (fd) card.classList.add('has-launch');
|
||||
|
||||
const cap = document.createElement('div');
|
||||
cap.className = 'tier-cap' + (pol.highlighted ? ' gold' : '');
|
||||
cap.textContent = pol.name;
|
||||
card.appendChild(cap);
|
||||
|
||||
if (fd) {
|
||||
const ribbon = document.createElement('div');
|
||||
ribbon.className = 'tier-launch-ribbon';
|
||||
ribbon.textContent = discountLabel(fd);
|
||||
card.appendChild(ribbon);
|
||||
|
||||
const remaining = fd.remaining_uses;
|
||||
if (typeof remaining === 'number' && remaining > 0) {
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'tier-launch-meta';
|
||||
meta.textContent = 'Limited: ' + remaining + ' of ' + (fd.max_uses || 0) + ' remaining';
|
||||
card.appendChild(meta);
|
||||
}
|
||||
const orig = document.createElement('div');
|
||||
orig.className = 'tier-price-original';
|
||||
orig.textContent = fmtSats(pol.price_sats);
|
||||
card.appendChild(orig);
|
||||
}
|
||||
|
||||
// Headline price. Free policies just say "Free"; paid policies
|
||||
// show the discounted price when a featured discount applies.
|
||||
const priceWrap = document.createElement('div');
|
||||
priceWrap.className = 'tier-price';
|
||||
const priceNum = document.createElement('span');
|
||||
priceNum.className = 'price-num';
|
||||
const priceSub = document.createElement('span');
|
||||
priceSub.className = 'price-sub';
|
||||
if (pol.price_sats === 0) {
|
||||
priceNum.textContent = 'Free';
|
||||
priceSub.textContent = 'forever';
|
||||
} else {
|
||||
const effective = fd ? fd.discounted_price_sats : pol.price_sats;
|
||||
priceNum.textContent = fmtSats(effective);
|
||||
const suffix = cadenceSuffix(pol);
|
||||
priceSub.textContent = pol.is_recurring && suffix ? suffix : '';
|
||||
if (!pol.is_recurring) priceSub.textContent = '';
|
||||
}
|
||||
priceWrap.appendChild(priceNum);
|
||||
if (priceSub.textContent) priceWrap.appendChild(priceSub);
|
||||
card.appendChild(priceWrap);
|
||||
|
||||
if (pol.description) {
|
||||
const pitch = document.createElement('p');
|
||||
pitch.className = 'tier-pitch';
|
||||
pitch.textContent = pol.description;
|
||||
card.appendChild(pitch);
|
||||
}
|
||||
|
||||
// Merge marketing bullets + (visible) entitlements into a single
|
||||
// <ul> in the operator-controlled order. Identical pattern to the
|
||||
// buy page's tier-features list.
|
||||
const hidden = new Set(pol.hidden_entitlements || []);
|
||||
const entItems = (pol.entitlements || [])
|
||||
.filter((slug) => !hidden.has(slug))
|
||||
.map((slug) => {
|
||||
const entry = catalog[slug];
|
||||
return entry && entry.name ? entry.name : slug;
|
||||
});
|
||||
const bullets = pol.marketing_bullets || [];
|
||||
const order = (pol.marketing_bullets_position === 'below')
|
||||
? entItems.concat(bullets)
|
||||
: bullets.concat(entItems);
|
||||
if (order.length > 0) {
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'tier-features-list';
|
||||
order.forEach((text) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = text;
|
||||
list.appendChild(li);
|
||||
});
|
||||
card.appendChild(list);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
async function loadLiveTiers() {
|
||||
const grid = document.getElementById('tier-grid-live');
|
||||
if (!grid) return;
|
||||
try {
|
||||
const [productR, policiesR] = await Promise.all([
|
||||
fetch(KEYSAT_API + '/v1/products/' + PRODUCT_SLUG, { cache: 'no-cache' }),
|
||||
fetch(KEYSAT_API + '/v1/products/' + PRODUCT_SLUG + '/policies', { cache: 'no-cache' }),
|
||||
]);
|
||||
if (!productR.ok || !policiesR.ok) return; // leave static fallback
|
||||
const productJ = await productR.json();
|
||||
const policiesJ = await policiesR.json();
|
||||
const product = productJ.product || productJ;
|
||||
const catalog = {};
|
||||
(product.entitlements_catalog || []).forEach((e) => { catalog[e.slug] = e; });
|
||||
const policies = (policiesJ.policies || []).filter((p) => !p.is_trial);
|
||||
if (policies.length === 0) return;
|
||||
// Empty the grid + render the live cards.
|
||||
while (grid.firstChild) grid.removeChild(grid.firstChild);
|
||||
policies.forEach((p) => grid.appendChild(renderCard(p, catalog)));
|
||||
} catch (_) {
|
||||
// Silent fallback. The static cards stay rendered.
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadLiveTiers);
|
||||
} else {
|
||||
loadLiveTiers();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user