Las Aventuras de 🐺villawolf y 🤖chigu — Episodio 3: La Primera Factura ⚡Las Aventuras de 🐺villawolf y 🤖chigu — Episodio 3: La Primera Factura ⚡
Ep 2: El Vault Conecta terminó con chigu compartiendo el vault con Moto, mi laptop, y la VM relay del homelab, y con un modelo de tres capas para que nadie le borrara archivos al otro. Hoy entra a Bitcoin: chigu necesita una wallet. Mi plan estaba claro. Cinco horas después, el plan era otro. Pero la arquitectura aguantó.
El plan que tenía a las 10 de la mañanaEl plan que tenía a las 10 de la mañana
ADR 2, la decisión de wallet que cerré en F0, seis días antes, decía:
- Usar Blink (continuidad con
chigu@blink.sv, identidad pública ya activa) - Exponer la wallet detrás del estándar NWC (Nostr Wallet Connect)
- Emitir una connection string con scope
[MAKE_INVOICE, LOOKUP_INVOICE]. SinPAY_INVOICE - Hablarle al wallet desde Python con
nostr-sdk
Era una decisión madurada. Investigada. Auditada. Validada en papel.
A las 10 de la mañana abrí el dashboard de Blink, busqué la opción de crear una NWC connection.
No existía.
Sorpresa: Blink no expone NWC en producción todavíaSorpresa: Blink no expone NWC en producción todavía
Mandé la mutation GraphQL directamente, por si el dashboard estaba desactualizado:
curl -s https://api.blink.sv/graphql \
-H "X-API-KEY: $BLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"mutation { nwcConnectionCreate(input: ...) { ... } }"}'
Respuesta:
{
"errors": [{
"message": "parsing error: cannot query field 'nwcConnectionCreate' on type 'Mutation'",
"extensions": {"code": "PARSING_ERROR"}
}]
}
Probé en el endpoint de staging (api.staging.blink.sv/graphql). Mismo error.
Busqué en el GitHub oficial de Blink. Hay un repo llamado blink-nwc , un subgraph en desarrollo, no federado todavía en el supergraph público. El feature existe en código, no en producción.
Quince minutos después de empezar, sabía que NWC estaba bloqueado en F1. Mi plan limpio acababa de morir.
La reacción: tres caminosLa reacción: tres caminos
Pausé. Abrí la nota .md donde llevo las decisiones del proyecto. Listé las opciones.
A. Adelantar Alby Hub a F1.
Self-hosted, NWC nativo desde el día 1. Pero implica abrir un canal Lightning con un LSP, fee inicial de varios miles de sats. Y rompe la identidad pública chigu@blink.sv que ya estaba publicada en el sitio, perfil Nostr (pausado hasta la autonomía). Caro y disruptivo para un agente que todavía no movía sats.
B. Pivote a la API de Blink directamente.
Saltar NWC en F1. Hablar GraphQL contra api.blink.sv con una API key con scope mínimo. NWC se mueve a F2+ cuando: (a) Blink lo deploye en producción, o (b) salte a Alby Hub. La identidad pública intacta. Costo cero.
C. Adoptar blink-mcp, el servidor MCP oficial de Blink.
Lo más rápido, el equipo de Blink ya escribió todo. Pero ata la wallet al SDK de Anthropic, contradice la tesis pública (NWC como estándar de la capa wallet), y el trabajo de portabilidad lo pago igual el día que quiera migrar.
Elegí B.
Razón estructural: la capa de abstracción que diseñé en F0 (el contrato Tool interno) aísla este cambio. El agente sigue viendo dos capacidades — make_invoice y lookup_invoice. Solo cambia la implementación detrás. Cero impacto en la lógica de negocio. Una enmienda menor a ADR 2, no una reescritura.
Por primera vez en este proyecto, la arquitectura modular paga dividendos visibles. No fue gratis diseñarla, me costó dos sesiones de F0. Pero aquí, contra un cambio externo que no planifiqué, absorbe el golpe sin que el resto del sistema se entere.
Buenas prácticas de la API key, el corazón del episodioBuenas prácticas de la API key, el corazón del episodio
Aquí vienen los detalles operacionales que importan más que el código. Si alguien copia este artículo para armarse su propio agente con wallet, esto es lo que tiene que entender.
Scopes mínimos por defectoScopes mínimos por defecto
La API de Blink expone tres scopes al emitir una key: Read, Receive, Write.
- Read: ver balances, transacciones, info de cuenta
- Receive: crear invoices, direcciones para recibir pagos
- Write — enviar pagos, modificar settings de la cuenta
Para chigu en F1 marqué Read + Receive. Write desmarcada.
¿Por qué importa? Sin Write, el endpoint lnInvoicePaymentSend (pagar invoices) retorna error de autorización antes de evaluar el payload. Si chigu fuera jailbreakeado mañana y alguien construyera un BOLT11 perfecto para vaciarle el wallet, Blink lo rechaza a nivel API, no a nivel prompt. La defensa vive en el protocolo.
Es exactamente la propiedad que prometía NWC con scope [MAKE_INVOICE, LOOKUP_INVOICE], ahora ejercida por el scope system nativo de Blink.
Verificar antes de configurarVerificar antes de configurar
Antes de pegar la API key recién emitida en el .env del servidor, dos tests contra Blink desde mi laptop:
TEST 1: Read scope debe funcionar:
curl -s https://api.blink.sv/graphql \
-H "X-API-KEY: $CHIGU_API_KEY" \
-d '{"query":"query { me { username } }"}'
Esperado: retorna username: chigu. ✓
TEST 2: Write scope debe rechazar:
curl -s https://api.blink.sv/graphql \
-H "X-API-KEY: $CHIGU_API_KEY" \
-d '{"query":"mutation { lnInvoicePaymentSend(input: {paymentRequest: \"lnbc1pjhfake\", ...}) { errors { message } } }"}'
Esperado: error de autorización antes de evaluar el paymentRequest. La respuesta textual que recibí:
"Unexpected error occurred ... (code: AuthorizationError: not authorized to execute mutations)"
Si TEST 2 me hubiera retornado un error tipo "invoice inválido" o "payment request mal formateado", significaría que pasó el check de scope y por lo tanto Write estaría activa. La key estaría generada mal, y la habría revocado antes de seguir.
El check tiene que ser sobre el comportamiento, no sobre los checkboxes del dashboard.
La API key nunca pasa por chat ni por dev-logLa API key nunca pasa por chat ni por dev-log
La API key vive aparte. Punto.
Para cargarla en la sesión del laptop, uso un comando que la lee sin imprimirla:
read -rs CHIGU_API_KEY && export CHIGU_API_KEY
La terminal queda esperando. Pego la key. Enter. No aparece en pantalla, no queda en el historial de bash.
Para verificar que cargó bien:
echo "len=${#CHIGU_API_KEY} prefix=${CHIGU_API_KEY:0:6}"
Solo imprime el largo y los primeros seis caracteres. Suficiente para validar que pegué algo del tamaño correcto que empieza con blink_.
Para transferirla al .env del servidor uso SSH con heredoc:
ssh root@chigu 'cat >> /opt/chigu/.env' <<EOF
CHIGU_BLINK_API_KEY=$CHIGU_API_KEY
EOF
La key viaja encriptada por SSH desde la variable de mi sesión hasta el archivo en el servidor. Nunca aparece en pantalla, nunca en logs, nunca en historial.
Permisos del archivo destino: chmod 600.
Atomic swap y rotación calendarizadaAtomic swap y rotación calendarizada
Cuando la rote, cada 90 días, marcando en mi control:
- Backup del
.envvivo:cp .env .env.bak.<timestamp>(mi camino de rollback) - Borrar la línea vieja:
sed -i "/^CHIGU_BLINK_API_KEY=/d" .env - Append la nueva via SSH-heredoc (igual que antes)
- Smoke test con la nueva
- Solo entonces revoco la vieja en el dashboard
Nunca al revés. Si revoco la vieja antes y la nueva falla en runtime, dejo a chigu sin wallet hasta que arregle el problema. El backup .env.bak.* me garantiza poder volver a la vieja en segundos si algo se rompe.
El primer invoiceEl primer invoice
Con la key configurada y los scopes validados, escribí el adapter: una clase Python (BlinkInvoiceTool) que implementa el contrato Tool interno de chigu. Dos capacidades adentro — make_invoice y lookup_invoice. Cliente HTTP en httpx. Total: ~170 líneas.
Corrí el smoke test desde mi laptop, vía SSH al servidor:
chigu paso 5 — smoke test BlinkInvoiceTool
endpoint: https://api.blink.sv/graphql
api_key: len=70 prefix=blink_
>>> make_invoice(amount_sats=10, memo='chigu paso5 smoke 2026-05-16T21:21:06')
ok=True
payment_hash: c33f0793178ac1c741fdadfa0bd84aaa3d30f68bc1a4cae77a330fcd7b726be4
satoshis: 10
>>> lookup_invoice(payment_request=lnbc100n1p4q3h7rpp5cvls0ych3tquw...)
ok=True
status: PENDING
SMOKE TEST OK — paso 5 funcional
El invoice está vivo. 10 sats. Estado pendiente. Esperando que alguien lo pague.
El BOLT11 completo, por si quieres probarlo (al momento de leer esto puede estar expirado, pero el hecho de que esté aquí es evidencia de que chigu puede emitir):
lnbc100n1p4q3h7rpp5cvls0ych3tquws0a4haqhkz24g7npa5tcxjv4em6xv8u67mjd0jq
dpuvd5xjem4ypcxzum0x5s8xmt0ddjjqv3sxgmz6vp495cnv4pjxyaryvf6xqmqcqzpuxq
rwzqsp50fuc6vcf4mkzg77q08xdtaxqarkw9rns72y29cw6a0t3n5kx2q6s9qxpqysgqx3
wucstt6lmqqlwu3uyc0ya72x0j3gy7axa75p4ev377scy82329gz6tn8yka0kyp72dglh5
twfl0jdt4a3y8syqjmxa9x68gs0jassqusw9zf
Si te aparece como inválido al copiarlo a tu wallet, es porque expiró, los invoices Lightning tienen vida finita. El próximo episodio mostrará uno fresco emitido por chigu en runtime real, pagado externamente, y el callback que confirma que llegó.
La lecciónLa lección
Si la wallet hubiera estado hardcoded en el agente, el cambio NWC → Blink GraphQL habría sido reescribir el agente entero. Con la capa de abstracción Tool en el medio, fue cambiar un adapter y nada más.
La inversión de F0 en abstracciones, que en su momento parecía over-engineering para un agente que todavía no movía sats, acaba de pagar su primer dividendo concreto. ADR 2 enmendada, no rota. Tres líneas del roadmap intactas: Blink en F1, Alby Hub self-hosted en F2, LND propio en F4 cuando llegue el disco SATA. Una sola cosa cambió: el contrato interno que F1 expone hoy.
Eso es lo que valida la disciplina de la capa de abstracción. No la teoría, el momento en que algo externo no planificado golpea y la arquitectura absorbe el golpe sin propagarlo al resto del sistema.
El siguiente pasoEl siguiente paso
El Episodio 4: pago 10 sats al invoice que chigu acaba de emitir. chigu detecta el callback. Por primera vez la wallet del agente tiene fondos por una acción del agente. Es el hito que cierra F1, el "Hola wallet" verificable.
chigu@blink.sv ·_@chigu.techflows.work· Nostr:npub18rl9xeaxw0leee0easqu9cngrq0ny4zsmdsx4jhj5fkfmstgklhq2q5mqz
Si esto te es útil, un zap es la mejor señal.
Serie: Las Aventuras de villawolf y chigu
Episodio 3 de N · anterior → Ep 2: El Vault Conecta · siguiente → Ep 4: El Primer Sat
The part where the abstraction layer saved the whole setup was satisfying to read ngl ⚡