Recompensa a tus jugadores por votar
Tus jugadores votan por tu servidor en GameTopBase y tú les das una recompensa en el juego. Esta página explica cómo hacerlo de forma segura: solo se premian votos reales, una vez cada uno.
Cómo funciona
- 1El jugador abre tu enlace de voto con el nombre de su cuenta del juego.
- 2GameTopBase comprueba que es una persona y que no ha votado ya en las últimas 12 horas (ni desde su IP ni con esa cuenta).
- 3El voto se guarda y suma en el ranking.
- 4Si has puesto una URL de postback, GameTopBase avisa a tu servidor con un aviso firmado con tu secreto.
- 5Tu servidor comprueba la firma, se asegura de no haber pagado ya ese voto y entrega la recompensa.
Da la recompensa solo cuando llega un postback con la firma correcta, o tras confirmarlo con la API de comprobación. Nunca porque el jugador diga que ha votado ni desde el navegador.
1. Enlace de voto
Envía a tus jugadores a este enlace (desde la web, el launcher, Discord o un comando del juego):
https://gametopbase.com/es/servidores/<tu-servidor>/votar?username=<cuenta>
<tu-servidor> es el identificador de tu servidor en GameTopBase: la última parte de la dirección de tu ficha. Por ejemplo, en …/servidores/craftnation es craftnation. En tu panel, en «Integración de votos», tienes el enlace ya completo.
| Parámetro | Descripción |
|---|---|
| username | Opcional. Nombre de la cuenta del juego que recibe la recompensa: de 1 a 32 caracteres (letras, números, espacio, _ - .). Si el enlace lo trae, el campo sale ya relleno. |
Sin nombre de cuenta el voto cuenta igual para el ranking, pero no hay a quién premiar: el postback llega con username vacío.
En el enlace solo va username, a propósito. Lo abre el jugador en su navegador, así que podría cambiar cualquier dato de la URL: la IP la toma GameTopBase de la conexión real y el resto (vote_id, fecha, firma) lo genera en su servidor. Tu servidor recibe todo eso después, en el postback firmado.
En tu panel, en «Promociona tu servidor», tienes el botón y el enlace listos para copiar.
2. Votos reales: qué comprueba GameTopBase
Antes de contar un voto, GameTopBase comprueba en su servidor (no en el navegador del jugador):
- Una persona, no un botVerificación de Cloudflare Turnstile, un formulario firmado que caduca y una trampa invisible para bots.
- Un voto por IPUna IP solo puede votar a tu servidor una vez cada 12 horas, y como mucho 20 votos por hora en toda la web.
- Un voto por cuentaUna cuenta del juego solo cobra un voto cada 12 horas aunque vote desde otras IP o con VPN. No distingue mayúsculas: «Jugador» y «jugador» son la misma.
- IP verdaderaLa IP sale de la conexión real, no de lo que envía el navegador, así que no se puede falsear para votar más veces.
- Aviso firmadoCada postback lleva una firma HMAC-SHA256 hecha con tu secreto, que cubre todos los datos: no se puede inventar ni modificar.
- PrivacidadLas IP se guardan cifradas de forma irreversible. Solo se conserva sin cifrar mientras quedan reintentos del aviso a tu servidor (unos 40 minutos como mucho). Solo tu servidor recibe la IP del votante, en el postback.
3. Postback: el aviso a tu servidor
Pon la URL de tu script en el panel de tu servidor, en «Integración de votos». Después de guardar cada voto, GameTopBase hace una petición GET a esa URL con estos parámetros:
| Parámetro | Tipo | Descripción |
|---|---|---|
| username | texto | Cuenta del juego que escribió el jugador (vacío si no escribió ninguna). |
| ip | texto | IP desde la que se votó (IPv4 o IPv6). |
| timestamp | entero | Momento del envío en segundos Unix. Cambia en cada reintento. |
| vote_id | texto | Identificador único del voto. No cambia en los reintentos ni entre tus URLs. |
| signature | texto | Firma HMAC-SHA256 en hexadecimal (64 caracteres). |
https://tu-juego.com/vote.php?username=jugador123&ip=203.0.113.7×tamp=1767225600&vote_id=cm4x9k2p00001&signature=<64 hex>
- Tu script tiene 5 segundos para responder.
- No se siguen redirecciones: usa la URL final, a ser posible con https.
- Solo se envía a direcciones públicas (nada de localhost ni redes internas).
4. Verifica cada aviso
Tu script tiene que hacer estas comprobaciones, en este orden, antes de dar nada:
- Firma: calcula HMAC-SHA256 con tu secreto sobre el texto de abajo y compárala con signature usando una comparación de tiempo constante (hash_equals en PHP, timingSafeEqual en Node).
- Fecha: rechaza el aviso si timestamp se aleja más de 5 minutos de tu hora. Así un aviso antiguo copiado no sirve.
- Cuenta: si username está vacío, responde 200 y no des nada.
- Una sola vez: guarda vote_id en una tabla con clave única antes de pagar. Si ya estaba, es un reintento: responde 200 y no pagues otra vez.
- Recompensa: entrégala a username.
- Respuesta: contesta con un código 2xx (por ejemplo 200).
Texto que se firma (los valores tal cual llegan, separados por dos puntos):
signature = HMAC-SHA256(secreto, "servidor:username:ip:timestamp:vote_id") → hex
El secreto está en tu panel, en «Integración de votos». Es solo para tu servidor: no lo pongas nunca en el launcher, en la web ni en el cliente del juego.
Ejemplo completo en PHP
<?php
// vote.php: recibe el aviso de voto de GameTopBase y entrega la recompensa.
// Pon la URL de este archivo como "URL de postback" en tu panel de GameTopBase.
const GAMETOP_SECRET = 'TU_SECRETO_DEL_PANEL'; // Panel > Integración de votos > Secreto
const GAMETOP_SERVER = 'tu-servidor'; // el identificador de tu ficha (última parte de su dirección)
$user = $_GET['username'] ?? '';
$ip = $_GET['ip'] ?? '';
$ts = $_GET['timestamp'] ?? '';
$voteId = $_GET['vote_id'] ?? '';
$sig = $_GET['signature'] ?? '';
// 1. La firma demuestra que el aviso viene de GameTopBase y que nadie ha cambiado ningún dato.
$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('firma no valida');
}
// 2. Solo avisos recientes: uno antiguo copiado no sirve.
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) {
http_response_code(403);
exit('aviso caducado');
}
// 3. Sin cuenta el voto cuenta para el ranking, pero no hay a quién premiar.
if ($user === '') {
http_response_code(200);
exit('ok');
}
// 4. Cada voto se paga una sola vez. vote_id no cambia en los reintentos: se guarda ANTES de
// pagar en una columna con clave única, así dos avisos a la vez no pueden pagar dos veces.
// CREATE TABLE gametop_votes (vote_id VARCHAR(40) PRIMARY KEY, username VARCHAR(32), paid_at DATETIME);
$db = new PDO('mysql:host=localhost;dbname=tu_juego', 'usuario', 'clave');
$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); // ya se pagó (es un reintento): no se vuelve a dar
exit('ok');
}
// 5. >>> AQUÍ ENTREGAS LA RECOMPENSA a la cuenta $user <<<
// Es el único punto en el que el voto está verificado y llega por primera vez.
try {
dar_recompensa($user); // tu código: monedas, objetos, días de VIP...
} catch (Throwable $e) {
// Si falla, se quita la marca y se responde con error: GameTopBase lo reintenta (servidores premium).
$db->prepare('DELETE FROM gametop_votes WHERE vote_id = ?')->execute([$voteId]);
http_response_code(500);
exit('error');
}
// 6. Responde 2xx: GameTopBase da el aviso por entregado.
http_response_code(200);
echo 'ok';El mismo ejemplo en Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const GAMETOP_SECRET = "TU_SECRETO_DEL_PANEL"; // Panel > Integración de votos > Secreto
const GAMETOP_SERVER = "tu-servidor";
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. Firma: de GameTopBase y sin cambios
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("firma no valida");
}
// 2. Solo avisos recientes
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(403).send("aviso caducado");
}
// 3. Sin cuenta, nada que pagar
if (user === "") return res.send("ok");
// 4. Una sola vez por vote_id: guárdalo con clave única ANTES de pagar
const firstTime = await saveVoteId(voteId, user); // tu código: false si ya existía
if (!firstTime) return res.send("ok");
// 5. >>> AQUÍ ENTREGAS LA RECOMPENSA a la cuenta user <<<
try {
await giveReward(user); // tu código
} catch (e) {
await deleteVoteId(voteId); // así el reintento (premium) puede pagarlo
return res.status(500).send("error");
}
// 6. Respuesta 2xx
res.send("ok");
});
app.listen(8080);5. Respuestas y reintentos
| Código | Descripción |
|---|---|
| 2xx | Aviso recibido. No se vuelve a enviar. |
| Otro código o sin respuesta en 5 s | Servidores premium: se reintenta a los 1, 5 y 30 minutos (4 intentos), con el mismo vote_id y una firma nueva. Cada intento sale en tu panel. |
| Sin premium | Se envía una sola vez. Si tu servidor estaba caído, usa la API de comprobación como respaldo. |
Los servidores premium pueden añadir hasta 2 URLs de postback más: todas reciben el mismo vote_id, así que puedes usar varias sin pagar dos veces.
6. API de comprobación
Pregunta a GameTopBase por un voto desde tu servidor. Sirve para confirmar un postback, para dar la recompensa con un comando del juego o como respaldo si un aviso no llegó.
Envía el secreto en la cabecera Authorization: Bearer <secreto>. Si tu sistema no permite cabeceras, puedes usar el parámetro key=<secreto>, pero queda en los registros de acceso.
| Parámetro | Descripción |
|---|---|
| vote_id | ¿Existe este voto? Sin límite de tiempo. La forma más segura de confirmar un postback. |
| username | ¿Ha votado esta cuenta en las últimas 12 horas? Sin distinguir mayúsculas. |
| ip | ¿Ha votado esta IP en las últimas 12 horas? |
Usa uno de los tres en cada consulta, junto a server=<tu-servidor>.
curl -H "Authorization: Bearer <secreto>" \
"https://gametopbase.com/api/v1/vote-check?server=<tu-servidor>&vote_id=cm4x9k2p00001"
curl -H "Authorization: Bearer <secreto>" \
"https://gametopbase.com/api/v1/vote-check?server=<tu-servidor>&username=jugador123"
{
"voted": true,
"voteId": "cm4x9k2p00001",
"username": "jugador123",
"votedAt": "2026-10-25T10:00:00.000Z",
"nextVoteAt": "2026-10-25T22:00:00.000Z"
}| Campo | Descripción |
|---|---|
| voted | true si se encontró un voto. |
| voteId | Identificador del voto (el mismo que llega en el postback), o null. |
| username | Cuenta con la que se votó, o null. |
| votedAt | Fecha del voto (ISO 8601, UTC), o null. |
| nextVoteAt | Cuándo podrá volver a votar, o null. |
| Código | Descripción |
|---|---|
| 200 | Consulta correcta (voted puede ser true o false). |
| 400 | Falta vote_id, username o ip, o tiene un formato no válido. |
| 401 | El servidor no existe o el secreto no es correcto. |
Recompensa con un comando del juego
Si no usas postback: cuando el jugador escriba /recompensa, consulta con username. Si voted es true y ese voteId no está en tu tabla de votos pagados, guárdalo y entrega la recompensa. Es la misma regla del paso 4: un voteId, un pago.
7. Lista de seguridad
- El secreto solo vive en tu servidor. Si se filtra, pulsa «Regenerar» en tu panel: el anterior deja de valer al momento.
- Comprueba siempre la firma y la fecha, aunque la URL de tu script sea secreta.
- Guarda vote_id con clave única antes de pagar: es lo que impide pagar dos veces.
- No des recompensas desde el navegador ni porque el jugador diga que ha votado.
- Usa https en la URL de postback.