GameTopBase
Inicio

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

  1. 1El jugador abre tu enlace de voto con el nombre de su cuenta del juego.
  2. 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).
  3. 3El voto se guarda y suma en el ranking.
  4. 4Si has puesto una URL de postback, GameTopBase avisa a tu servidor con un aviso firmado con tu secreto.
  5. 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ámetroDescripción
usernameOpcional. 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 bot
    Verificación de Cloudflare Turnstile, un formulario firmado que caduca y una trampa invisible para bots.
  • Un voto por IP
    Una 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 cuenta
    Una 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 verdadera
    La 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 firmado
    Cada postback lleva una firma HMAC-SHA256 hecha con tu secreto, que cubre todos los datos: no se puede inventar ni modificar.
  • Privacidad
    Las 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ámetroTipoDescripción
usernametextoCuenta del juego que escribió el jugador (vacío si no escribió ninguna).
iptextoIP desde la que se votó (IPv4 o IPv6).
timestampenteroMomento del envío en segundos Unix. Cambia en cada reintento.
vote_idtextoIdentificador único del voto. No cambia en los reintentos ni entre tus URLs.
signaturetextoFirma HMAC-SHA256 en hexadecimal (64 caracteres).
https://tu-juego.com/vote.php?username=jugador123&ip=203.0.113.7&timestamp=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:

  1. 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).
  2. Fecha: rechaza el aviso si timestamp se aleja más de 5 minutos de tu hora. Así un aviso antiguo copiado no sirve.
  3. Cuenta: si username está vacío, responde 200 y no des nada.
  4. 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.
  5. Recompensa: entrégala a username.
  6. 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ódigoDescripción
2xxAviso recibido. No se vuelve a enviar.
Otro código o sin respuesta en 5 sServidores 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 premiumSe 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ámetroDescripció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"
}
CampoDescripción
votedtrue si se encontró un voto.
voteIdIdentificador del voto (el mismo que llega en el postback), o null.
usernameCuenta con la que se votó, o null.
votedAtFecha del voto (ISO 8601, UTC), o null.
nextVoteAtCuándo podrá volver a votar, o null.
CódigoDescripción
200Consulta correcta (voted puede ser true o false).
400Falta vote_id, username o ip, o tiene un formato no válido.
401El 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.