Hacki
All BUIDLs

About the project

Qué es

Cada comensal escanea el QR de su mesa, arma su pedido desde el teléfono, sigue el estado en tiempo real y paga lo suyo o toda la mesa. La cocina ve las comandas con un cronómetro. El encargado tiene una caja que le contesta preguntas en castellano.

Dos piezas de Tether hacen el trabajo pesado, y ninguna es un wrapper.

QVAC: la parte difícil no es llamar al modelo

Un Qwen3-4B (del registro propio de QVAC) corre en la CPU del local, cargado in-process con @qvac/sdk. No hay servidor aparte ni API key.

Lo que hace que un 4B sea confiable acá es la gramática. El JSON Schema se convierte a GBNF y restringe el muestreo. El enum del campo id no es la carta entera: es lo que hay con stock y no le rompe la restricción alimentaria a quien pregunta.

Para una comensal celíaca, los ids con gluten no están en la gramática. No es que le pidamos al modelo que no los diga: no tiene el token para escribirlos.

sin restricciones → Burrata · Papas bravas sin-gluten → Risotto · Pesca · Limonada ← la burger no aparece vegano → Limonada de menta

Y el mismo modelo, con el mismo prompt, sin el esquema:

sin esquema → "Lo siento, pero no puedo cumplir con la solicitud..." 0 sugerencias con esquema → {"sugerencias":[{"id":"burger","motivo":"..."}]}

El agente de caja: el modelo local operando la billetera

El encargado escribe en castellano y el modelo elige qué comando de WDK CLI usar, lo usa, y contesta con lo que volvió.

> ¿cuánto llevamos cobrado hoy?

  1. [ver_caja] → cobrado hoy $19.690 · ingresos $17.900 · propinas $1.790
  2. [responder] → "Hoy se ha cobrado un total de $19.690, incluyendo

$17.900 en ingresos y $1.790 en propinas."

El modelo de seguridad tiene tres capas y ninguna confía en la de arriba:

  1. La gramática. accion es un enum de seis valores y destinatario es un

enum de la allowlist. Una dirección arbitraria no es alcanzable: no hay camino en la gramática que la produzca. Una inyección en el campo de texto tampoco, por el mismo motivo.

  1. Las políticas, en código. Tope por operación, tope diario y allowlist se

evalúan en TypeScript antes de tocar el CLI. El rechazo vuelve al modelo como resultado de herramienta, así que tiene que explicárselo al encargado.

  1. El humano. wdk send no está entre las acciones del agente. Llega hasta

el dry-run y ahí termina. Hay un test que falla si alguien agrega una acción que transmita.

WDK CLI: el binario, no una librería

El checkout hace spawn de wdk y parsea su --json. El flujo es wdk send --dry-run → confirmación humana → wdk send, con la vista previa expirable, de un solo uso y atada al monto. Un send exitoso devuelve recibo aunque después falle la lectura de balances, para que la app nunca invite a pagar dos veces.

Evidence, not vibes

Dos arneses corren la misma tarea N veces y publican la tabla.

Asistente de la carta — 60 llamadas:

caso id en carta restricción sin repetir* mediana consulta libre 100% 100% 100% 16,5 s celíaca 100% 100% 100% 11,5 s vegana (1 sola opción) 100% 100% 53% 7,8 s algo fuera de la carta 100% 100% 100% 12,4 s TOTAL 100% 100% 88% 12,4 s

Las dos columnas que importan dan 100% en 60 corridas. El 53% es el hallazgo honesto: cuando queda una sola opción elegible, el modelo llena los tres lugares repitiéndola. Un enum no expresa unicidad y GBNF tampoco, así que la gramática no puede evitarlo — lo ataja el validador.

* La columna se mide sobre lo que dijo el modelo, antes del validador. La primera versión la medía después y daba 100% siempre mientras el contador de descartes marcaba 7. Un número que no puede fallar no es evidencia.

Agente de caja — 50 consultas: 100% en las cuatro métricas. Pero la primera corrida dio 82/98/94/86, con el caso del cobro en 10%. Lo que lo movió:

  • Un campo opcional en la gramática. wallet no estaba en required, así

que el modelo emitía {"accion":"ver_saldo"} sin decir de cuál billetera — con decodificación restringida siempre toma el camino más corto que la gramática permite. La herramienta nunca se llamaba.

  • Un bucle. Llamaba a ver_saldo cuatro veces y terminaba describiendo el

cobro en vez de hacerlo. Se destrabó sacándole la herramienta del enum.

  • Una afirmación falsa. Con el dry-run recién hecho contestaba "cobro

realizado con éxito". Ahora la aclaración la agrega el código y el modelo no puede contradecirla: la última palabra sobre si la plata se movió no la tiene el modelo.

Modelo y hardware

Modelo: Qwen3-4B-Instruct · Q4_K_M · 2,50 GB Motor: llamacpp-completion · ctx 4096 · CPU Máquina: Windows 11 · AMD Ryzen AI 9 365 · 10 núcleos / 31 GB RAM Carga: 11,5 s con el modelo ya bajado Latencia: carta — mediana 12,4 s · p95 16,7 s agente — mediana 27,8 s (2,9 pasos promedio)

Lo que NO está verificado

  • El broadcast on-chain. Las wallets de Sepolia están creadas y desbloquean

bien, pero están en 0 USD₮: falta que alguien mande USD₮ de prueba. El recorrido completo se demuestra con WDK_CLI_MODE=simulado, que se declara en la terminal, en GET /api/config y con un cartel en el checkout.

  • El motivo puede ser vacío de contenido aunque sea gramaticalmente

perfecto. Ninguna gramática puede exigir que una oración sea útil.

  • Con una sola opción elegible el modelo repite el 47% de las veces. Lo

ataja el validador; el modelo lo sigue haciendo.

Correr desde un clon limpio

cd mesa npm install --allow-scripts=@tetherto/wdk-cli npm run build npm start

Node ≥ 22.18.0. La primera corrida baja el modelo (2,5 GB) a ~/.qvac/models.

http://localhost:3000/mesa/12 el comensal http://localhost:3000/cocina cocina, caja y el agente

Paquetes: @qvac/sdk@0.17.1 · @tetherto/wdk-cli@1.0.0-beta.2 · @tetherto/wdk@1.0.0-beta.16 · @tetherto/wdk-wallet-evm@1.0.0-beta.17. Red: Sepolia. Token USD₮ de prueba: 0xd077A400968890Eacc75cdc901F0356c943e4fDb.

79 tests con node:test.