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.
Qué contiene una ficha
Sección titulada «Qué contiene una ficha»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.
Referencia de campos
Sección titulada «Referencia de campos»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). |
El número THB
Sección titulada «El número THB»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.
Qué puede cambiar, y qué no podrá jamás
Sección titulada «Qué puede cambiar, y qué no podrá jamás»| 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:
- Toma los ocho campos del snapshot:
registry_code,slug,avatar_name,type,scope,operates_at,responsible_nameyregistered_at. - Normaliza los valores:
typeyscopeson sus identificadores en minúsculas (p. ej.,voice_clone); sioperates_atno se indicó, se usa la cadena vacía"";registered_atse formatea en ISO-8601 UTC con exactamente seis dígitos de microsegundos y unaZfinal:AAAA-MM-DDTHH:MM:SS.ssssssZ. - 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
jsonbde 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. - Calcula el hash:
content_hashes el SHA-256 de esos bytes en hexadecimal en minúsculas (64 caracteres).
El orden canónico de las claves, explicado
Sección titulada «El orden canónico de las claves, explicado»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:
| # | Clave | Longitud |
|---|---|---|
| 1 | slug | 4 |
| 2 | type | 4 |
| 3 | scope | 5 |
| 4 | avatar_name | 11 |
| 5 | operates_at | 11 |
| 6 | registered_at | 13 |
| 7 | registry_code | 13 |
| 8 | responsible_name | 16 |
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.
Ejemplo completo
Sección titulada «Ejemplo completo»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.
Verificar en Python
Sección titulada «Verificar en Python»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
Verificar en Node
Sección titulada «Verificar en Node»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
Casos límite que conviene conocer
Sección titulada «Casos límite que conviene conocer»operates_atvacío: una ficha sin ubicación declarada hashea la cadena vacía""para ese campo, nonull. Los fragmentos de arriba ya lo hacen conrec.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
jsonbde PostgreSQL (caracteres literales, sin escapes innecesarios). Tantojson.dumps(..., ensure_ascii=False)como elJSON.stringifyde JavaScript coinciden con él para nombres y URLs habituales.
Verificar una ficha de principio a fin
Sección titulada «Verificar una ficha de principio a fin»- Obtén la ficha desde la API pública (o lee los campos en su página pública).
- Reconstruye el snapshot canónico con la receta de arriba.
- Compara tu SHA-256 con el
content_hashpublicado. Si coinciden, los datos que ves son exactamente los que se registraron, en la fecha indicada.