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
- Payload hash — SHA-256 of canonical JSON of the forecast payload (sorted object keys, no whitespace variance).
- Log entry hash — SHA-256 of canonical JSON of
{ seq, predictionId, feedVersionId, payloadHash, prevHash, serverTimestamp }, chained viaprevHash(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.