Cairns Protocol: Working Design #
Scope #
This is the implementation design for Cairns, an open protocol on ATProto for splitting up work on public problems, checking it, and crediting whoever did it. For a plain-language introduction, read the README first. HelpPeer is the first client.
Cairns promises that every claim is signed and attributable, and that anyone can recompute a snapshot from the records it lists. Confidence is an interval that keeps narrowing as evidence arrives. Nothing is ever final.
Cairns does not promise that agreement is truth, that reported effort was really spent, or that a snapshot saw every record. It does not prevent collusion. It stops people from choosing their own confirmers, and lets labelers remove bad actors after the fact.
Out of scope: private or embargoed work, since every record is public; storing work products, which Cairns tracks by hash; and payments, since credit is accounting and paying for work would change the threat model. Public records include public metadata. A stone shows its author, sponsor, timing, tools, models and self-reported cost. Contributors who don't want to reveal those should use a separate identity.
The reference workloads are OpenElections precinct extraction first, then Mersenne prime search and the Voynich manuscript.
Vocabulary #
The protocol itself is a handful of record types and three actions. Everything else is either a term for something a reading derives, or a client command.
Records #
| Record | What it is |
|---|---|
| source | A content-addressed input: a PDF, an image, a dataset, a search-space definition. |
| stone | A signed claim at a site. Its result is found, null (searched, found nothing), attempt (tried an open-ended task, didn't finish) split (the site is too large for one stone; here are its parts) or converted (the site's source, read through a transform; here is the output's hash). An equivalence stone is a stone whose site is a pair of sites. |
| witness | Signed evidence about a stone that is not a competing claim: a check result, a judgment, or a recall of a faulty method. |
| check | An optional deterministic program, compiled to WASM, that anyone can re-run for the same result. |
| policy | The rules that turn evidence into confidence and credit. |
| project | Maintainers, a scope, its layers of work and a policy. |
| snapshot | A signed, pinned reading. |
| seen | A timekeeper's record, once per epoch, of which records existed. |
| sponsor, sponsoredBy | The two halves of the link between a human and an agent. |
| follow, block | One participant following or blocking another. Neither changes any weight. |
| member, memberOf | The two halves of the link between an organization and a person. |
| issue | A report or question about a project, policy, site or stone, with comments and a maintainer-set state. |
| procedure | How to do the work at one course's sites: a skill bundle of instructions, checklist and examples. |
| transform | A Starlark script that turns a source into a derived source, run inside the interpreter check Cairns ships. |
| export | A snapshot's output sent to an official repo, with the hash of each file. |
A schema record holds a payload schema's definition. Two housekeeping records round this out: compromise marks the window when a key was stolen, and schemaAlias maps one payload schema to another that means the same.
The three actions are place a stone, witness a stone, and amend your own stone. Withdrawing is amending to an empty stone.
Derived terms #
| Term | Meaning |
|---|---|
| mark | A typed pointer into a source: a page region, a cell range, an integer interval. |
| site | The unit of work: a base, a mark and a payload schema. Stones at the same site compete. |
| course | One layer of a project. Course 0 marks into sources. Each higher course builds on values from the course below. |
| cairn | Every stone and witness at one site. |
| apex | Each author's newest stone at a site. Only apex stones count. |
| retrace | A stone at a site that already has one, made without looking at the existing values. |
| reading | The result of applying a policy: tallies and a confidence interval for each candidate value at every site. |
| anchor | Evidence that doesn't rely on contributors' honesty, such as a check or a match against an official total. |
| composer | What rebuilds a parent value from its children, which lets a course be split and sampled: the built-in concat, or a check. |
| cluster | Sites linked by same equivalence stones. |
| epoch | A fixed span of drand beacon rounds, 1 hour by default. Draws, sampling and timekeeping run on epochs. |
| draw | The random pick of which sponsors may retrace which sites in an epoch. |
A reading also gives each stone a status. Statuses are computed, not stored: leading, pebble (an honest read that trails the leading value), hollow (refuted by an anchor) and withdrawn. A stone whose site has no single leader is tied; an attempt, a sealed payload, a stone at a site whose canonicalizer cannot run, and a stone whose author has two sponsors give no read. A stone whose author has another unamended stone at the site is branched and gives no read either. A stone that fails a required check is hollow and gives no read. A stone that a deny list or an honored label gives weight 0 is excluded and gives no read (see Labels and blocks).
Each author's read at a site is the apex of their amend chain. An amend takes effect only when it has the same author and site as the stone it names and pins that stone's current CID. Stones are identified by URI, because identical records in two repos share a CID. An agent counts as its human's when the human's sponsor record and the agent's sponsoredBy record name each other; an author without such a link sponsors itself. The leading candidate has the highest low bound, then the most agreeing weight; if two candidates share both, the site is tied.
The people and services are participants (any DID, human or agent), sponsors (humans vouching for agents), maintainers (who run a project), indexers (AppViews that compute readings; anyone can run one) and timekeepers (indexers a policy names to publish seen records).
Client commands belong to HelpPeer and the cairns CLI, not the protocol: foundation (like git init), fetch (sources into a local store, by hash), draw, task (a site's task packet), read (like git status), diff, snapshot, export (like git push), fork, mount (like a submodule), recall, join, projects, follow, block, issue and mcp. There is no merge. Cairns recomputes the reading whenever a stone lands, so concurrent work only adds evidence.
Where records live #
Every participant writes to their own ATProto repo on a PDS, next to any Bluesky posts. Placing a stone is a createRecord call with collection org.cairnsproto.stone. A PDS stores record types it doesn't know. Each PDS streams its changes to the relay, and the Cairns indexer reads org.cairnsproto.* records from that stream. The indexer is a cache anyone can rebuild, and nobody's work depends on it accepting a write.
- People sign in to a client such as HelpPeer with ATProto OAuth, and the client writes stones into their repo.
- Agents write either to their sponsor's repo or to their own. In the sponsor's repo, each stone names its agent in
method.agent, a stable label the sponsor chooses. Draws are keyed on the sponsor either way, so independence doesn't change, and onboarding is just signing in. An agent can also have its own DID and account, linked to its human bysponsorandsponsoredByrecords. Then a label or block can target one agent without touching the sponsor's other agents.cairns sponsor link <agent>, signed in as the human, andcairns sponsor accept <human>, signed in as the agent, write the two halves. Links don't chain, and an agent can accept only one human, since an agent with two sponsors counts for neither. - v1 runs on bsky.social. Each account can create about 11,666 records a day (35,000 points at 3 per create).
- Accounts can move later. A record's address is its DID, collection and record key, and migrating to another PDS keeps all three, so references still resolve. This needs did:plc, which bsky.social uses.
- A HelpPeer PDS comes next when agent volume grows. Indexers can read its stream directly, so the Bluesky relay's limits for new servers only delay third parties.
How a project builds upward #
A project is a stack of courses. (In masonry, a course is one layer of stones in a wall.) Each course's sites point into values from the course below, so splitting the work is itself work, and it earns credit from everything built on it later.
- Course 0 marks into a source: a page, a folio, an exponent range.
- Higher courses bind to a parent value. A site's base is a source hash, or a parent site plus a digest of the parent value it builds on. Children built on different parent candidates are different sites. If a parent value loses, its children lose with it and earn nothing.
- The project defines its courses: for each one, the parent course, mark type, payload schema, composer, minimum mark size and bounds, and for a course that breaks its parent value down, its split. Closed bounds mean a null proves the region empty unless the search was wrong. Open bounds mean any failure can be superseded later.
- Derived stones combine inputs across sites, such as a county total from precinct rows. Both kinds of edge carry dependency and credit.
| Course | A site is | A stone says | Best evidence |
|---|---|---|---|
| 0 · Slice | One scanned page | Where each article, photo and caption sits | Retraces agree on the regions |
| 1 · Transcribe | One article region | The article's text | Blind retraces, compared after canonicalizing |
| 2 · Extract | One paragraph of a transcription | The facts it states | Retraces, plus a quote that must match the transcription |
| 3 · Link | A question across articles | A timeline entry derived from course-2 stones | Contradictions surface as contested sites |
The same shape fits the other workloads. Elections go from a PDF page to table regions, raw grids, normalized rows and a county total checked against the certified figure. Mersenne goes from a search space to range slices to a result per exponent. Voynich goes from folios to line regions, glyph transcriptions and hypothesis tests.
Each course binds to values from the course below. Credit flows back down, multiplied by s per course.
A course opens sites only on values the course below has made confident enough, and every credited stone sends a share of its credit back down to the stones it stands on.
Work breaks down, then builds up. A course 0 site is often "break this source into its parts", and its value is the list of parts: the contests in a results PDF, say, with the pages each covers. A course 0 opens a site on each source its mark type can read, by the MIME type the maintainers' source records give it: a pdfRegion course on PDFs, a sheetRange course on spreadsheets. A source with no type given opens every course 0. A course's split is a JSON pointer to an array in the parent value, such as /contests. When a parent value's low bound clears the open-children bar, the course opens one site per element of that array, built on that value and marked by a pointer to the element (/contests/0, /contests/1, ...). A course built without a split on a course that converts its sources opens one site per conversion instead (see Transforms). Indexers list those sites as open work, so the first read of each needs no draw.
A child site is held until its parent site is open and the parent value it builds on has cleared the open-children bar. A site more than 64 parent links below a site on a source or a pair is held whatever its values, which bounds the work a reading does on one chain. A stone placed on a held site is kept and tallied, so nobody's early work is lost, but it counts for nothing: the site gets no draws, earns no credit, passes no flow-back and goes into no export. It counts once its parent value opens it, and if that value loses instead, the site stays held.
When the retrace budget is spent and intervals still overlap, the site is ambiguous until new evidence arrives.
Each agreeing read or anchor narrows the leading value's interval. When its low bound clears the open-children threshold, child sites open and run the same loop. When it clears the export threshold, the value goes into the next export. Retraces keep counting after that.
How finely work can be split #
A course can be split only when a parent value can be rebuilt from its children. A composer does that rebuilding. The built-in concat joins every array field of the parts in order and requires every other field to be equal in all parts, which covers lists of contests, articles or rows; anything else, such as summing, is a composer check. Article text is its sentences joined. A precinct total is its rows summed. A null over a range holds if every sub-range is null. A long computation chains its segments end to start.
Without a composer, items are atomic: one glyph, one yes-or-no judgment, one caption's meaning. Atomic items gain confidence only from whole retraces, which is fine because they are small. Each course also sets a minimum size, below which checking costs more than it saves.
Split stones #
A maintainer can't know in advance how big every site will be: one county's results are a page, another's are 110. So a site in a course with a composer can be split by the agent who finds it too large. A split stone has result split and an inline payload {parts: [{pages: {first, last}}, ...]}, page ranges that tile the site's pages in order. Its value is that payload under $type org.cairnsproto.draft.defs#split, so it never shares a digest with a found value, and it is tallied at the site like any other value: a retracer who agrees places the same split.
Agreement needs the same parts, so the split rule belongs in the course's procedure and is mechanical: "if the site covers more than 10 pages, split it into ranges of 10 from its first page." cairns source split <dir> --pages 10 writes that split into a task packet's stone. The only judgment left is whether the site needs splitting.
When a split clears the open-children bar, the course opens one site per part, in the same course, built on the split value and marked /parts/0, /parts/1, ... A part takes the course's procedure and schema like any site, and can be split again. A course built on this one opens its sites on the parts' values, so a split costs it nothing. A split stone's credit is a breakdown read's: a share flows back from the work built on it.
A split stone in a course without a composer isn't the project's: indexers leave it out of the project's reading, and cairns place refuses it. cairns foundation refuses a course whose sites can cover many pages without a composer: a course 0 over whole PDFs with no minMark, and any split course. A composer is what rebuilds a split site's whole value from its parts' values, for an export or to check a found stone placed at the split site. Readings don't run composers yet: a split site's value stays the split, and a found stone there competes with it as another value.
Laying a foundation #
cairns foundation creates a project, the way git init creates a repo, and makes whoever runs it the maintainer. It names the sources (or an open scope), lays out courses from the spec's defaults, attaches a default policy, and names a timekeeper. Payload schemas and procedures are records too, so one commit to the maintainer's repo publishes everything a contributor needs; nothing needs a domain or a separate host. cairns foundation --update <project> republishes the same file at the project's own URI, in one commit. Records whose content is already in the repo are reused, and the rest are created. The project is rewritten in place. Schemas, procedures and policies that neither the new version, the version it replaces nor another project in the repo pins are deleted; the replaced version keeps routing until the new one activates, so its records go on a later update. Sources stay, since stones name them by hash. The defaults split work finely, because composers only combine upward: sentence-level work can become article text later, but article-level work can't be split into sentences. Every site should be one agent's work in one sitting; where a maintainer can't bound a site's size in advance, the course names a composer and its procedure a split rule, so an oversized site is split by the first agent to find it (see Split stones). docs/maintain.md walks a maintainer through it.
| Source type | Default courses and chunk |
|---|---|
| PDF or scanned page | Slice into regions, then transcribe per line |
| Image | One region per labeled object |
| Text | Per sentence |
| Table | Per cell, composed into rows |
| Search space | Per unit (exponent, integer), stacked into ranges |
Procedures #
A course's procedure says how to do one site of work. It is a skill bundle in the Agent Skills format: a SKILL.md with instructions, plus a checklist, worked examples and reference files. The project pins one procedure per course by strongRef, and a stone records the procedure it followed in method.procedure.
- Text and images only. A bundle that contains executables or a
scripts/directory is rejected. Anything the agent runs to check its own work is a Cairns check that the procedure lists, so it runs as WASM under the check profile. - Tools come from the client. A procedure lists the tools it needs, such as
source.region(fetch a mark's region of the source),pdf.renderorcheck.run. The CLI andcairns mcpprovide these, scoped to the drawn site. The CLI providespdf.renderascairns pdf render,pdf.textascairns pdf text(each word with its box inpdfRegionunits)source.splitascairns source split, which writes a split stone by page count (see Split stones), andsource.regionascairns source region, which renders what a task packet's mark covers: apdfRegion's page and box, animageRegion's frame and box, every page of a PDF a site covers whole (or an image's first frame), or for a mark into the parent value, the pages its entry names inpages: {first, last}; andtransform.runascairns transform run, which runs a transform script or record on a source inside the interpreter check and, for a record, can write a converted stone (see Transforms). PDFs go through Poppler in a separate process with a time limit; images decode in process under size and memory limits. Routing skips contributors whose client lacks a listed tool, the same way it skips models that don't count. - Variants per model family. A procedure can carry a variant for a model family. This is where prompts tuned for one model belong.
- Examples never leak live answers. Worked examples come from sites past the stop-routing bar or from sites marked as examples, so an example can't hand a retracer the value it's checking.
- People use the same procedure. HelpPeer renders
SKILL.mdas the instructions on its work page, as escaped Markdown.
A procedure is prompt text, so changing one is an executable-surface change under pinning and the activation delay. A change that keeps the procedure's tools and checks can be adopted before it activates (see Maintainer changes). Policies can weight or require procedure versions the way they do models, and a recall can target a procedure as well as a method or config hash.
Transforms #
Some sources can't be read the way a page is: a spreadsheet export, a zip of XML, a folder of HTML reports, each county's layout a little different. Reading them takes code, and code written by strangers is how malware spreads. A transform turns one of these sources into a clean JSON source without anyone running native code on it.
- A transform is a script. It is a
transformrecord holding a Starlark script, the source MIME types it accepts and the payload schema of what it produces. Starlark is a small Python dialect built to run untrusted code deterministically: no I/O, no clock, no randomness, and no unbounded loops. - Cairns ships the interpreter. Every transform runs inside one interpreter check that Cairns publishes, versions and pins like any check. It runs as WASM under the check profile, and it carries the decoders transforms call: zip, XLSX, CSV, XML and HTML. The source's bytes go in and the output's bytes come out. A hostile script can only produce wrong output, and agreement catches wrong output. Clients decode these formats only inside the interpreter, never natively.
- Its output is a derived source. Anyone can re-run the transform and get the same bytes, so a derived source needs no host: a client that needs one runs the transform itself. A
sourcerecord whosederivedFromnames the input's hash and the transform can still publish a copy. It proves where the output came from, not that the transform read it correctly. - Converting is a course. A converting course's sites are whole raw sources, and its schema is the one its derived sources follow. A converted stone has result
convertedand an inline payload{source, transform}: the sha256 of the transform's output on the site's source, and the transform record at the version that produced it. Its value is the output's hash alone under$typeorg.cairnsproto.draft.defs#conversion, so it never shares a digest with afoundvalue. The transform is provenance: it tells a client what to re-run. It is tallied like any other value: two stones agree when their outputs have the same hash, so two transforms written independently, or two versions of one transform, that produce the same bytes are two agreeing reads, and the sites built on the conversion stay the same sites. A new version that changes the bytes is a competing value, and the work built on whichever value loses loses with it. Only a site on a source takes one.cairns transform run <record> --stone <dir>runs a transform record on a task packet's source, checks the output against the packet's schema and writes the converted stone. - The next course works the derived source. When a conversion clears the open-children bar, each course built on the converting course without a
splitopens one unmarked site on it. Indexers never see derived sources.cairns taskre-runs a transform on the raw source for any site whose base chain starts from a conversion, and gives the agent the output as the site's source. It tries the transforms the conversion's stones name, those the project lists first, and uses the first whose output has the conversion's hash. It refuses the packet when no transform's output has the hash the conversion names, which makes the conversion hollow, or follows the converting course's schema. No agent works on bytes that don't follow from the source. Courses above that one split its values as usual. - A maintainer can vouch for one. A transform listed in the project's
transformsfor a source's MIME type is trusted to read that layout, so a converted stone naming it needs no agreeing reads: every client re-runs it before building on it. Listing one is an executable-surface change, under pinning and the activation delay. When a site's only candidate holds a converted stone naming a listed transform at the listed version, its interval is [1, 1]. When another candidate exists, the vouched stone counts as a passed anchor for its candidate and every candidate is tallied as usual, so a wrong vouched conversion can be outvoted. - Techniques spread as records. An agent that finds a layout no transform handles writes one, checks it with
cairns transform run, publishes it and places a converted stone; other contributors can find it withcairns transform list --accepts <mime>, read it and run it with nothing at risk. A procedure for a converting course liststransform.runand names the transforms it expects. When none fits, the agent opens an issue rather than guessing. - Script text is data. An agent reads another contributor's transform the way it reads a source: comments and strings in it are not instructions. The join review lints transforms a project lists the way it lints procedures.
- Size is bounded. The interpreter's memory and gas are the check profile's. A source too large for them is worked by archive member, or split, as any oversized site is.
Mounting other projects #
A project can build on another project's courses. One day's newspaper is a project, a month project mounts the days, and a year project mounts the months. A mount pins one upstream snapshot. Moving to a newer one works like a dependency bump: a snapshot diff shows which upstream sites changed, and only downstream work built on those goes stale. When downstream work earns credit, the share that flows upstream is recorded in the downstream project under its policy.
Credit flows down the stack #
When a stone earns credit c, each stone beneath it earns c × s^d, where d is the distance in courses and s is the policy's flow-back share (default 0.25). Flow-back is added on top of a stone's own credit. One stone can collect at most five times its own credit in flow-back, so the cheap work of slicing a page can't out-earn the transcription built on it. Maintainers can change the cap.
Evidence and confidence #
Nothing in Cairns is ever finally settled. The protocol counts evidence, the policy turns those counts into a confidence interval for each candidate value, and each action has its own bar on that interval.
The default evidence is agreement. Most projects have no good automated check, and they don't need one: two independent contributors reading the same piece and getting the same answer is evidence. Checks are optional. They exist for problems where searching is hard and verifying is cheap: a Mersenne residue, a counterexample, a Lean proof, a reconciliation against certified totals. A project that has one must ship it as WASM under the Cairns check profile, so every indexer gets the same result. Fuzzy work is verified by judges answering a pinned prompt instead (see Judges). Anchors, evidence that doesn't depend on anyone's honesty, are the strongest evidence. Each anchor is valid only within its stated boundary: a county total does not validate individual precincts.
Tallies and the confidence function #
For each candidate value at a site, after canonicalization, a reading counts agreeing sponsors (drawn and volunteered), disagreeing sponsors, sampled segments, anchors passed and failed, and labels. These are integers, so every indexer with the same records gets the same tallies.
The policy names a confidence function, itself a check, that maps tallies to an interval [low, high]. The default treats each independent read as a trial and reports a 90% interval on the share of reads that agree:
| Reads | Interval |
|---|---|
| 1 agrees | 0.22 to 0.97 |
| 3 agree | 0.47 to 0.99 |
| 3 agree, 1 disagrees | 0.34 to 0.92 |
| 10 agree | 0.76 to 1.00 |
The default interval is computed exactly, so every indexer gets the same integers. With a agreeing and d disagreeing reads it is the equal-tailed 90% interval of Beta(a+1, d+1), in parts per million and rounded outward. Reads are counted per sponsor, and a sponsor whose agents read different values at a site counts for neither. Each read is weighted by its model weight and, if volunteered, the volunteered weight, and only whole reads count: two volunteered reads make one.
Without an anchor, the low bound stays at 0 until at least one drawn retrace agrees, so self-chosen confirmations alone never clear a bar. A passed anchor stands in for the drawn retrace. A failed anchor or a failed required check sets the low bound to 0. Sampled evidence narrows the interval only as far as the sample supports.
Two reads from one setup can share a mistake, so agreement between them is weaker evidence than agreement between two setups. A read's setup is its method.model, modelVersion and config hash; a read that declares no model is its own setup. Among one candidate's counted reads, the highest-weighted read of each setup keeps its weight and each other read of that setup is weighted further by the policy's models.sameSetupWeightPpm, 1,000,000 by default. A read's family is its method.model; a read that declares no model is a family of its own sponsor. A candidate clears the open-children and export bars only when its counted agreeing reads come from at least thresholds.minFamilies families, 1 by default. Its interval doesn't change, so routing still sends it work.
Thresholds #
| Action | Default bar on the low bound |
|---|---|
| Open child sites in the next course | 0.8 |
| Finalize credit | 0.9 |
| Export | 0.95 |
| Stop routing new work | 0.99, or when one more read would barely move the interval |
Consumers can apply stricter bars than the maintainer. Routing sends work where one more read would raise confidence the most, weighted by the maintainer's priorities.
A site's need is the need of the priority that names its site key, else of the priority that names its course, else 1. A project's need is the sum of the needs of its open and contested sites.
Sites are described by their intervals: open (no candidates), contested (two candidates' intervals overlap), ambiguous (still overlapping after the retrace budget is spent; reopens on new evidence) and escalated (anchors and reads disagree, so the policy's escalation list is asked, and their answer is a witness). A contested or tied site is ambiguous once its drawn reads number at least retraceBudget times the number of its candidates that schedule retraces (see Floods), counting one if none does. A new candidate that schedules retraces raises the budget, which reopens the site. An ambiguous site adds nothing to its project's need and is routed only when a priority names it.
Draws #
Draws stop contributors from choosing who confirms them. In each epoch, a sponsor is drawn for a site when hash(beacon || siteKey || sponsorDID) falls below a threshold T. The beacon is drand quicknet, a public random value every 3 seconds that anyone can verify. Draws are keyed on the sponsor, so one sponsor gets one draw per site per epoch no matter how many agents it runs.
Epoch e holds quicknet rounds eN+1 through (e+1)N. Its beacon is round eN, the round that closes epoch e-1, so the beacon is known as soon as the epoch starts. Epoch 0 has no beacon. Readers verify each beacon's BLS signature against the quicknet public key and take its randomness as sha256 of the signature. A sponsor is drawn when the first 8 bytes of sha256("cairns/draw/v1" || 0x00 || randomness || siteKey bytes || sponsorDID), read as a big-endian integer x, satisfy x · 10⁶ < T · 2⁶⁴, with T in parts per million. Integer math keeps every indexer's draws identical.
Example: Alice and her friend Bob both read 412 at a precinct site. Neither was drawn, so the low bound for 412 stays at 0. An hour later the beacon draws Carol, who also reads 412, and confidence can now climb. If Carol reads 421 instead, the intervals overlap and the site needs more retraces.
| Setting | Default | Effect |
|---|---|---|
| Starting threshold | 1/32 | With 100 active sponsors, about 3 are drawn per contested site |
| Escalation | Doubles each epoch without a drawn retrace | A quiet site opens to everyone within about 5 hours |
| Retrace window | 24 epochs (one day) | How long a drawn sponsor has to deliver |
| Volunteered weight | 0.5 | Volunteered retraces count, but can't lift a low bound alone |
| Drawn agreeing retraces needed | 1 | High-value policies can require 2 |
| Epoch length | 1200 rounds (1 hour) | As short as 20 rounds (1 minute) for time-critical work |
The default starting threshold suits a pool of about 100 sponsors. With a few sponsors, escalation reaches everyone within five epochs, and then a draw picks nobody in particular. A maintainer who gives cairns foundation an expectedSponsors estimate gets a starting threshold that draws about two of them per site, 2/N rounded up and capped at everyone, unless the policy sets one itself. The estimate is client input; only the threshold is recorded, and foundation --update with a new estimate retunes it as the pool grows.
Time-critical projects, such as tabulating results on election night, shorten the epoch, the retrace window and the escalation schedule in their policy. Nothing else changes, but there are costs. A timekeeper's cadence follows the shortest epoch that names it, so a one-minute epoch means up to 1,440 seen records a day, about an eighth of a bsky.social account's daily record limit, however many projects share it. Blind payloads open sooner, which narrows the window for copying but doesn't close it. With few sponsors online, short epochs escalate quiet sites to everyone quickly, so a fast project needs a large pool of contributors ready at the start.
Draws choose who retraces. A site no stone has reached yet needs no draw: its first reads are volunteered and still earn full credit, so cairns draw --list lists such sites separately as open work for anyone.
A sponsor must exist for one full epoch before it can be drawn (first seen in epoch c, it can be drawn from epoch c+2, after the beacon it might grind against was unknown at creation), and a sponsor that already has a stone at a site can't be drawn for it. Draws are not Sybil resistance: someone running several sponsor identities can wait for one to be drawn. Policies that need more can admit sponsors only through a labeler's attestation or an allowlist.
A stone's draw.beaconRound claim holds when the round is a verified beacon for epoch e, the site had opened by e, the stone was first seen in epoch e or within the retrace window after it, its sponsor was eligible in e, and the beacon draws the sponsor at the threshold for e. That threshold starts at the policy value and doubles for each epoch since the site opened or since the last epoch with a drawn stone, whichever is later. A site opens in the epoch its earliest current stone was first seen. Without that bound, a stone at a new site could choose among every beacon in its window and be drawn far more often than the threshold allows.
Public draws let anyone see who was drawn for a site as soon as the beacon lands, and lean on them before they deliver. For high-stakes projects, such as a disputed election, a policy can set secretDraws: true. Each sponsor then publishes a VRF public key in a drawKey record. A sponsor is drawn when VRF(key, beacon || siteKey) falls below T, and the stone carries the VRF proof, so nobody else knows who was drawn until the stone arrives.
- One key at a time. A new key takes effect one full epoch after it's published, so a sponsor can't keep trying keys against a beacon they've already seen.
- The CLI computes secret draws locally. The indexer can't list or route them. Escalation still counts drawn retraces once they arrive.
- Missed draws are invisible. A drawn sponsor who never delivers leaves no trace, so a secret-draw policy should start with a higher threshold.
A policy can set mergeOrgs: true, so members of one organization count as a single sponsor: the draw is keyed on the org's DID, an org with a stone at a site can't be drawn for it, and agreeing reads from members of the same org count as one. It is off by default.
Gold sites #
A gold site is an ordinary open site whose answer the maintainer already knows, from checking it by hand or against an anchor. Gold sites let a setup (a model version, procedure and config hash) find out it is wrong before its stones do damage. The common case is an honest contributor whose setup has quietly degraded.
- Commit, then reveal. Each epoch the maintainer publishes a
goldrecord holding only a Merkle root over(siteKey, canonical value, salt)entries, so nobody can tell which sites are gold. After the retrace window for that epoch closes, the maintainer publishes the entries, and anyone can check them against the root. An entry that isn't revealed, or doesn't match the root, is void. - Fresh sites only. Gold sites have no public stones when they're committed, so there's nothing to copy. Routing treats them like any other open site, because the indexer doesn't know which they are.
- Scores are public. For each project, the indexer reports every setup's agreement with revealed gold entries. The CLI shows a contributor their own score, and HelpPeer shows all of them. By default a score only informs. A policy can set
calibration, a weight curve such as half weight until 10 gold passes. - Gold answers can be wrong. If many setups that agree with each other disagree with a gold entry, the indexer flags that entry for the maintainer.
- A revealed entry counts as the maintainer's own volunteered read, unless it came from an anchor, in which case the anchor counts.
Blind retraces #
Stones are public, so a drawn retracer could copy the existing value instead of doing the work. Task packets leave existing values out, which keeps honest agents from being anchored. For more, a policy can set blind: true: contributors encrypt each payload to the drand round that closes the epoch, using drand's timelock encryption. Nobody can read any payload from that epoch until the round is published, and then they all open at once. This costs up to one epoch of delay. It does not stop people who share answers outside the protocol.
Other policy rules #
- Canonicalizers, not tolerances. The policy names a canonicalizer for each payload schema, so "1,234" and "1234" can be the same candidate. It is either a check or a list of built-in steps applied in order to every string value, repeated until the value stops changing:
text(Unicode NFC, trim, collapse whitespace),integerStrings(drop a plus sign, leading zeros and digit-group separators),lowercase, oridentity. A schema the policy does not name uses identity. Candidates are equal when their canonical values have the same digest. Fuzzy domains such as molecular dynamics are out of v1. - Required checks. A candidate that fails a required check has a low bound of 0. Whoever computes a reading runs each of the policy's
requiredChecksthat accepts a stone's schema on that stone's payload under the check profile, and gives the reading the results; a check witness is never taken on trust. The input is the DAG-CBOR of an inline payload's data. A run that aborts or runs out of gas fails, and so does a stone whose payload isn't inline, since nobody can check it. A stone that fails is hollow, and its candidate's low bound is 0. The stone's refuter is the author of the firstcheckResultwitness on it that names the failing check, by seen epoch and then URI, among witnesses whose sponsor isn't the stone's: refuting your own stone earns nothing. - Retrace budget. Each site gets a fixed number of automatic retraces. New ones are scheduled only when a new distinct value appears, so dissent always counts but cannot force unlimited work. The budget is
retraceBudgetdrawn reads per candidate that schedules retraces. - Dependencies. A child's confidence is capped by its parent's, and a derived stone's by its weakest input. Input graphs must be acyclic.
- Composition. A policy can extend another and override fields.
Judges #
Most fuzzy work can't be checked by a program, but it can be checked by a question. "Find a way to resolve this plot conflict" becomes "Does this resolve the plot conflict?" A policy pins a verifier prompt per schema by hash, so every judge answers the same question. Judges are drawn like retracers, and each verdict is a judgment witness that names the prompt. Verdicts tally like reads, so agreement among drawn judges raises the interval the same way. A judgment is never an anchor, because a prompt is not deterministic.
Models #
A policy lists the model families, versions and thinking levels it counts, and the weight each gets. This does two things:
- Routing. Indexers draw a sponsor only for sites where its declared model counts, so nobody spends tokens on work that earns nothing.
- Weighting. Stones from unlisted models are still recorded, at the policy's default weight, which may be 0.
A disagreement about model quality is settled by forking, not argument. If one maintainer excludes a model and someone else thinks it's good enough, they fork the policy with that model counted. Both forks read the same records, and a policy diff shows exactly which sites the choice changes. Prompts tuned per model family belong in the project's task packets, not the protocol. method.model is self-reported, so a model list coordinates people; it doesn't secure anything.
Equivalence stones #
An equivalence stone claims two sites are the same thing, or different. It is placed and retraced like any other stone. Sites linked by same form a cluster, and duplicate work in a cluster becomes extra evidence instead of waste.
| Problem | The two sites | "Same" means |
|---|---|---|
| Hypothesis dedup (Voynich) | Two hypothesis specs | They make the same predictions |
| Entity resolution | Two donor records | The same person or organization |
| Wire stories | Articles in two newspapers | The same story |
| Duplicate scans | One page scanned by two archives | The same printed page |
- The site is the pair of site keys in canonical order. The payload is
same,differentorunclear. Sites aren't tied to projects, so equivalences can cross projects. - Only declared properties transfer. Each equivalence schema lists what a
sameedge shares. Two scans of one page share the transcription, not the scan quality. - Conflicts generate work, not merges. If A=B and B=C but A≠C, the cluster is contested and pools nothing. Retraces go to the edge with the least support first. A false
sameedge corrects itself the same way. - The first to open a site keeps the opening credit when two sites turn out to be the same.
Credit #
Credit recognizes contributors who build a project in the direction its maintainer set. It is accounting defined by the policy, not a currency. It is computed per project, pinned in each snapshot, and rolled up from agents to their sponsors. Across projects a participant has a profile, not a sum, so credit earned in one project can't buy weight in another.
| Contribution | Credit |
|---|---|
| Stone at an open, contested or prioritized site | Full weight, once its value clears the finalize-credit bar |
| Stone at a site past the stop-routing bar | Little or none |
| Stone others build on | Flow-back from above, capped at five times its own credit |
| Refuting a hollow stone | Full weight |
| Pebble at a genuinely marginal item: the intervals overlapped, or the site ended ambiguous | Half weight |
| A read that was simply wrong where the other reads clearly agreed | Nothing, and no penalty |
| Leading read at a site that ends ambiguous | Full weight. Showing the source itself is unclear is a finding |
| Volunteered retrace | Half weight, matching its weight as evidence |
attempt stone |
A small fixed amount, never coverage credit |
A reading computes credit per apex stone, in parts per million of one full-weight read, and multiplies it by the stone's model weight. A leading read earns once the leading value's low bound clears the finalize-credit bar: full weight if it was drawn or among the site's first reads, the volunteered weight otherwise. A pebble or tied read earns the pebble weight only at a contested or tied site. An attempt earns the attempt weight. A stone that wasn't drawn earns nothing if, on the reads first seen in earlier epochs, the site had already cleared the stop-routing bar. A child stone passes the flow-back share of its own credit to the credited stones at its parent site that read the value it builds on, split equally with any remainder going to the lowest URIs, and each stone receives at most capMultiple times its own credit. Received credit isn't passed on. Credit rolls up to sponsors with each author's share. A refuter earns full weight for each hollow stone it refuted, and it rolls up to the refuter's sponsor like any credit. Refutation credit isn't passed on as flow-back. A hollow stone earns nothing, so its earlier credit is taken back by the next snapshot. At an ambiguous site the leading read earns as if its value had cleared the finalize-credit bar, and pebble and tied reads earn the pebble weight. An excluded stone earns nothing.
- Need scales credit. A stone's own credit is multiplied by its site's need from the policy's priorities, so a site with need 0 earns nothing and a site with need 3 earns triple. Flow-back passes on the scaled credit.
- Credit is retroactive. If a value's low bound falls below the finalize bar, or a null sample fails, later snapshots take the credit back.
- Open-scope projects dedup before crediting. A new site earns placement credit only if no earlier site matches it by spec hash or a
sameedge. Otherwise it counts as a retrace. - Effort is reported, not proven.
method.costis what a contributor says they spent. LLM inference doesn't replay bit for bit, so it is never verified.
Null results and attempts #
A null stone ("searched this mark, found nothing") is a result. It marks ground as covered so nobody searches it again. A null exists only in a course with closed bounds, where the mark is a defined space such as a page region, an exponent range or a parameter grid. It means the region has nothing unless the search was wrong.
In a course with open bounds, such as Voynich hypotheses or a counterexample search, a failure is an attempt. It records the effort and sends the site to someone else. It never marks ground as covered.
Checking a whole null costs as much as producing it, so large nulls are stacked. The author places one stone per segment, then a parent null built from them, with the children pinned in canonical order. After the parent appears in a seen record, the beacon samples one child per epoch. Each clean sample narrows the parent's interval. One failed sample makes the parent contested and reverts that author's other unconfirmed nulls in the same pool.
A null without segments is sampled whole, when the policy sets nullSampling. A pool is one author's apex nulls in one course, in seen order: by seen epoch, then URI. A null is sampled at initialPpm while its pool has no passes, and at sampleNumerator/n after n passes, never above initialPpm or below floorPpm. Only samples decided in an epoch before the null was seen count, and a failure starts the count over. The beacon of the epoch two after the null was seen decides: the null is sampled when the leading u64 of sha256("cairns/null-sample/v1" || 0 || randomness || stoneURI), as a fraction of 2^64, falls below the rate. Until that beacon is among the inputs, the null is awaiting and reads like any stone. A sampled null's sample is the first drawn read at its site from another sponsor, seen no earlier than the null. Reading the same value passes it; any other value fails it, and the null goes hollow. With no such read yet it is pending. A null the beacon skips stands in for its own retraces, the way a vouched conversion does, so its site needs no more reads unless another value competes. It earns credit only once its pool clears: poolClearSamples passes since the pool's last failure. A failed sample reverts the author's skipped nulls in the pool that were seen no later than the epoch the failing read was seen in: they stop standing in and need retraces like any read. The sites blob gives each pooled null its sample (awaiting, unsampled, pending, passed or failed) and poolCleared.
- Computation segments record start and end state hashes, so a sampled segment can be re-run from its published start. Chaining proves the segments connect, not that any state was computed.
- Transcription segments are sentence sites whose marks point at the original scan, so a checker compares against the source, never against the parent's possibly wrong text.
- Most sampling failures are honest disagreement over a marginal item, not fraud.
| Setting | Default |
|---|---|
| Segments per stacked null | As fine as the composer allows, ideally 20 or more, so one check costs at most 5% of the work |
| Credit for a routine segment check | 1/k of the parent's credit for k segments |
| Credit for catching a wrong segment | Full weight |
| Sampling for nulls without segments | 100% at first, falling as 3/n after n passes, floor 10% |
| Clearing a pool for final credit | 60 passing samples, which bounds the false-null rate at 5% |
A contributor who skips 10% of segments in every null is caught with 65% probability across 10 nulls and 96% across 30. Nulls with real certificates, such as Pietrzak proofs for Mersenne, are checked directly.
Trust and bad actors #
Cairns trusts by default and verifies through readings. Every stone is a candidate, and there is no trust list to join. One bad actor can make a site contested, which is a visible "check this" signal, but can't push a value over a bar alone. A label from a labeler the policy trusts zeroes an identity's or a stone's weight, and every later reading recomputes without it.
Maintainers and timekeepers #
Each project has a maintainer, one DID or several, who writes the project and policy records and names the labelers, escalation list and timekeepers. Their authority covers their own reading, never the data. Anyone who disagrees forks: copies the policy, changes it, and keeps every stone.
Several rules need to know when a record existed, and an author can always backdate their own record. So the policy names timekeepers, and cairns foundation names the Cairns timekeeper unless the policy lists its own. A timekeeper dates records, not projects. It publishes a signed seen record covering the rounds after afterRound through a beacon round, with a Merkle root over every Cairns record it first saw in them except seen records, and a leaf document listing them. Its windows close on multiples of the shortest epoch among active projects that name it, a window with nothing new is folded into the next, and previous chains the records. Policies with real stakes can require anchoring seen roots externally, for example with OpenTimestamps.
Labels and blocks #
A policy lists each labeler it trusts and which of that labeler's labels it honors. Cairns defines these label values:
| Label | Applies to | Effect |
|---|---|---|
bad-actor |
An identity, and through sponsor links its agents | Weight 0 in every reading that honors it |
exclude |
One stone, issue or issue comment | Weight 0; hides an issue or comment |
recheck |
A stone, a method or a config hash | Re-sample, like a recall |
unsafe |
A project, policy or procedure version | Clients refuse to fetch or run anything under it; stones that followed an unsafe procedure get weight 0 |
tag:<name> |
A project | A discovery tag from someone other than the maintainer |
A labeler's labels apply in creation order. A negation withdraws the earlier label with the same subject and value, a label without a CID covers every version of its record, and an expired label has no effect. A label counts for a project only when its labeler is in the policy's list with that value among its honored labels; honoring tag:* honors every tag:<name>. A reading applies the labels in force at its seenThrough round's time, so a snapshot pins its label state, and the snapshot's labels field hashes the canonical set it applied. In a reading, bad-actor on a DID excludes every stone it authored or sponsored, exclude on a stone excludes that stone, and unsafe on a procedure version excludes every stone whose method.procedure names it.
Contributors can add labelers of their own. The client applies them on top of the project's, the same way a consumer can apply stricter bars. This matters most for unsafe, because the maintainer picks the project's labelers.
Blocking works in three layers:
- Personal. Anyone can block a sponsor with a public
blockrecord, or locally with--private. The client hides that sponsor and its agents and leaves them out of the user's own reading. The client recomputes that reading itself fromgetInputs, so a private block never reaches the indexer. - Project. A maintainer lists DIDs in the policy's
denylist, and they and their agents get weight 0 in that project's reading. Anyone who thinks the list is wrong forks without it. - Global. A labeler applies
bad-actor, and every policy that honors that label from that labeler drops the identity. Public block records are one signal a labeler can weigh, alongside the signals under Collusion.
A stone authored or sponsored by a DID on the deny list is excluded. A block record never changes a weight on its own. Only a deny list or a label from a labeler the policy names does, so a crowd can't block an honest dissenter out of every reading. A consumer's own blocks, extra labelers and stricter bars form an overlay that changes only that consumer's local reading; a snapshot never applies one. Blocking a sponsor covers all of its agents, including agents linked later. Blocking an agent covers only that agent.
Mistake or malfeasance #
Automatic consequences ignore intent: a failed sample or refutation makes a stone hollow and withdraws its credit, the same for a bug or a lie. Sanctions need a pattern and a person: a label against an identity needs evidence of intent, applied by the maintainer or a trusted labeler.
The common case is an honest contributor with a degraded setup, such as a friend whose agent tweak makes it wrong 10% of the time. Draws and retraces outvote the bad reads, and the credit rules pay nothing for clear misses. To make such setups traceable, method records exact tool and model versions plus a hash of the agent's configuration (system prompt, harness settings, tools). A recall is a witness that flags every stone from one method or configuration, across all authors, for re-sampling. It accuses no one. The configuration hash is self-reported, so it helps honest contributors find their own problems, not catch adversaries.
Evidence that separates mistakes from fraud:
- Reproduction. Re-running the declared method on a failed segment either reproduces the wrong output (faulty method, honest report) or doesn't (misreported method). This works well for computation and poorly for LLM work.
- Clustering. Failures clustered by method across authors point to a bad tool. Failures clustered by author across methods point to a bad actor.
- Selectivity. Honest errors land at random. Fraud concentrates where checking is unlikely.
Collusion #
Cairns does not try to stamp out collusion. That is a bottomless design problem. Draws stop self-chosen confirmers, every stone is attributable, and labelers handle the rest. Signals a labeler can watch for: dense mutual confirmation with few outside confirmations, agreement rates on contested sites far above the base rate, confirmed stones that later fail anchors, and sponsors whose agents only confirm each other.
Residual risks:
- Two colluding sponsors, one of them drawn, can push a value past the export bar before anyone notices. Later readings recover, but an export made in the meantime carries the bad value, and its snapshot shows exactly which stones produced it.
- Moving votes between precincts while keeping county totals exact passes the reconciliation anchor. Only independent retraces catch it.
- Sybil resistance comes down to the cost of a human identity.
Floods #
Anyone can place stones, so an indexer has to survive a flood. A flood also costs honest contributors, because each new distinct value at a site schedules retraces.
- New identities ramp up. An indexer processes each sponsor's stones per project at a rate that starts at 200 per epoch, in the epoch of the sponsor's first stone in the project, and doubles after every epoch in which none of that sponsor's stones is refuted or labeled. An epoch in which a label the policy honors lands on one of the sponsor's stones (
exclude) or on the sponsor or one of its agents (bad-actor) sets the next epoch's rate back to 200. A label lands in the epoch that holds its creation time. An indexer doesn't run checks, so it can't confirm a refutation and doesn't count one. Stones over the rate wait in a queue, oldest seen first, and are never dropped, so an honest newcomer is only delayed. A queued stone is absent from the indexer's inputs, reading, draws and need until it is processed. The queue is computed from each stone's seen round and the labels, so an indexer that restarts rebuilds the same one. Atrustedlabel on a sponsor from a labeler the policy honors fortrustedlifts that sponsor's rate. - Dissent schedules retraces only from established sponsors. A stone's value can make a site contested at once, but it schedules automatic retraces only if its sponsor had existed for one full epoch when the stone was placed. A candidate schedules retraces when at least one of its counted reads was seen at least two epochs after its sponsor's first epoch, so a whole epoch lies between them. This rule is part of the reading, so every indexer applies it the same way.
- Rates affect timing, not results. Queued stones enter the reading once processed, with the seen epoch they arrived in, and a snapshot pins exactly which records it saw.
Maintainer changes #
A maintainer, or someone holding a maintainer's key, can amend a project or policy so that contributors' machines fetch or run something hostile. These are the parts of a project a client acts on:
| Surface | Risk | Defense |
|---|---|---|
| Checks, canonicalizers, composers, transforms, the confidence function | Running hostile code | Only WASM under the check profile: three host imports, no network or files, a gas limit. Clients never run native code a project names. |
| Sources | Exploits in PDF or image parsers; executables passed off as data | Fetch by hash and verify before opening. Each mark type allows a fixed set of MIME types. Size caps. Parse in a sandbox: PDFs in a separate process, other formats only inside the transform interpreter. |
| Procedures, judge prompts and task packet text | Prompt injection against agents that have tools | Packets are data. An agent works under a capability scoped to its drawn site (see Driving Cairns with an LLM). |
| Labelers, escalation, timekeepers, deny list | Capturing trust, such as naming a labeler that excludes critics | Shown in the review diff. The contributor's own labelers still apply. |
Three mechanisms cover these:
- Pinning. Joining a project pins the project and policy CIDs. A new version shows as a diff in two groups: the executable surface (checks, sources, procedures, prompts, packet templates, labelers, import and publish targets) and accounting (thresholds, weights, priorities). Executable changes wait for the contributor's explicit approval, and until then the client doesn't draw for that project. A contributor can opt to accept accounting changes automatically.
- Activation delay. A version whose executable surface differs from the version in effect takes effect
activationDelayepochs after the indexer first sees it, with the delay and epoch length taken from the version in effect. The default is 24 epochs, one day. Until then the indexer routes and reads under the version in effect, which gives labelers time to applyunsafe. Two kinds of change take effect at once: accounting-only changes, includingstatus, and dropping sources from an enumerated scope with nothing else on the executable surface changed. Each version is compared with the version in effect when it arrives, so a change published on top of a pending one waits too, and a version that reverts a pending change takes effect at once and the pending one never does. Versions arrive in the order of their repo's commit revisions, so an indexer that sees a commit twice applies it once, and a version that returns arrives again.activationDelayitself is on the executable surface, so lowering it waits out the old delay. The indexer routes only under the version in effect and the versions still waiting, so a burst of versions can't push the version in effect out.listDrawsnames the version it routed under and any version still waiting, with the round it activates. A client accepts draws under its pinned version or a version it pinned before, so a contributor who pinned the new version keeps working under the old one until it activates, unless the new version is a procedure change. - Early adoption of procedure changes. A procedure change is a waiting version whose executable surface differs from the version in effect only in its courses' procedure references, where each new procedure keeps the
toolsandchecksof the one it replaces and changes only itsbundleandvariants. It leaves sites, draws and routing as they are. The indexer routes under the version in effect, a contributor who approved the change works its sites under the new procedures at once, and everyone else moves when it activates. Readings compare stones by value whichever procedure they followed; when a fix changes the right answer and not just its wording, a policy that requires the new procedure or a recall that targets the old one sorts the stones out. Approving a procedure change before it activates needs a person: the client asks the person through a system dialog or, failing that, its terminal, and refuses when it can reach neither. An agent's shell can open neither for input, so an agent, or text an agent has read such as an issue reply, can start an approval but not complete it. The review shows each procedure's text diff with lint findings: URLs, shell commands, tools the procedure doesn't list, requests for keys or files, and text addressed to the agent. A maintainer'sfoundation --updatecounts as their review. Their own client goes further than early adoption: it works every routed site under the version they published whenever the site's course keeps everything but its procedure and composer, whatever else the version changes. The delay protects contributors from a maintainer, not a maintainer from their own change, and their stones are checked against the version they published. - Evidence is kept. A PDS can rewrite or delete a record, and then proves only its current version. An indexer keeps every executable version it has seen, meaning project, policy, procedure, check and transform records, with the commit proof it fetched when it first saw each, and never lets go of one.
getRecordProofserves the proofs, so a version that ran on someone's agent stays open to inspection after its author rewrites it, and a labeler has time to catch it. A client that needs a transform or check at a version its PDS no longer holds, such as the transform a converted stone names, loads that version from a proof kept on its machine or by the indexer and checks the commit's signature against the author's key.cairnsnever rewrites a transform or check record in place: a new version is a new record. - Labels. A client refuses to fetch or run anything under a project, policy or procedure version labeled
unsafeby any labeler the contributor honors, and a reading that honors the label gives weight 0 to stones that followed anunsafeprocedure.
Contributors who reject a change fork from the pinned version and keep every stone. The delay doesn't help against a hostile source present from the first version, and pinning doesn't help against something a contributor approves. Sandboxing and labels have to cover those.
Identity and keys #
- Identity is the DID, never the handle. Signatures are verified when a record arrives, and the timekeeper's
seenrecord pins that. For did:plc, the public audit log keeps every key change. - A stolen key is handled by marking a window. After recovery, the author publishes a
compromiserecord covering the affected epochs, and their stones in that window count as withdrawn, credit included. - did:web is allowed but weaker, since it keeps no key history. Policies with real stakes can require did:plc.
Deferred to v2 #
Standing: a per-epoch reputation, computed as a reading over a participant's history and used only to decide which stones get checked first.
Snapshots and amendments #
A snapshot pins one reading: the policy, the label state, the check profile, the timekeeper epoch it covers, a Merkle root of its input records, and the results. The root is an RFC 6962 tree over every input record's URI, a zero byte and its binary CID, sorted. The sites, gaps and output blobs are DAG-CBOR, sorted by site key, and credit is sorted by stone URI and sponsor DID, so implementations that agree write the same bytes. output holds each site past the export bar with its base and mark, its leading value, the value's digest, its bounds and the stones that read it, so a render check can place child values under their parents. Downloading "the elections dataset" means taking the latest snapshot's export.
cairns snapshot writes one to the signed-in repo. It takes the latest seen record from a timekeeper the policy names as seenThrough, walks its previous chain back to round 0, and checks each leaf document against its hash, window and root. The reading uses only the indexer's inputs whose URI and CID those leaves list, so a record first seen after seenThrough, or changed since, waits for the next snapshot, and the command lists what it left out. Seen epochs come from the windows too, not from the indexer: a record first listed in window (a, b] was seen in the epoch holding round b, and a sponsor's first epoch is the earliest epoch of any leaf it authored. The first window also covers everything before the timekeeper started and dates it all to its last epoch. The sites, gaps, output and credit blobs and inputRoot come from a local reading. readingImpl is the commit the CLI was built from and the version of the reading rules it implements, and a build with uncommitted changes refuses to publish. The rules version goes up whenever the same inputs can produce a different reading. previous is the author's last snapshot of the project. After publishing, the command writes the snapshot's bundle locally while every input is still at the version it read.
- A snapshot certifies reproducibility, not completeness. It says "these are the records I saw, and this is what they produce." A missed record goes into the next version.
- Snapshots pin influence. Deleting a stone after a snapshot used it doesn't erase it from that snapshot.
- Gaps need an enumerable scope. Elections and Mersenne can list every gap. Open-scope projects such as Voynich hypotheses describe only what has been explored.
- A snapshot bundle replays offline. It is a CAR file whose root is a
cairns/bundle/v1manifest, with a plain-text VERIFY.md beside it. It holds every input record, the snapshot, the project, theseenchain and the source and check records the project uses, each with the signed commit and MST path that prove it; the authors' DID documents; the leaf documents, beacons, label state, check modules and lexicon JSON. Replay runs the required checks from the bundle's modules, so a hollow stone is hollow offline too.cairns bundle verifychecks every block's hash, every commit's signature and MST path, and theseenchain, dates the inputs by its windows, and replays the reading to the snapshot'sinputRootand blobs without contacting a server. A verifier whose rules version differs from the snapshot's checks the proofs andinputRoot, then refuses the replay and names thereadingImplcommit to build. A PDS proves a record only at its current version, but a commit signed earlier still proves the version it held. While a newer project version waits out its activation delay, snapshots pin the routed version the repo no longer holds, so the CLI keeps every proof it fetches,foundation --updatekeeps the proofs of the versions it replaces before writing, and an indexer keeps the proof of every executable version it sees (see Maintainer changes).bundle buildfalls back to a proof kept on its machine, inside a bundle built earlier, or served by the indexer'sgetRecordProof. Inputs otherwise must be unchanged when a bundle is built. - Erasure leaves a tombstone. A contributor who deletes a stone, or their account, withdraws it, as with any deletion. New snapshots and bundles carry a tombstone in its place: the CID and site, with no author or payload. Old snapshot records still name the CID in their input root, so anyone who kept an old bundle can still verify it. HelpPeer stops serving old bundles that contain erased records and removes the contributor's profile. The CC0 dedication isn't revoked for copies others already hold.
Amending is the only way to change a stone, and only its author can amend it, at the same site. To disagree with someone else, place a competing stone.
- Order comes from the amend chain, never from timestamps or record keys.
- Two unamended tips at one site carry no weight until the author amends one over the other. So
cairns placemakes a stone at a site where its author already has one amend that stone, unless the stone names what it amends;cairns config existing-stone refusemakes it stop instead. - Withdrawal is an empty amend. A deleted stone is gone, so deleting an amend makes the stone it amended the author's read again.
cairns deletedeletes the author's whole chain at the site, which withdraws. - Correcting yourself early is free. Amending after a refutation still leaves the hollow stone on your record.
- An amend always withdraws the old value, so an author's dissent disappears as soon as they correct it.
- A drawn retrace can't become a copy. If an amend's new value matches a candidate that was already visible at the site when the amend was made, the stone counts as a volunteered read instead of a drawn one. Any stone whose
amendsnames one of its author's own stones is an amend here, even once that stone is deleted. An amend to a value nobody else has keeps its drawn status. Underblind: truepolicies, values are sealed until the epoch opens, so amends within the epoch keep drawn status.
Publishing to an official repo #
Most projects feed a dataset someone already publishes, such as openelections-data-mi. The project's publish field names that repo, its branch and path, the output format, a render check that turns a snapshot's output into files, and the output license. Data flows both ways.
Out: exports. cairns export renders a snapshot's values past the export bar into files, opens a pull request on the official repo, and publishes an export record naming the snapshot and each file's hash. The pull request carries the snapshot CID, so a reviewer can trace every value to the stones behind it. The official repo's own maintainers decide what to merge. A render check outputs DAG-CBOR {files: [{path, bytes}]} under the shared checks/files.json payload schema. Rendered files go in publish.path, the sidecar, bundle and VERIFY.md in <publish.path>/cairns/, and CITATION.cff at the repo root. One snapshot is one branch, cairns/export/<rkey>, and one pull request, so exporting a snapshot again finds the existing export instead of opening another.
The official repo stays the place consumers download from, in its own format. Each export adds two files next to the data:
cairns.json, the confidence sidecar: the snapshot CID, and for each exported value its site key, low and high bounds, and status. Consumers who care about certainty filter on it, and everyone else ignores it.CITATION.cff, listing as authors the contributors whose finalized credit stands behind exported values, by handle and DID, ordered by credit, with the project's maintainers. This is how contributors get cited outside Cairns.- The snapshot bundle, so the evidence survives even if no indexer is left running. It includes the lexicon JSON and a plain-text
VERIFY.mdthat explains how to check it by hand. Source blobs stay as hashes and URLs, so the bundle stays small. A bundle too large to commit goes up as a release asset, and the export record names it.
When an export corrects values from an earlier one, everyone following the project gets a notification naming the changed sites. The snapshot diff is the changelog.
In: the published state. The official repo is a source too. Each time its branch head moves, a timekeeper publishes a source record naming the repo and commit. Indexers don't read file contents, so a maintainer runs cairns import: it runs the project's import check on the files at that commit, which maps them back to site keys and values, and publishes an importResult record holding each file's sha256 and the site values. Indexers read import results only from the project's maintainers, and the latest one stands for the published state. Then:
- A merge is acceptance. When every file of an export appears in an import result with a matching hash, the indexer marks the export merged at that commit. Anyone can verify this with git. A merge records the publisher's decision, not new evidence, so it doesn't raise confidence.
- Published values are compared, not counted. Each site shows whether the published value matches the leading candidate. A match drops the site's routing need to 0 unless a priority names that site, so nobody redoes published work by default. A mismatch marks the site
divergesand routes retraces to it. Once the leading value clears the export bar, the next export carries the correction as an erratum. Published values never enter a reading's inputs, so tallies are the same with or without them. - The branch head is the current published state. A revert or a later edit changes it. Past exports and merges stay recorded.
The protocol names git URLs only. Opening pull requests and watching merges on GitHub belongs to the client, such as HelpPeer's GitHub app.
Licenses #
| What | License | Where it's stated |
|---|---|---|
| Stones, witnesses and every other Cairns record | CC0-1.0, for the whole protocol | The lexicons. Clients show it before a contributor's first stone, and a sponsor accepts it for its agents through the sponsor record. |
| Sources | The source's own rights | source.license (an SPDX expression or a URL) |
| Checks and procedures | Chosen by the author | check.license, procedure.license |
| A project's output | Chosen by the maintainer | publish.license |
- Every stone is CC0, so forks, mounts and cross-project equivalence stones never run into license conflicts over stones.
- CC0 covers only the contributor's own rights. A transcription still carries the source's rights in the text it reproduces. The output license must fit the source licenses.
cairns foundationchecks the SPDX expressions it knows and asks the maintainer to confirm the rest. - Bundles respect source licenses. A snapshot bundle includes a source blob only if its license allows redistribution. Otherwise the bundle lists its hash and URLs.
- Placing a stone asserts the right to dedicate it. A contributor can't dedicate text they don't hold rights to.
Forks #
A fork is a new project record with forkedFrom and a policy that extends the parent's. Records belong to sites, not projects, so a fork changes how records are read, never which records exist.
- Soft fork: changes only the policy, such as models, weights, thresholds or labelers. Courses and schemas match, so site keys match, and both forks read every stone either side places.
- Hard fork: changes the chunking or a payload schema. Site keys differ, so the forks share sources but no stones, unless a transform, a
schemaAliasor equivalence stones bridge them.
Two forks are compatible on a course when their site keys match there. Beyond that, how much they share is a matter of weights.
What a fork inherits #
- Timekeepers. A fork keeps its parent's timekeepers unless it overrides them, so stones placed before the fork stay dated and credit for need still works.
- Draw parameters. A fork keeps its parent's draw settings unless it overrides them, so a retrace drawn under one fork counts as drawn under the other.
- A pinned parent policy.
extendspins one version. Later changes to the parent reach the fork only when it re-pins.
While split #
Contributors choose which fork's draw to call, and each indexer routes by its own policy. One stone can earn credit in both forks, because the ledgers are separate and profiles aren't summed. A policy diff lists every site the two forks read differently.
Which fork was right can be measured. Both forks read the same records, so an indexer can compute, for each model family or configuration, how often its stones agree with anchors and with drawn retraces. "DeepSeek is good enough here" becomes an agreement rate, not an argument.
Rejoining #
There is no merge. To take the other fork's position, a maintainer amends the project to point at a new policy: its own, revised, or one that extends the other fork's. The next reading includes every qualifying stone ever placed, including those placed for the other fork.
- Credit is retroactive. Newly counted stones earn credit in the next snapshot, with need weighted by when they were placed. A policy can set a start date instead, but the default counts every stone.
- Past snapshots stand. Each records its policy CID, so the snapshot chain shows where the policy changed and what that changed.
- Downstream projects move when they choose, because mounts pin snapshots.
- Adopting a fork wholesale means setting
status: archivedandsucceededByto the other project. Clients send contributors there, and downstream maintainers see the pointer when they next upgrade a mount.
Hard forks don't rejoin on their own. One side adopts the other's structure, or bridges it with a transform.
Dormant projects #
A project is dormant when work only its maintainers can do has gone unanswered for the policy's dormancy period (default 30 days). Being quiet doesn't count. A finished project with nothing waiting never goes dormant, and no maintainer has to check in. Maintainer-only work is:
- an escalated site with no witness from a maintainer or the escalation list;
- an issue with no maintainer comment or
issueState; - a gold commitment past its reveal deadline;
- values past the export bar with no new snapshot, when the policy sets
snapshotCadence; - a project or policy version labeled
unsafeby a labeler the project names, with no new version.
Dormancy is derived, so any indexer computes the same answer. Maintainers get a notification as each item ages. While a project is dormant, clients show its most active soft fork alongside it: the one whose maintainers aren't dormant and that published the most snapshots in the period. Stones keep counting in both, because soft forks share sites. Nobody takes over the project record, so dormancy can't be used to hijack a project. A maintainer who answers the pending work ends dormancy.
Project lifecycle #
This walkthrough follows the OpenElections Michigan 2026 primary.
- Lay the foundation.
cairns foundationpublishes a source record per county PDF; a project record with four courses (slice pages into table regions, transcribe each region into a raw grid, normalize rows with a deterministic transform, derive county totals); a policy that includes a reconciliation check against the Secretary of State's certified figures; payload schemas; and a timekeeper. - Start the indexer. It reads
org.cairnsproto.*from Jetstream and publishes aseenrecord every epoch. The first snapshot is all gaps. - Climb the courses. An agent calls
draw, gets a packet such as "slice page 4 of the Kalkaska PDF," and places a stone. When a drawn retrace agrees, the page's table regions open as course-1 sites. Disagreements trigger retraces. A truly unreadable cell can end ambiguous. - Derive and check. Clients re-run the course-2 transform, so normalized rows need no retraces. Course-3 totals are compared with the certified figures, and a mismatch sends retraces to the rows beneath it.
- Handle events. A reissued PDF is a new source, and equivalence stones carry over its unchanged pages. A labeler's flag removes a bad actor's weight. A recall re-samples a faulty method's stones.
- Publish snapshots on the maintainer's cadence. Each chains to the one before, so
diffshows progress. - Export finished parts. When a county has no gaps, its CSV is exported from a snapshot as a pull request to
openelections-data-mi. - Mark the project complete. The maintainer sets
status: complete. Indexers stop routing work but keep accepting stones, and a late stone can still reopen a site.archivedmeans the maintainer stops publishing snapshots. Anyone can fork at any point.
Every snapshot lists its gaps, and those gaps become the next round of draws.
Contributors and clients #
Most of a contributor's experience is client behavior built on the indexer queries under Record definitions. The protocol adds the follow, block and issue records, project tags, and a standard query API, so any client works against any indexer.
Joining a project #
cairns join stores a project in the client's local config and pins its project and policy CIDs (see Maintainer changes). It also follows the project from the signed-in account, unless asked not to, because an indexer dates a sponsor by its first record: an account that has published nothing is never eligible to be drawn. The follow also subscribes the contributor to the project's exports. Leaving a project keeps the follow. A contributor's current projects are the ones they joined plus any project where they or their agents have an apex stone.
cairns foundation and foundation --update join the maintainer to the version they publish: writing it counts as reviewing it, so the pin moves to it and the project is followed.
Running cairns join again on a joined project shows how the current version differs from the pinned one, field by field. Accounting is a fixed list: the project's name, description, tags and status, and the policy's thresholds, weights, priorities, draw and credit settings, models, retrace budget, null sampling and blind. Every other field is on the executable surface, including fields a later lexicon adds. --approve <cid> pins the version the review showed and is refused if the project has moved on since. Approving a procedure change that hasn't activated asks the person through a system dialog or the terminal and moves the client to it at once. --accept-accounting re-pins accounting-only versions without review. Until a version is approved, the client takes draws only while the indexer still routes under a version the contributor pinned, and fetch stays on the pinned version's sources. Before pinning, drawing or fetching, the client asks each honored labeler for unsafe labels on the project, its policy and the policy that one extends. The honored labelers are the contributor's own, added with --labeler, and those the pinned or current policy honors unsafe from. A labeler that can't be reached fails the check. An open scope doesn't list its sources, so a source added to one doesn't change the project's CID and isn't caught by pinning.
Seeing your draws #
cairns draw --list shows, for each current project, the sites the contributor's sponsor holds a deliverable draw for, the sites whose threshold has escalated until anyone may retrace, and how much of each draw's window is left. A draw stays deliverable through its window, so a site is listed when the beacon of the current epoch, or of an earlier one whose window is still open, draws the sponsor; the newest such epoch is listed, since it leaves the most time. Epochs before the site's first stone are not listed. The client passes its agent's declared model and thinking level, and the indexer lists only sites where that model counts. Every draw is hash(beacon || siteKey || sponsorDID) < T over public inputs, so the client recomputes each one rather than trusting the indexer. For each project it also says when the sponsor can first be drawn: the start of the epoch two after the one its first record was seen in, from the indexer's first-seen round, or estimated from now for a sponsor with no record yet. cairns join reports the same after following the project.
The client checks each listed draw against the version of the project it acts on: the indexer must have read that version, the beacon's signature must verify, its round must be its epoch's, and that epoch's window must still be open, the threshold must be one escalation can reach from the policy's start, and the draw hash must fall under it. A draw that fails is dropped and reported. Only the indexer knows the sponsor's first-seen epoch and the site's stones, so eligibility and how far a threshold has escalated come from it. A project waiting for review, labeled unsafe, or whose policy gives the declared model no weight lists nothing, and the output says why.
--cached keeps only draws whose sources are already in the local content store. The indexer returns the source hashes each draw needs, including the course-0 sources under a higher-course site, and the client checks them against the store without network access.
Finding new projects #
cairns projects lists active projects. Filters cover tags, whether the contributor's model counts, and labels from the contributor's labelers. Given a model, the indexer leaves out each project whose policy gives that model no weight. Projects rank by need, highest first. Projects where followed sponsors work can be listed separately. The indexer hides a project its policy's labelers mark unsafe, and the client also hides one marked by the contributor's own labelers.
Maintainers set up to 10 tags on the project record, lowercase and kebab-case, such as elections or ocr. Labelers can add tag:<name> labels, which the indexer reports apart from the maintainer's own tags, each with the labeler that added it. A tag filter matches either kind. Model and source requirements come from the policy and courses, so they don't need tags.
Organizations #
An organization, such as a newsroom, a class or a lab, is its own DID. It is linked to each member by a member record in the org's repo and a memberOf record in the member's, and both halves are needed. A sponsor's agents belong to the org through their sponsor. An org can be a project maintainer. Credit and badges roll up from members to the org per project, the same way they roll up from agents to sponsors, and HelpPeer gives each org a page. Membership changes independence only under a policy with mergeOrgs: true (see Draws).
Following #
A follow record follows a sponsor, and through sponsor links all of its agents. A client uses follows to show the sponsor's recent stones, projects and credit, and for discovery. A follow can also name a project, which is how data consumers subscribe to its exports and corrections. Follows never affect draws, weights or credit, because a follow that raised weight would be a ready-made collusion channel.
Issues #
An issue is a report or question about a project, a policy version, a site or a stone: a wrong source, a schema bug, a policy disagreement. Anyone can comment. Only a maintainer's issueState record opens or closes an issue; the latest by createdAt stands. An issue never changes confidence. To dispute a value, place a stone; to resolve an escalation, a maintainer or escalation member writes a witness. Clients show an agent's issues under its sponsor. Indexers leave out issues and comments carrying an exclude label the policy honors, which hides spam.
Cost and budgets #
method.cost is tokens (input, output, cached) and wall-clock seconds. These are facts the harness reports, comparable across contributors on the same model. They're still self-reported.
- Cost per site. For each course and model family, the indexer reports median cost per site, and from the open sites, an estimate of what finishing the course would take. A contributor sees the cost before drawing, and a maintainer sees what a cheaper model would save.
- Budgets are local.
cairns run --budgetcaps spending per day or per run, with shares across projects such aselections:3,voynich:1. The CLI converts tokens to dollars or to a share of a usage plan using the contributor's own rates, and stops drawing when the budget runs out. Prices and plans never enter a record.
Notifications #
Every event worth telling a contributor about can be derived from records and readings, so notifications are a standard indexer query, listNotifications, not a record type. Any indexer can serve them. The CLI reads them with cairns inbox, agents poll the same query, and HelpPeer adds an inbox and email. Read state stays with the client.
| Event | Sent to |
|---|---|
| Your stone's site became contested, or your stone went hollow or lost the lead | The stone's author and its sponsor |
| A site you placed at diverges from the official repo | Every author with an apex stone there |
| A draw window is about to close | The drawn sponsor |
| A project you joined has a version waiting for review, or its activation delay ends | Contributors who joined it |
| An issue you opened or commented on has a reply or a new state | Its participants |
| Maintainer-only work is aging toward the dormancy period | The project's maintainers |
| A sponsor you follow placed stones in a new project | Followers |
| A project you follow exported, or an export corrected earlier values | The project's followers |
A contested notification doesn't include the competing values. It offers cairns recheck, which re-sends the site's task packet without existing values, so an honest agent looks at the source again without being anchored. An amend made after the agent has seen the competing values follows the rules under Snapshots and amendments.
Badges #
Indexers derive badges from signed snapshots. There is no award record. An indexer computes a project's badges from the latest snapshot a maintainer of the project published. Each badge names the rule that grants it, the snapshot it was computed from, and the stone CIDs that earned it, so anyone with the snapshot can check it. Badges count only credit that has cleared the finalize-credit bar: the own and flow-back credit in the credit blob of stones whose status in the sites blob is leading. Like credit, they can disappear in a later snapshot. They roll up from agents to their sponsors.
The indexer serves each badge as JSON from getBadges and as an SVG with the snapshot's date, for HelpPeer profiles and READMEs. Rule names are versioned, so changing a rule never silently changes an existing badge.
Driving Cairns with an LLM #
Every command has JSON output, and cairns mcp serves the same commands as MCP tools, so an agent can find projects, list draws, work them and file issues.
cairns task <site> writes a site's task packet to a work directory, by default under $CAIRNS_HOME/work so packets stay out of the directory the agent runs in: the course's procedure files, the payload schema, links to the sources the site's base chain starts from, the canonical parent value for a higher course, and a stone.json with the site and procedure filled in and the payload left empty. Every record is taken at the project version the client acts on, and every site and parent value the indexer points to is checked against its hash. The packet never includes values already read at the site. When the sponsor is drawn there, the packet gives the beacon round to place with; otherwise the read counts as volunteered. Running it again refreshes the packet and keeps the agent's stone.json.
cairns next queues the sites a sponsor should work across its current projects and writes the first one's packet. Drawn sites come first, the soonest-closing retrace window first, because drawn reads are what raise a value's low bound. Open sites follow, ordered by sha256("cairns-next-v1" || 0 || sponsorDID || 0 || siteKey), so each sponsor walks the open sites in its own stable order and sponsors working at the same time rarely start the same site. A site listed by two projects appears once. The ordering is part of cairns-core, so every client hands a sponsor the same next site. A site the account already has a stone at is left out, checked against the account's own repo, since an indexer may ignore a stone such as a split stone in a course the version in effect gives no composer. A site the sponsor or its account already reported in an open issue is left out and listed apart until the issue closes, so an agent doesn't start each run on the same blocked site; other people's open issues are shown on the site's entry.
Two rules keep the agent contained:
- An agent working a draw holds a capability for that one site. It can fetch that site's sources and place or amend stones there, and nothing else. Source contents, task packets, judge prompts and issue text are untrusted data, and nothing in them can widen the capability.
- Actions that change trust or what runs need a person. Joining, approving a new pinned version, blocking, changing labelers,
foundationand policy amends are proposals over MCP. The human applies them withcairns apply.
Record definitions #
The envelopes live under org.cairnsproto, resolved through a DNS TXT record at _lexicon.cairnsproto.org. Development uses org.cairnsproto.draft, resolved through _lexicon.draft.cairnsproto.org, and may be discarded. The permanent lexicons are published once, at the v1 freeze. A payload schema is not an NSID. It is a schema record holding lexicon-format defs, named by sha256("cairns/schema/v1" || 0x00 || DAG-CBOR(defs)), and a stone names its schema by that hash. Publishing one is writing a record, so a maintainer needs no domain, and the same definition published from any repo is the same schema. Its main def is an object and every ref in it is local, so nothing outside the record can change what it means. A reading checks each found inline payload against the schema record among its inputs whose defs hash to the stone's schema, and rejects a stone whose payload fails, so it counts for nothing. An indexer's inputs include the schema records the project's courses pin, at the pinned versions; a site whose schema record is not among the inputs is read unvalidated. The lexicon JSON in /lexicons is authoritative; the sketch below summarizes it. Lexicons have no floating-point type, so every fraction (thresholds, weights, the draw threshold) is an integer in parts per million, and every record carries createdAt. A CID written as text, as a cid string or a $link, is its canonical spelling: base32 lowercase for CIDv1. Records compare CIDs as strings, so validators refuse every other spelling of the same CID. A DAG-CBOR decoder refuses bytes that are not the canonical encoding of the value they hold, so one value has one encoding and one CID.
org.cairnsproto.source // anyone may publish; records with the same hash name the same source
hash: { alg: "sha256", value }
mimeType, size
urls: [string] // where to fetch; the hash is the identity
derivedFrom?: { hash, transform: strongRef transform }
license?: string // SPDX expression or URL; governs redistribution in bundles
git?: { repo, commit } // a published state of an official repo; hash alg is "git-sha1" or "git-sha256"
org.cairnsproto.stone
site: { base: source hash | { parent: site key, value: digest } | [site key, site key], mark?: Mark, schema: schema hash }
// equivalence stones use a sorted pair and no mark; their schema lists the properties that transfer
payload: { data: object } | { blob: blob } | { hash, urls } | { sealed: blob, round }
// validated against site.schema; sealed = timelock-encrypted under blind policies
result: "found" | "null" | "attempt" | "split" | "converted" // null only in closed-bound courses; attempt never counts as coverage; split only in a course with a composer; converted only on a source
evidence?: { blob | hash, urls }
state?: { startHash, endHash, urls } // computation segments
inputs?: [strongRef stone] // derived stones pin exact inputs; acyclic
amends?: strongRef stone // same author and same site only
method: { tool, toolVersion, model?, modelVersion?, thinking?, config?: hash, procedure?: strongRef procedure,
agent?: string, env?, cost? } // agent: label for an agent writing to its sponsor's repo
// versions exact; config hashes the agent setup; config and cost are self-reported
// cost: { inputTokens, outputTokens, cachedTokens?, seconds }
// payload, result and method are optional only so a withdrawal (amends, nothing else) is valid
draw?: { beaconRound, proof? } // the epoch whose beacon drew this retrace; proof under secretDraws
org.cairnsproto.witness
stone: strongRef stone
evidence: checkResult | judgment | recall // a union, so the kind is the member's $type
checkResult: { check: strongRef check, output: canonical bytes hash }
judgment: { verdict: "supports" | "refutes" | "unclear", rationale?, prompt?: hash }
recall: { method: { tool, toolVersion, model?, modelVersion?, config? }, evidence: strongRef stone[] }
amends?: strongRef witness
org.cairnsproto.check
module: blob (WASM)
profile: { version, instrumenter } // Cairns check profile
gasLimit: integer
accepts?: [schema hash] // absent when the input isn't a payload, as for the transform interpreter
output?: schema hash // absent when another record names it, as a transform does
proves: string // the exact claim a passing result supports
source?: { repo, commit } // the git commit the module was built from
license: string // SPDX expression
org.cairnsproto.procedure
bundle: { files: [{ path, text }] } | zip blob | { hash, urls } // Agent Skills format: SKILL.md, checklist,
// examples, references; no executables. Text bundles live in the record itself.
tools: [string] // client-provided tools, e.g. "source.region", "pdf.render", "check.run"
checks?: [strongRef check] // self-checks the agent may run before placing
variants?: [{ family, bundle }] // per model family
license: string
org.cairnsproto.transform // anyone may publish
interpreter: strongRef check // the interpreter check Cairns ships, at the version the script was written for
script: string // Starlark; no load() of other records
accepts: [mimeType]
output: schema hash
name, description?
tags?: [string] // layouts it handles, for discovery, such as "enhanced-voting-xlsx"
license: string
org.cairnsproto.policy
extends?: strongRef policy
canonicalizers: [{ schema: schema hash, using: { check: strongRef check } | { steps: [builtin] } }]
// builtins: identity, text, integerStrings, lowercase; unlisted schemas use identity
requiredChecks, retraceBudget, nullSampling
priorities: [{ course | site selector, need }]
draw: { epochRounds, startThresholdPpm, windowEpochs, volunteeredWeightPpm, drawnToRaise }
credit: { pebbleWeightPpm, attemptWeightPpm, volunteeredWeightPpm, flowBack: { sharePpm, capMultiple }, dedup? }
confidence?: strongRef check // maps tallies to [low, high] per candidate; absent = built-in Beta-share interval
thresholds: { openChildrenPpm, finalizeCreditPpm, exportPpm, stopRoutingPpm, minFamilies? }
// every numeric field has a lexicon default matching the tables above
blind?: boolean
mergeOrgs?: boolean // members of one org count as one sponsor; default false
secretDraws?: boolean // VRF draws from each sponsor's drawKey; default false
dormancy?: integer // days maintainer-only work may wait; default 30
snapshotCadence?: integer // days; with it, export-ready values waiting longer count as pending work
judges?: [{ schema: schema hash, prompt: blob hash, minJudges }] // verdicts tally like reads, never anchors
models?: { accept: [{ family, versions?, thinking?, weight }], defaultWeight, sameSetupWeightPpm? }
calibration?: [{ goldPasses: integer, weight }] // per-setup weight curve; absent = scores inform only
labelers: [{ did, labels: [label value] }] // only the listed labels are honored, with their standard effects
deny?: [did] // weight 0 in this reading, including agents through sponsor links
activationDelay: integer // epochs after first `seen` before executable-surface additions take effect
escalation, timekeepers: [did]
org.cairnsproto.project
maintainers: [did]
status: "active" | "complete" | "archived"
name?, description?
scope: { sources: [hash], sites?: external site-key list } | open
courses: [{ id, parent?, markType, schema: schema hash, schemaRecord: strongRef schema, compose?: builtinComposer { name: "concat" } | checkComposer { check: strongRef },
minMark?: Mark, bounds: "closed" | "open", procedure?: strongRef procedure }]
publish?: { repo, branch, path, format, render: strongRef check, import?: strongRef check, license }
mounts?: [{ project: strongRef project, snapshot: strongRef snapshot, course }]
outputSchema: schema hash
tags?: [string] // at most 10, lowercase kebab-case, for discovery
transforms: [strongRef transform] // vouched for: a converted stone naming one needs no reads
policy: strongRef policy
forkedFrom?: strongRef project
succeededBy?: strongRef project // set with archived when adopting another fork
org.cairnsproto.gold // maintainer only, one per epoch: { project, beaconRound, root }
org.cairnsproto.goldReveal // { gold: strongRef gold, entries: [{ siteKey, value: digest, salt }] }
org.cairnsproto.seen // one per timekeeper window, across every project
afterRound?, beaconRound, firstSeenRoot, firstSeen?: external leaves, previous?, externalAnchor?: { system, reference }
org.cairnsproto.snapshot
project, previous?, policy, labels?: hash of the applied label set, profile?, seenThrough, inputRoot
sites: blob // tallies and [low, high] per candidate, apex stone CIDs
credit: blob
output: blob // values past the export bar
gaps: blob
readingImpl: { repo, commit, rules } // rules absent means 1
org.cairnsproto.sponsor // in the human's repo: { agent: did, since }
org.cairnsproto.sponsoredBy // in the agent's repo: { sponsor: did, since }
org.cairnsproto.member // in the org's repo: { member: did, since }
org.cairnsproto.memberOf // in the member's repo: { org: did, since }
org.cairnsproto.drawKey // in the sponsor's repo: { publicKey, alg: "ecvrf-edwards25519-sha512-tai" }; effective one epoch later
org.cairnsproto.compromise // { fromSeen, toSeen }
org.cairnsproto.schema // { name?, description?, defs }; identity is the hash of defs alone
org.cairnsproto.schemaAlias // { from: schema hash, to: schema hash }; a reading applies only its policy author's,
// one-to-one, acyclic, meaning unchanged; stones keep their original hash
org.cairnsproto.export
project: strongRef project
snapshot: strongRef snapshot
target: { repo, branch, path }
files: [{ path, sha256 }]
pr?: string // URL; merge status is derived from the repo, not stored here
org.cairnsproto.importResult // counts only from a project maintainer
project: strongRef project
source: strongRef source // the branch-head commit
check: strongRef check // publish.import at the version that ran
files: [{ path, sha256 }]
values: blob // DAG-CBOR { version, values: [{ site: site key, digest }] }
org.cairnsproto.follow // { subject: did | strongRef project, createdAt }; never affects weight
org.cairnsproto.block // { subject: did, reason?, evidence?: [strongRef stone], createdAt }; never affects weight on its own
org.cairnsproto.issue
subject: strongRef project | strongRef policy | strongRef stone | { project: strongRef project, site: site key }
kind?: "source" | "schema" | "policy" | "bug" | "other"
title, body, createdAt
org.cairnsproto.issueComment // { issue: strongRef issue, replyTo?: strongRef issueComment, body, createdAt }
org.cairnsproto.issueState // { issue: strongRef issue, state: "open" | "closed", reason? }; counts only from a project maintainer
- A retrace has no record type of its own. It is a stone at the same site.
- References pin versions. Every reference is a strongRef (URI plus CID), so editing a stone can't change what a witness vouched for.
- The schema is part of the site, so stones that compete are always comparable.
- A site key is
sha256("cairns/site/v1" || 0x00 || DAG-CBOR(site)), after resolving the site's schema through schema aliases. Every hash in the site must be lowercase hex sha256, and an equivalence pair must hold two distinct keys in ascending order, so a site has exactly one key. A canonical value's digest is the same construction with the tagcairns/value/v1. - Checks run under a pinned profile, not an engine. Profile
cairns-check/1is core WebAssembly with mutable globals, sign extension, multi-value, bulk memory and reference types, and without floating point, SIMD or threads. A module has one memory of at most 1024 pages, no start function, and exportsmemoryandrun. It may import onlycairns.read(ptr, cap) -> len, which copies up tocapinput bytes and returns the input's length;cairns.write(ptr, len), which appends to the output; andcairns.abort(ptr, len), which fails with a message. A check record holds the module as built. Every runner applies the profile's metering pass before running it, rather than trusting metering a publisher added. The pass charges each straight-line run of instructions its length before it starts, one gas per byte for bulk memory instructions and host copies, and traps when gas runs out or calls nest deeper than 512. Gas is a property of the module and input, not the engine. Profilecairns-check/2is the same with floating point and saturating float-to-int conversions allowed. Its pass also follows every instruction whose NaN result can differ between CPUs (add,sub,mul,div,sqrt,min,max,ceil,floor,trunc,nearest,promoteanddemote) with a select that replaces a NaN with the canonical quiet NaN (0x7fc00000,0x7ff8000000000000), at no gas. Every other float instruction is exactly rounded or only moves bits, so a float result is the same bits on every CPU. A check record names the first profile its module fits. Indexers are content-blind: they never fetch source bytes or run checks, transforms or imports. Clients run them and publish what they find as stones, witnesses and import results. - Large payloads live off the PDS, as a hash plus fetch URLs. Storage is out of scope: any copy matching the hash is valid.
- Indexers flag unreachable sources. A source with no URL still serving its hash can't be retraced, so indexers report it to the maintainer, who can add a mirror to a new
sourcerecord with the same hash. - Envelopes change slowly. Published lexicons can only add optional fields, so required fields are kept to a minimum.
Indexer queries #
Indexers serve a standard set of XRPC queries, so the CLI and HelpPeer work against any indexer. Where an answer can be recomputed from public inputs, the response carries those inputs, and clients check them instead of trusting the indexer.
| Query | Returns | Client can verify |
|---|---|---|
org.cairnsproto.listDraws |
Drawn and escalated sites for a sponsor across projects, filtered by model and optionally to one project or site, each with its beacon round and needed source hashes, and separately the course-0 sites nobody has opened | Every draw, from the beacon |
org.cairnsproto.listProjects |
Active projects by tag, accepted model and need, with median cost per site per course and model family and an estimated cost to finish | Nothing; ranking and estimates are advisory |
org.cairnsproto.getReading |
Tallies and intervals for sites under a policy version | By recomputing from the listed records |
org.cairnsproto.getInputs |
A reading's input records with their values, its beacons, and each author's first-seen epoch | Each record against its CID and signature |
org.cairnsproto.getProfile |
A participant's credit per project and their badges, each pinned to a snapshot | Against the snapshot |
org.cairnsproto.getBadges |
A participant's badges, each with its rule, snapshot and earning stones | By recomputing from the snapshot's credit and sites blobs |
org.cairnsproto.getRecordProof |
The commit proof of a project, policy, procedure or check record at a version the indexer saw, kept for good | The commit's signature and MST path |
org.cairnsproto.listIssues |
Issues for a subject, with comments and maintainer state | Each record's signature |
org.cairnsproto.listNotifications |
Events for a DID since a cursor, each pointing at the records behind it | By fetching those records |
org.cairnsproto.listCalibration |
Each setup's gold passes and failures in a project, and flagged gold entries | Against the revealed entries and their roots |
Typed marks #
A mark is a typed union. Marks nest through refinedBy (a zip member, then a page in that PDF, then a box on the page), borrowed from the W3C Web Annotation model. Each mark type is a def in the mark lexicon, so pdf.region is org.cairnsproto.draft.mark#pdfRegion. Ranges include their start and exclude their end.
| Source | Mark type | Fields |
|---|---|---|
pdf.region |
page, bbox? in hundredths of a PDF point | |
| Image | image.region |
frame?, bbox in pixels |
| Spreadsheet | sheet.range |
sheet name, A1 range |
| CSV, TSV | table.range |
header rows, row range, columns |
| HTML snapshot | text.quote or css.selector |
quote with prefix and suffix, or a selector |
| Plain text, code | text.position |
line range or character offsets |
| Git tree | git.path |
commit, path |
| JSON | json.pointer |
RFC 6901 pointer |
| Archive | archive.member |
member path, then refinedBy |
| Audio, video | media.time |
start, end in milliseconds |
| Search space | space.range |
space hash, coordinates or interval |
Sites match on exact equality of base, mark and schema. Marks point at the original source, never a rendering of it. A mark never contains the answer, so a retrace can read the same region without being handed the existing value. Projects can define new mark types as lexicons.
Workloads #
Cairns fits best where checking is cheaper than doing. It strains on fuzzy agreement, very large outputs, graph-shaped answers, nulls without certificates, and anything that can't be public.
It doesn't fit writing code. Independent agents rarely produce the same code, so agreement carries little evidence, and neither a payload schema nor a check can tell a correct change from one with hidden behavior. The only real gate is the target repo's own review, so code is out of scope.
| Workload | Stone payload | Best evidence | Where it strains |
|---|---|---|---|
| Election precinct extraction | Raw grid, then normalized rows | Reconciliation with certified totals | Votes moved between precincts keep totals exact; only retraces catch it |
| Mersenne prime search | Result, residue hash, proof certificate | Pietrzak proof check | Trial-factoring nulls have no certificate. GIMPS already coordinates this, so it is a test case |
| Voynich transcription | Transliterated text | Blind retraces | No totals to reconcile. Ambiguous glyphs stay ambiguous |
| Voynich hypotheses | Test results; nulls for refuted families | Re-running tests; held-out prediction | Scope is open. Whether decoded text means anything is a judgment |
| Counterexample search | Counterexample, or null for a range | Evaluating the counterexample | Nulls without certificates need full retraces |
| Formal math (Lean) | Proof or lemma graph | Lean kernel check | Whether the Lean statement matches the English problem |
| Bug reproduction | Report with a reproducer | Running the reproducer | "Audited, found nothing" is a weak null; security bugs can't be public |
| Citation verification | Supported or not | Retraced judgment | No anchor; paywalled sources |
| Entity resolution | Same or different | Retraces, external IDs | Answers form a graph; equivalence stones handle it |
| Molecular dynamics | Trajectory hash, statistics | Statistical agreement | Needs tolerances, which v1 excludes |
Voynich shows how cranks become useful. A claimed decipherment is a candidate at its own hypothesis site. Retraces re-run its tests and apply its key to folios it wasn't derived from. Whoever refutes it earns credit, the stone goes hollow, and the refuted family becomes a null nobody has to search again. Competing hypotheses are separate sites, so several can survive until the evidence separates them.
Why not git #
Cairns could be emulated in a shared git repo, with stones as append-only files and CI computing the reading. That brings back write access or a pull-request gate. ATProto gives each participant a signed repo of their own and a public stream any indexer can read. Git also resolves conflicts by keeping one value, while Cairns keeps every competing stone as evidence that can flip the reading later. Git still has two roles: check and transform programs live in git and are referenced by commit, and exports land in git.
Normative spec work #
Each guarantee the implementation is tested against carries an anchor, such as draw.hash. A test that checks one cites it with a // claim: draw.hash comment, and crates/cairns-testkit/uncited-claims.txt lists the claims no test cites yet.
These need exact rules and test vectors rather than more design:
- Deterministic readings: numeric representation for tallies and intervals, rule precedence, and tie-breaking, so independent implementations produce byte-identical snapshots.
- Amend chains and sponsorship history: transition rules for sponsor links that start, end or change while an agent's stones are in flight.
- Snapshot bundle format: CAR layout, manifest schema and verification steps.
- Cairns check profile: the normative WASM profile and host imports, with cross-engine test vectors.
- Transform interpreter: the Starlark dialect, the decoder builtins and their output shapes, and the limits on input size.
Test vectors should cover branched amendments and sponsorship changes; draws involving identities created before the beacon round was known; a child bound to a parent value that later loses; committed sampling, including a disputed seen epoch; offline replay of a bundle; conflicting equivalence edges, schema aliases and anchors; and blind payloads that fail to decrypt or decrypt to an invalid value.