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
- 1The player opens your vote link with their game account name.
- 2GameTopBase checks they are a person and haven't voted in the last 12 hours (from their IP or with that account).
- 3The vote is saved and counts towards the ranking.
- 4If you set a postback URL, GameTopBase notifies your server with a notification signed with your secret.
- 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.
| Parameter | Description |
|---|---|
| username | Optional. 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 botCloudflare Turnstile verification, a signed form that expires and an invisible bot trap.
- One vote per IPAn IP can vote for your server once every 12 hours, and at most 20 votes per hour across the whole site.
- One vote per accountA 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 IPThe IP comes from the real connection, not from what the browser sends, so it can't be faked to vote more times.
- Signed notificationEvery postback carries an HMAC-SHA256 signature made with your secret that covers all the data: it can't be forged or modified.
- PrivacyIPs 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:
| Parameter | Type | Description |
|---|---|---|
| username | string | Game account the player typed (empty if they didn't type one). |
| ip | string | IP the vote came from (IPv4 or IPv6). |
| timestamp | integer | Time of sending in Unix seconds. Changes on every retry. |
| vote_id | string | Unique vote identifier. Stays the same across retries and your URLs. |
| signature | string | HMAC-SHA256 signature in hexadecimal (64 characters). |
https://your-game.com/vote.php?username=jugador123&ip=203.0.113.7×tamp=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:
- 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).
- Time: reject the notification if timestamp is more than 5 minutes away from your clock. An old copied notification is useless.
- Account: if username is empty, answer 200 and give nothing.
- 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.
- Reward: give it to username.
- 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
| Code | Description |
|---|---|
| 2xx | Notification received. It isn't sent again. |
| Any other code or no answer in 5 s | Premium 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 premium | Sent 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.
| Parameter | Description |
|---|---|
| vote_id | Does this vote exist? No time limit. The safest way to confirm a postback. |
| username | Has this account voted in the last 12 hours? Case-insensitive. |
| ip | Has 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"
}| Field | Description |
|---|---|
| voted | true if a vote was found. |
| voteId | Vote identifier (the same one the postback carries), or null. |
| username | Account the vote was cast with, or null. |
| votedAt | Vote date (ISO 8601, UTC), or null. |
| nextVoteAt | When they can vote again, or null. |
| Code | Description |
|---|---|
| 200 | Successful request (voted can be true or false). |
| 400 | vote_id, username or ip is missing or malformed. |
| 401 | The 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.