The two records
CS2: MW vs LDP, 2026-10-07, Games Total O/U 2.5
Polymarket market 5369848, disputed twice, resolved Under on chain.
- Predge record
- GET /v1/settlement-risk/5369848
- FairSeal receipt
- nr_2e090e4c78df92765ea0b600f298c8dc · JSON
- payload_hash
- d65f90881964b342c3de964b6ee3e6751417229ef4c2d96ee1cb8bf7d792320c
- Snapshot time
2026-10-13T01:37:09Z(record'schecked_at)
Montana Grizzlies vs Idaho Vandals, O/U 151.5
Polymarket market 1137824, college basketball, 2026-01-08.
- Predge record
- GET /v1/settlement-risk/1137824
- FairSeal receipt
- nr_6f99fa85e571dc8e766d014a9da49440 · JSON
- payload_hash
- 11a2bf5505b6d25c3b04c8c0049ba837b6097061d19574e27e44526dd57cf5e2
- Snapshot time
2026-10-13T01:37:12Z(as stated in the receipt)
One anchor for both
- Base tx
- 0xf8ded45d2daa2e83cf490bc20d2e7ea0bbadd3887c30a3114729a9a5e3db683f
- Block
- 52533131 ·
2026-10-13T01:40:09Z - Contract
- 0xEE09671Bbc6B932BF1461CAff360bfDD9bEA55ca
- Merkle root
- a04116161641635e63504d2a892f3a0029b0cc584dd204234ffdcf5e5e5e6e9c
01Check the Predge signature
Fetch a record and check two things: sha256(canonical) equals content_hash, and the ed25519 signature over canonical is valid under the key with kid 13fa3d18a369e6c7 in the published key set. The rule that produces canonical is in the canonicalization spec.
This check runs on the response you fetch, not on the snapshot FairSeal hashed. Settlement-risk records are live: every request re-reads the chain and returns a new checked_at, so canonical and content_hash change on every call. The hash you compute today will not equal the payload_hash in the receipt. That is expected. The exact canonical bytes FairSeal hashed on 13 October are not published.
Node 18+, no packages
// Node 18+, no packages: node predge_check.mjs 5369848
import { createHash, createPublicKey, verify } from "node:crypto";
const id = process.argv[2] ?? "5369848";
const env = (await (await fetch(`https://api.predge.io/v1/settlement-risk/${id}`)).json()).attestation;
const keys = (await (await fetch("https://api.predge.io/.well-known/predge-keys.json")).json()).keys;
const hash = createHash("sha256").update(env.canonical, "utf8").digest("hex");
const pub = createPublicKey({ format: "der", type: "spki",
key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(env.public_key, "hex")]) });
console.log("sha256(canonical) ", hash);
console.log("matches content_hash ", hash === env.content_hash);
console.log("ed25519 signature valid ", verify(null, Buffer.from(env.canonical, "utf8"), pub, Buffer.from(env.signature, "hex")));
console.log("key listed, active, kid ok ", keys.some(k => k.active && k.kid === "13fa3d18a369e6c7" && k.public_key === env.public_key));Python 3.8+, with cryptography
# Python 3.8+, needs `pip install cryptography`: python3 predge_check.py 5369848
import hashlib, json, sys, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
get = lambda url: json.load(urllib.request.urlopen(url, timeout=20))
mid = sys.argv[1] if len(sys.argv) > 1 else "5369848"
env = get(f"https://api.predge.io/v1/settlement-risk/{mid}")["attestation"]
keys = get("https://api.predge.io/.well-known/predge-keys.json")["keys"]
canon = env["canonical"].encode("utf-8")
digest = hashlib.sha256(canon).hexdigest()
print("sha256(canonical) ", digest)
print("matches content_hash ", digest == env["content_hash"])
Ed25519PublicKey.from_public_bytes(bytes.fromhex(env["public_key"])).verify(bytes.fromhex(env["signature"]), canon)
print("ed25519 signature valid True") # verify() raises InvalidSignature otherwise
print("key listed, active, kid ok ", any(k["active"] and k["kid"] == "13fa3d18a369e6c7" and k["public_key"] == env["public_key"] for k in keys))Both print the hash of the response you just fetched, then true for each check (Python prints True):
sha256(canonical) <changes on every call>
matches content_hash true
ed25519 signature valid true
key listed, active, kid ok true
For a full check that also rebuilds canonical from payload and verifies envelope_signature, save a response and run verify-record.mjs or verify_record.py.
02Check the FairSeal receipt
A receipt commits to a hash in three steps, all of which you can recompute:
- Leaf.
leaf = SHA256(receipt_id + "||" + payload_hash), with both values as hex text. - Merkle path. For each sibling
s:node = SHA256(min(node, s) + max(node, s)), sorting the pair as hex text. The last node is the root. - Anchor. The root must appear in the calldata of the anchor transaction on Base, sent to FairSeal's anchor contract. You can see it on Basescan (input data of
anchorBatch).
The script below does all three with the Python standard library and a public Base RPC. It takes the receipt and its Merkle path from the FairSeal API, but the leaf, the root and the on-chain check are computed locally, so a wrong path or hash cannot pass.
Python 3.8+, standard library only
# Python 3.8+, standard library only: python3 fairseal_check.py nr_2e090e4c78df92765ea0b600f298c8dc
import hashlib, json, sys, time, urllib.request
RPC = "https://mainnet.base.org" # any public Base mainnet RPC works
CONTRACT = "0xee09671bbc6b932bf1461caff360bfdd9bea55ca"
EXPECTED = { # the payload hashes listed on this page
"nr_2e090e4c78df92765ea0b600f298c8dc": "d65f90881964b342c3de964b6ee3e6751417229ef4c2d96ee1cb8bf7d792320c",
"nr_6f99fa85e571dc8e766d014a9da49440": "11a2bf5505b6d25c3b04c8c0049ba837b6097061d19574e27e44526dd57cf5e2",
}
sha = lambda s: hashlib.sha256(s.encode()).hexdigest() # inputs are hex strings, hashed as text
def rpc(method, *params):
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": list(params)}).encode()
req = urllib.request.Request(RPC, body, {"Content-Type": "application/json", "User-Agent": "verify"})
return json.load(urllib.request.urlopen(req, timeout=20))["result"]
rid = sys.argv[1] if len(sys.argv) > 1 else "nr_2e090e4c78df92765ea0b600f298c8dc"
r = json.load(urllib.request.urlopen(f"https://api.fairseal.io/v1/notarize/{rid}", timeout=20))
p = r["proof"]
node = leaf = sha(rid + "||" + r["payload_hash"])
for sib in p["merkle_path"]:
node = sha(min(node, sib) + max(node, sib))
tx = rpc("eth_getTransactionByHash", p["anchor_tx"])
ok = rpc("eth_getTransactionReceipt", p["anchor_tx"])["status"] == "0x1"
ts = int(rpc("eth_getBlockByNumber", tx["blockNumber"], False)["timestamp"], 16)
print("payload_hash as listed here ", r["payload_hash"] == EXPECTED.get(rid))
print("leaf recomputed ", leaf == p["leaf_hash"])
print("root recomputed ", node == p["merkle_root"], node[:16] + "...")
print("tx to FairSeal contract, ok ", tx["to"].lower() == CONTRACT and ok)
print("root in tx calldata ", node in tx["input"].lower())
print("anchored in block", int(tx["blockNumber"], 16), "at", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)))Output for either receipt:
payload_hash as listed here True
leaf recomputed True
root recomputed True a04116161641635e...
tx to FairSeal contract, ok True
root in tx calldata True
anchored in block 52533131 at 2026-10-13T01:40:09Z
If your Python reports CERTIFICATE_VERIFY_FAILED (common with the python.org installer on macOS), run its Install Certificates.command or set SSL_CERT_FILE=/etc/ssl/cert.pem. FairSeal also documents a second route: call getBatchRoot(batch_id) on the contract and compare it with 0x + root.
03What each proof shows, and what it doesn't
Predge signature
- The
canonicalstring you fetched was signed by the key Predge publishes as active (kid13fa3d18a369e6c7). - Changing any signed byte makes the check fail.
- It does not show the facts are correct. The chain facts in a record (UMA state, dispute transactions, on-chain resolution) can be re-checked on any Polygon RPC;
risk_level,recommendationand the statistics are Predge's reading and computation. - It does not show when the record was signed.
checked_atis a time Predge states inside the signed payload, not an independent timestamp. - It says nothing about the 13 October snapshots: the bytes you fetch today are different.
- Fields outside
attestation.payload(for example the top-level market title) are display data and are not covered bysignature.
FairSeal receipt
- The receipt's
payload_hash, bound to its receipt id, is a leaf under a Merkle root that a successful transaction sent to FairSeal's anchor contract on Base in block 52533131. - So that hash existed no later than
2026-10-13T01:40:09Z, and the receipt cannot be changed or backdated without breaking the proof.
- It does not show which bytes the hash came from. Without the snapshot's canonical string, nobody can check that the hash is of a Predge-signed record or what that record said. That link rests on FairSeal's statement in the receipt.
- It does not show the record's content was correct, or that it did not exist earlier.
Taken together. The two proofs are independent today. One shows that Predge signs these records with its published key. The other shows that two specific hashes were fixed on Base by 01:40:09 UTC on 13 October 2026. If a snapshot's canonical string is ever disclosed, anyone can close the gap: check that its sha256 equals the receipt's payload_hash and that the Predge signature over it is valid. That would show Predge signed that exact content no later than the anchor time.