> **Project: Roblox Script Key System**
>
> Build a full-stack key licensing system for Roblox scripts. Tech stack: Node.js backend, MySQL or SQLite database, plain HTML/CSS/JS frontend. I own `saiops.cc`, `api.saiops.cc`, and `key.saiops.cc`.
>
> ---
>
> **DATABASE SCHEMA — create tables for:**
>
> `keys` table:
> - `key` (unique string)
> - `created_at` (timestamp)
> - `expires_at` (timestamp)
> - `hwid_limit` (int — 0 = unlimited, 1 = single HWID lock, N = N unique HWIDs)
> - `first_used_at` (timestamp, nullable)
> - `duration_days` (int)
> - `is_active` (boolean)
> - `note` (optional label, e.g. "given to user X")
>
> `sessions` table (one row per script execution):
> - `id` (auto increment)
> - `key` (foreign key)
> - `hwid` (string)
> - `username` (Roblox username)
> - `executor` (string, e.g. "Synapse X", "Fluxus")
> - `game_id` (Roblox PlaceId)
> - `instance_id` (Roblox JobId for server join)
> - `started_at` (timestamp)
> - `last_ping_at` (timestamp — updated every 30s to track session duration)
>
> `hwid_bindings` table:
> - `key` (foreign key)
> - `hwid` (string)
> - `first_used_at` (timestamp)
>
> ---
>
> **API ENDPOINTS at `api.saiops.cc`:**
>
> `POST /validate`
> - Body: `{ key, hwid, username, executor, game_id, job_id }`
> - Logic:
>   1. Check key exists and is not expired
>   2. Check HWID limit — if `hwid_limit = 0`, allow anyone. If `hwid_limit = N`, check how many unique HWIDs are in `hwid_bindings` for this key. If under limit, add HWID if new. If over limit, reject.
>   3. Log session row
>   4. Return: `{ valid: true, message: "OK" }` or `{ valid: false, reason: "..." }`
>
> `POST /ping`
> - Body: `{ key, hwid, job_id }`
> - Updates `last_ping_at` for the matching active session
> - Used to track how long user is running the script
>
> `GET /keyinfo?key=XXXX`
> - Returns: created_at, expires_at, hwid_limit, hwids bound, first_used_at, is_active, remaining days
>
> `POST /admin/generate` (protected by secret admin token in header)
> - Body: `{ duration_days, hwid_limit, note }`
> - Generates a random key (e.g. `SAI-XXXX-XXXX-XXXX`), inserts into DB, returns key
>
> All endpoints should use CORS headers allowing only `saiops.cc` and `key.saiops.cc` as origins. Admin endpoint must require `Authorization: Bearer ADMIN_SECRET` header.
>
> ---
>
>
> Admin page at `saiops.cc/admin` (hidden from nav) — password protected via localStorage token:
> - Form to generate a key: input for duration (days), HWID limit, optional note
> - Calls `api.saiops.cc/admin/generate`
> - Shows generated key with copy button
> - Table showing all keys from DB with columns: key, created, expires, hwid_limit, HWIDs used, first used, active/expired status
>
> ---
>
> **FRONTEND — `key.saiops.cc`:**
>
> Key info lookup page:
> -  box to show the key
> - Calls `api.saiops.cc/keyinfo`
> - Displays: status (active/expired), expiry date, time remaining, HWID slots used/total, when key was first used
>
> ---