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 audit-log automation. (A click-through publisher dashboard that skips the API entirely is in development.)
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.
If you'd rather not write any code: a publisher dashboard that wraps this entire flow in the browser — register or sign in with your API key, submit the opt-out, see the DNS challenge as a copyable card, watch status update automatically, and view the anchored proof (with the Arweave link) once it lands — is currently in development. Until it ships, the API below is the integration path; once it does, these sections remain the reference for anyone automating the flow or auditing what the dashboard does on their behalf.
2. Authentication and credentialing
2.1 Getting a publisher account
Publisher accounts are currently provisioned by request: email publishers@akaeon.com with your organization name and the domain(s) you intend to register, and we'll issue your account and API key. Self-serve registration (and the dashboard flow that wraps it) is in development; everything after credentialing — submission, DNS verification, status, withdrawal — is already fully self-serve through the API below.
2.2 Using the token
Your API key is issued exactly once — the registry stores only a hash, and there is no way to recover a lost key. If you lose it, request a new one.
Authorization: Bearer akr_<base64url-encoded-random-bytes>
There is one token namespace in v1: akr_.... There is no separate test/live token distinction and no akr_pub_ prefix — every registered key is a live, production key against the same registry.
2.3 Token rotation
There is no key-rotation endpoint in v1. If you need a new key, request another via publishers@akaeon.com; the previous key continues to work (issuing a new key does not revoke prior ones). 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_...
Content-Type: application/json
{
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00Z"
}
| Field | Required | Description |
|---|---|---|
domain | Yes | Lowercase, no trailing dot, no scheme, no port. |
policy | Yes | no-training is the only supported value in v1. See §3.4. |
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 | Yes | ISO 8601 UTC 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. |
There is no webhook_url field in v1. Poll GET /v1/optouts/:submission_id (§5.1) for status.
3.2 Response
202 Accepted:
{
"ok": true,
"submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"status": "pending_dns_verification",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00.000Z",
"submitted_at": "2026-05-11T00:00:01.000Z",
"dns_challenge": {
"host": "_akaeon-registry-challenge.example-publisher.com",
"txt_record": "akaeon-registry-v1=<32-byte-nonce-b64>",
"expires_at": "2026-05-12T00:00:01.000Z",
"ttl_hours": 24
},
"instructions": "Publish a DNS TXT record at \"_akaeon-registry-challenge.example-publisher.com\" with the exact value above, then poll GET /v1/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3 for status transitions. The challenge expires in 24 hours."
}
Store the submission_id and dns_challenge.txt_record — both are needed for the next two steps.
3.3 Error responses
The registry returns { "ok": false, "error": "<code>", "message": "..." } on every error. Codes relevant to submission:
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_body | One of domain, policy, scope, effective_from is missing or not a string. |
| 400 | invalid_domain_length / invalid_domain_format | Domain failed length or format validation. |
| 400 | invalid_policy | policy is not no-training. |
| 400 | invalid_scope | scope is not domain or subdomain. |
| 400 | invalid_effective_from | effective_from did not parse as a timestamp. |
| 401 | missing_authorization / invalid_credentials | Bearer token missing, malformed, or not recognized. |
| 403 | publisher_suspended | The account is suspended. |
| 409 | active_optout_exists | An active opt-out already exists for this domain (in pending_dns_verification, dns_verified, pending_anchor, or anchored status). The response includes existing_submission_id and existing_status. Withdraw the existing one before submitting a new policy for the same domain. |
| 429 | rate_limit_exceeded | Per-account submission limit hit. Response includes window, limit, count, retry_after_seconds. |
3.4 The policy vocabulary
v1 supports exactly one policy value:
| Policy | Meaning |
|---|---|
no-training | Do not use content from this domain to train any machine-learning model. |
There is no no-training-commercial or no-training-generative distinction in v1 — that granularity has been discussed but is not implemented. If your intent doesn't fit no-training, 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) — while the submission is pending_dns_verification, the response echoes back the dns_challenge block (host, TXT value, expiry) so you can double-check exactly what the registry expects.
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_...
Returns one of six 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. |
pending_anchor | Verified and waiting for the next batch-anchoring run. |
anchored | Included in a Merkle batch and that batch's root is on Arweave. The full bundle is returned. |
failed | The submission did not complete verification or anchoring. |
expired | The DNS challenge window (24h) elapsed without verification. |
There is no withdrawn status on the original submission. A withdrawal is a separate submission row (record_type: "domain_optout_withdrawal") that references the original via withdraws_submission_id — the original row is never modified. See §6.2.
The anchored response is the one that matters. Its shape:
{
"ok": true,
"submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"status": "anchored",
"record_type": "domain_optout",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00.000Z",
"submitted_at": "2026-05-11T00:00:01.000Z",
"dns_verified_at": "2026-05-11T00:02:14.000Z",
"withdraws_submission_id": null,
"canonical_record": { /* the signed record */ },
"leaf_hash": "a3f5d8b9c2e1...",
"merkle_proof": [ "...", "...", "..." ],
"batch": {
"id": "01J9XX...",
"merkle_root": "f3a9d8b4c2e1...",
"leaf_count": 2814,
"arweave_tx_id": "ABC123_arweave_tx_id_here_KXYZ",
"anchored_at": "2026-05-11T15:00:00.000Z"
}
}
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.leaf_hash/merkle_proof— 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 equalbatch.merkle_root.batch— the Merkle root, leaf count, and Arweave transaction id for the batch your submission was anchored in. Build the Arweave URL ashttps://arweave.net/<batch.arweave_tx_id>;batch.anchored_atis the registry's own clock, not the audit-credible time (that's the Arweave block timestamp, recovered by fetching the transaction).- For an unauthenticated, shareable version of this bundle, use the public verify endpoint (§7):
GET /v1/public/optouts/:submission_id/verify. Cite that URL in legal correspondence; the recipient does not need an Akaeon account to verify.
5.2 No webhooks in v1
There is no webhook mechanism in v1 — the registry does not push notifications to a publisher-controlled endpoint. Poll GET /v1/optouts/:submission_id, or use GET /v1/optouts (§5.4) to list all of your submissions at once.
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.
5.4 Listing your submissions
GET https://api.akaeon-registry.com/v1/optouts?limit=50
Authorization: Bearer akr_...
Returns every submission you've made — opt-outs and withdrawals both — newest first:
{
"ok": true,
"count": 2,
"submissions": [
{
"submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"status": "anchored",
"record_type": "domain_optout",
"domain": "example-publisher.com",
"policy": "no-training",
"scope": "domain",
"effective_from": "2026-05-11T00:00:00.000Z",
"submitted_at": "2026-05-11T00:00:01.000Z",
"dns_verified_at": "2026-05-11T00:02:14.000Z",
"dns_challenge_expires_at": null,
"withdraws_submission_id": null,
"batch_id": "01J9XX..."
}
],
"next_cursor": null
}
Query parameters:
| Param | Required | Description |
|---|---|---|
limit | No | Integer, 1–200. Default 50. |
cursor | No | A submission_id from a previous page's next_cursor. Pages strictly older than the cursor (exclusive). |
This is a summary view — dns_challenge_expires_at is only non-null while a row is pending_dns_verification, and the full audit bundle (canonical_record, merkle_proof, batch) is not included here; fetch GET /v1/optouts/:submission_id for that.
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 submission row (record_type: "domain_optout_withdrawal") that references the original via withdraws_submission_id. The original row is never modified.
DELETE https://api.akaeon-registry.com/v1/optouts/01J9XW8ZQ7K3F0HXFZBV4QXRH3
Authorization: Bearer akr_...
202 Accepted:
{
"ok": true,
"withdrawal_submission_id": "01J9XY...",
"original_submission_id": "01J9XW8ZQ7K3F0HXFZBV4QXRH3",
"status": "dns_verified",
"message": "Withdrawal accepted. It will be batched and anchored on the next scheduler tick. The original opt-out record is preserved on Arweave; the withdrawal is anchored separately."
}
There is no request body — no reason field. Withdrawal is only available for a submission currently in anchored status (attempting to withdraw a pending or already-withdrawn submission returns 409), and it does not require a fresh DNS challenge: your API key plus the reference to a previously-verified submission is treated as sufficient proof. The withdrawal itself still goes through batching and anchoring — it starts at dns_verified and reaches anchored on the next batch tick, same as any other submission. Both the original and the withdrawal remain permanently visible on-chain; 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
| Endpoint | Limit | Notes |
|---|---|---|
POST /v1/optouts | 10/hour, 100/day (default tier); 100/hour, 1,000/day (partner tier) | Per publisher account. Most publishers submit a handful of records, lifetime — these ceilings are sized for bulk-onboarding scenarios. |
GET /v1/optouts | 120/hour, 1,000/day | Per publisher account. Higher than the submission ceiling to accommodate status polling. |
GET /v1/optouts/:id and DELETE /v1/optouts/:id are not currently rate-limited beyond requiring a valid API key. For higher submission limits (bulk-onboarding many domains at once), contact engineering@akaeon.com ahead of time — a publisher's rate_limit_tier can be raised without a code change.
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
- Request a publisher account via
publishers@akaeon.comand save the issuedakr_...key — it is shown exactly once. - 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/optouts. Confirm you receive asubmission_idand adns_challengeblock. - Publish the TXT record at your DNS provider.
- Poll
GET /v1/optouts/<id>until the status flips toanchored(worst case: batch cadence + ~2 minutes for Arweave confirmation). - Fetch the bundle from the public verify endpoint (§7) and run the three-check verifier (§6.1) on it. Confirm all three pass.
Estimated effort: half a day for a competent ops engineer.
9.2 Second half-day — production hardening
- Store the
akr_...key in your secret manager, not in source control or logs. - Record the full anchored bundle (from the public verify endpoint, §7) 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 URL are stored, so legal or compliance can find them without paging you.
- If you anticipate ever needing to withdraw, read §6.2 (
DELETE /v1/optouts/:id) so the flow isn't a surprise later.
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.