Guide · v1.0.0

From zero to a product you can sell.

Seven steps, the least work. Three are clicks in your vendor console, three are copy-paste on your machine, one proves it end to end. Nothing here is a guess - every route and command is live today.

The whole path, at a glance:
  1. Console: become a vendor → create a product & plan → connect test payments
  2. Your machine: add a ~5-line licence check → protect your code → unseal at runtime
  3. Prove & ship: buy your own product in test mode → switch on live payments

1 Become a vendor

Console · ~2 min

Sign in and turn your account into a seller at Become a vendor. This provisions your signing keys (used to mint and verify licences) behind the scenes - you don't touch them.

The free Dev tier is enough to complete every step below and sell in test mode. Real customers need a paid plan - see pricing.

2 Create a product and a plan

Console · ~3 min

In Products, add a product. Give it a short product code (e.g. acme-pro) - you'll use that exact code in your app and in the Protect CLI. Add at least one plan (a price). That's all a licence needs to exist.

3 Connect test payments

Console · ~5 min

Open Getting paid and connect Stripe (and/or PayPal) in test mode. Nothing is charged for real. When a test payment succeeds, Key-Warden issues the licence automatically - you don't write any fulfilment code.

Don't know what to type into the card form? The exact test card numbers / sandbox logins appear inline on your Get started checklist at the moment you need them.

4 Add the licence check to your app

Your machine · ~10 min

Install the SDK for your stack and validate the customer's key at startup. Read your two gateway keys from the environment - never hard-code them. Get them from SDK & Tools.

// npm install @key-warden/sdk const kw = require('@key-warden/sdk'); const res = await kw.validate(licenceKey, { apimKey: process.env.KW_APIM_KEY, clientKey: process.env.KW_CLIENT_KEY, machineId: kw.machineIdFrom(os.hostname(), userId), }); if (!res.valid) lockFeatures(res.reason); // offline later, no network needed: kw.verifyToken(res.token, process.env.KW_PUBLIC_KEY);
// dotnet add package KeyWarden.Sdk using KeyWarden; var res = await KeyWardenClient.ValidateAsync(licenceKey, new ValidateOptions { ApimKey = Environment.GetEnvironmentVariable("KW_APIM_KEY"), ClientKey = Environment.GetEnvironmentVariable("KW_CLIENT_KEY"), MachineId = KeyWardenClient.MachineIdFrom(Environment.MachineName, userId), }); if (!res.Valid) LockFeatures(res.Reason); // offline later: KeyWardenClient.VerifyToken(res.Token, publicKeyBase64);
# pip install keywarden import keywarden as kw res = kw.validate(licence_key, apim_key=os.environ['KW_APIM_KEY'], client_key=os.environ['KW_CLIENT_KEY'], machine_id=kw.machine_id_from(socket.gethostname(), user_id)) if not res['valid']: lock_features(res.get('reason')) # offline later: kw.verify_token(res['token'], os.environ['KW_PUBLIC_KEY'])
// composer require key-warden/sdk use KeyWarden\KeyWardenClient; $res = KeyWardenClient::validate($licenceKey, [ 'apimKey' => getenv('KW_APIM_KEY'), 'clientKey' => getenv('KW_CLIENT_KEY'), 'machineId' => KeyWardenClient::machineIdFrom(gethostname(), $userId), ]); if (!$res['valid']) lockFeatures($res['reason'] ?? null); // offline later: KeyWardenClient::verifyToken($res['token'], getenv('KW_PUBLIC_KEY'));

✓ At this point you already have a sellable product. Steps 5 and 6 harden it so your code can't simply be copied and run without a licence - strongly recommended, not mandatory to sell.

5 Protect your code (Protect CLI)

Your machine · ~10 min

Enable protection for your product at Protect your code and click Reveal key to copy your product's base-64 content key. Then run the Protect CLI in your build. It obfuscates your build (mandatory - it can't be turned off), seals your licensed files with AES-256-GCM, embeds your public key, watermarks the build, and verifies the result - all locally. Your source never leaves your machine.

npx @key-warden/protect init # writes kw-protect.json - set product code + publicKey $env:KW_CONTENT_KEY = "<base64 key>" # from Protect your code -> Reveal key npx @key-warden/protect build # obfuscate -> seal -> embed-key -> watermark -> check
npx @key-warden/protect init # writes kw-protect.json - set product code + publicKey export KW_CONTENT_KEY="<base64 key>" # from Protect your code -> Reveal key npx @key-warden/protect build # obfuscate -> seal -> embed-key -> watermark -> check

Ship the produced *.sealed files plus dist/kw-public-key.txt. Gate your CI with npx @key-warden/protect check - it exits non-zero if anything (including obfuscation) is missing.

The content key is never stored in kw-protect.json - only in the KW_CONTENT_KEY environment variable, so it stays out of your repo.

6 Unseal at runtime

Your machine · ~5 min

The sealed files only decrypt once Key-Warden hands the content key to a valid, activated licence - bound to that machine. Add three lines after your validate() call:

const machineId = kw.machineIdFrom(os.hostname(), userId); const res = await kw.validate(licenceKey, { apimKey, clientKey, machineId }); const key = kw.unlockFromToken(res.token, machineId); // machine-bound content key const code = kw.unseal(sealedBlob, key); // your decrypted file
var machineId = KeyWardenClient.MachineIdFrom(Environment.MachineName, userId); var res = await KeyWardenClient.ValidateAsync(licenceKey, opts); var key = KeyWardenClient.UnlockFromToken(res.Token, machineId); var code = KeyWardenClient.Unseal(sealedBlob, key);
machine_id = kw.machine_id_from(socket.gethostname(), user_id) res = kw.validate(licence_key, apim_key=..., client_key=..., machine_id=machine_id) key = kw.unlock_from_token(res['token'], machine_id) code = kw.unseal(sealed_blob, key)
$machineId = KeyWardenClient::machineIdFrom(gethostname(), $userId); $res = KeyWardenClient::validate($licenceKey, [/* ... */]); $key = KeyWardenClient::unlockFromToken($res['token'], $machineId); $code = KeyWardenClient::unseal($sealedBlob, $key);

Prefer real-time revocation over offline? Swap unlockFromToken + unseal for unsealOnline(), which fetches the key live so a revoked licence stops working immediately.

7 Prove it, then go live

Console · ~5 min

Buy your own product once in test mode - the single best check that everything is wired: payment → licence issued → your app validates → sealed code unseals. Your Get started checklist ticks the test-sale step when it sees a real end-to-end purchase.

When that's green, switch the payment connection to live (and make sure you're on a paid plan for production customers). You're selling.

+ Offer a free trial (optional)

Console + your machine · ~5 min

On any plan you can set a Trial (days) value (1-60) in your dashboard. Customers then get a self-serve, no-card trial at key-warden.com/trial?plan=<plan-code> - the buy page also shows a Start N-day free trial button automatically. Key-Warden emails them a licence key that is node-locked to one device and expires when the trial ends.

A trial key validates exactly like a paid one - your validate()/verifyToken() call already refuses it once it expires (reason expired). The 1.3.0 SDKs add trialInfo(), isTrial() and daysRemaining() so you can show a countdown and an expired state:

// A trial key validates exactly like a paid one - it just carries // trial:true and an exp. Read those for display (SDK 1.3.0+): const res = kw.verifyToken(cachedToken, process.env.KW_PUBLIC_KEY); if (res.valid) { const t = kw.trialInfo(res); // { isTrial, expired, expiresAt, daysRemaining } if (t.isTrial) showBanner(`Trial - ${t.daysRemaining} day(s) left`); runApp(); } else if (res.reason === 'expired') { showPaywall('Your trial has ended. Enter a licence key to continue.'); }
// A trial key validates exactly like a paid one - it just carries // trial:true and an exp. Read those for display (SDK 1.3.0+): var res = KeyWardenClient.VerifyToken(cachedToken, publicKeyBase64); if (res.Valid) { var t = KeyWardenClient.TrialInfo(res); // TrialStatus { IsTrial, Expired, ExpiresAt, DaysRemaining } if (t.IsTrial) ShowBanner($"Trial - {t.DaysRemaining} day(s) left"); RunApp(); } else if (res.Reason == "expired") { ShowPaywall("Your trial has ended. Enter a licence key to continue."); }
# A trial key validates exactly like a paid one - it just carries # trial:true and an exp. Read those for display (SDK 1.3.0+): res = kw.verify_token(cached_token, os.environ['KW_PUBLIC_KEY']) if res['valid']: t = kw.trial_info(res) # {'is_trial','expired','expires_at','days_remaining'} if t['is_trial']: show_banner(f"Trial - {t['days_remaining']} day(s) left") run_app() elif res.get('reason') == 'expired': show_paywall('Your trial has ended. Enter a licence key to continue.')
// A trial key validates exactly like a paid one - it just carries // trial:true and an exp. Read those for display (SDK 1.3.0+): $res = KeyWardenClient::verifyToken($cachedToken, getenv('KW_PUBLIC_KEY')); if ($res['valid']) { $t = KeyWardenClient::trialInfo($res); // ['isTrial','expired','expiresAt','daysRemaining'] if ($t['isTrial']) show_banner("Trial - {$t['daysRemaining']} day(s) left"); run_app(); } elseif (($res['reason'] ?? '') === 'expired') { show_paywall('Your trial has ended. Enter a licence key to continue.'); }

These helpers are for display only - always gate access on validate()/verifyToken() first. daysRemaining rounds up (the last partial day still reads "1 day left") and is 0 once expired. Trial keys are node-locked, so pass the same machineId you use for validate().

That's the whole path.

Console: become a vendor → product & plan → test payments. Machine: validate → protect → unseal. Then a test purchase and flip to live.