Skip to content

Verify an anchor

Once a day DEBYKO posts one number to a contract on Base: the root of a Merkle tree over every row of history it stored for a UTC day. A week later a row of that day can be shown to be under that number, by anyone, with a few lines of code and the chain, without trusting DEBYKO’s word for it.

The anchor proves the data did not change after the anchor time; it proves nothing about its correctness at collection.

Status: deployed, no anchor posted yet. The contract DebykoAnchor (MIT) is on Base mainnet at 0x0AF8259DBfC613825718d4d49A05F9F6fdA15086 since 2026-10-02 (block 52071448, transaction 0xf518ef40…), owner debyko.base.eth, source verified on BaseScan. Days are computed daily; the first anchor waits for the owner’s word after a check of the history it would fix. Until then GET /v2/anchors/{day} answers ANCHOR_NOT_FOUND.

For each UTC day D, the rows of these tables with a time in D: ticker, mark, funding, open interest, stats, depth-derived, trades, liquidations and candles. A day is anchored at D + 7, at 02:00 UTC or later, once the Writer’s window for it has settled (the same rule that makes an answer about the past settled); never before, so that a correction found in the week after a day is made before it is fixed on a chain. After it is anchored the day is final: a correction of an anchored day is recorded as a new row, never as an edit, and GET /v2/anchors/proof answers ANCHOR_CHANGED for a row whose part of the day no longer matches. Rows that retention has dropped are not in the proofs any more; the day’s root stays on the chain.

  1. A row is written as the RFC 8785 (JSON Canonicalization Scheme) text of an object with the listing (segment, symbol) and every anchored column of the table. Members are sorted, there is no whitespace, strings are escaped as JSON.stringify does. Every number is a string, the digits PostgreSQL stores with the zeros after the decimal point that carry no value dropped (100.500 is "100.5", 2.000 is "2"); integers (a sequence number passes 2^53) are strings too; a time is yyyy-MM-ddTHH:mm:ss.ffffffZ, UTC; a boolean is true or false; an empty value is null. The proof carries the text that was hashed (canonical).
  2. A leaf is SHA-256(0x00 ‖ canonical row). A node is SHA-256(0x01 ‖ left ‖ right). The tree over n leaves splits at the largest power of two below n, nothing is duplicated (RFC 6962 section 2.1, the tree Certificate Transparency uses; index and rows in a proof are a leaf’s place in it).
  3. A partition is one table’s rows of the day, in a fixed order (the cold class, then the listing, the time and the rest of the table’s key, text by bytes), and its root is the root of the tree over their leaves.
  4. The day’s tree has one leaf per partition that has rows, in table-name order: SHA-256(0x00 ‖ text) where text is the canonical form of {"day", "root", "rows", "schema_version", "table"} (the partition’s root in hex). The day root is the root of that tree. schema_version is the form of all of the above; a root is reproducible only under the version it was made under.
  5. The contract DebykoAnchor (MIT, contracts/anchor/ in the repository) stores the day root under yyyymmdd (20261001) and emits Anchored(day, root, ts). Only the owner address can post, a day can be posted once, and roots(day) is public. The transaction that posts a day carries the owner’s Base Builder Code (bc_girn7if9) as an ERC-8021 data suffix after the call’s arguments, which the contract ignores.

GET /v2/anchors/proof?venue=&symbol=&layer=&at= (a key; counts as a history request) answers with the row, the text that was hashed, the leaf, and two audit paths: path takes the leaf, as leaf index of rows, to partition.root; partition.path takes the partition’s leaf, as leaf partition.index of partition.count, to day_root. GET /v2/anchors/{day} (public) answers with the day root, its partitions and the transaction that posted it. A real answer is an object like this one (an example, not market data: its row is made up, and so are the contract and the transaction):

{
"evaluated_at": "2026-10-02T09:00:00Z",
"day": "2026-09-24",
"network": "base-sepolia",
"schema_version": 1,
"layer": "trade",
"table": "trade",
"row": {
"event_time": "2026-09-24T00:00:04.000000Z",
"generation": "1",
"origin": "ws",
"price": "100.5",
"received_at": "2026-09-24T00:00:04.100000Z",
"segment": "binance-usdm",
"sequence": "4",
"side": "buy",
"size": "0.25",
"symbol": "BTCUSDT",
"venue_uid": "u-4"
},
"canonical": "{\"event_time\":\"2026-09-24T00:00:04.000000Z\",\"generation\":\"1\",\"origin\":\"ws\",\"price\":\"100.5\",\"received_at\":\"2026-09-24T00:00:04.100000Z\",\"segment\":\"binance-usdm\",\"sequence\":\"4\",\"side\":\"buy\",\"size\":\"0.25\",\"symbol\":\"BTCUSDT\",\"venue_uid\":\"u-4\"}",
"leaf_hash": "60c4ea47d6b5f1faeffe03cf321ddd4a20bb785f3db49172c15ff25587142e0f",
"index": 4,
"rows": 7,
"path": [
"87122b194b1f4379cc9545d7feff22b8da54f8b3fdf618e4bb863f95cec297f3",
"094ca79a6712e8a3388a9d399033904f16c01e144d13af5e0435f8fbdd49311d",
"a31afce4571f2d12a5d8325f812f56e8471803c507fcfd51a9cd9a911b9ede1b"
],
"partition": {
"root": "abac80dbad67260c3ef16428f618398f78e321b1cd396f80d035c0d8890c4e2d",
"leaf": "22efd614b937beba880beb7dff69da48e3fdd066ed582576d485834f1e9b9a01",
"index": 1,
"count": 2,
"path": [
"971a078eff0b2f39ef5e869dcbac4145d1b4f636cea71e17f27ab4c1fa0667ea"
]
},
"day_root": "868c3a277f83080566093d9f46bb61717a82f808958e3a51fe50fa1f50ced4a8",
"anchor": {
"anchored": true,
"status": "anchored",
"contract": "0x1111111111111111111111111111111111111111",
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"block": 4242,
"anchored_at": "2026-10-01T02:05:00Z",
"anchored_lag_days": 7,
"explorer_url": "https://sepolia.basescan.org/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"verify": "https://docs.debyko.com/attest/verify/"
}

To verify one:

  1. Re-serialise row as RFC 8785 and check it is canonical. Hash canonical as above: that is leaf_hash.
  2. Walk path from the leaf to partition.root (RFC 9162 section 2.1.3.2). Rebuild the partition’s leaf from day, partition.root, rows, schema_version and table, and walk partition.path to day_root.
  3. Compare day_root with the chain: eth_call of roots(uint32) (selector 0x081dc681) with the day as yyyymmdd, on the contract named in anchor.contract, on the network named in network. If the numbers are equal, the row was in DEBYKO’s history on the day the transaction (anchor.tx_hash, in block anchor.block) was mined, and is unchanged.

Standard library only, about forty lines. python3 verify.py proof.json --rpc https://mainnet.base.org --contract 0x0AF8259DBfC613825718d4d49A05F9F6fdA15086 (or --root <hex> to compare with a root you hold; without either it checks the proof against the day root inside it).

#!/usr/bin/env python3
"""Verifies a DEBYKO anchor proof (GET /v2/anchors/proof) with the standard library alone.
python3 verify.py proof.json checks the proof against the day root inside it
python3 verify.py proof.json --root <hex> ... and against a root you got elsewhere (the chain, GET /v2/anchors/{day})
python3 verify.py proof.json --rpc https://mainnet.base.org --contract 0x0AF8259DBfC613825718d4d49A05F9F6fdA15086 asks the chain itself
Exit 0 and "valid" when every step holds; exit 1 and the step that failed otherwise."""
import hashlib, json, sys, urllib.request
def h(*parts): return hashlib.sha256(b"".join(parts)).digest()
def walk(leaf, index, size, path):
"""RFC 9162 section 2.1.3.2: the root an audit path takes `leaf` (index `index` of `size`) to."""
fn, sn, r = index, size - 1, leaf
for p in map(bytes.fromhex, path):
if sn == 0: return b""
if fn & 1 or fn == sn:
r = h(b"\x01", p, r)
while not fn & 1 and fn: fn, sn = fn >> 1, sn >> 1
else:
r = h(b"\x01", r, p)
fn, sn = fn >> 1, sn >> 1
return r if sn == 0 else b""
def chain_root(rpc, contract, day):
"""roots(uint32) of the DebykoAnchor contract: the selector 0x081dc681 and the day as yyyymmdd in one 32-byte word."""
data = "0x081dc681" + format(int(day.replace("-", "")), "064x")
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call", "params": [{"to": contract, "data": data}, "latest"]}).encode()
request = urllib.request.Request(rpc, body, {"content-type": "application/json"})
return json.load(urllib.request.urlopen(request))["result"][2:]
def verify(proof, root=None):
row, canonical = proof["row"], proof["canonical"]
# 1. the text that is hashed is the canonical (RFC 8785) form of the row: sorted members, no whitespace, strings as they are
if json.dumps(row, sort_keys=True, separators=(",", ":"), ensure_ascii=False) != canonical: return "the canonical text is not the row"
# 2. the row's leaf, and the path from it to its partition's root
leaf = h(b"\x00", canonical.encode())
if leaf.hex() != proof["leaf_hash"]: return "the leaf hash is not the hash of the canonical text"
partition = proof["partition"]
if walk(leaf, proof["index"], proof["rows"], proof["path"]).hex() != partition["root"]: return "the path does not lead to the partition's root"
# 3. the partition's leaf in the day's tree commits to its table, size and root, and its path leads to the day's root
text = json.dumps({"day": proof["day"], "root": partition["root"], "rows": proof["rows"], "schema_version": proof["schema_version"], "table": proof["table"]},
sort_keys=True, separators=(",", ":"))
pleaf = h(b"\x00", text.encode())
if pleaf.hex() != partition["leaf"]: return "the partition's leaf is not what the partition says it is"
if walk(pleaf, partition["index"], partition["count"], partition["path"]).hex() != proof["day_root"]: return "the partition's path does not lead to the day's root"
if root is not None and root.lower().removeprefix("0x") != proof["day_root"]: return "the day's root is not the one you hold"
return None
if __name__ == "__main__":
args = sys.argv[1:]
proof = json.load(open(args[0]))
root = args[args.index("--root") + 1] if "--root" in args else None
if "--rpc" in args:
root = chain_root(args[args.index("--rpc") + 1], args[args.index("--contract") + 1], proof["day"])
if set(root) == {"0"}: sys.exit("invalid: nothing is anchored for " + proof["day"] + " on that contract")
problem = verify(proof, root)
print("valid" if problem is None else "invalid: " + problem)
sys.exit(0 if problem is None else 1)

The same in Node (18 or later), node verify.mjs proof.json ... with the same options:

#!/usr/bin/env node
// Verifies a DEBYKO anchor proof (GET /v2/anchors/proof) with Node's standard library alone (Node 18 or later).
//
// node verify.mjs proof.json checks the proof against the day root inside it
// node verify.mjs proof.json --root <hex> ... and against a root you got elsewhere (the chain, GET /v2/anchors/{day})
// node verify.mjs proof.json --rpc https://mainnet.base.org --contract 0x0AF8259DBfC613825718d4d49A05F9F6fdA15086 asks the chain itself
//
// Exit 0 and "valid" when every step holds; exit 1 and the step that failed otherwise.
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const h = (...parts) => createHash('sha256').update(Buffer.concat(parts)).digest();
const hex = (buffer) => buffer.toString('hex');
/** RFC 9162 section 2.1.3.2: the root an audit path takes `leaf` (index `index` of `size`) to. */
function walk(leaf, index, size, path) {
let fn = BigInt(index), sn = BigInt(size) - 1n, r = leaf;
for (const p of path.map((x) => Buffer.from(x, 'hex'))) {
if (sn === 0n) return Buffer.alloc(0);
if ((fn & 1n) === 1n || fn === sn) {
r = h(Buffer.from([1]), p, r);
while ((fn & 1n) === 0n && fn !== 0n) { fn >>= 1n; sn >>= 1n; }
} else {
r = h(Buffer.from([1]), r, p);
}
fn >>= 1n; sn >>= 1n;
}
return sn === 0n ? r : Buffer.alloc(0);
}
/** RFC 8785 for what an anchored row holds (strings, booleans, null): members sorted by UTF-16 code units, no whitespace. */
const canonicalOf = (value) => (value !== null && typeof value === 'object'
? '{' + Object.keys(value).sort().map((k) => JSON.stringify(k) + ':' + canonicalOf(value[k])).join(',') + '}'
: JSON.stringify(value));
/** roots(uint32) of the DebykoAnchor contract: the selector 0x081dc681 and the day as yyyymmdd in one 32-byte word. */
async function chainRoot(rpc, contract, day) {
const data = '0x081dc681' + BigInt(day.replaceAll('-', '')).toString(16).padStart(64, '0');
const answer = await fetch(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: contract, data }, 'latest'] }) });
return (await answer.json()).result.slice(2);
}
export function verify(proof, root) {
const { row, canonical, partition } = proof;
if (canonicalOf(row) !== canonical) return 'the canonical text is not the row';
const leaf = h(Buffer.from([0]), Buffer.from(canonical));
if (hex(leaf) !== proof.leaf_hash) return 'the leaf hash is not the hash of the canonical text';
if (hex(walk(leaf, proof.index, proof.rows, proof.path)) !== partition.root) return "the path does not lead to the partition's root";
const text = canonicalOf({ day: proof.day, root: partition.root, rows: proof.rows, schema_version: proof.schema_version, table: proof.table });
const partitionLeaf = h(Buffer.from([0]), Buffer.from(text));
if (hex(partitionLeaf) !== partition.leaf) return "the partition's leaf is not what the partition says it is";
if (hex(walk(partitionLeaf, partition.index, partition.count, partition.path)) !== proof.day_root) return "the partition's path does not lead to the day's root";
if (root !== undefined && root.toLowerCase().replace(/^0x/, '') !== proof.day_root) return "the day's root is not the one you hold";
return null;
}
const args = process.argv.slice(2);
if (args[0] !== undefined && import.meta.url === new URL(process.argv[1], 'file:').href) {
const proof = JSON.parse(readFileSync(args[0], 'utf8'));
const flag = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
let root = flag('--root');
if (flag('--rpc')) {
root = await chainRoot(flag('--rpc'), flag('--contract'), proof.day);
if (/^0*$/.test(root)) { console.error(`invalid: nothing is anchored for ${proof.day} on that contract`); process.exit(1); }
}
const problem = verify(proof, root);
console.log(problem === null ? 'valid' : `invalid: ${problem}`);
process.exit(problem === null ? 0 : 1);
}
  • It does not say the figures were right when they were collected, only that these are the figures DEBYKO held a week after the day, and that they have not changed since.
  • A day anchored on a test network (base-sepolia, not used for the real anchors) is a rehearsal: nobody is bound by it. Check network in the answer.
  • It says nothing about rows DEBYKO does not store (a tail listing’s depth, a venue’s trades it does not collect) or that retention has dropped (the day’s root stays; the proofs of the dropped rows are gone).