Licences, activation and the extension manager

How the .lic files, the activation routes, the six-hourly heartbeat and the per-product status flags actually work, and why a working install stops working after a move or a firewall change.

8 min readUpdated 6 August 2026licence, activation, extensions, heartbeat, fingerprint

Licensing is the single most common way a working Bicrypto install stops working. Almost always it is one of three things: the machine changed, the server lost outbound HTTPS, or a product is licensed but its status flag is still off. This page is the model behind all three.

What a licence actually is

Activation writes one file per product: lic/<productId>.lic, in a lic/ directory at the project root.

That file is an AES-256-GCM envelope. The encryption key is derived with scrypt from a shipped secret plus this host's hardware fingerprint, so the file can only be decrypted on the machine that wrote it. There is no fallback and no degraded mode — a .lic presented on different hardware simply fails to authenticate, and the product reads as unlicensed.

Copying lic/ to a new server, restoring a backup onto different hardware, cloning a VM, or in some hosting environments a hardware change under a running VM, all produce files that cannot decrypt. It does not fail over. It fails shut.

Never carry lic/ across a server move. Reactivate on the new box with the purchase codes instead. lic/ is deliberately excluded from the backup guidance in Backup and restore for exactly this reason.

lic/ must be writable by the user PM2 runs the backend as. Activation writes atomically (temp file, then rename) into that directory, so a non-writable directory fails activation rather than corrupting anything — but the installer's blanket permission pass sets every file to 644 and chowns the tree to the app owner, which can leave a licence written earlier as root unwritable by the app.

ls -ld lic/
ls -l  lic/

Two independent flags decide whether a feature is on

This is the distinction that costs operators the most time.

Flag Where it lives What it controls
The licence lic/<productId>.lic on disk Opens the licence gate. Without it, the product's API routes answer 403.
The status flag status on the product's extension, ecosystemBlockchain or exchange row Enables the feature: menus, screens, cron jobs, everything the platform reads to decide the product is in use.

A .lic alone unblocks the gate and does nothing visible. A status flag alone switches a menu on that then 403s. Both are needed, and they are set from different places — the licence from the activation screen, the status flag from the extension manager's toggle.

The headless activation script does both in one pass, which is why it is the easier path when you have shell access.

The activation routes

All four are on the permission create.license.

Activates the core product or an addon from an Envato purchase code
Activates from an Envato licence file already dropped into /lic
Asks the licence service whether a purchase code is valid — writes nothing
Reports licence status, security level and any detected Envato file

The screen is /admin/system/license. It has no entry in the admin menu — you arrive there from an Extension or Exchange detail page, from the System Updates screen, or by typing the URL. With no query parameters it activates the core product; ?productId=<id> activates a specific addon, which is how those detail pages link to it. It also accepts ?return=/some/path to send you back afterwards and ?needsActivation=true when a gate redirected you there.

GET status is the one to read when something looks wrong. It returns licenseStatus (active, expired, revoked, invalid, not_found, not_activated), isValid, and a securityLevel from 0 to 4. If the cached status says invalid it forces a fresh revalidation against the file on disk before answering, so it self-heals the common "I activated but it still says no" case.

verify is deliberately not on the audit trail: it only asks the question. activate is the audited action.

The activate route catches its own errors and returns { success: false, message: "…" } with a 200 status, so the reason is in the body rather than the status code. Read the message line on screen; do not judge it by the response code.

Air-gapped activation

If the server cannot make outbound calls at activation time, download the licence file from Envato, put it in lic/ at the project root, and use the file-based option on the same screen. The backend reads the purchase code out of that file and activates with it.

This does not remove the need for outbound HTTPS afterwards — see the heartbeat below.

Headless activation for managed hosting

pnpm activate-product <productId> <purchaseCode> [clientName]

scripts/activate-product.mjs does exactly what the panel's activate flow does, without a browser, and prints a single JSON line. It runs the same server code — verify with the licence service and write the encrypted machine-bound .lic, flip the product's status flag on its extension / ecosystemBlockchain / exchange row, then revalidate so the gate opens without a restart. It prefers the compiled backend/dist and falls back to the source tree in development.

Why a screen can stay locked immediately after activating

Activation writes the file. It then has to make that file visible to a process that has already cached the pre-activation verdict in four independent places:

  1. The per-product licence validator cache — memory plus Redis. An invalid result is held for five minutes, and its Redis key embeds the process start time, so in practice only a restart used to clear it.
  2. SecurityManager's cached security level, which the enforcement gate turns straight into a 403 whenever it is below 2.
  3. The server's 60-second core-licence decision cache. The activation page redirects after about two seconds, so the first request after activation landed inside that window every time.
  4. The extension .lic existence cache inside the request middleware, with a five-minute TTL of its own.

Both activation routes now clear all four, in that order, before returning. It is best-effort by design: the licence is activated by the time that runs, so a failure in any step is logged rather than turned into a reported failure, and the next request self-heals.

If a screen is still locked a minute after a successful activation, restart the backend before you go looking for another fault.

The heartbeat, and the 72-hour grace period

A cron job named licenseHeartbeat runs every 6 hours. It collects every product on the install that has a purchase code and sends one batched request to https://updates.mashdiv.com, carrying:

  • the host hardware fingerprint,
  • the server's public IP address (looked up once a minute and cached; falls back to 127.0.0.1 if the lookup fails),
  • the domain, taken from the host part of NEXT_PUBLIC_SITE_URL (or APP_PUBLIC_URL),
  • version, Node version, platform, architecture, uptime, memory, load and timezone.

The response can mark a product revoked, expired or not_activated, which forces an immediate revalidation of that product, and can carry commands to clear a cache or force a revalidation.

When the licence service is unreachable the install keeps running on a grace period of 72 hours, and the same 72 hours is the maximum total offline time allowed per validation cycle. So a firewall change that blocks egress to updates.mashdiv.com on 443 produces a site that worked perfectly for three days and then started refusing. Allow that host outbound, permanently — it is a runtime dependency, not just an install-time one.

curl -sS -o /dev/null -w '%{http_code}\n' https://updates.mashdiv.com
pm2 logs cron --lines 200 | grep -i heartbeat

The heartbeat runs on the scheduler, so on a default deployment it is the cron process that needs the egress, not just the API.

One product changes a setting when its licence lapses

If the Chart Engine licence (product 61364182) comes back revoked or expired, the heartbeat rewrites your chart settings to TradingView — including display.chartType inside the binarySettings row. That is deliberate: the alternative is a chart area that renders nothing. If your binary chart silently changed from Chart Engine to TradingView, check that licence before you check anything else. See Binary options: switching it on.

Before activation, almost everything answers 403

An unactivated core refuses nearly every API route with 403. A short exempt list keeps the install recoverable — enough to sign in and reach the licence screen, and nothing more:

/api/auth
/api/user/profile
/api/settings
/api/admin/system/license
/api/admin/system/extension
/api/admin/finance/exchange/provider/active
/api/geo
/api/admin/system/geo-restriction

Plus /api/health, /uploads/, and /api/docs. Everything else is gated.

The two geo entries are on that list on purpose: a geo-blocked visitor must be able to load the page explaining why, and you must be able to correct a bad country rule — neither can sit behind licensing without creating a state nobody can get out of.

The System Updates screen behaves the same way: without a verified licence it replaces itself with a purchase-code activation form, so there is no update UI at all until activation succeeds. See Updating.

The extension manager

Admin → System → Extension Manager, at /admin/system/extension. The page is gated on access.extension.

Lists every extension, blockchain and exchange provider with licence and update status
Turns one product on or off
Turns several products on or off at once

The listing is the single place that shows both flags side by side. For each product it returns:

Field Meaning
status The status flag on the product's own row — whether the feature is enabled.
licenseVerified Whether lic/<productId>.lic exists on disk.
hasLicenseUpdate Whether the licence service reports a newer version than the installed one.
licenseVersion · licenseReleaseDate · licenseSummary What the licence service says about the latest release.

licenseVerified is an existence check on the file, done with a single directory read of lic/. It answers "is there a licence file for this product", not "does it decrypt on this machine". A .lic carried over from another server therefore shows as verified here and still 403s at the gate — which is the exact shape of the "the panel says it is licensed but the feature is dead" report.

The id in the status route is the product's productId, not a row id.

The status toggle updates the row and clears the settings cache. It leaves the .lic in place, so turning the product back on needs no reactivation. Turning it off is the correct way to retire a feature; deleting its .lic is not.

When activation will not go through

Work down this list; each step rules out one layer.

  1. Confirm outbound HTTPS from the server itself, not from your laptop.

    curl -sS -o /dev/null -w '%{http_code}\n' https://updates.mashdiv.com
  2. Confirm lic/ is writable by the app user.

    ls -ld lic/
    sudo chown -R <app-user>:<app-user> lic/
  3. Read the message, not the status code. The activate route answers 200 with success: false and a reason.

  4. Read the backend log.

    pm2 logs backend --lines 200 | grep -i license
  5. Confirm the account has create.license. The System Updates screen is a known trap here: the page is gated on access.system.update but every button on it calls a route requiring create.license, so a role with only the first sees a fully rendered screen and gets permission errors from every action.

  6. If it activated but the screen is still locked, wait a minute for the caches above, then restart the backend.

  7. If it worked and stopped after a move, reactivate on the new hardware. Do not copy the old .lic across.

For the symptom-first version of this list, see Troubleshooting.