Skip to content

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
  • The network’s entry in spec/networks.json (see Networks and addresses). For TestNet: application 772983107, created in round 67852714, genesis hash SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/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/indexer
    npm ci
  • An Algorand node and indexer. The manifest names AlgoNode’s public services (https://testnet-api.algonode.cloud and https://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.

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:

Terminal window
curl -s "https://testnet-idx.algonode.cloud/v2/applications/772983107" > app.json
node -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.

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):

Terminal window
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):

Terminal window
cd projects/indexer
node --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:):

Terminal window
curl -s "https://testnet-idx.algonode.cloud/v2/transactions?tx-type=pay&min-round=67852714&note-prefix=YXJpYWRuZS9saXN0cy83NzI5ODMxMDc6"

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:

Terminal window
cd projects/indexer
node --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 1

You 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.

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.

Terminal window
cd projects/indexer
node --input-type=module -e "
import { cidHexToText } from './src/content.ts'
console.log(cidHexToText(process.argv[1]))
" 0170122079945c72c858fdd73e0cfdda76a62ef46a57b8eb7de3eb32b7cfc49a97b07468

This 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:

Terminal window
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:

Terminal window
cd projects/indexer
node --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>

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):

Terminal window
cd spec/fixtures
npm ci
node compute-cid.mjs /absolute/path/to/my-copy

It 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:

Terminal window
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-copy

Never 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.

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):

Terminal window
curl -s -o review.md "https://ipfs.ariadne.press/ipfs/<cid>"
cd spec/fixtures
node --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.md

The printed CID must equal the one in the Reviewed or Commented event.

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.

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:

Terminal window
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts ingest
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts status

ingest 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:

Terminal window
curl -s https://api.ariadne.press/testnet/status

rebuild 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:

Terminal window
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts rebuild

To 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:

Terminal window
ARIADNE_NETWORK=testnet ARIADNE_DB=./verify-testnet.sqlite node src/main.ts serve

Your API answers on http://localhost:3000, with the same routes as the public one (see Indexer API and Run an indexer).

Ask both indexers the same question and compare the answers, for example with jq:

Terminal window
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.

  • 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 .algo names (from NFD) are not on the chain.

See What is on the blockchain, and what is not for the complete picture.