Skip to main content
License Operations is a Discord-native control plane for KeyAuth customer support: look up a license, reset a hardware ID, adjust a subscription, disable an account, and keep Discord roles in step — without opening the KeyAuth dashboard.
Before relying on any of this operationally, read What KeyAuth cannot do below. Four features ship deliberately reduced because KeyAuth’s API does not support the full version, and each is labelled in the bot’s UI at the point of use, not only here.
KeyAuth remains authoritative. This layer never becomes the source of truth for license state. Everything it stores is either a clearly-marked cache or data KeyAuth has no concept of: local reset policy, staff notes, Discord associations, and a record of what the bot itself did.

Architecture

No cog performs HTTP directly. Every read and write goes through the license service, which:
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.
The re-read step is why results distinguish applied from verified. A KeyAuth call returning 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

  1. 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.
  2. Otherwise, the union of capabilities granted to the member’s Discord roles.
  3. 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 stores hwid = 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:
All of this is enforced by the bot, not KeyAuth. KeyAuth stores a single cooldown timestamp, keeps no counter, and its resetuser call succeeds regardless of whether that cooldown has elapsed. The reset ledger this bot maintains is the real enforcement — the UI says “policy” wherever it shows an allowance, precisely so it isn’t read as a KeyAuth guarantee.
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 only extend (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 search runs against the local mirror for this reason, not a live call.
  • No server-side search: fetchallkeys returns 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.