Ir al contenido

Fichas

La ficha se crea al registrarte: con fecha, con número y sellada con un hash criptográfico que cualquiera puede recalcular. Registrarte la mantiene privada: solo cuando solicitas la verificación la ficha pasa a ser una entrada pública en el directorio. Esta página es la referencia completa, incluida la receta exacta del hash y cómo verificar una ficha en tres lenguajes distintos.

Estos son los campos públicos de una ficha verificada:

Campo Significado
registry_code El número THB, p. ej. THB-2026-00001.
slug El identificador de la URL pública de la ficha: https://platform.thehumanbehind.com/r/laia.
avatar_name El nombre del avatar, voz, agente o creación con IA registrada.
type avatar · voice_clone · agent · image · video · music.
scope general, o un ámbito sensible: health, finance, legal.
operates_at Dónde opera: el perfil, sitio o canal declarado.
responsible_name El nombre de la persona que responde por él.
status active, o unclaimed (una ficha a la espera de su titular, ver Reclamaciones).
verification_level registered (sello verde) o verified (sello dorado, con verified_at). Ver Verificación.
registered_at Fecha y hora del alta, en ISO-8601 UTC.
content_hash SHA-256 del snapshot del alta, con la receta de más abajo.

El correo de la persona responsable no es un campo de la ficha. Nunca forma parte del hash, nunca se muestra en la página pública y nunca lo devuelve la API. De forma estructural, no solo por política: la superficie pública lee de una vista de base de datos que no lo contiene.

Una mirada más de cerca a los campos con valores acotados:

Campo Valores y reglas
type Exactamente un identificador en minúsculas: avatar (un personaje o presentador digital), voice_clone (una voz sintética), agent (una IA autónoma que actúa por alguien), image (una imagen generada), video (un vídeo generado) o music (una pieza musical).
scope general por defecto. Los ámbitos sensibles health, finance y legal marcan un avatar que opera en un dominio regulado y de más peso.
slug Minúsculas, dígitos y guiones, derivado del nombre del avatar. Se asigna una vez y es inmutable. Si la base está cogida, se añade un sufijo numérico (-2, -3…).
operates_at Una URL o alias, o vacío. Cuando está vacío, en el hash se trata como la cadena vacía "" (esto importa al verificar).

Cada ficha recibe un identificador con el formato THB-<año>-<número>, p. ej. THB-2026-00001: el año del alta más un número secuencial dentro de ese año, relleno con ceros hasta un mínimo de cinco dígitos. Lo asigna la base de datos de forma atómica en el momento de la inserción, es correlativo sin huecos dentro de cada año y jamás se reutiliza, ni siquiera si la ficha se despublica después. Dos fichas nunca comparten número y el contador nunca retrocede, así que el número es en sí mismo una prueba aproximada del orden de registro.

Editable por el titular Inmutable para todos (titular y administradores)
avatar_name, type, scope, operates_at, responsible_name. registry_code, registered_at, slug.

Los triggers de la base de datos rechazan cualquier intento de cambiar un campo inmutable, venga del titular o venga de nuestros administradores. Tu número, tu fecha y tu slug son tuyos desde el primer segundo y nadie los mueve.

El content_hash funciona distinto, y la diferencia importa al verificar. Describe siempre la ficha tal y como está hoy: si editas un campo descriptivo, el registro lo recalcula, así que un recálculo a partir de los valores actuales cuadra siempre. Lo que conserva el pasado no es el hash, sino el registro que hay debajo: cada versión que ha tenido una ficha queda archivada de forma permanente, con su propio hash y su propia fecha, en una tabla de solo-añadir que nadie puede reescribir ni borrar, nosotros incluidos. Una edición añade una versión, nunca sustituye ninguna, y todo ese historial se ancla a diario en Bitcoin. El registro no guarda una instantánea congelada: las guarda todas. La despublicación también es «blanda»: la ficha deja de listarse, pero su URL sigue respondiendo con un aviso fechado (HTTP 410) para que la constancia permanezca.

El hash de contenido: receta pública exacta

Sección titulada «El hash de contenido: receta pública exacta»

El content_hash es un resumen SHA-256 que calcula la propia base de datos, en la misma transacción que crea la ficha. Cualquiera puede recalcularlo a partir de datos públicos. La receta, byte a byte:

  1. Toma los ocho campos del snapshot: registry_code, slug, avatar_name, type, scope, operates_at, responsible_name y registered_at.
  2. Normaliza los valores: type y scope son sus identificadores en minúsculas (p. ej., voice_clone); si operates_at no se indicó, se usa la cadena vacía ""; registered_at se formatea en ISO-8601 UTC con exactamente seis dígitos de microsegundos y una Z final: AAAA-MM-DDTHH:MM:SS.ssssssZ.
  3. Serializa como un objeto JSON de una sola línea en forma canónica: claves ordenadas primero por longitud y después por orden de bytes (es la forma textual canónica del jsonb de PostgreSQL). Para estos ocho campos, el orden es por tanto siempre: slug, type, scope, avatar_name, operates_at, registered_at, registry_code, responsible_name. Tras cada clave van dos puntos y espacio (: ) y los pares se separan con coma y espacio (, ). El resultado se codifica en UTF-8.
  4. Calcula el hash: content_hash es el SHA-256 de esos bytes en hexadecimal en minúsculas (64 caracteres).

El paso 3 es la única parte sutil. PostgreSQL ordena las claves de jsonb primero por su longitud en bytes y luego, entre claves de igual longitud, por valor de byte. Las ocho claves del snapshot son todas ASCII, así que el valor de byte es simplemente el orden alfabético. Este es el ordenamiento aplicado a nuestras ocho claves:

#ClaveLongitud
1slug4
2type4
3scope5
4avatar_name11
5operates_at11
6registered_at13
7registry_code13
8responsible_name16

Como estas ocho claves no cambian nunca, este orden es fijo para siempre: puedes fijarlo en el código. Solo slug/type (ambas de longitud 4), avatar_name/operates_at (ambas de longitud 11) y registered_at/registry_code (ambas de longitud 13) necesitaron el desempate, y en cada pareja va primero la clave alfabéticamente menor.

Para una ficha llamada Laia (THB-2026-00001), el snapshot canónico es exactamente esta línea:

{"slug": "laia", "type": "avatar", "scope": "general", "avatar_name": "Laia", "operates_at": "", "registered_at": "2026-07-30T23:44:44.547749Z", "registry_code": "THB-2026-00001", "responsible_name": "AIGiner S.L."}

Compruébalo en cualquier máquina con una shell:

printf '%s' '{"slug": "laia", "type": "avatar", "scope": "general", "avatar_name": "Laia", "operates_at": "", "registered_at": "2026-07-30T23:44:44.547749Z", "registry_code": "THB-2026-00001", "responsible_name": "AIGiner S.L."}' | sha256sum
# 23ec5dae1e4b330a1ebe266db49bf2d0ca88b7bb6fff0e0eeca1106bbbc5fb6d

La salida coincide con el content_hash publicado de la ficha. Cambia un solo carácter en cualquier punto del snapshot y el hash será completamente distinto: eso es lo que hace que la ficha sea a prueba de manipulaciones.

Este fragmento descarga la ficha de la API pública, reconstruye el snapshot canónico con la receta de arriba y compara. Solo librería estándar:

import json, hashlib, urllib.request

SLUG = "laia"
url = f"https://platform.thehumanbehind.com/api/v0/records/{SLUG}.json"

# Con User-Agent propio: el de urllib por defecto ("Python-urllib/3.x") lo
# rechaza el Browser Integrity Check de Cloudflare con un 403. Cualquier
# cadena vale; identificarse es lo educado.
req = urllib.request.Request(url, headers={"User-Agent": "thb-verify/1.0"})
rec = json.load(urllib.request.urlopen(req))

# The eight snapshot fields; a missing operates_at becomes "".
fields = ["registry_code", "slug", "avatar_name", "type",
          "scope", "operates_at", "responsible_name", "registered_at"]
snapshot = {k: (rec.get(k) or "") for k in fields}

# The timestamp is canonicalised before hashing: the registry seals
# "...862834Z", while the API returns the raw value "...862834+00:00".
# Skip this line and the digest will not match.
snapshot["registered_at"] = snapshot["registered_at"].replace("+00:00", "Z")

# Canonical form = PostgreSQL jsonb text: keys sorted by (length, then bytes),
# ": " after each key and ", " between pairs.
ordered = dict(sorted(snapshot.items(), key=lambda kv: (len(kv[0]), kv[0])))
canonical = json.dumps(ordered, separators=(", ", ": "), ensure_ascii=False)

digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
print(digest == rec["content_hash"])  # True

La misma comprobación en Node 18+ (fetch global, node:crypto):

import { createHash } from "node:crypto";

const SLUG = "laia";
const res = await fetch(
  `https://platform.thehumanbehind.com/api/v0/records/${SLUG}.json`,
);
const rec = await res.json();

// The eight snapshot fields; a missing operates_at becomes "".
const fields = ["registry_code", "slug", "avatar_name", "type",
                "scope", "operates_at", "responsible_name", "registered_at"];
const snapshot = {};
for (const k of fields) snapshot[k] = rec[k] ?? "";

// The timestamp is canonicalised before hashing: the registry seals
// "...862834Z", while the API returns the raw value "...862834+00:00".
// Skip this line and the digest will not match.
snapshot.registered_at = snapshot.registered_at.replace("+00:00", "Z");

// Canonical form = PostgreSQL jsonb text: keys sorted by length then bytes.
const keys = Object.keys(snapshot).sort(
  (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0),
);
const canonical =
  "{" +
  keys.map((k) => JSON.stringify(k) + ": " + JSON.stringify(snapshot[k])).join(", ") +
  "}";

const digest = createHash("sha256").update(canonical, "utf8").digest("hex");
console.log(digest === rec.content_hash); // true
  • operates_at vacío: una ficha sin ubicación declarada hashea la cadena vacía "" para ese campo, no null. Los fragmentos de arriba ya lo hacen con rec.get(k) or "" / rec[k] ?? "".
  • Fichas editadas: el hash sella los valores del primer día. Si un campo editable cambió desde el alta, un recálculo a partir de los valores actuales no coincidirá; es lo esperado. Para confirmar un hecho histórico, verifica con los valores originalmente registrados.
  • Valores no ASCII: la forma canónica es UTF-8 y usa el mismo escapado JSON que el jsonb de PostgreSQL (caracteres literales, sin escapes innecesarios). Tanto json.dumps(..., ensure_ascii=False) como el JSON.stringify de JavaScript coinciden con él para nombres y URLs habituales.
  1. Obtén la ficha desde la API pública (o lee los campos en su página pública).
  2. Reconstruye el snapshot canónico con la receta de arriba.
  3. Compara tu SHA-256 con el content_hash publicado. Si coinciden, los datos que ves son exactamente los que se registraron, en la fecha indicada.