How to verify our records
When we read a signal from a channel we write down exactly what it said, in a fixed byte format, and hash it. Once a day every new snapshot goes into a Merkle tree and the root of that tree — 32 bytes — is written to a contract in Arbitrum. From then on, two things stop being possible without it showing: editing what a signal said, and quietly removing one from a day that was already anchored.
New to this? Read the plain-English version — what the anchor proves, what it does not, and the same check in five steps.
Batch 20260923 — 106 snapshots on Arbitrum One, written Sep 23, 2026. Transaction
What this proves, and what it does not
An anchor proves that a specific set of bytes was in our database no later than the moment its root was written, and that a particular signal was one of a fixed, published set for that day. Timestamping and non-removal. Nothing else.
It does not prove a channel published that text — we wrote the snapshot down, and the chain only witnessed when. What connects a snapshot to the live message is a separate, ordinary check: for 90 days after publication we re-read source messages and compare them with the snapshot we stored, and every channel page carries the count of its signals whose text changed or whose message is gone. Nor does an anchor say a trade was any good; that is the replayed track record, and it is measured, not witnessed.
And it proves nothing about signals we recorded before this format existed. Those carry an older digest that does not cover the message text at all, and their pages say so rather than implying a proof we cannot give.
Checking one signal, in four steps
- 1. Open a signal's verification page — every anchored signal on a channel page links to one. It shows the snapshot bytes, their SHA256, and the branch of the tree above them.
- 2. Hash the bytes yourself: sha256(payload). That digest is the leaf. Do not take a digest from us and check that — hash the payload, always. A tree can be folded from any 32 bytes, so the only thing that ties a proof to a real signal is that its leaf came from readable bytes you hashed.
- 3. Fold the leaf with each sibling in the branch, in order, and you get the day's root.
- 4. Ask the contract what root it holds for that day — roots(batchId), one call, no account anywhere — and compare. Equal means the bytes existed when that transaction was mined.
A batch page lists every leaf of its day, so the whole root can be rebuilt rather than one branch: that is the check that catches a signal having been removed.
Which anchors are ours
One private key signs these transactions, and it lives on our server. If it ever leaked, whoever took it could write roots into our contract from our address — so "a transaction from our address" cannot be what makes an anchor count. Below is every contract and address we anchored with, and when. A transaction outside these periods is not treated as proof on this site, including one of ours, and the list changes only by a commit to our source.
| Period | Contract | Written by |
|---|---|---|
|
2026-08-29 – 2026-08-29
Development contract (Arbitrum One) · development
|
0xbb508f97f1e918b74d34fb6c1be1260e2ce12ee0 | 0x57c89be59322a75e3b7c116d9a2e7b0332a257d1 |
|
2026-08-29 – current
Production contract (Arbitrum One)
|
0xceacc40cf7b7c797d990dce7164fcccd87dad693 | 0x57c89be59322a75e3b7c116d9a2e7b0332a257d1 |
The snapshot format, byte for byte
Printed from the module that produces the bytes, so this page cannot drift from what we hash.
The payload is a single UTF-8 encoded JSON object, serialised as:
json.dumps(obj, sort_keys=True, ensure_ascii=False,
separators=(',', ':'), allow_nan=False)
so: keys sorted by code point, no whitespace anywhere, non-ASCII characters
(emoji, Cyrillic, CJK) written literally rather than escaped, and no trailing
newline. The bytes are the UTF-8 encoding of that string, with no Unicode
normalisation of any kind — a channel's text is hashed as it arrived.
Keys and their value rules:
===================== =========================================================
account_id int — our `SocialAccount.pk`
channel str — the channel's handle without the @, as it read
at snapshot time. Present next to `account_id` because a
stranger cannot resolve our primary key to a channel, and
a proof they have to take our word to read is half a proof
direction str — "long" / "short" / "unknown"
entry str or null — see the Decimal rule below. Null when the
channel published no entry price: either it said "enter at
market" (`entry_is_market`) or it published no entry at
all. The two are different and only `entry_is_market`
tells them apart
entry_is_market bool — the channel said "enter at market"
leverage int or null
origin str — "captured" when the snapshot was taken as the
message was read, "rebuilt" when it was reconstructed
later from our database (the retroactive backfill of
pre-v2 history). Inside the hashed bytes on purpose: a
reconstruction that could pass for a capture is the exact
lie this feature exists to make impossible
published_at str — UTC, always %Y-%m-%dT%H:%M:%S.%fZ (microseconds
always present, always the Z suffix)
raw_text str — the source message verbatim, inside the JSON, so
newlines and emoji survive a separator-free format
source_message_id int — Telegram's message id in that channel
stop_loss str or null — price stop
stop_loss_pct str or null — percentage stop, for channels that publish
"Stop: 5%" instead of a price
symbol str — as stored on the row (truncated to 20 chars)
targets list of str — the full published ladder, in order
version int — always 2; domain separation between versions
===================== =========================================================
Decimal rule: every decimal value is a JSON *string*, produced by
str(Decimal) with no normalisation — "1.50" and "1.5" are
different payloads, because they are different things the channel wrote.
Numbers are never emitted as JSON numbers: a float would round a price.
What the payload is not. It is a snapshot of what the channel published
and what we extracted from it at that moment — not of our later opinion about
the trade. Outcomes, scores, `mask_name` and `levels_consistent` are excluded:
they change (a mask gets fixed, `resolve_signals --requeue` reruns), and an
anchor over values that legitimately change would be an anchor that stops
verifying for honest data. `raw_text` is in the payload, so anyone can re-run
any extraction against it themselves.
Reproducibility caveat, deliberate. The payload records the values as they
were at import. The database may store them slightly differently — Postgres
quantises `entry_price` to 8 decimal places, so a row can read back as
0.00012340 where the payload says 0.0001234 — and for a market-entry
signal `entry_price` is overwritten by the resolver outright. Hence the rule
above: verify against the stored payload, never against a payload rebuilt from
the row.
The tree, byte for byte
Same rule: this is the specification the code carries, not a description of it.
1. Leaves. One leaf per signal, the 32 raw bytes of its `content_hash`
(`sha256` of the v2 payload) — the hex string decoded, *not* re-hashed and
not the hex text. A reader who computed `sha256(payload)` themselves holds
a leaf already.
2. Order. By our `Signal.pk` ascending. The order is not derivable from
the leaves themselves, so the batch page publishes the ordered list; the
root pins it.
3. Pairing. Left to right, two at a time:
parent = sha256(left_32_bytes || right_32_bytes)
concatenated raw bytes, no separator, no length prefix, no hex.
4. The odd one out is promoted, never duplicated. A level with an odd
number of nodes carries its last node up to the next level unchanged.
Duplicating it instead — Bitcoin's rule — is the CVE-2012-2459 class of
bug: two different leaf lists (…, X) and (…, X, X) then produce the same
root, so "the batch contained exactly these signals" stops being a
statement the root can support.
5. A single leaf is its own root. No self-pairing, by rule 4.
6. An empty set has no root. `merkle_root([])` raises. Zero anchored to a
contract is indistinguishable from "never anchored", and a day with no new
signals simply gets no batch — an empty day is not an error and not a
transaction.
7. No domain separation between leaves and internal nodes, deliberately —
and the rule that makes it safe is rule 7a, not an accident.
Without separation an internal node is a well-formed "leaf": hand a
verifier the digest `sha256(A||B)` together with the siblings above it and
the arithmetic works out to the root, because the arithmetic cannot tell
which level a 32-byte value came from. Nothing about our payloads prevents
that, and an earlier version of this docstring claimed otherwise.
7a. A leaf is only ever established by hashing a published payload.
The claim we make in public is "this snapshot was in the batch", never
"this digest was in the tree" — so the reader's first step is always
`sha256(payload)`, which is why `verify_payload` below is the entry point
the verify page uses and `verify_proof` is the low-level half. Forging a
membership claim therefore needs a *preimage* of an internal node: a
canonical v2 payload (a JSON object starting with {"account_id": and
carrying the channel, the message id and the message text) that hashes to
`sha256(A||B)`. That is a second-preimage attack on SHA256, which is the
assumption the whole feature already rests on.
Separation is the belt to that braces, and it is left off so a leaf stays
exactly what a reader computes with three lines of Python — no prefix byte
to get wrong, no way to "almost" verify. The moment a leaf comes from
anywhere other than a v2 payload, or a page starts accepting a digest
typed in by the reader, this reasoning expires and separation has to be
added — which changes every root, so it would be a new tree version rather
than a patch.
8. Hex is lowercase and roots are `0x`-prefixed; leaves are stored bare,
as `content_hash` is. Both spellings are accepted on the way in.