# Licensing

PRIOR is proprietary, licensed software. Steering requires a valid, ED25519-signed license. Paid
licenses are **node-locked** (bind-on-first-use); the free trial is **expiry-only** and binds to
nothing. This page explains the tiers, what a license binds, and how to apply one. For plans,
pricing, and the pre-purchase FAQ, see
[eagle-logic.com/get-prior](https://eagle-logic.com/get-prior) or contact
**sales@eagle-logic.com**.

---

## What a license grants

- The engine verifies the license **locally** against a baked ED25519 public key; verification needs
  no server and **never happens in the inference path**. Connected tiers make a periodic background
  license check-in (see [Connected vs. air-gapped licensing](#connected-vs-air-gapped-licensing));
  air-gapped tiers make none.
- Without a valid license, PRIOR still runs, but **degrades to plain inference with no gate
  actuation** (`x-prior-steering: disabled`). You are never locked out of the model; you just don't
  get policy enforcement.
- A license binds: **customer id, hardware id, tier, expiry, and capabilities.** An expired,
  tampered, or wrong-host license is rejected.

At 1.0–1.2, **every paid tier, and the free trial too, ships the full capability set.** Tiers differ
only by **node quantity, term, and support**, not by which features you get.

---

## Tiers

| Tier | Price | License files (nodes) | Term | Node-lock | Fulfilment |
|---|---|---|---|---|---|
| **Free Evaluation** | $0 | 1 | 30 days (hard) | none (expiry-only) | email → instant |
| **Single Node** | $995 / yr | 1 | 1 yr | bind-on-first-use | Stripe → instant |
| **Team Pack** | $2,450 / yr | 5 | 1 yr | bind-on-first-use | Stripe → instant |
| **Platform** | $4,950 / yr | 12 | 1 yr | bind-on-first-use | Stripe → instant |
| **Enterprise / Sovereign** | Custom ($10k+) | N / unlimited | Custom | bind-on-first-use *or* pre-bound | Contact sales |

- **The metric is the node, not the seat.** One PRIOR container fronts as many application seats as
  you like; you license the running instances.
- **A node pack is N independent single-node licenses**, not one counted license. A 5-node Team Pack
  is five `.lic` files, each independently node-locked on first use.
- **The free trial is full-featured**, not a crippled binary. It is a 30-day, all-capabilities
  license with no node binding, so trial → paid is a pure license swap.
- **Air-gapped** is its own SKU, priced per site or fleet, issued on request. See
  [Connected vs. air-gapped licensing](#connected-vs-air-gapped-licensing).
- **Enterprise** covers custom procurement, >12 nodes, and long-lived (non-refreshing) licenses.

### Engine tier mapping

The website SKUs map onto the engine's `LicenseTier` enum:

| SKU | Engine tier |
|---|---|
| Free Evaluation | `TRIAL` |
| Single / Team / Platform | `PRIVATE` (self-hosted, node-locked) |
| Air-gapped (per site / fleet) | `AIR_GAPPED` |

---

## Node-locking (bind-on-first-use)

Paid licenses ship **unbound**. On first boot the engine resolves the node's stable identity and
binds the license to it; thereafter that file only validates on that node.

- The bound identity is a **node-identity token**, not literally bare metal. The engine resolves a
  stable identity (a hardware id, a cluster id, or a volume id, depending on the environment).
- **Trial** licenses skip binding entirely (expiry-only), so evaluation is frictionless.

### Moving a licence to another node

Since **1.2.0** this is self-service and immediate — no support ticket, and it does not consume or
burn the licence. Release it from the old node:

```bash
curl -s -X POST http://localhost:8089/admin/license/deactivate \
  -H "Authorization: Bearer $ADMIN_KEY"
```

The replacement node claims it on its next start, with **no waiting period**. Steering stops on the
released node the moment it is released, so at most one node is ever steering on a licence — which is
why the handover can be instant rather than gated behind a timeout.

Two things worth knowing:

- **Keep `PRIOR_STATE_DIR` on a persistent volume** and a container rebuild keeps its identity, so
  there is nothing to transfer at all. This is the cheapest answer to redeploys and rescheduled pods.
- If a node dies *without* releasing its licence, a replacement takes over automatically once the old
  one has been silent for a few days. You do not need to call us — but releasing it explicitly, as
  above, is instant, so prefer that whenever the old node is still reachable.

---

## Applying a license

**At deploy time**, mount the file. There is no key to supply — the verification key is **compiled
into the engine**, so a licence we signed validates out of the box:

```bash
docker run -d --name prior --gpus all \
  -p 127.0.0.1:8089:8089 \
  -e PRIOR_NATIVE_MODEL=/models/model.gguf \
  -v /srv/models:/models:ro \
  -v /path/to/license.bin:/app/license.bin:ro \
  -v prior-state:/app/state \
  ghcr.io/eagle-logic/prior:slim
```

> Earlier docs showed a `PRIOR_LICENSE_PUBLIC_KEY` environment variable here. **It no longer exists.**
> Baking the trust key into the environment meant the engine read it back at runtime, so anyone could
> point a published image at a key they held the private half of. Rotating the verification key is a
> release, not a deployment setting — and if you set that variable today, nothing happens.

**At run time**, upload it live (no restart) via the API or the WebUI:

```bash
curl -s -X POST http://localhost:8089/admin/license/upload \
  -H "Authorization: Bearer $ADMIN_KEY" \
  --data-binary @license.bin
```

**Check status:**

```bash
curl -s http://localhost:8089/admin/license/status -H "Authorization: Bearer $OPERATOR_KEY"
```

Returns the variant, tier, capability list, expiry + days remaining, and customer id. The WebUI's
**Access → License** panel shows the same, with an Active/Unlicensed badge and an upload control.

Since 1.1.0 the status also reports **`Degraded`** — the license is structurally valid, but its
check-in was refused or has gone stale past the window, so steering is off on this node until it
reconnects. `Degraded` is distinct from `Expired` (the signed window actually lapsed) and from
`Unlicensed` (no license present).

---

## Connected vs. air-gapped licensing

> **Running an engine older than 1.2.0? Upgrade it.** Check-in now proves possession of the signed
> license, and engines from 1.1.0 and earlier do not send that proof, so their check-in is refused.
> The failure is silent: the engine keeps answering and keeps steering until its check-in window
> lapses, and then steering switches off and the model passes through unchanged. The upgrade is a
> pull and a restart, your license file is unchanged and needs no reissue, and
> `x-prior-steering: enabled` on any response is how you confirm it took.

- **Connected self-serve tiers** (`PRIVATE`, `CLOUD`) make a periodic, PII-free **license check-in**
  to the license server, roughly every two weeks (`PRIOR_LICENSE_CHECKIN_DAYS`, default 14). It
  carries only the license id and an opaque per-node identifier, never prompts, completions, or model
  data. The check-in confirms the subscription is active and that the node holds its one seat; a
  node that is refused (seat taken elsewhere, or subscription cancelled) or that cannot check in for
  the full window **degrades to plain, unsteered inference** on that node until it reconnects. The
  same loop auto-refreshes an approaching-expiry license (`PRIOR_LICENSE_REFRESH_URL`, or the server
  default). None of this happens in the inference path; it is a background task.
- **Air-gapped** licenses make **no outbound traffic of any kind**, at any point. In exchange they
  require a one-time **offline activation**: the engine prints a challenge, we sign it, and you paste
  the signed response back. Until that response is applied the license loads but does **not** steer,
  so plan the handshake as part of installation rather than after it. The license itself is
  long-lived and does not refresh.
- Because node counting cannot be enforced offline, air-gapped licenses are **priced per site or
  fleet** rather than per node. `PRIOR_CLUSTER_ID` gives every node at a site one shared identity, so
  they emit the same challenge and are activated by the same response. This is a standard SKU, but it
  is issued by hand rather than self-serve: ask us and we will cut one.

---

## Local development

**There is no licence bypass.** A licence verifies or it does not load — local development included.

`PRIOR_LICENSE_MODE=dev` used to skip verification and **was removed**: it was readable from the
environment, so a published image could be unlocked with a single `docker run -e`. Nothing replaced
it, and setting it today has no effect at all.

For local work, build with the `dev-license` cargo feature. That compiles in a **second trust
anchor** — a dev public key — alongside the production one, so a locally signed licence validates on
your own build and nowhere else. Production images compile that anchor out entirely, so the dev key
cannot be made to work in the field, and the production signing key never touches a dev box.

---

See also: **[Deployment](deployment.md)** · **[Concepts](concepts.md)**.
