Architecture
1
Check permission
Server-side, from the resolved actor’s capabilities — never trusted from the client.
2
Capture state before
Read the account’s current state before making any change.
3
Perform the KeyAuth call
Execute the mutation against the seller or application API.
4
Re-read and compare
Read the account’s state again afterward and compare it to what was expected.
5
Record the outcome
Write the result to the immutable audit timeline — success or failure.
success: true is not proof the change actually took effect, and the UI never presents it as
though it were.
Both existing KeyAuth clients are reused unchanged underneath this layer. The application client
(API 1.3) verifies an Ed25519 signature over the response on every call. The seller client
handles all administrative operations and redacts the seller key from every error path. Both
share the same sliding-window rate limiter, so a reconciliation sweep can never starve
interactive commands.
Permissions
Seventeen capabilities rather than one admin bit, because the person who should be able to check an expiry is rarely the person who should be able to delete an account.license.view_sensitive, license.hwid_override, license.revoke, license.delete,
license.transfer, license.bulk, and license.admin are treated as sensitive — they
expose secrets or destroy data, and are never granted implicitly by a preset.
Resolution order
- Configured admins and the guild owner hold every capability. The existing permission model is preserved rather than replaced, so nobody gets locked out of a bot they already administer.
- Otherwise, the union of capabilities granted to the member’s Discord roles.
- Otherwise, nothing. Default deny.
Presets
Two presets exist for convenience, applied with/licops permissions preset:
Neither preset includes anything destructive —
revoke, delete, transfer,
view_sensitive, bulk, and admin are always granted explicitly, one at a time.
Customer linking
Ownership is proven by license key, never by password./license link resolves the key through
the seller API, confirms it belongs to a real account, and records
discord_id → keyauth_username.
A unique index on the username means one KeyAuth account can be claimed by exactly one Discord
user — without it, anyone who learned a key could bind themselves to an account somebody else
controls and then reset its hardware ID. A Discord user holds at most one link; transferring
onto an already-linked user is refused rather than silently orphaning the account they had.
Hardware ID management
Nobody ever sees or types a hardware ID. The bot clears the binding server-side; KeyAuth storeshwid = NULL, and the next login from any machine binds that machine automatically — the
customer just opens their loader and signs in.
Reset policy
Configured with/licops policy set:
Staff resets do not consume a customer’s allowance — a support agent helping someone should not
burn the quota that person needs.
Subscription operations
Quick buttons on the panel offer 1 / 7 / 30 / 90 days; a modal takes a custom value, where a
negative number shortens the subscription. Every change records the before state, the requested
change, the after state, the acting staff member, a timestamp, and a reason.
Disable, revoke, delete
Three different KeyAuth operations with different reversibility, and the layer never conflates them:
All three require confirmation and a reason captured in a modal. Delete is verified by absence:
the account is re-checked afterward, and if it still exists the operation is reported as
failed, not succeeded.
Disable is the one case that can’t be verified at all —
userdata exposes no paused flag — and
the result says so rather than implying a check that didn’t happen.
Role synchronisation and reconciliation
Roles sync when a license is linked, enabled, disabled, revoked, or expires; when a customer rejoins the server; on a manual/licops roles sync; and during reconciliation.
Only mapped roles are ever touched. A sync that computed “roles this member should have” and
removed everything else would strip moderator and booster roles on first run. Mapping-time
validation also rejects a role positioned at or above the bot’s own, which is the most common
cause of a silent sync failure.
Reconciliation
The KeyAuth dashboard is a fully functional second writer — someone can always change a license there directly. Reconciliation re-reads accounts from KeyAuth, corrects the local mirror, applies any resulting role changes, and alerts on drift. Triggered with/licops reconcile, or on a configurable recurring schedule. It is
batched, ordered by staleness (so an interrupted run resumes naturally), idempotent, serialised
by a lock, and stops immediately on a rate limit rather than spending the remaining budget —
breaching KeyAuth’s 30 requests / 30 seconds costs 30–60 minutes of downtime. Drift is always
resolved in KeyAuth’s favor; the mirror is a cache, never the source of truth.
What KeyAuth cannot do
Four features ship deliberately reduced, because KeyAuth’s seller API does not offer the full version of what you’d expect. Each is labelled in the bot’s UI at the point where it’s used, not only here.No authentication history
The seller API exposes no per-user authentication log.userdata returns lastlogin, ip, and
hwid — one data point, not a history.
Consequence: the panel shows last known activity only. There is no session-history view,
because there is no data to build one from. Use the KeyAuth dashboard for anything more.
No absolute expiry setter
KeyAuth offers onlyextend (add) and subtract (remove). There is no “set expiry to date X.”
Consequence: “Set Expiration” computes a delta from the expiry read moments earlier. This is
a read-modify-write and is not atomic — a dashboard edit landing in between would make the
result wrong. The result reports the expiry KeyAuth actually ended up with, and warns when it
differs from what was asked for by more than a day.
No license transfer
assignkey binds a key to a user, but nothing in KeyAuth’s API moves an existing account or
subscription between users.
Consequence: /license transfer moves the Discord association only. KeyAuth-side
ownership is unchanged, and the result says so explicitly. Doing more would mean deleting and
recreating the account, losing its history in the process.
No reset allowance
Covered above under Hardware ID management — KeyAuth has no counter and enforces no quota of its own.Other limits
- Rate limit: 30 requests / 30 seconds; a breach costs 30–60 minutes.
/license searchruns against the local mirror for this reason, not a live call. - No server-side search:
fetchallkeysreturns everything at once, with no filter or cursor. Mirror refresh is explicit and staff-triggered. - Email: not reliably exposed and not searchable. Email lookup is refused with an explanation rather than silently returning nothing.
- HWID search: covers mirrored licenses only — KeyAuth itself cannot search by hardware ID.
- Paused state: invisible in
userdata, so an active-looking account may actually be disabled until reconciliation runs. The panel warns about this. - Seller key: GET-only, sent in the query string, account-wide, and unscopeable. IP-whitelist it — see Configuration.
Data model
Nine additive tables. No existing table was changed to add this layer.
Never stored: plaintext license keys (a SHA-256 fingerprint plus a display mask instead),
passwords, full hardware IDs (prefix and hash only), or IP addresses.
Audit
Every mutation writes to the action timeline with the action, actor, actor kind (staff / customer / system), before state, after state, reason, success, and timestamp. Failures are recorded too — a run of unsuccessful link attempts is the signature of someone guessing at license keys. The table is append-only; there is no update or delete path, because an audit trail staff can edit is not an audit trail. View it with the History button on a license panel, or/license customer → History for a rollup.
Alerts
Enabled by configuring an alert channel. Every alert passes a deduplication gate first — without one, a sweep over a few hundred licenses would post a few hundred messages and the channel would be muted within a day.Rollout checklist
Before enabling License Operations in production:1
IP-whitelist the seller key
In Seller Settings. This layer makes many more seller calls than the base bot, which raises
the value of the whitelist.
2
Confirm HWID checking is enabled
Check with
/licops status.3
Grant capabilities deliberately
Start with the
Support preset rather than hand-picking capabilities for a first rollout.4
Set a reset policy before enabling self-service
Otherwise customers reset with no limit at all.
5
Map roles
Check the bot’s role sits above every role you map.
6
Run /licops reconcile once
Populate the mirror before relying on
/license search.7
Only then enable the recurring reconcile schedule
Initial mirror population already spends real API budget — do that in batches of roughly 50
rather than one large sweep, then schedule ongoing sweeps once the mirror is warm.