Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github: 0xNyk
custom: ["https://0xnyk.gumroad.com/l/dictx"]
custom: ["https://dictx.splitlabs.io/buy"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<p align="center">
<a href="https://github.com/0xNyk/dictx/releases"><img src="https://img.shields.io/github/v/release/0xNyk/dictx?label=Download&style=for-the-badge&color=4f46e5" alt="Download" /></a>
&nbsp;
<a href="https://0xnyk.gumroad.com/l/dictx"><img src="https://img.shields.io/badge/Get_Dictx_Pro-$29-10b981?style=for-the-badge" alt="Get Dictx Pro" /></a>
<a href="https://dictx.splitlabs.io/buy"><img src="https://img.shields.io/badge/Get_Dictx_Pro-$29-10b981?style=for-the-badge" alt="Get Dictx Pro" /></a>
</p>

<p align="center">
Expand All @@ -28,7 +28,7 @@

---

> **Free and open source.** Dictx is GPL-3.0 licensed — you can build it from source with full functionality. [Buy Dictx Pro](https://0xnyk.gumroad.com/l/dictx) ($29 one-time) for a signed binary, auto-updates, and to support development.
> **Free and open source.** Dictx is GPL-3.0 licensed — you can build it from source with full functionality. [Buy Dictx Pro](https://dictx.splitlabs.io/buy) ($29 one-time) for a signed binary, auto-updates, and to support development.

Dictx is a cross-platform desktop application for speech transcription. Press a shortcut, speak, and your words appear in any text field — no cloud, no API keys, no data leaving your computer.

Expand Down
86 changes: 86 additions & 0 deletions docs/commercial/checkout-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Dictx Commerce Migration (Gumroad -> Professional Checkout)

This runbook moves Dictx Pro sales to `https://dictx.splitlabs.io/buy` while keeping the OSS/free path unchanged.

## Scope

- Product: Dictx Pro (signed binaries + auto-updates)
- Price: `$29` one-time
- Free version: unchanged (GPL source build)

## 1) Domain + Checkout

1. Create `dictx.splitlabs.io` as your Vercel custom domain and serve the landing site there.
2. Configure branded checkout:

- Product name: `Dictx Pro`
- Offer copy: `Signed binaries, auto-updates, and direct support`
- Price: `USD 29 one-time`

3. Enable customer portal for:

- Receipts/invoices
- Download access
- Billing/profile management

## 2) License + Entitlements

Define one entitlement key:

- `dictx_pro`

Entitlement grants:

- Access to release downloads
- Auto-update eligibility
- Priority support queue (if enabled)

## 3) Webhook Processing

Use a webhook endpoint to sync purchases to your entitlement store.

Events to handle:

- `order.paid` (grant entitlement)
- `subscription.active` (if you add annual support plans)
- `order.refunded` or `subscription.canceled` (revoke entitlement)

Implementation reference:

- [scripts/commerce/polar-webhook-example.ts](/Users/nyk/repos/dictx/scripts/commerce/polar-webhook-example.ts)

## 4) App + Repo Link Updates

Completed in this repo:

- `README` Pro links now point to `https://dictx.splitlabs.io/buy`
- In-app CTA links point to a shared `PRO_PURCHASE_URL`
- GitHub funding link points to `https://dictx.splitlabs.io/buy`

## 5) Migration Messaging

1. Send announcement to existing buyers.
2. Publish FAQ with key points:

- Existing licenses remain honored
- New purchases go through `https://dictx.splitlabs.io/buy`
- Support contact stays unchanged

3. Use template:

- [customer-migration-email.md](/Users/nyk/repos/dictx/docs/commercial/customer-migration-email.md)

## 6) Validation Checklist

Before launch:

- Checkout success flow creates receipt + customer record
- Webhook signature validation works in production
- `dictx_pro` entitlement is granted/revoked correctly
- Customer portal access works from receipt email
- Purchase links from app + README resolve to `https://dictx.splitlabs.io/buy`

After launch:

- Track conversion rate from in-app CTA
- Track support tickets tagged `billing` and `license`
20 changes: 20 additions & 0 deletions docs/commercial/customer-migration-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Subject: Dictx Pro checkout is now at dictx.splitlabs.io/buy

Hi,

We upgraded Dictx Pro purchasing to a new hosted checkout at:
https://dictx.splitlabs.io/buy

What this means:

- New purchases now happen on our new checkout page
- Existing customers keep access to Dictx Pro
- Dictx remains open source (GPL) and free to build from source

If you need help with receipts, downloads, or license access, reply to this email and include:

- Purchase email
- Approximate purchase date
- Platform (macOS / Windows / Linux)

Thanks for supporting Dictx.
115 changes: 115 additions & 0 deletions scripts/commerce/polar-webhook-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { createHmac, timingSafeEqual } from "node:crypto";

type PolarEventData = {
id?: string | number;
product_id?: string | number;
customer_email?: string;
metadata?: Record<string, string>;
};

type PolarWebhookEvent = {
type: string;
data: PolarEventData;
};

type EntitlementChange = {
customerEmail: string;
entitlement: "dictx_pro";
active: boolean;
sourceEvent: string;
};

const WEBHOOK_SECRET = process.env.POLAR_WEBHOOK_SECRET ?? "";
const SIGNATURE_HEADER =
process.env.POLAR_SIGNATURE_HEADER ?? "polar-signature";
const PORT = Number.parseInt(process.env.PORT ?? "8787", 10);

const verifySignature = (payload: string, signatureHeader: string): boolean => {
if (!WEBHOOK_SECRET || !signatureHeader) return false;

const computed = createHmac("sha256", WEBHOOK_SECRET)
.update(payload, "utf8")
.digest("hex");

const provided = signatureHeader.trim();
if (computed.length !== provided.length) return false;

return timingSafeEqual(Buffer.from(computed), Buffer.from(provided));
};

const toEntitlementChange = (
event: PolarWebhookEvent,
): EntitlementChange | null => {
const customerEmail =
event.data.customer_email ?? event.data.metadata?.customer_email ?? "";
if (!customerEmail) return null;

if (event.type === "order.paid") {
return {
customerEmail,
entitlement: "dictx_pro",
active: true,
sourceEvent: event.type,
};
}

if (
event.type === "order.refunded" ||
event.type === "subscription.canceled"
) {
return {
customerEmail,
entitlement: "dictx_pro",
active: false,
sourceEvent: event.type,
};
}

return null;
};

const persistEntitlementChange = async (
change: EntitlementChange,
): Promise<void> => {
// TODO: Replace with DB write (e.g. Postgres/Supabase/SQLite)
console.log(
JSON.stringify({
action: "entitlement_sync",
...change,
at: new Date().toISOString(),
}),
);
};

Bun.serve({
port: PORT,
routes: {
"/webhooks/polar": async (req: Request) => {
const rawBody = await req.text();
const signature = req.headers.get(SIGNATURE_HEADER) ?? "";

if (!verifySignature(rawBody, signature)) {
return new Response("invalid signature", { status: 401 });
}

let event: PolarWebhookEvent;
try {
event = JSON.parse(rawBody) as PolarWebhookEvent;
} catch (_error) {
return new Response("invalid json", { status: 400 });
}

const change = toEntitlementChange(event);
if (change) {
await persistEntitlementChange(change);
}

return new Response("ok", { status: 200 });
},
},
fetch() {
return new Response("not found", { status: 404 });
},
});

console.log(`Polar webhook example listening on http://localhost:${PORT}`);
152 changes: 152 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dictx | Private speech-to-text for serious writing</title>
<meta
name="description"
content="Dictx is privacy-first desktop speech-to-text. Runs locally, ships signed binaries, and auto-updates on Pro."
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;700;800&family=Fraunces:opsz,wght@9..144,500;9..144,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="grain"></div>
<header class="topbar">
<a class="brand" href="#top">Dictx</a>
<nav>
<a href="#pricing">Pricing</a>
<a
href="https://github.com/0xNyk/dictx"
target="_blank"
rel="noreferrer"
>GitHub</a
>
<a class="cta-mini" href="/buy">Get Pro</a>
</nav>
</header>

<main id="top">
<section class="hero">
<p class="eyebrow">Split Labs</p>
<h1>
Talk. Edit. Ship.
<span>Without sending your voice to the cloud.</span>
</h1>
<p class="lead">
Dictx is desktop speech-to-text for people who write all day. Local
transcription, polished text output, and pro-grade distribution.
</p>
<div class="hero-actions">
<a class="btn btn-primary" href="/buy">Buy Dictx Pro</a>
<a
class="btn btn-ghost"
href="https://github.com/0xNyk/dictx/releases"
target="_blank"
rel="noreferrer"
>Download Release</a
>
</div>
<p class="subline">
$29 one-time • signed binary • auto-updates • macOS / Windows / Linux
</p>
</section>

<section class="pillars">
<article>
<h2>Private by default</h2>
<p>
All core transcription runs locally on your machine. No voice data
routed through Dictx servers.
</p>
</article>
<article>
<h2>Fast enough for flow state</h2>
<p>
Built for real writing sessions, with quick trigger-to-text loops
and model options for speed or precision.
</p>
</article>
<article>
<h2>Built for Obsidian workflows</h2>
<p>
Capture, clean, and export voice notes into your vault with
metadata-rich output.
</p>
</article>
</section>

<section id="pricing" class="pricing">
<h2>Choose your path</h2>
<div class="cards">
<article class="card free">
<p class="card-label">Free</p>
<p class="price">$0</p>
<ul>
<li>Full GPL source code</li>
<li>Build from source</li>
<li>All core transcription features</li>
<li>Community support</li>
</ul>
<a
class="btn btn-ghost"
href="https://github.com/0xNyk/dictx"
target="_blank"
rel="noreferrer"
>View Source</a
>
</article>
<article class="card pro">
<p class="card-label">Pro</p>
<p class="price">$29<span> one-time</span></p>
<ul>
<li>Signed installers</li>
<li>Built-in auto-updates</li>
<li>Professional distribution</li>
<li>Funds active development</li>
</ul>
<a class="btn btn-primary" href="/buy">Get Dictx Pro</a>
</article>
</div>
</section>

<section class="faq">
<h2>FAQ</h2>
<details open>
<summary>Is Dictx still open source?</summary>
<p>Yes. Dictx remains GPL-licensed and free to build from source.</p>
</details>
<details>
<summary>What does Pro include?</summary>
<p>
Pro is the convenience layer: signed binaries and updater flow so
you do not have to compile manually.
</p>
</details>
<details>
<summary>How do updates work?</summary>
<p>
The app checks signed release metadata and applies updates in-app
when available.
</p>
</details>
</section>
</main>

<footer>
<p>© <span id="year"></span> Split Labs. Dictx by 0xNyk.</p>
</footer>

<script>
document.getElementById("year").textContent = String(
new Date().getFullYear(),
);
</script>
</body>
</html>
Loading