Todos los proyectos
Live · web + APKProyecto 09
Calculadora cambiariaAndroid · Web · WindowsMulti-tasa

Venekambio

Una cifra, seis monedas, cero servidores. Sin login, sin cuentas y sin backend: las tasas se leen desde el propio teléfono y todo lo que se guarda se queda en él. $0/mes operativo.

FlutterRiverpodNuxt 4 SSGTypeScriptVercel (estático)Windows

Una cifra, seis monedas, cero servidores.

En una caja venezolana conviven el dólar BCV, el USDT, el euro, el Monitor y los bolívares. Venekambio los muestra todos a la vez con un solo tecleo, y lo hace sin backend: las tasas se leen desde el propio teléfono.

Moneda base
Tecleas una cifra
250 USDT
USDT · sin cambiar de pantalla, sin segundo tap
BSbase
Bs240.875,00
BCVoficial
$281,07
EURoficial
€246,57
MONparalelo
$249,87
ETHP2P
Ξ0,0936
Tasas promedio tal como las devolvieron las fuentes el 28-sep-2026. Cambia la base: los seis resultados salen de la misma función.
$0/mesde operación: sin backend, sin base de datos, sin cuentas
6monedas por cada cifra: Bs, BCV, EUR, MON, USDT, ETH
3superficies: Android, web y un launcher para Windows
01 · Cómo funciona

De abrir la app a seis resultados sin pasar por un servidor

Pulsa los pasos o deja que avance. Cada paso ilumina la pieza de la que habla.

Abres la app
sin login, sin pasos
4 fuentes
en paralelo · 5 s
Snapshot
inmutable
convert()
from → Bs → to
6 monedas
hero + 9 chips
Paso 1 de 5 · avanza solo
La primera pantalla ya es la calculadora
No hay cuenta, ni onboarding, ni formulario. Abres y ves el teclado numérico bajo el display. Todo lo que la app recuerda (base, historial, datos de Pago Móvil) vive en el propio teléfono.

La conversión completa. Es la misma función en la app, en la web y en el launcher de Windows.

// web/composables/useConvert.ts
if (from === to) return value
const bs = from === 'BS' ? value : value * (rates[from] ?? 0)
if (to === 'BS') return bs
const target = rates[to]
if (!target || target === 0) return 0
return bs / target
02 · Arquitectura

Lo que hay y lo que no hay

Tres clientes, cuatro fuentes públicas y el estado en el dispositivo. La casilla del servidor propio está vacía a propósito.

Sin backend
Fuentes públicas
ve.dolarapi.com
BCV · EUR · Monitor
2 requests
criptoya.com
USDT · ETH (Binance P2P)
2 requests
4 requests HTTPS en paralelo · timeout 5 s · sin clave
Cliente
Android
Flutter · Riverpod
Web
Nuxt 4 SSG · TypeScript
Windows
Flutter · quick-launcher
todo lo que se guarda se queda aquí
Dispositivo
Prefs del teléfono
SharedPreferences · historial (20) · ajustes · Pago Móvil
localStorage
orden y visibilidad de chips
Servidor propio
no existe
$0/mes

La entrega también es estática: GitHub construye la web en Vercel, y el APK y el instalador de Windows son dos ficheros más en /downloads del mismo dominio.

03 · Un ejemplo de punta a punta

Un kilo de queso, cuatro tasas y un cliente esperando

Un caso inventado con las tasas reales del 28-sep-2026, para ver qué mira cada persona antes de hablar de decisiones.

1
07:40 · mercado · ejemplo
Carmen abre la app
Vende queso. Compra la mercancía en USDT y cobra en bolívares. Abre Venekambio: cuatro peticiones salen a la vez y en el hero aparece el punto verde de Actualizado 07:40. Su base guardada es USDT.
2
4,20 USDT
Teclea lo que le costó el kilo
Sin cambiar de pantalla ve Bs 4.046,70, $ 4,72 BCV, € 4,14 y $ 4,20 MON. Decide cobrar Bs 4.500 el kilo.
3
el cliente
Mira otra tasa distinta
El cliente compara mentalmente contra el BCV, la referencia que ve en el supermercado: Bs 4.500 son $ 5,25 BCV. Carmen, en cambio, sabe que son 4,67 USDT, y que le quedan 0,47 USDT por kilo. Las dos cifras salen de la misma pantalla.
4
Pago Móvil
Cobra sin dictar cuatro datos
Banco, cédula, teléfono y nombre están guardados en el teléfono. Los muestra, el cliente transfiere. La operación queda en el historial local, que conserva las últimas 20.
5
lunes siguiente
La tasa del BCV no cambia aunque el BCV ya publicó otra
Es lunes bancario. La app sigue mostrando la tasa vigente del viernes, porque lee la vigente y no la última publicada. Por qué importa, en el reto 02.
04 · Los retos

Cinco veces en que lo obvio estaba mal

Cada reto sigue el mismo patrón: lo que cualquiera haría primero, por qué no sirve, lo que se hizo y el número que lo demuestra. El detalle técnico queda plegado.

01

Una conversión no es un cálculo, son cuatro

Lo obvio
Hacer lo que hacen todas las calculadoras de cambio: eliges A, eliges B, tecleas, ves un número.
Por qué falla
En una caja venezolana nadie necesita una conversión. El comerciante compra en USDT y vende en BCV; el cliente paga en bolívares y compara contra BCV; si tiene ahorros en USDT calcula cuánto cambiar. Con A → B eso son tres o cuatro repeticiones del mismo monto, con la fila esperando.
Calculadora A → B · en caja
250 · USDT → Bs
250 · USDT → BCV
250 · USDT → EUR
Tres veces el mismo monto, tres cambios de moneda, con el cliente esperando.
Venekambio
250 · base USDT
Una entrada. Bs, BCV, EUR, MON y ETH ya calculados en la misma pantalla.
Lo que se hizo
La calculadora no produce un resultado, produce un snapshot. Al teclear, la conversión corre contra las seis monedas a partir del mismo RatesSnapshot inmutable: hero para la base y una tira de chips con las otras ya calculadas. Sin scroll, sin tabs, sin segundo tap.
6 → 1seis resultados por un tecleo. Calcular seis en vez de uno cuesta nada; lo caro es el fetch, y ocurre una vez por sesión.
Ver detalle técnico

En Dart: visibleCodes.map((code) => convert(value, baseCode, code, snap)). En TypeScript, la misma línea sobre snapshot.asRateMap(). La función es pura y triangula por Bs, así que el orden de las monedas o cuántas estén visibles no cambia nada.

El coste es que la pantalla muestra más números que cualquier competidor. Se compensa con jerarquía (un hero grande, chips pequeños) y con la posibilidad de ocultar y reordenar chips, que se guarda en el dispositivo.

02

La tasa de hoy no es la última que publicó el BCV

Lo obvio
Tomar el último número que aparece en la fuente. Es el más reciente, así que es el de hoy.
Por qué falla
El BCV publica cada tarde la tasa con fecha valor del siguiente día bancario. Y los lunes bancarios la banca no opera: el viernes se publica ya la del martes, saltándose el lunes. La fila más reciente el lunes 14 de septiembre decía 842,2067 y regía el martes; la vigente ese lunes seguía siendo 832,4883, la del viernes.
Vie 11-sep
832,4883
Tasa con fecha valor 11. El viernes por la tarde el BCV publica ya la siguiente.
Lun 14-sep · bancario
832,4883
La banca no opera. Sigue rigiendo la del viernes. No existe fila para este día.
Mar 15-sep
842,2067
La publicada el viernes trae fecha valor martes: se salta el lunes.
Leer la última fila publicada
842,2067 ✗
Ese lunes cobrarías con una tasa que todavía no rige.
Leer la tasa vigente
832,4883 ✓
La que el BCV tiene en vigor hoy, aunque ya haya publicado la de mañana.
Lo que se hizo
La app lee el endpoint de tasa vigente (/v1/cotizaciones), que filtra por fecha valor, y nunca la última fila del histórico. La fecha valor viaja en Rate.timestamp; la hora que ve el usuario en Actualizado 07:40 es la de la lectura, no la del BCV. Se verificó contra la página del BCV un lunes bancario.
+1,17 %de error al leer la fila equivocada ese lunes (842,2067 frente a 832,4883). En una venta de Bs 50.000 son 0,69 dólares BCV cobrados de más.
Ver detalle técnico

Lo que hay que saber de ve.dolarapi.com: fechaActualizacion no es cuándo se actualizó el dato, es la fecha valor del BCV. La ruta de tasa vigente filtra fecha ≤ hoy; la ruta de históricos no filtra y expone la fila con fecha futura. El 28-sep-2026, un lunes, el histórico ya traía la fila del martes 29 (857,8876) mientras la vigente era 857,0058.

No hay endpoint para "la tasa de mañana". El único modo de verla es ese desliz del histórico, y en un lunes bancario no existe fila para ese día: el histórico devuelve 404.

La app no muestra hoy la fecha valor al usuario, solo la hora de lectura. Un comerciante que necesite saber si "la de mañana" ya salió tiene que ir a la página del BCV.
03

Cuatro fuentes, ninguna con SLA, ningún servidor propio

Lo obvio
Un backend que agregue las cuatro fuentes, las normalice, las cachee y exponga un endpoint estable.
Por qué falla
Ese backend es un servidor 24/7, un pipeline, una base de datos y una factura mensual. Para un producto cuyo público es el que menos puede pagarlo, la factura obliga a monetizar, y monetizar añade la fricción que se quería quitar. Y no arregla el problema de fondo: las fuentes siguen siendo las mismas.
ve.dolarapi.com200 · ok
/v1/cotizaciones
BCV y EUR oficiales
ve.dolarapi.com200 · ok
/v1/dolares/paralelo
Dólar Monitor
criptoya.com200 · ok
/api/binancep2p/USDT/VES/1
USDT P2P · compra y venta
criptoya.comtimeout 5 s
/api/binancep2p/ETH/VES/1
ETH → 0 · su chip queda sin tasa, los otros cinco siguen
Future.wait en Dart, Promise.allSettled en TypeScript: cada petición devuelve su resultado o null, nunca tumba a las demás.
Lo que se hizo
El cliente consume las cuatro fuentes directo, en paralelo, en cada apertura. Cada petición va envuelta en un _safeGet con timeout de 5 s que devuelve null si falla. La moneda que no llegó vale 0: la conversión devuelve 0 y su chip se apaga en vez de tumbar la pantalla. Sin servidor no hay punto único de fallo ni nadie que vea qué consulta cada usuario.
4 de 4peticiones independientes. Si ETH cae, BCV, EUR, Monitor y USDT siguen llegando. Sin reintentos automáticos: un reintento largo es peor que un 0 visible.
Ver detalle técnico

El wrapper HTTP no tiene interceptores ni retries; captura ClientException, TimeoutException y FormatException y devuelve null. El repositorio se inyecta por Riverpod, así que los tests lo sustituyen por un FakeRatesRepository sin red.

Deuda consciente: no se guarda el último snapshot. Si la primera apertura no tiene red, no hay tasas hasta que vuelva. Está en la retrospectiva.
04

Una tasa no siempre es un número

Lo obvio
Modelar la tasa como un double: bolívares por unidad, y listo.
Por qué falla
El BCV publica un decreto: un número sin spread. USDT y ETH no son un número, son un libro de órdenes con compra y venta reales, y esa diferencia es información para quien va a operar. Un solo campo obliga a elegir: aplastar el spread del P2P en un promedio, o inventarle un spread fantasma al BCV. Una pierde información; la otra miente.
BCV · oficial
857,0058
Un decreto, un número. No hay spread que mostrar: compra = venta = promedio.
USDT · Binance P2P
Compra
963
Venta
964
Prom
963,50
Un libro de órdenes: el que compra y el que vende ven números distintos. Tres chips en la app.
Lo que se hizo
Un solo tipo Rate con tres lados (buy, sell, avg) y un campo aparte, bsPerUnit, como valor canónico para convertir. Para fuentes oficiales los tres lados son el mismo número. Para P2P se guardan compra y venta reales y el promedio se calcula; si una pata llega en 0 se usa la otra para no propagar ceros.
9 chipspara 6 monedas: USDT y ETH exponen compra, venta y promedio; BCV, EUR y Monitor un solo valor. Cada chip se puede ocultar.
Ver detalle técnico

Bs como pivote evita la matriz de pares: seis monedas serían 30 conversiones; con un valor por moneda contra Bs bastan seis. El modelo es idéntico en web/types/rate.ts y app/lib/domain/entities/rate.dart, con el catálogo de monedas alineado a mano entre los dos.

history7d existe en el modelo pero ninguna de las fuentes publica histórico: hoy son siete copias del promedio. Está en la retrospectiva.

05

Diseñar para quien nunca instaló una app

Lo obvio
Un onboarding de tres pantallas, registro con correo y un tutorial la primera vez.
Por qué falla
El usuario es un comerciante de mercado, muchas veces con resistencia previa a lo digital y poca paciencia con textos largos. Cada paso antes del primer cálculo es una razón para cerrar y no volver. Y hay un segundo usuario, el adulto mayor, para quien un número pequeño es un número que no existe.
display_number.dart · scaleFor(formatted)
4.046,70≤ 9 caracteres · 100 %
1.240.875,50> 9 caracteres · 82 %
12.573.545,22> 12 caracteres · 70 %, el piso
Lo que se hizo
Sin login, sin cuentas, sin onboarding: la primera pantalla es la calculadora con las tasas cargando. Un teclado numérico grande en vez de un campo de texto. El número del hero parte de 64 px y baja por escalones (82 % y 70 % como piso) en vez de truncarse. Modo grande opcional. Los datos de Pago Móvil se guardan sin validaciones que interrumpan.
44 pxmínimo de cada tecla, 52 px en Modo grande. Paleta base (tinta #1A1613 sobre #FAF6F0) elegida cumpliendo contraste AA antes de dibujar un solo componente.
Ver detalle técnico

En la web el hero usa clamp(38px, 4.5vw, 52px) y la misma escala por longitud. La demo de la landing acepta teclado físico y hace scroll al display al tocar.

El coste es una app con menos funciones visibles que un competidor: sin gráficos, sin alertas, sin perfil. Cada función nueva es una superficie más que aprender, y ese usuario no va a aprenderla.

"En caja, frente al cliente, el flujo no sobrevive a la fricción real."
Cada decisión del producto pasa por la misma pregunta: ¿le sirve a un comerciante que necesita ver cuatro tasas en dos segundos? Si la respuesta no es directa, la decisión está mal planteada.
05 · Decisiones

Un solo filtro: ¿esto exige un servicio mensual?

Si la respuesta era sí y había alternativa razonable, se eligió la alternativa. Lo que sigue es cada decisión con su coste.

WHY
¿Por qué sin backend?
Porque cada servicio es una factura, y una factura obliga a cobrar a quien menos puede. Las fuentes son públicas y sin clave, la conversión son cinco líneas y el estado cabe en el teléfono. Un servidor solo añadiría latencia, un punto único de fallo y un lugar donde se ven las consultas de la gente.
Profundizar

Lo que se pierde: caché compartida entre usuarios, histórico propio de tasas y la posibilidad de cambiar de fuente sin publicar una versión nueva. Lo primero no importa a esta escala; lo segundo y lo tercero son el precio de $0/mes y se aceptan a sabiendas.

Todo el stack · $0/mes
Backend
$0 · no existe
Las 4 fuentes se leen desde el cliente, en cada apertura.
Hosting web
$0 · Vercel · estático
Nuxt 4 SSG: HTML prerenderizado, deploy automático desde GitHub.
Base de datos
$0 · en el dispositivo
SharedPreferences en Flutter, localStorage en la web. Historial de 20 operaciones.
Tasas
$0 · APIs públicas
dolarapi.com y criptoya.com, sin clave, sin cuenta.
Distribución
$0 · ficheros estáticos
APK de 51 MB e instalador de Windows de 11 MB en /downloads del propio dominio.
Cuentas
$0 · no existen
Sin login ni perfiles. No hay nada que proteger porque no hay nada que guardar fuera.
Analítica
$0 · GA4 · anónima
Solo en la web, en producción, con DNT respetado y sin IP.
El único gasto recurrente es el dominio, unos $10 al año. Todo lo demás vive en tiers gratuitos sin techo relevante para el tamaño actual del producto.
WHY
¿Por qué Flutter y no React Native o nativo?
Dos plataformas nativas son dos ciclos de release para una sola persona. React Native era viable, pero el puente JS se nota en teléfonos económicos, que son justo los del público, y la interfaz depende de control fino sobre tipografía y escalas. Flutter compila a nativo y cada widget es suyo.
Profundizar

El coste es el peso: 51 MB de APK, más que su equivalente nativo. Viene del motor de Flutter embebido, es fijo y no crece con las funciones. Para un APK que se descarga desde la web es algo a vigilar, no un bloqueo.

La misma decisión sirvió para la tercera superficie: el launcher de Windows es Flutter con window_manager, hotkey_manager y tray_manager. Se invoca con Ctrl + Alt + Espacio, evalúa expresiones completas (1.250 × 3 + 980) con precedencia y paréntesis, y vive en la bandeja.

En Windows los efectos translúcidos (acrylic, Mica) descuadraban la superficie de render respecto a la ventana sin marco. Se probó con una ventana normal para descartar el layout y se dejó el panel opaco. No es bug de widgets: es el compositor.
WHY
¿Por qué Nuxt 4 SSG y no una SPA o SSR?
La web tiene que encontrarse ("calculadora BCV", "cambio Bs USDT") y cargar rápido en redes inestables. Una SPA sirve HTML vacío y deja el SEO al azar del JavaScript. SSR exige un servidor 24/7. SSG prerenderiza en build, se sirve como estático y deja la calculadora reactiva en el cliente.
Profundizar

Flutter Web para la landing quedó descartado por lo mismo: pinta en canvas y los rastreadores ven una página vacía. El contenido de marketing solo cambia al volver a construir; para una página que cambia al mes, no al día, es un coste aceptable con deploy automático desde main.

Detalles que sí importan en una web de un dominio nuevo: canonical en www, OG image en PNG (los scrapers no leen SVG fiable), robots y sitemap estáticos, favicon generado desde el icono de la app para que las dos superficies compartan identidad.

WHY
¿Por qué tres copias de la conversión y ningún paquete compartido?
Porque son cinco líneas puras. Un paquete entre Dart y TypeScript exigiría un pipeline solo para mantenerlas sincronizadas, y el launcher de Windows copia el core de la app a propósito para no acoplarse a lo que está en producción. La duplicación es deuda consciente y barata.
Profundizar

Lo que sí está alineado a mano y se declara en el código: el catálogo de monedas, el modelo Rate y la definición de los nueve chips, con un comentario en cada lado que apunta al otro fichero. El día que haya un tercer consumidor en Dart, el paquete deja de ser overkill; está en la retrospectiva.

WHY
¿Por qué el APK se descarga del propio dominio?
La web está viva y la gente ya entra; la app no depende de que una tienda la apruebe. El APK es un fichero estático en /downloads del mismo hosting: sin servidor extra, sin pipeline aparte. La confianza viene del dominio que el usuario ya está visitando, no de un repositorio de terceros.
Profundizar

El instalador de Windows sigue el mismo camino: Inno Setup, sin privilegios de administrador, instala en el perfil del usuario y no viene firmado, así que SmartScreen avisa la primera vez. Firmarlo cuesta un certificado anual, y hoy no lo justifica.

Coste para el usuario: activar "instalar desde fuentes desconocidas" una vez, y sin actualización automática: para la siguiente versión hay que volver a la web.
06 · Qué haría distinto

Cinco decisiones que hoy tomaría al revés

Hice
Un histórico de 7 días en el modelo con siete copias del promedio
Hoy haría
No exponer un dato hasta tenerlo. Ninguna fuente publica histórico; un gráfico plano no informa y crea una expectativa que el producto no cumple.
Hice
La versión visible en la web escrita a mano ("Android · v1.0")
Hoy haría
Leerla del versionName en build. El desfase entre lo que dice la web y lo que se instala es trivial de causar y silencioso de detectar.
Hice
convert() y el modelo Rate sin tests unitarios
Hoy haría
Escribirlos desde el primer commit: es código puro, el caso ideal. El evaluador de expresiones del launcher de Windows sí nació con sus tests; la conversión debió nacer igual.
Hice
No guardar el último snapshot de tasas
Hoy haría
Persistir la última lectura con su hora y mostrarla marcada como vieja cuando no hay red. Hoy una primera apertura sin conexión es una app sin tasas.
Hice
Copiar el core de la app al launcher de Windows
Hoy haría
Al aparecer el segundo consumidor en Dart, extraer un paquete local. Copiar fue correcto para no tocar producción; dejarlo copiado ya no lo es.
07 · Si te llevas tres cosas

Recap

1
Una cifra, seis monedas: la calculadora produce un snapshot, no un resultado, porque en una caja venezolana nadie necesita una sola conversión.
2
Cero servidores y $0/mes: el cliente lee cuatro fuentes públicas en paralelo, cada una puede fallar sola, y todo lo que se guarda se queda en el teléfono.
3
La tasa de hoy no es la última publicada: el BCV publica con fecha valor, y en un lunes bancario leer la fila más reciente cobra un 1,17 % de más.
Enlaces