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?
- [ver_caja] → cobrado hoy $19.690 · ingresos $17.900 · propinas $1.790
- [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:
- La gramática.
acciones un enum de seis valores ydestinatarioes 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.
- 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.
- El humano.
wdk sendno 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.
walletno estaba enrequired, 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_saldocuatro 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.
Hacki