Satsplit es una plataforma de bounties colaborativos construida 100% sobre Nostr + Lightning, sin base de datos centralizada. Toda la data vive encriptada en relays mediante NIP-44.
- Framework: React 19 + TypeScript + Vite
- Nostr: nostr-tools (NIP-07, NIP-46 login)
- Responsabilidades:
- UI/UX para bounties (lista, crear, fondear, resolver)
- Autenticación Nostr (window.nostr, nsecBunker)
- Comunicación con backend API
- Runtime: Node.js 22+ con Express + TypeScript
- Nostr: nostr-tools (SimplePool, NIP-44 encryption)
- Responsabilidades:
- Encriptar/desencriptar data con NIP-44
- Publicar eventos en relays
- Gestionar NWC (Nostr Wallet Connect) para pagos
- API REST para frontend
Todos los eventos se publican con el pubkey del backend (derivado de BACKEND_NSEC). El contenido (content) está encriptado con NIP-44.
| Kind | Tipo | Descripción | Tags |
|---|---|---|---|
| 30078 | Bounty | Bounty individual (parametrized replaceable) | d, title, status, creator |
| 30079 | Contribution | Registro de fondeo (zap receipt) | d, bounty_id, amount, npub |
| 30080 | Distribution | Distribución de premio (splits ejecutados) | d, bounty_id, splits |
| 30081 | Config | Configuración global (admin + NWC) | d = config |
Tags públicos (no encriptados):
{
"kind": 30078,
"pubkey": "<backend_pubkey>",
"tags": [
["d", "<bounty_id>"], // unique ID
["title", "Fix bug #123"], // título público
["status", "open"], // open | funded | in-progress | completed | cancelled
["creator", "<npub>"] // creador del bounty
],
"content": "<encrypted_json>",
"created_at": 1234567890
}Content encriptado (NIP-44):
{
id: string,
title: string,
description: string,
creator: string, // npub
target: number | null, // sats (opcional)
status: BountyStatus,
contributors: [
{ npub: string, amount: number, timestamp: number }
],
submissions: [
{ npub: string, pr?: string, description: string, timestamp: number }
],
distributions: [
{ npub: string, percentage: number, amount: number }
],
total_funded: number,
created_at: number,
completed_at?: number
}Tags públicos:
{
"kind": 30081,
"pubkey": "<backend_pubkey>",
"tags": [
["d", "config"]
],
"content": "<encrypted_json>",
"created_at": 1234567890
}Content encriptado:
{
admin_npub: string, // npub del admin (único con permisos)
nwc_uri: string, // nostr+walletconnect://... (tesoro)
treasury_balance?: number,
relays: string[],
created_at: number
}El backend tiene su propio nsec (variable BACKEND_NSEC). Esta clave se usa para:
- Firmar todos los eventos publicados
- Encriptar data sensible (bounties, config)
- Desencriptar data al leer de relays
// Encriptar
const conversationKey = nip44.v2.utils.getConversationKey(
backendPrivkey,
recipientPubkey // mismo backend pubkey para self-encryption
);
const ciphertext = nip44.v2.encrypt(JSON.stringify(data), conversationKey);
// Desencriptar
const plaintext = nip44.v2.decrypt(ciphertext, conversationKey);
const data = JSON.parse(plaintext);Self-encryption: El backend encripta con su propia pubkey como "recipient", permitiendo que solo el backend pueda leer la data.
Satsplit usa un solo NWC URI configurado por el admin. Este wallet actúa como "tesoro" del sistema:
- Recibe todos los fondos de contribuciones
- Paga todas las distribuciones cuando un bounty se completa
nostr+walletconnect://<pubkey>?relay=<relay>&secret=<secret>
Ejemplo:
nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861bb4168e5142acc?relay=wss://relay.getalby.com/v1&secret=secret123
El backend implementa las siguientes operaciones (NIP-47):
| Método | Descripción | Uso en Satsplit |
|---|---|---|
make_invoice |
Crear invoice para recibir pago | Fondear bounty |
pay_invoice |
Pagar un invoice existente | Distribuir splits (uno por colaborador) |
get_balance |
Consultar balance del wallet | Admin dashboard |
multi_payment |
Pagar múltiples invoices (batch) | Distribución eficiente de splits |
1. Fondear Bounty:
Usuario → Frontend: "Fondear 10,000 sats"
Frontend → Backend: POST /bounties/:id/fund { amount: 10000 }
Backend → NWC: make_invoice(10000, "Bounty: Fix bug #123")
NWC → Backend: invoice (lnbc...)
Backend → Frontend: { invoice: "lnbc..." }
Frontend → Usuario: Mostrar QR / pagar con wallet
Usuario → Lightning: Paga invoice
Fondos → Tesoro NWC
2. Distribuir Premio:
Creador → Frontend: Define splits (60% A, 40% B)
Frontend → Backend: POST /bounties/:id/complete { splits: [...] }
Backend: Calcula montos (6k sats, 4k sats)
Backend → NWC: multi_payment([
{ invoice: "lnbc6000...", amount: 6000 }, // A's invoice
{ invoice: "lnbc4000...", amount: 4000 } // B's invoice
])
NWC → Lightning: Paga ambos invoices
Backend: Actualiza bounty.status = "completed"
Backend → Nostr: Publica evento kind:30080 (distribution)
sequenceDiagram
Frontend->>Backend: GET /setup/status
Backend->>Nostr: Buscar kind:30081 (config)
Nostr-->>Backend: No encontrado
Backend-->>Frontend: { setup_complete: false }
Frontend->>Usuario: "Primera vez: conectar con Nostr"
Usuario->>Frontend: Login con NIP-07 (npub1abc...)
Frontend->>Backend: POST /setup/init { admin_npub: "npub1abc..." }
Backend->>Nostr: Publica kind:30081 (config con admin)
Backend-->>Frontend: { success: true }
Frontend->>Usuario: "Ingresar NWC URI del tesoro"
Usuario->>Frontend: Pega NWC URI
Frontend->>Backend: POST /setup/nwc { nwc_uri: "nostr+walletconnect://..." }
Backend->>NWC: Verificar conexión (get_balance)
NWC-->>Backend: Balance OK
Backend->>Nostr: Actualiza kind:30081 (config con NWC)
Backend-->>Frontend: { success: true }
Frontend->>Usuario: "Setup completo! 🎉"
sequenceDiagram
Usuario->>Frontend: Crear bounty
Frontend->>Backend: POST /bounties { title, description, target, creator_npub }
Backend->>Backend: Genera bounty_id único
Backend->>Backend: Encripta bounty (NIP-44)
Backend->>Nostr: Publica kind:30078
Backend-->>Frontend: { bounty }
Frontend-->>Usuario: "Bounty creado!"
Funder->>Frontend: Fondear 10k sats
Frontend->>Backend: POST /bounties/:id/fund { amount: 10000 }
Backend->>Nostr: Lee config (NWC URI)
Backend->>NWC: make_invoice(10000, "Bounty XYZ")
NWC-->>Backend: invoice
Backend-->>Frontend: { invoice }
Frontend-->>Funder: Mostrar QR
Funder->>Lightning: Paga invoice
Lightning->>NWC: Fondos recibidos
Backend->>Backend: Actualiza bounty.total_funded += 10000
Backend->>Nostr: Publica kind:30078 actualizado
sequenceDiagram
Creador->>Frontend: Completar bounty con splits
Frontend->>Backend: POST /bounties/:id/complete { splits: [...] }
Backend->>Backend: Valida splits suman 100%
Backend->>Backend: Calcula montos
Backend->>Nostr: Lee NWC URI de config
Backend->>NWC: multi_payment([...invoices])
NWC->>Lightning: Paga splits
Lightning-->>Colaboradores: Reciben sats
Backend->>Backend: bounty.status = "completed"
Backend->>Nostr: Publica kind:30078 (bounty completado)
Backend->>Nostr: Publica kind:30080 (distribution record)
Backend-->>Frontend: { success: true, payments }
Frontend-->>Creador: "Pagos enviados! ⚡"
- ✅
BACKEND_NSECen variable de entorno (.env) - ✅ Nunca exponerlo via API
- ✅ Solo backend puede encriptar/desencriptar
⚠️ Si se compromete: toda la data histórica es legible
// Middleware requireAdmin
const config = await fetchConfig();
if (config.admin_npub !== req.body.admin_npub) {
return res.status(403).json({ error: 'Unauthorized' });
}- Solo el
admin_npubpuede:- Ver balance del tesoro
- Modificar config
- Acceder a
/admin/*endpoints
- ✅ Encriptado con NIP-44 en config event
- ✅ Solo backend puede desencriptar
- ✅ Nunca retornado via API (ni siquiera a admin)
⚠️ Admin debe guardar backup del URI externamente
- Data encriptada → relays solo ven ciphertext
- Relays pueden retener eventos pero no leerlos
- Relays pueden censurar (solución: usar múltiples)
- Relays pueden perder data (solución: backup manual)
GET /setup/status
Response: { setup_complete: boolean, admin_npub?: string, has_nwc?: boolean }
POST /setup/init
Body: { admin_npub: string }
Response: { success: true, admin_npub: string }
POST /setup/nwc
Body: { nwc_uri: string }
Response: { success: true }GET /bounties
Response: { bounties: Bounty[] }
GET /bounties/:id
Response: { bounty: Bounty }
POST /bounties
Body: { title, description, target?, creator_npub }
Response: { success: true, bounty: Bounty }
POST /bounties/:id/fund
Body: { amount: number }
Response: { success: true, invoice: string, amount: number }
POST /bounties/:id/submit
Body: { npub, description, pr? }
Response: { success: true, bounty: Bounty }
POST /bounties/:id/complete
Body: { splits: { npub, percentage }[] }
Response: { success: true, bounty: Bounty, payments: PaymentResponse[] }GET /admin/balance
Body: { admin_npub } // auth
Response: { balance: number, unit: "sats" }
GET /admin/config
Body: { admin_npub } // auth
Response: { admin_npub, has_nwc, relays, created_at }- NWC mock: Pagos simulados (falta implementar NIP-47 completo)
- Invoice generation: Usuarios deben generar invoices manualmente
- Zap receipts: No se escuchan automáticamente (falta listener)
- Balance tracking: No se actualiza en tiempo real
- Frontend incompleto: Solo estructura base del boilerplate
- Implementar NWC real (NIP-47 completo)
- Listener de zap receipts (NIP-57)
- Frontend: lista de bounties
- Frontend: crear bounty form
- Frontend: fondear con invoice
- Sistema de submissions
- Distribución funcional
- Dashboard admin
- Notificaciones
- Milestone-based bounties
- GitHub integration
- Reputation system
- Mobile app (React Native)
- NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md
- NIP-44: https://github.com/nostr-protocol/nips/blob/master/44.md (Encryption)
- NIP-47: https://github.com/nostr-protocol/nips/blob/master/47.md (NWC)
- NIP-57: https://github.com/nostr-protocol/nips/blob/master/57.md (Zaps)
- nostr-tools: https://github.com/nbd-wtf/nostr-tools
Bitcoin or Death. 💀
Powered by La Crypta