# Prompt de integración TwinKYC Anchor (para Codex / Claude / cualquier IA que programe el backend del exchange)

> Copia TODO este archivo y pégalo como prompt en tu asistente de código. Está escrito para que la IA
> implemente, sin ambigüedad, la integración entre el backend del exchange y TwinKYC Anchor.

---

## 0) Contexto que debe entender la IA

Eres el ingeniero backend de un exchange de criptomonedas. Debes integrar el backend del exchange con
**TwinKYC Anchor**, un servicio externo que **ancla (estampa) verificaciones KYC en una red EVM privada**.

Regla de oro (no negociable): **TwinKYC nunca debe recibir datos personales**. No envíes nombres, correos,
teléfonos, direcciones, números de documento, imágenes ni PDFs. Solo se envían:

- `subject_ref`: identificador **opaco** y estable del usuario (bytes32 hex).
- `payload_hash`: hash del expediente KYC (bytes32 hex).
- Metadatos no identificativos: nivel KYC, código de país ISO-3166-1 alpha-2, nombre del proveedor KYC,
  fechas, códigos numéricos de razón/riesgo.

Los documentos y PII permanecen **cifrados dentro del exchange**. On-chain solo viajan hashes.

### Datos de conexión

```
Base URL:      https://kyc.twinbot.io/api/public/v1
Documentación: https://kyc.twinbot.io/docs
OpenAPI 3.1:   https://kyc.twinbot.io/api/public/v1/openapi.json  (impórtalo en Postman/Swagger)
Credenciales:  X-Api-Key (key_id) + secreto HMAC, emitidos en el panel admin de TwinKYC
```

Guarda `TWINKYC_KEY_ID` y `TWINKYC_SECRET` como variables de entorno / secret manager. Nunca en el repo,
nunca en el frontend.

---

## 1) Cómo derivar `subject_ref` y `payload_hash`

### `subject_ref` — referencia opaca del usuario

Debe ser determinista, estable en el tiempo, y **no reversible** a la identidad del usuario.

```
subject_ref = "0x" + hex( HMAC_SHA256( key = SUBJECT_REF_PEPPER, msg = "user:" + internal_user_id ) )
```

- `SUBJECT_REF_PEPPER`: secreto de 32+ bytes que **solo vive en el exchange**. Nunca se comparte con TwinKYC.
- Guarda `subject_ref` en tu tabla de usuarios (`users.kyc_subject_ref`) para no recalcularlo y poder mapear
  webhooks de vuelta al usuario.
- Resultado: 0x + 64 caracteres hex (bytes32).

### `payload_hash` — huella del expediente KYC

Hash del expediente **canonicalizado**. Debe ser reproducible: si mañana quieres probar que ese expediente
existía, tienes que poder recalcular exactamente el mismo hash.

```
canonical = JSON con claves ordenadas alfabéticamente, sin espacios, UTF-8, sin campos nulos
payload_hash = "0x" + hex( SHA256( canonical ) )
```

Ejemplo de objeto canonical (queda **solo** en tu backend, jamás se envía):

```json
{"doc_back_sha256":"…","doc_front_sha256":"…","doc_number_hmac":"…","doc_type":"passport","dob":"1990-01-31","full_name":"…","liveness_score":0.98,"nationality":"MX","provider":"sumsub","provider_applicant_id":"…","verified_at":"2026-08-08T12:00:00.000Z"}
```

Persiste el `canonical` cifrado (AES-256-GCM) junto al usuario, más `payload_hash` en claro. Así puedes
auditar y verificar contra la cadena en cualquier momento.

### Otros hashes del módulo de compliance

Mismo patrón `sha256(canonical_json)` → bytes32 para: `screening_hash`, `source_of_funds_hash`,
`consent_hash`, `evidence_hash`, `travel_rule_hash`, `policy_hash`.

---

## 2) Autenticación: firma HMAC-SHA256 en cada petición

Cada request lleva 3 cabeceras. La firma se calcula sobre esta cadena base:

```
base = `${timestamp}.${METHOD}.${path_sin_query}.${sha256_hex(body)}`

X-Api-Key:    <key_id>
X-Timestamp:  <unix seconds>
X-Signature:  hex( HMAC_SHA256(secret, base) )
Content-Type: application/json
```

Detalles obligatorios:

- `METHOD` en MAYÚSCULAS (`POST`, `GET`).
- `path_sin_query` incluye el prefijo completo: `/api/public/v1/kyc/stamp` (sin `?a=b`).
- `body` es el **string exacto** que envías. Para GET usa `""` (su sha256 es el de la cadena vacía).
- `timestamp` en segundos; tolerancia ±300s. Sincroniza el reloj (NTP).
- Firma el body **serializado una sola vez**; no vuelvas a serializar al enviar.

### Implementación de referencia (Node.js / TypeScript)

```ts
import { createHash, createHmac } from "node:crypto";

const BASE = "https://kyc.twinbot.io/api/public/v1";
const KEY_ID = process.env.TWINKYC_KEY_ID!;
const SECRET = process.env.TWINKYC_SECRET!;

const sha256 = (s: string) => createHash("sha256").update(s, "utf8").digest("hex");

export async function twinkyc<T = unknown>(
  method: "GET" | "POST",
  path: string,            // ej. "/kyc/stamp"
  body?: unknown,
): Promise<{ status: number; data: T }> {
  const fullPath = `/api/public/v1${path.split("?")[0]}`;
  const raw = body === undefined ? "" : JSON.stringify(body);
  const ts = Math.floor(Date.now() / 1000).toString();
  const signature = createHmac("sha256", SECRET)
    .update(`${ts}.${method}.${fullPath}.${sha256(raw)}`)
    .digest("hex");

  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      "content-type": "application/json",
      "x-api-key": KEY_ID,
      "x-timestamp": ts,
      "x-signature": signature,
    },
    ...(method === "POST" ? { body: raw } : {}),
  });
  return { status: res.status, data: (await res.json()) as T };
}
```

### Implementación de referencia (Python)

```python
import hashlib, hmac, json, time, requests

BASE = "https://kyc.twinbot.io/api/public/v1"

def twinkyc(method: str, path: str, body=None, key_id="", secret=""):
    full_path = "/api/public/v1" + path.split("?")[0]
    raw = "" if body is None else json.dumps(body, separators=(",", ":"), sort_keys=True)
    ts = str(int(time.time()))
    base = f"{ts}.{method}.{full_path}.{hashlib.sha256(raw.encode()).hexdigest()}"
    sig = hmac.new(secret.encode(), base.encode(), hashlib.sha256).hexdigest()
    headers = {"content-type": "application/json", "x-api-key": key_id,
               "x-timestamp": ts, "x-signature": sig}
    r = requests.request(method, BASE + path, headers=headers,
                         data=raw if method == "POST" else None, timeout=15)
    return r.status_code, r.json()
```

### Códigos de error

| Código | Significado | Qué hacer |
| --- | --- | --- |
| 401 | Firma inválida o timestamp fuera de ventana | Revisa base string / reloj |
| 403 | IP no permitida o API key expirada | Añade la IP en el panel o rota la key |
| 404 | `subject_ref` desconocido | Estampa primero con `/kyc/stamp` |
| 422 | Payload inválido | Valida formato bytes32 y tipos |
| 429 | Rate limit | Respeta `X-RateLimit-Reset`, backoff exponencial |
| 503 | Modo mantenimiento | Reintenta con backoff; encola localmente |

Respuestas traen `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.

---

## 3) Qué debe implementar el backend del exchange

### 3.1 Modelo de datos (añadir a tu esquema)

```sql
ALTER TABLE users ADD COLUMN kyc_subject_ref  text UNIQUE;      -- 0x + 64 hex
ALTER TABLE users ADD COLUMN kyc_level        smallint DEFAULT 0;
ALTER TABLE users ADD COLUMN kyc_frozen       boolean  DEFAULT false;
ALTER TABLE users ADD COLUMN kyc_expires_at   timestamptz;

CREATE TABLE kyc_anchor_log (
  id             bigserial PRIMARY KEY,
  user_id        bigint NOT NULL REFERENCES users(id),
  subject_ref    text NOT NULL,
  payload_hash   text NOT NULL,
  anchor_id      text,                 -- id devuelto por TwinKYC
  status         text NOT NULL DEFAULT 'queued', -- queued|pending|confirmed|failed|revoked
  tx_hash        text,
  idempotency_key text UNIQUE,
  payload_encrypted bytea,             -- canonical JSON cifrado (AES-256-GCM)
  created_at     timestamptz DEFAULT now(),
  updated_at     timestamptz DEFAULT now()
);

CREATE TABLE kyc_webhook_events (      -- idempotencia de webhooks
  delivery_id text PRIMARY KEY,
  event_type  text NOT NULL,
  received_at timestamptz DEFAULT now(),
  payload     jsonb NOT NULL
);
```

### 3.2 Flujo cuando un KYC se aprueba en el exchange

1. El proveedor KYC (Sumsub/Jumio/interno) aprueba al usuario.
2. Construye el `canonical`, calcula `payload_hash`, cífralo y guárdalo.
3. Obtén o crea `subject_ref`.
4. Inserta fila en `kyc_anchor_log` con `idempotency_key` (ej. `sha256(subject_ref + payload_hash)`).
5. Encola un job asíncrono (BullMQ/Celery/SQS) que llama `POST /kyc/stamp`. **Nunca bloquees el request HTTP
   del usuario esperando la cadena.**
6. Guarda `anchor_id`; espera el webhook `stamp.confirmed` para marcar `confirmed`.

### 3.3 Llamadas principales

```jsonc
// POST /kyc/stamp   (scope: stamp)  -> 202
{
  "subject_ref": "0x…64hex",
  "payload_hash": "0x…64hex",
  "meta_hash": "0x…64hex",          // opcional
  "kyc_level": 2,                    // 0-255
  "country_code": "MX",
  "provider": "sumsub",
  "verified_at": "2026-08-08T12:00:00.000Z",
  "idempotency_key": "…"             // reintento seguro
}
// 202 { "anchor_id": "…", "subject_ref": "0x…", "status": "pending" }

// POST /kyc/bulk-stamp  -> hasta 200 items { "items": [ {…}, {…} ] }

// POST /kyc/verify   (scope: read) -> ¿este payload_hash está on-chain?
{ "subject_ref": "0x…", "payload_hash": "0x…" }

// POST /kyc/bulk-status  -> estado de hasta 500 referencias
{ "subject_refs": ["0x…","0x…"] }

// GET  /kyc/{subject_ref}          -> historial
// GET  /kyc/{subject_ref}/proof    -> prueba Merkle de inclusión
// POST /kyc/revoke                 -> { "subject_ref", "reason_code" }
// POST /kyc/level                  -> { "subject_ref", "kyc_level", "reason_code" }
// POST /kyc/freeze                 -> { "subject_ref", "frozen": true, "reason_code" }
// POST /kyc/extend                 -> { "subject_ref", "expires_at": "2027-01-31T00:00:00.000Z" }
// GET  /kyc/expiring?days=30&limit=100
// GET  /compliance/report?from=&to=      -> agregados sin PII
// GET  /health   (público)   GET /stats   GET /contract?abi=1
```

### 3.4 Módulo de compliance v3 (todo bytes32 / enteros, cero PII)

```jsonc
POST /kyc/screening       { "subject_ref", "screening_hash", "sanctions_result": 0, "pep_tier": 0, "aml_risk": 12 }
POST /kyc/source-of-funds { "subject_ref", "source_of_funds_hash" }
POST /kyc/consent         { "subject_ref", "consent_hash", "consent_at" }
POST /kyc/review          { "subject_ref", "next_review_at" }
POST /kyc/evidence        { "subject_ref", "evidence_hash", "evidence_type_code": 1 }
POST /kyc/travel-rule     { "subject_ref", "travel_rule_hash" }
POST /kyc/erase           { "subject_ref", "reason_code" }        // GDPR art.17, deja lápida
POST /kyc/compliance-check{ "subject_ref", "min_level": 2, "max_screening_age_days": 365, "max_aml_risk": 50 }
// -> { "compliant": true, "reason_code": 0, "reason": "compliant" }
```

Usa `/kyc/compliance-check` como **gate on-chain** antes de permitir retiros grandes, trading con
apalancamiento, o alta de nuevos pares fiat.

---

## 4) Webhooks: qué endpoint debe crear el exchange

Crea **un** endpoint HTTPS público en tu backend, por ejemplo:

```
POST https://api.tu-exchange.com/webhooks/twinkyc
```

Regístralo en el panel de TwinKYC (pestaña **Webhooks**), donde recibirás el **secreto del webhook**.

### Cabeceras que recibirás

```
x-timestamp:   <unix seconds>
x-signature:   hmac_sha256(webhook_secret, `${timestamp}.POST.${path}.${sha256(body)}`)
x-event-type:  stamp.confirmed
x-delivery-id: <uuid>   -> úsalo para idempotencia
content-type:  application/json
```

`path` es el pathname de **tu** URL de webhook (ej. `/webhooks/twinkyc`).

### Cuerpo

```json
{
  "event": "stamp.confirmed",
  "emitted_at": "2026-08-08T12:00:03.120Z",
  "data": {
    "anchor_id": "6f6d…7f11",
    "subject_ref": "0x…",
    "payload_hash": "0x…",
    "tx_hash": "0x…",
    "block_number": 148213,
    "status": "confirmed"
  }
}
```

### Eventos a manejar

| Evento | Acción sugerida en el exchange |
| --- | --- |
| `stamp.submitted` | marcar `pending`, guardar `tx_hash` |
| `stamp.confirmed` | marcar `confirmed`, habilitar features del nivel KYC |
| `stamp.failed` | alertar a ops, reencolar con backoff |
| `revoke.confirmed` | bajar nivel del usuario, bloquear retiros |
| `kyc.expired` | pedir re-verificación, restringir cuenta |
| `kyc.level_changed` | sincronizar `users.kyc_level` y límites |
| `kyc.frozen` / `kyc.unfrozen` | congelar / descongelar operaciones |
| `batch.committed` | actualizar pruebas Merkle |
| `policy.anchored` | registrar en tu log de compliance |
| `kyc.screening_attested`, `kyc.source_of_funds_attested`, `kyc.consent_recorded`, `kyc.review_scheduled`, `kyc.evidence_attached`, `kyc.travel_rule_attested`, `kyc.compliance_event`, `kyc.erased` | sincronizar tu expediente de compliance |
| `system.alert` | notificar a on-call (gas bajo, RPC caído, cola atascada) |

### Handler de referencia (Express)

```ts
import express from "express";
import { createHash, createHmac, timingSafeEqual } from "node:crypto";

const app = express();

app.post("/webhooks/twinkyc",
  express.raw({ type: "application/json" }),   // ¡body crudo, no parseado!
  async (req, res) => {
    const raw = req.body.toString("utf8");
    const ts = String(req.header("x-timestamp") ?? "");
    const sig = String(req.header("x-signature") ?? "");
    const deliveryId = String(req.header("x-delivery-id") ?? "");

    // 1. anti-replay
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).send("stale");

    // 2. firma
    const bodyHash = createHash("sha256").update(raw, "utf8").digest("hex");
    const expected = createHmac("sha256", process.env.TWINKYC_WEBHOOK_SECRET!)
      .update(`${ts}.POST./webhooks/twinkyc.${bodyHash}`)
      .digest("hex");
    const a = Buffer.from(sig), b = Buffer.from(expected);
    if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).send("bad signature");

    // 3. idempotencia
    const fresh = await db.insertWebhookEventIfNew(deliveryId, raw);
    if (!fresh) return res.status(200).send("ok");   // duplicado

    // 4. responde RÁPIDO (<5s) y procesa en background
    res.status(200).send("ok");
    await queue.add("twinkyc-event", JSON.parse(raw));
  });
```

Requisitos del endpoint:

- Responder **2xx en menos de 5 segundos**; procesa en cola. Si no, TwinKYC reintenta con backoff
  exponencial (30s, 1m, 2m, 4m … hasta 1h).
- Ser **idempotente** por `x-delivery-id`.
- Aceptar solo HTTPS con certificado válido.
- No confiar en el body sin verificar la firma.

---

## 5) Checklist de implementación (la IA debe completar todo)

- [ ] Variables de entorno: `TWINKYC_KEY_ID`, `TWINKYC_SECRET`, `TWINKYC_WEBHOOK_SECRET`, `SUBJECT_REF_PEPPER`.
- [ ] Utilidad `subjectRef(userId)` determinista con pepper.
- [ ] Canonicalizador JSON + `payloadHash(canonical)`.
- [ ] Cliente HTTP firmado (sección 2) con reintentos, timeout 15s y backoff en 429/503.
- [ ] Migraciones de BD (sección 3.1).
- [ ] Worker asíncrono que estampa al aprobar KYC, con `idempotency_key`.
- [ ] Endpoint `POST /webhooks/twinkyc` verificado, idempotente y encolado.
- [ ] Sincronización de `kyc_level`, `kyc_frozen`, `kyc_expires_at` desde eventos.
- [ ] Gate de compliance con `/kyc/compliance-check` antes de retiros/operaciones sensibles.
- [ ] Job diario: `GET /kyc/expiring?days=30` → avisar a usuarios por re-verificar.
- [ ] Job de salud: `GET /health` cada 5 min → alerta si `status != ok`.
- [ ] Tests: firma HMAC (vectores fijos), verificación de webhook, idempotencia, reintentos.
- [ ] **Auditoría de privacidad**: ningún campo enviado a TwinKYC contiene PII. Escribe un test que lo asegure.

## 6) Prohibiciones explícitas

- ❌ No enviar nombres, emails, teléfonos, direcciones, documentos, selfies ni IDs de proveedor legibles.
- ❌ No usar el `internal_user_id` como `subject_ref` (es reversible / correlacionable).
- ❌ No exponer `TWINKYC_SECRET` ni el secreto del webhook en frontend, logs o mensajes de error.
- ❌ No procesar un webhook sin verificar la firma HMAC.
- ❌ No llamar a la API de forma síncrona dentro del request del usuario final.
