>
Tutorial 2026-03-07

Desarrollo de sistemas de banca y cajeros para FiveM

TDYSKY

TDYSKY

Fundador y desarrollador principal de Agency Scripts

Arquitectura del sistema bancario

Un sistema bancario es la columna financiera de cualquier servidor de rol de FiveM, gestionando todo desde simples depósitos en efectivo hasta transferencias complejas entre jugadores y cuentas compartidas de organizaciones. La mayoría de frameworks como QBCore y ESX incluyen una gestión básica de dinero, pero un sistema bancario dedicado lo amplía con gestión adecuada de cuentas, registro de transacciones y una interfaz de cajero pulida que mete al jugador en el lado financiero del rol. La arquitectura se divide en tres capas: la base de datos guarda los saldos y registros de transacciones, el servidor valida cada operación y aplica las reglas de negocio, y el cliente ofrece las interfaces del cajero y del mostrador bancario mediante NUI. Toda operación de dinero debe pasar por el lado del servidor, porque la manipulación de dinero en el cliente es el vector de exploits número uno en los servidores de FiveM. Incluso mostrar un saldo debería venir de un callback de servidor, no de datos cacheados en cliente susceptibles de manipulación.

Esquema de base de datos para banca

Tu base de datos bancaria debe soportar cuentas personales, cuentas compartidas de organizaciones y negocios y un registro completo de transacciones. El log de transacciones no es opcional, porque hace de funcionalidad orientada al jugador y a la vez de herramienta para que los admins investiguen exploits de dinero. Diseña el esquema para manejar un alto throughput, porque los servidores con tráfico procesan cientos de transacciones por minuto en hora punta:

CREATE TABLE IF NOT EXISTS bank_accounts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    account_number VARCHAR(20) UNIQUE NOT NULL,
    owner_citizenid VARCHAR(50) NOT NULL,
    account_type ENUM('personal', 'business', 'gang', 'shared') DEFAULT 'personal',
    balance BIGINT DEFAULT 0,
    account_name VARCHAR(100) DEFAULT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_frozen BOOLEAN DEFAULT FALSE,
    INDEX idx_owner (owner_citizenid),
    INDEX idx_type (account_type)
);

CREATE TABLE IF NOT EXISTS bank_transactions (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    account_id INT NOT NULL,
    type ENUM('deposit', 'withdraw', 'transfer_in', 'transfer_out', 'paycheck', 'purchase') NOT NULL,
    amount BIGINT NOT NULL,
    balance_after BIGINT NOT NULL,
    description VARCHAR(255) DEFAULT NULL,
    other_account VARCHAR(20) DEFAULT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_account (account_id),
    INDEX idx_created (created_at),
    FOREIGN KEY (account_id) REFERENCES bank_accounts(id)
);

CREATE TABLE IF NOT EXISTS bank_account_access (
    account_id INT NOT NULL,
    citizenid VARCHAR(50) NOT NULL,
    permission ENUM('view', 'withdraw', 'full') DEFAULT 'view',
    PRIMARY KEY (account_id, citizenid),
    FOREIGN KEY (account_id) REFERENCES bank_accounts(id)
);

La tabla bank_account_access permite cuentas compartidas con varios jugadores y distintos niveles de permiso. Un líder de banda podría tener acceso completo al tesoro de la banda mientras los miembros corrientes solo ven el saldo. Usar BIGINT para el saldo evita problemas de overflow en servidores con economías infladas donde los saldos llegan a miles de millones.

Lógica de transacciones en servidor

Cada transacción financiera debe ser atómica y validarse en el servidor. Usa transacciones de base de datos para que el dinero nunca se cree o destruya en medio de una transferencia. Cuando el jugador A envía dinero al B, tanto la deducción de A como la adición a B deben tener éxito a la vez, o ninguna debe aplicarse. Implementa comprobaciones de saldo suficiente, estado de cuenta congelada, límites diarios de transferencia y montos mínimos. Aquí va una implementación segura de transferencia:

RegisterNetEvent('banking:server:transfer', function(targetAccount, amount, description)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    amount = math.floor(tonumber(amount) or 0)
    if amount <= 0 then
        TriggerClientEvent('QBCore:Notify', src, 'Invalid amount', 'error')
        return
    end

    local citizenid = Player.PlayerData.citizenid
    local senderAccount = GetPlayerPrimaryAccount(citizenid)

    if not senderAccount or senderAccount.is_frozen then
        TriggerClientEvent('QBCore:Notify', src, 'Account unavailable', 'error')
        return
    end

    if senderAccount.balance < amount then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        return
    end

    -- Comprueba el límite diario de transferencia
    local todayTransfers = GetDailyTransferTotal(senderAccount.id)
    if todayTransfers + amount > Config.DailyTransferLimit then
        TriggerClientEvent('QBCore:Notify', src, 'Daily limit exceeded', 'error')
        return
    end

    -- Transferencia atómica mediante transacción de base de datos
    local success = MySQL.transaction.await({
        {
            query = 'UPDATE bank_accounts SET balance = balance - ? WHERE id = ? AND balance >= ?',
            values = {amount, senderAccount.id, amount}
        },
        {
            query = 'UPDATE bank_accounts SET balance = balance + ? WHERE account_number = ?',
            values = {amount, targetAccount}
        },
        {
            query = 'INSERT INTO bank_transactions (account_id, type, amount, balance_after, description, other_account) VALUES (?, "transfer_out", ?, (SELECT balance FROM bank_accounts WHERE id = ?), ?, ?)',
            values = {senderAccount.id, amount, senderAccount.id, description or 'Transfer', targetAccount}
        },
    })

    if success then
        TriggerClientEvent('QBCore:Notify', src, 'Transfer complete: $' .. amount, 'success')
        TriggerClientEvent('banking:client:refreshBalance', src)
    else
        TriggerClientEvent('QBCore:Notify', src, 'Transfer failed', 'error')
    end
end)

Fíjate en la cláusula WHERE balance >= ? en la query de deducción: actúa como última barrera contra condiciones de carrera en las que dos transferencias simultáneas podrían sobregirar la cuenta. Esta comprobación a nivel de base de datos es imprescindible aunque ya valides el saldo en Lua, porque pueden llegar varias peticiones entre tu comprobación y la actualización real.

Interfaz del cajero

La interfaz del cajero es un panel NUI compacto que da acceso rápido a las funciones bancarias principales: consultar saldo, depositar efectivo, retirar efectivo y transferir dinero. Mantén el diseño limpio y familiar, porque los jugadores esperan instintivamente que un cajero funcione como en la vida real. Muestra el saldo actual bien destacado en la parte superior, con botones de acción debajo para cada operación. Las vistas de depósito y retirada deberían incluir botones con cantidades predefinidas para valores habituales como 100, 500, 1000 y 5000 dólares, junto a un campo de cantidad personalizada. Para transferencias, ofrece campos para el número de cuenta del destinatario y el importe, además de un campo opcional de concepto. Muestra un paso de confirmación antes de ejecutar cualquier transacción, para evitar que clics accidentales cuesten dinero a los jugadores. Incluye una lista de transacciones recientes con las últimas 10 entradas para que los jugadores puedan verificar su actividad sin necesidad de acercarse a una sucursal. La UI debe sentirse ágil, así que obtén saldo y datos de transacciones en un único callback cuando se abre el menú en lugar de hacer peticiones separadas para cada dato.

Configuración de interacción con el cajero

Coloca los puntos de interacción en las ubicaciones existentes de props de cajero por todo el mapa de GTA. FiveM ofrece una lista de hashes de modelos de cajero que puedes recorrer para encontrar todos los props del mundo. Usa un sistema de target como ox_target para una interacción limpia, o recurre a comprobaciones de proximidad cerca de cada prop. Cuando el jugador interactúe con un cajero, reproduce una animación de uso y abre el panel NUI:

local atmModels = {
    'prop_atm_01', 'prop_atm_02', 'prop_atm_03',
    'prop_fleeca_atm', 'v_5_b_atm1'
}

-- Usando ox_target para la interacción con el cajero
for _, model in ipairs(atmModels) do
    exports.ox_target:addModel(GetHashKey(model), {
        {
            name = 'use_atm',
            icon = 'fas fa-credit-card',
            label = 'Use ATM',
            onSelect = function(data)
                local ped = PlayerPedId()
                local atmCoords = GetEntityCoords(data.entity)

                -- Mira hacia el cajero
                TaskTurnPedToFaceCoord(ped, atmCoords.x, atmCoords.y, atmCoords.z, 1000)
                Wait(1000)

                -- Reproduce animación del cajero
                RequestAnimDict('mini@atmenter')
                while not HasAnimDictLoaded('mini@atmenter') do Wait(10) end
                TaskPlayAnim(ped, 'mini@atmenter', 'enter', 8.0, -8.0, -1, 0, 0, false, false, false)

                -- Abre la UI del cajero
                QBCore.Functions.TriggerCallback('banking:server:getAccountData', function(data)
                    SetNuiFocus(true, true)
                    SendNUIMessage({
                        action = 'openATM',
                        balance = data.balance,
                        transactions = data.recentTransactions,
                        accountNumber = data.accountNumber
                    })
                end)
            end
        }
    })
end

Historial de transacciones y extractos

El historial de transacciones convierte tu sistema bancario de una simple máquina de depósitos y retiradas en una herramienta completa de gestión financiera. Los jugadores deberían poder consultar su historial completo en las sucursales, filtrado por rango de fechas, tipo de transacción o importe. Cada entrada debe mostrar fecha, tipo, importe, saldo resultante, descripción y la otra parte implicada en el caso de las transferencias. Implementa paginación en el servidor, porque cargar miles de transacciones de golpe bloqueará el frame de la NUI y disparará el uso de memoria. Devuelve 20-30 transacciones por página y deja que el jugador cargue más según necesite. En las sucursales, ofrece funciones extra más allá del cajero: abrir cuentas nuevas, gestionar permisos de cuentas compartidas, generar extractos para un período concreto y solicitar préstamos si tu servidor contempla esa mecánica. Guarda las descripciones de transacciones como cadenas legibles para que las transacciones automáticas de trabajos, compras en tiendas e impuestos muestren entradas claras que el jugador entienda sin contexto.

Mecánicas de atraco bancario

Los atracos a bancos son de los eventos más emocionantes en cualquier servidor de rol, creando escenarios de alta tensión con criminales, policía, negociadores de rehenes y espectadores. Un sistema de atracos bien diseñado incluye varias fases: vigilancia previa, inicio del atraco, hackeo o taladrado de los sistemas de seguridad mediante minijuegos, carga del botín y huida con la policía persiguiendo. Empieza definiendo qué bancos se pueden atracar, su nivel de dificultad, cooldowns y los ítems necesarios. El atraco debería exigir herramientas concretas como termita para puertas de cámara, dispositivos de hackeo para paneles de seguridad y bolsas deportivas para transportar el botín. Implementa minijuegos de dificultad progresiva para cada capa de seguridad, donde fallar un hackeo dispare alarmas adicionales o bloquee aún más la cámara:

Config.BankRobberies = {
    ['fleeca_1'] = {
        label = 'Fleeca Bank - Legion Square',
        coords = vector3(149.73, -1042.65, 29.37),
        vault = vector3(144.87, -1044.16, 29.37),
        tier = 1,  -- 1=Fleeca, 2=Paleto, 3=Pacific Standard
        cooldown = 7200,  -- 2 horas
        minPolice = 3,
        requiredItems = {'electronickit', 'thermite'},
        reward = { min = 40000, max = 80000, markedBills = true },
        securityLayers = {
            { type = 'hack', difficulty = 'easy', time = 30 },
            { type = 'thermite', time = 10 },
            { type = 'drill', time = 45 },
        },
    },
    ['pacific_standard'] = {
        label = 'Pacific Standard Bank',
        coords = vector3(255.85, 225.60, 101.88),
        vault = vector3(262.20, 222.10, 101.68),
        tier = 3,
        cooldown = 14400,  -- 4 horas
        minPolice = 6,
        requiredItems = {'electronickit', 'thermite', 'advancedlaptop'},
        reward = { min = 200000, max = 400000, markedBills = true },
        securityLayers = {
            { type = 'hack', difficulty = 'hard', time = 20 },
            { type = 'hack', difficulty = 'hard', time = 20 },
            { type = 'thermite', time = 8 },
            { type = 'drill', time = 60 },
            { type = 'hack', difficulty = 'expert', time = 15 },
        },
    },
}

Usa billetes marcados como recompensa del atraco en lugar de efectivo limpio, obligando a los criminales a blanquear el dinero mediante ciclos de juego adicionales como lugares de blanqueo o cambios de dinero sucio. Esto alarga el rol del atraco más allá del golpe en sí y abre oportunidades para investigaciones policiales. Notifica al sistema de central cuando empieza un atraco y sigue el progreso en el servidor para que los agentes puedan responder tácticamente en función del avance de los criminales.

Seguridad y medidas antiexploit

Los sistemas financieros son el objetivo principal de los exploiters porque el dinero se traduce en poder directo in-game. Más allá de la validación en servidor ya comentada, implementa varias capas adicionales de seguridad. Añade rate limiting a todos los eventos bancarios para que un único jugador no pueda disparar cientos de peticiones de depósito o transferencia por segundo. Registra cada transacción financiera con los identificadores del jugador origen y un timestamp, para que los admins puedan rastrear el flujo de dinero e identificar exploits de duplicación. Implementa un sistema de reversión de transacciones para deshacer transacciones fraudulentas cuando se descubran exploits. Establece límites por transacción y acumulados diarios que escalen con la antigüedad de la cuenta y el tiempo de juego total, haciendo que las cuentas recién creadas sean menos útiles para blanqueo. Monitoriza patrones sospechosos como transferencias de ida y vuelta rápidas entre dos cuentas, depósitos que coincidan exactamente con una retirada de otro jugador en cuestión de segundos o aumentos de saldo sin registro de transacción asociado. Envía alertas por webhook de Discord cuando se detecte actividad sospechosa para que el equipo de moderación investigue en tiempo real sin esperar reportes de jugadores. Plantéate un sistema de cuentas congeladas donde los admins puedan bloquear cuentas durante investigaciones, evitando que el dinero explotado se gaste o se transfiera mientras se resuelve el caso.

Integración con la economía del servidor

Tu sistema bancario debería ser el hub central de todo el flujo monetario del servidor. Enruta las nóminas de los trabajos por la banca para que los jugadores reciban su salario como depósito bancario con un registro claro que muestre qué trabajo pagó y cuánto. Conecta las compras de tiendas para disparar retiradas bancarias cuando los jugadores paguen con tarjeta en lugar de efectivo, creando un rastro que aporta realismo y da motivos para usar la banca más allá del simple almacenamiento. Implementa facturación automática para costes recurrentes como impuestos de propiedades, seguros de vehículos y gastos de explotación de negocios que se descuenten de la cuenta bancaria a intervalos regulares. Si la cuenta no tiene fondos para un pago automático, el sistema debería registrar el fallo y disparar consecuencias como avisos de incautación o lapsos de seguro. Enlaza la banca con tu recurso de teléfono para que los jugadores consulten su saldo, vean transacciones recientes y hagan transferencias rápidas sin visitar físicamente un cajero o sucursal. Esta integración convierte la banca de una funcionalidad aislada en el sistema nervioso financiero que conecta cada actividad económica del servidor.

Compartir este artículo

¿Listo para mejorar tu servidor?

Echa un vistazo a nuestros scripts premium de FiveM en la tienda de Agency Scripts o únete a nuestra comunidad de Discord para soporte y novedades.