Downloading a Permadata stamp
A Permadata v2 stamp is a permanent, sealed record of a file on the X1 chain. This doc is the practical guide to pulling a stamped file back out of the chain into a working local file (notld.mp4), then proving it is the exact bytes that were stamped.
1. What a stamp is, and where the numbers live
A stamp is a 512-byte account owned by the Permadata program MoViekvxc6uK6yDBf2XidPPph5ewU4XVWj2LKD4Zk3w. For files larger than ~10 KB it uses the Chunked tier: the file is split into 900-byte chunks, and each chunk is stored as one append_chunk transaction (nothing is written to account state — the transaction history is the storage). A 1.86 GB film is 2,069,456 chunks × 900 B = 2,069,456 transactions.
The fields you care about, at their byte offsets inside the account data:
| field | offset | width | meaning |
|---|---|---|---|
| chunk_size | 143 | u16 LE | bytes per chunk (900) |
| chunk_count | 145 | u32 LE | number of chunks (2,069,456) |
| size_bytes | 153 | u64 LE | total file size (1,862,510,336) |
| content_hash | 161 | 32 B | SHA-256 of the whole file — the canonical identity |
| chunk_root_hash | 193 | 32 B | Merkle root over the chunk hashes |
content_hash is the one that matters most: the stamp PDA itself is derived from it, and it is what ties “these exact bytes” to the on-chain record. chunk_root_hash is a secondary integrity structure you can check if you want to prove every chunk.
For stamp ALuMA6zrW9NWGACyK7SbExwyWteAeDz9YEANFkizNUU4 (the public-domain film Night of the Living Dead):
chunk_size = 900
chunk_count = 2,069,456
size_bytes = 1,862,510,336 (≈ 1.73 GiB)
content_hash = 8d5f6dd2bc41ab9b16db69b182beccddef1547da2dba6e3a3301da092da3672d
merkle_root = 58fbca5656decbd3beb675000fd034862f0087e2f121e07cba75b239c9953ba92. The tool: permadata-get
Single-file, zero-dependency Node CLI. Run it anywhere Node ≥ 18 exists:
node tools/downloader/permadata-get.mjs <stamp> -o notld.mp4
# or, symlinked/on PATH as `permadata-get`:
permadata-get <stamp> -o notld.mp4It enumerates the stamp’s transaction history, fetches those transactions in batched JSON-RPC requests, extracts each chunk (verifying its committed hash), and writes each chunk to its byte offset — so it never holds the file in memory. Progress lives in a sidecar file; an interrupted run resumes without re-fetching.
permadata-get <stamp> -o notld.mp4 \
--rpc https://rpc.mainnet.x1.xyz \
--batch-size 80 --concurrency 8
# verify an existing file against a stamp (no network download):
permadata-get --verify-only notld.mp4 <stamp>
# stop early (testing):
permadata-get <stamp> -o notld.mp4 --max-chunks 50000Flags:
| flag | default | meaning |
|---|---|---|
| -o, --out | — | destination file (required) |
| --rpc | https://rpc.mainnet.x1.xyz | JSON-RPC endpoint |
| --api-key | $X1_API_KEY | API key, sent as an X-API-Key header |
| --batch-size | 80 | getTransaction calls per HTTP request (max ~85 on the public RPC) |
| --concurrency | 8 | parallel batch requests |
| --state | .permadata-get-state | sidecar progress dir |
| --max-chunks | — | stop after N chunks |
| --no-merkle | — | skip the Merkle-root check |
| --verify-only | — | verify <file> against <stamp>, exit non-zero on mismatch |
On completion it prints RESULT: PASS (exit 0) or RESULT: FAIL (exit 1).
3. Downloading with the public RPC (and its real limits)
https://rpc.mainnet.x1.xyz is the free, unauthenticated RPC. It works for full downloads, with measured limits (2026-10-19, from the NOTLD run):
| batch size | result | chunks/s (1 request at a time) |
|---|---|---|
| 10 | OK | ~194 |
| 25 | OK | ~415 |
| 50 | OK | ~443 |
| 80 | OK | — |
| 85 | OK (max) | — |
| 88 / 100 / 200 | HTTP 413 (payload too large) | — |
Max usable batch ≈ 85. With --batch-size 80 --concurrency 8 the sustained rate is ~930 chunks/s ≈ 0.80 MB/s — the full 1.86 GB NOTLD film takes ~37 minutes. The ceiling is the public RPC’s request rate, not our code: each 80-call batch takes ~0.7 s server-side, and ~12 concurrent batch requests/s is what the free tier will sustain before it starts rate-limiting (429).
Bottom line: the free public RPC can download a film, but it is ~0.8 MB/s. For speed, use an API key (below) or your own node (below).
4. Downloading with an x1scroll API key (fast path)
A keyed endpoint raises the request budget, which is the only thing capping throughput. When it ships, point the tool at it:
export X1_API_KEY="<your key>"
permadata-get <stamp> -o notld.mp4 \
--rpc https://rpc.x1scroll.io \
--api-key "$X1_API_KEY"The key is sent as an X-API-Key header. The exact endpoint URL and the rate limit are still being finalized — this is a placeholder. Contact support@x1scroll.io (placeholder) for key issuance. The tool already implements 429/Retry-After backoff and automatic batch splitting on 413, so it degrades gracefully on any endpoint.
5. Pointing the tool at your own node
Any X1 node with transaction history enabled works:
permadata-get <stamp> -o notld.mp4 --rpc http://127.0.0.1:8899
# or over an SSH tunnel to a node that only listens locally:
# ssh -L 8899:127.0.0.1:8899 user@node then use --rpc http://127.0.0.1:8899Note: a validator running with transaction history disabled (common, to save disk) will answer Transaction history is not available from this node for getTransaction. That node can’t serve downloads — use an archival/indexer node or the public RPC.
6. Verifying a download independently (no Permadata tool)
You do not need our tool to prove a file is authentic. Two independent checks:
SHA-256 of the whole file (the canonical identity):
sha256sum notld.mp4
# must equal the stamp's content_hash:
# 8d5f6dd2bc41ab9b16db69b182beccddef1547da2dba6e3a3301da092da3672dMerkle root over the 900-byte chunks (leaf = sha256(0x00 ‖ u32le(seq) ‖ chunk), node = sha256(0x01 ‖ left ‖ right), odd nodes promoted, no duplication):
python3 - <<'PY'
import hashlib, sys
data = open(sys.argv[1], 'rb').read()
def h(b): return hashlib.sha256(b).digest()
level = []
for seq in range(0, len(data), 900):
chunk = data[seq:seq+900]
level.append(h(b'\x00' + seq.to_bytes(4,'little') + chunk))
while len(level) > 1:
nxt = []
for i in range(0, len(level)-1, 2):
nxt.append(h(b'\x01' + level[i] + level[i+1]))
if len(level) % 2: nxt.append(level[-1])
level = nxt
print(level[0].hex())
PY
# must equal the stamp's chunk_root_hash:
# 58fbca5656decbd3beb675000fd034862f0087e2f121e07cba75b239c9953ba9(seq is the chunk index; the byte offset of chunk seq is seq * 900. The last chunk is short — 836 bytes — which the loop above handles naturally.)
The content hash is also the seed of the stamp’s PDA, so you can cross-check the address itself: findProgramAddress(["pm_stamp", content_hash], program) == stamp.
7. Troubleshooting
- HTTP 429 (rate limited) — the tool backs off on
Retry-Afterand retries. Lower--concurrency, or use a keyed endpoint / your own node. The run is resumable, so just restart it. - HTTP 413 (payload too large) — your
--batch-sizeis too big for that endpoint. The tool auto-splits batches on 413; or set--batch-size 50. Transaction history is not available from this node— the node has history disabled. Use the public RPC or an archival node (see §5).- “N chunks missing” / incomplete file — the run was interrupted and the sidecar state is newer than the file, or the stamp has chunks your endpoint won’t serve. Re-run: it resumes from the sidecar and fills the gaps. Do not trust a partial file — the final verification refuses to PASS with missing data.
- Wrong content type / file won’t play — the tool writes raw bytes; the
content_typeis in the stamp (offset 137), not in the file. Rename the output to the right extension (.mp4for NOTLD) and open it with any player. If a player can’t decode it, check the file’s SHA-256 first — a wrong hash means the bytes are wrong, not the codec. --verify-onlyfails on a file that “should” be right — it compares SHA-256 and (by default) the Merkle root. A single flipped byte changes the SHA-256. Re-download; the downloader verifies each chunk hash as it goes.
8. Worked example — NOTLD
# one command: download + verify
bash examples/fetch-notld.sh
# or by hand:
permadata-get ALuMA6zrW9NWGACyK7SbExwyWteAeDz9YEANFkizNUU4 -o notld.mp4
# …prints RESULT: PASS (exit 0), sha256 = 8d5f6dd2…3672d
sha256sum notld.mp4
# 8d5f6dd2bc41ab9b16db69b182beccddef1547da2dba6e3a3301da092da3672d notld.mp4Measured on the public RPC: 2,069,456 chunks in ~37 min at ~0.80 MB/s, peak RSS ~480 MB (the signature index, not the file — the file is never held in memory), SHA-256 and Merkle root both PASS.