Otros clientes: Nostr de terceros y móvil

Por: Artiko
buzznostrnip-29nip-42nakfluttermovilclientes

Otros clientes: Nostr de terceros y móvil

Buzz no es una aplicación con un backend propietario detrás. Es un relay Nostr que habla NIP-29 (grupos basados en relay) de forma nativa y usa NIP-42 para autenticar. Eso tiene una consecuencia concreta y muy práctica: cualquier cliente Nostr que implemente NIP-29 y NIP-42 puede conectarse directamente a tu relay Buzz, leer canales, escribir mensajes, reaccionar y buscar, sin pasar por la app de escritorio.

Este capítulo cubre exactamente eso: el procedimiento real documentado en el NOSTR.md del repositorio, qué funciona y qué no, qué se pierde frente a la app oficial, y en qué estado están de verdad los clientes móviles.

Un aviso previo que evita confusiones: el viejo proxy de compatibilidad NIP-28 fue eliminado. Ya no existe una capa de traducción para clientes de chat NIP-28 clásicos. La única puerta es NIP-29 directo contra el relay.


1. El scope de comunidad cuando te conectas

Antes de conectar nada, hay que entender qué “ves” al abrir el WebSocket.

Buzz trata la URL/dominio del relay como autoritativa para la comunidad. En el despliegue de un solo relay que se distribuye hoy, detrás de esa URL hay exactamente una comunidad, así que los clientes NIP-29 existentes siguen usando la misma URL de WebSocket, los mismos kinds, los mismos tags y las mismas rutas HTTP/media/git. En un despliegue multi-comunidad, cada comunidad se alcanza por su propio dominio o subdominio, y el backend resuelve la comunidad a partir del host antes de manejar tráfico AUTH, EVENT, REQ, REST, media, git, búsqueda o workflows.

El formato de cable de Nostr no gana un tag de tenant. Los tags #h que envía el cliente siguen nombrando canales/grupos y se contrastan contra la comunidad derivada del host. Los eventos sin #h —perfiles, DMs envueltos en gift wrap, notificaciones de membresía, listas, estado, notas long-form, eventos de workflow y de sistema y otros flujos “globales”— son globales solo dentro de la comunidad conectada.

De ahí se deduce lo que a un usuario le importa de verdad:

  • Tu keypair es tuyo en todas las comunidades.
  • Tu perfil, tus DMs y tu contenido sin canal no se heredan entre dominios de comunidad. Una pubkey puede unirse a varias comunidades y repostea su perfil en cada una.
  • Si apuntas tu cliente Nostr a otra URL, estás en otro mundo: otros canales, otros miembros, otro índice de búsqueda.

Esto es lo mismo que ya viste en Qué es Buzz y cómo se trabaja en él, pero visto desde el cable en vez de desde la interfaz.


2. Qué necesitas para conectar un cliente de terceros

Tres cosas:

  1. La URL WebSocket del relay. En desarrollo local es ws://localhost:3000. En un relay de equipo será el wss:// que te dio quien lo opera.
  2. Tu clave privada Nostr. Es la identidad; no hay tokens ni sesiones. Si necesitas repasar cómo se genera y se guarda, está en Tu identidad: claves, perfil y dispositivos.
  3. Permiso de entrada, si el relay lo exige.

Ese tercer punto es territorio de operador. El arranque rápido documentado es este:

# 1. (Opcional) Habilitar allowlist de pubkeys — debe fijarse ANTES de arrancar el relay
export BUZZ_PUBKEY_ALLOWLIST=true

# 2. Arrancar el relay (levanta servicios Docker y corre migraciones)
just relay &                         # relay en :3000

# 3. Añadir un pubkey al allowlist (si está habilitado)
#    Insertar directamente — todavía no hay comando de CLI para esto.
PGPASSWORD=buzz_dev psql -h localhost -U buzz -d buzz -c \
  "INSERT INTO pubkey_allowlist (pubkey) VALUES (decode('<64-char-hex-pubkey>', 'hex'))"

# 4. Conectar cualquier cliente NIP-29 + NIP-42 a ws://localhost:3000

Los pasos 1 a 3 los ejecuta quien administra el relay, no tú. Si tu pubkey no está autorizada, el relay responde auth-required: verification failed sin decir por qué. Pídele el alta a la persona que opera el relay; el procedimiento completo —allowlist, membresía NIP-43, roles— está en el curso de administrador.

El ciclo de conexión

sequenceDiagram
    participant C as Cliente Nostr
    participant R as buzz-relay
    C->>R: WebSocket sobre el host de la comunidad
    R->>R: Resuelve la comunidad desde el host
    R-->>C: AUTH con challenge
    C->>R: AUTH con evento kind 22242 firmado
    R->>R: Verifica firma y allowlist
    R-->>C: Conexión autenticada
    C->>R: REQ y EVENT
    R-->>C: Eventos, EOSE, OK

El relay envía el reto NIP-42 de inmediato, de forma proactiva. El cliente debe responder con el evento firmado antes de enviar eventos o suscripciones. Los clientes que implementan NIP-42 hacen esto solos.


3. Descubrir grupos

Los canales de Buzz se publican como estado de grupo NIP-29 firmado por el relay. Todos los eventos de descubrimiento llevan un tag d con el UUID del canal.

KindTagsContenido
39000d, name, closed siempre; about si la descripción no está vacía; private si aplica; hidden solo en canales DMMetadatos del grupo
39001d + tags p con etiqueta de rol owner o adminLista de admins
39002d + tags p de todos los miembrosLista de miembros
# Todos los grupos que puedes ver
nak req -k 39000 --auth --sec <privkey> ws://localhost:3000

# Miembros de un grupo concreto (filtrando por tag d)
nak req -k 39002 --tag "d=<channel-uuid>" --auth --sec <privkey> ws://localhost:3000

Dos advertencias del documento que ahorran horas de depuración:

  • Estos eventos se almacenan channel-scoped, para que se aplique el control de acceso: las listas de miembros de un canal privado solo son visibles para sus miembros. Como efecto colateral, una suscripción global en vivo del tipo {kinds:[39000]} no los recibe por fan-out. Los clientes descubren grupos con REQ históricos. El push en vivo para descubrimiento de canales abiertos es una mejora futura.
  • El tag closed siempre se emite, por convención NIP-29, porque los canales de Buzz requieren membresía explícita. Pero los canales open siguen siendo legibles y escribibles por no miembros en tiempo de ejecución. El tag refleja el modelo de membresía, no la aplicación del acceso.

4. Notificaciones de membresía

Cuando te añaden o te quitan de un canal, el relay firma una notificación:

KindSignificadoTagsAlcance
44100Miembro añadidop = pubkey objetivo, h = UUID de canalCommunity-global
44101Miembro eliminadop = pubkey objetivo, h = UUID de canalCommunity-global

Se almacenan community-global (channel_id = None dentro de la comunidad conectada) para que clientes y agentes puedan suscribirse sin conocer los UUID de canal por adelantado. Los eventos kind:44100/44101 enviados por un cliente se rechazan: solo el keypair del relay puede firmarlos.

nak req -k 44100 -k 44101 --tag "p=<your-hex-pubkey>" \
  --auth --sec <privkey> ws://localhost:3000

Restricción de suscripción. Los REQ globales que puedan casar con kinds p-gated —44100, 44101 y 1059— deben incluir un filtro #p donde todos los valores sean tu pubkey autenticada. El relay rechaza suscripciones que omitan #p o incluyan otras pubkeys, para impedir espiar cambios de membresía y DMs ajenos. El error literal es restricted: p-gated events require #p matching your pubkey.


5. Enviar, leer, reaccionar, borrar, crear

Estos son los comandos tal como aparecen en el documento, usando nak (el cliente CLI de Nostr verificado manualmente contra Buzz):

# Enviar un mensaje kind:9
nak event -k 9 -c "Hello from NIP-29!" --tag "h=<channel-uuid>" \
  --auth --sec <privkey> ws://localhost:3000

# Suscribirse a los mensajes de un canal
nak req -k 9 --tag "h=<channel-uuid>" --stream \
  --auth --sec <privkey> ws://localhost:3000

# Reaccionar a un mensaje (#h opcional pero recomendado; el canal se deriva del #e objetivo)
nak event -k 7 -c "+" --tag "h=<channel-uuid>" --tag "e=<message-event-id>" \
  --auth --sec <privkey> ws://localhost:3000

# Suscribirse a reacciones — incluir #h para entrega en vivo
nak req -k 7 --tag "h=<channel-uuid>" --stream \
  --auth --sec <privkey> ws://localhost:3000

# Borrar un mensaje (#h opcional; #e obligatorio; debe ser de autoría propia)
nak event -k 5 -c "reason" --tag "h=<channel-uuid>" --tag "e=<message-event-id>" \
  --auth --sec <privkey> ws://localhost:3000

# Crear un grupo
nak event -k 9007 --tag "name=my-channel" --tag "visibility=open" \
  --auth --sec <privkey> ws://localhost:3000

# Buscar mensajes (NIP-50)
nak req -k 9 --tag "h=<channel-uuid>" --search "search query" -l 20 \
  --auth --sec <privkey> ws://localhost:3000

# Responder a un mensaje (threading NIP-10)
nak event -k 9 -c "Reply text" --tag "h=<channel-uuid>" \
  --tag "e=<parent-event-id>;;reply" \
  --auth --sec <privkey> ws://localhost:3000

# Traer DMs con gift wrap (NIP-17)
nak req -k 1059 --tag "p=<your-hex-pubkey>" \
  --auth --sec <privkey> ws://localhost:3000

Qué kind usar para qué

AcciónKindReglas duras
Mensaje de canal9Requiere tag #h. Sin él se rechaza con invalid: channel-scoped events must include an h tag
Reacción7NIP-25 estándar. El canal se deriva del #e del objetivo; el #h del cliente se ignora para determinar canal. Reacciones a eventos desconocidos se rechazan
Borrado5NIP-09 estándar, solo auto-autoría. #e obligatorio, #h opcional
Perfil0NIP-01. Se sincroniza a la tabla de usuarios. Los handles NIP-05 deben canonicalizar al dominio de este relay; los de fuera o inválidos se limpian en silencio
Crear canal9007Tag name; opcionalmente visibility y channel_type
Añadir usuario9000En canal open, cualquiera, sujeto al channel_add_policy del objetivo. En privado, solo owner/admin
Quitar usuario9001Auto-remoción permitida con guarda de último owner. Quitar a otros: owner/admin
Editar metadatos9002Tags name/about: owner/admin. Tags topic/purpose: cualquier miembro
Borrado administrativo9005El autor siempre puede borrar lo suyo; si no, owner/admin. El objetivo debe estar en el mismo canal
Borrar el grupo9008Solo owner
Pedir unirse9021Solo canales open. Los privados se rechazan en ingest
Salir del grupo9022Cualquier miembro, con guarda de último owner
Presencia20001Efímero. Cadena de estado arbitraria truncada a 128 caracteres
”Está escribiendo”20002Efímero, no se almacena

Búsqueda NIP-50

La búsqueda funciona como REQ de una sola tanda, no como suscripción persistente:

{"search":"query","kinds":[9],"#h":["<uuid>"]}

El relay devuelve resultados ordenados por relevancia y cierra con EOSE. Estas consultas no se registran como suscripciones persistentes, así que no esperes que sigan llegando resultados nuevos. Si quieres el modelo completo de búsqueda que ofrece la app, está en Búsqueda: encontrar la conversación, el patch y la decisión.

La trampa de las reacciones

Es el error más común y está documentado explícitamente. El relay deriva el canal de una reacción a partir de su objetivo #e, así que las reacciones a eventos channel-scoped quedan también channel-scoped. El fan-out en vivo mantiene estrictamente separadas las suscripciones channel-scoped y las globales. Por lo tanto:

  • {"kinds":[7]} → no recibe ninguna de esas reacciones.
  • {"kinds":[7],"#h":["<channel-uuid>"]} → sí.

El matching por #h funciona lleve o no la reacción firmada un tag h: los tags h explícitos se casan directamente, y las reacciones sin tag casan por su canal almacenado.

Hilos NIP-10

Las respuestas enviadas por WebSocket con tags ["e","<root>","","reply"] crean thread_metadata de forma atómica y son visibles en las consultas REST de hilo. Los padres desconocidos se rechazan. El modelo de hilos desde la app está en Hilos, reacciones y actividad.

DMs NIP-17

Los DMs viajan como gift wrap kind:1059, aceptados con claves de firma efímeras, almacenados community-global y entregados por suscripciones filtradas por #p. No se indexan en búsqueda. Crear un DM emite un kind:39000 con tag hidden más notificaciones kind:44100, así que los clientes NIP-29 descubren los DMs por el flujo estándar de descubrimiento de grupos.

Lo que no está: NIP-04 y NIP-44 no están implementados, y el kind:10050 (lista de relays de DM) está diferido. Si tu cliente favorito solo sabe hacer DMs al estilo NIP-04, no verá nada. Más contexto en Mensajes directos y privacidad.


6. Límites del protocolo que tu cliente debe respetar

LímiteValor en el códigoDónde vive
Tamaño máximo de trama WebSocket524.288 bytes (512 KiB), ajustable con BUZZ_MAX_FRAME_BYTESDEFAULT_MAX_FRAME_BYTES
Suscripciones máximas por conexión1024MAX_SUBSCRIPTIONS
Resultados históricos máximos por filtro1000DEFAULT_MAX_PAGE_LIMIT
Filtros máximos por REQ10max_filters de NIP-11
Longitud máxima del id de suscripción256max_subid_length de NIP-11
Contenido máximo de un evento65.536 bytesmax_content_len de NIP-11

La forma honesta de leer esta tabla: no la memorices, pregúntasela al relay. Los tres primeros valores son los que el binario aplica hoy, y el propio relay los publica en su documento NIP-11, así que basta un curl:

curl -s -H 'Accept: application/nostr+json' http://localhost:3000 | jq .limitation

Si consultas ARCHITECTURE.md verás una tabla de constantes con 65.536 de trama y 500 de límite histórico: esa sección quedó desfasada respecto del código, y el limitation del NIP-11 es la fuente que manda. El que sí sigue siendo 65.536 es el contenido de un evento, que es el límite que de verdad te topa al escribir mensajes largos o pegar diffs.

Y un detalle de NIP-01 que rompe suscripciones ingenuas: kinds: [] (array vacío explícito) significa “no casar con nada”, no comodín. Esas suscripciones no se indexan y nunca reciben eventos. Omitir el campo kinds por completo sí significa “todos los kinds”.


7. Qué no vas a ver desde un cliente de terceros

Aquí está el corte real entre “hablar el protocolo” y “usar Buzz”.

Hay kinds que funcionan en el cable pero son solo-Buzz: ningún cliente NIP-29 estándar los renderiza.

KindQué esEfecto en un cliente de terceros
40002Contenido enriquecido de mensaje v2Llega, no se pinta
40003Edición de mensajeLlega, no se aplica: verás el original

A eso se suma todo lo que ni siquiera es NIP-29: canvases (kind 40100), definiciones de workflow (30620) y sus disparos, eventos de huddle, memoria de agente, perfiles de agente y personas. Un cliente genérico no tiene interfaz para nada de eso.

Y hay cosas que directamente no funcionan hoy:

FunciónEstadoPor qué
Crear invitación (kind:9009)ParcialSe acepta y almacena, pero el manejador de efecto está diferido: no-op con warning en el log
Roles de grupo (kind:39003)NoDefinido en el registro de kinds pero el relay no lo emite
DMs NIP-04 / NIP-44NoSolo NIP-17 gift wrap. kind:10050 diferido

Resumen honesto: un cliente Nostr de terceros te sirve para leer y escribir conversación —mensajes, hilos, reacciones, borrados, DMs gift-wrapped, búsqueda, membresía—. Para agentes, workflows, canvases, huddles, git y proyectos necesitas la app de escritorio o buzz-cli, que es justo lo que viste en buzz-cli: el workspace desde la terminal.

Clientes probados

ClientePlataformaEvidenciaAlcance verificado
BuzzTestClientRust, en el repoE2E automatizadoFlujo NIP-29 completo: descubrimiento 39000/39001/39002, envío y recepción kind:9, reacciones, borrados, cumplimiento del tag h
E2E nostr interopRust, en el repoE2E automatizadoBúsqueda NIP-50 (3 tests), hilos NIP-10 (3 tests), gift wraps NIP-17 (3 tests), descubrimiento de DM (1 test)
nakCLIManual, verificadoEnvío y recepción kind:9, búsqueda NIP-50, respuestas de hilo NIP-10, descubrimiento de grupos

No verificados en el repositorio, anecdóticos o esperados por su soporte NIP-29: Chachi (web y móvil, basado en NDK) y 0xchat (móvil). Si los usas, estás en territorio no probado.

Diagnóstico rápido

SíntomaCausaArreglo
auth-required: verification failedPubkey fuera del allowlist, si está activo, o falló NIP-42Que el operador añada tu pubkey; verifica el reto/respuesta NIP-42
invalid: channel-scoped events must include an h tagkind:9 enviado sin tag #hIncluye --tag "h=<channel-uuid>"
invalid: reaction target event not foundLa reacción referencia un evento desconocidoAsegúrate de que el evento objetivo existe en el relay
No llegan eventos de descubrimientoEl canal es privado y no eres miembroÚnete al canal primero

8. Los clientes móviles: estado real

Esto hay que decirlo sin adornos. En la tabla del README del proyecto —“Works today / Being wired up / Strong opinions, pending code”— los clientes móviles (iOS + Android, Flutter) están en la columna 🚧 Being wired up, junto con las compuertas de aprobación de workflows y los eventos de ciclo de vida de huddles. No están en la columna de lo que funciona hoy.

Lo que sí existe es un proyecto Flutter completo en mobile/, con estructura de producción y una superficie ya considerable.

Qué hay en mobile/

Es una aplicación Flutter llamada buzz, con SDK de Dart ^3.11.4, gestión de estado con Riverpod + Hooks (HookConsumerWidget), tema Catppuccin Latte en claro y Macchiato en oscuro —el mismo que el escritorio—, tokens de espaciado Grid, linting con flutter_lints + riverpod_lint, y aislamiento de features: ningún import cruzado entre features salvo a través de shared/.

La navegación principal son tres pestañas: Home, Activity y Search.

Módulos presentes bajo lib/features/:

  • channels — el bloque más grande con diferencia: página de canal, ventana de canal, hilos, barra de composición, selector de emoji, visor de medios, menciones, silenciados, favoritos, secciones y orden de canales, indicador de escritura, insignia de no leídos, acciones de mensaje, fila de reacciones, actividad de agentes, canales efímeros y etiquetas de canales DM.
  • activity — bandeja de entrada, feed, borradores de composición, recordatorios y estado de leído de la bandeja.
  • search — página de búsqueda y búsquedas recientes.
  • forum — publicaciones, tarjetas, hilos y proveedor de foro.
  • pulse — notas, tarjetas de nota, actividad de agente y composición de nota.
  • profile — avatar, perfil de usuario, presencia, estado de usuario y hoja de cambio de estado.
  • pairing — el flujo de emparejamiento con QR.
  • invites — unirse por invitación.
  • settings y home.

Bajo lib/shared/ hay piezas que muestran hasta dónde llega: relay con sesión, socket, filtros Nostr, modelos, subida y autenticación de medios, validación y límite de tasa; crypto con nip44.dart, ecdh.dart, hkdf.dart y nip_oa.dart; read_state para sincronizar la posición de lectura; más custom_emoji, emoji, mentions, reminders, deeplink, community y theme.

Una nota de honestidad sobre el propio repositorio: la sección “Architecture” del mobile/README.md todavía describe features/home/ como “Placeholder home surface”. El código real ya va muy por delante de ese README.

Cómo entras: emparejamiento NIP-AB

En móvil no tecleas tu clave privada. El flujo de alta es un emparejamiento de dispositivo NIP-AB: la app abre una pantalla de emparejamiento, escanea un código QR o acepta un código pegado, se suscribe a eventos kind:24134 etiquetados a su pubkey efímera, y valida cada evento —kind correcto, origen esperado, tag p apuntando a ti, descarte de duplicados—. Hay verificación con código SAS: si no coincide, el emparejamiento se cancela por seguridad con el mensaje SAS code mismatch — pairing cancelled for security.

flowchart TD
    A["La app móvil abre la pantalla de emparejamiento"] --> B["Escanea el QR o pega el código"]
    B --> C["Se suscribe a kind 24134 con su pubkey efímera"]
    C --> D["Recibe el evento cifrado del dispositivo origen"]
    D --> E{"¿Coincide el código SAS?"}
    E -- No --> F["Emparejamiento cancelado por seguridad"]
    E -- Sí --> G["Guarda la comunidad con su nsec"]
    G --> H["Sesión autenticada en esa comunidad"]

La app guarda una comunidad por credencial: cada comunidad almacenada lleva su propio nsec, hay una comunidad activa, y al cerrar sesión en una, si quedan otras, cambia a la siguiente en vez de devolverte a la pantalla de emparejamiento. Eso encaja exactamente con el modelo de “identidad portable, perfil por comunidad” del que hablamos al principio.

El emparejamiento entre dispositivos también aparece en Tu identidad: claves, perfil y dispositivos.

Enlaces profundos e invitaciones

La app móvil parsea enlaces buzz://:

  • buzz://message?channel=<uuid>&id=<hex> con &thread=<hex> opcional, para saltar a un mensaje concreto de un canal.
  • buzz://join?relay=<ws o wss>&code=<code>, el traspaso a la app instalada desde un enlace de invitación.

También reconoce enlaces de invitación HTTPS canónicos y los convierte al relay correspondiente. El host buzz://connect es solo de escritorio y la app móvil lo ignora.

Cómo se ejecuta y se comprueba

Los comandos del repositorio, para quien quiera compilarlo:

# Instalar dependencias
cd mobile && flutter pub get

# Desde la raiz del repo: abre el simulador de iOS, aplica la identidad de
# worktree y lanza flutter run
just mobile-dev

# Directo, con servicios y relay ya corriendo
cd mobile && flutter run

# Comprobaciones
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test

Desde la raíz también existen just mobile-install, just mobile-fmt, just mobile-fix, just mobile-check, just mobile-test, just mobile-build-android (APK de depuración sin firmar) y just mobile-clean (desinstala builds de depuración con sufijo de worktree; nunca toca instalaciones de producción). Ninguna de esas recetas levanta el relay: el README.md de mobile/ todavía dice que just mobile-dev arranca Docker y el relay, pero la receta real del Justfile solo abre el simulador y ejecuta flutter run. El relay lo levantas aparte, con just relay.

La configuración de desarrollo se pasa por un .env.json, cuyo ejemplo declara exactamente dos claves:

{
  "BUZZ_RELAY_URL": "http://localhost:3000",
  "BUZZ_DEV_PUBKEY": "<your-hex-pubkey-here>"
}

Las builds de depuración creadas desde un worktree de git reciben un identificador de aplicación único derivado del nombre del directorio del worktree (com.buzz.buzzMobile.<slug> en iOS, xyz.block.buzz.mobile.<slug> en Android) más una etiqueta de rama solo visual en el nombre. Así, un worktree conserva exactamente una app instalada —y su estado de sesión— entre cambios de rama, y builds de varios worktrees conviven. Las builds de release y profile mantienen siempre la identidad y el nombre de producción.

La firma de release de Android exige variables de entorno con la keystore de subida y es trabajo de quien publica, no del usuario: eso vive en el curso de administrador.


9. Cómo elegir cliente

flowchart TD
    A["¿Qué necesito hacer?"] --> B{"¿Conversación, hilos, reacciones, DM, búsqueda?"}
    B -- Sí --> C["Un cliente NIP-29 de terceros sirve"]
    B -- No --> D{"¿Agentes, workflows, canvases, huddles, git?"}
    D -- Sí --> E["App de escritorio o buzz-cli"]
    D -- No --> F{"¿Automatización desde scripts?"}
    F -- Sí --> G["buzz-cli con JSON de entrada y salida"]
    F -- No --> E

La app de escritorio es la superficie completa y la referencia de este curso; buzz-cli es la vía de automatización y agentes, tratada en los capítulos 10 y 11; un cliente Nostr de terceros te da conversación desde donde no tengas la app, con las limitaciones de arriba; y el móvil Flutter existe en el repositorio pero está oficialmente “cableándose”.


Resumen

  • Buzz habla NIP-29 nativo y autentica con NIP-42; los clientes de terceros se conectan directo al relay. El antiguo proxy de compatibilidad NIP-28 fue eliminado.
  • La URL es la comunidad. El backend resuelve la comunidad desde el host antes de AUTH, EVENT, REQ, REST, media, git, búsqueda o workflow. Perfiles y DMs no se heredan entre dominios.
  • Kinds de conversación: 9 mensaje (requiere #h), 7 reacción (el canal sale del #e objetivo), 5 borrado (solo auto-autoría), 9007 crear grupo, 9021/9022 unirse y salir, 0 perfil.
  • Descubrimiento por 39000/39001/39002 mediante REQ históricos: las suscripciones globales en vivo no reciben esos eventos.
  • Los kinds p-gated 44100, 44101 y 1059 exigen un filtro #p con tu propia pubkey; si no, la suscripción se rechaza.
  • NIP-50 es búsqueda de una tanda con EOSE, no suscripción; NIP-10 usa ["e","<root>","","reply"] y rechaza padres desconocidos; NIP-17 entrega DMs gift-wrapped kind:1059 fuera del índice de búsqueda.
  • Límites según el código y el limitation de NIP-11: trama de 512 KiB (BUZZ_MAX_FRAME_BYTES), 1024 suscripciones por conexión, 1000 resultados históricos por filtro, 10 filtros por REQ y 65.536 bytes de contenido por evento. kinds: [] significa “nada”, no comodín.
  • Frente a la app oficial pierdes: ediciones y contenido enriquecido (40003 y 40002 llegan pero no se renderizan), canvases, workflows, huddles, agentes y git. Las invitaciones kind:9009 se almacenan pero su efecto está diferido, y el kind:39003 no lo emite el relay.
  • Clientes verificados en el repo: BuzzTestClient, la suite E2E nostr interop y nak. Chachi y 0xchat no están verificados.
  • Los clientes móviles Flutter para iOS y Android están en la columna “being wired up” del README. En mobile/ hay una app Riverpod + Hooks con pestañas Home, Activity y Search, features de canales, hilos, foro, pulse, búsqueda, perfil, invitaciones y emparejamiento NIP-AB por QR con verificación SAS.

Siguiente: Flujos completos y buenas prácticas