Point-in-time attestation

Ask us what a network's metric was on any date — and get a proof you can check against Bitcoin without trusting us.

Messari, Dune, DefiLlama, Token Terminal hand you a number. None of them can prove they didn't change it afterwards. We can. Every night the DePIN API's published values are written to a batch file, each row is hashed, and the batch's Merkle root is committed to the Bitcoin blockchain via OpenTimestamps. The root is a one-way fingerprint: change one character of one row and every proof breaks.

A competitor cannot retrofit this. They would have to have been anchoring since August 9. The 24 consecutive daily roots below are the capability, and each carries a verify_url on every value so you can check the source too.

Verify us in 90 seconds (real transcript)

This is the worked example for symbol=2Z metric=devices_total date=20260912. Every command and output below was actually run against the live endpoints on 2026-09-13 — nothing on this page is illustrative. Fetch the attestation:

$ curl -s "https://kairossignal.com/v1/attest?symbol=2Z&metric=devices_total&date=20260912" -H "X-API-Key: <your-key>"
{
 "attested": true,
 "query": {"symbol": "2Z", "metric": "devices_total", "date": "20260912"},
 "value_as_published": {
  "symbol": "2Z", "metric": "devices_total",
  "value": "94", "unit": "devices",
  "source": "doublezero_data_api",
  "verify_url": "https://data.malbeclabs.com/api/dz/devices?limit=1000",
  "as_of": "2026-09-12T00:24:01Z",
  "row_sha256": "76c47d0a2292a1fe7ac283164babd03b943cc6057a88b0b85ed9a24b8ee0c566",
  "row_tsv": "2Z\tdevices_total\t94\tdevices\tdoublezero_data_api\thttps://data.malbeclabs.com/api/dz/devices?limit=1000\t2026-09-12T00:24:01Z"
 },
 "batch": {
  "stamp_day": "20260912", "rows": 4062807,
  "merkle_root": "49daee474d17536e1ff1d20c5c2103c608f11f34120a3dd1cf417c3062050726",
  "batch_sha256": "37fce7a1458a9b88dd3cb81ef12698bfa72f02197d9ac8252852196bd705036a",
  "batch_tsv_url": "https://kairossignal.com/attestations/batch_20260912.tsv"
 },
 "merkle_inclusion_proof": {
  "leaf_index": 22653, "leaf_sha256": "76c47d0a2292a1fe…",
  "path": [ {"level":0,"index":22653,"sibling":"f0ea2e9af8d129e8…","position":"right"}, … 22 steps ],
  "algorithm": "leaf = sha256(utf8 row); parent = sha256(left||right); an odd last node is hashed with itself"
 },
 "opentimestamps": {
  "proof_url": "https://kairossignal.com/attestations/batch_20260912.root.ots",
  "verify_command": "ots verify batch_20260912.root.ots",
  "bitcoin_status": "bitcoin_confirmed",
  "anchored_late": false, "anchor_utc": null,
  "bitcoin_branches": [{"height": 966587, "verified": true,
    "block_hash": "0000000000000000000226d5aa983ec34759419b1ff87636020bf8c30470fc90",
    "block_time_utc": "2026-09-12T01:02:52Z"}],
  "honesty": "SAME-DAY ANCHOR: the OpenTimestamps stamp was created on the stamp_day itself."
 }
}

Three honest-miss examples, so you know the endpoint refuses to invent — each is a different kind of "no", and none of them is a made-up number:

Step 1 — download the evidence (no key needed)

$ curl -s https://kairossignal.com/attestations/batch_20260912.root
49daee474d17536e1ff1d20c5c2103c608f11f34120a3dd1cf417c3062050726

$ curl -s https://kairossignal.com/attestations/batch_20260912.tsv -o batch_20260912.tsv
$ wc -l batch_20260912.tsv && stat -c%s batch_20260912.tsv
4062806 batch_20260912.tsv
544254482

$ curl -s https://kairossignal.com/attestations/batch_20260912.root.ots -o batch_20260912.root.ots
That batch is 519 MiB. Each daily batch is a full dump of every value in the warehouse, so it grows with the estate — the first ones in August were about 1 MB. If you only want to confirm the root is in Bitcoin, steps 1 and 4 need just the .root (65 bytes) and the .root.ots (2.6 KB); the large download is only needed to rebuild the tree yourself in steps 2 and 3.
Why wc -l says 4,062,806 but the batch holds 4,062,807 rows: the file's last line has no trailing newline, and wc -l counts newline characters. Count non-empty lines yourself — any verifier that "corrects" the row count by trusting wc -l would be off by one. The attestation response's batch.rows is the number the Merkle tree was built over.

Step 2 — hash the exact row (mind the newline)

$ awk 'NR==22654{printf "%s",$0}' batch_20260912.tsv | sha256sum
76c47d0a2292a1fe7ac283164babd03b943cc6057a88b0b85ed9a24b8ee0c566  -
printf "%s",$0 (or head -c -1) matters: sed -n 22654p | sha256sum hashes the row plus its trailing newline and gives c5dd968e… — a different digest. The attested leaf is sha256 of the row WITHOUT the newline, matching the engine's row_sha256. This is the single most common way a first-time verifier concludes "the proof is wrong" when it isn't. Note NR is 1-indexed and leaf_index is 0-indexed: line 22,654 is leaf 22,653.

Step 3 — rebuild the root from the proof (12 lines of Python)

# verify_row.py — reads the attestation + batch_20260912.tsv, trusts only sha256
import hashlib, json, urllib.request
url = "https://kairossignal.com/v1/attest?symbol=2Z&metric=devices_total&date=20260912"
att = json.load(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "verify_row.py"})))
rows = [l.rstrip("\n") for l in open("batch_20260912.tsv") if l.strip()]
leaf = hashlib.sha256(rows[att["merkle_inclusion_proof"]["leaf_index"]].encode()).hexdigest()
h = bytes.fromhex(leaf)
for st in att["merkle_inclusion_proof"]["path"]:
    s = bytes.fromhex(st["sibling"])
    h = hashlib.sha256(h + s).digest() if st["position"] == "left" else hashlib.sha256(s + h).digest()
print("leaf:        ", leaf)
print("root:        ", h.hex())
print("merkle_root: ", att["batch"]["merkle_root"])
print("MATCH:", h.hex() == att["batch"]["merkle_root"])
$ python3 verify_row.py
leaf:         76c47d0a2292a1fe7ac283164babd03b943cc6057a88b0b85ed9a24b8ee0c566
root:         49daee474d17536e1ff1d20c5c2103c608f11f34120a3dd1cf417c3062050726
merkle_root:  49daee474d17536e1ff1d20c5c2103c608f11f34120a3dd1cf417c3062050726
MATCH: True
Why the explicit User-Agent. Our CDN currently rejects Python's default Python-urllib/3.x user agent with a 403 before the request reaches us, while curl, requests and any explicit agent string are served normally. That is our bug, not yours, and it is being removed; until then the one-line Request(...) wrapper above is the workaround. We would rather publish the workaround than ship you an example that fails.

Step 4 — check the root against Bitcoin

$ ots verify batch_20260812.root.ots
Calendar https://alice.btc.calendar.opentimestamps.org: Timestamped by transaction 490bf16704078d18f0f6d5fedc2aca2c51584314981bc41c5dba52670b1a20c3; waiting for 6 confirmations
Calendar https://finney.calendar.eternitywall.com: Pending confirmation in Bitcoin blockchain
Calendar https://bob.btc.calendar.opentimestamps.org: Pending confirmation in Bitcoin blockchain
Calendar https://btc.calendar.catallaxy.com: Pending confirmation in Bitcoin blockchain

And for the one batch that already completed its Bitcoin confirmation (the Aug 9 batch, which was stamped same-day):

$ ots verify batch_20260809.root.ots
Assuming target filename is 'batch_20260809.root'
Hashing file, algorithm sha256
Got digest 406fd956b49aa59ebe4ee5d14a1ef4dd911b84362f017ba58e8e4e0beaea1955
Attestation block hash: 000000000000000000022e510406e099cec4e3ab77ed0629ec206592b8ca40cb
Success! Bitcoin block 961777 attests existence as of 2026-08-09 UTC

ots normally talks to your own Bitcoin node; if you run one, drop the flag. This server has no node, so our transcript pointed ots --bitcoin-node at a shim that serves getblockhash/getblockheader from blockstream.info/mempool.space public APIs — the attestation check (attested digest == block header's merkle root) happens inside the real ots client either way, and the block headers come from third parties, not from Kairos.

The honesty model — read before you buy

Late anchors, surfaced per date. The nightly stamp step was broken 2026-08-10 through 2026-08-31 (a cron PATH bug). Those 22 batches' OpenTimestamps proofs were created on 2026-09-01. They prove a batch existed by the anchor date, not by its stamp_day — and every API response says so explicitly via anchored_late: true plus the anchor timestamp. From 2026-09-01 the nightly stamping works again, and new batches are stamped the same night. We do not present a backfilled anchor as a same-day one; that would be exactly the class of overstatement this product exists to sell against.
Retention — 20 of 36 batches no longer have their rows. The batch TSVs for 2026-08-09 through 2026-08-28 were deleted from this box on 2026-09-05 during a disk clean-up. Their manifests, .root files and OpenTimestamps proofs survive, so those Merkle roots are still in Bitcoin and still mean exactly what they meant: the batch existed, in those exact bytes, by that block. What is gone is the row-level data the root commits to, so no inclusion proof can be rebuilt for those days and /v1/attest returns an explicit miss saying so, naming the root and the OTS proof that are still checkable. The 16 batches from 2026-08-29 onward are complete. We are telling you this rather than letting you discover it: a proof you cannot reproduce is not a proof, and the failure was ours.

What a Bitcoin-confirmed root proves: the exact bytes of that day's batch existed, in that exact order, by the time the anchoring block was mined. Nobody — not us, not a attacker with our database — can alter one historical value without breaking the proof.

What it does NOT prove: that the upstream source told the truth that day. That is what each value's verify_url is for — check the source yourself. And it says nothing about trading performance; Kairos attests data integrity and point-in-time, not signals.

The batch registry (measured, not declared)

Bitcoin status below is re-measured on every call by parsing the .ots files and comparing attested digests against live block headers from public Esplora APIs — it is not a label we set. Full data: /v1/attestations.

stamp_dayrowsmerkle rootanchorBitcoin statusevidence
202608091,3699cd92fde048419f2…same-dayconfirmed — block 961777 (2026-08-09).ots
202608102,5504417a5c1534ad1c7…backfilled 09-01pending.ots
202608116,18080477f0734fca0fd…backfilled 09-01pending.ots
202608129,49205a9f360a317d788…backfilled 09-01pending.ots
2026081312,962d4a66a81aac31ab2…backfilled 09-01pending.ots
2026081417,003001cca2c03fd35be…backfilled 09-01pending.ots
2026081521,0342e763d9560308173…backfilled 09-01pending.ots
2026081624,9113234190fe599bd78…backfilled 09-01pending.ots
2026081728,9495e432e34d9a48327…backfilled 09-01pending.ots
2026081839,114bfd3fa22181df17e…backfilled 09-01pending.ots
2026081975,1933d04e19e87b48f6a…backfilled 09-01pending.ots
20260820104,189c5f215b774ba429b…backfilled 09-01pending.ots
20260821124,96082d1f7f58ef7e474…backfilled 09-01pending.ots
20260822163,1977d30f4e42e1d1b4b…backfilled 09-01pending.ots
20260823261,99483facc0322113e9a…backfilled 09-01pending.ots
20260824365,240baedc9aa589c6b6d…backfilled 09-01pending.ots
20260825545,006da3033b3ceb2c4e0…backfilled 09-01pending.ots
20260826729,9335b7b1a9b18f45fb2…backfilled 09-01pending.ots
20260827910,14217ae8162be35ea87…backfilled 09-01pending.ots
202608281,095,89071b24a68ce054245…backfilled 09-01pending.ots
202608291,281,974567aa5dbaa114779…backfilled 09-01pending.ots
202608301,467,337d5ef2a83a5f6997a…backfilled 09-01pending.ots
202608311,652,447aa85b21cc1f815d6…backfilled 09-01pending.ots
202609012,018,5842d32689a65ecc941…same-daypending.ots

Every batch's full evidence is public, no key: /attestations/batch_<day>.tsv (the rows), /attestations/batch_<day>.root (the Merkle root), /attestations/batch_<day>.root.ots (the OpenTimestamps proof). Nothing else under that path is served.

API

GET /v1/attest?symbol=<SYM>&metric=<M>&date=<YYYYMMDD>
    proof=last|all|none      (default last: the freshest row that UTC day)
GET /v1/attestations          (batch registry with measured Bitcoin status)

Responses are ed25519-signed (X-Kairos-Signature/X-Kairos-Timestamp headers; key at /.well-known/kairos-signing-key.json) so what we served you, and when, is also provable after the fact.

Scope, stated plainly

Point-in-time data integrity for values the Kairos DePIN API published. Not a promise about upstream accuracy (that's the verify_url on each value), not a signal, not financial advice. Rows appear in a batch only if they were published that day; a (symbol, metric, date) we cannot produce returns an honest miss — never a synthesised value, never a zero-fill.