Skip to content

Protocol specification

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 flat grant when a comment is resolved
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
FLAG_THRESHOLD 30 cumulative flag weight in the current dispute round that triggers disputed
EPOCH_DAY_SECONDS 86400 epoch_day = latest_timestamp // 86400
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.


  • 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

Every key length MUST be asserted in tests.

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
rep_reason: 1 article_upvote | 2 review_useful | 3 comment_upvote | 4 comment_resolved

0 is invalid for every enum.

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 weights, monotonic
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>

[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-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 weight applied at vote time, immutable
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
created_at uint64

reputation

rep uint64 never decreases in M1
updated_at uint64
cap_day uint32 epoch day of the last grant
cap_today uint16 reputation received on cap_day (all sources)

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
disputed status_before_dispute governance resolve_dispute(cleared)
disputed retracted governance resolve_dispute(retracted)

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.

Status new_version invite/accept co-author review vote_article claim_reputation comment flag
preprint ✓ ✓ ✓ ✓ ✓ ✓ ✓
under_review ✓ ✓ ✓ ✓ ✓ ✓ ✓
final ✗ ✗ ✓ ✓ ✓ ✓ ✓
disputed ✗ ✗ ✓ ✗ ✗ ✓ ✓ (recorded, no state change)
retracted ✗ ✗ ✗ ✗ ✗ ✓ ✗

Votes on reviews and comments follow the article’s vote_article column. [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: the third column is now “accept co-authorship” (there are no invitations after publication) and it is ✓ in final too: the list is fixed at publication, so a late confirmation changes nothing for the others. It stays ✗ in disputed and retracted.] [EDIT D-020 / D-055 / D-058: resolve_comment follows the vote_article column too; resolve_dispute is allowed only in disputed; in the disputed row “recorded, no state change” means no STATUS change: the flag box is written, round_flag_weight keeps accumulating and updated_at is stamped.]

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-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}: 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 in that round. Later changes to the accepted-author set (a commenter, voter, reviewer or flagger who is invited and accepts co-authorship afterwards) do not invalidate earlier actions; they remain on record and keep their effects. Under the pull model this is safe: a co-author’s claimed_total starts at the vote_total current at acceptance, which already includes any vote they cast before, so nobody ever claims reputation from their own vote. [EDIT D-072: the safety argument is now simpler: an address that voted on the article or reviewed it can never become an accepted author, so nobody claims from their own vote and no reviewer becomes an author of what they reviewed. Flags and comments made before confirming stay on record.]

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

  • flag() requires the flagger’s reputation in the article’s primary field ≥ MIN_FLAG_REP, flagger not an accepted author, and no existing flag by this flagger in the current dispute round.
  • flag_weight = 1 + isqrt(flagger_rep_in_primary_field) at flag time, stored in the flag box.
  • The contract adds flag_weight to round_flag_weight. When round_flag_weight ≥ FLAG_THRESHOLD and status ∈ {preprint, under_review, final}, status becomes disputed and status_before_dispute is set.
  • Flags are immutable and append-only. Each flag counts exactly once.
  • resolve_dispute() (governance only) sets the outcome, increments dispute_round, resets round_flag_weight to 0, and clears status_before_dispute. Flag history from earlier rounds is preserved (boxes and events) but does not count toward later rounds.
  • FLAG_THRESHOLD is absolute in M1. // DECISION: revisit after observing real activity. [EDIT D-058 / D-065: the flagger needs an existing reputation box with rep >= MIN_FLAG_REP; round_flag_weight accumulates also while disputed; the transition emits no StatusChanged (Flagged carries new_status); resolve_dispute(retracted) revokes the ASA like an author retraction; dispute_round 65 535 takes no flags and cannot be resolved, so no article can be stuck in disputed.]

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

w is the voter’s weight in the article’s primary field at the moment of the vote.

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.

M2 gate. With more than one accepted author, the same votes can be claimed once per author (N× article-vote reputation for N authors). M2 MUST fix a deterministic allocation rule before implementing multi-author claims and record it in DECISIONS.md. Candidates: (a) full vote_total per author, inflation bounded only by REP_CAP_DAY (current text; rationale: reputation belongs to people, not to papers); (b) equal split with the submitter taking the remainder; (c) submitter full, co-authors half. M1 is single-author and unaffected.

[EDIT D-054 (OWNER, 2026-09-29): candidate (b) is the rule. 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) and per (address, field, epoch day).
  6. Reputation never decreases in M1.
  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 ≤ article.vote_total always. [EDIT D-072: more precisely au.claimed_total ≤ entitlement ≤ article.vote_total, and the entitlements of all authors never add up to more than vote_total.]
  9. A co-author accepted after votes were cast can never claim those earlier votes. [EDIT D-072 (OWNER): replaced by: the author list is fixed at publication; every accepted author is entitled to an equal share of all votes; an address that voted on or reviewed the article can never become an accepted author.]

M1/M2 are not Sybil-resistant. Reciprocal voting between fresh accounts grows reputation slowly (weight 1 each, damped by isqrt, bounded by REP_CAP_DAY) but is not prevented. The author↔reviewer loop is closed (§8.2); the reviewer↔reviewer loop across two articles is not. Mitigations (stake, attestations, field-entry requirements) are post-M2 and MUST NOT be improvised earlier.

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. Every event’s first field is schema_version: uint8 (currently 1). Payloads are compact: per application call, logs are limited to 32 entries and 1 024 bytes total. CIDs in events are the 36-byte raw form.

FieldAdded(v, field_id: uint16, name: string)
GovernanceChanged(v, previous_governance: address, new_governance: address, ts: uint64)
Published(v, article_id: uint64, asa_id: uint64, author: address, primary_field: uint16, secondary_field: uint16,
type: uint8, parent_article: uint64, cid: bytes36, coauthors: address[], ts: uint64)
# EDIT D-023: asa_id added; EDIT D-072: coauthors added (114 bytes + 32 per co-author)
ArticleClaimed(v, article_id, author, ts)
Versioned(v, article_id, version: uint16, cid: bytes36, prev_cid: bytes36, ts)
StatusChanged(v, article_id, from: uint8, to: uint8, actor: address, ts)
CoauthorInvited(v, article_id, coauthor: address, ts) # EDIT D-072: removed
CoauthorAccepted(v, article_id, coauthor: address, author_count: uint16, claimed_total: uint64, ts)
ReputationClaimed(v, article_id, author: address, consumed: uint64, ts) # followed by 1–2 ReputationChanged
Reviewed(v, article_id, review_seq: uint64, reviewer: address, recommendation: uint8, cid: bytes36, ts)
Voted(v, target_kind: uint8, article_id, target_seq: uint64, voter: address, weight: uint64, ts)
Commented(v, article_id, comment_seq: uint64, author: address, reply_to: uint64, root_seq: uint64,
depth: uint8, cid: bytes36, mentions: address[], ts)
CommentResolved(v, article_id, comment_seq, resolved_by: address, ts)
Flagged(v, article_id, dispute_round: uint16, flagger: address, reason: uint8, weight: uint64,
round_total: uint64, ts)
DisputeResolved(v, article_id, dispute_round: uint16, outcome: uint8, new_status: uint8, ts)
ReputationChanged(v, address, field_id: uint16, delta: uint64, new_total: uint64, reason: uint8, ts)
# contract v3 declarations (EDIT D-106): one event per call, no box
IdentityDeclared(v, address, scheme: uint8, value: string, ts) # scheme 1 = ORCID iD; "" withdraws
DoiDeclared(v, article_id, version: uint16, doi: string, declared_by: address, ts) # version 0 = every version; "" withdraws
Followed(v, follower: address, target: address, on: bool, ts)

[EDIT D-017: bytes36 is the ARC-4 type byte[36]; selectors derive from the type-only signature.] [EDIT D-020 / D-058: Flagged is (v, article_id, dispute_round, flagger, reason, weight, round_total, new_status: uint8, ts) = 73 bytes; 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: CoauthorAccepted.claimed_total is kept for compatibility and is 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); with 5 mentions the log is exactly 278 bytes.] target_seq is 0 for article votes. Commented with 5 mentions is ≈ 270 bytes, within budget. Events MUST contain enough information to rebuild every indexer projection without reading boxes. 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 in M1; log as open question).

[EDIT D-001/D-003/D-023: create(governance: address) exists (creation only, no bare create, no update/delete paths); publish, vote_article, claim_reputation and add_field (in M2 also submit_review, vote_review, comment, vote_comment, flag) take a leading pay: PaymentTransaction argument whose amount must cover the application’s min-balance increase; article_id is the contract sequence.] [EDIT D-072 / D-073: publish takes a last argument coauthors: address[]; invite_coauthor is removed; extend() changes nothing and emits nothing: a client adds ceil(boxes / 8) − 1 calls to it in the publish group, where boxes = 3 + (1 if secondary) + (1 if parent) + co-authors.] [EDIT D-055 / D-061: accept_coauthorship, resolve_comment and resolve_dispute create nothing and take no payment; comment(..., mentions) expects the addresses in strictly ascending order.]

# governance (sender == governance address)
add_field(field_id: uint16, name: string)
resolve_dispute(article_id: uint64, outcome: uint8)
set_governance(new_governance: address) # migrate governance (e.g. to PQ multisig)
# publishing
publish(cid: bytes36, primary_field: uint16, secondary_field: uint16,
type: uint8, parent_article: uint64) -> uint64 # returns article_id (sequence; the asa_id is in the box and in Published) EDIT D-023
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 (M2)
invite_coauthor(article_id: uint64, coauthor: address) # sender == author
accept_coauthorship(article_id: uint64) # sender == invited address
# reviews
submit_review(article_id: uint64, cid: bytes36, recommendation: uint8) -> uint64 # review_seq
vote_review(article_id: uint64, review_seq: uint64)
# votes
vote_article(article_id: uint64)
claim_reputation(article_id: uint64) # sender == accepted author
# comments
comment(article_id: uint64, cid: bytes36, reply_to: uint64,
mentions: address[]) -> uint64 # comment_seq
vote_comment(article_id: uint64, comment_seq: uint64)
resolve_comment(article_id: uint64, comment_seq: uint64) # sender == author
# flags
flag(article_id: uint64, reason: uint8)
# 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, value <= 64 bytes (D-107)
declare_doi(article_id: uint64, version: uint16, doi: string) # sender == accepted author; version <= current; a non-empty
# DOI starts with "10.", <= 200 bytes, article not retracted (D-108)
follow(target: address, on: bool) # target != zero address, != sender (D-109)

Governance is a single address stored in global state, changeable only by a call signed by the current governance address (set_governance(new: address), emits GovernanceChanged), so it can migrate without redeploying the contract and the indexer can rebuild the governance history from events alone. Governance pays its own fees and MBR (field boxes) like any other actor.

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 set_governance. // 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 decrease as a result of any user action (invariant 22, tested).

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.

Operation Payer Components
publish author call fee + inner fee + ASA MBR + article box + submitter co-author box
claim author opt-in MBR + call fee + inner fee
vote_article voter vote box + fee (never a reputation box, never author-count dependent)
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 + fee
comment commenter comment box + reputation box (if first) + fee
vote_comment voter vote box + fee
invite_coauthor author co-author box + fee
accept_coauthorship co-author fee only
flag flagger flag box + fee
add_field governance field box + fee

[EDIT D-072 / D-056 (M2, measured in M2_REPORT.md): article box (9 + 192 bytes) = 82 900; publish deposit = 212 200 + 29 300 per declared co-author (944 700 µAlgo with 25); review box (17 + 85) = 43 300; reviewer index (42 + 8) = 22 500; comment box (17 + 118) = 56 500; flag box (43 + 17) = 26 500; first review = 91 100, first comment = 81 800 (they include the actor’s own reputation box).] MBR figures (fees excluded, they are measured): article box (9 + 192 bytes) = 82 900; co-author box (42 + 25 bytes) = 29 300; reputation box (35 + 22 bytes) = 25 300; vote box (42 or 50 + 24 bytes) = 28 900 or 32 100. Publish total = 212 200 µAlgo deposit; 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, reconciled in M1_REPORT.md] Measure, don’t assume. M1_REPORT.md reports the actual fee paid per operation on LocalNet, separately for Ed25519 and pqsig senders.


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

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, round, intra, app_id, event_name, event_version, payload_json, ts) — primary key (tx_id, intra); ingestion is idempotent.
  • 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; a review qualifies when its author is not an article author, has reputation >= 10 in the primary or secondary field or a verified ORCID iD with >= 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.]
  • 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 in M1/M2.
  13. Reputation grants are deterministic from on-chain state and integer-valued.
  14. Resolved comments cannot be resolved twice.
  15. Flags are append-only; each counts once, only in its round.
  16. Status transitions outside §7.3 always fail.
  17. Retracted and disputed content remains queryable.
  18. No article, co-author, version, review, comment, vote or flag record is ever deleted.
  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.
  22. The application account’s spendable balance never decreases as a result of a user action (every MBR delta is covered by the caller; every inner fee is pooled from the caller).
  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-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.
  30. [EDIT D-065] No article can be left in disputed without a possible resolution.

M1/M2 explicitly consider, and either mitigate or document as a known limitation:

Threat Handling
Sybil accounts, reciprocal vote/review farming across accounts Known limitation, damped by isqrt and REP_CAP_DAY; no prevention
Author↔reviewer “useful” cartel on the same article prevented: author’s useful votes grant 0
Comment-resolution farming prevented: commenter not an author, one-time resolution
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: claimed_total initialised to vote_total at acceptance [EDIT D-072: prevented by the closed list: nobody can be added after publication]
Vote cost or log budget exploding with author count prevented: pull model; vote touches no reputation box
Flag spam MIN_FLAG_REP, one flag per round per address, weight-based threshold
Voter forced to fund others’ storage prevented: reputation boxes self-funded (§10.5)
Platform drained by MBR prevented: invariant 22
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; indexer surfaces duplicates
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 set_governance
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.
  • 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.
  • 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.

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.

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.

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