Licensing model
How a licence is represented, how a camera gets one, and what the enforcement actually stops. Two revenue lines run at once: per unit, and per API use.
| Line | Charged on | Enforced by |
|---|---|---|
| Per unit | Each physical camera activated | Batch seat accounting + device binding |
| Per API use | Metered inference calls | On-device quota + usage spool → invoice |
Commercial terms are per programme; this page documents the mechanism, not the numbers.
1. The token#
A licence is a signed, self-contained string:
XCAICX1.<base64url(payload_json)>.<base64url(ed25519_signature)>The signature covers the ASCII bytes XCAICX1.<base64url(payload)> — the encoded payload, not the decoded JSON.
Note
That detail matters more than it looks. Verification never depends on signer and verifier agreeing on JSON key order or whitespace, which is the single most common way licensing systems break in production. The C++ verifier and the Python signer are independent implementations; the test suite runs tokens minted by one through the other precisely to catch drift.
Decoded payload:
{
"lic": "LIC-D3ACF708DB6048AB",
"sku": "oem-industrial",
"sub": "CUST-DA6871393C",
"iss": "xcaicx",
"dev": "b2cd209a6826bd8b33c0fdaad6304106",
"ent": {"detect": true, "defect": true, "ppe": true, "anpr": false},
"lim": {"max_streams": 8, "max_fps": 30, "api_calls": 0},
"iat": 1786584928, "nbf": 1786584928, "exp": 0,
"grace_days": 30,
"act": "optional",
"batch": {"id": "B-2026-1C4417D5", "units": 5000}
}| Claim | Meaning |
|---|---|
lic | Licence id. What revocation refers to. |
sku | Catalogue entry this was issued against |
sub | Customer id |
dev | Bound hardware fingerprint, or "*" for an unbound batch key |
ent | Entitlement set. The SDK refuses to start a module whose bit is false. |
lim.max_streams | Concurrent stream cap |
lim.max_fps | Per-stream FPS cap |
lim.api_calls | Metered quota; 0 means unmetered |
iat / nbf / exp | Issued-at, not-before, expiry. exp: 0 is perpetual. |
grace_days | Days past exp during which everything still runs |
act | required forces the per-unit activation exchange; optional does not |
batch | Batch id and purchased unit count, on batch keys |
Signing is Ed25519. The private key lives only on the licensing server (or an HSM). The public key is compiled into the SDK, so a camera can verify a licence with no network and nothing on the device can mint one.
You can inspect any token locally:
xcaicxctl inspect unit.token --pubkey server/keys/root_ed25519.pub2. Per-unit licensing#
OEM path — royalty per manufactured camera#
- Sell an OEM a batch of N units:bash
xcaicxctl batch --customer CUST-CAMCO --sku oem-industrial --units 5000
- They receive one batch key —
dev: "*",act: "required",batch.units: 5000— and install it on the factory provisioning line. It is a licence to activate, not a licence to run: presented to an engine directly, it fails withACTIVATION_REQUIRED.
- Each camera, on first boot, calls
POST /v1/activatewith its hardware fingerprint. The server:- checks the batch has units left (
402 Payment Requiredif exhausted), - mints a unit-bound licence —
dev= that camera's fingerprint,act: "optional"
- decrements the batch counter by one.
- checks the batch has units left (
- The camera stores the unit token in
state_dirand uses it from then on.
That decrement is the billing event. One activation, one unit.
Careful
Re-activation is free, by design. A device already on file gets its existing token back and consumes no additional unit. A factory reflash, an SD-card swap or a firmware reinstall must not be a billing event — treating one as a billing event is the fastest way to lose an OEM account.
End-user path — subscription per camera#
Sell an edge-* SKU directly. Each camera gets its own licence with a term (exp) and a grace_days window:
xcaicxctl license --customer CUST-PLANT7 --sku edge-industrial \
--device 3f2a9c... --days 365Air-gapped path#
Pre-provision device-bound tokens offline; the camera never touches a network. See Air-gapped deployment.
xcaicxctl mint --sku edge-industrial --customer CUST-PLANT7 \
--device 3f2a9c... --days 365 \
--key keys/root_ed25519.key -o unit.token3. The device fingerprint#
This is the most important decision in an integration, because it determines how hard your product is to clone. The SDK derives a fingerprint in this order:
XCAICX_DEVICE_IDenvironment variable ← use this/sys/firmware/devicetree/base/serial-number(ARM SoC)/sys/class/dmi/id/product_uuid(x86)/etc/machine-id- macOS
IOPlatformUUID(dev workstations) - First non-loopback MAC address ← weak, and reported as such
An explicit override wins outright — you know your hardware better than anything the SDK can sniff:
export XCAICX_DEVICE_ID="$(cat /sys/fsl_otp/HW_OCOTP_CFG0)"Fingerprints derived from a MAC address are flagged weak_binding on the activation record and surface in xcaicxctl fleet, so you can see which of your units are trivially clonable.
Note
The SDK still runs on a weak fingerprint. Refusing to start a customer's production line over a fingerprint technicality is worse than the revenue at risk.
4. Expiry, grace and clock rollback#
- Past
expbut withingrace_days: state isXCAICX_LIC_GRACE. Everything still works. Surface it as a warning so the customer renews before the hard stop. - Past
exp + grace_days:LICENSE_EXPIRED. Analytics stop; the video path must not. - The SDK persists a clock high-water mark in
state_dir. A clock set behind it yieldsLICENSE_CLOCK_ROLLBACK, so winding the RTC back does not buy another year.
A camera with a dead RTC battery that boots at epoch will trip rollback detection. That is the correct behaviour, and the fix is NTP or a battery — but it is worth knowing before a technician calls you about it.
5. Revocation#
xcaicxctl revoke LIC-D3ACF708DB6048AB --reason chargebackRevocation propagates through GET /v1/revocations, which every online SDK pulls on heartbeat and caches in state_dir/crl.json. A revoked licence is refused at the next engine start and blocked from reporting usage immediately.
An offline camera keeps working until it next reaches the network. That is inherent to offline licensing, and if it matters for your deployment the answer is a shorter term rather than a longer revocation list.
6. What enforcement actually buys you#
Each row is covered by the 45 checks in core/tests/test_license.cpp:
| Attack | Result |
|---|---|
| Flip a byte in the payload | BadSignature |
Re-encode payload with anpr: true, keep the signature | BadSignature |
| Copy a licence to another camera | WrongDevice |
| Wind the clock back a year to dodge expiry | ClockRollback |
| Use a token past its term | Expired, after grace_days |
| Use a revoked licence | Revoked |
| Forge a batch key with your own Ed25519 key | 403 at activation |
| Present a validly-signed batch key we never sold | 403 — no seat behind it |
| Replay a usage record to avoid billing | Deduped, duplicates: 1 |
| Reboot to reset the metered counter | Counters resume from disk |
You can exercise every one of these yourself in the token lab on xcaicx.com, which runs real Ed25519 against a throwaway keypair generated in your browser.
Be honest about the threat model#
Do not
Offline licensing is deterrence, not DRM. An attacker with root on the camera and a disassembler can patch out the check — that is true of every software licensing scheme that runs on hardware the attacker controls.
What this design does buy:
- Casual copying is dead. Device binding plus signatures stop the realistic threat: an integrator cloning one paid image onto fifty cameras.
- Entitlements cannot be widened. Buying a detection-only SKU and enabling ANPR requires forging Ed25519.
- Seat accounting is server-side. Unit counts cannot be inflated on-device.
- Tamper leaves evidence. Weak fingerprint sources are recorded per activation; a cluster of weakly-bound units on one customer is worth a phone call.
If you need more, the escalation is hardware: bind to a secure element or fused SoC serial via XCAICX_DEVICE_ID and have the bootloader verify SDK integrity. The SDK supports that today — it is a deployment decision, not a code change.
7. Protecting the signing key#
It is the root of trust for every licence you will ever issue.
- Production: KMS or HSM, never on the API server's disk.
server/keys/*.keyis gitignored;keygenwrites it mode 600 and refuses to overwrite without--force.- Rotation:
license_pubkeyin the engine config overrides the compiled-in key at runtime, so a rotation can be staged through configuration before a firmware release makes it permanent.
More in Security & threat model and Running the authority.