BTCintel
nodeok
mempool68,600 tx
node next1.08 sat/vB
retarget40 blocks
chainmainnet

Docs

Citing btcintel

Every claim btcintel serves carries a citation you can quote and anyone can check later, without trusting us: the claim's id, the version you read, and the first sixteen hex characters of a hash over exactly what the source asserted. Versions never change; a claim that is later retracted or expires keeps its hash and its citation.

Ids and versions

KindIdWhat it says
annotationann_<n>a label, category, abuse or role a source put on an address, transaction, block or script
attributionatt_<n>a block attributed to a pool by a signature match or a manual row
relationrel_<n>a typed relation between two entities, never about a subject
eventevt_<n>a dated event a source reported, with what it cites

version starts at 1 and increments whenever the content hash changes. GET /v1/claims/{id} answers the current version; ?version=N pins an earlier one; /versions lists them all with their hashes, the actor and the time. A version is immutable: you can quote @v1 of a claim that is now at @v3 and a reader will fetch exactly what you saw. The same objects reach agents through the MCP tool get_claim (For agents).

The citation string

btcintel:<annotations|attributions|relations|events>:<id>@v<version> sha256:<first 16 hex of content_sha256>

btcintel:annotations:33932@v2 sha256:84ea2a24bc08df5e

Quote it as it comes, in a footnote or a data field. The claim page (/claims/ann_33932) copies it in one click and shows every version.

What is hashed

The hash covers what the source asserted and nothing of our bookkeeping. For an annotation, the content object has exactly these fields:

Content fieldFrom the claim JSON
kind"annotation"
subject{kind, key} as given
label, concepts, method, evidenceas given; concepts and evidence are [] when empty
source_idsource.id
fetch_sha256fetch.sha256
confidence_classthe stored class with its vocabulary prefix (confidence:trusted_provider); the claim shows it trimmed in confidence.class, and content carries it whole
valid_from, valid_to, retracted_at, first_reported_atvalidity.from, validity.to, validity.retracted_at, first_reported_at — RFC 3339 in UTC, to the second (2026-09-22T14:37:05Z), as content carries them; the optional ones only when present
category, abuse, role, entity_idas given, only when present; entity_id is entity.id

Never hashed: claim_id, version, citation, content_sha256, created_at, updated_at, source.name and source.attribution, fetch.id, fetch.url and fetch.fetched_at, confidence.level, metadata, retraction_reason. An attribution's content object is {kind: "attribution", subject, entity_id, method, source_id, matcher_version, confidence_class, evidence} plus fetch_sha256, signature_id and the validity times when present; a relation's is {kind: "relation", about: "entity", from_entity, to_entity, type, source_id, confidence_class, evidence} plus fetch_sha256 and validity; an event's is {kind: "event", event_kind, occurred_at (a date), occurred_precision, title, source_id, cites, refs} plus summary, fetch_sha256 and retracted_at when present.

Serialisation and hash

The content object is serialised with the JSON Canonicalization Scheme (RFC 8785): object keys sorted by their UTF-16 code units, no whitespace, strings escaped exactly as JSON.stringify does, numbers written as ECMAScript writes them. The hash is SHA-256 over those bytes; content_sha256 is its lowercase hex and the citation carries the first sixteen characters.

Worked example

The Akira ransom payment recorded by Ransomwhere on transaction 71e4dae2…, claim ann_33932, version 2. Fetch it:

curl -s 'https://api.btcintel.io/v1/claims/ann_33932?version=2' | jq '{citation, content_sha256, label, subject, confidence, validity}'
{
  "citation": "btcintel:annotations:33932@v2 sha256:84ea2a24bc08df5e",
  "content_sha256": "84ea2a24bc08df5e30e45d5383de7ff1edbb8029fe532d980219242b4a25e24c",
  "label": "Akira",
  "subject": { "kind": "tx", "key": "71e4dae29ed1bc9fe008bba7f3542fd5961b780c220ca8778100a7cb8bd4c81b" },
  "confidence": { "class": "trusted_provider", "level": 50 },
  "validity": { "from": "2026-09-22T14:37:05.466118Z", "to": null, "retracted_at": null, "retraction_reason": null }
}

The claim also carries content: the exact object the hash covers, as the table above describes it. Canonicalise it and hash it — here in Node (the same code runs in a browser):

import { createHash } from "node:crypto";

const claim = await (await fetch("https://api.btcintel.io/v1/claims/ann_33932")).json();
// RFC 8785: keys by UTF-16 code units, JSON.stringify escaping, ECMAScript numbers.
const canon = (v) =>
  v === null || typeof v !== "object" ? JSON.stringify(v)
  : Array.isArray(v) ? "[" + v.map(canon).join(",") + "]"
  : "{" + Object.keys(v).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).map((k) => JSON.stringify(k) + ":" + canon(v[k])).join(",") + "}";
const hash = createHash("sha256").update(canon(claim.content)).digest("hex");
console.log(hash === claim.content_sha256, claim.citation.endsWith(hash.slice(0, 16)));
// true true

A superseded version (?version=N below the current one) answers its hash and citation only; its content is not retained, so re-verification of an old version compares against the bytes you kept.

The canonical bytes begin

{"abuse":"abuse:ransomware","concepts":[],"confidence_class":"confidence:trusted_provider","entity_id":"storm-1567","evidence":[{"kind":"amount_sat","value":488528790},…

and hash to 84ea2a24bc08df5e30e45d5383de7ff1edbb8029fe532d980219242b4a25e24c: the hash the API declared, and the sixteen characters the citation carries. Change one character of the label and the hash moves — that is what the citation pins. The comparison in the snippet uses plain string ordering; it equals UTF-16 order for keys without astral characters, which is every key btcintel writes.

When a citation stops matching

  • The version moved. The current claim has a new version and hash; fetch the version you quoted with ?version=N and it still matches.
  • The claim was retracted. The version you quoted still verifies; the current version says why in validity.retraction_reason. Say so when you cite it.
  • Nothing matches. Tell us: the feedback route takes the claim id and what you computed. A hash we cannot reproduce is a bug on our side, not yours.