Docs

Everything to lock your app with Dvv Auth keys and sell them on autopilot.

Quick start

  1. Log in, make an app. You get an App ID, an App secret (goes in your program) and a Seller key (only on YOUR servers - never in the program).
  2. Make keys on the dashboard (or let your shop make them - see Auto-selling).
  3. In your program: when it starts, send the key + a device ID to the check. If ok is true and the signature matches, let them in.
  4. Run /auth-setup app:YOUR_APP_ID in your Discord so buyers can reset their device with /reset-hwid.

Time keys (1 day, 30 days, 1 year...) start the first time the key is used. Lifetime keys never run out.

The check (any language)

POST https://auth.davibevilla.com/api/1/check
Content-Type: application/json

{ "app": "YOUR_APP_ID", "key": "XXXXX-XXXXX-XXXXX-XXXXX", "hwid": "device id", "version": "1.0", "nonce": "new random string" }

Answer:

{ "ok": true, "code": "ok", "msg": "Key OK", "key": "...", "expires": 1767225600000, "lifetime": false, "note": "", "t": 1762560000000, "sig": "hex" }

Codes: ok bad_key expired banned hwid_mismatch no_hwid paused version no_app slow. Show msg to the user - it's written for them.

Check the signature: sig = HMAC-SHA256 (key = your app secret) of nonce|ok|key|expires|t where ok is 1 or 0, as lowercase hex. Also check t (ms) is within 60 seconds of now. Without this, anyone can fake a server that always says yes.

"version" is optional: set one on the dashboard and older builds get version (update needed). Limit: 60 checks a minute per IP.

Go

package main

import (
	"bytes"; "crypto/hmac"; "crypto/rand"; "crypto/sha256"; "encoding/hex"; "encoding/json"
	"fmt"; "net/http"; "os/exec"; "strings"; "time"
)

const appID, appSecret = "YOUR_APP_ID", "YOUR_APP_SECRET"

type answer struct {
	OK bool `json:"ok"`; Code, Msg, Key, Sig string; Expires, T int64
}

func hwid() string { // Windows machine UUID
	out, _ := exec.Command("powershell", "-NoProfile", "(Get-CimInstance Win32_ComputerSystemProduct).UUID").Output()
	return strings.TrimSpace(string(out))
}

func Check(key string) (bool, string) {
	nb := make([]byte, 16); rand.Read(nb); nonce := hex.EncodeToString(nb)
	body, _ := json.Marshal(map[string]string{"app": appID, "key": key, "hwid": hwid(), "nonce": nonce})
	res, err := http.Post("https://auth.davibevilla.com/api/1/check", "application/json", bytes.NewReader(body))
	if err != nil { return false, "Can't reach the key server" }
	defer res.Body.Close()
	var a answer
	if json.NewDecoder(res.Body).Decode(&a) != nil { return false, "Bad answer" }
	ok := 0; if a.OK { ok = 1 }
	m := hmac.New(sha256.New, []byte(appSecret))
	fmt.Fprintf(m, "%s|%d|%s|%d|%d", nonce, ok, a.Key, a.Expires, a.T)
	good := hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(a.Sig))
	fresh := time.Since(time.UnixMilli(a.T)).Abs() < time.Minute
	return a.OK && good && fresh, a.Msg
}

func main() {
	var key string; fmt.Print("Key: "); fmt.Scanln(&key)
	if ok, msg := Check(key); !ok { fmt.Println(msg); return }
	fmt.Println("Welcome!")
}

Python

import requests, hmac, hashlib, uuid, subprocess, time

APP, SECRET = "YOUR_APP_ID", "YOUR_APP_SECRET"

def hwid():
    return subprocess.check_output(["powershell", "-NoProfile", "(Get-CimInstance Win32_ComputerSystemProduct).UUID"]).decode().strip()

def check(key):
    nonce = uuid.uuid4().hex
    j = requests.post("https://auth.davibevilla.com/api/1/check", json={"app": APP, "key": key, "hwid": hwid(), "nonce": nonce}, timeout=10).json()
    msg = f"{nonce}|{1 if j['ok'] else 0}|{j.get('key', '')}|{j.get('expires', 0)}|{j['t']}"
    good = hmac.compare_digest(hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).hexdigest(), j.get("sig", ""))
    fresh = abs(time.time() * 1000 - j["t"]) < 60000
    return j["ok"] and good and fresh, j["msg"]

ok, msg = check(input("Key: "))
if not ok:
    print(msg); raise SystemExit
print("Welcome!")

Making an .exe: pip install pyinstaller then pyinstaller --onefile app.py. Use an obfuscator (PyArmor) - plain PyInstaller exes are easy to unpack.

C# (.exe)

using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json;

static class Auth {
  const string App = "YOUR_APP_ID", Secret = "YOUR_APP_SECRET";
  static string Hwid() => System.Security.Principal.WindowsIdentity.GetCurrent().User!.Value;
  public static async Task<(bool ok, string msg)> Check(string key) {
    string nonce = Guid.NewGuid().ToString("N");
    var body = JsonSerializer.Serialize(new { app = App, key, hwid = Hwid(), nonce });
    using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
    var res = await http.PostAsync("https://auth.davibevilla.com/api/1/check", new StringContent(body, Encoding.UTF8, "application/json"));
    var j = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
    bool ok = j.GetProperty("ok").GetBoolean();
    string k = j.TryGetProperty("key", out var kk) ? kk.GetString()! : "";
    long exp = j.TryGetProperty("expires", out var e) ? e.GetInt64() : 0, t = j.GetProperty("t").GetInt64();
    using var h = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
    string sig = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes($"{nonce}|{(ok ? 1 : 0)}|{k}|{exp}|{t}"))).ToLower();
    bool fresh = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - t) < 60000;
    return (ok && sig == j.GetProperty("sig").GetString() && fresh, j.GetProperty("msg").GetString()!);
  }
}
// var (ok, msg) = await Auth.Check(Console.ReadLine()!); if (!ok) { Console.WriteLine(msg); return; }

Publish with dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true. .NET is easy to decompile - use an obfuscator (ConfuserEx / Obfuscar).

C++ (.exe)

Uses libcurl + OpenSSL (vcpkg: vcpkg install curl openssl nlohmann-json).

#include <curl/curl.h>
#include <openssl/hmac.h>
#include <nlohmann/json.hpp>
#include <chrono> #include <random> #include <string>

static const std::string APP = "YOUR_APP_ID", SECRET = "YOUR_APP_SECRET";
static size_t put(char* p, size_t s, size_t n, std::string* o) { o->append(p, s * n); return s * n; }
static std::string hex(const unsigned char* d, unsigned n) { static const char* h = "0123456789abcdef"; std::string s; for (unsigned i = 0; i < n; i++) { s += h[d[i] >> 4]; s += h[d[i] & 15]; } return s; }

bool check(const std::string& key, const std::string& hwid, std::string& msg) {
  std::mt19937_64 r(std::random_device{}()); std::string nonce = std::to_string(r());
  std::string body = nlohmann::json{{"app", APP}, {"key", key}, {"hwid", hwid}, {"nonce", nonce}}.dump(), out;
  CURL* c = curl_easy_init(); curl_slist* hd = curl_slist_append(nullptr, "Content-Type: application/json");
  curl_easy_setopt(c, CURLOPT_URL, "https://auth.davibevilla.com/api/1/check"); curl_easy_setopt(c, CURLOPT_HTTPHEADER, hd);
  curl_easy_setopt(c, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, put); curl_easy_setopt(c, CURLOPT_WRITEDATA, &out);
  bool sent = curl_easy_perform(c) == CURLE_OK; curl_slist_free_all(hd); curl_easy_cleanup(c);
  if (!sent) { msg = "Can't reach the key server"; return false; }
  auto j = nlohmann::json::parse(out, nullptr, false); if (j.is_discarded()) { msg = "Bad answer"; return false; }
  bool ok = j.value("ok", false); msg = j.value("msg", "");
  std::string text = nonce + "|" + (ok ? "1" : "0") + "|" + j.value("key", "") + "|" + std::to_string(j.value("expires", 0LL)) + "|" + std::to_string(j.value("t", 0LL));
  unsigned char mac[32]; unsigned len = 0;
  HMAC(EVP_sha256(), SECRET.data(), (int)SECRET.size(), (const unsigned char*)text.data(), text.size(), mac, &len);
  long long now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
  return ok && hex(mac, len) == j.value("sig", "") && std::llabs(now - j.value("t", 0LL)) < 60000;
}

HWID idea: the volume serial (GetVolumeInformationA) or the machine GUID in the registry (HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid). Protect the exe with VMProtect / Themida if you can.

Node / Discord bots

const crypto = require("crypto");
const APP = "YOUR_APP_ID", SECRET = "YOUR_APP_SECRET";

async function check(key, hwid) {
  const nonce = crypto.randomUUID();
  const j = await (await fetch("https://auth.davibevilla.com/api/1/check", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ app: APP, key, hwid, nonce }) })).json();
  const sig = crypto.createHmac("sha256", SECRET).update(`${nonce}|${j.ok ? 1 : 0}|${j.key || ""}|${j.expires || 0}|${j.t}`).digest("hex");
  return { ok: j.ok && sig === j.sig && Math.abs(Date.now() - j.t) < 60000, msg: j.msg };
}

// a bot people host themselves: key in their .env, HWID = their machine (or their bot's ID)
const { ok, msg } = await check(process.env.LICENSE_KEY, require("os").hostname());
if (!ok) { console.log(msg); process.exit(1); }

Check again every few hours so a banned / ran-out key stops the bot.

Roblox game

In a server Script (Game Settings > Security > Allow HTTP Requests). For Roblox, either turn OFF "lock to one device" for the app, or send the game's ID as the HWID - then a key works in one game only.

local HttpService = game:GetService("HttpService")
local APP, KEY = "YOUR_APP_ID", "THEIR-KEY-HERE"

local function check()
	local ok, res = pcall(HttpService.RequestAsync, HttpService, {
		Url = "https://auth.davibevilla.com/api/1/check", Method = "POST", Headers = { ["Content-Type"] = "application/json" },
		Body = HttpService:JSONEncode({ app = APP, key = KEY, hwid = tostring(game.GameId) }),
	})
	if not ok then return false, "Can't reach the key server" end
	local j = HttpService:JSONDecode(res.Body)
	return j.ok == true, j.msg
end

local good, msg = check()
if not good then
	warn("[License] " .. tostring(msg))
	return -- (stop: the rest of your system doesn't run)
end

Roblox can't do HMAC easily, and a server Script can't be seen by players anyway - the signature check matters most in programs on people's PCs.

Roblox models & plugins

Anything you sell as a model can be opened and read, so don't ship the real code in it. Do it like Dvv Bridge:

  1. The model / plugin only has a small loader that asks for the key.
  2. Your own server (or a Cloudflare Worker) checks the key with the Seller API (below) and only then sends back the real script.
  3. The plugin puts the script in the game (needs the plugin's Script Injection permission).
-- PLUGIN (loader): ask for the key, get the script from YOUR server
local HttpService = game:GetService("HttpService")
local res = HttpService:RequestAsync({ Url = "https://your-server.example/install", Method = "POST",
	Headers = { ["Content-Type"] = "application/json" }, Body = HttpService:JSONEncode({ key = keyBox.Text, game = game.GameId }) })
local data = HttpService:JSONDecode(res.Body)
if data.ok then
	local s = Instance.new("Script"); s.Name = "MySystem"; s.Source = data.source; s.Parent = game.ServerScriptService
end

Your server: {act:"info", key} with your seller key -> if not banned / ran out, answer the source. Never put the seller key in the plugin.

Auto-selling (Sellix, Sell.app, any shop)

Make keys the moment someone pays - no stock to refill. On the dashboard open your app and copy the shop URL:

https://auth.davibevilla.com/api/1/shop/YOUR_SELLER_KEY?days=30

days = how long each key lasts (0 = lifetime). Optional: prefix=VIP, note=sellix. It makes as many keys as were bought and answers them as plain text, one per line. If the shop sends the same order twice, it gets the same keys back (never doubled).

Sellix

  1. Products > Create > type Dynamic.
  2. Webhook / dynamic URL: your shop URL (one product per key length: 30 days, lifetime...).
  3. Optional: paste your Sellix webhook secret in the app's "shop secret" box - then only Sellix's signed requests make keys.

Sell.app

  1. Product > Variant > Deliverable: Dynamic.
  2. Webhook URL: your shop URL.
  3. Optional: put the webhook secret in "shop secret" (Sell.app signs with a Signature header).

Any other shop

If it can call a URL after payment and show the answer to the buyer, use the same shop URL (send ?qty= if it doesn't send a quantity). If it can't, use the Seller API from your own backend.

The shop URL has your seller key in it: keep it private. Leaked? Make a new seller key on the dashboard and update the shop.

Seller API (your own site / bot)

From YOUR server only. POST https://auth.davibevilla.com/api/1/seller with JSON:

{ "sellerkey": "sk_...", "act": "gen", "count": 1, "days": 30, "note": "order 123", "prefix": "" }  -> { "ok": true, "keys": ["..."] }
{ "sellerkey": "sk_...", "act": "info",   "key": "..." }  -> { ok, days, used, expires, hwid_locked, banned, discord, note }
{ "sellerkey": "sk_...", "act": "extend", "key": "...", "days": 30 }
{ "sellerkey": "sk_...", "act": "reset",  "key": "..." }   (clear the HWID)
{ "sellerkey": "sk_...", "act": "ban",    "key": "...", "why": "chargeback" }
{ "sellerkey": "sk_...", "act": "unban" | "delete", "key": "..." }
// Node: make a key after a payment on your own site
const r = await fetch("https://auth.davibevilla.com/api/1/seller", { method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sellerkey: process.env.DVV_SELLER_KEY, act: "gen", count: 1, days: 30, note: order.id }) });
const { keys } = await r.json(); // email / DM it to the buyer

Discord bot

Invite the bot to your server, then:

Keeping it safe