How to Verify a NakedPnL Trader Track Record in 10 Minutes
A step-by-step tutorial for independently verifying any NakedPnL track record using SHA-256 hash chains, Bitcoin OpenTimestamps, and exchange API responses.
- Every NakedPnL track record is a SHA-256 hash chain you can recompute locally with no trust in NakedPnL.
- The full verification has four phases: chain fetch, hash recomputation, OpenTimestamps anchor check, and optional raw-API replay.
- Phase 1 to 3 take under ten minutes with the Python or Node.js snippets in this guide.
- If any chainHash, contentHash, or Bitcoin attestation fails to match, the track record is broken and the trader's profile will display a chain-break alert.
- Verification is a due-diligence input, not investment advice. Use it the same way you would use an auditor's report.
NakedPnL is a publisher of verified investment performance. The output of that pipeline is a public registry where every row is a daily NAV snapshot bound to the previous row by a SHA-256 hash chain, and every daily batch of chain heads is anchored to Bitcoin via OpenTimestamps. None of that matters unless you, the reader, can re-verify the chain on your own machine. This guide walks through the four phases of full verification end to end.
If you have a Python 3.11+ runtime or Node.js 20+ on your laptop, the entire process takes under ten minutes for a track record with one year of daily snapshots. We will use the public chain JSON endpoint, the Web Crypto API or hashlib, and the OpenTimestamps verifier endpoint. No login is required. No NakedPnL API key is required.
Phase 1 — Fetch the public chain JSON
Every public NakedPnL profile exposes a JSON endpoint at /verify/chain/[handle].json. The endpoint returns the full ordered chain of NAV snapshots for that handle, including sequence numbers, content hashes, chain hashes, the previous chain hash, and the canonicalized raw-response digest. The endpoint is rate-limited per IP but does not require authentication, because the data is already public registry content.
Pick any verified trader. We will use the placeholder handle `cpx-trader` throughout. Replace it with the real handle you want to inspect. The first command pulls the chain into a local file so the rest of the verification is offline.
curl -s https://nakedpnl.com/verify/chain/cpx-trader.json \
-o cpx-trader.chain.json
jq '.entries | length' cpx-trader.chain.json
# 365 <-- one year of daily snapshotsEach entry in the JSON document has the following shape. The fields you must care about are sequence, contentHash, chainHash, and previousHash. The rawResponseDigest is what you would recompute in Phase 4 if you decide to do an end-to-end replay against the venue API.
{
"sequence": 0,
"snapshotDate": "2025-05-07",
"navUsd": "12450.27",
"venue": "BINANCE",
"contentHash": "9f4c...e2a1",
"chainHash": "1aa9...b7d0",
"previousHash": "genesis",
"rawResponseDigest": "8b22...4f01"
}Phase 2 — Verify the genesis row
The first row of every chain is special. Its previousHash is the literal ASCII string `genesis`. Any chain that does not start with that exact value is broken at the root and the rest of the verification is moot. The chainHash of the genesis row is therefore SHA-256 of the bytes 'genesis' concatenated with the contentHash of that first row. This is the only row in the chain whose previous hash is not itself a SHA-256 digest, and it exists so the recursion has a defined base case.
import json, hashlib
with open("cpx-trader.chain.json") as f:
chain = json.load(f)["entries"]
genesis = chain[0]
assert genesis["sequence"] == 0
assert genesis["previousHash"] == "genesis"
expected = hashlib.sha256(
("genesis" + genesis["contentHash"]).encode("utf-8")
).hexdigest()
assert expected == genesis["chainHash"], "Genesis chain hash mismatch"
print("Genesis row OK:", genesis["snapshotDate"])Phase 3 — Recompute every chainHash
Now walk the chain forward. For every row at index i greater than zero, the expected chainHash is SHA-256 of (chain[i-1].chainHash || chain[i].contentHash). If a single row in the middle has been edited, every downstream row will fail this check. This is the property that makes the chain tamper-evident: an attacker who silently rewrites row 142 must also rewrite rows 143 through 365, but cannot rewrite the OpenTimestamps Bitcoin anchor that fixes row 365 in time.
def sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
prev = "genesis"
for row in chain:
expected = sha256(prev + row["contentHash"])
if expected != row["chainHash"]:
raise SystemExit(
f"Chain break at sequence {row['sequence']} "
f"({row['snapshotDate']})"
)
prev = row["chainHash"]
print(f"Chain OK: {len(chain)} rows, head={prev[:12]}...")The same algorithm in JavaScript using the Web Crypto API runs in any modern browser, in Node.js 20+, or in Deno. The only nuance is that crypto.subtle.digest returns an ArrayBuffer that you must hex-encode yourself.
const sha256 = async (s) => {
const data = new TextEncoder().encode(s);
const buf = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
const chain = (await (await fetch(
"https://nakedpnl.com/verify/chain/cpx-trader.json"
)).json()).entries;
let prev = "genesis";
for (const row of chain) {
const expected = await sha256(prev + row.contentHash);
if (expected !== row.chainHash) {
throw new Error("Break at " + row.sequence);
}
prev = row.chainHash;
}
console.log("Chain OK:", chain.length, "rows");Phase 4 — Verify the OpenTimestamps Bitcoin anchor
Recomputing the chain only proves internal consistency. It does not prove that NakedPnL did not silently rebuild the entire chain yesterday. To get external time-evidence, NakedPnL builds a Merkle tree every day at 00:05 UTC over the chain heads of every public entity. The Merkle root is submitted to OpenTimestamps calendar servers, which batch-anchor it into the public Bitcoin ledger.
After Bitcoin includes the calendar's commitment in a block (typically within a few hours), NakedPnL upgrades the .ots receipt with the Merkle path from the daily root all the way down to a Bitcoin block header. Once the proof is in the UPGRADED state with a btcBlockHeight set, the chain head as it existed on that date is anchored to a public block whose hash you can independently verify against any Bitcoin node or block explorer.
The public verifier endpoint /api/verify/[date] returns the full attestation bundle for a given UTC date. The response includes the dailyMerkleRoot, the OpenTimestamps proof in serialized form, the chain heads it covers, and the Bitcoin block height once the proof is upgraded.
curl -s https://nakedpnl.com/api/verify/2026-05-06 | jq '.'
# {
# "date": "2026-05-06",
# "merkleRoot": "5fe1...c9b2",
# "status": "UPGRADED",
# "btcBlockHeight": 893421,
# "btcBlockHash": "0000000000000000000272...",
# "otsProof": "<base64 .ots receipt>",
# "entityHeads": [
# { "handle": "cpx-trader", "chainHash": "1aa9...b7d0" },
# { "handle": "atlas", "chainHash": "7be4...d901" }
# ]
# }There are two checks to perform here. First, confirm that the chain head you finished Phase 3 with is present in the entityHeads array for the most recent UPGRADED date. If the chain head you computed locally is not in the list, either you are looking at an off-by-one date or the snapshot has not yet been anchored. Second, run the .ots receipt through the standard OpenTimestamps client to confirm the Bitcoin attestation independently of NakedPnL.
# Decode the base64 .ots receipt from the API response
echo "<base64 here>" | base64 -d > 2026-05-06.ots
# Install the official client (https://opentimestamps.org)
pip install opentimestamps-client
# Save the merkle root as a binary file
echo -n "5fe1...c9b2" | xxd -r -p > 2026-05-06.root
# Verify
ots verify 2026-05-06.ots
# Success! Bitcoin block 893421 attests existence as of 2026-05-06Phase 5 (optional) — Replay the raw exchange API
The strongest possible verification level requires the trader's cooperation. The contentHash of every row is computed over the canonicalized raw exchange API response, not over the trader-friendly NAV number. If the trader gives you read-only API credentials for the same venue, you can re-fetch the historical balance at the same snapshot timestamp and recompute the contentHash yourself. If it matches, you have proven that NakedPnL did not invent or alter the underlying data.
This phase is optional because it requires venue credentials. Most third-party verifiers (allocators, journalists, regulators) stop after Phase 4 because the OpenTimestamps anchor already binds the contentHash to a Bitcoin block. The raw-replay phase exists for traders who want to demonstrate the strongest possible audit trail to a single counterparty.
| Phase | What it proves | Trust in NakedPnL? |
|---|---|---|
| 1 — Fetch | You have the same JSON the public registry serves. | Low |
| 2 — Genesis | The chain is correctly rooted. | Zero |
| 3 — Recompute | No row has been silently edited. | Zero |
| 4 — OpenTimestamps | The chain head existed by a specific Bitcoin block. | Zero |
| 5 — Raw replay | The published data matches the venue's actual history. | Zero |
Common verification failures and what they mean
If Phase 3 fails on a single row, the chain has been broken at that sequence number. This is rare in practice because NakedPnL writes are atomic and append-only, but it can happen if a snapshot was retroactively edited. The trader's profile will display a chain-break alert and the registry rank will be suspended pending investigation.
If Phase 4 fails because the merkleRoot does not match the local chain head, you may be looking at the wrong UTC date. The OpenTimestamps anchor at /api/verify/[date] uses calendar dates, so for a snapshot at 23:55 UTC on 2026-05-06 the anchor will be at /api/verify/2026-05-06 and the OTS submission cron at 00:05 UTC on 2026-05-07.
If Phase 5 fails because the recomputed contentHash differs by even one byte, double-check the canonicalization step. Different JSON libraries serialize floats and key order differently. The reference canonicalization rules are documented at /docs/verification along with a Python and JavaScript reference implementation.
Putting it all together — a single script
The four phases compose into a single script you can run on any modern Python install. The complete reference implementation is below. It exits non-zero on any verification failure, which makes it suitable for inclusion in a CI pipeline if you maintain a watchlist of handles.
import json, sys, hashlib, urllib.request, base64
def sha256(s):
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def fetch(url):
with urllib.request.urlopen(url) as r:
return json.loads(r.read())
def verify_chain(handle):
chain = fetch(f"https://nakedpnl.com/verify/chain/{handle}.json")["entries"]
prev = "genesis"
for row in chain:
expected = sha256(prev + row["contentHash"])
if expected != row["chainHash"]:
sys.exit(f"BREAK at sequence {row['sequence']}")
prev = row["chainHash"]
return chain[-1]
def verify_ots(date, head_hash):
bundle = fetch(f"https://nakedpnl.com/api/verify/{date}")
if bundle["status"] != "UPGRADED":
print(f"Anchor for {date} is {bundle['status']} (not yet on Bitcoin)")
return
heads = {e["chainHash"] for e in bundle["entityHeads"]}
if head_hash not in heads:
sys.exit(f"Head {head_hash[:12]} missing from anchor {date}")
print(f"Anchored at Bitcoin block {bundle['btcBlockHeight']}")
if __name__ == "__main__":
handle, date = sys.argv[1], sys.argv[2]
head = verify_chain(handle)
print(f"Chain OK: {handle} sequence={head['sequence']}")
verify_ots(date, head["chainHash"])Run it once a week against the handles you care about. If the script ever exits non-zero, that is signal worth investigating before any allocation decision.