Especificación Técnica

ceiba.to: Especificación de Protocolo

Versión 0.4 — 2026-05-10 · koa.dev · Para diseñadores de protocolo, desarrolladores, criptógrafos y contribuidores de estándares abiertos
Estado de implementación: Las secciones marcadas con [IMPL] tienen implementación en producción. Las secciones marcadas con [SPEC] son especificaciones formales aún no implementadas al 100%. Las secciones marcadas con [DRAFT] están sujetas a revisión.

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

CapaTecnologíaRolEstado
TransporteHTTPS + REST/JSON-LDAPI pública de entidades[IMPL]
SemánticaSchema.org + ceiba: namespaceVocabulario de datos[IMPL]
Agentes IAMCP (anthropic-mcp-python)Consultas de agentes[IMPL]
Micropagosx402 (HTTP 402 + USDC)Monetización API-native[IMPL]
IdentidadWebAuthn Level 2 (Passkeys)Autenticación sin contraseña[SPEC]
Identidad Step-0SMS OTP (Twilio)Bootstrap de reclamo[SPEC]
RecuperaciónAtestación social (Shamir-inspired)Social recovery[SPEC]
Entidadesceiba-manifest (JSON + firma Ed25519)Formato ciempiés portátil[IMPL]
Scaffoldingkoa-gen CLIGeneración de nuevas ceibas[IMPL]

1.2 Almacenamiento

El protocolo es agnóstico del backend. La implementación de referencia soporta tres modos:

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

CeibaDominioEntidadesBackendStack extra
La Carteleralacartelera.app158 comediantes indexados, 183 forosSQLiteMCP server
Socialessociales.latEventos baile LATAMSupabasefuente 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.

OutboundInbound (complemento)Tipos de entidades
performs_athas_performerPerson → Venue/Event
hosts_showhosted_atVenue → Event
produced_byproducesEvent → Person/Org
located_inhas_locationAny → Place/City
member_ofhas_memberPerson → Organization/Ceiba
managed_bymanagesVenue/Ceiba → Person
supplier_ofsupplied_byOrg → Venue/Business
listed_onlistsAny → Ceiba/Directory
attestsattested_byPerson → Person (confianza)
routes_viaroutes_forRoute → Adapter (dominio financiero)
priced_inpricesProduct → Currency
event_inhas_eventEvent → Ceiba
teachestaught_byPerson → Style/Discipline
employsemployed_byOrg → Person
sibling_ofsibling_ofAny → Any (simétrico)
derived_fromhas_derivativeEntity → Entity (fork)
ownsowned_byPerson/Org → Venue/Business
knowsknown_byPerson → Person (trust graph)
Nota: 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

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"
Restricciones Layer 0: (1) Una entidad solo puede ser reclamada una vez. Intentar reclamar una entidad ya reclamada retorna 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.

Aclaración criptográfica: Este mecanismo NO es Shamir Secret Sharing. No hay división criptográfica de ninguna clave. Es un protocolo de atestación social: N entidades de confianza firman afirmaciones que habilitan el registro de un nuevo Passkey. La seguridad depende de la integridad social de los testigos, no de suposiciones criptográficas.
// 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:

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
Distinción clave: 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):

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ónKarma máximoCap por día/parNotas
Corrección menor (typo, horario)11 karma/día/entidadaddress, hours, phone
Campo nuevo (text)3–10pesado por importancia del campo
Campo nuevo (numérico)2–5capacity, price, birth_year
Foto/media53 medias/entidad/semanadeduplicación por hash
Verificación QR en persona501/par attester-entidad/añofirma con Passkey del verificador
Conector nuevo verificado8requiere evidencia en source_url
Contribución de botmismo pesomismo capflagged bot: true en contribution record
Farming via colisión: El escenario de mayor riesgo es la colisión titular-contribuidor (A reclama entidad, B contribuye basura, A acepta para pagar a B colludiendo). Mitigación: el análisis de reputación del titular pondera el historial de aceptaciones. Si un titular acepta >80% de contribuciones de un único contribuidor, se activa revisión de moderación. Los pagos de contribuciones aceptadas en <2 minutos se retienen 24h adicionales.

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

ComponenteTecnologíaVersion/NotasDespliegue real
APIFastAPI (Python)≥0.110 + Pydantic v2La Cartelera, Sociales
ORM/DB localSQLite + WAL modePython sqlite3 stdlibLa Cartelera, Sociales
DB hostedSupabase (PostgreSQL 15)RLS por entity_id[IMPL] — disponible
ArchivalIPFS (Kubo)CID v1, sha2-256[SPEC] — no impl. aún
FrontendVanilla JS + HTMLSin build stepTodas las ceibas
Reverse proxyCaddy 2Caddyfile + systemdkoa-files, bob
Autenticaciónpy_webauthn≥2.0[SPEC]
SMS OTPTwilio VerifyAPI v2[SPEC]
Micropagosx402 (HTTP 402 + USDC)Base L2 / Polygon[SPEC] — no impl.
Agentes IAanthropic-mcp-python≥0.1La Cartelera
Scaffoldingkoa-gen CLIinternoTodas 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:

RequisitoNivelDescripción
Manifiesto /.well-known/ceiba.jsonREQUIREDJSON válido contra schema v1 (ver §8.1)
API REST + OpenAPIREQUIREDEndpoint /openapi.json disponible y válido
Tipos Schema.orgREQUIREDbody.@type en cada entidad es un tipo Schema.org válido
Proveniencia de camposREQUIREDCada campo de body con entrada en sources[]
Historial de versionesREQUIREDGET /v1/entities/{id}/history retorna versiones previas
Identidad por reclamoREQUIREDEndpoint POST /v1/claim implementado
Sync pullREQUIREDGET /v1/entities?updated_after= con paginación
Passkey / WebAuthnRECOMMENDEDLayer 1 identity (sin esto, solo Layer 0)
MCP serverRECOMMENDEDAcceso nativo para agentes IA
x402 / micropagosOPTIONALPara ceibas con economía karma activa
IPFS archivalOPTIONALPara resiliencia a largo plazo
Social recoveryOPTIONALRequiere 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.

NamespaceTipoSchema.org equivalentAplicabilidad
ceiba:performs_at / has_performerperformer / performerInPerson ↔ Event/Venue
ceiba:hosts_show / hosted_atorganizer / locationVenue ↔ Event
ceiba:member_of / has_membermemberOf / memberPerson ↔ Organization
ceiba:located_in / has_locationcontainedInPlace / containsPlaceAny ↔ Place
ceiba:owns / owned_byowns / —Person ↔ Business
ceiba:attests / attested_by— (ceiba-native)Person ↔ Person
ceiba:knows / known_byknows / —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ónKarma baseCapCondición
body.name (corrección typo)0.51/día/entidad
body.city2requiere source_url
body.phone_e1645requiere source confidence=high
body.social_handles (nuevo)3verificado por scraping
body.description3≥50 chars, no spam
body.birth_year2confidence=medium o más
body.genres (nuevo tag)15 tags/entidaddel vocabulario oficial
Media/foto principal53/entidad/semanahash único, min 400px
Conector nuevo (observado)8source_url de evento/cartelera
Verificación QR en persona501/par/añoPasskey del verificador requerido
Reporte de entidad obsoleta2si el titular confirma
Contribución de bot (flag)mismomismobot: 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))

Estado de implementación — 2026-05-10: La Cartelera (lacartelera.app) implementa §2 (modelo ciempiés), §6 (replicación pull), y §7 (stack completo). Las secciones §3 (identidad WebAuthn), §4 (trust graph formalizado), y §5 (karma económico v0.4) son especificaciones formales en preparación para implementación en Q3 2026. Contribuciones bienvenidas: git.koanet/inge/ceiba.

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

Changelog v0.4 — 2026-05-10