GameTopBase
Home

Reward your players for voting

Your players vote for your server on GameTopBase and you give them an in-game reward. This page explains how to do it safely: only real votes are rewarded, and each one only once.

How it works

  1. 1The player opens your vote link with their game account name.
  2. 2GameTopBase checks they are a person and haven't voted in the last 12 hours (from their IP or with that account).
  3. 3The vote is saved and counts towards the ranking.
  4. 4If you set a postback URL, GameTopBase notifies your server with a notification signed with your secret.
  5. 5Your server checks the signature, makes sure it hasn't paid that vote already and gives the reward.

Only give the reward when a postback with a valid signature arrives, or after confirming it with the check API. Never because the player says they voted, and never from the browser.

1. Vote link

Send your players to this link (from your website, launcher, Discord or an in-game command):

https://gametopbase.com/en/servers/<your-server>/vote?username=<account>

<your-server> is your server's identifier on GameTopBase: the last part of your page's address. For example, in …/servers/craftnation it is craftnation. Your dashboard, under "Vote integration", has the link already filled in.

ParameterDescription
usernameOptional. Name of the game account that gets the reward: 1 to 32 characters (letters, numbers, space, _ - .). If the link carries it, the field comes pre-filled.

Without an account name the vote still counts for the ranking, but there is nobody to reward: the postback arrives with an empty username.

Only username goes in the link, on purpose. The player opens it in their browser, so they could change anything in the URL: GameTopBase takes the IP from the real connection and generates the rest (vote_id, time, signature) on its own server. Your server receives all of that later, in the signed postback.

In your dashboard, under "Promote your server", the button and link are ready to copy.

2. Real votes: what GameTopBase checks

Before counting a vote, GameTopBase checks on its own server (not in the player's browser):

  • A person, not a bot
    Cloudflare Turnstile verification, a signed form that expires and an invisible bot trap.
  • One vote per IP
    An IP can vote for your server once every 12 hours, and at most 20 votes per hour across the whole site.
  • One vote per account
    A game account only earns one vote every 12 hours, even when voting from other IPs or a VPN. Case-insensitive: "Player" and "player" are the same.
  • Real IP
    The IP comes from the real connection, not from what the browser sends, so it can't be faked to vote more times.
  • Signed notification
    Every postback carries an HMAC-SHA256 signature made with your secret that covers all the data: it can't be forged or modified.
  • Privacy
    IPs are stored irreversibly hashed. One is only kept unhashed while retries of the notification to your server are pending (about 40 minutes at most). Only your server receives the voter's IP, in the postback.

3. Postback: the notification to your server

Set your script's URL in your server's dashboard, under "Vote integration". After saving each vote, GameTopBase sends a GET request to that URL with these parameters:

ParameterTypeDescription
usernamestringGame account the player typed (empty if they didn't type one).
ipstringIP the vote came from (IPv4 or IPv6).
timestampintegerTime of sending in Unix seconds. Changes on every retry.
vote_idstringUnique vote identifier. Stays the same across retries and your URLs.
signaturestringHMAC-SHA256 signature in hexadecimal (64 characters).
https://your-game.com/vote.php?username=jugador123&ip=203.0.113.7&timestamp=1767225600&vote_id=cm4x9k2p00001&signature=<64 hex>
  • Your script has 5 seconds to answer.
  • Redirects are not followed: use the final URL, preferably with https.
  • Only public addresses are called (no localhost or internal networks).

4. Verify every notification

Your script must run these checks, in this order, before giving anything:

  1. Signature: compute HMAC-SHA256 with your secret over the text below and compare it with signature using a constant-time comparison (hash_equals in PHP, timingSafeEqual in Node).
  2. Time: reject the notification if timestamp is more than 5 minutes away from your clock. An old copied notification is useless.
  3. Account: if username is empty, answer 200 and give nothing.
  4. Only once: store vote_id in a table with a unique key before paying. If it was already there, it's a retry: answer 200 and don't pay again.
  5. Reward: give it to username.
  6. Answer: respond with a 2xx code (for example 200).

Signed text (the values exactly as received, separated by colons):

signature = HMAC-SHA256(secret, "server:username:ip:timestamp:vote_id")  →  hex

The secret is in your dashboard, under "Vote integration". It is for your server only: never put it in your launcher, website or game client.

Full PHP example

<?php
// vote.php: receives GameTopBase's vote notification and gives the reward.
// Set this file's URL as the "Postback URL" in your GameTopBase dashboard.

const GAMETOP_SECRET = 'YOUR_DASHBOARD_SECRET'; // Dashboard > Vote integration > Secret
const GAMETOP_SERVER = 'your-server';           // your page's identifier (last part of its address)

$user   = $_GET['username']  ?? '';
$ip     = $_GET['ip']        ?? '';
$ts     = $_GET['timestamp'] ?? '';
$voteId = $_GET['vote_id']   ?? '';
$sig    = $_GET['signature'] ?? '';

// 1. The signature proves the notification comes from GameTopBase and nothing was changed.
$expected = hash_hmac('sha256', GAMETOP_SERVER . ":$user:$ip:$ts:$voteId", GAMETOP_SECRET);
if (!is_string($sig) || !hash_equals($expected, $sig)) {
    http_response_code(403);
    exit('invalid signature');
}

// 2. Recent notifications only: an old copied one is useless.
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) {
    http_response_code(403);
    exit('expired');
}

// 3. Without an account the vote counts for the ranking, but there is nobody to reward.
if ($user === '') {
    http_response_code(200);
    exit('ok');
}

// 4. Each vote is paid only once. vote_id doesn't change on retries: store it BEFORE paying
//    in a column with a unique key, so two notifications at once can't pay twice.
//    CREATE TABLE gametop_votes (vote_id VARCHAR(40) PRIMARY KEY, username VARCHAR(32), paid_at DATETIME);
$db = new PDO('mysql:host=localhost;dbname=your_game', 'user', 'pass');
$stmt = $db->prepare('INSERT IGNORE INTO gametop_votes (vote_id, username, paid_at) VALUES (?, ?, NOW())');
$stmt->execute([$voteId, $user]);
if ($stmt->rowCount() === 0) {
    http_response_code(200); // already paid (it's a retry): don't give it again
    exit('ok');
}

// 5. >>> THIS IS WHERE YOU GIVE THE REWARD to account $user <<<
//    The only point where the vote is verified and arrives for the first time.
try {
    give_reward($user); // your code: coins, items, VIP days...
} catch (Throwable $e) {
    // If it fails, remove the mark and answer with an error: GameTopBase retries it (premium servers).
    $db->prepare('DELETE FROM gametop_votes WHERE vote_id = ?')->execute([$voteId]);
    http_response_code(500);
    exit('error');
}

// 6. Answer 2xx: GameTopBase marks the notification as delivered.
http_response_code(200);
echo 'ok';

The same example in Node.js (Express)

const crypto = require("crypto");
const express = require("express");
const app = express();

const GAMETOP_SECRET = "YOUR_DASHBOARD_SECRET"; // Dashboard > Vote integration > Secret
const GAMETOP_SERVER = "your-server";

app.get("/gametop/vote", async (req, res) => {
  const q = (name) => (typeof req.query[name] === "string" ? req.query[name] : "");
  const user = q("username"), ip = q("ip"), ts = q("timestamp"), voteId = q("vote_id"), sig = q("signature");

  // 1. Signature: from GameTopBase and unchanged
  const expected = crypto.createHmac("sha256", GAMETOP_SECRET)
    .update(GAMETOP_SERVER + ":" + user + ":" + ip + ":" + ts + ":" + voteId)
    .digest("hex");
  if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(403).send("invalid signature");
  }

  // 2. Recent notifications only
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return res.status(403).send("expired");
  }

  // 3. No account, nothing to pay
  if (user === "") return res.send("ok");

  // 4. Only once per vote_id: store it with a unique key BEFORE paying
  const firstTime = await saveVoteId(voteId, user); // your code: false if it already existed
  if (!firstTime) return res.send("ok");

  // 5. >>> THIS IS WHERE YOU GIVE THE REWARD to account user <<<
  try {
    await giveReward(user); // your code
  } catch (e) {
    await deleteVoteId(voteId); // so the retry (premium) can pay it
    return res.status(500).send("error");
  }

  // 6. Answer 2xx
  res.send("ok");
});

app.listen(8080);

5. Responses and retries

CodeDescription
2xxNotification received. It isn't sent again.
Any other code or no answer in 5 sPremium servers: retried after 1, 5 and 30 minutes (4 attempts), with the same vote_id and a new signature. Every attempt shows in your dashboard.
Without premiumSent only once. If your server was down, use the check API as a backup.

Premium servers can add up to 2 more postback URLs: all of them get the same vote_id, so you can use several without paying twice.

6. Check API

Ask GameTopBase about a vote from your server. Use it to confirm a postback, to give the reward with an in-game command, or as a backup if a notification didn't arrive.

Send the secret in the Authorization: Bearer <secret> header. If your system can't send headers, you can use the key=<secret> parameter, but it ends up in access logs.

ParameterDescription
vote_idDoes this vote exist? No time limit. The safest way to confirm a postback.
usernameHas this account voted in the last 12 hours? Case-insensitive.
ipHas this IP voted in the last 12 hours?

Use one of the three in each request, together with server=<your-server>.

curl -H "Authorization: Bearer <secret>" \
  "https://gametopbase.com/api/v1/vote-check?server=<your-server>&vote_id=cm4x9k2p00001"

curl -H "Authorization: Bearer <secret>" \
  "https://gametopbase.com/api/v1/vote-check?server=<your-server>&username=jugador123"

{
  "voted": true,
  "voteId": "cm4x9k2p00001",
  "username": "jugador123",
  "votedAt": "2026-10-25T10:00:00.000Z",
  "nextVoteAt": "2026-10-25T22:00:00.000Z"
}
FieldDescription
votedtrue if a vote was found.
voteIdVote identifier (the same one the postback carries), or null.
usernameAccount the vote was cast with, or null.
votedAtVote date (ISO 8601, UTC), or null.
nextVoteAtWhen they can vote again, or null.
CodeDescription
200Successful request (voted can be true or false).
400vote_id, username or ip is missing or malformed.
401The server doesn't exist or the secret is wrong.

Reward with an in-game command

If you don't use a postback: when the player types /reward, query by username. If voted is true and that voteId isn't in your paid votes table, store it and give the reward. Same rule as step 4: one voteId, one payment.

7. Security checklist

  • The secret only lives on your server. If it leaks, press "Regenerate" in your dashboard: the old one stops working at once.
  • Always check the signature and the time, even if your script's URL is secret.
  • Store vote_id with a unique key before paying: that's what prevents paying twice.
  • Never give rewards from the browser or because the player says they voted.
  • Use https for your postback URL.