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.
LinkedIn
GitHub
marco@marcorivera.dev
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.
▸ 3-min walkthroughVideo de 3 min
▸ CodeCódigo
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.
▸ 3-min walkthroughVideo de 3 min
▸ CodeCódigo
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.
▸ 3-min walkthroughVideo de 3 min
▸ CodeCódigo
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.
▸ 3-min walkthroughVideo de 3 min
▸ CodeCódigo
StackStack
- LanguagesLenguajes — JavaScript / Node.js, Python, SQL
- DataDatos — PostgreSQL, Supabase,
REST API integration (cursor pagination, rate limits, backoff)integración de APIs REST (paginación por cursor, rate limits, backoff)
- InfrastructureInfraestructura — Railway, AWS (IAM Identity Center, Bedrock), Git,
scheduled jobs, WhatsApp alertingjobs programados, alertas por WhatsApp