Concrete example: a publisher opt-out, end to end
Walk one specific scenario from submission to lab-side acknowledgment, demonstrating to a technical evaluator exactly what code paths run, what artifacts each step produces, and what's verifiable independently.
The scenario
example-publisher.com publishes long-form journalism. They want to register a domain-wide opt-out: "do not train any model on content fetched from this domain." They want the opt-out to be:
- Verifiable independently by any AI lab without trusting Akaeon.
- Provably timestamped — they need to point at a record predating a training cutoff.
- Auditable in both directions — the publisher wants proof their request was honored; the lab wants proof they checked the registry at training time.
A compliance engineer at examplelabs.ai will call the registry's lookup API during their data-ingestion pipeline. They need an answer they can put in their audit log that survives later challenge.
This document walks every step from publisher submission to lab acknowledgment. Everything below is shipped and running in production at api.akaeon.com; the file paths point at the code that serves each step.
Step 1 — Publisher submits the opt-out
The publisher's automation hits:
POST https://api.akaeon.com/v1/optouts
Authorization: Bearer akr_<publisher-api-key>
Content-Type: application/json
{
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z"
}
The handler at services/akaeon-registry/src/routes/optouts.ts does four things in order:
-
Validates the request shape. Rejects unknown
policyvalues or malformed domains. -
Issues a DNS challenge token — a random 32-byte nonce, base64-encoded, stored against the pending submission with a 24h TTL.
-
Returns
202 Acceptedwith the challenge:{ "ok": true, "submission_id": "01J9XW...", "status": "pending_dns_verification", "domain": "example-publisher.com", "policy": "no-training", "scope": "domain", "effective_from": "2026-05-11T00:00:00Z", "submitted_at": "2026-05-11T14:23:00Z", "dns_challenge": { "host": "_akaeon-registry-challenge.example-publisher.com", "txt_record": "akaeon-registry-v1=<32-byte-nonce-b64>", "expires_at": "2026-05-12T14:23:00Z", "ttl_hours": 24 }, "instructions": "<human-readable steps for adding the TXT record>" } -
Persists the row in the
optout_submissionstable. Nothing is signed, nothing is anchored. The submission is provisional until DNS verifies.
Where this lives architecturally: top band of the architecture diagram — the registry's API surface, peer to Stelais's API surface.
Step 2 — DNS challenge verification
The publisher's ops team adds the TXT record at their DNS provider.
A polling worker in services/akaeon-registry/src/services/dnsVerifyService.ts sweeps pending submissions every 60 seconds (REGISTRY_DNS_VERIFY_INTERVAL_MS, deliberately independent of the batch scheduler). For each pending row:
- Resolves the
_akaeon-registry-challenge.<domain>TXT record against three resolvers — Cloudflare1.1.1.1, Google8.8.8.8, and Quad99.9.9.9— to defeat single-resolver poisoning. - Requires unanimous, byte-for-byte agreement: every resolver must return a record matching the expected value.
- On match: marks the submission
dns_verifiedand leaves it queued for the next batch. - On no-match or partial-match: leaves the submission pending. On TTL expiry: marks the row
expired— it is kept, never deleted, so the submission history stays complete.
Why DNS and not OAuth / certificate-based auth? DNS control is the existing, durable proof-of-domain-authority that every publisher already operates. OAuth ties the registry to a centralized identity provider; cert auth requires the publisher to manage a separate keypair the DNS-based flow deliberately avoids. DNS gets the job done with zero new infrastructure on the publisher side and is the same primitive Let's Encrypt, Google Search Console, and ACME challenge flows already rely on.
Step 3 — Build the canonical opt-out record
Once dns_verified, the registry constructs the canonical record. This step is the first place the registry touches the brand-neutral core packages that Stelais uses in production.
The opt-out canonical payload is built by buildOptoutCanonicalPayload in core/arweave/src/canonicalPayload.ts, alongside the pre-existing buildCanonicalPayload and buildSnapshotCanonicalPayload:
{
"version": 1,
"type": "domain_optout",
"submission_id": "01J9XW...",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z",
"submitted_at": "2026-05-11T14:23:00Z",
"dns_verified_at": "2026-05-11T14:31:00Z",
"dns_challenge_record_sha256": "<sha256-of-the-txt-record-value-as-resolved>",
"publisher_account_id": "01J9XW...",
"app": "akaeon-registry",
"network": "arweave"
}
(Withdrawal records carry one additional field, withdraws_submission_id, pointing at the opt-out they supersede.)
Note the app and network fields. Same pattern as Stelais's existing buildCanonicalPayload — both are caller-supplied with no defaults. The brand-coupling rule makes this clean: the registry passes 'akaeon-registry' and 'arweave' deliberately; nothing else assumes a brand.
Step 4 — Sign the record
The registry signs with its own Ed25519 keypair. The signing flow is the same code Stelais runs in production — same createCanonicalMessageBuilder, same ed25519Sign, same key encryption shape.
The brand-coupling lives in the prefix the registry closes over:
// services/akaeon-registry/src/services/registrySigningService.ts
import { createCanonicalMessageBuilder } from '@akaeon/core-verification'
const optoutBuilder = createCanonicalMessageBuilder({
prefix: 'akaeon-registry:optout:v1',
})
// The core builder gained a variadic build(components: string[]) form
// alongside Stelais's legacy two-arg (userId, fileHashHex) signature —
// see "resolved questions" below for why core was extended rather than
// wrapped locally.
const message = optoutBuilder.build([submission_id, domain, policy])
// => "akaeon-registry:optout:v1|<submission_id>|<domain>|<policy>"
The signing key itself: a service keypair owned by the registry, not the publisher. (The publisher's identity is established by DNS in step 2; the signature here is the registry's attestation that we observed the verified submission.)
One nuance worth knowing: the per-opt-out signature is issued fresh each time a bundle is served (step 7), not minted once and stored. The artifact that fixes the record in time is the anchored canonical record itself — any verifier reconstructs the leaf hash and walks the inclusion proof to the batch root without needing to trust the per-response signature.
A sharp prospect will ask: "Why isn't the publisher signing this?"
Answer: they could, and a future version may add it as an optional second signature. For v1, the registry's signature is the load-bearing one because the registry's promise to the lab is "we verified this submission via DNS at this time." The publisher's signature would prove "the publisher intended this opt-out" — useful, not strictly necessary, and requires the publisher to manage a keypair the DNS-based flow deliberately avoids.
Step 5 — Batch into the next anchor
Stelais anchors one Arweave transaction per proof — the performAnchor path uploads a single canonical payload. That works for creator proofs because the volume is bounded by creator activity (each user has a daily quota).
The registry can't use the same one-tx-per-record model because publisher opt-outs arrive at much higher volume (every domain on the public internet is a potential submission). So the registry runs a batching layer in services/akaeon-registry/src/services/batchService.ts:
- Accumulate verified records as Postgres rows —
optout_submissionswithstatus = 'dns_verified'and nobatch_id, claimed underSELECT … FOR UPDATE. No Redis, no in-memory queue: the database is the queue, consistent with the codebase-wide no-Redis-in-v1 rule. - Hourly (
REGISTRY_BATCH_INTERVAL_MS, configurable), close the current batch. - Build a Merkle tree over the canonical record hashes (
src/lib/merkle.ts) — leafSHA-256(0x00 || canonical_record_bytes), interiorSHA-256(0x01 || left || right), RFC 6962-style. Odd-count levels promote the last node unchanged rather than duplicating it Bitcoin-style — a detail that matters again in step 8. - Build the batch canonical payload:
version,type: "optout_batch",batch_id,started_at,closed_at,merkle_root_sha256_hex,leaf_count,tree_construction: "rfc6962-style", the registry's signature,app,network. The leaf-hash list is not in the on-chain payload — only the root is. The leaves stay in the registry's database, addressable bysubmission_id, and are served to anyone who needs an inclusion proof. - Anchor the root through the same preflight discipline Stelais uses — kill switch, cost cap, daily/monthly budget checks — then the Turbo upload path. One Arweave transaction per batch, not per opt-out.
- Write back: each submission row gets its
canonical_record,leaf_hash,batch_id,leaf_index, andmerkle_proof(the sibling hashes a verifier needs). The batch row inoptout_batchesholds what's shared —merkle_root,leaf_count,arweave_tx_id,anchored_at— and submissions reach it through theirbatch_id.
A successful batch produces one Arweave transaction id that fixes the position of every opt-out in that batch in public, third-party-operated time. The publisher's submission is now provably anchored as of a public network timestamp — the property that makes it useful as evidence against a training cutoff.
Note on Merkle vs. the snapshot anchor. Stelais has a parallel structure for infringement snapshots using buildSnapshotCanonicalPayload. That path is per-snapshot, not Merkle-batched, because the volume is bounded by user-initiated capture. The registry's higher volume is the reason for Merkle; the substrate underneath is the same.
Step 6 — Publisher retrieves the acknowledgment
v1 is polling-based — there are no webhook callbacks. The publisher's automation checks status:
GET https://api.akaeon.com/v1/optouts/01J9XW...
Authorization: Bearer akr_<publisher-api-key>
While the submission is pending, the response repeats the DNS challenge so a publisher who lost the original 202 can recover it. Once the batch lands, the same endpoint returns the full audit bundle:
{
"ok": true,
"submission_id": "01J9XW...",
"status": "anchored",
"record_type": "domain_optout",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z",
"submitted_at": "2026-05-11T14:23:00Z",
"dns_verified_at": "2026-05-11T14:31:00Z",
"withdraws_submission_id": null,
"canonical_record": { "...": "the step-3 payload, verbatim" },
"leaf_hash": "9c31...",
"merkle_proof": ["<sibling-hash-level-0>", "<sibling-hash-level-1>", "..."],
"batch": {
"id": "01J9XX...",
"merkle_root": "f3a9...",
"leaf_count": 2814,
"arweave_tx_id": "ABC123...",
"anchored_at": "2026-05-11T15:00:00Z"
}
}
There is also an unauthenticated public endpoint, GET /v1/public/optouts/<submission_id>/verify, which re-runs the verification chain server-side for anyone holding a submission id.
The publisher's audit log now contains a row pointing at a public Arweave transaction id. They can verify the registry's claim against the public network without trusting Akaeon further.
Step 7 — Lab calls the lookup endpoint
This is the moment the registry's value is realized. A compliance engineer at examplelabs.ai is about to ingest content from example-publisher.com. Their pipeline issues:
GET https://api.akaeon.com/v1/lookup?domain=example-publisher.com
Authorization: Bearer <lab-api-key>
(Two optional query params: include_withdrawn=true returns superseded opt-outs with their withdrawal records, and as_of=<timestamp> answers the historical question "what was anchored as of this moment?" — the shape a compliance review actually needs.)
The registry responds with the full bundle the lab needs to put in their audit log:
{
"domain": "example-publisher.com",
"lookup_at": "2026-05-12T09:14:00Z",
"registry_version": "v1",
"optouts": [
{
"submission_id": "01J9XW...",
"status": "anchored",
"record_type": "domain_optout",
"canonical_record": {
"version": 1,
"type": "domain_optout",
"submission_id": "01J9XW...",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z",
"submitted_at": "2026-05-11T14:23:00Z",
"dns_verified_at": "2026-05-11T14:31:00Z",
"dns_challenge_record_sha256": "...",
"publisher_account_id": "01J9XW...",
"app": "akaeon-registry",
"network": "arweave"
},
"registry_signature": {
"canonical_message": "akaeon-registry:optout:v1|01J9XW...|example-publisher.com|no-training",
"signature": "<base64-ed25519>",
"public_key": "<base64-32-byte-raw>",
"signature_scheme": "ed25519",
"version": "v1"
},
"merkle_inclusion": {
"leaf_hash": "<sha256-of-canonical-record>",
"leaf_index": 142,
"tree_size": 2814,
"merkle_proof": [
"<sibling-hash-level-0>",
"<sibling-hash-level-1>",
"..."
],
"merkle_root": "f3a9...",
"tree_construction": "rfc6962-style"
},
"anchor": {
"arweave_tx_id": "ABC123...",
"arweave_url": "https://arweave.net/ABC123...",
"arweave_block_height": 1650123,
"anchored_at": "2026-05-11T15:00:00Z"
}
}
]
}
The lab's pipeline records this entire response, verbatim, in their audit trail before deciding whether to ingest the URL.
When a domain has no opt-outs, the response instead carries a signed no_optouts_attestation — with the lab's public lab_key_id watermarked inside the signed bytes — and, for credentials with the paid-tier entitlement, an anchored_non_inclusion proof against the daily state commitment. Pipelines checking many domains use POST /v1/lookup/bulk (up to 1,000 domains per call). Both are covered in the lab integration runbook.
Step 8 — Lab independently verifies the chain
The lab's audit verifier runs three independent checks, none of which call back to Akaeon:
// lab-side-verify.mjs — runs in the lab's environment, no Akaeon code
import crypto from 'node:crypto'
const bundle = /* one entry of the lookup response's `optouts` array */
// === Check 1: Ed25519 signature on the canonical message ================
//
// The exact same verification the Stelais public verify endpoint uses —
// same 32-byte raw Ed25519 pubkey wrapped in DER SPKI, same crypto.verify.
const SPKI_HEADER = Buffer.from('302a300506032b6570032100', 'hex')
const pubKey = crypto.createPublicKey({
key: Buffer.concat([
SPKI_HEADER,
Buffer.from(bundle.registry_signature.public_key, 'base64'),
]),
format: 'der',
type: 'spki',
})
const sigOk = crypto.verify(
null,
Buffer.from(bundle.registry_signature.canonical_message, 'utf8'),
pubKey,
Buffer.from(bundle.registry_signature.signature, 'base64'),
)
if (!sigOk) throw new Error('registry signature invalid')
// === Check 2: Merkle inclusion proof rolls up to the claimed root =======
//
// RFC 6962 §2.1.2. Note tree_size is a required input: the registry's
// trees promote the last node of an odd-count level unchanged, so the
// left/right decision at each step depends on where the right edge of
// the level sits — a naive "shift right each level" fold silently fails
// on non-power-of-2 batches.
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest()
const { leaf_hash, leaf_index, tree_size, merkle_proof, merkle_root } =
bundle.merkle_inclusion
let fn = leaf_index
let sn = tree_size - 1
let r = Buffer.from(leaf_hash, 'hex')
for (const siblingHex of merkle_proof) {
if (sn === 0) throw new Error('proof longer than path')
const p = Buffer.from(siblingHex, 'hex')
if ((fn & 1) === 1 || fn === sn) {
r = sha256(Buffer.concat([Buffer.from([0x01]), p, r]))
while ((fn & 1) === 0) { fn >>= 1; sn >>= 1 }
} else {
r = sha256(Buffer.concat([Buffer.from([0x01]), r, p]))
}
fn >>= 1
sn >>= 1
}
if (sn !== 0 || r.toString('hex') !== merkle_root) {
throw new Error('merkle proof does not reconstruct claimed root')
}
// === Check 3: The claimed root actually appears on Arweave ==============
const anchored = await fetch(bundle.anchor.arweave_url).then((res) => res.json())
if (anchored.merkle_root_sha256_hex !== merkle_root) {
throw new Error('arweave-anchored root does not match claimed root')
}
console.log('VERIFIED — opt-out is signed by the registry, included in a batch, and that batch is anchored on Arweave')
The three checks correspond to the three trust claims:
| Check | Trust claim |
|---|---|
| 1. Ed25519 signature on canonical message | "The registry actually attested to this opt-out." |
| 2. Merkle inclusion proof reconstructs the claimed root | "This specific opt-out was part of the claimed batch." |
| 3. Arweave transaction body contains the claimed root | "The batch root was actually anchored, at the public-network timestamp." |
The lab's audit log now contains:
- A timestamped record of the lookup.
- A bundle whose every claim is independently checkable.
- An Arweave transaction id that fixes the publisher's opt-out in public time.
Their compliance review later, against any challenge ("you trained on example-publisher.com after they opted out"), produces the Arweave tx and the inclusion proof. The burden of proof inverts: the challenger has to disprove a public-network timestamp.
The boundary between substrate and extension
| Step | Substrate (predates the registry, Stelais runs it in production) | Registry extension (shipped, in production) |
|---|---|---|
| 1. Publisher submits | — | POST /v1/optouts + DNS challenge issuance (routes/optouts.ts) |
| 2. DNS challenge verify | — | Multi-resolver polling worker (services/dnsVerifyService.ts) |
| 3. Canonical record | core-arweave payload pattern (app, network brand-neutral) | buildOptoutCanonicalPayload schema, added to core |
| 4. Sign | core-verification (createCanonicalMessageBuilder, ed25519Sign, key encryption) | Registry's keypair + brand prefix (akaeon-registry:optout:v1); core builder extended to variadic components |
| 5. Batch + anchor | Anchor preflight (kill switch, cost caps) + Turbo upload + spend logging | Merkle tree builder (lib/merkle.ts); hourly per-batch anchor cadence (services/batchService.ts) |
| 6. Publisher ack | — | Status polling endpoints + public verify route |
| 7. Lab lookup | — | GET /v1/lookup + bundle assembly, attestations, bulk endpoint |
| 8. Lab verifies | The Ed25519 verify path is identical to the existing Stelais public verify endpoint (same library, same RFC 8032) | The RFC 6962 inclusion check; standard library, no special tooling |
What the table makes clear: the cryptographic spine — signing, canonical-payload anchoring, brand-neutral core packages, public Arweave trust root — predates the registry and runs for Stelais every day. The registry's code is the API surface (steps 1, 2, 6, 7) and the Merkle batching glue (step 5) — built on top of the substrate, now in production alongside it.
The open architectural questions, as they were resolved
The v1 design surfaced four decisions before implementation. All four have been made; they're kept here because the reasoning explains the shape of the shipped code.
1. Canonical message arity — resolved: extend core. createCanonicalMessageBuilder gained a variadic build(components: string[]) alongside the legacy two-arg (userId, fileHashHex) form Stelais uses. All of the registry's message builders (opt-out, batch, attestations, state roots) call the array form against the shared core primitive — one message builder, two calling conventions, no fork.
2. Merkle batching primitive location — resolved: registry-local. The tree builder lives at services/akaeon-registry/src/lib/merkle.ts, not in core-arweave. Core stays brand-neutral and batching-free; the primitive gets hoisted only if a second batching consumer emerges.
3. Publisher keypair — still deferred. v1 requires nothing from the publisher beyond the DNS challenge. Publisher-signed submissions (a two-signature record: publisher's intent + registry's verification) remain a v2 candidate, pending signal that labs' compliance posture would actually change because of it.
4. Anchor the root or a full leaf manifest — resolved: root only, then superseded. v1 anchors only the Merkle root; leaves stay in Postgres. The stronger version of the underlying question — can a third party get anchored-grade answers without the registry as mediator? — was later answered by the daily anchored state commitment, which serves anchored membership and non-membership proofs per domain without ever publishing an enumerable leaf list. The registry's publication policy now treats an enumerable list as something that should never be anchored.
Companion documents
- Technical specification — the normative cryptographic spec the steps above reference.
- Lab integration runbook — the engineering reference for step 7 and step 8 from the lab's side, including the bulk endpoint and attestations.
- Standalone verifier — the copy-paste-ready code for step 8.
- Architecture — the single picture that makes the relationship between Stelais, the registry, the core packages, and Arweave visible at a glance.