Build a key-generation service of your own and plug it into KeySystem.API
How it plugs in
Add an entry with Type = "custom" to KeySystem.API, next to (or instead of) Platoboost/PandaAuth/Jnkie. Put whatever fields you want in it — ServiceId, a name, anything — you'll get them back inside your functions.
CheckKey(key, ctx) is required. It's called with what the player typed, and must return valid, reason.
GetKeyLink(ctx) is optional. It powers the "Get a key" button, runs when the player clicks it, and must return a URL, or nil, "reason" on failure.
ctx is every field you put on the entry, plus ctx.Request — the executor's HTTP function (request/http_request/syn.request/http.request, whichever exists), or nil if the executor has none.
Don't just compare a string
The whole script — including anything you hardcode inside CheckKey — runs on the player's own device. A fixed key, or a secret used to compute one locally, can be read straight out of the running script (even minified/obfuscated) with basic executor tools. Whatever decides "valid" has to live on a server you control, not inside the function.
Recommended design: sessions
Give your backend three states for a key request: pending → issued → consumed.
1. GetKeyLink asks your backend to start a session. It replies with a session_id and a url (your ad page/whatever, with the session id attached).
2. The player completes your flow on that page. Your backend generates the key there and marks the session issued.
3. CheckKey sends the session_id together with the key. Your backend checks they match an issued session and, if so, marks it consumed before answering.
Someone capturing that last request and replaying it gets nothing: the session is already consumed, so your backend refuses it a second time. Give pending sessions a short expiry too, so an unused one can't be used to skip your flow later.
Note this only protects the acquisition step. Whether the issued key itself should keep validating afterwards (for example with SaveKey = true, so the player isn't asked again next session) is a separate decision your backend makes — e.g. by giving the key its own expiry, independent of the session that produced it.
Example
This is a starting point, not a drop-in service — the URLs are placeholders for your own backend.
local HttpService = game:GetService("HttpService")
local Ophyn = loadstring(game:HttpGet('https://ophyn.space/interface'))()
local session -- remembers the session between GetKeyLink and CheckKey
Ophyn.new({
Title = "Ophyn",
Description = "Key System",
Logo = "rbxassetid://111673746737789",
Theme = "Plant-Dark",
Folder = "Ophyn-KSY",
getkey = true,
discord_link = "discord.gg/yourcode",
Discord = "true",
Website = "false",
KeySystem = {
SaveKey = true,
API = {
{ -- your own service
Type = "custom",
ServiceId = "my-service", -- read back as ctx.ServiceId-- Called when the player clicks "Get a key".-- Your backend creates a session and hands back where to send them.
GetKeyLink = function(ctx)
ifnot ctx.Request thenreturnnil, "your executor doesn't support HTTP requests."endlocal ok, res = pcall(ctx.Request, {
Url = "https://yoursite.com/api/session/start",
Method = "POST",
Headers = { ["Content-Type"] = "application/json" },
Body = HttpService:JSONEncode({ service = ctx.ServiceId }),
})
ifnot ok ortype(res) ~= "table"or res.StatusCode ~= 200thenreturnnil, "Could not start the key flow"endlocal data = HttpService:JSONDecode(res.Body)
session = data.session_id -- kept for CheckKey belowreturn data.url
end,
-- Called with whatever the player typed into the key box.
CheckKey = function(key, ctx)
ifnot session thenreturnfalse, "Get a key first"endifnot ctx.Request thenreturnfalse, "your executor doesn't support HTTP requests."endlocal ok, res = pcall(ctx.Request, {
Url = "https://yoursite.com/api/session/validate",
Method = "POST",
Headers = { ["Content-Type"] = "application/json" },
Body = HttpService:JSONEncode({
session_id = session,
key = key,
}),
})
ifnot ok ortype(res) ~= "table"or res.StatusCode ~= 200thenreturnfalse, "Could not reach the key server"endlocal data = HttpService:JSONDecode(res.Body)
return data.valid == true, data.reason
end,
},
},
},
Callback = function(key)
-- your script hereend,
})
What your backend needs
• Start a session — create a session_id, store it as pending with an expiry, return it with a URL.
• Issue the key — on your flow page, once the player finishes, generate the key, store it against that session_id, mark it issued.
• Validate — check the session_id is issued and the key matches; mark it consumed and answer valid = true. Anything else (unknown, pending, already consumed, expired, wrong key) answers valid = false with a reason.
What this doesn't solve
This stops a captured request from being replayed to skip your flow. It doesn't stop someone who legitimately has a still-valid key from handing it to someone else — nothing running on the player's device can prevent that, because the player's device is what you'd have to trust to enforce it. If key-sharing matters to you, that's a separate decision to make in how long a key stays valid and what you tie it to.