# Ficha: cómo integrar un juego

> Contexto completo para integrar un juego con **Ficha**, la billetera común de los juegos de un grupo de
> amigos. Sirve para personas y para asistentes de IA: déjalo en la raíz del repositorio de tu juego (o
> enlázalo desde tu `AGENTS.md` / `CLAUDE.md`) y tu asistente sabrá integrarlo.
>
> - Producción (dinero real): https://ficha.bezenti.com
> - Pruebas (fichas de mentira): https://ficha-pruebas.bezenti.com
> - Esta guía siempre actualizada: https://ficha.bezenti.com/FICHA.md
> - Especificación OpenAPI 3.1: https://ficha.bezenti.com/openapi.json

## Qué es Ficha

Una sola moneda para muchos juegos hechos por gente distinta. Lo que un jugador gana en un juego lo gasta en
otro, se lo envía a un amigo o lo retira a Yape. **Una ficha vale S/ 1.** Cada persona tiene una cuenta y una
billetera. Cada juego tiene su propia cuenta, la **banca**.

Tu juego hace dos cosas con el dinero de los jugadores:

- **Cobrar**: pasa fichas del jugador a la banca de tu juego (entrada a una partida, compra de un objeto…).
- **Pagar**: pasa fichas de la banca de tu juego al jugador (premio, reembolso…).

Nada más. Tu juego no crea fichas, no retira dinero y no mueve dinero entre jugadores: solo cobra hacia su
banca y paga desde ella.

## Qué necesita tu juego (lista corta)

1. **Una cuenta en Ficha** y el juego registrado en https://ficha.bezenti.com/desarrolladores (o primero en
   el entorno de pruebas). Te da:
   - el **id público** del juego (`slug`, por ejemplo `mi-juego`). No es secreto.
   - el **secreto** (`fsk_…`). Se muestra una sola vez. Es como la llave de la banca.
2. **Un servidor** (cualquier cosa que haga peticiones HTTPS y guarde el secreto en una variable de entorno):
   un backend en Node, Python, .NET o Go, un servidor dedicado de Unity o Godot, una función serverless, el
   servidor de Roblox o un bot de Discord. **Sin servidor solo puedes conectar jugadores; no puedes cobrar ni
   pagar**, porque el secreto no puede ir dentro del juego que descarga el jugador.
3. **Guardar el id de jugador de Ficha** de cada jugador (un entero) junto a tu usuario o sesión.
4. **Decidir la economía**: cuánto cuesta entrar, cuánto se paga, y **poner fondos en la banca** para poder
   pagar premios (desde el panel del juego: «Poner de tu saldo»).
5. **Una referencia única por operación**, para que reintentar nunca cobre dos veces.
6. **Manejar los errores** con mensajes claros para el jugador (sin saldo → que recargue; límite → que lo
   suba).

## Conceptos

| Concepto | Qué es |
| --- | --- |
| Céntimos | Todos los montos son enteros en céntimos: `150` = S/ 1.50. Nunca decimales. |
| Jugador | Persona con cuenta en Ficha. Tu juego la identifica por su `id` (entero), que recibe al conectarla. |
| Conexión (vinculación) | Permiso que el jugador le da a tu juego. Sin ella no puedes cobrarle ni pagarle. |
| Límite diario | Máximo que tu juego puede cobrarle a ese jugador en 24 horas (ventana móvil). Lo elige el jugador. Pagar no tiene límite. |
| Banca | Cuenta de tu juego. Los cobros entran ahí, los pagos salen de ahí. Solo puedes pagar lo que tiene. |
| Referencia | Id único tuyo para cada cobro o pago (`partida-8812-entrada`). Reintentar con la misma referencia devuelve la operación original con `"repetida": true` y no mueve dinero otra vez. |
| Secreto | `fsk_…`. Autentica a tu servidor: `Authorization: Bearer fsk_…`. Solo servidor. |

## Dónde corre cada cosa

```
 Juego del jugador (cliente)          Tu servidor (con el secreto)             Ficha
 ───────────────────────────          ────────────────────────────             ─────
 pide código de conexión ───────────────────────────────────────────────────▶ POST /api/v1/vinculaciones
 muestra «BCDF-GH23»                                                            (sin secreto)
 el jugador lo escribe en ficha.bezenti.com/vincular ─────────────────────────▶ (el jugador aprueba)
 manda codigo_dispositivo ──────────▶ GET /api/v1/vinculaciones/{cd} ─────────▶ estado = aprobada, jugador.id
                                      guarda jugador.id
 «quiero jugar» ────────────────────▶ POST /api/v1/cobros (entrada) ──────────▶ jugador → banca
 jugada / resultado ────────────────▶ decide quién ganó (¡en el servidor!)
                                      POST /api/v1/pagos (premio) ─────────────▶ banca → jugador
```

Regla de oro: **el cliente pide, el servidor decide**. Quién ganó y cuánto se paga lo calcula tu servidor. Si
el cliente pudiera decir «gané 10», cualquiera mentiría.

## Paso 1: conectar a un jugador

### Opción A: con un código (sirve para cualquier tecnología)

1. El juego (cliente o servidor, no hace falta secreto) pide un código:

   ```http
   POST https://ficha.bezenti.com/api/v1/vinculaciones
   Content-Type: application/json

   {"juego": "mi-juego", "limite_diario": 2000}
   ```

   Respuesta `201`:

   ```json
   {
     "codigo": "BCDF-GH23",
     "codigo_dispositivo": "fdc_…",
     "url": "https://ficha.bezenti.com/vincular",
     "url_directa": "https://ficha.bezenti.com/vincular?codigo=BCDF-GH23",
     "expira_en": 600,
     "intervalo": 3
   }
   ```

2. Muestra el `codigo` en pantalla junto a `url` (o un QR de `url_directa`). El jugador entra, inicia sesión,
   ve qué juego es, elige su límite diario y pulsa «Conectar».
3. Consulta cada `intervalo` segundos (sin secreto):

   ```http
   GET https://ficha.bezenti.com/api/v1/vinculaciones/{codigo_dispositivo}
   ```

   `estado` pasa de `pendiente` a `aprobada`, `rechazada` o `vencida` (a los 10 minutos). Al aprobarse:

   ```json
   { "estado": "aprobada", "juego": "mi-juego", "jugador": { "id": 12, "usuario": "mateo_99", "nombre": "Mateo" } }
   ```

4. **Si el código lo pidió el cliente**, el servidor no debe fiarse de un `id` que le mande el cliente (un
   cliente modificado puede mandar el de otra persona). El cliente le pasa el `codigo_dispositivo` al servidor
   y **el servidor consulta él mismo** esa ruta, comprueba `estado == "aprobada"` y `juego == "mi-juego"`, y
   guarda `jugador.id`. Nadie puede adivinar un `codigo_dispositivo`.

La conexión es permanente: no hay que repetirla en cada partida. Guarda el `id`.

### Opción B: con redirección (juegos web)

1. Registra en el panel tu dirección de regreso exacta, p. ej. `https://mijuego.com/ficha/callback`
   (`http://localhost:…` vale para probar).
2. Genera un `estado` aleatorio, guárdalo en la sesión del jugador y llévalo a:

   ```
   https://ficha.bezenti.com/autorizar?juego=mi-juego&redireccion=https://mijuego.com/ficha/callback&estado=<aleatorio>&limite=2000
   ```

3. Al aprobar, vuelve a tu dirección con `?codigo=fac_…&estado=…` (o `?error=rechazado&estado=…`).
4. Comprueba que `estado` es el que guardaste y canjea el código desde tu servidor (una sola vez, antes de 5
   minutos):

   ```http
   POST https://ficha.bezenti.com/api/v1/token
   Authorization: Bearer fsk_…
   Content-Type: application/json

   {"codigo": "fac_…", "redireccion": "https://mijuego.com/ficha/callback"}
   ```

   Respuesta: `{"jugador": {"id": 12, "usuario": "mateo_99", "nombre": "Mateo"}}`.

## Paso 2: cobrar y pagar (siempre desde el servidor)

```http
POST https://ficha.bezenti.com/api/v1/cobros
Authorization: Bearer fsk_…
Content-Type: application/json

{"jugador": 12, "monto": 50, "concepto": "Entrada a la partida", "referencia": "partida-8812-entrada"}
```

Pagar es igual en `POST /api/v1/pagos`. Respuesta (`201` nueva, `200` si es un reintento):

```json
{
  "id": 481, "tipo": "cobro", "jugador": 12, "monto": 50,
  "concepto": "Entrada a la partida", "referencia": "partida-8812-entrada",
  "saldo_jugador": 1450, "banca": 3050,
  "creado": "2026-11-04T19:20:11.000Z", "repetida": false
}
```

- `concepto`: texto corto que verá el jugador en su historial (máx. 140 caracteres).
- `referencia`: obligatoria, 1–100 caracteres de `[A-Za-z0-9_.:-]`, única por juego entre cobros y pagos.
- Antes de cobrar puedes consultar `GET /api/v1/jugadores/{id}` → `saldo`, `disponible_hoy`, `vinculado`.
- Se puede pagar a un jugador que te desconectó (lo ganado es suyo); cobrarle, no.

### Convención de referencias

Deriva la referencia de algo que ya identifica la operación en tu juego, así es estable entre reintentos y
reinicios:

```
<juego>-<id de partida o compra>-<qué>      reflejos-8812-entrada, reflejos-8812-premio
tienda-<id de pedido>                        tienda-a91f2
torneo-<id>-premio-<puesto>-<jugador>        torneo-7-premio-1-12
```

Guarda la operación como «pendiente» con su referencia **antes** de llamar a Ficha. Si el proceso se cae a
mitad, al volver repite la llamada con la misma referencia: Ficha te devuelve el resultado original.

### Flujo típico de una partida

1. Comprueba (opcional) `GET /jugadores/{id}`: si `saldo` o `disponible_hoy` no alcanzan, avisa antes.
2. Comprueba que tu banca puede pagar el premio máximo (`GET /api/v1/banca` → `saldo`).
3. Cobra la entrada (`partida-N-entrada`). Si falla, no empieces la partida.
4. Juega. El servidor decide el resultado.
5. Paga el premio (`partida-N-premio`). Si falla por red o por `banca_insuficiente`, guárdalo como pendiente
   y reintenta más tarde con la **misma** referencia.

## Errores

Siempre con esta forma. **Compara `codigo`, no el mensaje**:

```json
{ "error": { "codigo": "saldo_insuficiente", "mensaje": "Saldo insuficiente: tienes S/ 0.20 y necesitas S/ 0.50.", "saldo": 20, "falta": 30 } }
```

| `codigo` | HTTP | Qué hacer |
| --- | --- | --- |
| `sin_secreto`, `secreto_invalido` | 401 | Revisa la variable de entorno. Si regeneraste el secreto, actualízalo. |
| `juego_suspendido` | 403 | El cajero suspendió tu juego. Habla con él. |
| `jugador_no_vinculado` | 403 | Ese jugador nunca conectó tu juego: muéstrale el paso de conexión. |
| `autorizacion_revocada` | 403 | Te desconectó. Pídele que vuelva a conectar (ya no puedes cobrarle). |
| `limite_excedido` | 403 | Pasa su límite diario. Trae `limite_diario` y `disponible_hoy`. Que lo suba en ficha.bezenti.com/conexiones o espere. |
| `saldo_insuficiente` | 402 | No le alcanza. Trae `saldo` y `falta`. Enlaza a ficha.bezenti.com/billetera/recargar. |
| `banca_insuficiente` | 402 | Tu banca no alcanza para el premio. Pon fondos en el panel; guarda el pago como pendiente y reintenta. |
| `referencia_repetida` | 409 | Usaste esa referencia para otra operación distinta. Es un bug tuyo: referencias únicas. |
| `monto_invalido`, `concepto_invalido`, `referencia_invalida`, `jugador_invalido`, `json_invalido`, `limite_invalido`, `juego_invalido`, `codigo_invalido` | 400 | Datos mal formados: lee `mensaje`. |
| `no_encontrado` | 404 | La vinculación u operación no existe. |
| `error_interno` | 500 | Reintenta con la misma referencia: nunca se cobra dos veces. |

Reintenta automáticamente **solo** errores de red, timeouts y 5xx. Los 4xx no cambian por reintentar.

## Todas las rutas

Base: `https://ficha.bezenti.com` (o `https://ficha-pruebas.bezenti.com`).

| Método y ruta | Secreto | Para qué |
| --- | --- | --- |
| `POST /api/v1/vinculaciones` | no (CORS abierto) | Pide un código de conexión. Cuerpo: `juego`, `limite_diario?` |
| `GET /api/v1/vinculaciones/{codigo_dispositivo}` | no (CORS abierto) | Estado; al aprobarse trae `juego` y `jugador` |
| `POST /api/v1/token` | sí | Canjea el código de la vinculación web. Cuerpo: `codigo`, `redireccion` |
| `GET /api/v1/jugadores/{id}` | sí | `id, usuario, nombre, saldo, vinculado, limite_diario, disponible_hoy` |
| `POST /api/v1/cobros` | sí | Jugador → banca. Cuerpo: `jugador, monto, concepto, referencia` |
| `POST /api/v1/pagos` | sí | Banca → jugador. Mismo cuerpo |
| `GET /api/v1/operaciones/{referencia}` | sí | Busca un cobro o pago por tu referencia |
| `GET /api/v1/banca` | sí | `saldo` de la banca y lo cobrado y pagado |
| `GET /api/v1/movimientos?limite=50&antes=<id>` | sí | Historial de la banca; `siguiente` para paginar |
| Página `/vincular?codigo=…` | — | Donde el jugador escribe el código |
| Página `/autorizar?juego=&redireccion=&estado=&limite=` | — | Vinculación web |

## Librerías (SDK)

Sin dependencias, con reintentos y referencia automática (un UUID por llamada, reutilizado en los
reintentos). Descárgalas de `https://ficha.bezenti.com/sdk/<lenguaje>/`:

| Lenguaje | Archivo | Uso mínimo |
| --- | --- | --- |
| JavaScript / TypeScript (Node 18+, Deno, Bun) | `sdk/js/ficha.js` + `ficha.d.ts` | `new Ficha(secreto, url?)` → `await ficha.cobrar(id, 50, "Entrada", "partida-1")` |
| Python 3.8+ (stdlib) | `sdk/python/ficha.py` | `Ficha(secreto, url=…)` → `ficha.cobrar(id, 50, "Entrada", "partida-1")` |
| C# (.NET 6+, Unity servidor) | `sdk/csharp/Ficha.cs` | `new Ficha(secreto, url)` → `await ficha.CobrarAsync(id, 50, "Entrada", "partida-1")` |
| Godot 4 (GDScript) | `sdk/godot/ficha.gd` | nodo `Ficha`; devuelve `Resultado` con `ok`, `datos`, `codigo` |
| Lua 5.1–5.4 / LuaJIT | `sdk/lua/ficha.lua` | `Ficha.nuevo{secreto=…, url=…, http=Ficha.http_curl()}` |

Métodos en todos (con la convención de nombres de cada lenguaje): `vincular`, `estadoVinculacion`,
`esperarVinculacion`, `urlAutorizacion`, `canjearCodigo`, `jugador`, `cobrar`, `pagar`, `operacion`,
`banca`, `movimientos`. Errores como excepción (`FichaError`) con `codigo`, `status`, `mensaje` y extras
(en Godot, `Resultado.ok == false`).

Para apuntar al entorno de pruebas pasa la URL: `new Ficha(secreto, "https://ficha-pruebas.bezenti.com")`.

¿Otro lenguaje? Es HTTP con JSON: cualquier cliente sirve. También puedes generar uno desde `openapi.json`.

## Recetas por tecnología

- **Juego web con servidor (Node/Python/…)**: vinculación por redirección; cobros y pagos en tus rutas del
  servidor. Ejemplo completo: `https://ficha.bezenti.com/sdk/ejemplos/reflejos/` (jugable en
  https://reflejos.bezenti.com).
- **Unity / Godot / GameMaker / LÖVE (escritorio o celular)**: el cliente pide el código, lo muestra y manda
  el `codigo_dispositivo` a tu servidor; el servidor verifica, guarda el id y hace cobros y pagos. El
  servidor puede ser un build dedicado del motor (Unity Dedicated Server, `godot --headless`) o un backend
  aparte en cualquier lenguaje.
- **Unity WebGL**: `HttpClient` no funciona; usa `UnityWebRequest` contra las rutas públicas (tienen CORS).
- **Roblox**: `HttpService` solo funciona en scripts de servidor (ServerScriptService): ahí van el secreto
  (en un `Secret` de la experiencia) y las llamadas. Activa «Allow HTTP Requests». El código se muestra en
  una GUI del jugador.
- **Minecraft (plugin)**: el plugin es el servidor: guarda el secreto en su configuración y llama a la API.
  Un comando `/ficha conectar` muestra el código en el chat.
- **Bot de Discord**: el bot es el servidor. `/conectar` responde con el código (mensaje efímero) y consulta
  hasta que se apruebe; guarda `discord_id → jugador.id`.
- **Sin servidor propio**: una función serverless (Cloudflare Workers, Vercel, Netlify) con el secreto en sus
  variables de entorno y dos rutas tuyas («empezar partida», «terminar partida») que validen y llamen a Ficha.

## Probar sin dinero real

1. Crea tu cuenta en https://ficha-pruebas.bezenti.com y pulsa «Recibir S/ 50 de prueba» en la billetera.
2. Registra ahí tu juego (secreto distinto del de producción) y pon fichas de prueba en su banca.
3. Apunta tu juego a esa URL. Crea una segunda cuenta de prueba para jugar como otra persona.
4. Cuando todo funcione, registra el juego en https://ficha.bezenti.com, cambia URL y secreto, y pon fondos
   reales en la banca.

## Seguridad (obligatorio)

- **El secreto solo en el servidor**, en una variable de entorno. Nunca en el cliente, ni en el repositorio,
  ni en un `.exe`, `.apk`, `.pck`, `.love` o JavaScript del navegador: se extrae en minutos.
- **El servidor decide** resultados y montos. El cliente solo pide.
- **No confíes en el id de jugador que mande el cliente**: verifica la vinculación en el servidor (paso 1.4).
- **Pon en la banca solo lo necesario** para los premios del día: limita el daño de un error.
- **Referencias deterministas** y operación «pendiente» guardada antes de llamar.
- Si el secreto se filtró: «Crear secreto nuevo» en el panel; el anterior deja de valer al momento.

## Lista de control antes de publicar

- [ ] El secreto está en una variable de entorno del servidor y no aparece en el repositorio ni en el cliente.
- [ ] Los jugadores pueden conectar su cuenta y el id se guarda tras verificarlo en el servidor.
- [ ] Cobros y pagos se hacen solo desde el servidor, con referencias deterministas y únicas.
- [ ] El resultado de cada partida lo decide el servidor.
- [ ] `saldo_insuficiente`, `limite_excedido`, `jugador_no_vinculado`, `autorizacion_revocada` y
      `banca_insuficiente` muestran un mensaje claro con qué hacer.
- [ ] Los pagos fallidos quedan pendientes y se reintentan con la misma referencia.
- [ ] Antes de cobrar una entrada, la banca tiene para el premio.
- [ ] Probado de punta a punta en ficha-pruebas.bezenti.com con dos cuentas.
- [ ] En producción: juego registrado, secreto nuevo y banca con fondos.

## Preguntas frecuentes

**¿Cuánto cobra Ficha?** Nada por ahora: ni a jugadores ni a juegos.

**¿Cómo gano dinero con mi juego?** Lo que tu juego cobra y no paga se queda en su banca. Desde el panel lo
pasas a tu saldo y de ahí lo retiras a Yape o lo gastas en otros juegos.

**¿Puedo tener monedas propias dentro del juego?** Sí, pero dentro de tu juego. Lo que entra y sale de Ficha
siempre es en fichas (soles).

**¿Qué pasa si mi servidor se cae a mitad de una partida?** Si cobraste la entrada y no llegaste a pagar,
al volver paga con la misma referencia (o reembolsa con un pago). Nunca se duplica nada.

**¿El jugador ve lo que le cobro?** Sí: cada cobro y pago aparece en su historial con tu `concepto` y el
nombre de tu juego.
