Publisher integration runbook
Step-by-step guide for a publisher registering a domain opt-out. One ops engineer, half a day. DNS-based proof of authority, no new infrastructure.
Audience: Publisher-side technical lead deciding whether to onboard, followed by the ops or platform engineer who actually publishes the DNS record and wires up the submission.
Time to integrate (estimate): Half a day for the minimal path (one POST, one TXT record, one polled status check). Add half a day for a webhook-based receipt and audit-log automation.
Prerequisites: You control the DNS zone for the domain you intend to register. You can submit one HTTPS request and add one TXT record. That is the whole prerequisite surface — there is no SDK to install, no keypair to manage, no library to vendor.
Companion documents: Technical specification for the cryptographic model · Lab integration runbook for what labs do with your record · Concrete example for one submission walked end-to-end.
1. The 30-second integration story
You make one authenticated POST telling the registry which domain you control and what policy you want to register. The registry returns a DNS challenge — a TXT record you publish at _akaeon-registry-challenge.<your-domain>. Once the registry observes the record across multiple resolvers, it signs your opt-out, batches it with others into a Merkle tree, and anchors the root on Arweave. You get back a public Arweave transaction id and a verify URL you can cite in legal correspondence, compliance filings, or licensing negotiations.
The whole interaction is three HTTP requests on your side (submit, publish DNS, poll for status) plus one DNS change. There is no ongoing relationship after that — your opt-out is a public, third-party-timestamped artifact that outlives the registry.
2. Authentication and credentialing
2.1 Getting a publisher account
v1 is manual approval; self-service is a v1.1 target. To request an account, email publishers@akaeon.com with:
- The domain (or list of domains) you intend to register opt-outs for.
- The legal entity that owns the domain, if not obvious from WHOIS.
- A contact address for notifications about your submissions (we recommend a role-based mailbox, not a personal one — opt-outs are long-lived, individuals rotate).
Approval is intentionally lightweight — we are confirming you are who you say you are, not licensing anything. The DNS challenge does the load-bearing authority check; account approval is a sanity layer on top.
2.2 Using the token
Once approved, you receive a 256-bit bearer token transmitted once over an encrypted channel and never again — the registry stores only an Argon2id hash. If you lose it, it must be reissued.
Authorization: Bearer akr_pub_live_<32_random_alphanumeric_chars>
Tokens are scoped to one of two environments:
akr_pub_test_...— staging environment for dry-runs against a non-production Arweave gateway. Useful for verifying your DNS publishing pipeline before committing a production record.akr_pub_live_...— production environment.
2.3 Token rotation
Rotate via POST /v1/keys/rotate (publisher admin only, returns the new token in the response body). The old token continues to work for 30 days after the new one is issued. Treat the token as you would any production secret — store it in your secret manager, do not paste it into logs, do not check it into source control.
3. The submission endpoint
3.1 Request
POST https://api.akaeon-registry.com/v1/optouts
Authorization: Bearer akr_pub_live_...
Content-Type: application/json
{
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z",
"webhook_url": "https://ops.example-publisher.com/akaeon/notify"
}
| Field | Required | Description |
|---|---|---|
| domain | Yes | Lowercase, punycode-encoded for IDNs, no trailing dot, no scheme, no port. |
| policy | Yes | One of no-training, no-training-commercial, no-training-generative. See §3.4 for the policy vocabulary. |
| scope | Yes | domain (covers the apex and all subdomains) or subdomain (covers only the exact host). Path-scoped opt-outs are not v1. |
| effective_from | No | RFC 3339 timestamp. The publisher's declared effective date. The audit-credible date is the Arweave block timestamp, not this field — but this field is preserved in the canonical record so a reader can see what you intended. |
| webhook_url | No | HTTPS URL the registry will POST to once the opt-out is anchored. Without this, poll the status endpoint instead. |
3.2 Response
202 Accepted:
{
"status": "pending_dns_verification",
"submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"challenge": {
"record_name": "_akaeon-registry-challenge.example-publisher.com",
"record_type": "TXT",
"record_value": "akaeon-registry-v1=<32-byte-nonce-b64>",
"expires_at": "2026-05-12T14:23:00Z"
},
"status_url": "https://api.akaeon-registry.com/v1/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3"
}
Store the submission_id and record_value — both are needed for the next two steps.
3.3 Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_DOMAIN | Domain failed validation (malformed, IP address, IDN not punycode-encoded). |
| 400 | INVALID_POLICY | Policy not in the supported vocabulary (see §3.4). |
| 401 | INVALID_TOKEN | Bearer token missing, malformed, or revoked. |
| 409 | ALREADY_PENDING | A submission for this (domain, scope, policy) is already pending DNS verification. The response includes the existing submission_id. |
| 409 | ALREADY_ANCHORED | An active opt-out for this (domain, scope, policy) already exists. To replace it, withdraw the existing record first (see §6.2). |
| 429 | RATE_LIMITED | Per-account submission limit hit. |
3.4 The policy vocabulary
v1 supports a small, deliberate set of policies. The vocabulary is fixed because labs need to interpret the value programmatically — a free-text policy field would shift the interpretation cost onto the lab and produce ambiguity exactly where the registry should be unambiguous.
| Policy | Meaning |
|---|---|
| no-training | Do not use content from this domain to train any machine-learning model. |
| no-training-commercial | Do not use content from this domain to train models used in commercial products. Research use permitted. |
| no-training-generative | Do not use content from this domain to train generative models. Discriminative / retrieval models permitted. |
If your intent doesn't fit any of these, contact us before submitting. We will not invent a one-off policy value, but the vocabulary is versioned and we expect it to grow.
4. Publishing the DNS challenge
4.1 The record
At your DNS provider, add a TXT record:
_akaeon-registry-challenge.example-publisher.com. 300 IN TXT "akaeon-registry-v1=<the-nonce-the-registry-returned>"
A few specifics that trip people up:
- TTL. 300 seconds is plenty. The registry polls; we don't need a long TTL. A shorter TTL also lets you remove the record sooner (see §4.4).
- Wrapping. The record value is a single short string. If your provider's UI wraps long TXT values in quotes, that's fine; quoting is part of the TXT wire format.
- Subdomain match. If you submitted with
scope: subdomainfornews.example-publisher.com, the challenge record is_akaeon-registry-challenge.news.example-publisher.com, not_akaeon-registry-challenge.example-publisher.com. The challenge always lives at the exact host being claimed. - No CNAME. The challenge record must be a TXT directly at the challenge name, not a CNAME pointing elsewhere. We follow CNAME chains for the apex resolution but not at the challenge name itself.
4.2 How the registry verifies
A worker polls pending submissions on a short cadence (initial design: every 60 seconds). For each pending row:
- Resolves the challenge TXT record against multiple independent resolvers (Cloudflare
1.1.1.1, Google8.8.8.8, Quad99.9.9.9) to defeat single-resolver poisoning. - Confirms every resolver returns a record matching the expected nonce.
- Records the DNSSEC chain (where present) into the submission row.
- On all-resolvers match: marks the submission
dns_verifiedand enqueues it for anchoring. - On partial-match or no-match: leaves the submission pending. On the 24h TTL: marks it
expiredand deletes the challenge.
The multi-resolver requirement is deliberately strict. A successful verification is the registry's attestation that every public resolver we checked agreed your record was published at this time; partial agreement would weaken that claim.
4.3 If verification doesn't fire
Common causes, roughly ordered by frequency:
- Propagation delay. Most providers propagate in under a minute, but some take longer. Check the record yourself with
dig +short TXT _akaeon-registry-challenge.<your-domain> @1.1.1.1and@8.8.8.8and@9.9.9.9. If any returns nothing, wait and re-check. - Wrong record name. Double-check the leading underscore and that you didn't accidentally use your apex (
example-publisher.com) instead of the challenge name. Some DNS UIs strip the_if you paste carelessly. - Wrong record value. The full value includes the
akaeon-registry-v1=prefix. Without the prefix the registry won't recognize the record even if the nonce is correct. - Provider quirks. A handful of providers split long TXT values across multiple chunks; our resolver follows the standard reassembly rule, but if you see partial-match across resolvers it's worth checking. Cloudflare and Route 53 are well-behaved; some legacy registrars are not.
If none of the above explains it, hit GET /v1/optouts/<submission_id> (see §5.1) — the status response includes the registry's last poll attempt and its observed result per resolver.
4.4 Removing the challenge record after verification
Once your submission moves to anchored (§5), the challenge record has served its purpose. You can leave it in place indefinitely or remove it; the registry does not re-check it. Most publishers remove it once anchored to keep their DNS clean.
The challenge record being present is not what makes the opt-out durable. The canonical record on Arweave is. Removing the challenge after the fact does not invalidate anything.
5. Confirming the anchor
5.1 Polling
GET https://api.akaeon-registry.com/v1/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3
Authorization: Bearer akr_pub_live_...
Returns one of four statuses:
| Status | Meaning |
|---|---|
| pending_dns_verification | Submission received; DNS challenge issued; not yet observed across all resolvers. |
| dns_verified | DNS observed across all resolvers; queued for the next anchoring batch. |
| anchored | Included in a Merkle batch and that batch's root is on Arweave. The full bundle is returned. |
| withdrawn | Previously anchored; the publisher has subsequently withdrawn the opt-out. The original record remains visible via the public verify endpoint with withdrawn_at set. |
The anchored response is the one that matters. Its shape:
{
"submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"status": "anchored",
"canonical_record": { /* the signed record */ },
"registry_signature": { /* Ed25519 signature on the canonical record */ },
"merkle_inclusion": {
"leaf_hash": "a3f5d8b9c2e1...",
"leaf_index": 142,
"tree_size": 2814,
"merkle_proof": [ "...", "...", "..." ],
"merkle_root": "f3a9d8b4c2e1...",
"tree_construction": "rfc6962-style"
},
"anchor": {
"arweave_tx_id": "ABC123_arweave_tx_id_here_KXYZ",
"arweave_url": "https://arweave.net/ABC123_arweave_tx_id_here_KXYZ",
"arweave_block_height": 1234567,
"arweave_block_timestamp": "2026-05-11T15:00:14Z",
"anchored_at": "2026-05-11T15:00:00Z"
},
"verify_url": "https://api.akaeon-registry.com/v1/public/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3/verify"
}
Field by field, what each gives you:
canonical_record— the exact bytes the registry signed and hashed into the Merkle tree. This is the artifact a third party will verify against.registry_signature— the Ed25519 signature attesting that the registry observed your verified submission at the moment of signing. Verifiable in 10 lines of standard-library code (see the verifier).merkle_inclusion— the proof that your record is part of the batch whose root was anchored. The verifier reconstructs the root from your leaf using the sibling hashes; the result must equalmerkle_root.anchor— the Arweave transaction id, URL, block height, and block timestamp. The block timestamp is the audit-credible time — the public network's clock, not the registry's.verify_url— an unauthenticated endpoint anyone can hit to re-fetch the bundle. Cite this URL in legal correspondence; the recipient does not need an Akaeon account to verify.
5.2 Webhook receipt
If you set webhook_url at submission, the registry POSTs the same bundle to your endpoint when the anchor lands:
POST https://ops.example-publisher.com/akaeon/notify
Content-Type: application/json
X-Akaeon-Signature: sha256=<hex-hmac-of-body>
{ /* same bundle as the GET response above */ }
The webhook body is HMAC-signed with a per-publisher webhook secret distinct from the API key. Verify it before trusting the body. Reference Node verification:
import crypto from 'node:crypto'
function verifyWebhook(rawBody, signatureHeader, webhookSecret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader),
)
}
The webhook secret is set on the publisher account, not per-submission. Rotate it via POST /v1/keys/rotate?kind=webhook.
5.3 Anchoring latency
The registry anchors batches on a fixed cadence — initial design hourly, configurable. Worst-case latency from dns_verified to anchored is the cadence interval plus the Arweave block confirmation time (median ~2 minutes). Best-case is whatever is left in the current interval. Plan around the worst case.
If you need a faster anchor for a specific submission (legal deadline, regulatory filing), email engineering@akaeon.com and we can force-close the current batch. This is not a self-serve operation in v1; the per-batch anchor cost is fixed and we batch deliberately to keep it amortized.
6. Verifying your own anchor
The anchored bundle is independently checkable. You don't have to take the registry's word that your record landed; you can prove it. Do this at least once after your first successful submission so you have hands-on confidence in the chain.
6.1 The three checks
Same shape as the lab-side verifier, run from your side:
// publisher-self-verify.mjs — confirm your own opt-out is real.
import crypto from 'node:crypto'
const SPKI_HEADER = Buffer.from('302a300506032b6570032100', 'hex')
const sha256 = (b) => crypto.createHash('sha256').update(b).digest()
const bundle = await fetch(
'https://api.akaeon-registry.com/v1/public/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3/verify'
).then(r => r.json())
// 1. Registry signature on the canonical message
const pk = 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'),
pk,
Buffer.from(bundle.registry_signature.signature, 'base64'),
)
// 2. Merkle inclusion proof reconstructs the claimed root
let c = Buffer.from(bundle.merkle_inclusion.leaf_hash, 'hex')
let i = bundle.merkle_inclusion.leaf_index
for (const s of bundle.merkle_inclusion.merkle_proof) {
const [l, ri] = (i & 1) === 0 ? [c, Buffer.from(s, 'hex')] : [Buffer.from(s, 'hex'), c]
c = sha256(Buffer.concat([Buffer.from([0x01]), l, ri]))
i >>= 1
}
const merkleOk = c.toString('hex') === bundle.merkle_inclusion.merkle_root
// 3. The claimed root actually appears on Arweave
const arBody = await fetch(bundle.anchor.arweave_url).then(r => r.json())
const arOk = arBody.merkle_root_sha256_hex === bundle.merkle_inclusion.merkle_root
console.log({ sigOk, merkleOk, arOk })
If all three pass, your opt-out is signed by the registry, part of a batch whose root is on Arweave, and the public-network timestamp on that anchor is the audit-credible date. That is the artifact you can cite.
6.2 Withdrawing an opt-out
Opt-outs are not deleted — the historical record is part of the audit value. Withdrawal is additive: a new record stating "as of withdrawn_at, the prior opt-out no longer applies."
POST https://api.akaeon-registry.com/v1/optouts/01J9XW.../withdraw
Authorization: Bearer akr_pub_live_...
Content-Type: application/json
{
"reason": "policy-change"
}
The withdrawal goes through the same DNS challenge, signing, and anchoring flow as the original submission. Both records remain visible. A lab querying GET /v1/lookup will see the withdrawal in the response (with include_withdrawn=false, the default, the opt-out is omitted from optouts[]; with include_withdrawn=true, both records appear). The history is the audit trail.
7. The public verification surface
These endpoints require no authentication. Use them when you want to share a verifiable link, when you want to verify someone else's record, or when you want offline verification that doesn't depend on your API key.
| Endpoint | Returns |
|---|---|
| GET /v1/public/optouts/:submission_id/verify | Full bundle, unauthenticated. The URL you cite in legal correspondence. |
| GET /v1/public/batches/:batch_id | Batch metadata, Merkle root, leaf count, Arweave tx id. Used to cross-check that a root matches the registry's view. |
| GET /v1/public/registry-key | Current and historical Ed25519 public keys with validity windows. |
| GET /v1/public/registry-key/history | Arweave transaction ids of the key catalog records — so a verifier can recover the historical keys from Arweave directly without trusting the registry. |
The registry's commitment is that these endpoints remain available for as long as the registry operates, and that the data they return is mirrored to at least one independent secondary domain.
For the strictest posture: every claim about your opt-out can be verified against Arweave directly, treating the registry's public verify endpoint as a convenience mirror. Your anchored opt-out outlives the registry.
8. Rate limits and operational concerns
8.1 Rate limits (initial proposal; final at v1 launch)
| Endpoint | Per-account limit | Notes |
|---|---|---|
| POST /v1/optouts | 10/min, 100/hour, 1,000/day | Most publishers submit a handful of records, lifetime. This is sized for bulk-onboarding scenarios (a media group registering many domains at once). |
| GET /v1/optouts/:id | 60/min | Status polling. Use the webhook if you'd otherwise poll faster than every 10 seconds. |
| POST /v1/optouts/:id/withdraw | 10/min, 100/day | Same shape as submission. |
| GET /v1/public/* | 50/s per IP, no daily cap | Unauthenticated; protected by per-IP limit. |
For higher limits (bulk-onboarding more than ~1,000 domains in a day), contact engineering@akaeon.com ahead of time and we will provision accordingly.
8.2 Degraded modes
If the registry's anchoring path is degraded (Arweave gateway slow, ArDrive Turbo credit balance insufficient, internal safety control triggered), new submissions still accept — they sit in dns_verified until the anchoring resumes. Your submission_id and DNS challenge remain valid; you do not need to re-submit.
If anchoring is degraded for more than a configurable window, the registry surfaces a banner on the public verify pages and posts an incident to a status page. Subscribe to the status page if your operational tempo cares about anchoring latency.
8.3 Retention and durability
The publisher's submission and the registry's signature are stored in Postgres indefinitely. The canonical record's Merkle root is on Arweave, which is the durability guarantee — Postgres can be lost and rebuilt from the on-chain records.
The registry's commitment to keep the lookup and verify endpoints available is contractual, not cryptographic. The Arweave-anchored record's verifiability is cryptographic and does not depend on the registry continuing to operate.
9. Integration checklist
For a publisher technical lead estimating effort:
9.1 First half-day — minimum viable submission
- Email
publishers@akaeon.comand obtain anakr_pub_test_...token. - Decide which domain(s), scope, and policy you want to register. If you have more than one, do the first one by hand before scripting.
- Submit one opt-out via
POST /v1/optoutsagainst the staging environment. Confirm you receive asubmission_idand a DNS challenge. - Publish the TXT record at your DNS provider.
- Poll
GET /v1/optouts/<id>until the status flips toanchored(worst case: cadence interval + ~2 minutes). - Run the three-check verifier (§6.1) on the bundle. Confirm all three pass.
Estimated effort: half a day for a competent ops engineer.
9.2 Second half-day — production hardening
- Switch to the
akr_pub_live_...token. Store it in your secret manager. - Configure a
webhook_urlfor receipt notifications. Verify the HMAC signature in your handler. - Record the full anchored bundle in your own audit log, keyed by
submission_id. The audit log is your defense if you ever need to point at the record under challenge. - Document where the bundle and the
verify_urlare stored, so legal or compliance can find them without paging you. - Smoke-test withdrawal against staging if you anticipate ever needing to withdraw.
Estimated effort: half a day.
9.3 If you operate at scale
- If you intend to register more than ~100 domains, contact us first so we can raise your limits and walk through a bulk-onboarding flow.
- If you intend to register opt-outs across multiple subdomains as policy changes, automate the submission against a config-managed list of
(domain, scope, policy)triples; treat each row as a desired-state record and reconcile. - If your compliance posture needs documentary proof that the registry itself is operating correctly, run the verifier against a known-good submission of yours weekly and alert on failures.
10. Frequently asked publisher questions
Q: Why do I need to publish a DNS record? Can't I just authenticate with the API key?
The DNS challenge proves you control the domain at the moment of submission. The API key proves you control the account. Without the DNS challenge, an attacker who stole your API key could register opt-outs for unrelated domains. With the DNS challenge, an attacker without DNS control can't register anything — even with the API key.
Q: Is the opt-out public?
Yes. Visibility is the design. The registry's value to you is that any third party can verify your opt-out without going through us; that property requires the record to be public. If you need a non-public opt-out, the registry is not the right tool — talk to labs directly under NDA.
Q: What about subdomain wildcards?
scope: domain covers the apex and all subdomains. scope: subdomain covers only the exact host. Wildcard-pattern matching (e.g. *.example-publisher.com but not blog.example-publisher.com) is not v1. If you have a use case for it, tell us.
Q: Does the opt-out apply retroactively?
Legally, that depends on jurisdiction — we don't give legal advice. Cryptographically, the effective_from field captures what you intended; the Arweave block timestamp captures when the public network observed your intent. A lab can see both. What they do with that information is their compliance decision; we're not the enforcer.
Q: A lab trained on my content anyway. What does Akaeon do?
Nothing directly. The registry produces evidence; it doesn't litigate. The point of the cryptographic chain is that your record is defensible enough that a court or arbitrator will treat it as load-bearing. The enforcement step is yours, with your counsel.
Q: Can I register an opt-out for a domain I don't own?
The DNS challenge prevents this. You can submit, but the DNS verification will never succeed and the submission expires after 24h.
Q: What if Arweave goes away?
The substrate is designed to migrate. Existing records remain verifiable against existing Arweave transactions for as long as Arweave or any mirror of it exists. Future submissions could be anchored to a different content-addressable substrate — your existing record is unaffected.
Q: How do I report a bug or request a feature?
engineering@akaeon.com for production issues. feedback@akaeon.com for feature requests. The registry maintains a public changelog documenting every API change.