Verifying a prediction — without trusting aiternam
aiternam's entire claim is demonstrated accuracy over time. That claim is only worth something if a prediction provably existed at its stated time and was never altered — otherwise a reputation system is just "trust us, we didn't backdate anything."
So every prediction can be anchored to Bitcoin. You can download a small proof bundle and verify, with your own tools, that the exact prediction existed no later than a specific Bitcoin block — without aiternam being involved at all. If aiternam vanished tomorrow, the bundle in your hands would still prove it.
This page is the complete, frozen specification needed to do that.
The honest scope. The proof shows the prediction's content existed by the anchor time (the batch's Bitcoin block). For markets that is exactly what matters — it was committed before the outcome, and resolution horizons are days to years, far longer than the anchor delay. The proof does not put aiternam's own precise
created_aton-chain; see Caveats.
The proof bundle
Download it from any prediction: GET /v1/predictions/{id}/proof (or the "Download proof"
button). It is a single self-contained JSON file:
{
"aiternam_proof_version": 1,
"prediction_id": "…",
"anchor_status": "bitcoin",
"hash_version": 1,
"content_hash": "…", // sha256 of the canonical payload — the integrity proof
"payload": { … }, // the EXACT sealed forecast (raw values)
"merkle": {
"merkle_version": 1,
"root": "…", // the value committed to Bitcoin
"leaf_index": 12,
"tree_size": 40,
"path": ["…", "…"] // sibling hashes, leaf → root
},
"bitcoin": {
"anchored_at": "…",
"block_height": 849834,
"ots_base64": "…" // an OpenTimestamps proof: root → Bitcoin
}
}Verification is three independent checks. Each is something you compute; none asks you to trust a number aiternam gave you.
Step 1 — Recompute the content hash
The content_hash is sha256 over a canonical, language-portable serialization of the
payload. Recompute it yourself from the raw payload and check it matches. If it does, the
forecast — asset, direction, confidence, baseline price, timestamp, everything — is exactly what
was sealed; a single changed character would change the hash.
Canonicalisation rule — frozen as hash_version = 1:
- Build the object
{"hash_version": 1, "payload": <canonicalised payload>}. - Canonicalise the payload recursively:
- Every number becomes a fixed-point decimal string with exactly 12 fractional
digits, round-half-to-even — e.g.
0.73 → "0.730000000000",67000.5 → "67000.500000000000". (This is the crucial portability rule: raw float formatting differs between languages —1.0vs1, exponent thresholds — so we pin a single string form.) - Booleans stay JSON booleans; strings and
nullstay as-is.
- Every number becomes a fixed-point decimal string with exactly 12 fractional
digits, round-half-to-even — e.g.
- Serialize to JSON with keys sorted, compact separators (
,and:), UTF-8, no whitespace. content_hash = sha256(that UTF-8 JSON), lowercase hex.
The hash_version is bound into the hash, so the rule itself cannot be swapped silently. A
future change to the rule bumps the version; a hash sealed under v1 verifies under v1 forever.
Two clarifications. Both describe
hash_version = 1as it has always worked — neither is a change to it, and no already-sealed hash moves.
- The payload is not a fixed set of keys. Canonicalise whatever keys the bundle's
payloadactually contains. A field that does not apply to a prediction is omitted, never written asnull— a multi-day forecast, for instance, carries no sub-day horizon field at all — and predictions made later may carry optional fields earlier ones never had. A verifier that hard-codes a key list will fail on a prediction it wasn't written for; one that walks the object it was given keeps working forever.- "Number" means the decimal fields. Whole-number fields (e.g.
horizon_days,horizon_minutes) are serialized as plain JSON integers — exactly what the reference implementation below does, quantizing only floats. If your language's JSON parser can't tell1.0from1(notably JavaScript), read the bundle'spayload_float_fields: it names precisely the payload keys to render as 12-digit decimals.
Reference implementation (Python — any language reproduces it):
import hashlib, json
from decimal import Decimal, ROUND_HALF_EVEN, localcontext
def canon(v):
if isinstance(v, bool): return v
if isinstance(v, float):
with localcontext() as ctx:
ctx.prec = 50
return format(Decimal(str(v)).quantize(Decimal("1e-12"), ROUND_HALF_EVEN), "f")
if isinstance(v, dict): return {k: canon(x) for k, x in v.items()}
if isinstance(v, list): return [canon(x) for x in v]
return v
def content_hash(payload, version=1):
obj = {"hash_version": version, "payload": canon(payload)}
blob = json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
assert content_hash(bundle["payload"], bundle["hash_version"]) == bundle["content_hash"]⚠️ JSON parsers that read decimals as floats are fine here — the canonicalisation re-quantizes every float to the same 12-digit decimal string, so
0.73and0.7300000000001-style parse noise collapse to the identical bytes. If your language can read the raw payload as exact decimals, better still.
Step 2 — Walk the Merkle proof to the root
Anchoring batches many predictions into one Merkle tree and commits only the root to
Bitcoin (one Bitcoin transaction covers an entire batch). Your prediction's content_hash is a
leaf; the path lets you recompute the root and confirm your leaf is in it.
Tree rule — frozen as merkle_version = 1: the RFC 6962 (Certificate Transparency) binary
Merkle tree. We use a published standard with audited verifiers in many languages, and its
domain separation makes leaf/node confusion impossible.
- Leaf hash:
SHA256(0x00 || content_hash_bytes)(the hash decoded from hex to 32 bytes). - Node hash:
SHA256(0x01 || left || right). - Tree shape: split at the largest power of two strictly below
n(the RFC 6962 rule — not the Bitcoin "duplicate the last node" rule, which is ambiguous). - Leaf order: leaves are the batch's
content_hashes as 32-byte values, sorted ascending and de-duplicated, so the root depends only on the set of hashes.
Verification (RFC 6962 §2.1.1) — reconstruct the root from your leaf, leaf_index, tree_size
and path, and check it equals root:
import hashlib
def _leaf(b): return hashlib.sha256(b"\x00" + b).digest()
def _node(l, r): return hashlib.sha256(b"\x01" + l + r).digest()
def verify_inclusion(leaf_bytes, index, tree_size, path, root):
if not 0 <= index < tree_size: return False
fn, sn, r = index, tree_size - 1, _leaf(leaf_bytes)
for p in path:
if (fn & 1) or fn == sn:
r = _node(p, r)
if not (fn & 1):
while not (fn & 1) and fn != 0: fn, sn = fn >> 1, sn >> 1
else:
r = _node(r, p)
fn, sn = fn >> 1, sn >> 1
return sn == 0 and r == root
m = bundle["merkle"]
assert verify_inclusion(
bytes.fromhex(bundle["content_hash"]), m["leaf_index"], m["tree_size"],
[bytes.fromhex(s) for s in m["path"]], bytes.fromhex(m["root"]),
)Step 3 — Verify the root is in Bitcoin
The ots_base64 field is a standard OpenTimestamps proof that the Merkle root is committed
in a Bitcoin block. Decode it and verify with any OpenTimestamps client — for example:
echo "<ots_base64>" | base64 -d > proof.ots
ots verify --digest <merkle.root> proof.ots
# → "Success! Bitcoin block <height> attests existence as of <date>"The OpenTimestamps verifier checks the proof against the Bitcoin blockchain (via a Bitcoin node or a block explorer) — not against aiternam. The block's timestamp is the trustless "existed no later than" instant.
Conclusion
If all three checks pass, you have established — with zero trust in aiternam — that this exact prediction (Step 1) was part of a batch (Step 2) whose root was committed to Bitcoin at a specific block (Step 3). It could not have been backdated or altered after that block.
Caveats (stated, not hidden)
- Residual-trust window. Between a prediction's creation and the next batch anchor,
immutability still rests on aiternam's database. Anchors run on a short cadence and resolution
horizons are days-to-years, so this window is negligible — but it exists.
anchor_statustells you the current state:unanchored→pending(root submitted to the timestamp calendars) →bitcoin(Bitcoin attestation in hand). - Granularity is the batch, not the second. The trustless claim is "existed no later than the
batch's Bitcoin block," which is looser than aiternam's recorded
created_at. For markets the meaningful fact is "predicted before the outcome," and horizons dwarf the anchor delay. - Backfilled history. Predictions made before anchoring launched were anchored in one initial
batch, so their
anchored_atis later than theircreated_at. Their content hash was still sealed at creation; only the Bitcoin attestation is retroactive, and the bundle shows both times honestly.
> The exact rules above are the same code aiternam runs: `hash_version = 1` lives in
> `common/security.py` and `merkle_version = 1` in `common/merkle.py`. Both are frozen.