KITKIT 29 ago 2026Aug 29, 2026
Gastos de viaje en grupo: divide las cuentas sin una app de terceros Group trip expenses: split the bill without a third-party app
Un viaje entre amigos deja decenas de gastos en monedas mezcladas y una pregunta: ¿quién le debe cuánto a quién? Con una página propia y una hoja de Google lo resuelves sin darle tus cuentas a nadie. A trip with friends leaves dozens of expenses in mixed currencies and one question: who owes whom how much? With your own page and a Google Sheet you solve it without handing your financial data to anyone.
Los promptsThe prompts
Archivos de texto. Ábrelos, cópialos y pégalos en el asistente que uses. Plain text files. Open them, copy and paste into whichever assistant you use.
Gastos de viaje en grupo: divide las cuentas sin una app de terceros
Un viaje entre amigos genera cuarenta gastos en monedas mezcladas y una sola pregunta al final: ¿quién le debe cuánto a quién? Las apps que resuelven esto piden cuenta, correo y anuncios. Aquí la construyes tú: una página que corre en el teléfono de todos, guarda en una hoja de Google tuya, funciona sin señal en el metro, y al final del viaje te dice las transferencias mínimas para quedar a mano.
Dos decisiones técnicas
1. Una bitácora que solo crece
El error natural es guardar “el estado”: la lista de gastos como un archivo que todos editan. Dos teléfonos guardando a la vez se pisan, y alguien pierde su ramen.
Todo es, en cambio, una bitácora de eventos que solo crece:
{ "tipo": "persona", "id": "u1", "nombre": "Marta" }
{ "tipo": "gasto", "id": "g7", "descripcion": "Ramen", "monto": 45000,
"moneda": "JPY", "pago": "u1", "entre": ["u1", "u2", "u3"] }
{ "tipo": "anulacion", "gasto": "g7" }json
Nadie edita ni borra: corregir un gasto es anularlo y capturarlo de nuevo. Agregar filas nunca choca con las filas de otro: no hay conflictos de concurrencia que resolver. Además queda el historial: la hoja muestra quién anuló el taxi y cuándo.
2. El dinero se cuenta en centavos enteros
Los números con decimales de tu lenguaje no saben sumar dinero: 0.1 + 0.2
no da 0.3. En una lista de cuarenta gastos, esos restos se acumulan hasta
falsear los totales, y con el descuadre se va la confianza del grupo.
La regla: el monto se convierte a centavos enteros al entrar ("450.50" →
45050), toda la aritmética es con enteros, y solo se vuelve texto al
mostrarse. Y al leer el monto, acepta coma y punto por igual: el teclado
numérico de un teléfono en español escribe 12,50, y “4.500,50” leído
ingenuamente son cuatro pesos y medio.
El reparto que suma exacto
100 pesos entre 3 no son 33.33: sobra un centavo. El reparto correcto:
- A cada quien el piso de su parte exacta.
- Los centavos que falten se reparten de uno en uno a quienes mayor fracción perdieron con el piso.
- A igual fracción, decide el orden alfabético del id — para que dos teléfonos calculen exactamente lo mismo.
La prueba que no puede faltar: para cualquier gasto, la suma de las partes es exactamente el monto, y las partes difieren entre sí a lo más un centavo.
Quién le paga a quién: transferencias mínimas
Con los saldos netos (positivo = le deben, negativo = debe), el arreglo es un
algoritmo voraz: el que más debe le paga al que más le deben, hasta saldar.
Garantiza a lo más n-1 transferencias para n personas: cuatro amigos
liquidan con tres pagos o menos, sin pagos cruzados.
Las propiedades que tu IA debe probar (pídele las pruebas, no solo el código):
- Los saldos de todo el grupo suman cero siempre.
- El arreglo paga exactamente el saldo de cada quien, ni un centavo más.
- Nunca hay más de
n-1transferencias.
La hoja de Google como servidor
Una hoja de cálculo es la base de datos
(una fila por evento) y un Apps Script publicado como aplicación web es la
puerta: GET devuelve la bitácora, POST agrega una fila. La hoja te queda
además como historial legible y como panel: ver los gastos crudos es abrirla.
El acceso se controla con una clave larga en el enlace. El enlace del grupo se arma con la URL del script y esa clave:
https://tu-pagina/viaje/?viaje=japon26&script=URL_DEL_SCRIPT&clave=TU_CLAVE
Compartir el viaje es mandar ese enlace por WhatsApp; revocarlo es cambiar la clave. Y si alguien abre la página sin los parámetros (un enlace recortado al reenviar), cae en modo “solo local”, sin conexión con el grupo: el indicador de arriba lo dice, y lo que capture ahí se queda en su teléfono.
Siete trampas (las que muerden de verdad)
1. La advertencia de “aplicación no verificada”
Al autorizar tu Apps Script por primera vez, Google muestra una advertencia de seguridad: “Google no ha verificado esta aplicación”. Es normal: la aplicación es tuya, la acabas de escribir. “Configuración avanzada” → “ir al proyecto” → permitir. Esta pantalla solo la ves tú, una vez, al autorizar; tu grupo entra por el enlace sin ver ningún aviso.
Dos condiciones más que muerden aquí: usa una cuenta personal de Google (las cuentas de empresa o escuela suelen bloquear el acceso “Cualquier usuario”, y tus amigos verían un error de permisos). Y si después corriges el script, guardar no basta: hay que publicar nueva versión en “Administrar implementaciones”, o Google sigue sirviendo la vieja.
2. Apps Script no admite POST con application/json
fetch con Content-Type: application/json contra un Apps Script falla por
CORS. Lo que sí pasa: POST con cuerpo de texto plano y redirect: 'follow'
(Google responde con una redirección). Pídeselo a tu IA con esas palabras:
“el POST al Apps Script va como text/plain siguiendo redirecciones”.
3. El teléfono sin señal no puede ser un error
En el metro no hay internet y ahí es donde se anota el ramen. Los eventos
propios se encolan en localStorage y se suben cuando vuelve la señal. Como
los eventos solo se agregan, subir tarde no pisa lo de nadie. La interfaz
debe decir la verdad: “2 por subir” es información, no un error. Y un
latido cada cinco segundos empuja la cola si hay algo pendiente: el evento
online del navegador no siempre dispara al recuperar señal.
4. La moneda sin tipo de cambio no se suma en silencio
Si alguien anota 1000 THB y nadie configuró cuánto vale un baht, lo peor que puedes hacer es sumarlo como cero o ignorarlo callado: el total miente. El gasto se aparta y la página avisa “1 gasto quedó fuera: su moneda no tiene tipo de cambio”. Con el aviso, el tipo de cambio se corrige antes de liquidar.
5. El tipo de cambio es una decisión, no un dato
Si actualizas el tipo de cambio a mitad del viaje, ¿los gastos ya anotados se recalculan? En esta receta sí: el monto original se guarda en su moneda y la conversión pasa al mostrar. Es la opción simple y auditable — pero díselo al grupo, porque los saldos pueden moverse un poco al ajustar el cambio.
6. El mismo evento puede llegar dos veces
La cola offline sube un gasto, el teléfono se muere antes de marcarlo como subido, y al revivir lo sube otra vez: dos filas idénticas en la hoja. Si tu reductor las cuenta a las dos, el ramen aparece doble y los saldos mienten. La regla: un gasto con un id ya visto se ignora. Es una línea de código y evita saldos inflados por reintentos de red.
7. prompt() y confirm() no funcionan en todos lados
Los diálogos nativos del navegador son el atajo obvio para “¿cómo se llama?” y “¿seguro?”. En los navegadores integrados de las aplicaciones (WhatsApp, Instagram — donde tu grupo VA a abrir el enlace) a veces no aparecen. Si algo no funciona “solo cuando lo abro desde WhatsApp”, es esto: abre en el navegador de verdad, o pide a tu IA reemplazarlos por diálogos propios.
El grupo: planes, votos y lugares en la misma bitácora
La bitácora que solo crece acepta más tipos de evento sin tocar lo construido, y esa es la razón de haberla elegido. Tres más convierten el divisor de cuentas en el tablero del viaje:
{ "tipo": "plan", "id": "pl1", "texto": "Fushimi Inari al amanecer", "dia": "2026-11-04" }
{ "tipo": "voto", "id": "v9", "plan": "pl1", "valor": 1 }
{ "tipo": "lugar", "id": "lg7", "nombre": "Ichiran", "ciudad": "Kioto", "estrellas": 4 }json
Las reglas caben en tres líneas: un voto por persona y plan, gana el último (cambiar de opinión es votar otra vez); los planes se ordenan por día y, a igual día, por votos; un lugar lleva sus estrellas y, si quieres, la ciudad (se queda puesta: en Kioto se anotan cinco lugares seguidos). Todo en la misma hoja: el gasto del ramen, el voto de mañana y las estrellas del restaurante son filas vecinas, y cualquiera del grupo ve lo de los demás al cabo de unos segundos.
Dos trampas propias de aquí. La primera: la vista nunca encoge. La página relee la hoja cada 40 segundos (y de inmediato al volver a la pestaña), y una lectura vieja que llega tarde no puede quitar de la pantalla el voto que acabas de dar; por eso lo leído se une a lo que ya se tenía en vez de reemplazarlo. La segunda: un voto sin id propio se confunde con otro dado en el mismo milisegundo cuando la cola los reintenta; dale id a todo.
El cierre: del viaje al mapa de recuerdos
Al volver, el viaje no debería quedarse en una hoja de cálculo. El botón de
cierre pide nombre, país y fechas, guarda esos metadatos como un evento más
(el título cambia para todo el grupo) y arma un recuerdo: los lugares
agrupados por ciudad, cada ciudad con el promedio redondeado de sus
estrellas (los lugares sin calificar no bajan la nota), todo fechado con
el mes del viaje, las personas por nombre y un id propio del cierre.
{ "version": 1, "id": "rec-japon26", "viaje": { "nombre": "Japón 2026", "pais": "Japón" }, "fecha": "2026-11",
"personas": ["Licio", "Marta"],
"ciudades": [ { "nombre": "Kioto", "puntuacion": 4,
"lugares": [ { "nombre": "Fushimi Inari", "estrellas": 5 } ] } ] }json
Ese paquete lo recibe el mapa de viajes: si las dos
páginas viven en el mismo sitio, viaja solo por localStorage; si no, se
copia y se pega. El mapa no se cree nada: relee el paquete campo por
campo, pregunta el país, deja elegir quiénes de los tuyos estuvieron (los
invitados del viaje no van al mapa) y funde: una ciudad que ya existía suma
una visita y conserva su primera fecha; la nueva se crea con sus estrellas y
sus lugares como nota. Y apunta el id de cada cierre que aplica: el mismo
recuerdo pegado dos veces no es una segunda visita. El registro del viaje
alimenta el recuerdo, y las cuentas quedan saldadas por el camino.
Qué puede salir mal
- Borrar filas de la hoja a mano. Se puede — es tu hoja — pero rompe la regla de “solo crece”. Si borras el gasto pero no su anulación, o al revés, el estado de todos cambia. Para corregir: anula desde la página.
- La clave viaja en el enlace. Suficiente para un grupo de amigos; no para secretos. Quien tenga el enlace, entra. Cambiar la clave saca a todos.
- Cuota de Apps Script. Los límites gratuitos aguantan de sobra un grupo de viaje; no aguantarían una app pública.
- Dos pestañas en modo local.
localStorageno tiene transacciones: dos pestañas del mismo viaje guardando en el mismo instante pueden pisarse un evento. El modo local es “un aparato, una pestaña”; para varias personas anotando a la vez está la hoja, donde agregar filas nunca choca.
Group trip expenses: split the bill without a third-party app
A trip with friends leaves dozens of expenses in mixed currencies and a single question at the end: who owes whom how much? The apps that solve this demand an account, an email, and serve ads. Here you build it yourself: a page that runs on everyone’s phone, saves to a Google Sheet you own, works with no signal on the subway, and at the end of the trip tells you the minimal transfers to settle up.
Two technical decisions
1. A log that only grows
The natural mistake is to store “the state”: the list of expenses as a file everyone edits. Two phones saving at the same time overwrite each other, and someone loses their ramen.
Instead, everything is an event log that only grows:
{ "tipo": "persona", "id": "u1", "nombre": "Marta" }
{ "tipo": "gasto", "id": "g7", "descripcion": "Ramen", "monto": 45000,
"moneda": "JPY", "pago": "u1", "entre": ["u1", "u2", "u3"] }
{ "tipo": "anulacion", "gasto": "g7" }json
Nobody edits or deletes: correcting an expense means voiding it and entering it again. Appending rows never collides with anyone else’s: there are no concurrency conflicts to resolve. And you get history for free: the sheet shows who voided the taxi, and when.
2. Money is counted in integer cents
Your language’s floating-point numbers can’t add money: 0.1 + 0.2 is not
0.3. Over forty expenses those leftovers pile up until the totals are
wrong, and with the mismatch goes the group’s trust.
The rule: the amount is converted to integer cents on the way in
("450.50" → 45050), all arithmetic is on integers, and it only becomes
text when displayed. And when reading the amount, accept comma and period
alike: a Spanish phone keyboard types 12,50, and “4.500,50” read naively
is four and a half pesos.
The split that adds up exactly
100 pesos split three ways is not 33.33: a cent is left over. The correct split:
- Everyone gets the floor of their exact share.
- The missing cents go one by one to whoever lost the largest fraction to the floor.
- On a tie, the alphabetical order of the id decides — so that two phones compute exactly the same thing.
The essential test: for any expense, the shares sum exactly to the total, and differ from each other by at most one cent.
Who pays whom: minimal transfers
With the net balances (positive = is owed, negative = owes), the settlement
is a greedy algorithm: the biggest debtor pays the biggest creditor, until
everything is settled. It guarantees at most n-1 transfers for n
people: four friends settle with three payments or fewer, with no circular
transfers.
The properties your AI must prove (ask for the tests, not just the code):
- The balances of the whole group always sum to zero.
- The settlement pays exactly each person’s balance, not a cent more.
- There are never more than
n-1transfers.
The Google Sheet as the server
A spreadsheet is the database (one row per event) and an Apps Script
published as a web app is the door: GET returns the log, POST appends a
row. The sheet doubles as a readable audit log and dashboard: to inspect the
raw expenses, just open it.
Access is controlled by a long key in the link. The group link is built from the script URL and that key:
https://your-page/viaje/?viaje=japan26&script=SCRIPT_URL&clave=YOUR_KEY
Sharing the trip is sending that link on WhatsApp; revoking it is changing the key. And if someone opens the page without the parameters (a link truncated when forwarding), they land in “local only” mode, disconnected from the group: the indicator at the top says so, and whatever they enter there stays on their phone.
Seven traps that bite
1. The “unverified app” warning
When you authorize your Apps Script for the first time, Google shows a security warning: “Google hasn’t verified this app”. It’s normal: the app is yours, you just wrote it. “Advanced” → “go to project” → allow. Only you see this screen, once, when authorizing; your group enters through the link without any warning.
Two more catches here: use a personal Google account (work or school accounts usually block “Anyone” access, and your friends would see a permissions error). And if you fix the script later, saving isn’t enough: you must publish a new version under “Manage deployments”, or Google keeps serving the old one.
2. Apps Script doesn’t accept POST with application/json
fetch with Content-Type: application/json against an Apps Script fails
on CORS. What does work: POST with a plain-text body and redirect:
'follow' (Google answers with a redirect). Ask your AI in those words: “the
POST to the Apps Script goes as text/plain following redirects”.
3. Being offline is not an error
There’s no internet on the subway, and that’s where the ramen gets entered.
Your own events are queued in localStorage and uploaded when the signal
returns. Since events are only appended, uploading late overwrites nobody.
The interface must tell the truth: “2 to upload” is information, not an
error. And a heartbeat every five seconds pushes the queue whenever
something is pending: the browser’s online event doesn’t always fire
when the signal returns.
4. Missing exchange rates are never summed silently
If someone enters 1000 THB and nobody set what a baht is worth, the worst you can do is add it as zero or ignore it quietly: the total lies. The expense is set aside and the page warns “1 expense was left out: its currency has no exchange rate”. With the warning, the rate gets fixed before settling.
5. The exchange rate is a decision, not raw data
If you update the exchange rate mid-trip, do the expenses already entered get recalculated? In this recipe, yes: the original amount is stored in its currency and the conversion happens on display. It’s the simple, auditable option — but tell the group, because balances may shift a little when the rate is adjusted.
6. The same event can arrive twice
The offline queue uploads an expense, the phone dies before marking it as uploaded, and on waking up uploads it again: two identical rows in the sheet. If your reducer counts both, the ramen shows up twice and the balances lie. The rule: an expense with an id already seen is ignored. One line of code, and no balances inflated by network retries.
7. prompt() and confirm() don’t work everywhere
The browser’s native dialogs are the obvious shortcut for “what’s your name?” and “are you sure?”. In the in-app browsers of messaging apps (WhatsApp, Instagram — where your group WILL open the link) they sometimes don’t appear. If something fails “only when I open it from WhatsApp”, this is it: open it in a real browser, or ask your AI to replace them with dialogs of your own.
The group: plans, votes and places in the same log
The log that only grows accepts more event types without touching what’s built, and that’s why it was chosen. Three more turn the bill splitter into the trip’s board:
{ "tipo": "plan", "id": "pl1", "texto": "Fushimi Inari at dawn", "dia": "2026-11-04" }
{ "tipo": "voto", "id": "v9", "plan": "pl1", "valor": 1 }
{ "tipo": "lugar", "id": "lg7", "nombre": "Ichiran", "ciudad": "Kyoto", "estrellas": 4 }json
The rules fit in three lines: one vote per person and plan, the last one wins (changing your mind is voting again); plans are sorted by day and, on the same day, by votes; a place carries its stars and, if you want, the city (it stays set: in Kyoto you log five places in a row). All in the same sheet: the ramen expense, tomorrow’s vote and the restaurant’s stars are neighboring rows, and anyone in the group sees everyone else’s updates within seconds.
Two traps specific to this. First: the view never shrinks. The page re-reads the sheet every 40 seconds (and immediately when you switch back to the tab), and an old read arriving late cannot remove the vote you just cast; what is read is merged into what you already had instead of replacing it. Second: a vote without its own id gets confused with another cast in the same millisecond when the queue retries them; give everything an id.
The closing: from the trip to the map of memories
Back home, the trip shouldn’t stay in a spreadsheet. The closing button
asks for a name, a country and dates, stores that metadata as one more event
(the title changes for the whole group) and builds a memory: the places
grouped by city, each city with the rounded average of its stars (unrated
places don’t drag it down), everything dated with the trip’s month, the
people by name, and an id of its own.
{ "version": 1, "id": "rec-japan26", "viaje": { "nombre": "Japan 2026", "pais": "Japan" }, "fecha": "2026-11",
"personas": ["Licio", "Marta"],
"ciudades": [ { "nombre": "Kyoto", "puntuacion": 4,
"lugares": [ { "nombre": "Fushimi Inari", "estrellas": 5 } ] } ] }json
That package is received by the travel map: if the
two pages live on the same site, it travels on its own through
localStorage; if not, you copy and paste it. The map trusts nothing:
it validates the package field by field, asks for the country, lets you pick
who from your group was there (travel guests stay off the map), and merges:
an existing city gains a visit and keeps its first-visit date; a new one is
created with its rating and its places as a note. It also records the id
of every closing it applies: the same memory pasted twice is not a second
visit. The trip log feeds the memory, and the bills get settled along the
way.
What can go wrong
- Deleting rows in the sheet by hand. You can — it’s your sheet — but it breaks the “only grows” rule. If you delete the expense but not its voiding, or the other way around, everyone’s state changes. To correct: void from the page.
- The key travels in the link. Enough for a group of friends; not for secrets. Whoever has the link gets in. Changing the key kicks everyone out.
- Apps Script quota. The free limits comfortably handle a travel group; they wouldn’t handle a public app.
- Two tabs in local mode.
localStoragehas no transactions: two tabs of the same trip saving at the same instant can overwrite an event. Local mode is “one device, one tab”; for several people entering at once there’s the sheet, where appending rows never collides.