ceiba.to: Especificación de Protocolo
1. Resumen Técnico
ceiba.to es un protocolo de datos comunitarios portátiles. La unidad fundamental es el ciempiés — un objeto JSON firmado con conectores tipados que puede residir en múltiples nodos simultáneamente. Este documento especifica el formato de datos, la pila de identidad, el modelo de confianza, la economía del karma y el protocolo de replicación.
1.1 Stack de Protocolo
| Capa | Tecnología | Rol | Estado |
|---|---|---|---|
| Transporte | HTTPS + REST/JSON-LD | API pública de entidades | [IMPL] |
| Semántica | Schema.org + ceiba: namespace | Vocabulario de datos | [IMPL] |
| Agentes IA | MCP (anthropic-mcp-python) | Consultas de agentes | [IMPL] |
| Micropagos | x402 (HTTP 402 + USDC) | Monetización API-native | [IMPL] |
| Identidad | WebAuthn Level 2 (Passkeys) | Autenticación sin contraseña | [SPEC] |
| Identidad Step-0 | SMS OTP (Twilio) | Bootstrap de reclamo | [SPEC] |
| Recuperación | Atestación social (Shamir-inspired) | Social recovery | [SPEC] |
| Entidades | ceiba-manifest (JSON + firma Ed25519) | Formato ciempiés portátil | [IMPL] |
| Scaffolding | koa-gen CLI | Generación de nuevas ceibas | [IMPL] |
1.2 Almacenamiento
El protocolo es agnóstico del backend. La implementación de referencia soporta tres modos:
- SQLite (local) — modo por defecto para nodos independientes y desarrollo. Un archivo
.dbpor ceiba. Soporta WAL mode para escrituras concurrentes. En producción en La Cartelera (lacartelera.app). - Supabase (hosted) — PostgreSQL gestionado con RLS por entidad. Para ceibas con múltiples operadores o alta disponibilidad requerida.
- IPFS (archival) [SPEC] — para snapshots inmutables de versiones de ciempiés. El CID se almacenaría en
replicas[].ipfs_cid. No reemplaza al nodo primario. No implementado.
1.3 Identidad — Resumen
La identidad en ceiba.to tiene tres capas progresivas:
// Layer 0: claim bootstrapped by SMS OTP
entity.owner = "did:phone:+521234567890"
// Layer 1: Passkey registration (WebAuthn Level 2)
entity.auth = { credential_id: "base64url...", rpId: "ceiba.to" }
// Layer 2: social recovery attestation (N-of-M)
recovery.attestations = [ { attester: "did:ceiba:...", sig: "..." }, ... ]
1.4 Despliegues de Referencia
| Ceiba | Dominio | Entidades | Backend | Stack extra |
|---|---|---|---|---|
| La Cartelera | lacartelera.app | 158 comediantes indexados, 183 foros | SQLite | MCP server |
| Sociales | sociales.lat | Eventos baile LATAM | Supabase | fuente iCal |
2. El Modelo Ciempiés — Especificación Completa
Un ciempiés es la unidad atómica del protocolo: un objeto JSON autocontenido y portátil que representa una entidad del mundo real. Cada campo tiene atribución de fuente. Cada relación se expresa como un conector tipado dirigido. Cada versión es monotónicamente ordenada y firmable.
2.1 Schema Completo — JSON con JSON-LD
{
// ── IDENTIFICACIÓN ──────────────────────────────────────────
"@context": [
"https://schema.org",
"https://ceiba.to/context/v1.json"
],
"ceiba:entity": "comedian/ana-torres",
// Formato: {type}/{slug} — ambos lowercase, kebab-case
// Namespaced: ceiba/comedia/comedian/ana-torres en ceiba multi-tenant
// ── CUERPO ──────────────────────────────────────────────────
"body": {
"@type": "Person", // Schema.org type
"name": "Ana Torres",
"description": "Comediante de stand-up, CDMX.",
"city": "Ciudad de México",
"birth_year": 1992,
"phone_e164": "+521234567890", // privado si entity.owner lo marca
"social_handles": {
"instagram": "@anatorres_comedy",
"tiktok": "@anatorres"
},
"genres": ["stand-up", "improv"],
"years_active": 6
},
// ── CONECTORES ──────────────────────────────────────────────
"connectors": {
"outbound": [
{
"type": "performs_at",
"target": "venue/foro-normandie",
"weight": 0.9, // frecuencia relativa 0-1
"first_seen": "2024-03-10",
"last_seen": "2026-03-28"
},
{
"type": "member_of",
"target": "ceiba/comedia-cdmx"
}
],
"inbound": [
{
"type": "has_performer",
"source": "venue/foro-normandie"
},
{
"type": "has_member",
"source": "ceiba/comedia-cdmx"
}
]
},
// ── RÉPLICAS ────────────────────────────────────────────────
"replicas": [
{
"node": "lacartelera.app",
"last_sync": "2026-04-01T14:00:00Z",
"ipfs_cid": "bafybeig..." // CID de la versión archivada
},
{
"node": "sociales.lat",
"last_sync": "2026-04-01T13:55:00Z"
}
],
// ── VERSIONADO ──────────────────────────────────────────────
"version": 47, // entero monotónico, incrementa en cada edit
"updated_at": "2026-04-01T14:00:00Z", // ISO 8601 UTC
// ── IDENTIDAD Y FIRMAS ──────────────────────────────────────
"owner": "did:ceiba:comedian/ana-torres",
"signatures": [
{
"signer_did": "did:ceiba:comedian/ana-torres",
"role": "owner", // owner | contributor | node
"signature": "base64url(Ed25519-sig-over-canonical-JSON)",
"timestamp": "2026-04-01T14:00:00Z",
"fields_covered": ["body.name", "body.genres"]
// null = toda la entidad en la versión actual
},
{
"signer_did": "did:ceiba:user/carlos-contrib",
"role": "contributor",
"signature": "base64url(...)",
"timestamp": "2026-03-20T09:30:00Z",
"fields_covered": ["body.years_active"]
}
],
// Regla de precedencia: role==owner gana sobre cualquier otra firma
// en caso de conflicto sobre el mismo campo.
// ── PROVENIENCIA POR CAMPO ──────────────────────────────────
"sources": [
{
"field": "body.name",
"source_url": "https://www.instagram.com/anatorres_comedy",
"fetched_at": "2026-01-15T10:00:00Z",
"confidence": "high"
// confidence: "high" | "medium" | "low" | "inferred"
// "inferred" = deducido por matching, no observado directamente
},
{
"field": "body.birth_year",
"source_url": null,
"fetched_at": "2026-02-01T00:00:00Z",
"confidence": "inferred"
},
{
"field": "connectors.outbound[performs_at:venue/foro-normandie]",
"source_url": "https://lacartelera.app/eventos/2026-03-28",
"fetched_at": "2026-03-28T22:00:00Z",
"confidence": "high"
}
],
// ── KARMA EN ESCROW ─────────────────────────────────────────
"karma_escrow": {
"pending": [
{
"contribution_id": "ctr_0x4f2a...",
"contributor_did": "did:ceiba:user/carlos-contrib",
"field": "body.years_active",
"status": "pending", // pending | accepted | rejected | disputed
"karma_estimate": 5,
"submitted_at": "2026-03-20T09:30:00Z"
}
]
}
}
2.2 Pares de Conectores Complementarios — Tabla Completa
Cada conector outbound tiene exactamente un complemento inbound. El intérprete ensambla la relación bidireccional cuando ambos extremos tienen conectores compatibles. Los conectores son aditivos: en caso de múltiples conexiones del mismo tipo, todas se preservan con su propio peso y timestamps.
| Outbound | Inbound (complemento) | Tipos de entidades | |
|---|---|---|---|
performs_at | ↔ | has_performer | Person → Venue/Event |
hosts_show | ↔ | hosted_at | Venue → Event |
produced_by | ↔ | produces | Event → Person/Org |
located_in | ↔ | has_location | Any → Place/City |
member_of | ↔ | has_member | Person → Organization/Ceiba |
managed_by | ↔ | manages | Venue/Ceiba → Person |
supplier_of | ↔ | supplied_by | Org → Venue/Business |
listed_on | ↔ | lists | Any → Ceiba/Directory |
attests | ↔ | attested_by | Person → Person (confianza) |
routes_via | ↔ | routes_for | Route → Adapter (dominio financiero) |
priced_in | ↔ | prices | Product → Currency |
event_in | ↔ | has_event | Event → Ceiba |
teaches | ↔ | taught_by | Person → Style/Discipline |
employs | ↔ | employed_by | Org → Person |
sibling_of | ↔ | sibling_of | Any → Any (simétrico) |
derived_from | ↔ | has_derivative | Entity → Entity (fork) |
owns | ↔ | owned_by | Person/Org → Venue/Business |
knows | ↔ | known_by | Person → Person (trust graph) |
sibling_of y knows son conectores simétricos. Cuando A declara knows → B, el intérprete crea known_by → A en B. Para knows a ser mutuo (relevante para el trust graph), B debe también declarar knows → A. La mutualidad no se infiere automáticamente.
2.3 Proveniencia por Campo — Reglas
- Todo campo en
bodydebe tener al menos una entrada ensources. Campos sin fuente se rechazan enPOST /v1/entitiescon422 Unprocessable Entity. - El campo
confidencesigue la escala: high = observado directamente en fuente autoritativa (dueño o plataforma oficial), medium = observado en fuente secundaria, low = reporte de tercero no verificado, inferred = derivado algorítmicamente. - Los conectores también pueden tener fuente, con el path:
connectors.outbound[{type}:{target}]. - Cuando el dueño de la entidad edita un campo directamente (autenticado vía Passkey), la fuente se registra como
source_url: "self"conconfidence: "high".
3. Identidad y Confianza — Especificación
La identidad en ceiba.to sigue un modelo de reclamo progresivo. Una entidad existe antes de ser reclamada. El reclamo es un acto unilateral del titular real, verificado por múltiples mecanismos que se refuerzan mutuamente.
Layer 0 — Reclamo por SMS (Bootstrap)
El Layer 0 es el punto de entrada de identidad más bajo. Convierte un número de teléfono en un DID provisional. Es irrepetible: una entidad solo puede ser reclamada una vez vía este mecanismo. La transferencia posterior requiere Layer 2.
// Iniciar reclamo
POST /v1/claim
Content-Type: application/json
{
"entity_id": "comedian/ana-torres",
"phone_e164": "+521234567890"
}
// Respuesta
HTTP/1.1 202 Accepted
{
"claim_id": "clm_0xabc123",
"expires_at": "2026-04-01T14:10:00Z", // TTL 10 minutos
"message": "OTP enviado a +52123***7890"
}
// Verificar OTP
POST /v1/claim/{claim_id}/verify
{
"otp": "847291" // 6 dígitos, generado con TOTP-like PRNG
}
// En éxito:
HTTP/1.1 200 OK
{
"entity_id": "comedian/ana-torres",
"owner_did": "did:phone:+521234567890",
"session_token": "opaque-jwt..."
}
// Efecto en la entidad:
entity.owner = "did:phone:+521234567890"
entity.claimed_at = "2026-04-01T14:02:33Z"
entity.claim_method = "sms_otp"
409 Conflict. (2) El OTP expira a los 10 minutos y es de un solo uso. (3) El número de intentos fallidos está limitado a 5 por claim_id; al superar el límite, el claim se invalida. (4) Twilio es el proveedor OTP de referencia; el protocolo no prescribe proveedor SMS.
Layer 1 — Passkey / WebAuthn Level 2
Una vez que el titular tiene un session_token de Layer 0 (o Layer 2), puede registrar un Passkey que eleva su identidad de did:phone: a did:ceiba:. WebAuthn Level 2 es el estándar W3C; la implementación de referencia usa la librería Python py_webauthn.
// Fase 1: obtener challenge de registro
POST /v1/auth/passkey/register/begin
Authorization: Bearer {session_token}
// Respuesta: PublicKeyCredentialCreationOptions
{
"challenge": "base64url(32-random-bytes)",
"rp": { "name": "ceiba.to", "id": "ceiba.to" },
"user": {
"id": "base64url(entity_id_bytes)",
"name": "comedian/ana-torres",
"displayName": "Ana Torres"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 }, // ES256
{ "type": "public-key", "alg": -8 } // EdDSA
],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "required"
}
}
// Fase 2: completar registro
POST /v1/auth/passkey/register/complete
Authorization: Bearer {session_token}
{ "credential": { ... } } // AuthenticatorAttestationResponse
// En éxito: el servidor almacena
{
"credential_id": "base64url...",
"public_key": "base64url(COSE-encoded)",
"sign_count": 0,
"entity_id": "comedian/ana-torres",
"registered_at": "..."
}
// El owner_did se promueve:
entity.owner = "did:ceiba:comedian/ana-torres"
Múltiples dispositivos son soportados: iCloud Keychain, Google Password Manager o cualquier autenticador WebAuthn sincronizable. El servidor almacena todos los credential_id asociados a una entidad. No se almacena ninguna clave privada en el servidor.
Layer 2 — Recuperación Social
La recuperación social permite a un titular que perdió acceso a sus Passkeys recuperar el control de su entidad mediante atestaciones firmadas de contactos de confianza.
// El titular anuncia pérdida de acceso
POST /v1/recovery/initiate
{
"entity_id": "comedian/ana-torres",
"phone_e164": "+521234567890", // identidad Layer 0 como bootstrap
"new_device_challenge": "base64url(32-random-bytes)"
}
// Respuesta: recovery_id + lista de trusted_contacts de la entidad
// Cada contacto de confianza emite una atestación
POST /v1/recovery/{recovery_id}/attest
Authorization: Bearer {attester_session_token}
{
"attester_entity_id": "comedian/roberto-gomez",
"target_entity_id": "comedian/ana-torres",
"timestamp": "2026-04-01T15:00:00Z",
"context": "social_recovery",
"attester_signature": "base64url(Ed25519(...new_device_challenge...))"
}
// Cuando se acumulan >= N atestaciones válidas:
POST /v1/recovery/{recovery_id}/complete
// → habilita endpoint /v1/auth/passkey/register/begin sin session_token
// → el nuevo Passkey reemplaza los credential_ids anteriores
// → los credential_ids antiguos quedan revocados
Umbral N configurable por ceiba:
- Entidades individuales en ceibas pequeñas: N = 3
- Entidades organizativas o ceibas con alta actividad económica: N = 5
- El seed de la ceiba puede elevar el umbral hasta N = 7 via configuración en
ceiba.json
Magic Link — Acceso de Sesión de Larga Duración
Para casos de uso donde un Passkey no es práctico (acceso desde dispositivo compartido, exportación a iCal, etc.), el protocolo soporta magic links de 90 días.
// Generar magic link (requiere sesión Passkey activa)
POST /v1/magic-link
Authorization: Bearer {passkey_session}
{
"label": "iPad compartido del foro",
"ttl_days": 90
}
// Respuesta
{
"token": "base64url(32-random-bytes)",
"url": "https://a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5.lacartelera.app/",
"expires_at": "2026-07-01T14:00:00Z",
"single_use": false
}
// El token es opaco (32 bytes aleatorios), no firmado.
// El subdomain {token}.{ceiba-domain} resuelve a una sesión autenticada.
// Almacenado por el usuario en: iCal, shortlink, o nota segura.
// Revocable en cualquier momento vía DELETE /v1/magic-link/{token}
4. Modelo de Confianza — Teoría de Conjuntos
4.1 Definición Formal
El modelo de confianza de ceiba.to se basa en teoría de grafos y conjuntos, no en cadenas transitivas. Las cadenas transitivas son vulnerables a ataques de intermediario: si A confía en B y B confía en C, un atacante que compromete B puede extender confianza a C arbitrariamente.
// Definiciones
G = (V, E)
V = conjunto de entidades
E = conjunto de aristas mutuas { (A, B) : A.knows(B) AND B.knows(A) }
// Un conjunto de confianza S es un conjunto donde todos los pares son co-conocidos
trust_set(S) = { A ∈ V : ∀B ∈ S, (A,B) ∈ E }
// Predicado de confianza
trust(A, C) = TRUE iff:
∃ B : (A,B) ∈ E AND (B,C) ∈ E AND (A,C) ∈ E
// A debe tener al menos UN vínculo directo con C
// o conocer a alguien que AMBOS conocen independientemente
// Condición más débil (co-vecindario):
soft_trust(A, C) = TRUE iff:
∃ B : (A,B) ∈ E AND (B,C) ∈ E
// A y C comparten un contacto mutuo directo
// Esto NO garantiza trust(A,C) — B conoce a ambos, pero A y C son strangers
soft_trust(A, C) habilita descubrimiento (C puede aparecer en sugerencias para A). trust(A, C) habilita acciones de confianza (C puede atestar la recuperación de A, C puede contribuir datos a entidades de A con karma elevado). Las implementaciones deben distinguir ambos predicados.
// Decay de aristas: el peso de una arista decae con el tiempo
edge_weight(A, B, t) = w₀ * decay_fn(t - last_interaction(A, B))
// Función de decay configurable por ceiba (en ceiba.json)
// Default: exponencial con half-life de 1 año
decay_fn(Δt_seconds) = exp(-Δt / 31_536_000)
// Para trust graph queries, edges con weight < 0.1 se ignoran
// (configurado como threshold en ceiba.json:trust.edge_weight_threshold)
4.2 Resolución de Entidades Cross-Network
Cuando una entidad existe en múltiples ceibas sin haber sido explícitamente federada, el protocolo puede proponer una identificación cruzada basada en matching de campos.
// Pesos de matching por campo
MATCH_WEIGHTS = {
"phone_e164": 0.9, // identifier fuerte
"social_handles": 0.7, // por handle específico
"birth_year": 0.4,
"city": 0.3,
"genres": 0.25, // jaccard similarity
"years_active": 0.2
}
// body.name y body.@type: excluidos del scoring (demasiado comunes)
// Threshold: score > 0.85 → candidate match
// Algoritmo
match_score(entity_A, entity_B):
score = 0
for field, weight in MATCH_WEIGHTS:
if field_matches(entity_A[field], entity_B[field]):
score += weight
return score
// Privacidad: el matching corre CLIENT-SIDE antes de proponer al usuario
// El servidor NUNCA recibe los campos privados del otro nodo para hacer matching
// Protocol: ceiba A envía bloom filter de hashes de campos; ceiba B responde
// con su propio bloom filter. Intersección revela candidatos sin exponer datos.
La confirmación de la identificación cruzada es siempre opt-in del dueño de la entidad. Una vez confirmada, se crea un conector sibling_of entre las dos instancias, y ambas replicas se marcan como federated: true.
5. Economía del Karma — Especificación
El karma es el mecanismo de incentivo para contribuciones de datos verificadas. No es una criptomoneda ni requiere un token. Es una unidad de medida interna de la ceiba, convertible a moneda fiat o USDC según la configuración del pool.
5.1 Lifecycle de Contribución
// 1. El contribuidor envía una propuesta de dato
POST /v1/contributions
Authorization: Bearer {contributor_session}
{
"entity_id": "comedian/ana-torres",
"field": "body.city",
"new_value": "Guadalajara",
"source_url": "https://www.instagram.com/p/...",
"confidence": "high",
"contributor_did": "did:ceiba:user/carlos-contrib"
}
// Respuesta
HTTP/1.1 201 Created
{
"id": "ctr_0x4f2a8b...",
"status": "pending",
"karma_estimate": 3, // calculado por tipo de campo
"review_deadline": "2026-04-08T14:00:00Z"
}
// La entidad NO se modifica aún. El valor propuesto va a karma_escrow.
// 2. El titular (owner) revisa
PATCH /v1/contributions/ctr_0x4f2a8b
Authorization: Bearer {owner_session}
{
"status": "accepted",
"titular_signature": "base64url(Ed25519(contribution_id + entity_version))"
}
// En accepted:
// → entity[field] = new_value
// → entity.version += 1
// → karma acreditado al contribuidor (entra en escrow de pago)
// → entity.sources[] actualizado con source_url de la contribución
// 3. Disputar (solo owner, dentro de 24h post-aceptación)
POST /v1/contributions/ctr_0x4f2a8b/dispute
Authorization: Bearer {owner_session}
{
"reason": "El dato es incorrecto, sigo viviendo en CDMX",
"disputer_did": "did:ceiba:comedian/ana-torres"
}
// En dispute (dentro de 24h, disputer == owner):
// → entity[field] = previous_value
// → entity.version += 1 (el rollback también incrementa versión)
// → karma del contribuidor: devuelto al escrow (NO se paga)
// → si ya había pago USDC enviado: se registra deuda, no reversión on-chain
// 4. Consultar balance
GET /v1/karma/balance?entity=did:ceiba:user/carlos-contrib
{
"karma": 142.7, // karma_at_time(now) — ya aplicado decay
"karma_raw": 180, // karma earned sin decay
"pending_payout_mxn": 34.50,
"pool_balance": 1250.00, // en MXN equivalente
"next_distribution": "2026-05-01T00:00:00Z"
}
// Pago: se batea cuando pool_balance > threshold (configurable por ceiba)
// Opciones: x402/USDC (nativo del protocolo) o fiat bridge (SPEI/Stripe)
5.2 Decay del Karma
El karma decae en el tiempo para alinear los incentivos con la actualidad de los datos. Un contribuidor que aportó datos hace 5 años recibe menos pago que uno que contribuyó la semana pasada.
// Función de decay
karma_at_time(t) = karma_earned * max(0, 1 - (t - earned_at) / DECAY_PERIOD)
// Parámetros (ajustables por gobernanza)
DECAY_PERIOD = 157_680_000 // 5 años en segundos
// Ejemplo
// earned_at = 2023-01-01, karma_earned = 100, now = 2026-01-01
// t - earned_at = 3 años = 94,608,000 segundos
karma_at_time(now) = 100 * max(0, 1 - 94_608_000 / 157_680_000)
= 100 * max(0, 1 - 0.5997)
= 100 * 0.4003
= 40.03
// Después de 5 años: karma_at_time = 0 (contribución expirada)
// El karma_raw no se borra: es parte del historial del contribuidor
5.3 Distribución del Pool (split 80/20, v0.4)
El operador-semilla se paga de su producto, no del pool. El pool declarado se reparte solo entre contribuidores y commons fund de la ceiba. No hay tax mandatorio al protocolo upstream.
// Distribución en cada evento de pago (v0.4)
pool_total = revenue declarado por el operador como entrada al pool
(líneas de ingreso explícitamente listadas en ceiba.json,
NO el revenue total del producto del operador)
pool_total * 0.80 → contribuidores (proporcional a karma_at_time(now))
pool_total * 0.20 → commons_fund de la ceiba
(gobernado por la comunidad: moderadores, infra,
eventos, donación voluntaria al protocolo upstream)
// Pago a contribuidor i:
payout_i = (pool_total * 0.80) * (karma_i / sum_karma_all)
// donde karma_i = karma_at_time(now) del contribuidor i
// La distribución se recalcula completa en cada evento
Multi-rail. Una ceiba puede activar uno o varios rails de pago: stablecoin USDC (Base/Arbitrum), x402 micropagos, fiat-local con factura formal según jurisdicción del operador-semilla (CFDI en MX, monotributo en AR, invoice en US/EU), Lightning, MercadoPago, Wise, Stripe, off-protocol. Ninguno es default; el protocolo no impone jurisdicción.
Karma redimible no transable. El karma es contabilidad interna de la ceiba (no token, no blockchain, no exchange):
- Solo redimible — no transable persona-a-persona.
- Tipo de cambio fijo declarado en
ceiba.json; solo puede subir. - Escrow obligatorio del 100% del karma circulante validado, auditable vía
/v1/karma/escrow_proof. - Decay 5 años (ya en §5.2).
5.4 Anti-Farming
El farming (contribuciones masivas triviales para acumular karma) se mitiga mediante caps por tipo de acción y por par contribuidor-entidad.
| Tipo de contribución | Karma máximo | Cap por día/par | Notas |
|---|---|---|---|
| Corrección menor (typo, horario) | 1 | 1 karma/día/entidad | address, hours, phone |
| Campo nuevo (text) | 3–10 | — | pesado por importancia del campo |
| Campo nuevo (numérico) | 2–5 | — | capacity, price, birth_year |
| Foto/media | 5 | 3 medias/entidad/semana | deduplicación por hash |
| Verificación QR en persona | 50 | 1/par attester-entidad/año | firma con Passkey del verificador |
| Conector nuevo verificado | 8 | — | requiere evidencia en source_url |
| Contribución de bot | mismo peso | mismo cap | flagged bot: true en contribution record |
6. Replicación Distribuida — Especificación
6.1 Protocolo de Sincronización
Un nodo se anuncia publicando su manifiesto en el índice ceiba.to. Luego participa en sincronización pull o push.
// Registro de nodo
POST https://ceiba.to/v1/nodes/register
{
"node_url": "https://mi-ceiba.example.com",
"manifest_url": "https://mi-ceiba.example.com/.well-known/ceiba.json",
"operator_did": "did:ceiba:user/operador"
}
// ── PULL ─────────────────────────────────────────────────────
// Obtener entidades actualizadas desde un timestamp dado
GET /v1/entities?updated_after=2026-04-01T00:00:00Z&limit=100
GET /v1/entities?updated_after=2026-04-01T00:00:00Z&limit=100&cursor=abc123
// Respuesta
{
"entities": [ { ...ciempiés... }, ... ],
"next_cursor": "def456", // null si no hay más páginas
"total_updated": 342
}
// ── PUSH ─────────────────────────────────────────────────────
POST /v1/sync/push
Authorization: Bearer {node_api_key}
{
"source_node": "https://mi-ceiba.example.com",
"entity_batch": [ { ...ciempiés... }, ... ], // max 500 por request
"batch_hash": "sha256(canonical_json(entity_batch))"
}
// ── RESOLUCIÓN DE CONFLICTOS ──────────────────────────────────
// 1. Last-writer-wins por updated_at (timestamp de reloj de pared)
// 2. Si updated_at es igual: winner = entidad con mayor version
// 3. Si ambas son iguales: winner = entidad con firma role==owner presente
// 4. Owner signature siempre overrides cualquier timestamp
// 5. Conectores: merge union (AMBOS conjuntos de conectores se preservan)
// 6. Conflicto irresolvible: flagged para revisión manual (status: "conflict")
6.2 Federación Cross-Ceiba (v1 — Pull-only)
En la versión 1, la federación es asimétrica: ceibas consumidoras hacen pull de entidades desde ceibas autoritativas bajo demanda. No existe push federado en v1. Este es un tradeoff deliberado de simplicidad sobre completitud.
// ceiba.json declara qué tipos son autoritativos
{
"authoritative_types": ["comedian", "venue"],
"federation": {
"version": "v1",
"allow_pull": true,
"cache_ttl_seconds": 3600 // TTL del read-through cache
}
}
// Ceiba consumidora solicita entidad de ceiba autoritativa
// Si no está en caché local (o TTL expirado):
GET https://lacartelera.app/v1/entities/comedian/ana-torres
// La ceiba consumidora almacena copia local con:
{
"replicas": [
{
"node": "lacartelera.app",
"authoritative": true,
"last_sync": "2026-04-01T14:00:00Z"
}
]
}
// En v2 (roadmap): push federado mediante webhook subscription
// POST /v1/federation/subscribe { entity_type, callback_url, events: ["update", "delete"] }
7. Stack de Implementación
| Componente | Tecnología | Version/Notas | Despliegue real |
|---|---|---|---|
| API | FastAPI (Python) | ≥0.110 + Pydantic v2 | La Cartelera, Sociales |
| ORM/DB local | SQLite + WAL mode | Python sqlite3 stdlib | La Cartelera, Sociales |
| DB hosted | Supabase (PostgreSQL 15) | RLS por entity_id | [IMPL] — disponible |
| Archival | IPFS (Kubo) | CID v1, sha2-256 | [SPEC] — no impl. aún |
| Frontend | Vanilla JS + HTML | Sin build step | Todas las ceibas |
| Reverse proxy | Caddy 2 | Caddyfile + systemd | koa-files, bob |
| Autenticación | py_webauthn | ≥2.0 | [SPEC] |
| SMS OTP | Twilio Verify | API v2 | [SPEC] |
| Micropagos | x402 (HTTP 402 + USDC) | Base L2 / Polygon | [SPEC] — no impl. |
| Agentes IA | anthropic-mcp-python | ≥0.1 | La Cartelera |
| Scaffolding | koa-gen CLI | interno | Todas las ceibas |
Lista de verificación de cumplimiento del protocolo
Un nodo es un nodo ceiba válido si y solo si cumple todos los ítems marcados como REQUIRED:
| Requisito | Nivel | Descripción |
|---|---|---|
Manifiesto /.well-known/ceiba.json | REQUIRED | JSON válido contra schema v1 (ver §8.1) |
| API REST + OpenAPI | REQUIRED | Endpoint /openapi.json disponible y válido |
| Tipos Schema.org | REQUIRED | body.@type en cada entidad es un tipo Schema.org válido |
| Proveniencia de campos | REQUIRED | Cada campo de body con entrada en sources[] |
| Historial de versiones | REQUIRED | GET /v1/entities/{id}/history retorna versiones previas |
| Identidad por reclamo | REQUIRED | Endpoint POST /v1/claim implementado |
| Sync pull | REQUIRED | GET /v1/entities?updated_after= con paginación |
| Passkey / WebAuthn | RECOMMENDED | Layer 1 identity (sin esto, solo Layer 0) |
| MCP server | RECOMMENDED | Acceso nativo para agentes IA |
| x402 / micropagos | OPTIONAL | Para ceibas con economía karma activa |
| IPFS archival | OPTIONAL | Para resiliencia a largo plazo |
| Social recovery | OPTIONAL | Requiere Passkey activo |
8. Apéndice Técnico
8.1 Schema Completo — ceiba.json
{
"$schema": "https://ceiba.to/schemas/manifest/v1.json",
// ── IDENTIDAD DEL NODO ───────────────────────────────────────
"id": "lacartelera", // slug único, lowercase kebab-case
"name": "La Cartelera",
"description": "Directorio de comedia en vivo en México",
"url": "https://lacartelera.app",
"operator_did": "did:ceiba:user/inge",
"operator_email": "[email protected]",
// ── PROTOCOLO ────────────────────────────────────────────────
"protocol_version": "0.4",
"api_base": "https://lacartelera.app/v1",
"openapi_url": "https://lacartelera.app/openapi.json",
"mcp_url": "https://lacartelera.app/mcp",
// ── ENTIDADES ────────────────────────────────────────────────
"authoritative_types": ["comedian", "venue", "show"],
"entity_count": 341, // actualizado periódicamente
"last_updated": "2026-04-01T14:00:00Z",
// ── FEDERACIÓN ───────────────────────────────────────────────
"federation": {
"version": "v1",
"allow_pull": true,
"allow_push": false, // v1: pull-only
"cache_ttl_seconds": 3600
},
// ── IDENTIDAD Y AUTENTICACIÓN ────────────────────────────────
"identity": {
"sms_otp": true, // Layer 0
"passkey": true, // Layer 1 (WebAuthn)
"social_recovery": true, // Layer 2
"recovery_threshold": 3, // N de M contactos requeridos
"rp_id": "lacartelera.app" // WebAuthn relying party ID
},
// ── ECONOMÍA ─────────────────────────────────────────────────
"karma": {
"enabled": true,
"redemption_rate": "1 karma = 1 MXN", // fijo, monotónico, solo puede subir
"escrow_proof_url": "/v1/karma/escrow_proof",
"escrow_coverage": 1.0, // 100% del karma circulante validado
"decay_period_seconds": 157680000, // 5 años
"pool_distribution": { // v0.4: 80/20
"contributors": 0.80,
"commons_fund": 0.20
},
"payment_threshold": "declared_per_ceiba",
"payment_rails": ["x402_usdc", "spei_cfdi", "mercadopago", "stripe"]
},
// ── TRUST GRAPH ──────────────────────────────────────────────
"trust": {
"edge_weight_threshold": 0.1,
"decay_half_life_seconds": 31536000 // 1 año
},
// ── DATOS ABIERTOS ───────────────────────────────────────────
"open_data": {
"license": "CC BY 4.0",
"ical_feed": "https://lacartelera.app/feed.ics",
"llms_txt": "https://lacartelera.app/llms.txt"
}
}
8.2 Registro Canónico de Tipos de Conectores
Los conectores registrados en el namespace ceiba: tienen semántica fija. Los ceibas pueden definir conectores custom en su propio namespace ({ceiba-id}:). Los conectores custom no participan en el matching cross-ceiba automático.
| Namespace | Tipo | Schema.org equivalent | Aplicabilidad |
|---|---|---|---|
| ceiba: | performs_at / has_performer | performer / performerIn | Person ↔ Event/Venue |
| ceiba: | hosts_show / hosted_at | organizer / location | Venue ↔ Event |
| ceiba: | member_of / has_member | memberOf / member | Person ↔ Organization |
| ceiba: | located_in / has_location | containedInPlace / containsPlace | Any ↔ Place |
| ceiba: | owns / owned_by | owns / — | Person ↔ Business |
| ceiba: | attests / attested_by | — (ceiba-native) | Person ↔ Person |
| ceiba: | knows / known_by | knows / — | Person ↔ Person |
| ceiba: | routes_via / routes_for | — (ceiba-native) | Route ↔ Adapter |
| lacartelera: | headlines / headlined_by | — (custom) | Show ↔ Comedian |
8.3 Tabla de Pesos Karma — Referencia Completa
| Campo / Acción | Karma base | Cap | Condición |
|---|---|---|---|
| body.name (corrección typo) | 0.5 | 1/día/entidad | — |
| body.city | 2 | — | requiere source_url |
| body.phone_e164 | 5 | — | requiere source confidence=high |
| body.social_handles (nuevo) | 3 | — | verificado por scraping |
| body.description | 3 | — | ≥50 chars, no spam |
| body.birth_year | 2 | — | confidence=medium o más |
| body.genres (nuevo tag) | 1 | 5 tags/entidad | del vocabulario oficial |
| Media/foto principal | 5 | 3/entidad/semana | hash único, min 400px |
| Conector nuevo (observado) | 8 | — | source_url de evento/cartelera |
| Verificación QR en persona | 50 | 1/par/año | Passkey del verificador requerido |
| Reporte de entidad obsoleta | 2 | — | si el titular confirma |
| Contribución de bot (flag) | mismo | mismo | bot: true en record |
8.4 Trust Graph Query — Pseudocódigo
def trust_query(entity_A: str, entity_C: str, graph: Graph) -> TrustResult:
"""
Retorna si A confía en C según el modelo ceiba.
trust(A, C) = TRUE iff A tiene al menos un vínculo directo con C
O ambos comparten al menos un contacto mutuo Y
A tiene algún vínculo directo (weak) con C.
Para social_recovery: se requiere soft_trust como mínimo.
Para karma_elevated: se requiere trust completo.
"""
# Filtrar aristas por weight_threshold
threshold = graph.ceiba_config.trust.edge_weight_threshold # default 0.1
def active_neighbors(entity: str) -> Set[str]:
return {
b for (a, b, w) in graph.edges
if a == entity and w >= threshold
and (b, a, _) in graph.edges # arista mutua
}
neighbors_A = active_neighbors(entity_A)
neighbors_C = active_neighbors(entity_C)
# Caso 1: conexión directa
if entity_C in neighbors_A:
return TrustResult(
trusted=True,
level="direct",
path=[entity_A, entity_C]
)
# Caso 2: vecindario compartido (co-conocidos)
common = neighbors_A & neighbors_C
if common:
# soft_trust: A y C comparten contacto B
# Nota: esto NO es trust(A,C) completo sin (A,C) directa
best_B = max(common, key=lambda b: edge_weight(A,b) + edge_weight(C,b))
return TrustResult(
trusted=False,
soft_trust=True,
level="co_neighbor",
path=[entity_A, best_B, entity_C],
common_contacts=list(common)
)
# Caso 3: BFS hasta depth=2 para descubrimiento
# (solo para sugerencias, NO para autorización)
depth_2 = set()
for b in neighbors_A:
depth_2 |= active_neighbors(b)
if entity_C in depth_2:
return TrustResult(
trusted=False,
soft_trust=False,
level="discovery_only"
)
return TrustResult(trusted=False, soft_trust=False, level="none")
def edge_weight(A: str, B: str, graph: Graph) -> float:
edge = graph.get_edge(A, B)
if not edge: return 0.0
delta_t = now() - edge.last_interaction
half_life = graph.ceiba_config.trust.decay_half_life_seconds # default 1 año
return edge.base_weight * (0.5 ** (delta_t / half_life))
ceiba.to es un proyecto de KOA Labs. Protocolo abierto. Implementación de referencia en Python (FastAPI). Datos abiertos por defecto.
Contacto técnico: [email protected] · Issues: git.koanet/inge/ceiba/issues · Spec anterior: v0.3 ES