Receipts
Every event carries a hash chained per source and signed in checkpoints. Verify what Langbot emitted, and when, in the browser, in code or offline.
A receipt is a fact about a fact. Every event the feed emits carries a content hash chained to the previous event from the same source, and the feed signs each source's chain head with an Ed25519 key it publishes. Anyone holding an event, even a redacted one, can recompute its hash, check it against the chain and the signed checkpoint, and prove that Langbot emitted exactly that fact at exactly that time. Receipts never carry speech beyond what the rights filter allows: the raw text enters only as raw_hash.
Try it without code at langbot.ai/verify.
The four fields
Every event (CONTRACTS §1) carries four receipt fields, set by the feed after t_emitted and before fan-out. They are null until the feed has seen the event.
| field | meaning |
|---|---|
hash | sha256 of the canonical fact, including prev_hash |
prev_hash | hash of the previous event the feed emitted for the same source_id; null for the first |
raw_hash | sha256 of raw; survives redaction and retention; null when raw is null |
chain_seq | 1-based position in the source's chain, in feed emission order |
How the hash is built
canonical(obj) is JSON with keys sorted by code point, no whitespace, non-ASCII escaped as \uXXXX with lowercase hex, floats rounded to 4 decimal places and written as integers when integral. In Python it is json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) after the numeric normalisation; in TypeScript it is receipts.canonical.
raw_hash = sha256(utf8(raw)) # null when raw is nullhash = sha256(utf8(canonical({ id, type, source_id, event_id, market_refs, phrase, raw_hash, speaker, confidence, t_spoken, t_detected, t_emitted, lag_vs_public_ms, source_kind, seq, prev_hash })))All 16 keys are always present (null when unset). speaker is null or exactly {label, name, confidence}. raw itself is never hashed directly, so a redacted public event (raw replaced by the phrase) and a retention-swept row (raw nulled) verify with the same hash. A licensed holder of raw can additionally check sha256(raw) == raw_hash.
Checkpoints
Every 30 seconds, and immediately on event_ended, the feed signs the chain head of every source that advanced:
canonical({"v":1, "source_id", "event_id", "chain_seq", "hash", "count", "created_at", "key_id"})The signature is Ed25519 over the UTF-8 bytes of that string, base64. The public key is the raw 32 bytes, base64; key_id is the first 16 hex characters of sha256(public_key). The key is served at GET /v1/receipts/key and GET /.well-known/langbot-receipts.json. A rotation produces a new key_id; old checkpoints keep theirs and the key endpoint lists previous.
Endpoints
| method | path | auth | returns |
|---|---|---|---|
GET | /v1/receipts/key | public | {key_id, algorithm:"ed25519", public_key, hash:"sha256", canonical, v:1, previous:[...]} |
GET | /v1/receipts/{evt_id} | public, plan delay | {event, chain:{source_id, chain_seq, prev_hash}, checkpoint, rights, verified} |
POST | /v1/receipts/verify | public, rate-limited | {results:[{id, status, computed_hash, stored_hash, chain_ok, chain_seq, checkpoint}]} for up to 500 events |
GET | /v1/receipts/checkpoints?source_id=&event_id=&limit= | public | {checkpoints:[...]} newest first |
GET | /v1/events/{event_id}/replay?format=json|jsonl&books=0|1&mine=0|1&since_seq=&source_id=&types= | token | the replay bundle |
POST | /v1/receipts/attest | token | the signed attestation row |
GET | /v1/receipts/attestations?event_id=&evt_id=&since=&limit= | token, owner only | {attestations:[...]} |
Lookup, batch verify, checkpoints and the key are public. Replay and attestations need a token. Every plan, Free included, can replay any recorded event (a replay counts one history query of the plan's daily allowance); the plan delay and the rights filter apply to every surface.
Replay bundles
GET /v1/events/{event_id}/replay returns everything an auditor or a backtest needs in one file:
{ "manifest": { "v": 1, "event_id": "ev_nvda_q3_2026", "generated_at": 1760054000000, "delay_ms": 60000, "rights": "restricted", "key": { "key_id": "56475aa75463474c", "public_key": "A6EHv/…" }, "sources": [ { "source_id": "src_nvda_q3_2026_dialin", "source_kind": "sip", "first_chain_seq": 1, "last_chain_seq": 412, "head_hash": "5d0e05c3…", "events": 412 } ], "filters": { "since_seq": null, "source_id": null, "types": null } }, "events": [ "… ordered by (source_id, chain_seq) …" ], "checkpoints": [ "… for these sources, within the range …" ], "books": [ { "ticker": "KXEARNINGSMENTIONNVDA-26OCT-BLACKWELL", "ts": 1760050123000, "yes_bid": 58, "yes_ask": 60, "book": {} } ], "attestations": [ "… yours, when mine=1 …" ]}format=jsonl writes one object per line with a kind member (manifest first, then event, checkpoint, book, attestation) and the header Content-Disposition: attachment; filename="<event_id>.replay.jsonl". The dashboard's Download replay button on an event produces this file; the Receipts page verifies one in the browser.
Verify
Curl:
# Public: lookup, batch verify, the signing keycurl https://api.langbot.ai/v1/receipts/evt_01K7…curl -X POST https://api.langbot.ai/v1/receipts/verify -H "Content-Type: application/json" \ -d '{"events":[{"id":"evt_01K7…", "type":"phrase_detected", …}]}'curl https://api.langbot.ai/v1/receipts/key # Token: replay bundle and attestationscurl -H "Authorization: Bearer lb_..." \ "https://api.langbot.ai/v1/events/ev_nvda_q3_2026/replay?format=jsonl&books=1&mine=1" -o ev_nvda_q3_2026.replay.jsonlPython (langbot.receipts has no network dependency; LangbotClient calls the feed):
from langbot import LangbotClient, receipts lb = LangbotClient(token="lb_...") assert receipts.event_hash(evt) == evt.hash # local, no network r = lb.receipt(evt.id) # GET /v1/receipts/{id}key = lb.receipts_key()assert receipts.verify_checkpoint(r.checkpoint, key.public_key) bundle = lb.replay("ev_nvda_q3_2026", books=True, mine=True)report = receipts.verify_bundle(bundle) # hashes, chain links, signatureslb.attest(evt.id, hash=evt.hash, action={"side": "yes", "count": 10})TypeScript:
import { Langbot, receipts } from "@langbot/sdk"; const lb = new Langbot({ token: process.env.LANGBOT_API_TOKEN }); const computed = await receipts.eventHash(evt);const r = await lb.receipt(evt.id);console.log(computed === r.event.hash ? "match" : "mismatch"); const bundle = await lb.replay("ev_nvda_q3_2026", { books: true, mine: true });const report = await receipts.verifyBundle(bundle);console.log(report.events, report.chain, report.checkpoints);CLI:
langbot receipt evt_01K7… # hash, chain position, nearest checkpointlangbot verify ev_nvda_q3_2026.replay.jsonl # every hash, link and signature, offlinelangbot replay ev_nvda_q3_2026 --books --mine > ev_nvda_q3_2026.replay.jsonllangbot attest evt_01K7… --action '{"side":"yes","count":10}'Attestations
An attestation is your side of the record: a signed statement that you acted on a fact. POST /v1/receipts/attest takes {evt_id, hash?, action, t_acted?}; the feed checks that the event exists (404) and that hash, when given, matches (409), then stores and signs:
canonical({"v":1, "id", "user_id", "evt_id", "event_hash", "action_hash", "t_acted", "created_at", "key_id"})with action_hash = sha256(canonical(action)). Some rules:
actionis an object of at most 4 KiB. It is opaque to Langbot, stored for you, and returned only to you. It is never read as a signal and never aggregated.- It must not contain venue credentials. The SDKs strip keys that look like secrets (
api_key,secret,token,private_key,password) before sending. t_acteddefaults tocreated_at. Your attestations are listed atGET /v1/receipts/attestationsand in the dashboard under Receipts, where the signatures are checked in the browser.
Test vector
Two events from src_demo; every implementation (feed, langbot.receipts, @langbot/sdk, the web verifier) reproduces these values in its tests.
event 1: event_started, id evt_01K7Q0000000000000000000A1, raw '{"status":"live"}', t_spoken 1760050100000, t_detected 1760050100020, t_emitted 1760050100025, seq 0, sip, everything else null/[] raw_hash 290f663cc2ed7fec14d831023497265da83974028929bc73c4e9711e949c8780 hash b6e2623266cb2610b807c6702adaa5e933d6100a4ba40527687d4b25d46dfcf5 (prev_hash null)event 2: phrase_detected, id evt_01K7Q0000000000000000000A2, market_refs ["KXEARNINGSMENTIONNVDA-26OCT-BLACKWELL"], phrase "blackwell", raw "Blackwell ramp is ahead of plan — café", speaker {label "SPK_1", name "Jensen Huang", confidence 0.91}, confidence 1.0, t_spoken 1760050123412, t_detected 1760050123488, t_emitted 1760050123491, lag_vs_public_ms 18740, seq 1, sip raw_hash cd3e6e94eacccde5bc85f6d2220fc3ae224ff44742c7703409294cf8699aa58a hash 5d0e05c3b0d59c99b42955d3212ef9ac261c61398f3e2bec970debdb3efd02e8 (prev_hash = hash of event 1)checkpoint with seed base64 AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= (bytes 0..31): public_key A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg= key_id 56475aa75463474c message {"chain_seq":2,"count":2,"created_at":1760050124000,"event_id":"ev_demo","hash":"5d0e05c3…","key_id":"56475aa75463474c","source_id":"src_demo","v":1} signature cEf385INp3hAoHb38pVECtt5GHXtp4YHFErEFAfCWLyJ+FUjGS8hfe4vN3T0HPI1guw9oh4DdU0LkqiVSq71Bw==The canonical string of event 2, the exact bytes that are hashed:
{"confidence":1,"event_id":"ev_demo","id":"evt_01K7Q0000000000000000000A2","lag_vs_public_ms":18740,"market_refs":["KXEARNINGSMENTIONNVDA-26OCT-BLACKWELL"],"phrase":"blackwell","prev_hash":"b6e2623266cb2610b807c6702adaa5e933d6100a4ba40527687d4b25d46dfcf5","raw_hash":"cd3e6e94eacccde5bc85f6d2220fc3ae224ff44742c7703409294cf8699aa58a","seq":1,"source_id":"src_demo","source_kind":"sip","speaker":{"confidence":0.91,"label":"SPK_1","name":"Jensen Huang"},"t_detected":1760050123488,"t_emitted":1760050123491,"t_spoken":1760050123412,"type":"phrase_detected"}