Home Apps My Apps Buy My Licenses Docs Pricing Register App Get License

zkLicensing Developer Guide

zkLicensing lets you add privacy-preserving license verification to any software using zero-knowledge proofs on Mina Protocol. No per-app license backend, no email addresses, no off-chain user database.

This documentation covers the developer integration. If you're a user looking to buy or verify a license, see Buy a License or Verify.

Quick Start

Get license verification into your app in under 10 minutes.

  1. Register your app on zklicensing.com/register — connect your Mina wallet, set your price, deploy the zkApp. Takes ~5 minutes.
  2. Copy your verification key from the post-deploy screen.
  3. Install the SDK in your project.
  4. Call verifyLicense() at app startup.
bash # Install the SDK npm install zklicensing
typescript import { verifyLicense } from 'zklicensing'; // `proof` is the JSON the buyer downloaded — load it however you like. const result = await verifyLicense({ licenseHash: proof.licenseHash, productAddress: 'B62qm4KpTRs...Xp9N', // your zkApp address expirySlot: proof.expirySlot, purchaseSlot: proof.purchaseSlot, tier: proof.tier, keeperUrl: 'https://zklicensing.com/api/verify', }); if (!result.valid) { showUpgradePrompt(); return; } if (result.tier === 'pro') { unlockProFeatures(); }

The SDK calls the keeper's /verify endpoint for the first ~14 days after purchase (the on-chain refund window). Once the keeper confirms the window has closed, the SDK writes a one-shot anchor into the available storage (localStorage in browsers, an injected storage adapter in Node) and from that point on verifies fully offline using local time + the anchor's slot baseline. result.source tells you which path ran ('chain' vs 'offline'). A renewal advances the proof's expirySlot, which invalidates the anchor and forces one more chain check.

How It Works

The system has three layers:

  • zkApp (Mina smart contract) — holds the authoritative license state on-chain. Exposes methods buyLicense(), renewLicense(), requestRefund(), and releaseFunds().
  • Proof JSON — a ~22 kB file the buyer downloads. Contains a zk proof attesting "a valid license exists for this app and tier" without revealing the buyer's wallet address.
  • Verification key — a public key you embed in your app. Used to verify the proof offline in milliseconds.
After the 14-day refund window closes the SDK is fully offline — it stops calling the keeper, anchors locally, and estimates the current slot from the user's clock. Before that window closes, every verifyLicense call goes to the keeper. The embed-VK path skips the keeper but still needs a Mina RPC read to confirm the license hasn't been refunded.

Mobile Apps (Android / iOS)

The recommended pattern is buy in the browser, unlock in the app. The browser purchase flow works on both desktop and mobile — Auro on mobile connects through WalletConnect — so users on a phone can complete the purchase without leaving their device. After payment, transfer the proof.json file into the mobile app.

For verification on mobile you have two options. The simplest is to call the hosted /api/verify endpoint directly over HTTPS — one round-trip per check, no SDK to embed. The smarter option is to mirror the JavaScript SDK's behaviour: call the keeper for the first ~14 days, then cache a one-shot anchor (in SharedPreferences on Android, UserDefaults on iOS) and verify fully offline after — no further HTTP, no further Mina RPC. The keeper response includes currentSlot and purchaseSlot — graduate to the offline branch only once currentSlot > purchaseSlot + REFUND_WINDOW_N, where REFUND_WINDOW_N = 13 440 slots (≈ 14 days at one slot every 90 seconds, post-Mesa). Ready-to-paste snippets for both Android (Kotlin) and iOS (Swift) are generated on the register page.

Key Concepts

ZK Proof

A zero-knowledge proof is a cryptographic object that proves a statement is true without revealing the witness that makes it true. In our case: "a valid purchase transaction exists on Mina" is proven, and the proof file itself reveals no wallet address. Note that the underlying purchase transaction is visible on the Mina chain — pseudonymity comes from your wallet address not being tied to your real identity, not from the proof hiding the chain record.

Verification Key

The verification key is derived from your zkApp's circuit. It's a ~500-byte string you hard-code into your application binary. It's safe to distribute publicly — it can only verify proofs, not create them.

zkApp Address

The on-chain address of your deployed licensing contract. Optionally used for chain-based verification as a fallback when the buyer has lost their proof file.

Passphrase & Secret Hash

When buying, the buyer chooses a passphrase (a random UUID by default). This passphrase is never stored anywhere — its Poseidon hash (licenseHash = Poseidon.hash([secretHash])) is what gets recorded in the on-chain Merkle map. The passphrase is required to renew or refund a license, so buyers must keep it safe.

For refunds, the zkApp circuit also requires that the transaction be signed by the original buying wallet. Both the passphrase and the original wallet are checked in-circuit — passphrase alone is not sufficient to claim a refund. Buyers must use the same Auro wallet they used at purchase time.

Grace Period

After a license expires, buyers get a 7-day grace window to renew. During this window verifyLicense() still returns valid: true and sets inGracePeriod: true on the result so your app can show a renewal reminder. Renewal within the grace period extends the expiry from the original expiry date, not from today — so no days are lost. After the grace window closes, the license is fully expired and renewal resets the clock from the current date.

No Vendor Revocation

By design, vendors cannot revoke or modify a license once it has been purchased. The on-chain contract has no method to alter or remove a license entry — only the buyer can clear their own entry by calling requestRefund() within the 14-day refund window. This guarantee is what makes the proof file the buyer holds permanently valid for the licensed period: there is no kill switch a vendor can pull. Disputes must be handled off-chain, and access cannot be cut.

Registering Your App

Go to register.html and complete the 5-step form:

  • Step 1 — Connect wallet. Your Auro wallet becomes the zkApp's admin key. Keep it safe — it's the only way to update pricing or release escrowed funds.
  • Step 2 — App info. Name, App ID (used in proof files and the buyer URL), description, category.
  • Step 3 — Pricing. Your Mina payment address and license tiers. You can add multiple tiers (Pro, Enterprise).
  • Step 4 — Settings. Verification-key visibility (public/private) and proof format (JSON or base64). The 7-day grace period and 14-day refund window are baked into the shared circuit and apply to every deployment.
  • Step 5 — Deploy. Signs a Mina transaction that publishes your zkApp contract.
Deployment costs ~1 MINA in network transaction fees, plus a 100 MINA one-time registration fee to list your app on the marketplace.

Listing Requirements

Deploying the zkApp puts your licensing contract on Mina Devnet immediately. The public marketplace listing is moderated separately — we review every submission and every subsequent edit before it goes live. Reviews land within 7 business days in most cases, and up to 15 business days at the latest.

To pass review, your listing must meet the following:

  • Real product. The app must be a working piece of software a user can actually run, not a placeholder, demo, or test deployment.
  • Honest metadata. Name, description, category, and tier names must match what the software actually does. Tier names cannot imply features the app does not deliver.
  • Reachable vendor. A working support contact (URL or email) where licensees can reach you for help or refund issues outside the 14-day automated window.
  • Working logo URL. If you provide a logo, it must be a public HTTPS URL that loads — a square image, at least 128×128, under 500 KB.
  • Plausible pricing. Pricing must be set in good faith. Listings with obvious wash-pricing (e.g. 0.0001 MINA "premium" tier) will be held.
  • Non-malicious. The app must not harm or deceive the people who run it, and must not infringe third-party rights.
  • Compliance with the vendor terms. See Legal & Privacy.

Edits to an approved listing (name, description, logo, pricing display, tier names) go back through the same queue. Your live listing keeps showing the previously approved version until the edit is reviewed — so users never see in-flight changes.

The on-chain zkApp is yours and starts working the moment you deploy — you can issue licenses via your own integration even while the marketplace listing is pending. Moderation only gates discovery on zklicensing.com/apps.

Spotted a listing that doesn't meet these requirements? Buyers can report it: .

SDK Reference

TypeScript SDK published as zklicensing on npm. Works in both browsers and Node. From other languages, call the keeper's GET /verify HTTP endpoint directly and replicate the keeper-then-anchor pattern in idiomatic platform storage.

verifyLicense(proof, options?)

proof: ProofInput — fields read from the buyer's proof file.

FieldTypeDescription
licenseHashstringPoseidon hash that identifies the license on-chain
productAddressstringYour deployed zkApp's Mina address (B62q…)
expirySlotnumberOn-chain expiry slot from the proof. Used to invalidate stale anchors on renewal
purchaseSlotnumberOn-chain slot of the purchase. Used to determine when the refund window has closed
tierstring?Tier label from the proof (e.g. "pro")
keeperUrlstring?Base URL of the keeper or hosted /api/verify. May also be passed via options.keeperUrl

options: VerifyOptions — all optional. Defaults are suitable for browser use.

OptionTypeDescription
keeperUrlstring?Override the keeper URL from the proof
storageStorage | nullWhere the one-shot anchor is persisted. Defaults to globalThis.localStorage; pass null to disable the offline path; pass a { getItem, setItem } adapter in Node
fetchertypeof fetchCustom fetch implementation. Defaults to the global fetch
now() => numberClock source for the offline branch. Defaults to Date.now

Return value — VerifyResult

FieldTypeDescription
validbooleanWhether the license is currently valid
tierstring"pro" | your custom tier name
expirySlotnumberOn-chain expiry slot
inGracePeriodbooleanTrue if expired but within the 7-day grace window
reasonstring | nullHuman-readable reason when valid is false or grace applies
sourcestring'chain' when the result came from the keeper; 'offline' when it came from the local anchor

Anchor behaviour

  • The anchor is keyed by licenseHash and stores { anchoredSlot, anchoredAtMs, expirySlot, tier }.
  • The anchor is set only when the keeper itself reports currentSlot > purchaseSlot + REFUND_WINDOW_N (REFUND_WINDOW_N = 13 440 slots ≈ 14 days). A user winding their local clock forward cannot graduate the anchor early.
  • When the proof's expirySlot stops matching the anchor's (i.e. the user renewed), the anchor is invalidated and the SDK calls the keeper once more.
  • In the offline branch the SDK estimates the current slot from (now − anchoredAtMs) / MS_PER_SLOT, where MS_PER_SLOT = 90 000 ms is Mina's post-Mesa slot time (one block every 90 seconds). The same 7-day grace window applies as in the chain check.

zkApp Circuit

The zkApp is written in o1js (TypeScript framework for Mina zkApps). The circuit enforces three rules:

  • The purchase transaction was signed by a valid Mina private key.
  • The payment amount equals the tier price at time of purchase.
  • The proof's public output — { tier, expiresAt } — is correctly derived from the on-chain state.
typescript (o1js) // Simplified circuit sketch class LicensingApp extends SmartContract { // On-chain state (5 of 8 field slots) @state(Field) licensesRoot = State<Field>(); // MerkleMap: licenseHash → expirySlot @state(PublicKey) vendor = State<PublicKey>(); @state(UInt64) licensePrice = State<UInt64>(); // nanomina @state(Field) escrowRoot = State<Field>(); // MerkleMap: licenseHash → Poseidon(amount,slot,buyerKey) @method async buyLicense(licWitness: MerkleMapWitness, escWitness: MerkleMapWitness, secretHash: Field) { // Buyer pays exact price into contract (held in escrow) const price = this.licensePrice.getAndRequireEquals(); AccountUpdate.createSigned(sender).send({ to: this.address, amount: price }); // Record license expiry = currentSlot + ONE_YEAR_SLOTS // licenseHash = Poseidon.hash([secretHash]) — buyer's passphrase hashed } @method async renewLicense(witness: MerkleMapWitness, secretHash: Field, currentExpiry: UInt32) { // Direct payment: 98% → vendor, 2% → platform (no escrow) // Extends from currentExpiry if within 7-day grace window } @method async requestRefund(licWitness: MerkleMapWitness, escWitness: MerkleMapWitness, secretHash: Field, currentExpiry: Field, purchaseSlot: UInt32, purchaseAmount: UInt64, buyerKey: PublicKey) { // sender must equal buyerKey — passphrase alone cannot steal the refund } @method async releaseFunds(escWitness: MerkleMapWitness, licenseHash: Field, purchaseSlot: UInt32, purchaseAmount: UInt64, buyerKey: PublicKey) { // Permissionless after 14-day refund window: 98% → vendor, 2% → platform } }

Proof JSON Format

The proof file downloaded by buyers has this structure:

json { "licenseHash": "0x4f3a...8b21", // Poseidon(secretHash) — license identity "productAddress": "B62qr...7kHsN", // vendor zkApp this proof is for "expirySlot": 1582400, // Mina slot at which the license expires "network": "mina:devnet", "txHash": "5JuQ...x9mZpQ", // purchase tx hash "tier": "pro", "verificationKeyHash": "0x9c2e...7d10", // circuit VK hash — must match embed "proof": { /* o1js Proof.toJSON() */ } }

FAQ

Plain-English answers about zero-knowledge proofs, privacy, refunds, and integration. User questions come first; developer-specific topics are at the bottom.

What is a ZK Proof?

A zk proof — short for zero-knowledge proof — is a small piece of cryptographic data that proves a statement is true without revealing the underlying secret that makes it true. The classic analogy: imagine proving you know the password to a vault without ever saying the password.

For zkLicensing, the statement is: "a valid license purchase exists on Mina for this app, with this expiry." The proof file you give to the app reveals only the license's validity and expiry — not your passphrase, not your wallet address. The app verifies it in milliseconds against its public verification key, offline, and unlocks.

What is still public. Every on-chain transaction on Mina — including your license purchase, renewal, and refund — is visible to anyone who looks at the blockchain. The transaction shows your wallet address, the price you paid, and the license hash. The proof file itself doesn't expose any of that, but the underlying chain record does. If your wallet address is otherwise tied to your identity, that link is recoverable. Use a fresh wallet for stronger pseudonymity.

Why doesn't zkLicensing collect my email?

Because we don't need it. A license is a cryptographic object, not an account. The proof file itself is the license — there's nothing to "send to your inbox." No password reset flow, no marketing list, no off-chain user database to breach. What you should protect: your proof.json file and your license passphrase — those are everything you need to use, renew, or refund the license.

What if I lose my proof file?

As long as you still have the Mina wallet you used to buy the license and the passphrase you chose at purchase time, you can regenerate the proof. Open the My Licenses page, connect your wallet, find the license, and click "Re-download proof". The keeper will rebuild the proof from the on-chain record.

If you lose both the proof file and the passphrase, the license is unrecoverable. There is no recovery email or support flow that can bypass this — the passphrase is required by the zkApp circuit itself. Treat the passphrase like a wallet seed phrase.

Can I use my license on more than one machine?

Yes — the proof file is portable and works on any machine that has the app installed. Copy proof.json to your laptop, desktop, or another device you own and the app will accept it. There is no per-device activation step. This is intentional: the proof is a portable bearer credential and the public output doesn't reveal the buyer's wallet. If your business model requires stricter enforcement, layer it on at the app level (e.g. per-install activation against your own backend after proof verification).

Can I use this with a mobile (Android / iOS) app?

Yes — the pattern is buy on the web, unlock in the app. The browser purchase flow works from both desktop and mobile (Auro on mobile connects through WalletConnect). Once paid, users transfer their proof.json file into the mobile app, and the app verifies it by calling the hosted /api/verify endpoint. The keeper response includes the slot numbers needed to mirror the JavaScript SDK's behaviour — call the keeper while the 14-day refund window is open, then cache a one-shot anchor in SharedPreferences / UserDefaults and verify fully offline after.

Can I get a refund?

Within 14 days of purchase, yes — a full refund, no questions asked. Your MINA sits in the zkApp's escrow during this window, not in the vendor's wallet. Visit the Refund page, connect the same Auro wallet you used to buy, enter your passphrase, and the contract returns the funds to your address. After 14 days, the funds are released to the vendor and the license becomes non-refundable. Renewals are also non-refundable — they bypass escrow entirely. This is enforced by the smart contract, not by a policy we could choose to bend.

How does the escrow work?

When a buyer purchases a license, their MINA goes to the zkApp contract — not directly to the vendor. It sits in escrow for 14 days. After 14 days the keeper automatically releases the funds to the vendor's payment address (the vendor can also trigger this manually from their dashboard). The 2% platform fee is deducted on release, so the vendor receives 98% of the purchase amount.

Refunds are bound to the original buyer's wallet address in-circuit. To claim a refund you need both your passphrase and the same Auro wallet you used at purchase time. Renewals bypass escrow entirely — funds go directly to the vendor (98%) and the platform (2%) at the moment of the transaction.

How does renewal work?

Open the Renew page, connect your wallet, paste in your existing proof.json, and pay the vendor's renewal price. The keeper generates a new proof with an extended expiry; you download it and replace your old proof.json. Apps will accept the new file automatically — the expiry is baked into the proof itself.

If you renew within 7 days of expiry, the new term extends from the original expiry date. After that, it extends from today.

What happens if the vendor disappears?

Your license keeps working until its expiry, regardless of what the vendor does. The proof is verified locally against a public verification key baked into the app — the vendor's servers aren't in the loop. Even if the vendor's company shuts down tomorrow, every already-purchased license continues to validate for its full term.

What you lose if the vendor disappears: the ability to renew at the end of the term and any support channel they provided. The app itself keeps running for paid users.

What happens when Mina is down?

It depends on which verification path your app uses and when the outage hits:

  • SDK path (verifyLicense) — only the first ~14 days are exposed to Mina downtime. The SDK calls the keeper while the on-chain refund window is open; once it closes, the SDK anchors locally and stops touching the network. Buyers whose anchor has graduated are unaffected by any subsequent outage.
  • Embed-VK path — verifies the zk proof locally, but still needs a Mina RPC read on every check to confirm the license hasn't been refunded. The RPC call can go to any public Mina endpoint or your own node — but if the Mina chain itself is unreachable, the check fails.
  • Raw /api/verify path (no SDK caching) — fails during Mina downtime on every request.

New purchases, renewals, and refunds always pause during Mina downtime, since they all write on-chain. For mission-critical apps the SDK is the most resilient option: after the refund window closes, it never touches the network at all.

Why do I need a Mina wallet?

Because payment, refund authorization, and renewal all happen as on-chain transactions signed by your wallet — there's no payment processor in the middle. The Mina blockchain is the system of record. We recommend Auro Wallet as a browser extension (Chrome / Firefox). Getting a Mina wallet and a small amount of MINA is a one-time setup.

Can I change my license price?

Yes — call the zkApp's setPrice() method from your admin wallet. Changes apply to new purchases only. Existing licenses are unaffected.

What's the 2% platform fee?

zkLicensing deducts 2% of each payment on-chain before sending the remainder to your payment address. This is enforced by the zkApp circuit — we can't change it after deployment.

Can I run the verifier myself without using zkLicensing servers?

Yes — two self-hosted options:

  • Run your own keeper or verify-service. Both are part of the open-source zklicensing package and expose the same GET /verify endpoint that the SDK calls. Point verifyLicense at it by passing keeperUrl: 'https://your-keeper.example.com'. zklicensing.com isn't in the loop.
  • Embed the verification key and run the Mina verifier locally on the buyer's proof.json. This path needs only a public Mina RPC endpoint to read the current Merkle root (any node will do) and never touches a keeper at all. It's the right choice when you cannot rely on any HTTP service being reachable.

Why should I trust this? / Is the source code open?

You don't have to trust us — you can verify. The zkApp circuit and the SDK are open source on GitHub. The verification key for every app is published on-chain. The proof file you receive can be inspected, verified by any independent tool that runs the Mina verifier, and re-checked against the on-chain Merkle root any time.

The pieces you'd need to trust to "fake" a license are: (a) the Mina blockchain itself (open, audited, ~100+ validators), and (b) the math of the underlying zero-knowledge proof system. Neither is something zkLicensing.com controls or could compromise unilaterally.