The ledger records each AI call the day it is asked and shows it only once its 30-day window has closed and it has been scored. That leaves a question the scoreboard cannot answer by itself: was any call changed after the price moved? Each ask day’s calls are sealed into a SHA-256 digest when they are stored, and the digest is published here while the calls stay hidden. Every digest includes the one before it, so changing any stored call would change its own day’s digest and every digest after it.
The chain is sealed by the ledger’s daily run, and that run has not sealed a link yet. The first run seals every call already stored.
curl -s https://stocktools.ai/api/ai-answer-ledger/chain > chain-saved.json. A public web archive snapshot of this page works as well.node verify-ledger-chain.mjs chain-saved.json.// verify-ledger-chain.mjs run: node verify-ledger-chain.mjs [your-saved-copy.json]
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const chain = await (await fetch('https://stocktools.ai/api/ai-answer-ledger/chain')).json();
const saved = process.argv[2] ? JSON.parse(readFileSync(process.argv[2], 'utf8')) : { links: [] };
const before = new Map(saved.links.map((l) => [l.seq, l.digest]));
let prev = chain.genesis;
for (const l of chain.links) {
const linked = l.prevDigest === prev ? 'linked' : 'BROKEN LINK';
let rows = 'hidden until scored';
if (l.salt) {
const d = createHash('sha256').update(`${l.prevDigest}\n${l.salt}\n${JSON.stringify(l.rows)}`).digest('hex');
rows = d === l.digest ? 'rows hash to the digest' : 'ROWS DO NOT MATCH';
}
const kept = !before.has(l.seq) ? 'not in your copy' : before.get(l.seq) === l.digest ? 'same as your copy' : 'CHANGED since your copy';
console.log(l.seq, l.day, linked, rows, kept);
prev = l.digest;
}For each link: sha256(previous digest + "\n" + salt + "\n" + rows), written as lowercase hex. The rows are that day’s calls sorted by id, each with these fields in this order, as JSON with no spaces: id, ticker, model, promptVersion, askedOn, dueOn, call, reason, priceAtAsk, askSession. In Python, json.dumps(rows, separators=(',', ':'), ensure_ascii=False) gives the same text from the JSON’s rows. The first link’s previous digest is 64 zeros. These are the fields recorded when a call is asked. The fields written at scoring time (the closing price on the due date, the move and the result) are left out, because they come from the outcome and cannot be sealed before it.
Why a salt: every field except the call is guessable (ten known tickers, the model, the dates), and a day has at most 310, about 59,000, combinations of calls. Without a secret ingredient, anyone could hash every combination and read the hidden calls off the published digest. Each link mixes in 32 random bytes that stay private until its calls are shown, and are published beside them then.
What this does not prove: that a call was never skipped. A day can hold fewer than ten calls when a quote or an answer could not be read, and the count of each link is published so a short day is visible. The chain is stored by us, so its protection comes from copies other people keep; the check above is only as strong as the oldest copy you compare against.
Keep going: the AI Answer Ledger · the settlement referee · methodology