Home Apps My Licenses FAQ 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 accounts.

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 a working license gate wired into your app in an afternoon*. The verifier-side call is a one-liner (verifyLicense()); the time budget goes into hosting your own copies of the buy / renew / refund pages, wiring the licensed/unlicensed toggle into your UI, and persisting the buyer's proof.json.

  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. Fork the example flow pagesbuy.html, renew.html, refund.html (plus bind.html and unbind.html for device-cap apps, and verify.html for non-custodial verify — all in the SDK repo under packages/sdk/examples/) — into your own site. In production these must be hosted under your origin (or an origin you control), not from zklicensing.com. Swap the constants (appId, zkAppAddress, verifier URL, pinned pubkeys) and serve the pages under Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp so the in-browser prover's SharedArrayBuffer can run.
  4. Install the SDK in your project.
  5. Call verifyLicense() at app startup with the buyer's proof.json.

* The buy / renew / refund pages hosted on zklicensing.com are working examples, not a production endpoint — they exist so you can read the source, run them locally against devnet, and see the exact wallet, prover, and callback wiring your fork needs. Real vendors host their own copies (rebranded, on their own origin, with their own constants) and drive buyers there. Once you have a Mina wallet installed, an app registered, and your verification key in hand, the mechanical work — forking three HTML files, editing the constants, deploying under COOP+COEP, and adding one verifyLicense() call — is comfortably an afternoon for a competent web dev on their first Mina integration. The SDK call itself is one line; the surrounding host-your-own-flow work isn't.

Install zklicensing from npm and call verifyLicense() at app startup — see the SDK README on npm for install, imports, and a minimal usage snippet.

The SDK calls the verifier's /verify endpoint for the first ~14 days after purchase (the on-chain refund window). Once the verifier 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 against the user's local clock. result.source tells you which path ran ('chain' vs 'offline'). A renewal changes the license's expiry, which invalidates the anchor and forces one more chain check.

How It Works

The system has three moving parts:

  • zkApp (Mina smart contract) — holds the authoritative license state on-chain. Exposes methods buyLicense(), renewLicense(), requestRefund(), releaseFunds(), and (for device-cap apps) bind() / unbind().
  • Proof JSON — a compact JSON file the buyer downloads at purchase. Carries licenseHash, zkAppAddress, expirySlot, network, tx hash, tier, and the verification-key hash the contract was deployed with. The wallet address that paid never appears in the file; it lives only in the on-chain purchase tx.
  • SDK verifyverifyLicense() hits the licensing service's /verify endpoint during the on-chain refund window, then writes a one-shot local anchor and runs fully offline against the user's clock. For callers that don't want to trust the service at all, verifyOffline + fetchOnChainLicensesRoot pull the root straight from Mina and fold the leaf locally.
After the 14-day refund window closes the SDK is fully offline — it stops calling the verifier, anchors locally, and estimates the current slot from the user's clock. Before that window closes, every verifyLicense call goes to the verifier.

Mobile Apps (Android / iOS)

The recommended pattern is buy in the browser, activate in the app with the passphrase. The buyer purchases on your web flow (Auro on mobile connects via WalletConnect so the whole thing works on a phone) and remembers the passphrase they chose at purchase. On first launch of the native app, they enter that passphrase into the app's own UI — no proof.json file transfer, no import step.

The native app derives licenseHash locally from the entered passphrase (a one-way hash of the passphrase-derived pubkey) and calls GET /verify?licenseHash=…&zkAppAddress=… to confirm the license exists and is unexpired on-chain. The SDK's anchor-then-offline pattern handles the rest: during the 14-day refund window every launch hits the verifier; after the window closes the SDK anchors locally and runs fully offline.

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 zkLicensing, the buyLicense() / renewLicense() / bind() / unbind() methods on the on-chain zkApp are all zk circuits — each execution produces a proof that the transition it applied to licensesRoot was legal (payment amount matches the tier price, refund window still open, ownership signature verified in-circuit, etc.). Those proofs are what Mina admits to update state. The buyer's downloaded proof.json is a compact receipt referencing that on-chain state — the wallet that paid appears in the on-chain purchase tx (public), but not in the receipt file the buyer carries.

Verification Key

Every deployed zkApp is bound to a verification key derived from its circuit. The VK lives on-chain as part of the zkApp account — the vendor does not embed it in their app. Its hash is stamped into every proof.json the SDK issues, so a trust-anchor verifier (see trustanchor.html and the zklicensing-verify-trust CLI) can check that the on-chain contract still runs the expected circuit and hasn't been swapped out.

zkApp Address

The on-chain address (B62q…) of your deployed licensing contract. Every proof.json carries it, every SDK call keys on it, and it's the single input the SDK needs to look up license state on Mina. A post-hardfork redeploy issues a fresh address; the SDK's generation-list plumbing (zklicensing/generation) tracks the lineage so licenses on retired addresses keep verifying.

Passphrase & Secret Hash

When buying, the buyer chooses a passphrase (a random UUID by default). This passphrase is never stored anywhere — a one-way hash of it (licenseHash) is what gets recorded in the on-chain Merkle map. The passphrase is required to verify ownership — to prove the license belongs to you when unlocking the app — so buyers must keep it safe.

Refunds and renewals do not require the passphrase. Both are authorised by a signature from the license's wallet: refund is bound to the original buying wallet in-circuit; renew is paid by whichever wallet signs the transaction, extending expiry for the on-chain licenseHash. Buyers who lose their passphrase can still refund within 14 days and can still renew — they just can't prove ownership at the app.

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.

Grace across a hardfork. Ahead of a Mina hardfork the platform freezes new sales and renewals so every escrow window can close (see Terms §8.2). If a license's expiry falls inside that freeze window the buyer physically cannot renew in time — so at migration to the successor Generation the circuit auto-bumps the expiry forward to the fork activation slot. The standard 7-day grace window then runs from fork activation, giving the buyer a full week post-fork to renew and preserve stacking continuity. Buyers whose original expiry was already past fork activation get their expiry migrated verbatim (no bump). The bump is a circuit-side invariant, not a keeper policy — the verify path reads the bumped expiry naturally.

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.

Device binding — cumulative cap with cooldown, no issuer

Vendors can commit a device cap into every license their app mints (registration form: "Device cap (non-custodial, no issuer)", 0–5). When deviceCap = 0 the license is unbound — any device holding the passphrase unlocks it. When deviceCap > 0, each activation appends a small commitment to the on-chain LicenseLeaf — a one-way hash derived from a per-device signing key the buyer generates locally at first launch (stored in the OS keychain — Keychain on macOS/iOS, DPAPI on Windows, libsecret on Linux, Android Keystore).

The circuit enforces two rules on every bind():

  • Cumulative cap. Once deviceCap distinct commitments have ever landed for a license, no new commitment can be added — even if the buyer has since unbind()'d some. This is not a "concurrent seats" model; it's a lifetime counter over the license's issued term.
  • Cooldown per slot. After a slot is unbound, the circuit refuses to rebind it for 1 week. Flat cooldown for every tier (monthly, yearly, five-year, perpetual) and every cap value — one number to reason about, not a per-tier formula. This prevents cycling a small cap into an effectively-unlimited license by rapidly unbinding and rebinding.

What device binding does not do: it doesn't enforce concurrent use. Two devices bound to the same license can both run the app simultaneously; the circuit only caps how many distinct devices ever activate, and how frequently a slot can turn over.

Cached-root property. Because the leaf hash includes the device-slots state, every bind()/unbind() writes a fresh licensesRoot on-chain. The SDK's verifyOffline module (see zklicensing/verify-offline) reconstructs the leaf locally from the cached snapshot, verifies the Merkle witness against the last-seen root, and confirms the current device's commitment appears in the slot array. All of that is pure client-side computation — no HTTP once the snapshot is cached.

Remote log-off via passphrase. The buyer can revoke a lost or stolen device from any browser without contacting the vendor: enter the passphrase on the vendor's unbind page (or the platform fallback), the client produces an ownership signature over the target device commitment, and the circuit clears that slot (subject to the cooldown before it can be reused). No backend account, no support ticket, no vendor involvement — the passphrase is the sole authority.

Vendor tradeoffs. Small caps (1–2) tighten piracy control but frustrate buyers with multiple machines; the cooldown means "I got a new laptop" is a real wait. Larger caps (5) feel generous but weaken the deterrent. Setting cap = 0 disables device binding entirely — the license is fully bearer-style and offline verification skips the device check. There's no "issuer" service to run either way: the circuit is the sole authority.

Registering Your App

Go to register.html and complete the 4-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 — Deploy. Signs a Mina transaction that publishes your zkApp contract. The 7-day grace period and 14-day refund window are built into the shared circuit and apply to every deployment — no per-app settings are required.
Deployment costs ~1 MINA for the Mina account-creation fee (paid once when the zkApp account is created), plus 10 MINA on devnet. This covers your zkApp going on-chain, the buyer / renew / refund example pages, and eligibility for a public marketplace listing (subject to moderation).

Deploy phases — what your wallet is being asked to do

Clicking Deploy zkApp on Mina kicks off five steps. Two of them open Auro, and they are not the same kind of action — knowing which is which makes the prompts easier to read.

  1. Proving deploy — the server compiles your tier's zkApp circuit and generates the zero-knowledge proof that authorises the deployment. No wallet interaction. This is the slow step (~30 seconds) because real Mina proving runs here.
  2. Confirming transaction — Auro pops up a transaction prompt. You are paying the network fee and the 10 MINA on devnet, and broadcasting the zkApp creation transaction to Mina. This deducts MINA from your wallet and is recorded on-chain. Approving it sends the transaction to the network; it then needs a block to land.
  3. Signing App ID — Auro pops up a signature prompt (no fee, no on-chain effect). You are signing your App ID string with the same wallet. zkLicensing verifies this signature to prove that the wallet that deployed the contract is the same wallet claiming the marketplace listing. Nothing is broadcast; nothing leaves your wallet but the signature itself.
  4. Registering app — the signature plus your listing metadata are POSTed to zkLicensing. The signature is verified against your wallet's public key, the metadata validated, and the listing written to the marketplace queue for moderation.
  5. Deploying zkApp contract — Mina settles the transaction broadcast in step 2 and the zkApp account becomes active. From this point the contract can receive buyLicense() calls.
Transaction vs. signature. A transaction pays fees, touches on-chain state, and is permanent the moment a block lands. A signature is just a cryptographic statement signed with your private key — no fee, no on-chain write, used only to prove identity off-chain. The deploy flow needs both: one transaction (to publish the contract) and one signature (to claim the marketplace listing).

If you deploy multiple tiers, this whole sequence runs once per tier — one transaction and one signature per tier.

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 favicon on your website. Your marketplace logo is auto-derived from the favicon of the website URL you register — there is no separate logo upload. Your site must serve an icon over HTTPS, reachable publicly (no auth), via a <link rel="icon"> or <link rel="apple-touch-icon"> tag, or a /favicon.ico fallback. For best marketplace rendering, prefer an SVG or an apple-touch-icon at 180×180 or larger, square, and legible on a dark background (transparent PNGs with light content work well). Update the website and re-submit to change the logo.
  • 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, website — which drives the 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.
.well-known manifest. At approval time the licensing service fetches https://<website-host>/.well-known/zklicensing/<appId>.json, verifies the platform signature, and checks that the payload's appId and generation list match the record. This proves the vendor controls the website they list. On mainnet a failed check blocks approval; on devnet the outcome is recorded (it feeds the trust-anchor badge) but does not block, so vendors can iterate without owning DNS.

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

Buying and renewing by zkApp address

The marketplace listing at zklicensing.com/apps is a discovery layer, not a payment gate. Anyone can buy or renew a license against any deployed LicensingApp contract by pointing the frontend directly at the zkApp address — no marketplace listing required.

To send a buyer straight to a specific contract (from your own hosted copies of the example pages):

https://yourdomain.com/buy.html?app=B62q…<- your zkApp address
https://yourdomain.com/renew.html                 <- reads app from the buyer's proof.json

The buy.html / renew.html / refund.html pages served from zklicensing.com are working examples — fork them into your own origin for production. The prover endpoints they call (POST /prove/buy, POST /prove/renew) remain platform-hosted.

What happens under the hood:

  • The buy / renew pages first look the address up in the marketplace listing (GET /apps) to fetch the human-readable name and prices.
  • If the address isn't listed — private beta, inactive listing, or an app that was never registered — the frontend falls back to reading the four tier prices directly from the contract's on-chain state. The quoted price is always what the on-chain contract will charge.
  • The POST /prove/buy and POST /prove/renew endpoints accept any zkApp address that resolves to a valid on-chain LicensingApp — they're prover-and-cache endpoints, never a moderator. Whether a transaction is allowed is decided by the on-chain contract's own logic and the buyer's wallet signature, not by the marketplace.
Deactivating a marketplace listing (setting its status to inactive from the vendor dashboard, or an admin rejecting an active listing via POST /apps/:appId/reject) hides the app from the storefront but does not stop existing customers from buying or renewing against the contract. To halt new licenses at the contract level, the vendor (or the platform, via the platform-side sales-freeze mechanism) must act on-chain.

Practical uses:

  • Private / unlisted apps. Deploy a LicensingApp, skip the marketplace submission, and hand your customers a buy.html?app=… URL. The purchase flow works identically.
  • Pre-review beta. Sell to early users while your marketplace listing is still in moderation — the on-chain zkApp is already live.
  • Existing customer renewals continue to work even if a listing is later set inactive or the vendor takes their listing down for maintenance.

SDK Reference

The full SDK reference — all entry points, function signatures, option types, return shapes, and platform snippets — lives in the package README on npm as the single source of truth:

zklicensing on npm →

Highlights:

  • zklicensingverifyLicense(proof, options?), the anchored chain-then-offline verifier that every vendor SDK integration starts from.
  • zklicensing/verify-offline + zklicensing/on-chain-root — non-custodial verify: fetch the on-chain licensesRoot straight from Mina and fold the license leaf locally, no trust in the licensing service required.
  • zklicensing/buy, /renew, /refund, /bind, /unbind, /migrate, /deploy — flow clients that wrap the corresponding POST /prove/* round-trips. Working example pages ship under packages/sdk/examples/.
  • zklicensing/generation + zklicensing-verify-trust (bin) — pin-verified generation lineage and the trust-anchor CLI that vendor CI runs to gate the trust badge.

One important behaviour worth calling out here: the chain-then-offline gate is server-authoritative. The SDK only graduates a license to the offline anchor when the verifier's response carries refundWindowClosed: true — a flag the backend computes from its own purchaseSlot record against the live currentSlot. The SDK never trusts a purchaseSlot supplied by the local proof file, so a user winding their local clock forward — or forging the proof metadata — cannot force the offline branch early. Full anchor semantics are documented in the README.

zkApp Circuit

The zkApp is written in o1js (TypeScript framework for Mina zkApps). Every mutating method (buyLicense, renewLicense, requestRefund, bind, unbind, migrateLicense) is a zk circuit that:

  • Reads and re-writes licensesRoot (appState[0]) — the Merkle map of all license leaves for the app — under a Merkle-map witness, so the transition is provably legal against the current on-chain state.
  • Enforces the method's own invariants in-circuit: buyLicense asserts payment equals the tier price and stamps expirySlot and the buyer's ownership commitment (licenseHash) into the leaf; renewLicense asserts payment equals the tier price and extends the leaf's expirySlot; bind / unbind re-verify the buyer's ownership signature in-circuit and mutate the leaf's device slot array; requestRefund asserts the caller's wallet matches the leaf's original buyer signature and that the refund window is still open.
  • Routes payment through the 98 / 2 vendor-platform split on-chain: buyLicense holds funds in escrow until releaseFunds or the 14-day refund window closes; buyLicenseNoEscrow (used when the window would cross a scheduled freeze) and renewLicense split at the transaction itself. The 2 % platform fee is enforced by the circuit and cannot be raised or waived after deployment.

The public constants that describe the circuit's shape (refund window, grace period, slot timing, device slot cap, versions the licensing service admits) are re-exported from the SDK package as zklicensing/contractInterface — a single source of truth for both the on-chain circuit and every SDK caller. The LicensingApp circuit source itself is not part of the vendor SDK surface; deploy / prove / verify all go through the SDK's flow clients rather than importing the circuit directly.

Proof JSON Format

The proof.json a buyer downloads at purchase is a small lookup receipt, not the ZK proof itself. It carries just enough metadata to locate the license on-chain and to detect drift (generation, pending states) — no secrets, no proof blob. Anyone holding this file can look up the license, but cannot pass an ownership challenge: that requires the passphrase-derived signing key, which never leaves the buyer's device.

A typical proof.json looks like:

{
  "licenseHash": "23396753589808381346842106807023119468361664311828286490014142264757754745210",
  "expirySlot": 888035,
  "zkAppAddress": "B62qrA4BXtPH7Xc9qJ8KrVgAqBNcBAMQVXGVNzNWmANXTK7SRoxuutC",
  "network": "devnet",
  "txHash": "5JuJ4WP5konF1hEK7nYngBJraFwoSxnaEUDHQULo9PqdR3QAtv3V",
  "tier": "monthly",
  "verificationKeyHash": "9653053481389649859551211585166613995174307854122054165997166041093170524407",
  "purchaseSlot": 859235,
  "purchaseAmount": "1000000000",
  "generation": 1
}

The actual ZK proof is compiled and generated by the platform prover on demand (/prove/verify, /prove/renew, /prove/refund) and returned inline in the response — it isn't stored in the receipt.

Developer FAQ

Integration-specific questions. For end-user questions (refunds, what a ZK proof is, what happens if Mina is down), see the user FAQ.

How does the escrow accounting work?

Buyer purchases go to the zkApp contract — not the vendor — and sit in escrow for 14 days. After that, funds auto-release to the vendor's payment address (the vendor can also trigger release manually from their dashboard). The 2% platform fee is deducted at release, so the vendor receives 98% of the purchase amount.

Refunds are bound to the buyer's wallet address in-circuit: only a signature from the original purchase wallet can claim a refund — no passphrase entry required. Renewals bypass escrow entirely — funds split 98% / 2% at the transaction itself, with no 14-day hold.

Can I change my license price after listing?

Yes — call the zkApp's setPrice() method from your admin wallet. Changes apply to new purchases only. Existing licenses keep their original terms; the on-chain verification key cannot be rewritten.

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 — it cannot be raised or waived after deployment.

Can I run zkLicensing under my own brand — no zklicensing.com presence?

Yes for the buyer-facing surface. The marketplace listing at zklicensing.com/apps is a discovery layer, not a requirement. Skip the marketplace submission, fork the buy / renew / refund example pages (bundled with the SDK under packages/sdk/examples/) onto your own origin, and buyers reach your app entirely under your domain — no zklicensing.com branding, no marketplace fallback. The on-chain zkApp is identical whether or not it's listed; see Buying and renewing by zkApp address.

What still depends on the platform backend. Deploy, buy, renew, refund, migrate, bind, and unbind all round-trip through the platform backend. Self-hosting the backend is not supported today. It's callable from any origin (CORS is open), so a vendor-branded frontend on your own domain calling the platform backend is fine — but the backend itself is a platform dependency.

Once the on-chain refund window has closed, the SDK writes a local anchor and stops calling the verifier at all — the steady-state offline gate is not on the network hot path.

How do I verify a license in a native mobile app?

The pattern is buy on the web, activate in the app with the passphrase — no file transfer between the browser and the phone. The buyer completes the purchase on your hosted buy.html (Auro / WalletConnect on mobile is fine) and remembers the passphrase they chose. On first launch of the native app, they enter that passphrase into the app's own UI; the app derives licenseHash locally (a one-way hash of the passphrase-derived pubkey) and calls GET /verify?licenseHash=…&zkAppAddress=… to confirm the license exists and is unexpired on-chain. Cache a one-shot anchor in SharedPreferences / UserDefaults (mirroring the JS SDK's refundWindowClosed-gated anchor logic) and verify fully offline against the local clock once the 14-day refund window has closed.

What's a "generation" and when does it change?

A generation is the version number the zkApp writes to its own on-chain state during initialize(). Every deployed LicensingApp starts at generation 1. When the platform coordinates a redeploy — either because Mina executes a hardfork that unfreezes the contract's impossibleDuringCurrentVersion permission, or because a security issue in the current generation needs to be cut off — the version constant is bumped and every app is redeployed to a fresh address with the new generation number stamped in.

What it means for your app. The SDK's SUPPORTED_LICENSING_APP_VERSIONS list gates which generations the licensing service will admit. During a migration window it contains both the old and the new generation, so buyer installs shipping either SDK version keep working. Once the platform prunes the list to just the current generation, verifies against a retired generation start failing on the buyer's next SDK call — this is the enforcement mechanism for a security-driven bump, and it's why no revocation list or CRL fetch is ever needed.

What buyers see. Nothing, in the common case. The mechanism is backend-side address routing: whichever zkApp address the buyer's app pinned at build time (typically the original generation-1 address) stays valid forever. The licensing service resolves the pinned address to the app's deploymentHistory and verifies license state against the current live contract. A buyer whose license record still lives on a retired contract (purchased pre-bump, never migrated) also keeps working: the licensing service walks retired addresses newest-first and honors the license against whichever contract's on-chain map it lives on.

What still verifies against the retired generation. Historical attestation, refund claims, and cross-generation migration all continue to work forever against retired addresses — the archive retains the events, and the verify service walks each app's deploymentHistory to serve proofs from whichever generation issued the license. Renewals do not work against retired generations: the SDK's renew flow detects the retirement from the platform-signed GET /apps/:appId payload and swaps in a two-step Migrate & Renew sequence that ports the paid credit forward before renewing on the current generation.

Continuity across the freeze. If a buyer's expiry falls inside the pre-hardfork freeze window (renewals are suspended there so escrows can close — see Terms §8.2), the migration step bumps the successor-tree expiry forward to the fork activation slot. The 7-day grace window then runs from fork activation, so a renewal any time in that week stacks on the bumped expiry rather than resetting. No buyer loses renewal continuity purely because their expiry landed in the freeze window.