Marco Rivera

Marco Rivera

Solutions Engineer — I build integrations that solve real operational problems.

Solutions Engineer — construyo integraciones que resuelven problemas operativos reales.

I operate businesses in Mexico City — my own and those of other owners — and I build the systems they run on: POS integrations, an auditable loyalty ledger, and the dashboards that decide what happens next. These run in production, against real customers and real money — every number below came out of a system I still maintain. Discovery, building the thing, and explaining it to a non-technical owner is already how I spend my week.

Opero negocios en la Ciudad de México — propios y de terceros — y construyo los sistemas sobre los que corren: integraciones con el punto de venta, un libro mayor auditable de lealtad, y los tableros con los que se decide qué sigue. Todo está en producción, con clientes y dinero reales — cada número de aquí abajo salió de un sistema que sigo manteniendo. Hacer discovery, construir la solución y explicársela a un dueño no técnico ya es en lo que se me va la semana.

Selected workTrabajo seleccionado

The loyalty program that was actually a coupon

El programa de lealtad que en realidad era un cupón

Loyverse POS API · Node.js · funnel analysis · custom dashboard

Problem
We paid a cash signup bonus to 464 customers, with no way to tell whether it bought loyalty or just discounts.
What I asked first
What's the correct denominator? Is the sample large enough to support a trend line? And what am I actually measuring — customer behavior, or program usage? Only 11–15% of receipts carry a customer ID, so it's the latter. That caveat ships on the dashboard itself, not buried in a README.
What I built
A three-part pipeline: cursor-paginated extraction from the Loyverse API with defensive backoff, a local cache so recomputation never hits the API, and a funnel view that reads generated JSON — no hardcoded numbers. Customer data is gitignored; the repo ships aggregate sample data so it renders for anyone who clones it.
Problema
Pagamos un bono de registro en efectivo a 464 clientes, sin forma de saber si eso compraba lealtad o solamente descuentos.
Qué pregunté primero
¿Cuál es el denominador correcto? ¿Alcanza la muestra para justificar una línea de tendencia? ¿Y qué estoy midiendo en realidad — comportamiento del cliente o uso del programa? Solo 11–15% de los recibos traen identificador de cliente, así que es lo segundo. Ese caveat va visible en el tablero, no escondido en un README.
Qué construí
Un pipeline de tres partes: extracción de la API de Loyverse con paginación por cursor y backoff defensivo, caché local para que recalcular nunca golpee la API, y una vista de embudo que lee un JSON generado — ningún número hardcodeado. Los datos de clientes están en gitignore; el repo incluye datos agregados de muestra para que renderice sin exponer a nadie.
Impact The obvious metric said 53.9% activation. I found that 87% of "post-signup purchases" happened on the signup day itself — that's the discount being applied, not a return visit. The honest number is 23.7%: roughly 3 in 4 enrolled customers never came back on a different day. The program was operating as a single-use coupon.
Impacto La métrica obvia decía 53.9% de activación. Encontré que el 87% de las "compras post-registro" ocurrían el mismo día del registro — o sea, era el descuento aplicándose, no un retorno. El número honesto es 23.7%: cerca de 3 de cada 4 registrados nunca volvieron en un día distinto. El programa operaba como cupón de un solo uso.

Replacing a daily manual stock count with a nightly job

Reemplazar el conteo diario de inventario por un proceso nocturno

Loyverse POS API · Node.js · Railway · scheduled jobs · WhatsApp alerts

Problem
I counted physical stock by hand every day — 30 minutes a day, about 15 hours a month. Despite that, we still ran out of an ingredient roughly four times a month, and I over-ordered everything else as a hedge against it.
What I asked first
What's the real unit of consumption? Sales are products, but stock is ingredients — the link between them is the recipe, so that's what had to be modeled. Then: what threshold counts as critical, and who acts on the alert? An alert nobody acts on is just noise arriving on schedule.
What I built
A Node service on Railway running a nightly scheduler. It pulls the day's sales from the Loyverse API, decomposes each product into its ingredients through its recipe, decrements stock, and sends a WhatsApp alert when any ingredient crosses its critical threshold — before the shortage, not after.
What it can't do
Computed stock drifts from physical stock, because spillage and waste never appear in sales data. The job replaces the daily count, not the periodic one — a physical recount every few weeks re-anchors the numbers. Treating the computed figure as ground truth would quietly rot the whole system.
Problema
Contaba el inventario físico a mano todos los días — 30 minutos diarios, unas 15 horas al mes. Aun así nos quedábamos sin algún insumo unas cuatro veces al mes, y compraba de más en todo lo demás como defensa contra eso.
Qué pregunté primero
¿Cuál es la unidad real de consumo? Las ventas son productos, pero el stock son insumos — lo que los une es la receta, así que eso era lo que había que modelar. Después: ¿qué nivel cuenta como crítico, y quién actúa sobre la alerta? Una alerta que nadie atiende es solo ruido con horario.
Qué construí
Un servicio en Node sobre Railway con un scheduler nocturno. Trae las ventas del día por la API de Loyverse, descompone cada producto en sus insumos según su receta, descuenta el stock, y manda una alerta por WhatsApp cuando algún insumo cruza su nivel crítico — antes del faltante, no después.
Lo que no puede hacer
El stock calculado se desfasa del físico, porque los derrames y la merma nunca aparecen en los datos de venta. El proceso reemplaza el conteo diario, no el periódico — un recuento físico cada pocas semanas vuelve a anclar los números. Tratar la cifra calculada como verdad absoluta pudriría el sistema entero en silencio.
Impact ~15 hours a month back from the daily count. Stockouts — around four a month, each one a lost sale — are now pre-empted by the alert instead of discovered at the counter. And purchasing moved from "buy extra just in case" to ordering against real consumption, which freed up cash and cut spoilage.
Impacto ~15 horas al mes recuperadas del conteo diario. Los faltantes — unos cuatro al mes, cada uno una venta perdida — ahora se anticipan con la alerta en vez de descubrirse en el mostrador. Y las compras pasaron de "pido de más por si acaso" a pedir con base en consumo real, lo que liberó efectivo y bajó la merma.

From spreadsheet to an auditable data layer, without losing a point

De hoja de cálculo a capa de datos auditable, sin perder un solo punto

PostgreSQL / Supabase · Node.js · Railway · scheduled sync

Problem
Loyalty balances for 475 customers lived in Google Sheets via Apps Script. A balance was a number in a cell — no history, no audit trail, no way to answer "why does this customer have 340 points?"
What I asked first
Can every balance be reconstructed from transactions? What happens if the sync runs twice? And does this script write anything back to the POS? I grep-audited it before running: one call, GET, no writes.
What I built
A ledger-backed schema in PostgreSQL — a balance is the sum of its transactions, each tagged with a reason and a tenant ID, multi-tenant by design. Partial unique indexes make duplicate opening balances structurally impossible. A nightly sync runs on Railway alongside the inventory scheduler.
Problema
Los saldos de lealtad de 475 clientes vivían en Google Sheets vía Apps Script. Un saldo era un número en una celda — sin historial, sin auditoría, sin forma de responder "¿por qué este cliente tiene 340 puntos?"
Qué pregunté primero
¿Se puede reconstruir cada saldo desde transacciones? ¿Qué pasa si el sync corre dos veces? ¿Y este script escribe algo de vuelta en el punto de venta? Lo audité por grep antes de correrlo: una sola llamada, GET, cero escrituras.
Qué construí
Un esquema con libro mayor en PostgreSQL — un saldo es la suma de sus transacciones, cada una con su razón y su identificador de tenant, multi-tenant por diseño. Índices únicos parciales hacen que un saldo de apertura duplicado sea estructuralmente imposible. Un sync nocturno corre en Railway junto al scheduler de inventario.
Impact 475 customers migrated, 700 ledger entries, zero duplicates and zero orphans — verified against the database, not against what the script printed. The first automated run detected and corrected 225 balances that an earlier integer column had silently rounded, and left a record of every adjustment. The system caught the bug the system was built to catch.
Impacto 475 clientes migrados, 700 asientos en el libro mayor, cero duplicados y cero huérfanos — verificado contra la base de datos, no contra lo que imprimió el script. La primera corrida automática detectó y corrigió 225 saldos que una columna entera anterior había redondeado en silencio, y dejó constancia de cada ajuste. El sistema atrapó justo el error para el que fue construido.

Reconciling two signup channels against one customer database

Reconciliar dos canales de registro contra una sola base de clientes

Data reconciliation · Loyverse API · Node.js · CSV cross-reference

Problem
Signups arrived through two channels — an external form and the website — and both were supposed to land in the POS customer database. Nobody had ever verified that they did. Every person who filled the form was promised a cash bonus.
What I asked first
What's the join key, and is it reliable? Am I comparing against the full customer list or a sample? I pulled all 434 customers through the API rather than eyeballing a page of results — a reconciliation run against a sample is worse than none, because it produces a number people trust.
What I built
A script that normalizes and deduplicates 399 form submissions down to 374 unique emails, pulls the complete paginated customer list from the API, and reports the exact set difference in both directions.
Problema
Los registros llegaban por dos canales — un formulario externo y el sitio web — y ambos debían aterrizar en la base de clientes del punto de venta. Nadie había verificado nunca que así fuera. A cada persona que llenó el formulario se le prometió un bono en efectivo.
Qué pregunté primero
¿Cuál es la llave de cruce y qué tan confiable es? ¿Estoy comparando contra la lista completa de clientes o contra una muestra? Traje los 434 clientes por API en lugar de mirar una página de resultados — una reconciliación contra muestra es peor que ninguna, porque produce un número en el que la gente confía.
Qué construí
Un script que normaliza y deduplica 399 respuestas del formulario a 374 correos únicos, trae la lista completa y paginada de clientes por API, y reporta la diferencia exacta en ambas direcciones.
Impact 98.4% matched, which confirmed the low identification rate was real and not an artifact of an uncounted parallel customer base — that validated the entire analysis above it. It also surfaced 6 people who were promised a bonus and never received it, one of them pending for over six months, and revealed that 85% of the customer base came from a channel nobody considered primary.
Impacto 98.4% de coincidencia, lo que confirmó que la baja tasa de identificación era real y no un artefacto de una base paralela de clientes sin contar — eso validó todo el análisis anterior. También sacó a la luz 6 personas a las que se les prometió un bono y nunca lo recibieron, una de ellas pendiente por más de seis meses, y reveló que el 85% de la base de clientes venía de un canal que nadie consideraba principal.

StackStack