Skip to content

Protocol specification

The protocol is now called Cabdell (D-165, 2026-10-11). This specification keeps its file name and the name ARIADNE in its text and in the identifiers it defines (the contract, its events and boxes, the per-article tokens Ariadne Article / ARIA), because they are fixed on chain until the next contract. Read ARIADNE as Cabdell.

Contract v4 is the MainNet candidate (D-163, decided by the owner after the adversarial audit of 2026-10-08: contract C-1..C-13, economics E-1..E-18, parity P-1..P-15). It is a new application: TestNet restarts, as at D-110. The sections below are rewritten where v4 changes them; the v2.x text they replace is not kept inline.

  • Disputes (sections 5, 6, 7.3, 7.4, 9). A flag weighs min(1 + isqrt(rep), 10), so a dispute needs three flaggers at least; the flagger’s reputation box in the primary field must be 30 days old (since); every flag carries a 1 ALGO bond in its payment. A round that never became a dispute lapses 30 days after its first flag. The threshold is always 30; the bond doubles per clearance by governance for 365 days after the last one (1 ALGO << clearances, at most x16). The flag that opens a dispute records disputed_at and creates the round’s outcome box. Anyone ends a dispute with expire_dispute after 60 days (not a clearance); governance clears at once or retracts only 14 days after the dispute started, publishing the CID of its reasons; the submitter may retract a disputed article. A dispute is a label, not a freeze: votes, claims, confirmations and resolutions stay open (new versions do not). settle_flag settles every bond once: back to the flagger when the round was retracted or lapsed; half to the submitter and half retained by the application when it was cleared; all retained when it expired. (Amended on 2026-10-08 after the economics re-run of v4 found self-immunisation: an author’s own aged accounts flagged his article so that the clearance or the expiry paid him the bonds and raised the threshold for honest flaggers.)
  • Damping (sections 6, 8, 10). One counter per (giver, receiver): a review, comment or article vote weighs w // n, n = 1 + the giver’s earlier grants to that receiver (for an article vote, the largest over all its authors, which the voter lists); a comment resolution grants 5 // n, and only when the resolver holds reputation 10 in the field. This replaces the per-pair rule sketched in D-150.
  • Governance (section 12.1). Two steps: propose_governance by governance (never zero, the current one or the application itself), accept_governance signed by the proposed address. set_governance is gone.
  • Other rules. Amendments only by an accepted author of the parent (C-6); DOIs only by the submitter (C-7); Followed.on canonical (C-8); resolve_comment takes a payment.
  • Events (section 11). schema_version = 2 and a global event_seq (1 for the creation event, +1 per event) in every event; DisputeResolved names who resolved and the reasons, Flagged carries the bond and the threshold (always 30), Voted the raw weight and n, FieldAdded its time; new FlagSettled (with what the application retained) and GovernanceProposed.
  • Invariant 22 restated (section 17): the spendable balance never falls below the bonds held; every bond is paid out or retained exactly once; retained amounts only accumulate.
  • Corrections from the parity audit (P-14): the section 7.4 table (confirmation in final), the section 12 method list (create, extend, pay, coauthors), the section 11 event list and sizes, the section 8.0 and 10.3 text superseded by D-072, cap_day of a new box, the chain_events key of section 15, the numbering of section 17, and the section 18 claims that were not true (“comment-resolution farming: prevented”, “the indexer surfaces duplicates”).
  • Program 7 307 of 8 192 bytes; costs and opcode budgets in projects/contracts/V4_REPORT.md.

Changelog v2.8 (M1 amendments, 2026-09-29)

Section titled “Changelog v2.8 (M1 amendments, 2026-09-29)”
  • D-023 (owner): article_id is a contract-assigned sequence; the article box stores asa_id. Reopens the section 2 row “Article identity”; the table text is left as written and the amendment lives here and in DECISIONS.md.
  • D-024 (owner): ARC-19 template url and reserve = CID digest from M1 (supersedes the section 7.1 DECISION about M5).
  • D-001 / D-003: pay transaction argument on box/asset-creating methods; create(governance) method (section 12).
  • D-027 / D-045: pqsig fee is a flat +2 minimum fees (usage model), not a per-byte surcharge; algokey pq check-address wording (sections 12.1, 13.2).
  • D-026 (owner): Pinata replaces web3.storage (sections 3, 14.4).
  • D-029: LocalNet pinned through a repo-owned docker-compose (section 3).
  • D-002: section 20 status-matrix criterion split between LocalNet and offline tests.
  • D-034 / D-047: taxonomy example wording and field-name limit of 96 bytes (section 4).
  • D-035: section 14.5 canonical parameters completed.
  • D-037: section 21 “decoding via ARC-56 client” replaced by the in-repo ARC-28 decoder / algokit-subscriber.
  • Editorial: section 3.1/3.2 order, section 16 numbering, bytes36 = ARC-4 byte[36], section 13.3 figures (article box 192 B, 82 900 µAlgo, publish deposit 212 200 µAlgo).

The inline edits are applied progressively during M1 and tagged EDIT D-xxx.

Changelog v2.9 (M2 amendments, 2026-09-29)

Section titled “Changelog v2.9 (M2 amendments, 2026-09-29)”
  • D-054 (owner): multi-author allocation = equal split of the vote weight among the accepted authors, remainder to the submitter. Closes the section 10.2 M2 gate and the section 22 open question. (Its first implementation, with three accumulators in the article box, was replaced by D-072: see changelog v2.10.)
  • D-055: pay argument on every M2 method that creates a box; status columns of the M2 methods (sections 7.4, 12).
  • D-056: box sizes of review, reviewer index, comment, flag (section 13.3).
  • D-057: pushed grants read the recipient’s box only for a real grant; event order (sections 10.2, 11).
  • D-058 / D-020 / D-065: Flagged carries new_status; flags keep accumulating while disputed; DisputeResolved.dispute_round is the resolved round; the last round (65 535) takes no flags (sections 7.4, 9, 11).
  • D-059 / D-061: mentions are passed in strictly ascending order of the raw address (sections 8.3, 11, 12).
  • D-060: updated_at is stamped only by calls that change an article field (section 6.4).
  • D-064: helpers are inlined; every method fits one application call with at least 120 opcodes to spare (section 21 C).
  • D-066: readings of section 8 confirmed by tests (reviewer who later becomes a co-author, votes on resolved comments).
  • New section 20.1: M2 acceptance criteria, in the style of section 20.

Changelog v2.10 (co-author model, 2026-09-29)

Section titled “Changelog v2.10 (co-author model, 2026-09-29)”
  • D-072 (owner, reopens the section 2 row “Multi-author”): the co-authors are declared in publish (at most 25, ascending) and never change; each one confirms by signing, in preprint, under_review or final, unless they already voted on or reviewed the article; every accepted author is entitled to an equal share of ALL votes since publication (floor for co-authors, ceiling for the submitter). invite_coauthor and CoauthorInvited are removed; the article box is 192 bytes again. It supersedes the mechanics of D-054 described in changelog v2.9 (sections 2, 6.4, 7.4, 7.6, 8.0, 10.2, 10.3, 11, 12, 13.3, 17, 18, 20.1).
  • D-073: extend() lends opcode budget and box references to a publication with many co-authors; the Layer C gate counts application calls (sections 12, 21 C).

Changelog v2.11 (M3 indexer and M4 hosting, 2026-10-01)

Section titled “Changelog v2.11 (M3 indexer and M4 hosting, 2026-10-01)”
  • D-076: chain_events primary key is (tx_id, log_index) (one application call emits up to three events); the indexer decodes the logs with the same ARC-56 definition as the contract tests (section 15).
  • D-077: the sample-article fixture’s authors[0] is the real zero address; the fixture CID changed accordingly (section 14.5).
  • D-084: the manifest gains startRound, algoIndexer and fetchGateway (section 3.2); the indexer pins a second copy through its own Kubo node and serves a gateway that never fetches (sections 14.4, 15).
  • D-085: Path A uploads the CAR built in the browser to Pinata with the author’s own key; Path B verifies through a public trustless gateway (section 14.4).
  • D-086: hosting is one server with Docker Compose and Caddy; the app, the indexer APIs and the IPFS gateway are three origins (sections 15, 16).

Name: Ariadne (tagline: “Ariadne — open science on Algorand”). Domain and EU trademark availability still to be checked before launch.

This document is normative. Words in MUST / MUST NOT / SHOULD / MAY are requirements. Every MUST is expected to have a corresponding test. When something is ambiguous, prefer the simplest option, implement it, and record it in DECISIONS.md at the repo root. Do not silently reopen anything in §2.


Ariadne is an open scientific publishing protocol where every action is a signed Algorand transaction: publishing, versioning, reviewing, voting, commenting, flagging. Article content lives on IPFS; Algorand holds canonical metadata, the content identifier and the complete action history. There is no editorial gatekeeper; quality emerges from field-specific, reputation-weighted voting.

Principles:

  1. Open access. No platform fees for authors or readers. Network fees and storage deposits exist and are always paid by the user performing the action (§13). The platform never subsidises and never pays on anyone’s behalf.
  2. Append-only. Nothing is ever deleted. State changes are appended; retraction is a state, not a removal.
  3. Permissionless. Anyone can act from day one with reputation 0. Reputation is earned, never granted.
  4. Durable content. Plain text (Markdown + LaTeX) plus its own assets, all under one content identifier, so it stays readable in 30 years.
  5. English as the language of publication. UI is minimalist, readable, modern.
  6. Chain is truth. Everything off-chain (indexer, web) is a projection that can be rebuilt from chain data. [EDIT D-107/D-108: the checks of declared ORCID iDs and DOIs are cached results of outside services (ORCID, Zenodo, DataCite), like the indexer’s content record of IPFS: the declarations are rebuilt from the chain; the checks are asked again from those services.]

2. Closed product decisions (do not reopen)

Section titled “2. Closed product decisions (do not reopen)”
Topic Decision
Storage Article content on IPFS as a directory (index.md + assets/), pinned. Chain stores the directory CID + metadata. Contract never reads IPFS.
Article identity Each article is an ASA created by the contract via inner transaction. article_id == asa_id. ARC-71 Non-Transferable ASA, the application being the Issuer (§7.1). Kept deliberately for wallet/explorer visibility; the article box, not the asset, is the authority on authorship.
Costs Every fee and every minimum-balance deposit is paid by the user whose action causes it. See §13 and invariant 22.
Voting Upvotes only. No downvotes.
Vote weight w = 1 + isqrt(rep) where rep is the voter’s reputation in the article’s primary field at vote time.
Reputation Per (address, field). Earned mostly from reviews others found useful; smaller amounts from article upvotes; tiny amounts from comments. Integer-valued, deterministic. Daily cap per (address, field). Article-vote reputation is pull-based: votes accumulate on the article; each author materialises their share with claim_reputation (§10.2). Review/comment reputation is pushed at vote time.
Decay Very slow, years. Not implemented in M1. updated_at stored on every reputation record so lazy decay can be added later.
Garbage filtering Bad content simply gets no votes and sinks. Unacceptable content (plagiarism, spam, fabricated data) is handled by flags (§9), which are not votes.
Fields Fixed two-level taxonomy (area → subfield) based on OECD Fields of Science. Governance-managed. Article has one primary field (100% rep) and optional secondary (50% rep). Both immutable after publish.
Status / type Two separate on-chain fields. status is a controlled lifecycle (§7.3). type is a controlled enum (§6.3); extending it requires governance or a contract version.
Format Markdown with LaTeX math. Mandatory front-matter header. Validated off-chain (web + indexer).
Comments Informal comments/questions with nested replies and @mentions. Tiny reputation. Author can mark a comment as resolved for a real grant.
Profiles Not stored. Derived by the indexer from on-chain history.
Identity Wallet is the identity. An ORCID iD can be linked by a signed declaration (contract v3), verified by a link back from the public ORCID record; it never confers reputation. [EDIT D-106/D-107: reopened by the owner on 2026-10-01 (was “ORCID link (signed attestation) is v2”).]
Multi-author Protocol-native co-authorship (§7.6): the submitting wallet invites, each co-author accepts by signing. Only accepted co-authors count. Editing rights stay with the submitter. Author count limited only by the counter width, 65 535 (pull model makes vote cost independent of it). Implemented in M2; the data model reserves it from M1.

[EDIT D-072 (OWNER, 2026-09-29; the table text above is left as written): the row “Multi-author” now reads: the submitting wallet DECLARES the co-authors when it publishes; each co-author accepts by signing; only accepted co-authors count; editing rights stay with the submitter; at most 25 co-authors per article; nobody can be added after publication.] [EDIT D-023 (owner decision): the row “Article identity” is amended: article_id is a contract-assigned sequence and the ASA id is stored in the article box; the ASA is still created (ARC-71). See the changelog above and DECISIONS.md.]


  • Chain: Algorand, node software ≥ 5.0 (native post-quantum accounts, AVM v13). LocalNet for development and tests, TestNet for deployment. MainNet deployment is a separate, later decision (after audit) but every component is multi-network from the start (§3.2). The AlgoKit LocalNet image MUST be pinned to a 5.x release so that pqsig transactions are accepted in tests. [EDIT D-029: AlgoKit 2.10.2 hardcodes algorand/algod:latest and has no pin option; the pin is the repo-owned localnet/docker-compose.yml (algod 5.0.2-stable by digest, consensus V42).]
  • Contracts: AlgoKit + Algorand Python (Puya). ABI: ARC-4. Application spec: ARC-56 (generated by Puya; used for typed clients). Events: ARC-28. State: box storage via BoxMap.
  • Contract testing: algorand-python-testing (offline AVM emulation) + algokit-utils (Python) against LocalNet + simulate before every real send.
  • Indexer: TypeScript (Node). @algorandfoundation/algokit-subscriber for event ingestion with watermark persistence and catch-up. SQLite (Postgres later). REST/JSON API.
  • Frontend: Next.js + TypeScript + Tailwind. Wallet via @txnlab/use-wallet. Typed contract client generated from the ARC-56 spec. Markdown: react-markdown + remark-math + rehype-katex, with sanitisation (§18). CID handling: multiformats (+ ipfs-unixfs / @helia/unixfs for directory CIDs).
  • Storage: IPFS via a pinning provider (Pinata; web3.storage/Storacha was decommissioned in 2026-05, a second provider is chosen in M5 [EDIT D-026]) behind a StorageProvider interface that pins directories.
  • Versions: pin every tool and library version at init (AlgoKit CLI, Puya, algokit-utils, subscriber, Next.js) and record them in DECISIONS.md. Never float on latest.

The same contract code is deployed once per network; each deployment is an independent Ariadne (different app id, different articles, different reputation). Nothing on-chain references a network; the transaction’s genesis hash does that. IPFS content is network-agnostic: the same CID can be published on TestNet and MainNet.

A single manifest, /spec/networks.json, is the only place networks are described. Deployment scripts write it; indexer, web and CLI read it:

{
"mainnet": { "genesisId": "mainnet-v1.0", "genesisHash": "...", "appId": 0,
"algod": "https://...", "indexerApi": "https://api.ariadne.../mainnet",
"ipfsGateway": "https://...", "explorer": "https://.../mainnet", "enabled": false },
"testnet": { "genesisId": "testnet-v1.0", "genesisHash": "...", "appId": 123456,
"algod": "https://...", "indexerApi": "https://api.ariadne.../testnet",
"ipfsGateway": "https://...", "explorer": "https://.../testnet", "enabled": true },
"localnet": { "genesisId": "...", "appId": 1001, "algod": "http://localhost:4001", "indexerApi": "http://localhost:3000", "enabled": true, "devOnly": true }
}

Rules: [EDIT D-084: every entry may also carry startRound (the round the application was created in, written by the deploy script: a fresh indexer starts there), algoIndexer (an Algorand indexer used only to catch up) and fetchGateway (a public trustless gateway used by the web app to verify a CID pasted by an author).]

  • A network with appId = 0 or enabled = false is not offered in any UI. MainNet stays that way until the audited deployment exists; enabling it is a manifest change, not a code change.
  • devOnly networks are compiled out of production builds.
  • Every signed transaction MUST carry the genesis hash of the selected network, and the web app MUST verify that the connected wallet’s network matches before enabling any sign button (§16.1).
  • Governance (§12.1) is per network: a separate PQ governance account for TestNet and for MainNet, never shared.
/ AlgoKit workspace root
DECISIONS.md
/spec this file, networks.json, taxonomy.json, article-template.md, notes-template.md
/projects
/contracts AlgoKit Python (Puya) project — official template, structure untouched
/indexer TS service: chain → events → projections → SQLite → API
/web Next.js app

/spec/taxonomy.json — OECD Fields of Science: 6 areas, ~40 subfields. Each subfield has a stable field_id: uint16 (area in the high byte, subfield in the low byte, e.g. 0x0303 = Medical and health sciences / Health sciences [EDIT D-034: sport science is a third-level example inside 3.3; 2.10/2.11 encode as 0x020A/0x020B; names are at most 96 bytes (D-047)]). 0 is reserved for “none”.

  • Fields live in contract state as an allow-list (box t + uint16 → name).
  • Only the governance address MAY add fields. Fields are never removed or renamed (append-only).
  • primary_field MUST be a registered field. secondary_field MUST be 0 or a registered field different from primary_field.

5. Constants (initial values, tune after M1 measurements)

Section titled “5. Constants (initial values, tune after M1 measurements)”
Constant Value Meaning
GRANT_REVIEW_NUM/DEN 3 / 1 review-useful grant multiplier
GRANT_COMMENT_NUM/DEN 1 / 5 comment upvote grant multiplier
GRANT_RESOLVED 5 grant when a comment is resolved, damped: GRANT_RESOLVED // n (§10.2) [EDIT D-163]
REP_CAP_DAY 50 max reputation received from all sources per (address, field, epoch day)
MIN_FLAG_REP 10 min reputation in the article’s primary field to flag; also the minimum of a resolver whose resolution grants anything [EDIT D-163]
FLAG_THRESHOLD 30 cumulative flag weight in the current dispute round that triggers disputed; it never grows (§9) [EDIT D-163 (amended)]
FLAG_WEIGHT_CAP 10 a flag weighs min(1 + isqrt(rep), 10): a dispute needs three flaggers at least [EDIT D-163]
FLAG_BOND 1 000 000 µAlgo bond of a flag, held by the application until settle_flag; FLAG_BOND << clearances within 365 days of the last clearance by governance (§9) [EDIT D-163 (amended)]
FLAG_REP_AGE_DAYS 30 age of the flagger’s reputation box in the primary field [EDIT D-163]
FLAG_WINDOW_DAYS 30 a round that never became a dispute lapses this long after its first flag [EDIT D-163]
DISPUTE_TIMEOUT_DAYS 60 anyone may expire a dispute this long after it started [EDIT D-163]
RETRACT_DELAY_DAYS 14 governance may retract a disputed article only this long after the dispute started [EDIT D-163]
CLEARANCE_MEMORY_DAYS / MAX_CLEARANCES 365 / 4 the bond stays doubled per clearance by governance this long after the last one, at most four doublings [EDIT D-163 (amended)]
EPOCH_DAY_SECONDS 86400 epoch_day = latest_timestamp // 86400; every “day” above is 86 400 seconds of latest_timestamp
MAX_CONTENT_BYTES 20 MB article directory size limit (off-chain, web + indexer)
MAX_COMMENT_DEPTH 6 max nesting depth of a reply (top-level comment = depth 0)
MAX_MENTIONS 5 max distinct addresses mentioned per comment

All arithmetic is uint64 integer arithmetic. There are no decimals anywhere in the protocol. Every timestamp the contract reads is Global.latest_timestamp, the time of the previous block (D-050); events carry that value as ts.


  • All box keys are binary, ≤ 64 bytes. Addresses are the raw 32-byte public key, never the 58-char text form.
  • Integers are big-endian fixed width as stated.
  • CIDs are stored as raw CIDv1 bytes, fixed 36 bytes: 0x01 (version) + codec + 0x12 0x20 (sha2-256, 32 bytes) + 32-byte digest.
    • Article content CID MUST have codec 0x70 (dag-pb: a UnixFS directory).
    • Review and comment CIDs MUST have codec 0x55 (raw: a single Markdown file).
    • The contract MUST reject any other version/codec/hash prefix, any length ≠ 36, and the all-zero digest. // DECISION: sha2-256 only in M1; extend via contract version.

[EDIT D-023: every uint64(asa_id) below reads uint64(article_id), the contract-assigned sequence; the ASA id is stored inside the article box.]

Box Key Bytes
field b"t" + uint16(field_id) 3
article b"a" + uint64(asa_id) 9
co-author b"au" + uint64(asa_id) + addr32 42
review b"r" + uint64(asa_id) + uint64(review_seq) 17
reviewer index b"ri" + uint64(asa_id) + addr32 → uint64(review_seq) 42
comment b"c" + uint64(asa_id) + uint64(comment_seq) 17
vote (article) b"va" + uint64(asa_id) + addr32(voter) 42
vote (review) b"vr" + uint64(asa_id) + uint64(review_seq) + addr32(voter) 50
vote (comment) b"vc" + uint64(asa_id) + uint64(comment_seq) + addr32(voter) 50
flag b"f" + uint64(asa_id) + uint16(dispute_round) + addr32(flagger) 43
reputation b"p" + addr32 + uint16(field_id) 35
dispute outcome b"o" + uint64(article_id) + uint16(dispute_round) 11
damping counter b"d" + sha256(addr32(giver) + addr32(receiver)) 33

Every key length MUST be asserted in tests. [EDIT D-163: the outcome box is created by the flag that opens a dispute and written by whatever ends it; the damping counter is created by the giver’s first grant to that receiver; neither is ever deleted. The counter key hashes the pair because prefix + two addresses would be 65 bytes. A flag box is deleted by settle_flag (section 9), the only box the contract ever deletes.]

type: 1 article | 2 notes | 3 amendment | 4 dataset | 5 replication
status: 1 preprint | 2 under_review | 3 final | 4 disputed | 5 retracted
recommendation: 1 accept | 2 minor_revision | 3 major_revision | 4 reject
flag_reason: 1 plagiarism | 2 spam | 3 fabricated_data | 4 other
vote_target: 1 article | 2 review | 3 comment
dispute_outcome: 1 cleared | 2 retracted | 3 expired # EDIT D-163: 3 = ended by timeout
rep_reason: 1 article_upvote | 2 review_useful | 3 comment_upvote | 4 comment_resolved

0 is invalid for every enum. [EDIT D-163: two exceptions: the outcome box holds 0 while its dispute is open (pending), and FlagSettled.outcome is 0 for a round that never became a dispute (lapsed). resolve_dispute accepts only 1 and 2; 3 comes only from expire_dispute.]

article

author addr32 immutable (submitting wallet)
primary_field uint16 immutable
secondary_field uint16 immutable (0 = none)
type uint8 immutable
parent_article uint64 immutable (required ≠ 0 iff type == amendment)
status uint8
status_before_dispute uint8 (0 unless status == disputed)
version uint16 starts at 1
current_cid bytes36
prev_cid bytes36 (all-zero for version 1)
created_at uint64
updated_at uint64
vote_total uint64 sum of the (damped) weights, monotonic <span class="spec-edit">[EDIT D-163]</span>
vote_count uint64 monotonic
review_seq_next uint64 starts at 1
comment_seq_next uint64 starts at 1
dispute_round uint16 starts at 1
round_flag_weight uint64 cumulative flag weight in current round
author_count uint16 1 + accepted co-authors
invited_count uint16 co-authors declared in publish (accepted or not), immutable <span class="spec-edit">[EDIT D-072]</span>
claimed bool
asa_id uint64 immutable (the ARC-71 ASA created by publish) <span class="spec-edit">[EDIT D-023]</span>
round_started_at uint64 ts of the first flag of the current round (the round lapses 30 days later) <span class="spec-edit">[EDIT D-163]</span>
disputed_at uint64 ts at which the last dispute started (expiry and retraction delay count from it) <span class="spec-edit">[EDIT D-163]</span>
cleared_at uint64 ts of the last clearance by governance (an expiry is not one) <span class="spec-edit">[EDIT D-163 (amended)]</span>
clearances uint8 clearances by governance so far, saturating at 4 <span class="spec-edit">[EDIT D-163 (amended)]</span>

[EDIT D-072: value = 192 bytes. The three accumulators added by the first implementation of D-054 are gone: with a fixed author list the shares are computed from vote_total at claim time. EDIT D-163: the four dispute fields are appended (every earlier offset is unchanged): value = 217 bytes.] [EDIT D-060: updated_at is stamped by every call that changes an article field; vote_review, vote_comment and resolve_comment do not write the article box.]

co-author (one per accepted or invited author, including the submitter, whose box is created by publish with accepted = true)

invited_at uint64
accepted bool
accepted_at uint64
claimed_total uint64 entitlement already converted into reputation by this author; starts at 0 for everybody <span class="spec-edit">[EDIT D-072]</span>

review

reviewer addr32
cid bytes36
recommendation uint8
created_at uint64
useful_total uint64 weighted "useful" votes, monotonic

comment

author addr32
cid bytes36
reply_to uint64 parent comment_seq (0 = top level)
root_seq uint64 comment_seq of the thread root (== own seq for top level)
depth uint8 0 for top level, parent.depth + 1 otherwise
created_at uint64
vote_total uint64
reply_count uint64 direct replies, monotonic
resolved bool
resolved_at uint64

Mentions are not stored in the comment box (no MBR for them); they live only in the Commented event and are projected by the indexer.

vote (all three kinds)

weight uint64 damped weight w // n applied at vote time, immutable <span class="spec-edit">[EDIT D-163]</span>
granted uint64 reputation pushed at vote time (review/comment votes); always 0 for article votes (pull, §10.2)
created_at uint64

flag

reason uint8
weight uint64 min(1 + isqrt(rep), 10) <span class="spec-edit">[EDIT D-163]</span>
created_at uint64
bond uint64 the bond held for this flag (what its payment deposited) <span class="spec-edit">[EDIT D-163]</span>

[EDIT D-163: 25 bytes. The bond is recorded so that a later change of the constant never pays out a different amount.]

reputation

rep uint64 never decreases
updated_at uint64
cap_day uint32 epoch day of the last grant; a new box takes its creation day
cap_today uint16 reputation received on cap_day (all sources)
since uint32 epoch day the box was created (flags need it 30 days old) <span class="spec-edit">[EDIT D-163]</span>

[EDIT D-163: 26 bytes.]

dispute outcome (one per round that became a dispute) [EDIT D-163]

outcome uint8 0 while the dispute is open, then 1 cleared | 2 retracted | 3 expired

damping counter (one per (giver, receiver)) [EDIT D-163]

grants uint64 earlier grants from the giver to the receiver (votes of every kind, resolutions)

Full version history is not stored in boxes; it is reconstructed from Versioned events (§11). The prev_cid link is a convenience for light clients.


Because an ASA recipient MUST opt in before receiving the asset, and the asset id does not exist until the contract creates it, publishing and claiming are separate actions.

The article ASA is an ARC-71 Non-Transferable ASA (NTA). The application account is the ARC-71 Issuer (ARC-71 requires the Issuer to be a smart-contract account). The three ARC-71 states map onto the article lifecycle:

ARC-71 state Ariadne ASA parameters
Issued published, unclaimed clawback = ZeroAddress, freeze = app, manager = app, reserve = app (or ARC-19 metadata, see below), held by the app
Held claimed by the author clawback = ZeroAddress, freeze = ZeroAddress, holder’s holding frozen, manager = app
Revoked retracted manager = ZeroAddress (set by the contract on the transition to retracted, whether by author or governance)

Creation parameters:

total = 1, decimals = 0
default_frozen = true
clawback = ZeroAddress # never set; ARC-71 MUST
freeze = application address # Issued
manager = application address
reserve = digest of the current CID, as an address # EDIT D-024: ARC-19 from M1 (the url is immutable, so the template must exist at creation)
unit_name = "ARIA"
asset_name = "Ariadne Article"
url = "template-ipfs://{ipfscid:1:dag-pb:reserve:sha2-256}" # EDIT D-024: ARC-19 template (51 bytes); reserve carries the digest and new_version moves it

Because default_frozen does not apply to the creator’s own holding, the application can transfer the asset without unfreezing itself.

  • publish() creates the ASA (inner asset-config), creates the article box, creates the sender’s co-author box with accepted = true, claimed_total = 0, records the sender as immutable author, and leaves the ASA in the application account (ARC-71 Issued). It does not create reputation boxes (§10.5).
  • claim_article() is optional. Preconditions: sender == author, sender opted in (paying their own opt-in MBR), claimed == false. The contract, in one atomic sequence of inner transactions: (1) unfreezes the author’s holding, (2) transfers the asset, (3) refreezes the author’s holding, (4) sets freeze = ZeroAddress via asset-config (ARC-71 Held, irreversible). claimed = true. [EDIT D-005/D-007: step (4) passes manager = app, reserve = digest(current_cid), freeze = Zero and clawback = Zero explicitly (an omitted address is zeroed forever); claim_article MUST fail once the article is retracted and is allowed in every other status.]
  • On every transition to retracted the contract sets manager = ZeroAddress (ARC-71 Revoked); the asset parameters become permanently immutable, which is consistent with retracted being terminal. [EDIT D-033: the same inner AssetConfig passes reserve = digest(current_cid), freeze = ZeroAddress (unconditionally) and clawback = ZeroAddress.]
  • Algorand lets any holder close out a frozen ASA back to its creator; ARC-71 explicitly preserves that right. If an author does so, the asset returns to the application account and claimed stays true; authorship is unaffected because it lives in the article box. Invariant 19 still holds.
  • After Held, no address can ever move the asset: clawback and freeze are both zero and the holding is frozen. Tests MUST attempt transfer, clawback and unfreeze from every role and expect failure.
  • new_version() bumps version by exactly 1, sets prev_cid = current_cid, current_cid = new cid, updated_at. [EDIT D-024: it also moves the ASA reserve to the new digest through one inner AssetConfig (manager = app, freeze = Zero if claimed else app, clawback = Zero), so the fee pools one inner transaction.]
  • Allowed only while status ∈ {preprint, under_review}.
  • The new CID MUST differ from current_cid.
  • Version numbers are strictly increasing; history is append-only (events).
From To Actor Via
preprint under_review author set_status
under_review final author set_status
preprint retracted author set_status
under_review retracted author set_status
final retracted author set_status
preprint / under_review / final disputed contract flag reaching FLAG_THRESHOLD (§9)
disputed status_before_dispute governance resolve_dispute(cleared), at any time
disputed retracted governance resolve_dispute(retracted), once 14 days have passed since disputed_at
disputed status_before_dispute anyone expire_dispute, once 60 days have passed since disputed_at
disputed retracted author set_status

All transitions not in this table MUST fail. preprint → final directly is not allowed. retracted is terminal. “author” here means the submitting wallet only. // DECISION: "cleared" restores the pre-dispute status instead of forcing final. [EDIT D-163: the last three rows are new. Every way out of disputed closes the dispute round: it writes the round’s outcome box (1 cleared, 2 retracted, 3 expired), increments dispute_round, resets round_flag_weight and status_before_dispute, and emits DisputeResolved; a clearance by governance also counts as a clearance (§9; D-163 amended: an expiry does not). The submitter’s own retraction emits StatusChanged followed by DisputeResolved (outcome 2, resolved_by = the submitter, zero reasons); a governance retraction emits DisputeResolved only. Both revoke the ASA.]

Status new_version accept co-authorship review vote_article, vote_review, vote_comment claim_reputation resolve_comment comment flag
preprint ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
under_review ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
final ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
disputed ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ (recorded, no status change)
retracted ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✗

[EDIT D-005: claim_article is allowed in preprint, under_review, final and disputed and MUST fail in retracted; set_status follows §7.3.] [EDIT D-072: there are no invitations after publication; confirming is allowed in final too, the list being fixed at publication.] [EDIT D-163: a dispute is a label and a jury case, not a freeze: votes, claims, confirmations and comment resolutions are allowed while disputed (the v2.x table refused them); only new versions stay closed, so the disputed content does not change under the jury. resolve_dispute and expire_dispute are allowed only in disputed; settle_flag in every status. In the disputed row “recorded, no status change” means: the flag box is written, round_flag_weight keeps accumulating, updated_at is stamped, disputed_at does not move.]

type == amendment requires parent_article ≠ 0, and the parent MUST exist. The parent MAY be in any status. An amendment’s fields SHOULD match the parent’s (enforced by the web app, not the contract). [EDIT D-163 (audit C-6): the sender MUST be an accepted author of the parent (the submitter or a co-author who confirmed); an amendment of an amendment needs an author of that amendment.]

[EDIT D-072 (OWNER): the first two bullets of v2.7 (kept further down for the record) are replaced by these two rules.]

  • Declared at publication. publish(..., coauthors: address[]) takes the complete list of co-authors: 0 to 25 addresses, strictly ascending by raw address (hence distinct), never the submitter. publish creates one co-author box per declared address with accepted = false (the submitter pays the MBR) and stores their number in invited_count. The list never changes: nobody can be added, removed or replaced later.
  • Confirmed by signature. A declared address MAY accept_coauthorship(article_id) once, while status ∈ {preprint, under_review, final, disputed} [EDIT D-163: disputed added]: sets accepted = true, accepted_at, increments author_count; claimed_total stays 0, because the share counts from publication. It MUST fail if that address already voted on the article or reviewed it. A declaration that is never confirmed is inert for that address (it may vote, review and flag like anybody) and its share is never claimed by anyone.

Original text (v2.7), no longer in force:

  • The submitting wallet MAY invite_coauthor(article_id, addr) while status ∈ {preprint, under_review}; addr ≠ author; no existing box for addr. Creates the co-author box with accepted = false (submitter pays MBR). The only limit is the uint16 counters: at most 65 535 invitations per article.
  • The invitee MAY accept_coauthorship(article_id): sets accepted = true, accepted_at, claimed_total = article.vote_total (only votes cast after acceptance ever count for this author), and increments author_count. One-time, irreversible. Invitations are never withdrawn or declined on-chain (append-only); an unaccepted invitation is simply inert. No reputation box is created here (§10.5).

In force:

  • “Accepted author” = submitter or any co-author with accepted = true. Accepted authors receive article-upvote reputation (§10.2) and are subject to every “not own article” rule (§8, §9). They have no editing, status or invitation rights.
  • On-chain authorship is canonical. Front-matter authors[] is informative and frozen per version: it reflects the author set the submitter intended when that version was published, and it is never required to match later acceptances. The indexer displays the on-chain accepted-author set as the article’s authors and shows the front-matter list as “as stated in version N”; a mismatch is a notice, not a validation failure. Accepting a co-author never requires a new version. // DECISION: option 1 (canonical on-chain) over forcing a new version; published content never changes.

8.0 Self-action rule (applies to §8 and §9)

Section titled “8.0 Self-action rule (applies to §8 and §9)”

Every “not own article” / “not own comment” check is evaluated at the time the action is performed, against the accepted-author set as it exists then. Later changes to the accepted-author set (a declared co-author who confirms afterwards) do not invalidate earlier actions; they remain on record and keep their effects. This is safe because an address that voted on the article or reviewed it can never become an accepted author (D-072): nobody claims from their own vote and no reviewer becomes an author of what they reviewed. Flags, comments and comment votes made before confirming stay on record. [EDIT D-072 / P-14: the v2.7 argument (claimed_total starting at vote_total) described the invitation model that D-072 removed.]

  • One review per (article, reviewer). Enforced via the reviewer index box.
  • reviewer MUST NOT be an accepted author of the article (at submission time, §8.0).
  • Reviews are immutable once submitted. A reviewer who changes their mind comments.
  • One vote per (target, voter). Vote boxes are append-only and immutable.
  • Article votes: voter MUST NOT be an accepted author. An article vote writes only the vote box and article.vote_total / vote_count; it never touches any reputation box (pull model, §10.2), so its cost is constant regardless of author count.
  • Review votes: voter ≠ reviewer. An accepted author of the article MAY vote a review “useful”, but that vote grants 0 reputation (recorded with its weight, granted = 0). // Rationale: the simplest review cartel is author↔reviewer; the author's judgement of a review of their own work is conflicted.
  • Comment votes: voter ≠ comment.author.
  • Weight is computed once, at vote time, from the voter’s reputation in the article’s primary field, and stored in the vote box.
  • Damping (D-163). Every vote is damped per (voter, receiver): n = 1 + the voter’s earlier grants to that receiver (one counter per ordered pair, shared by every kind of vote and by comment resolutions), and the vote weighs w' = w // n; w' is what the vote box records, what vote_total / useful_total / the comment’s vote_total add, and what the grant is computed from. The counter then becomes n (for a review vote of an accepted author too, which grants 0). For an article vote the receivers are all its authors: vote_article(pay, article_id, authors) takes the submitter and every declared co-author (accepted or not), strictly ascending, exactly invited_count + 1 addresses each with a co-author box; n = 1 + max over them of the voter’s counter, and every author’s counter grows by one. A weight-1 voter’s second vote for the same person weighs 0: intended. Beyond 8 box references (2 per author + article, vote box, the voter’s reputation and co-author boxes) the group carries extend() calls (D-073).
  • Anyone MAY comment, including accepted authors.
  • Nested replies. reply_to MUST be 0 (top-level) or an existing comment_seq of the same article, at any depth. The contract sets depth = parent.depth + 1, root_seq = parent.root_seq, increments parent.reply_count, and MUST fail if depth > MAX_COMMENT_DEPTH. A reply MAY target a resolved comment. Comments are allowed in every status (§7.4), including retracted.
  • Mentions. comment() takes mentions: address[] (0–MAX_MENTIONS entries). Entries MUST be distinct and MUST NOT include the sender. [EDIT D-061: the list MUST be strictly ascending by the raw 32-byte address (big-endian unsigned); strict order is how distinctness is enforced in one pass. Clients sort before sending; the order carries no meaning.] Mentions carry no reputation effect and no on-chain obligation; they exist so the indexer can build a mentions feed without parsing IPFS content. The comment body SHOULD render each mention as @[label](algo:ADDRESS) (web app inserts and resolves it); the on-chain list, not the body, is authoritative.
  • Replies and mentions are the only notification primitives in the protocol; both are derived by the indexer from events (§15).
  • resolve_comment(): sender MUST be the submitting author; comment MUST exist; comment.author MUST NOT be an accepted author; resolved MUST be false. Resolution is one-time and irreversible. Any depth may be resolved. [EDIT D-163 (audit C-3, E-4): the resolution grants GRANT_RESOLVED // n (n = 1 + the resolver’s earlier grants to the commenter, then the counter becomes n) and only when the resolver holds MIN_FLAG_REP in the article’s primary field; otherwise it grants nothing and touches no counter. The comment is resolved either way. resolve_comment takes a leading payment for the counter of a first grant. A declared co-author who has not confirmed is an ordinary commenter whose comments can be resolved (C-9, kept as D-072).]

[EDIT D-163: this section is rewritten for contract v4 (audit C-1, C-2, C-4, E-1, E-3, E-10, E-11). D-058 / D-065 still hold: an existing reputation box is needed, round_flag_weight accumulates while disputed, the transition emits no StatusChanged (Flagged carries new_status), the last round (65 535) takes no flags.]

  • flag(pay, article_id, reason) requires: status ≠ retracted; flagger not an accepted author; the flagger’s reputation box in the article’s primary field exists with rep ≥ MIN_FLAG_REP and is at least FLAG_REP_AGE_DAYS old (epoch_day ≥ since + 30); no flag by this flagger in the round the flag counts in; and a payment covering the flag box (and, for the flag that opens a dispute, the outcome box) plus the bond: FLAG_BOND << clearances while latest_timestamp < cleared_at + 365 days, else FLAG_BOND (1, 2, 4, 8 or 16 ALGO). The bond is recorded in the flag box and in Flagged, and held by the application.
  • flag_weight = min(1 + isqrt(rep), FLAG_WEIGHT_CAP) at flag time, stored in the flag box: a dispute needs three flaggers at least.
  • Rounds. round_started_at is the time of the first flag of the current round. A round whose status never became disputed lapses FLAG_WINDOW_DAYS after its first flag: the next flag after that moment first closes it (dispute_round + 1, round_flag_weight = 0) and then counts in the new round, which starts at its own time. A disputed round never lapses.
  • Threshold. FLAG_THRESHOLD (30), always: it never grows. [EDIT D-163 (amended): v4 as first merged doubled the threshold per clearance and counted expiries as clearances; an author could immunise his article with his own aged accounts.] clearances counts the clearances by governance (an expiry is not one), saturating at 4; it is never reset, but it only raises the bond within a year of the last clearance. When round_flag_weight ≥ FLAG_THRESHOLD and status ∈ {preprint, under_review, final}, status becomes disputed, status_before_dispute and disputed_at are set, and the round’s outcome box is created with 0 (pending).
  • Flags are immutable and each counts exactly once, only in its round.
  • Ends of a dispute (§7.3): resolve_dispute(article_id, outcome, reasons) by governance: cleared at any time, retracted only once latest_timestamp ≥ disputed_at + RETRACT_DELAY_DAYS; reasons is the 36-byte raw-codec CID of the published reasons, recorded in DisputeResolved. expire_dispute(article_id) by anyone once latest_timestamp ≥ disputed_at + DISPUTE_TIMEOUT_DAYS: the article returns to status_before_dispute, outcome 3. The submitter’s set_status(retracted): outcome 2. Each writes the outcome, increments dispute_round, resets round_flag_weight and status_before_dispute; only a clearance by governance sets clearances = min(clearances + 1, 4) and cleared_at. Flag history from earlier rounds is preserved in the events.
  • Settlement. settle_flag(article_id, dispute_round, flagger), by anyone, once the flag’s round is over: an earlier round, or the current round when it is not disputed and has lapsed. The outcome is the round’s outcome box, or “lapsed” (0) when the round never had one. Retracted (2) or lapsed: the whole bond goes back to the flagger; cleared (1): half the bond (bond // 2) goes to the article’s submitter and the other half is retained; expired (3): the whole bond is retained. “Retained” means it stays in the application account for ever (there is no withdrawal path: it is in effect burned). The flag box is deleted and its deposit goes back to the flagger; if the flagger’s account no longer exists (balance 0) it could not receive less than its minimum balance, so the deposit goes with the submitter’s half (cleared) or is retained too (expired). The payments are inner transactions with fee 0, pooled by the caller (cleared: two; retracted, lapsed, expired: one; fewer for a closed flagger). FlagSettled reports bond_to (the flagger, the submitter, or the zero address when nothing is paid), bond (what went to bond_to, the flagger’s own deposit refund apart) and retained; a flag is settled exactly once (its box is gone). [EDIT D-163 (amended)]
  • A false dispute therefore costs its flaggers their bonds (half of each pays its victim, half is retained) and doubles the bond of the next one for a year, without making it harder to reach; a dispute nobody resolves ends by itself after 60 days and pays nobody, so an author gains nothing by flagging his own article (half of every bond lost when governance clears it, all of it when it expires); a governance retraction comes no sooner than 14 days after the dispute started and publishes its reasons. Disputes freeze nothing but new versions (§7.4).

w(rep) = 1 + isqrt(rep) # isqrt = AVM `sqrt` opcode (integer)

rep 0 → 1, rep 100 → 11, rep 10 000 → 101. A missing reputation box reads as rep = 0; reading never creates a box.

10.2 Grants (integer arithmetic, floor division)

Section titled “10.2 Grants (integer arithmetic, floor division)”
Event Recipient(s) Field(s) Raw grant
article upvote (deferred) — nothing at vote time; article.vote_total += w'. Materialised later by each accepted author via claim_reputation (below)
review-useful upvote review.reviewer article primary w' * GRANT_REVIEW_NUM / GRANT_REVIEW_DEN; 0 if voter is an accepted author
comment upvote comment.author article primary w' * GRANT_COMMENT_NUM / GRANT_COMMENT_DEN
comment resolved comment.author article primary GRANT_RESOLVED // n, and 0 when the resolver’s reputation in the field is below MIN_FLAG_REP

w is the voter’s weight in the article’s primary field at the moment of the vote; w' = w // n is the damped weight (§8.2) and n = 1 + the giver’s earlier grants to that receiver. [EDIT D-163: damping per (giver, receiver), one counter for every kind of grant; it replaces the per-pair rule of D-150. Worked arithmetic: a weight-11 voter’s useful votes for one reviewer grant 33, 15, 9, 6 (11, 5, 3, 2 x 3); a resolver’s resolutions for one commenter grant 5, 2, 1, 1, 1, then 0: at most 10 per pair, ever.]

Daily cap (all sources): for the recipient’s reputation box, if cap_day ≠ epoch_day then cap_day = epoch_day, cap_today = 0. granted = min(raw, REP_CAP_DAY − cap_today); then rep += granted, cap_today += granted. A grant of 0 still records the vote and emits Voted, but emits no ReputationChanged.

Pull: claim_reputation(article_id). Sender MUST hold an accepted co-author box for the article (the submitter always does). Allowed statuses per §7.4.

N = 1 + article.invited_count # EDIT D-072: fixed at publication
entitled = article.vote_total / N # floor; the submitter takes the ceiling
claimable = entitled − au.claimed_total # MUST be > 0, else fail (was article.vote_total − au.claimed_total)
granted_primary = min(claimable, cap_remaining(sender, primary_field))
granted_secondary = min(granted_primary / 2, cap_remaining(sender, secondary_field)) # only if secondary ≠ 0
au.claimed_total += granted_primary

Vote weight and article reputation are 1:1 by definition (there is no multiplier constant for article votes). Whatever the daily cap does not allow stays claimable for a later day, because claimed_total advances only by granted_primary. The secondary-field shortfall is not carried over. // DECISION: simplicity over exactness for the secondary field.

Every accepted author claims independently, on their own schedule, paying their own fee. An author who never claims simply never materialises that reputation; the indexer shows the claimable amount on their profile.

[EDIT D-054 (OWNER, 2026-09-29): the v2.7 “M2 gate” (choose an allocation rule before multi-author claims) is closed: candidate (b), equal split with the remainder to the submitter. EDIT D-072: implemented over a fixed author list.] Allocation rule (D-054, D-072). The author list is fixed at publication, so the number of shares is a constant of the article: N = 1 + invited_count (the submitter plus every declared co-author, confirmed or not). With T = vote_total:

accepted co-author: entitled = T / N # floor
submitter: entitled = (T + N − 1) / N # ceiling: the remainder, as far as it never decreases

Both never decrease and together they never exceed T (they fall short by at most N − 1 weight units, which wait for the next votes). “Floor plus the whole remainder” is not used because it decreases when the total grows (N = 3: T = 5 gives 3, T = 6 gives 2). The share of a declared co-author who never confirms is never claimed by anyone. A single-author article behaves exactly as in M1.

  1. No self-voting, no self-review, no self-resolution, no self-flagging, measured against the accepted-author set at action time (§8.0).
  2. No actor ever receives reputation from their own action.
  3. Grants are integer-valued and deterministic from on-chain state.
  4. Grants use the voter’s weight at vote time, never a later value.
  5. Grants are bounded per target (one vote per target), per (address, field, epoch day) and, since D-163, per (giver, receiver): the n-th grant of one giver to one receiver uses w // n.
  6. Reputation never decreases.
  7. Every non-zero grant emits exactly one ReputationChanged per (address, field).
  8. Article-vote reputation is only ever materialised by the recipient’s own claim_reputation; au.claimed_total ≤ entitlement ≤ article.vote_total, and the entitlements of all authors never add up to more than vote_total. [EDIT D-072]
  9. The author list is fixed at publication; every accepted author is entitled to an equal share of all votes since publication; an address that voted on or reviewed the article can never become an accepted author. [EDIT D-072 (OWNER): replaces v2.7’s “a co-author accepted after votes were cast can never claim those earlier votes” (P-14).]

The protocol is not Sybil-resistant. Reciprocal voting between fresh accounts grows reputation slowly but is not prevented. Since D-163 the damping per (giver, receiver) makes every repeated grant between the same two addresses worth w // n, comment resolutions mint at most 10 per pair and nothing from a resolver below 10, and the flag rules (bond, three flaggers, 30-day-old reputation, a bond that doubles after clearances and is half or wholly lost when the dispute fails) make reputation-based griefing expensive; rings of many fresh addresses remain possible. Detection of coordinated voting is off-chain (indexer). The author↔reviewer loop is closed (§8.2).

A reputation box for (address, field) is created only by an action of that address, and paid by that address:

  • claim_reputation → claimer’s boxes for the article’s primary (and secondary) field, created on first claim.
  • submit_review → reviewer’s box for the article’s primary field (reviews are pushed at vote time, so the box must pre-exist).
  • comment → commenter’s box for the article’s primary field (same reason). publish and accept_coauthorship create no reputation boxes.

Consequently a vote never creates a box for anyone else, and a voter needs no box of their own. If a grant targets an address whose box does not exist (impossible by construction, but asserted), the call MUST fail rather than create it.


Every state-changing method emits exactly one canonical event plus zero or more ReputationChanged (the submitter’s own retraction of a disputed article emits StatusChanged then DisputeResolved). [EDIT D-138: the names below are those of the code (events.py, the ARC-56 file).] [EDIT D-163 (contract v4): every event starts with schema_version: uint8 = 2 and event_seq: uint64, the number of the event among all events of the application (1 for the creation event, then +1 per event, so an indexer detects a missed event as a gap and gets a total order); ts is always last. A decoder keys on (application, selector, schema_version): the v4 signatures differ from v3, so a v3 decoder never reads a v4 log.] Payloads are compact: per application call, logs are limited to 32 entries and 1 024 bytes total, ARC-4 return logs (12 bytes) included. CIDs in events are the 36-byte raw form. Sizes are of the whole log (4-byte selector included).

FieldAdded(v, seq, field_id: uint16, name: string, ts) # 27 + name bytes
GovernanceChanged(v, seq, previous_governance: address, new_governance: address, ts) # 85
GovernanceProposed(v, seq, proposed: address, ts) # 53 (D-163)
Published(v, seq, article_id: uint64, asa_id: uint64, author: address, primary_field: uint16,
secondary_field: uint16, article_type: uint8, parent_article: uint64, cid: bytes36,
coauthors: address[], ts) # 122 + 32 per co-author (922 with 25)
ArticleClaimed(v, seq, article_id, author, ts) # 61
Versioned(v, seq, article_id, version: uint16, cid: bytes36, prev_cid: bytes36, ts) # 103
StatusChanged(v, seq, article_id, from_status: uint8, to_status: uint8, actor: address, ts) # 63
CoauthorAccepted(v, seq, article_id, coauthor: address, author_count: uint16, claimed_total: uint64, ts) # 71
ReputationClaimed(v, seq, article_id, claimant: address, consumed: uint64, ts) # 69, then 1-2 ReputationChanged
Reviewed(v, seq, article_id, review_seq: uint64, reviewer: address, recommendation: uint8, cid: bytes36, ts) # 106
Voted(v, seq, target_kind: uint8, article_id, target_seq: uint64, voter: address,
weight: uint64, raw_weight: uint64, n: uint64, ts) # 94
Commented(v, seq, article_id, comment_seq: uint64, author: address, reply_to: uint64, root_seq: uint64,
depth: uint8, cid: bytes36, mentions: address[], ts) # 126 + 32 per mention (286 with 5)
CommentResolved(v, seq, article_id, comment_seq, resolved_by: address, ts) # 69
Flagged(v, seq, article_id, dispute_round: uint16, flagger: address, reason: uint8, weight: uint64,
round_total: uint64, new_status: uint8, bond: uint64, threshold: uint64, ts) # 97
DisputeResolved(v, seq, article_id, dispute_round: uint16, outcome: uint8, new_status: uint8,
resolved_by: address, reasons: bytes36, ts) # 101
FlagSettled(v, seq, article_id, dispute_round: uint16, flagger: address, outcome: uint8,
bond_to: address, bond: uint64, retained: uint64, ts) # 112 (D-163 amended)
ReputationChanged(v, seq, address, field_id: uint16, delta: uint64, new_total: uint64, reason: uint8, ts) # 72
# declarations (contract v3, EDIT D-106): one event per call, no box
IdentityDeclared(v, seq, address, scheme: uint8, value: string, ts) # scheme 1 = ORCID iD; "" withdraws
DoiDeclared(v, seq, article_id, version: uint16, doi: string, declared_by: address, ts) # version 0 = every version; "" withdraws
Followed(v, seq, follower: address, target: address, on: bool, ts) # 86; on is 0x80 or 0x00

[EDIT D-017: bytes36 is the ARC-4 type byte[36]; selectors derive from the type-only signature.] [EDIT D-020 / D-058: Flagged.new_status is the status after the call, so the transition to disputed needs no StatusChanged. DisputeResolved.dispute_round is the round that was resolved.] [EDIT D-072: CoauthorInvited is gone; CoauthorAccepted.claimed_total is kept and always 0; the declared co-authors are in Published.coauthors, in ascending address order.] [EDIT D-057: review / comment votes and resolve_comment emit the canonical event first, then at most one ReputationChanged.] [EDIT D-061: Commented.mentions is in ascending address order (not the order of the comment body).] [EDIT D-163: Voted.weight is the damped weight that the vote box records and the totals add; raw_weight = 1 + isqrt(rep) and n (for an article vote, 1 + the largest counter over its authors) let the indexer rebuild the damping counters. Flagged.dispute_round is the round the flag counts in (after closing a lapsed round); bond is the bond held (doubled after recent clearances); threshold is FLAG_THRESHOLD, always (D-163 amended). DisputeResolved.outcome is 1 cleared, 2 retracted, 3 expired; resolved_by is governance, the submitter (own retraction) or whoever expired it; reasons is the raw CID of the published reasons, all zero bytes for an expiry and an own retraction. FlagSettled.outcome is 0 for a lapsed round; bond_to is the flagger (lapsed, retracted), the submitter (cleared) or the zero address (expired: nothing paid); bond is what went to bond_to (the whole bond, half of it, or 0; plus the deposit of a closed flagger’s cleared flag); retained is what the application kept for ever (the other half, the whole bond, plus the deposit of a closed flagger’s expired flag) (D-163 amended). Followed.on is canonical: the most significant bit of the byte the caller sent (the one every ARC-4 decoder reads), re-encoded as 0x80 or 0x00. FieldAdded and DisputeResolved now carry their time and actor.] target_seq is 0 for article votes. Events MUST contain enough information to rebuild every indexer projection without reading boxes: the reference reducer tests/ref/replay.py rebuilds every box, the dispute outcomes, the damping counters, the bonds held and the amounts retained. vote_article emits exactly one Voted and never a ReputationChanged.


Every method: sender is the actor; validates status per §7.4; emits its event; fails on any violated precondition. Any method that creates a box or an asset MUST be grouped with a payment from the sender to the application account covering the exact MBR delta (§13); the contract MUST verify the payment and MUST fail on underpayment. Overpayment is accepted and kept (no refunds; open question). [EDIT D-163: a flag’s payment also carries FLAG_BOND, which the application holds and settle_flag pays out; the payment immediately precedes the call in the group (GroupIndex - 1), comes from the sender, goes to the application, never closes or rekeys.]

[EDIT D-001 / D-003 / D-023 / D-055 / D-072 / D-073 / D-163: the list below is the complete v4 ABI (P-14: the v2.7 list lacked create, extend, the pay arguments and coauthors, and still had invite_coauthor). Calls from other applications are accepted like any other sender (P-15): the actor is Txn.sender.]

# creation (no bare create, no update, no delete)
create(governance: address) # D-003; emits GovernanceChanged(previous = zero)
# governance (sender == governance address)
add_field(pay, field_id: uint16, name: string)
propose_governance(new_governance: address) # D-163: never zero, the current one or the application;
# replaces a pending proposal; emits GovernanceProposed
accept_governance() # D-163: sender == the proposed address; emits GovernanceChanged
resolve_dispute(article_id: uint64, outcome: uint8, reasons: bytes36) # outcome 1 at once, 2 after 14 days; raw CID of the reasons
# disputes (anyone)
expire_dispute(article_id: uint64) # D-163: 60 days after the dispute started
settle_flag(article_id: uint64, dispute_round: uint16, flagger: address) # D-163: once the round is over; 1-2 inner payments
# publishing
publish(pay, cid: bytes36, primary_field: uint16, secondary_field: uint16, article_type: uint8,
parent_article: uint64, coauthors: address[]) -> uint64 # article_id (sequence); an amendment needs an accepted
# author of the parent as sender (D-163)
extend() # D-073: changes nothing, lends budget and box references
claim_article(article_id: uint64) # sender == author, opted in
new_version(article_id: uint64, cid: bytes36) # sender == author
set_status(article_id: uint64, new_status: uint8) # sender == author, table §7.3
# co-authorship
accept_coauthorship(article_id: uint64) # sender == a declared co-author
# reviews
submit_review(pay, article_id: uint64, cid: bytes36, recommendation: uint8) -> uint64 # review_seq
vote_review(pay, article_id: uint64, review_seq: uint64)
# votes
vote_article(pay, article_id: uint64, authors: address[]) # D-163: every author, ascending
claim_reputation(pay, article_id: uint64) # sender == accepted author
# comments
comment(pay, article_id: uint64, cid: bytes36, reply_to: uint64,
mentions: address[]) -> uint64 # comment_seq
vote_comment(pay, article_id: uint64, comment_seq: uint64)
resolve_comment(pay, article_id: uint64, comment_seq: uint64) # sender == author (D-163: pay for a first counter)
# flags
flag(pay, article_id: uint64, reason: uint8) # pay = deposit + bond (FLAG_BOND << recent clearances, D-163)
# declarations (contract v3, EDIT D-106): emit their event only; no box, no payment, network fee only
declare_identity(scheme: uint8, value: string) # scheme >= 1 (any value 1-255 is accepted), value <= 64 bytes
declare_doi(article_id: uint64, version: uint16, doi: string) # sender == the submitter (D-163); version <= current; a
# non-empty DOI starts with "10.", <= 200 bytes, article not
# retracted; "" withdraws, in any status
follow(target: address, on: bool) # target != zero address, != sender (D-109)

pay is a payment transaction (ARC-4 pay). extend() calls: a client adds ceil(boxes / 8) − 1 of them to the group, where for publish boxes = 3 + (1 if secondary) + (2 if amendment) + co-authors, and for vote_article boxes = 4 + 2 × authors (an article with 25 co-authors: 56 boxes, 6 calls). [EDIT D-073 / D-163]

Governance is a single address stored in global state, changed in two steps: the current governance proposes (propose_governance(new), emits GovernanceProposed; a new proposal replaces the pending one) and the proposed address accepts by signing (accept_governance(), emits GovernanceChanged), so it can migrate without redeploying the contract, a mistyped or unsignable address can never take over, and the indexer can rebuild the governance history from events alone. Governance pays its own fees and MBR (field boxes) like any other actor. [EDIT D-163 (audit C-5): replaces the one-step set_governance, which accepted the application’s own address and so could brick governance for ever.]

The governance account is post-quantum from day one. At launch it is a native Falcon-1024 account (Algorand 5.0 pqsig), generated and backed up with algokey pq. The contract does not care how a transaction was signed (it only sees Txn.sender), so this is a deployment choice, not a contract feature; but the following are protocol requirements:

  • The governance address MUST be a native PQ account (hash-derived, not an Ed25519 public key) on TestNet and MainNet, and MUST be a different account on each network (§3.2). A test asserts that the configured governance address is PQ-compliant: algokey pq check-address ADDRESS exits 0 and prints “is PQ compliant”; in code, not is_ed25519_point(decode_address(addr)). [EDIT D-012: original sentence was inverted]
  • The application account needs no key at all: it is controlled exclusively by the contract’s inner transactions. Nothing to migrate.
  • The deployer account MUST hold no power after deployment: the application is created with no update and no delete program paths (immutable), and every privileged method checks the governance address, never the creator.
  • Governance transactions carry a Falcon-1024 signature (public key 1 793 B + signature ≈ 1.3 KB). Consensus v42 prices a pqsig-authorised transaction as a flat +2 minimum fees (usage 3 000 000), not per byte; the per-byte surcharge only applies to oversize notes, arguments and programs. The deployment scripts size the fee with the usage formula and confirm it with simulate (group-usage), which accepts a scheme-only placeholder pqsig before signing. [EDIT D-027/D-045]
  • Multisig: native post-quantum multisig is on Algorand’s roadmap but not yet in the protocol. Launch governance is therefore a single PQ signer (the operator), documented as such. When native PQ multisig ships, governance migrates to an M-of-N PQ account via propose_governance / accept_governance [EDIT D-163]. The PQ requirement is checked off chain (D-012): the contract accepts any address that can sign the acceptance. // DECISION: prefer waiting for native PQ multisig over a third-party contract-based Falcon vault; revisit if launch on MainNet comes first.
  • Authors, reviewers and voters MAY use PQ accounts too (e.g. a Pera Quantum account); the contract treats them like any other 32-byte address, and box keys are unaffected.

The platform pays nothing. Every network fee and every minimum-balance deposit caused by an action is paid by the account performing it. Concretely:

  • Transaction fees, including inner-transaction fees via fee pooling, are paid by the sender of the outer call. Fee amounts are read from the network at send time (§13.2), never assumed.
  • Every MBR increase on the application account (ASA creation, any box) is covered by a payment from the sender, grouped with the call and verified by the contract.
  • MBR increases on the user’s own account (ASA opt-in) are naturally the user’s.
  • The application account is funded once with its base minimum balance at deployment (operator cost, one-off). After that its spendable balance MUST never fall below the flag bonds it holds, every bond is paid out or retained exactly once, and retained amounts only accumulate (invariant 22, tested). [EDIT D-163: v2.x said “never decreases”; a flag bond is held in the spendable balance and settle_flag pays it out, which decreases it by exactly that bond; nothing else ever does.]
  • A flag bond is not a fee and not a deposit: it is held for the flagger and settled by settle_flag: back to the flagger (retracted, lapsed), half to the article’s submitter and half retained (cleared), all retained (expired). Retained amounts stay in the application account for ever. [EDIT D-163 (amended)]

User-facing wording: “No platform fees for authors or readers. You pay Algorand network fees (fractions of a cent) and a storage deposit, held by the protocol, for the data you publish.”

  • Network fee: determined by the network, never hard-coded. Clients MUST take minFee / suggested parameters from algod at send time. Inner transactions each cost the network minimum and MUST be covered by the caller through fee pooling. Post-quantum (pqsig) transactions pay a flat +2 minimum fees (usage model of consensus v42); the per-byte surcharge only applies to oversize notes, arguments and programs. The client sizes the fee as ceil(min_fee × usage / 1e6) and confirms it with simulate. [EDIT D-027]
  • ASA creation: +100 000 MBR on the application account.
  • ASA opt-in (claim): +100 000 MBR on the author’s account.
  • Box MBR: 2 500 + 400 × (key_len + value_len) per box, on the application account.

Because nothing is deleted, MBR is a permanent deposit, not a fee. [EDIT D-163: one exception: settle_flag deletes the flag box and returns its deposit to the flagger.]

Operation Payer Components
publish author call fee + inner fee + ASA MBR + article box + submitter co-author box + one co-author box per declared co-author
claim author opt-in MBR + call fee + inner fee
vote_article voter vote box + one damping counter per author never voted for before + fee (never a reputation box); with many authors, extend() calls
claim_reputation author 1–2 reputation boxes (if first in field) + fee
submit_review reviewer review box + reviewer index box + reputation box (if first) + fee
vote_review voter vote box + damping counter (first vote for this reviewer) + fee
comment commenter comment box + reputation box (if first) + fee
vote_comment voter vote box + damping counter (first vote for this commenter) + fee
resolve_comment author damping counter (first grant to this commenter, reputable resolver only) + fee
accept_coauthorship co-author fee only
flag flagger flag box + outcome box (if it opens the dispute) + fee, and the bond (1 ALGO, doubled per clearance by governance within a year), held
settle_flag anyone fee + 1 or 2 inner fees; returns the flag box deposit to the flagger and pays the bond out or retains it
expire_dispute, propose/accept_governance, resolve_dispute caller fee (+ one inner fee for a retraction)
add_field governance field box + fee

[EDIT D-163 (contract v4, measured in projects/contracts/V4_REPORT.md): article box (9 + 217 bytes) = 92 900; co-author box (42 + 25) = 29 300; reputation box (35 + 26) = 26 900; vote box (42 or 50 + 24) = 28 900 or 32 100; review box (17 + 85) = 43 300; reviewer index (42 + 8) = 22 500; comment box (17 + 118) = 56 500; flag box (43 + 25) = 29 700; outcome box (11 + 1) = 7 300; damping counter (33 + 8) = 18 900. Publish = 222 200 + 29 300 per declared co-author (954 700 with 25); first review = 92 700; first comment = 83 400; first article vote = 47 800 (28 900 + 18 900 per new author); a flag holds 1 029 700 (1 037 000 when it opens the dispute), of which 1 000 000 come back to someone through settle_flag and 29 700 to the flagger.] Measure, don’t assume. M1_REPORT.md, M2_REPORT.md (v3 figures) and V4_REPORT.md report the actual fee paid per operation on LocalNet, separately for Ed25519 and pqsig senders; new_version pools one inner transaction (ARC-19 reserve update); set_status to retracted pools one. [EDIT D-013/D-023/D-031: ARC-4 static tuple sizes]


An article’s CID is a UnixFS directory (dag-pb) with this layout:

index.md required — Markdown with front-matter
assets/ optional — images, figures, small data files referenced from index.md
  • index.md MUST reference assets by relative path (assets/fig1.png), never by external URL. The web app rewrites relative paths to the gateway URL at render time.
  • Total directory size ≤ MAX_CONTENT_BYTES. Larger material (raw datasets) is published as a separate type = dataset article and referenced by CID. [EDIT D-138: the limit applies to every publication, datasets included (the indexer checks every directory against MAX_CONTENT_BYTES); data larger than that goes to a data repository such as Zenodo and is referenced by its DOI.]
  • Reviews and comments are single raw Markdown files (codec 0x55).

/spec/article-template.md:

---
title: ""
authors: ["ADDRESS_1", "ADDRESS_2"] # informative, frozen per version; first MUST be the submitter (§7.6)
field: 0x0303 # MUST equal on-chain primary_field
secondary_field: 0x0501 # MUST equal on-chain secondary_field (omit if 0)
type: article # MUST equal on-chain type
parent: 0 # MUST equal on-chain parent_article
abstract: ""
keywords: []
references: [] # list of {cid | doi | url}
license: CC-BY-4.0
language: en
---
## Introduction
...

notes and amendment use /spec/notes-template.md (title, authors, field, type, parent, abstract, body).

  • On-chain metadata is canonical. The contract stores the CID and validates only its shape (§6.1).
  • Web app validates front-matter and directory layout before upload, computes the directory CID locally with the canonical parameters (§14.5) before signing, and compares with the pinning provider’s returned CID (Path A) or with the gateway-fetched content (Path B).
  • Indexer fetches the directory, parses index.md front-matter, and sets validation_status ∈ {valid, mismatch, malformed, unavailable, oversized} by comparing against on-chain metadata (field, secondary field, type, parent, and authors[0] == author). The rest of authors[] is compared only to produce a notice (§7.6), never a mismatch. Non-valid content is displayed with a warning, never hidden; the article remains protocol-valid.
  • Content availability ≠ protocol validity. An article whose content is temporarily unreachable is still valid on-chain.

The contract only ever sees a 36-byte CID. Getting the content onto IPFS is the author’s job, with the web app as the default helper. Three paths, all ending in the same publish() call:

Path A — web uploader (default, M4). The author drops index.md and assets/ into /publish. The browser builds the UnixFS DAG locally with the canonical parameters (§14.5), shows the resulting CID, then uploads the CAR to a pinning provider. The provider’s returned CID MUST equal the locally computed one, or the app refuses to continue. The author then signs the grouped payment + publish().

Path B — bring your own CID. The author pins the directory themselves (own Kubo node, Pinata, Filebase, anything [EDIT D-026: web3.storage no longer exists]) and pastes the CID into /publish. Before enabling the sign button the app MUST: fetch the CID through a gateway, check it resolves to a directory containing index.md, parse and validate the front-matter against the fields chosen in the form, and check total size ≤ MAX_CONTENT_BYTES. Exact commands for this path are in /spec/publishing-guide.md. [EDIT D-138: the guide is the documentation site (https://ariadne.press/docs/guides/publish/, projects/docs); /spec/publishing-guide.md points there.]

Path C — CLI (M5). ariadne publish ./my-article --wallet ... does A end to end from a terminal, for programmatic or batch publishing.

Pinning responsibility.

  • The author is responsible for at least one pin. In Path A the app pins with the author’s own provider credentials (stored in the browser, never sent to Ariadne servers) or with the operator’s provider under a per-address quota. // DECISION: quota size and whether it exists at all is an operator choice, not protocol.
  • The indexer pins every CID it validates, as a second copy (operator cost, ordinary infrastructure, not a user fee). [EDIT D-084: through the indexer’s own Kubo node (RPC never exposed), for article CIDs validated as valid and for every review and comment CID; the public gateway of that node serves only what it holds (Gateway.NoFetch), on its own origin. EDIT D-085: in Path A the browser uploads its CAR to Pinata with the author’s key (car=true, public network) and compares the returned CID; Path B fetches through a public trustless gateway and verifies every block, so that gateway is not trusted.]
  • Because content is addressed by hash, extra pins are always safe and anyone (including the author’s institution) may add one. The web app SHOULD show the CID and a “pin it yourself” hint on every article page.

Provider credentials are the author’s secrets and follow the same rule as wallet keys: the app never asks for them in chat, logs or telemetry, and stores them only in browser storage with a clear “forget” action.

Different tools can produce different CIDs for the same bytes. The web app, the CLI and the publishing guide MUST all use exactly:

CID version: 1
multibase: base32 (display) / raw bytes (on-chain)
hash: sha2-256
directory codec: dag-pb (UnixFS)
raw leaves: true
chunker: size-262144 (256 KiB)
DAG layout: balanced
wrap in directory: yes (the root is the directory, not index.md)

[EDIT D-035: additionally max 174 links per file node, HAMT sharding above 256 KiB of links (links-bytes estimation, fanout 256), links-first field order, no mode/mtime, hidden files excluded, entries sorted by name; never use -w (it adds a second directory level). Reference implementation: spec/fixtures/compute-cid.mjs.] Equivalent Kubo invocation, used as the reference for tests:

ipfs add -r --cid-version=1 --raw-leaves=true --chunker=size-262144 --hash=sha2-256 --pin=true ./my-article

Test fixture: a sample article directory in /spec/fixtures/sample-article/ with its expected CID committed; the web app’s local encoder and the indexer’s re-hash MUST reproduce it.

  • The CID is the canonical content identifier.
  • At least one active pinning provider is required in M1; the provider is behind StorageProvider.
  • The indexer records available, fetched_at, last_checked_at per CID and re-checks availability periodically.
  • Long-term archival replication (second provider, Filecoin/Arweave mirror) is post-M1.

Architecture (chain is truth, SQLite is cache):

Algorand app calls (ARC-28 logs)
→ @algorandfoundation/algokit-subscriber (watermark, catch-up, idempotent)
→ chain_events (normalized, append-only)
→ projections (articles, coauthors, versions, reviews, votes, comments, mentions, flags, disputes, reputation, fields, content)
→ SQLite
→ REST API

Tables:

  • chain_events(tx_id, log_index, round, intra, app_id, event_name, event_version, payload_json, ts) — primary key (tx_id, log_index) (one application call emits up to three events, D-076); ingestion is idempotent. [EDIT P-14 / D-163: the v4 events carry event_seq, which detects a missed event; decoders key on (application, selector, schema_version).]
  • watermark(app_id, last_round).
  • Projection tables as listed; every row traces to a chain_events row.
  • content(cid, available, validation_status, fetched_at, last_checked_at, size_bytes, title, abstract, keywords).
  • mentions(article_id, comment_seq, mentioned_address, by_address, ts) — from Commented.mentions.
  • notifications(address, kind, article_id, comment_seq, ts, read) — projection only; kind ∈ {reply, mention, comment_on_my_article, review_on_my_article, comment_resolved, coauthor_invite, followed} [EDIT D-072: coauthor_invite is derived from Published.coauthors; EDIT D-109: followed, for the first follow of a pair only]. Read state is off-chain and per indexer instance.
  • identities(address, scheme, value), article_dois(article_id, version, doi), follows(follower, target, active, since) — projections of the v3 declarations. [EDIT D-106..D-109]
  • orcid_checks, doi_checks — base tables, kept across rebuilds: the last check of each declared ORCID iD against its public record (it must list the address’s profile page) and of each DOI against its Zenodo or DataCite record (published, IsIdenticalTo the article page and, for a version, its CID); rechecked weekly, every ten minutes for two days after a declaration that is not verified yet. [EDIT D-107/D-108]

Requirements: replay from round N; rebuild all projections from chain_events; schema migrations; crash-safe restart. [EDIT D-076/D-084: chain_events key is (tx_id, log_index); round N is the manifest’s startRound; catch-up goes through algoIndexer when set; schema v2 adds the base table pins (second copy), kept across rebuilds.]

One indexer instance per network, each with its own database and its own base URL (indexerApi in networks.json). Databases are never shared or merged; every API response carries network and appId so a client can detect a mismatch. The indexer refuses to start if the algod it is pointed at reports a genesis hash different from the manifest entry it was configured with.

API:

  • GET /articles?field=&type=&status=&sort=score|new
  • GET /articles/:id (authors, versions, reviews, comments as a tree, flags summary, dispute state)
  • GET /articles/:id/comments?root= — one thread, ordered by (depth, created_at)
  • GET /articles/:id/citations, GET /users/:address/citations — citations between ARIADNE articles [EDIT D-126: derived from what each current version links to (its text and front-matter references): an article page of this network, a declared DOI not contradicted by its record, a version CID, or the amended parent; retracted citing articles are not counted. EDIT D-127, D-128: Thread, the citation measure: each article’s percentile among the articles of its field (area, then all, when fewer than 20), every pair compared at the age of the younger, by citations from articles with no author in common; a person’s Thread is the mean percentile of their articles weighted by 1 / authors, with one article at 50 added; amendments and retracted articles take no part. EDIT D-131: the Peer-reviewed seal, a derived view like citations: an article holds it while at least 2 qualified reviews are favourable (accept, minor revision) and outnumber the qualified unfavourable ones, and (EDIT D-136, seal rule 3) every author has a verified ORCID iD; a review qualifies when its author is not an article author, has a verified ORCID iD (EDIT D-133, seal rule 2), has reputation >= 10 in the primary or secondary field or >= 3 OpenAlex works in the area, has no conflict of interest (same ORCID iD, ARIADNE co-authorship or OpenAlex shared works within 3 years of the review, same current employer on ORCID), has >= 200 words read from IPFS, and, if favourable, does not return a favourable review received from an author within 2 years; suspended while disputed, none when retracted; GET /articles/:id gives seal_detail, lists give seal, /search/articles?sealed=true filters. Status 3 (“final”) is shown as “closed version”]
  • GET /notifications?after= — every address’s notifications after an id, for the push sender [EDIT D-124: browser notifications by Web Push from the web server; the verified ORCID record’s public contact data (websites, emails, countries, current employments) is a cached result like the check itself]
  • GET /cite?ids= or ?owner=&list= — what a bibliography needs of each article [EDIT D-123]
  • GET /users/:address/lists, GET /users/:address/lists/:id — public lists of articles [EDIT D-122: not contract events but notes of 0-ALGO payments to oneself (ariadne/lists/&lt;appId>: + JSON), read by the indexer with a second subscription filter and stored in chain_events as ListNote rows, so that rebuild replays them; a note that breaks the rules changes nothing. Private lists stay in the browser. EDIT D-139: overlay journals are public lists with a policy and editors, managed with more note operations (journal, invite, dismiss, editor, select, unselect); GET /journals, GET /users/:address/journals. EDIT D-141: a journal is organised in volumes of up to 1 000 articles (the next opens by itself when one is full, or when the founder opens it); closed volumes are frozen and keep their board. EDIT D-140: the authors of an article citing a retracted one receive a cited_retracted notification, derived from the citations. EDIT D-148: the same notes follow articles ({"op":"watch","articles":[…],"on":true|false}, at most 500 per address); the followers of an article receive watched_version, watched_status, watched_review and watched_comment notifications unless told in their own right; GET /users/:address/watching, and the viewer state says watching.]
  • GET /search/articles, /search/posts, /search/people, /search/suggest — full-text search over articles (title, abstract, keywords, body, authors, fields, DOIs), reviews and comments, and people, with filters, facets and highlighted passages [EDIT D-120: the search indexes (search_articles, search_posts) and the texts of reviews and comments (post_texts, kept only when they hash to their CID) are derived views, rebuilt from the chain plus IPFS; never an authority.]
  • GET /users/:address/notifications — replies, mentions and activity on the user’s articles
  • GET /users/:address → derived profile: publications (as submitter and as co-author), reviews, comments, reputation by field, claimable reputation per article (vote_total − claimed_total), timeline
  • GET /fields
  • GET /users/:address/following, /followers, /feed — on-chain follows and the activity of everyone followed [EDIT D-109]
  • GET /orcid/:id — addresses whose declaration of that ORCID iD is verified [EDIT D-107] [EDIT D-107/D-108: GET /users/:address adds identity.orcid (iD, check status, the record’s name once verified) and follow counts; GET /articles/:id adds dois[] with their checks and authors[].orcid (verified only).]

Ranking (indexer projection, never on-chain):

age_days = floor((now - created_at) / 86400)
score = vote_total / (age_days + 2) ** 0.8

Default feed excludes retracted; shows disputed with an explicit warning; content with validation_status ≠ valid shows a warning.


  1. / — feed (top / new), filter by field and type.
  2. /a/[id] — article: rendered Markdown with assets, authors (accepted), version history, reviews, threaded comments (collapse beyond depth 3, “continue thread” link), reply and @mention composer with address autocomplete (recent participants in this article first), vote, flag (if eligible), status and dispute banner.
  3. /publish — two tabs: Upload (Path A: editor with template pre-filled, asset upload, front-matter validation, local CID computation, pin, sign) and I have a CID (Path B: paste CID → fetch, validate, sign). Both show the CID, the exact deposit and the fee before signing, and link to the publishing guide.
  4. /u/[address] — derived profile; for the connected wallet, shows claimable reputation per article with a “Claim” action (one transaction each, or a grouped batch).
  5. /inbox — notifications for the connected wallet (replies, mentions, activity on own articles). Purely an indexer view; nothing on-chain.
  6. /fields — taxonomy browser.

Every screen that triggers a transaction MUST show the user the total cost (fee + deposit) before they sign.

[EDIT D-107..D-109: the profile lets its owner link an ORCID iD (with the steps to add the profile to the ORCID record) and shows a verified iD to everyone; an article page shows its verified DOIs, and its accepted authors can prepare a Zenodo draft (published by the author on Zenodo) and declare a DOI; follows are signed transactions; citations use ORCID names and DOIs.]

The web app runs against exactly one network at a time and lets the person switch between the enabled ones in networks.json.

  • Where: a selector in the header, always visible, showing the current network. On anything other than MainNet the whole UI carries a persistent coloured banner (“TestNet — articles here are for testing and carry no reputation on MainNet”).
  • Scope: the network is part of the URL, so links are never ambiguous: /a/123 is MainNet; /testnet/a/123 is TestNet; /localnet/... exists only in dev builds. Switching rewrites the current route to the same page on the other network when it exists there, otherwise to that network’s feed. The last choice is remembered in localStorage only as the default for a bare / visit.
  • Data isolation: the app talks only to the indexerApi of the selected network; nothing from another network is ever shown, cached across, or mixed in feeds, profiles or notifications. Switching clears in-memory state and the notification inbox.
  • Wallet: @txnlab/use-wallet is initialised for the selected network. Before enabling any sign button the app checks that the connected wallet reports the same genesis hash; on mismatch it disables signing and shows “Your wallet is on X, Ariadne is on Y” with a one-click switch of the app (never of the wallet, which the app cannot control). Every transaction is built with the manifest’s genesis hash, so a mismatched signature would be rejected by the network anyway.
  • Publishing: the publish flow shows the target network next to the cost. Publishing the same CID on both networks is allowed and produces two independent articles.
  • Profiles: /u/[address] on TestNet shows only TestNet activity and reputation; the page says so.
  • Disabled networks (MainNet before launch) do not appear in the selector at all. Enabling MainNet is: deploy, audit sign-off, fill appId, set enabled: true, redeploy the static site.

Design: monochrome, one accent colour, generous line-height, max text width ~68ch, system font stack or Inter, no cards or shadows. arXiv readability, modern layout.


The contract MUST preserve:

  1. Article identity (asa_id) is immutable.
  2. Article submitting author is immutable.
  3. Article primary and secondary fields are immutable.
  4. Article type and parent are immutable.
  5. An article has exactly one current version.
  6. Version numbers are strictly increasing by 1.
  7. A version never points to itself as predecessor; consecutive CIDs differ.
  8. A voter votes at most once per target.
  9. A reviewer reviews an article at most once.
  10. At the time the action is performed, an accepted author MUST NOT review, vote on, or flag their own article. Later changes to the accepted-author set do not invalidate earlier actions (§8.0).
  11. At the time of resolution, the comment’s author MUST NOT be an accepted author of the article. Same rule for later changes.
  12. Reputation never decreases.
  13. Reputation grants are deterministic from on-chain state and integer-valued.
  14. Resolved comments cannot be resolved twice.
  15. Each flag counts once, only in its round; its Flagged event is permanent, and its box lives until its bond is settled. [EDIT D-163]
  16. Status transitions outside §7.3 always fail.
  17. Retracted and disputed content remains queryable.
  18. No article, co-author, version, review, comment, vote, outcome or damping record is ever deleted; a flag box is deleted only by its own settlement, once its round is over. [EDIT D-163]
  19. The article ASA is never held by any account other than the application or the recorded author.
  20. Every box key is ≤ 64 bytes.
  21. Every state-changing call emits exactly one canonical event (the submitter’s own retraction of a disputed article adds the DisputeResolved of the round it ends). [EDIT D-163]
  22. The application account’s spendable balance never falls below the flag bonds it holds; every bond is paid out or retained exactly once; retained amounts only accumulate (every MBR delta is covered by the caller; every inner fee is pooled from the caller; only settle_flag pays anything out: one bond and the freed deposit of the flag box it deletes). [EDIT D-163: restated; v2.x read “never decreases as a result of a user action”.]
  23. Co-authorship exists only with the co-author’s signed acceptance.
  24. A reputation box is only ever created by, and paid for by, its own address.
  25. A reply’s depth equals its parent’s depth + 1, never exceeds MAX_COMMENT_DEPTH, and its root_seq equals its parent’s root_seq.
  26. Mentions are distinct, never include the commenter, and never exceed MAX_MENTIONS.
  27. vote_article never writes a reputation box; claim_reputation never advances claimed_total by more than it grants to the primary field.
  28. [EDIT D-072] The article-vote reputation materialised by all accepted authors of an article never exceeds vote_total.
  29. [EDIT D-065 / D-163] No article can be left in disputed without a possible resolution: anyone can end a dispute 60 days after it started.
  30. [EDIT D-072] The author list of an article never changes after publication; an address that voted on or reviewed an article is never an accepted author of it.
  31. [EDIT D-163] A damping counter only grows, by exactly one per grant from its giver to its receiver (for an article vote, one per author); a vote or a resolution uses n = the counter + 1 before it grows.
  32. [EDIT D-163 (amended)] A bond goes back to the flagger when its round was retracted or lapsed; half of it goes to the article’s submitter and half is retained when it was cleared; all of it is retained when it expired; whoever calls settle_flag. Nobody else ever receives any part of a bond, and the threshold never grows.
  33. [EDIT D-163] event_seq is 1 for the creation event and grows by exactly one per event of the application.

The protocol explicitly considers, and either mitigates or documents as a known limitation: [EDIT D-163: the v2.x rows on comment-resolution farming (“prevented”) and duplicate CIDs (“indexer surfaces duplicates”) were not true (audit C-3, E-4, E-8); they are corrected below, with the v4 rows on disputes and governance.]

Threat Handling
Sybil accounts, reciprocal vote/review farming across accounts Known limitation, damped by isqrt, REP_CAP_DAY and (D-163) the per-(giver, receiver) damping, which makes repeated grants between the same two addresses worth w // n; rings of many fresh addresses are not prevented; detection is off-chain
Author↔reviewer “useful” cartel on the same article prevented: author’s useful votes grant 0
Comment-resolution farming mitigated, not prevented (D-163): self-resolution is impossible (the commenter is never an author); a resolution grants 5 // n per (resolver, commenter), at most 10 per pair ever, and nothing from a resolver below reputation 10; a reputable resolver can still grant 10 to each of many fresh commenters
Fast reputation for flagging (minted, bought or farmed) mitigated (D-163): the reputation box must be 30 days old, a flag weighs at most 10, a dispute needs three flaggers, every flag carries a 1 ALGO bond
Dispute griefing (freezing a competitor’s article) mitigated (D-163, audit C-1, C-2, E-1): half of each bond paid to the victim of a cleared dispute and half retained, every bond of an expired dispute retained, bonds doubled for a year after each clearance by governance, rounds that lapse after 30 days, a dispute that expires after 60 days, and nothing frozen but new versions
Governance retracting an article in haste or with a stolen key mitigated (D-163, audit C-4, E-10): retraction only 14 days after the dispute started, with the CID of published reasons; clearance at once
Governance handed to an address nobody controls prevented (D-163, audit C-5): two-step change (the new address must sign), the application’s own address refused
Amendments of someone else’s article prevented (D-163, audit C-6): only an accepted author of the parent
A co-author overwriting the submitter’s DOI prevented (D-163, audit C-7): only the submitter declares or withdraws
Ambiguous booleans in events prevented (D-163, audit C-8): Followed.on is canonical
Mention spam / harassment MAX_MENTIONS per comment; mentions grant nothing; inbox mute per address (indexer-side, M5)
Deep-nesting abuse MAX_COMMENT_DEPTH enforced on-chain
Attaching someone as co-author without consent prevented: signed acceptance required [EDIT D-072: being declared restricts nothing; the declared address may still vote, review and flag]
Late co-author harvesting earlier votes prevented by the closed list: nobody can be added after publication, and an address that voted or reviewed can never confirm (D-072)
Vote cost or log budget exploding with author count prevented: pull model; vote touches no reputation box; an article vote damps over its authors with extend() calls (D-163)
Flag spam MIN_FLAG_REP with a 30-day-old box, one flag per round per address, capped weight, the bond, doubled after clearances (D-163)
Self-immunisation (an author’s own aged accounts dispute his article so that it is cleared or expires) prevented from paying (D-163 amended, economics re-run of v4): the threshold never grows, an expiry is not a clearance and retains every bond, a clearance returns only half of each bond to the author; the attempt costs him half the bonds or all of them, and a later honest dispute still needs only 30 (with bonds a retraction returns)
Voter forced to fund others’ storage prevented: reputation boxes self-funded (§10.5); damping counters paid by the giver
Platform drained by MBR or by bonds prevented: invariant 22 (restated by D-163: bonds held are paid out or retained once, as the contract fixes)
Malformed / oversized / mismatched IPFS content indexer validation status; size limit at upload and fetch
External assets disappearing prevented: assets must live inside the article directory
Duplicate CIDs across articles allowed on-chain; not yet surfaced by the indexer (audit E-8): planned, an article with a CID or DOI first used by another is to be shown as such
Duplicate versions rejected (CID must change)
Repeated event ingestion idempotent chain_events primary key
Indexer crash / restart watermark + replay
Missing pins availability tracking; article stays valid
Malicious Markdown / HTML sanitise rendered HTML; no raw HTML passthrough; asset paths restricted to assets/
Malicious LaTeX KaTeX with throwOnError: false, trust: false, maxSize/maxExpand limits
Unauthorised governance calls sender check against global governance address
Quantum compromise of the governance key governance is a native Falcon-1024 (post-quantum) account from launch; migrable via propose_governance / accept_governance [EDIT D-163]
Deployer key compromise application immutable (no update/delete); creator has no privileged path
ASA transfer attempts ARC-71: clawback always zero, freeze zero after claim, holding frozen; tests attempt transfer/clawback/unfreeze from every role and expect failure
Underpaid MBR contract verifies the grouped payment amount

M1 — Core protocol (LocalNet): taxonomy, publish (incl. submitter co-author box), claim_article, new_version, set_status, vote_article, claim_reputation (single author), reputation with daily cap, all events for these, MBR verification and invariant 22 test. Acceptance criteria in §20. M2 — Community layer: co-authorship (invite/accept [EDIT D-072: declare at publication / accept]; multi-author claim allocation decided per §10.2 gate before coding), reviews, review votes (author-vote = 0 grant), threaded comments with mentions, comment votes, resolution, flags, disputes, governance resolution. Same rigour as M1. M3 — Indexer: subscriber → chain_events → projections → API. Rebuild-from-zero test. Directory content validation. M4 — Web: feed, article page with assets, publish flow with wallet + IPFS directory pin, network switch (§16.1) with TestNet + LocalNet enabled and MainNet present but disabled in the manifest. TestNet deploy. M5 — Polish: search, ranking, reputation display, ORCID attestation, second storage provider. [EDIT D-106..D-110: ORCID attestation, DOIs through Zenodo and on-chain follows are done in contract v3 (TestNet application restarted, D-110).]

Do not scaffold M3/M4 until M1 and M2 acceptance pass.


M1 is complete only when all of the following pass on LocalNet:

Article lifecycle

  • publish creates exactly one ASA; article_id == asa_id.
  • Author, primary/secondary field, type and parent are immutable (mutation attempts fail).
  • Only status, status_before_dispute, version, current_cid, prev_cid, updated_at, counters and claimed ever change.
  • ASA parameters at creation match ARC-71 Issued exactly (clawback = ZeroAddress, freeze = app); after claim_article they match ARC-71 Held (freeze = ZeroAddress, holder frozen); after retracted manager = ZeroAddress.
  • ASA cannot be transferred to any account other than the recorded author; a claimed ASA cannot be transferred, clawed back or unfrozen by anyone (tested from author, app-call paths, governance and a stranger). A close-out by the author back to the app succeeds and leaves the article box untouched.
  • claim_article fails when the author is not opted in, and when the sender is not the author.
  • Version numbers strictly increase; new_version fails in final, retracted (and disputed); fails when CID is unchanged.
  • Every row of the §7.3 table succeeds; every transition not in the table fails (parametrised test over all 5×5 pairs and both actor kinds). [EDIT D-002: on LocalNet the matrix is set_status over the 25 pairs × {author, governance, stranger}; rows 6-8 (flag, resolve_dispute) belong to M2 acceptance; disputed-state negatives run offline with a seeded box.]
  • publish creates the submitter’s co-author box (accepted = true, claimed_total = 0) and no reputation box.

Taxonomy

  • Unknown field ids fail. Primary is mandatory. Secondary is 0 or a valid distinct field. Only governance adds fields.

Voting and reputation

  • Weight equals 1 + isqrt(rep) for a table of reputation values including 0, 1, 3, 4, 99, 100, 10 000.
  • Weight uses the reputation snapshot at vote time (test: vote, then raise rep, then check stored weight unchanged).
  • A voter with no reputation box votes successfully with weight 1 and no box is created for them.
  • Duplicate votes fail. Self-votes fail. Votes in disputed and retracted fail.
  • vote_article increments vote_total by the voter’s weight and writes no reputation box (asserted by box listing before/after).
  • claim_reputation: after N votes of known weights, the submitter’s claim grants exactly Σw to the primary field and Σw / 2 to the secondary; creates the reputation boxes on first claim (claimer pays); a second claim with nothing new fails; a claim by a non-author fails; claims in disputed and retracted fail.
  • Reputation is per (address, field); integer arithmetic verified against a reference implementation.
  • Daily cap: grants within one epoch day never exceed REP_CAP_DAY per (address, field); a capped claim advances claimed_total only by what was granted and the remainder is claimable the next epoch day (test spans two epoch days).
  • Reputation never decreases. Every non-zero grant emits ReputationChanged.
  • Reputation is replayable: replaying all events yields the same totals as the boxes.

Costs

  • Every box- or asset-creating call verifies a grouped payment from the sender equal to the MBR delta; underpayment fails; a payment from a third party fails.
  • Invariant 22 test: across a scripted sequence of every M1 operation, the application account’s (balance − min_balance) is identical before and after.
  • MBR and fees measured for publish, claim_article, vote_article, claim_reputation, add_field; recorded in M1_REPORT.md with the exact byte sizes of every box.

Storage

  • Every box key is ≤ 64 bytes (asserted per key type).
  • Article CID with codec ≠ 0x70 fails; malformed lengths and all-zero digest fail.

Events

  • Every state-changing call emits exactly one canonical ARC-28 event with schema_version = 1 (contract v4: schema_version = 2 and a gapless event_seq, D-163).
  • Events are sufficient to rebuild article and reputation state without reading boxes (test does this).

Security

  • Unauthorised governance calls fail. Invalid enums fail. Amendment without parent fails; parent must exist.
  • Application update and delete transactions fail from every account, including the creator. set_governance succeeds only from the current governance address (contract v4: propose_governance only from governance, accept_governance only from the proposed address, D-163).
  • A full M1 test run executes every governance call from a native Falcon-1024 (pqsig) account on LocalNet 5.x; the fee is sized via simulate.

20.1 M2 acceptance criteria [EDIT: added in v2.9; section 19 asks for “the same rigour as M1”]

Section titled “20.1 M2 acceptance criteria [EDIT: added in v2.9; section 19 asks for “the same rigour as M1”]”

M2 is complete only when all of the following pass on LocalNet, on top of every section 20 criterion:

Co-authorship and allocation (D-054, D-072) [EDIT D-072: rewritten for the list declared at publication]

  • publish accepts 0 to 25 co-authors in ascending order, never the submitter, never a duplicate; it creates their boxes and the submitter pays them; 26 fail; with many co-authors the group carries extend calls. Nobody can be added later.
  • A declared address confirms once, in preprint, under_review or final; an address that voted on or reviewed the article cannot confirm; flags and comments do not prevent it. Neither publish nor accept creates a reputation box.
  • After votes of known weights and any sequence of confirmations, every author’s claim equals the reference implementation (floor for co-authors, ceiling for the submitter) whenever they confirmed; the share of a co-author who never confirms is claimed by nobody; the reputation materialised by all authors never exceeds vote_total (property test).
  • Accepted authors cannot vote on, review or flag their article; a declared, unconfirmed address still can. Co-authors cannot version, change status or resolve.

Reviews

  • One review per (article, reviewer), raw-codec CID, recommendation 1–4, not by an accepted author, allowed until final and in disputed, never in retracted. The reviewer pays the review box, the index box and their own reputation box.
  • A useful vote grants w × 3 to the reviewer under the daily cap; an accepted author’s vote is recorded and grants 0; useful_total always grows by the weight; the voter never gets a box.

Comments

  • Threads: depth = parent.depth + 1 ≤ 6, root propagated, direct replies counted, replies only to comments of the same article, comments in every status.
  • Mentions: at most 5, strictly ascending, never the sender, stored only in the event.
  • A comment upvote grants w / 5 (floor); resolution grants 5, only by the submitter, only once, only for a comment of a non-author; all under the daily cap shared by every source. (Contract v4: votes and resolutions damped per pair, resolver minimum, section 8.2/8.3, D-163.)

Flags and disputes

  • A flag needs reputation ≥ 10 in the article’s primary field, weighs 1 + isqrt(rep), is unique per (article, round, flagger) and is never an author’s. The call that makes the round total reach 30 moves preprint / under_review / final to disputed (section 7.3 row 6).
  • Section 7.4 holds while disputed. Only governance resolves: cleared restores the previous status (row 7), retracted revokes the ASA (row 8); both start a new round from zero and keep the old flags. (Contract v4 replaces these two criteria with section 20.2, D-163.)

Costs, events, security

  • Every M2 deposit equals the MBR delta to the microAlgo; underpayment and third-party payment fail; invariant 22 holds across a scripted sequence of every M2 operation and every rejected call.
  • Every M2 call emits exactly one canonical event; replaying all events rebuilds every box of the application, byte for byte, with nothing missing and nothing extra.
  • Every method fits one application call: measured opcode budget ≤ 600, ≤ 8 box references, ≤ 1 024 log bytes; the program fits 8 192 bytes. Results in M2_REPORT.md, for Ed25519 and pqsig senders.

20.2 Contract v4 acceptance criteria [EDIT D-163: added in v3.0]

Section titled “20.2 Contract v4 acceptance criteria [EDIT D-163: added in v3.0]”

Contract v4 is complete only when all of the following pass, in the emulator and on LocalNet, on top of sections 20 and 20.1:

  • Disputes (section 9): a flag carries a 1 ALGO bond, weighs at most 10, needs a reputation box at least 30 days old; the threshold is always 30 and the bond 1 ALGO << clearances for 365 days after the last clearance by governance; a round with no dispute lapses 30 days after its first flag; a dispute expires 60 days after it started; retraction is allowed 14 days after it started, with the CID of published reasons, clearance at once; an author’s own retraction ends a dispute; votes, claims and reviews are allowed in disputed, new versions are not.
  • Settlement: every bond is settled exactly once by settle_flag: back to the flagger after a retraction or a lapsed round, half to the submitter and half retained after a clearance, all retained after an expiry, so an author’s own flags never pay him more than half of their bonds; the flag box’s deposit always returns to the flagger, or follows the submitter’s half (cleared) or is retained (expired) when the flagger’s account is closed; nothing is settled while the round is open. [EDIT D-163 (amended)]
  • Damping (section 8.2/8.3): every grant between the same giver and receiver is w // n, with n counted on-chain per pair; an article vote damps by the largest n over its authors, and must list every author in ascending order; a resolution grants nothing from a resolver below MIN_FLAG_REP.
  • Governance (section 12.1): two steps; zero, the current address and the application’s own address refused.
  • Identity: amendments only by an author of the parent; DOI only by the submitter; Followed.on canonical.
  • Costs and events: every new box (flag with bond, outcome, damping counter) paid to the microAlgo by its creator; invariant 22 restated (section 17) holds through every v4 operation; every event carries schema_version = 2 and an event_seq without gaps; replaying the events rebuilds every box, the new families included. Program ≤ 8 192 bytes, every method within the section 21 C gate; results in V4_REPORT.md.

A. Offline unit tests (algorand-python-testing): weight(), every grant formula, daily cap rollover, state transition table, key encoding lengths, CID validation (both codecs), enum validation, comment depth/root propagation, mention list validation.

B. LocalNet integration tests (algokit-utils, pytest): real ASA creation, inner transactions, opt-in and claim, frozen-transfer failure, grouped MBR payments (correct, under, third-party), invariant 22 sequence, box MBR measurement, event emission and decoding via the in-repo ARC-28 decoder built from the ARC-56 events list (the app clients do not decode ARC-28; the indexer uses algokit-subscriber) [EDIT D-037].

C. Simulation: every integration test runs simulate before sending, asserting resource/box references and opcode budget. Any method that needs more than one app call’s budget MUST be flagged in the milestone report. [EDIT D-073: the gate is 600 opcodes and 8 box references per application call of the group; only publish with more than 4-5 co-authors needs more than one call, through extend().]


22. Open questions (decide as they come up, log in DECISIONS.md)

Section titled “22. Open questions (decide as they come up, log in DECISIONS.md)”
  • Whether asset_name can carry the id at creation time; ARC-19 reserve encoding for the current-version pointer (M5).
  • Refund of MBR overpayment (M1 keeps it; revisit).
  • FLAG_THRESHOLD absolute vs relative to article activity. [EDIT D-163 (amended): closed, absolute and fixed at 30; the bond doubles for 365 days after each clearance by governance (at most four doublings)]
  • Whether disputed should also block submit_review. [EDIT D-020: closed, reviews stay allowed in disputed]
  • Multi-author allocation rule (§10.2 M2 gate) — MUST be decided before M2 implementation, not after TestNet. [EDIT D-054: closed by the owner, equal split with the remainder to the submitter]
  • [EDIT D-066] Whether a review should stop earning once its reviewer becomes an accepted co-author of the article.
  • [EDIT D-057] GRANT_COMMENT = 1/5 grants nothing for voters of weight below 5 (reputation below 16): tune with real activity.
  • Batch claiming across several articles in one call (M5 convenience).
  • Whether MainNet articles should be able to reference TestNet ones (currently: no; networks are fully independent).
  • Second pinning provider choice for M5.
  • Timing of the governance migration to native post-quantum multisig once Algorand ships it.