Verify without trusting ariadne.press
ariadne.press is a convenience, not an authority. The blockchain holds every action; IPFS holds every text, addressed by its hash; the indexer and the web app are projections that anyone can rebuild. This page shows how to check each layer yourself, from the quickest check to a complete independent rebuild. Terms such as event, box, CID and gateway are explained in the Glossary.
| What you check | Where the truth is | Tool |
|---|---|---|
| The program that enforces the rules | the Algorand application | any Algorand indexer, plus the ARC-56 file of the repository |
| Who did what, and when | the application’s events and boxes | any Algorand node or indexer |
| The text and files of an article, review or comment | IPFS, addressed by the CID stored on the chain | any IPFS gateway, then a hash |
| Rankings, the Peer-reviewed seal, citations, the Thread Score | computed from the above (and ORCID and OpenAlex for the seal) | your own indexer |
What you need
Section titled “What you need”-
The network’s entry in
spec/networks.json(see Networks and addresses). For TestNet: application772983107, created in round67852714, genesis hashSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=. -
curl, and for the scripts a copy of the repository with Node 24 or later. The scripts below use the indexer’s own code, so install it once:Terminal window cd projects/indexernpm ci -
An Algorand node and indexer. The manifest names AlgoNode’s public services (
https://testnet-api.algonode.cloudandhttps://testnet-idx.algonode.cloud), which are not run by ARIADNE; any other TestNet node or indexer, including your own, gives the same answers.
The commands are written for TestNet. For another network, change the application id and the URLs.
1. Check the program
Section titled “1. Check the program”The application is immutable: it has no update and no delete path (see Contract methods and events). What runs on the chain is therefore exactly the program it was created with. Compare it with the compiled program committed in the repository, from the repository root:
curl -s "https://testnet-idx.algonode.cloud/v2/applications/772983107" > app.jsonnode -e 'const app = require("./app.json").application.params;const spec = require("./projects/contracts/smart_contracts/artifacts/ariadne/Ariadne.arc56.json");console.log("approval program matches:", app["approval-program"] === spec.byteCode.approval);console.log("clear program matches:", app["clear-state-program"] === spec.byteCode.clear);'Both lines must say true. To go one step further, compile the contract from source with the pinned toolchain
(algokit project run build in projects/contracts; the versions are in DECISIONS.md) and compare your own
byteCode the same way.
2. Read the events
Section titled “2. Read the events”Every action is an application call whose logs carry ARC-28 events: a 4-byte selector followed by the event’s fields. The selectors and fields of all 18 events are listed in Contract methods and events.
Raw, from an Algorand indexer (each log is base64; page on with the next-token the answer gives):
curl -s "https://testnet-idx.algonode.cloud/v2/transactions?application-id=772983107&min-round=67852714&limit=100"Decoded, with the same decoder the ARIADNE indexer uses (src/events.ts, built from the ARC-56 file):
cd projects/indexernode --input-type=module -e "import { loadArc56 } from './src/arc56.ts'import { eventDefs, decodeLogs } from './src/events.ts'const defs = eventDefs(loadArc56())const url = 'https://testnet-idx.algonode.cloud/v2/transactions?application-id=772983107&min-round=67852714&limit=100'const { transactions } = await (await fetch(url)).json()for (const t of transactions) { const logs = (t.logs ?? []).map((l) => Buffer.from(l, 'base64')) for (const ev of decodeLogs(logs, defs)) console.log(t['confirmed-round'], ev.name, JSON.stringify(ev.args))}"The first lines show the creation (GovernanceChanged from the zero address to the governance address) and the fields
registered by governance (FieldAdded). Addresses come out as text, CIDs as 36 bytes in hex.
Public lists of articles are not contract events: they are notes of 0-ALGO payments that people send to themselves,
starting with ariadne/lists/<appId>:. Find them by their note prefix (the base64 of ariadne/lists/772983107:):
curl -s "https://testnet-idx.algonode.cloud/v2/transactions?tx-type=pay&min-round=67852714¬e-prefix=YXJpYWRuZS9saXN0cy83NzI5ODMxMDc6"3. Read the current state
Section titled “3. Read the current state”The events tell the history; the application’s boxes hold the current state. Each article has a box whose key is the
letter a followed by the article number as 8 bytes. Read article 1 from an Algorand node and decode it with the box
layout of the ARC-56 file:
cd projects/indexernode --input-type=module -e "import algosdk from 'algosdk'import { loadArc56, structTupleSignature } from './src/arc56.ts'const [app, id] = process.argv.slice(1)const key = Buffer.alloc(9)key.write('a')key.writeBigUInt64BE(BigInt(id), 1)const url = 'https://testnet-api.algonode.cloud/v2/applications/' + app + '/box?name=b64:' + encodeURIComponent(key.toString('base64'))const { value } = await (await fetch(url)).json()const spec = loadArc56()const values = algosdk.ABIType.from(structTupleSignature(spec, 'Article')).decode(Buffer.from(value, 'base64'))spec.structs.Article.forEach((f, i) => { const v = values[i] console.log(f.name, Array.isArray(v) || v instanceof Uint8Array ? Buffer.from(v).toString('hex') : String(v))})" 772983107 1You get the submitter, the fields, the type, the status, the version, the current CID (hex), the vote total and the
rest of the 192-byte record. The other boxes (co-authors, reviews, comments, votes, flags, reputation) follow the key
layout in Contract methods and events, with the structs CoAuthor, Review, Comment,
Vote, Flag and Reputation.
4. Check the content
Section titled “4. Check the content”The chain stores each article version as a 36-byte CID: a hash of the whole folder (index.md and assets/), computed
with fixed parameters. If the bytes you obtain hash to that CID, they are exactly what the author published, whoever
served them. That is why the gateway does not need to be trusted.
Turn the on-chain CID into text
Section titled “Turn the on-chain CID into text”cd projects/indexernode --input-type=module -e "import { cidHexToText } from './src/content.ts'console.log(cidHexToText(process.argv[1]))" 0170122079945c72c858fdd73e0cfdda76a62ef46a57b8eb7de3eb32b7cfc49a97b07468This example is the repository’s sample article; it prints
bafybeidzsrohfscy7xlt4dh53j3kmlxunjl3r2354pvtfn6pysnjpmduna. The article page and the API show the same text form.
Fetch an article from any gateway and check it
Section titled “Fetch an article from any gateway and check it”Ask a gateway for a CAR file (the folder’s blocks), here ARIADNE’s own, or any public trustless gateway such as
https://trustless-gateway.link:
curl -s -o article.car "https://ipfs.ariadne.press/ipfs/<cid>?format=car"Then check every block against its hash and recompute the folder’s CID with the canonical parameters, using the indexer’s code:
cd projects/indexernode --input-type=module -e "import { readFileSync } from 'node:fs'import { filesFromCar, computeDirectoryCid } from './src/content.ts'const [car, cid] = process.argv.slice(1)const files = await filesFromCar(new Uint8Array(readFileSync(car)), cid)if (!files) throw new Error('a block does not match its hash, or the CAR is incomplete')console.log([...files.keys()].join('\n'))console.log(await computeDirectoryCid(files) === cid ? 'CID reproduced' : 'CID differs')" /path/to/article.car <cid>Recompute a CID from a folder
Section titled “Recompute a CID from a folder”If you already hold the files (for example from ipfs get <cid> -o my-copy with Kubo, which checks every block itself),
recompute the CID from the folder with the script of the publishing guide (section 7a):
cd spec/fixturesnpm cinode compute-cid.mjs /absolute/path/to/my-copyIt prints the base32 CID, its 36 bytes in hex (01 70 12 20 followed by the sha2-256 digest) and the file list. Give
an absolute path: a relative one is read from spec/fixtures. The reference implementation is Kubo 0.43.x, in a fresh
repository without an import profile; to compute without storing anything, add --only-hash and leave out --pin:
ipfs add -r -Q --only-hash --cid-version=1 --raw-leaves=true --chunker=size-262144 --hash=sha2-256 --max-file-links=174 --max-directory-links=0 --max-hamt-fanout=256 ./my-copyNever use -w (--wrap-with-directory): it adds a second directory level. The repository’s fixture
spec/fixtures/sample-article must give bafybeidzsrohfscy7xlt4dh53j3kmlxunjl3r2354pvtfn6pysnjpmduna with every method.
Check a review or a comment
Section titled “Check a review or a comment”Reviews and comments are one Markdown file each, stored with a raw CID (bafkrei...): the sha2-256 hash of the bytes
themselves. Fetch the file from any gateway and hash it (in spec/fixtures, after its npm ci):
curl -s -o review.md "https://ipfs.ariadne.press/ipfs/<cid>"cd spec/fixturesnode --input-type=module -e "import { CID } from 'multiformats/cid'import * as raw from 'multiformats/codecs/raw'import { sha256 } from 'multiformats/hashes/sha2'import { readFileSync } from 'node:fs'const bytes = readFileSync(process.argv[1])console.log(CID.create(1, raw.code, await sha256.digest(bytes)).toString())" /path/to/review.mdThe printed CID must equal the one in the Reviewed or Commented event.
Compare the header with the chain
Section titled “Compare the header with the chain”Open index.md and compare its header with the on-chain record: field, secondary_field, type, parent and the
first address of authors must match (see The article header). This is what the
indexer reports as the content’s validation_status.
5. Rebuild the indexer from the chain
Section titled “5. Rebuild the indexer from the chain”The strongest check is to build your own copy of everything ariadne.press shows. The indexer starts from an empty
database at the application’s creation round, reads every event, and derives the same tables. From projects/indexer:
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts ingestARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts statusingest catches up through the Algorand indexer named in the manifest and stops; status prints the round reached and
the number of events, articles, reviews and comments. Compare them with the public ones:
curl -s https://api.ariadne.press/testnet/statusrebuild drops every table derived from the events and replays them from the stored events; the tests prove the result
is identical to ingesting step by step:
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts rebuildTo get the content checks, the seal, the citations and the Thread Score, run the full indexer and let its background loops work for a few minutes. It reads content from the manifest’s IPFS gateway, verifying every block as above:
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts serveYour API answers on http://localhost:3000, with the same routes as the public one (see
Indexer API and Run an indexer).
6. Compare with the public API
Section titled “6. Compare with the public API”Ask both indexers the same question and compare the answers, for example with jq:
curl -s http://localhost:3000/articles/1 | jq '.article | {status, version, cid, vote_total, seal_detail, citations}'curl -s https://api.ariadne.press/testnet/articles/1 | jq '.article | {status, version, cid, vote_total, seal_detail, citations}'| Value | Should be equal | Why it could differ |
|---|---|---|
| Status, version, CID, votes, reviews, comments, flags, reputation | always, once both have reached the same round | one indexer is behind the other (compare watermark in /status) |
content.validation_status |
yes, once both have read the content | the content was not reachable from one of them |
The ranking score |
yes, at the same moment | it depends on the current time |
seal, seal_detail |
yes, with the same review texts and the same ORCID and OpenAlex answers | a review text not read yet; an ORCID record or OpenAlex data that changed between the two checks |
citations.thread (Thread Score) |
yes, on the same day and with the same content and DOI checks | percentiles are recomputed every day, because articles age |
The seal and the Thread Score are reproducible on purpose: each seal names the version of its rule (rule, now 1),
and their parameters are in projects/indexer/src/seal.ts and projects/indexer/src/citations.ts. See
The Peer-reviewed seal and Citations and Thread Score.
What the chain alone cannot prove
Section titled “What the chain alone cannot prove”- ORCID iDs and DOIs. The chain records that an address declared an ORCID iD or a DOI. Whether the iD belongs to
that person, or the DOI record points back to the article, is a check against ORCID, Zenodo or DataCite, which the
indexer caches. Repeat it yourself: the ORCID record must list the profile page
(
https://ariadne.press/testnet/u/<address>on TestNet) among its websites, and the DOI record must say it is identical to the article page and, for one version, to that version’s CID. - Availability. A CID proves what the content is, not that someone still keeps it. If nobody pins a version, no gateway can serve it; anything you do obtain can still be checked.
- What lives only in browsers or outside services. Private lists, read marks of notifications and
.algonames (from NFD) are not on the chain.
See What is on the blockchain, and what is not for the complete picture.