Skip to main content

Verify a hash

Every sealed forecast is hash-logged before reveal. After reveal you can prove the plaintext matches what was committed.

What gets hashed​

  1. Payload hash — SHA-256 of canonical JSON of the forecast payload (sorted object keys, no whitespace variance).
  2. Log entry hash — SHA-256 of canonical JSON of { seq, predictionId, feedVersionId, payloadHash, prevHash, serverTimestamp }, chained via prevHash (genesis previous hash is 64 zero hex digits).

Daily head + chain integrity: GET /v1/log/head.

Server-side check​

GET /v1/predictions/:id (API key) returns:

{
"prediction": { "payload": { "type": "probability", "value": 0.61 }, "payloadHash": "…", "status": "revealed" },
"localHashVerification": true,
"disclaimer": "…"
}

localHashVerification is true when hash(payload) === payloadHash, false on mismatch, or null while still sealed (no plaintext).

curl​

# 1) Fetch revealed prediction
curl -sS "$API/v1/predictions/<id>" \
-H "authorization: Bearer $NEWSIGNAL_BUYER_KEY" | tee pred.json

# 2) Inspect localHashVerification
jq '.localHashVerification, .prediction.payloadHash, .prediction.payload' pred.json

# 3) Commit-log head + chain integrity
curl -sS "$API/v1/log/head" | jq '.integrity, .head'

Python — recompute payload hash​

import hashlib, json, os, urllib.request

API = os.environ.get("NEWSIGNAL_API_URL", "http://localhost:8787")
KEY = os.environ["NEWSIGNAL_BUYER_KEY"]

def canonicalize(value):
"""Match packages/core canonicalize: sorted keys, compact JSON."""
def sort_value(v):
if v is None or not isinstance(v, (dict, list)):
return v
if isinstance(v, list):
return [sort_value(x) for x in v]
return {k: sort_value(v[k]) for k in sorted(v)}
return json.dumps(sort_value(value), separators=(",", ":"), ensure_ascii=False)

def hash_payload(payload) -> str:
return hashlib.sha256(canonicalize(payload).encode("utf-8")).hexdigest()

def get_prediction(prediction_id: str):
req = urllib.request.Request(
f"{API}/v1/predictions/{prediction_id}",
headers={"authorization": f"Bearer {KEY}"},
)
with urllib.request.urlopen(req) as r:
return json.load(r)

body = get_prediction("<id>")
pred = body["prediction"]
assert pred.get("payload") is not None, "still sealed — wait for revealAt"
recomputed = hash_payload(pred["payload"])
print("payloadHash ", pred["payloadHash"])
print("recomputed ", recomputed)
print("match ", recomputed == pred["payloadHash"])
print("server verified ", body["localHashVerification"])

head = json.load(urllib.request.urlopen(f"{API}/v1/log/head"))
print("chain integrity ", head["integrity"])

TypeScript — recompute payload hash​

import { createHash } from 'node:crypto';

const API = process.env.NEWSIGNAL_API_URL ?? 'http://localhost:8787';
const KEY = process.env.NEWSIGNAL_BUYER_KEY;

function canonicalize(value: unknown): string {
const sortValue = (v: unknown): unknown => {
if (v === null || typeof v !== 'object') return v;
if (Array.isArray(v)) return v.map(sortValue);
const obj = v as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const key of Object.keys(obj).sort()) out[key] = sortValue(obj[key]);
return out;
};
return JSON.stringify(sortValue(value));
}

function hashPayload(payload: unknown): string {
return createHash('sha256').update(canonicalize(payload)).digest('hex');
}

const res = await fetch(`${API}/v1/predictions/${predictionId}`, {
headers: { authorization: `Bearer ${KEY}` },
});
const body = await res.json();
const pred = body.prediction;
if (!pred.payload) throw new Error('still sealed');

const recomputed = hashPayload(pred.payload);
console.log({
payloadHash: pred.payloadHash,
recomputed,
match: recomputed === pred.payloadHash,
localHashVerification: body.localHashVerification,
});

const head = await fetch(`${API}/v1/log/head`).then((r) => r.json());
console.log(head.integrity);

Or use @newsignal/core in-repo:

import { hashPayload } from '@newsignal/core';
// hashPayload(pred.payload) === pred.payloadHash

Log entry chain (advanced)​

Each append-only entry binds payloadHash into a previous-hash chain. Recompute an entry hash the same way (canonical JSON of the six fields above). GET /v1/log/head returns { ok: true } from verifyChain() when the stored chain is intact.